Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ on:
push:

env:
go-version: '1.20'
go-version: '1.25'
node-version: 8
artifacts-retention-days: 5

Expand Down
2 changes: 2 additions & 0 deletions deploy/ansible/roles/icarodb/files/icaro.sql
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ CREATE TABLE `accounts` (
`id` serial,
`creator_id` bigint unsigned NOT NULL,
`uuid` varchar(200),
`logto_org_id` varchar(64) DEFAULT NULL,
`type` varchar(200) NOT NULL,
`name` varchar(200) NOT NULL,
`username` varchar (200) NOT NULL,
Expand All @@ -17,6 +18,7 @@ CREATE TABLE `accounts` (
`created` datetime,
UNIQUE KEY (`username`),
UNIQUE KEY (`uuid`),
UNIQUE KEY (`logto_org_id`),
KEY(`username`),
KEY(`uuid`),
PRIMARY KEY(`id`)
Expand Down
19 changes: 18 additions & 1 deletion deploy/ansible/roles/sun/templates/sun-api-conf.j2
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"token_expires_days": 1,
"cors": {
"origins": ["https://{{ icaro.hostname }}"],
"headers": ["Token", "Content-Type"],
"headers": ["Token", "Content-Type", "Authorization"],
"methods": ["GET", "PUT", "POST", "DELETE"]
},
"captive_portal": {
Expand Down Expand Up @@ -103,5 +103,22 @@ icaro.sun_api.email_smtp_password is defined %}
"verb": "POST",
"endpoint": "/api/users"
}]
},
"oidc": {
"issuer": "{{ icaro.sun_api.oidc_issuer | default('') }}",
"client_id": "{{ icaro.sun_api.oidc_client_id | default('') }}",
"client_secret": "{{ icaro.sun_api.oidc_client_secret | default('') }}",
"redirect_uri": "{{ icaro.sun_api.oidc_redirect_uri | default('http://localhost:8080/api/auth/oidc/callback') }}",
"frontend_url": "{{ icaro.sun_api.oidc_frontend_url | default('http://localhost:8081') }}",
"scopes": {{ icaro.sun_api.oidc_scopes | default(['openid', 'profile', 'email', 'roles', 'urn:logto:scope:organizations', 'urn:logto:scope:organization_roles']) | to_json }},
"role_mapping": {{ icaro.sun_api.oidc_role_mapping | default(['super admin:admin']) | to_json }},
"org_admin_role": "{{ icaro.sun_api.oidc_org_admin_role | default('Admin') }}",
"org_reseller_role": "{{ icaro.sun_api.oidc_org_reseller_role | default('Reseller') }}",
"my_api_url": "{{ icaro.sun_api.oidc_my_api_url | default('') }}",
"my_api_key": "{{ icaro.sun_api.oidc_my_api_key | default('') }}",
"default_subscription_plan_id": {{ icaro.sun_api.oidc_default_subscription_plan_id | default(4) }},
"userinfo_audience": "{{ icaro.sun_api.oidc_userinfo_audience | default('') }}",
"userinfo_origins": {{ icaro.sun_api.oidc_userinfo_origins | default(['https://my.nethesis.it', 'https://beta.my.nethesis.it']) | to_json }},
"show_login_button": {{ icaro.sun_api.oidc_show_login_button | default(true) | lower }}
}
}
32 changes: 29 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,17 +1,43 @@
module github.com/nethesis/icaro

go 1.15
go 1.25.0

require (
github.com/appleboy/gofight/v2 v2.1.2
github.com/avct/uasurfer v0.0.0-20180817072212-dc0ec4fd1e87
github.com/coreos/go-oidc/v3 v3.19.0
github.com/fatih/structs v1.1.0
github.com/gin-contrib/cors v1.3.0
github.com/gin-gonic/gin v1.7.7
github.com/jinzhu/gorm v1.9.8
github.com/robfig/cron v0.0.0-20180505203441-b41be1df6967
github.com/satori/go.uuid v1.2.1-0.20180103174451-36e9d2ebbde5
github.com/stretchr/testify v1.4.0
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
github.com/stretchr/testify v1.7.0
golang.org/x/oauth2 v0.36.0
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
)

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-jose/go-jose/v4 v4.1.4 // indirect
github.com/go-playground/locales v0.13.0 // indirect
github.com/go-playground/universal-translator v0.17.0 // indirect
github.com/go-playground/validator/v10 v10.4.1 // indirect
github.com/go-sql-driver/mysql v1.4.1 // indirect
github.com/golang/protobuf v1.3.3 // indirect
github.com/jinzhu/inflection v0.0.0-20180308033659-04140366298a // indirect
github.com/json-iterator/go v1.1.9 // indirect
github.com/leodido/go-urn v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.12 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/ugorji/go/codec v1.1.7 // indirect
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 // indirect
golang.org/x/sys v0.0.0-20200116001909-b77594299b42 // indirect
google.golang.org/appengine v1.4.0 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/yaml.v2 v2.2.8 // indirect
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
)
13 changes: 10 additions & 3 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ github.com/avct/uasurfer v0.0.0-20180817072212-dc0ec4fd1e87 h1:TUuuMI5Sxsw5IVVu3
github.com/avct/uasurfer v0.0.0-20180817072212-dc0ec4fd1e87/go.mod h1:noBAuukeYOXa0aXGqxr24tADqkwDO2KRD15FsuaZ5a8=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/coreos/go-oidc/v3 v3.19.0 h1:F/xyOi3x1UnG1U27YVnM1N6bHiL1K2upi6U/0qr8r+I=
github.com/coreos/go-oidc/v3 v3.19.0/go.mod h1:DYCf24+ncYi+XkIH97GY1+dqoRlbaSI26KVTCI9SrY4=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
Expand All @@ -35,6 +37,8 @@ github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm
github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM=
github.com/gin-gonic/gin v1.7.7 h1:3DoBmSbJbZAWqXJC3SLjAPfutPJJRN1U5pALB7EeTTs=
github.com/gin-gonic/gin v1.7.7/go.mod h1:axIBovoeJpVj8S3BwE0uPMTeReE4+AfFtqpqaZ1qq1U=
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
Expand All @@ -59,7 +63,6 @@ github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
Expand Down Expand Up @@ -130,10 +133,10 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
Expand All @@ -159,6 +162,8 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
Expand Down Expand Up @@ -208,6 +213,8 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
144 changes: 144 additions & 0 deletions scripts/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
# OIDC organization bootstrap

`oidc_org_bootstrap.py` links existing Icaro **reseller** accounts to their
My (Logto) organization by generating the `UPDATE` statements that populate
`accounts.logto_org_id`. That column is the **only** link the OIDC login
uses at runtime: no email/VAT matching ever happens during login. This
script is where the (reviewed, one-time) matching happens instead.

Identity chain used for matching:

```
icaro accounts.uuid == old-my (noc) reseller.uuid
noc reseller --(uuid / piva / normalized company name)--> new-my reseller org
```

The script is **offline** (CSV in, SQL + reports out — it never touches a
database) and **idempotent**: every generated `UPDATE` carries
`AND logto_org_id IS NULL`, so re-running and re-applying only adds new
links and never rewrites existing ones.

## Prerequisites

1. The `accounts.logto_org_id` column must exist (new installs get it from
`deploy/ansible/roles/icarodb/files/icaro.sql`; existing databases need):

```sql
ALTER TABLE accounts
ADD COLUMN logto_org_id varchar(64) DEFAULT NULL AFTER uuid,
ADD UNIQUE KEY logto_org_id (logto_org_id);
```

2. Read access to the three data sources: the Icaro MariaDB, the old My
(noc) MariaDB, and the new My PostgreSQL.

## Step 1 — extract the input files

Both TSV (`mysql -B` output) and CSV are accepted; the delimiter is
auto-detected. Keep the headers exactly as below.

**Icaro accounts** (only `type='reseller'`; desk/customer accounts are
internal to Icaro and must never be linked):

```bash
mysql -B icaro -e "SELECT id, IFNULL(uuid,'') AS uuid, username, IFNULL(email,'') AS email
FROM accounts WHERE type='reseller';" > icaro_accounts.tsv
```

**Old My resellers** (noc):

```bash
mysql -B noc -e "SELECT r.uuid, IFNULL(r.piva,'') AS piva, IFNULL(r.company,'') AS company,
u.username, u.email
FROM reseller r JOIN User u ON u.id = r.user_id
WHERE r.deleted IS NULL;" > noc_resellers.tsv
```

**New My reseller organizations** (PostgreSQL; use `psql` so the JSON
`custom_data` is quoted correctly):

```bash
psql "$DATABASE_URL" -c "\copy (SELECT COALESCE(logto_id,'') AS logto_id, name,
COALESCE(custom_data::text,'{}') AS custom_data
FROM resellers WHERE deleted_at IS NULL)
TO 'my_orgs.csv' WITH CSV HEADER"
```

## Step 2 — dry run

```bash
python3 scripts/oidc_org_bootstrap.py \
--icaro-accounts icaro_accounts.tsv \
--noc-resellers noc_resellers.tsv \
--my-orgs my_orgs.csv \
--output-dir bootstrap-out
```

Nothing is written to any database. Output files:

| File | Content | Action |
|---|---|---|
| `matched.sql` | one `UPDATE` per linked account, with account → org and match method (`uuid`/`piva`/`name`) as a comment | review, then apply |
| `report_matched.csv` | same matches in tabular form | review |
| `report_icaro_only.csv` | accounts with no old-My reseller (self-registered / Icaro-only) | none: they keep the classic password login |
| `report_no_my_org.csv` | old-My resellers whose organization is not on the new My yet | re-run after the next org import batch |
| `report_ambiguous.csv` | multiple candidate orgs, or one org claimed by multiple accounts | resolve by hand |

## Step 3 — review and apply

Read `matched.sql` (especially any `name`-method matches, which are the
loosest) and the ambiguous report, then:

```bash
mysql icaro < bootstrap-out/matched.sql
```

## Re-running (important)

Run steps 1–3 again **after every batch of organization imports on the new
My**. If a reseller's organization exists on My but the bootstrap has not
linked it yet, their first OIDC login triggers JIT provisioning and creates
a *new empty* account instead of linking the historical one (runtime
deliberately performs no heuristic matching). Keeping import and bootstrap
close together minimizes that window; the section below covers the cleanup
when a duplicate slips through.

Apply with `--force` so one conflict does not stop the rest, and read the
errors — they are the duplicate detector:

```bash
mysql --force icaro < bootstrap-out/matched.sql
```

A `Duplicate entry ... for key 'logto_org_id'` error means that org was
already claimed by a JIT-created duplicate: reconcile it as below.

## Reconciling a JIT duplicate

Symptom: a reseller logged in via My before the bootstrap linked their
historical account. JIT created a fresh empty account (recognizable:
`username = logto_org_id`, journal line `OIDC provisioning: created company
account ...`), while the historical account still has the hotspots.

```sql
-- 0. find the two accounts
SELECT id, username, name, logto_org_id, created FROM accounts
WHERE logto_org_id = '<org_id>' OR (name LIKE '%<company>%' AND type = 'reseller');

-- 1. make sure the duplicate is empty (otherwise it needs a manual merge)
SELECT COUNT(*) FROM hotspots WHERE account_id = <dup_id>;

-- 2. move the link to the historical account
UPDATE accounts SET logto_org_id = NULL WHERE id = <dup_id>;
UPDATE accounts SET logto_org_id = '<org_id>' WHERE id = <legacy_id> AND logto_org_id IS NULL;

-- 3. drop the empty duplicate
DELETE FROM access_tokens WHERE account_id = <dup_id>;
DELETE FROM subscriptions WHERE account_id = <dup_id>;
DELETE FROM account_sms_counts WHERE account_id = <dup_id>;
DELETE FROM accounts WHERE id = <dup_id>;
```

The reseller's next login lands on the historical account. If step 1 shows
hotspots in the duplicate, do **not** delete it: move its hotspots to the
historical account first (or escalate — that is a merge, not a cleanup).
Loading
Loading