feat(identity): two provisioning settings, and the cloud and on-prem split stops being two products - #126
Conversation
… claim in a token Fourth in ADR-0018's order, and the last place where a right was read off the way somebody signed in. `config/oidc.yaml` maps the Keycloak user attribute `tenant_id` into `subject_tenant_id`, and three comparisons against the platform root decided from it whether a caller administers the installation. That attribute belongs to a Keycloak user — a login — so a person with two of them was an administrator through one and an ordinary member through the other. Administering the platform is now holding a membership of the platform root. One spelling of the rule, published so the identity directory and the identity gear cannot drift apart on it. Both signals are accepted while the migration runs, and that is the whole point of doing it in this order: an installation names its administrators in configuration (`platform_admins`), those memberships are seeded idempotently at every start, ADR-0016's backfill already wrote the rows for identities that carried the attribute, and only when both are true everywhere can the token reading go. Removing it before that is a lockout: a deployment whose administrators were only ever administrators by token would have none, and no way to make one. Seeding is logged rather than fatal. An installation that cannot reach its database has a larger problem than an unseeded administrator, and refusing to boot would hide it. VERIFIED ON A STAND, with nobody's token naming the root: administrator — not root in the token, seeded membership → 200 ordinary person — neither → 403 the seeded row → owner / bootstrap The directory's gate uses the same helper; on that stand it answers 503 because Keycloak admin is unconfigured there, so it is covered by construction rather than exercised. Signed-off-by: Andrej Kuchma <Andrej.Kuchma@constructor.tech>
…split stops being two products
Fifth in ADR-0018's order, and the part that makes §4 real: the domain model is
identical whether Studio runs as a cloud service or inside one company. What
differs is only how a person comes by their first organization, and that is now
two settings rather than a deployment mode with branches behind it.
studio-user.config.on_first_login — join this organization when a person
is first seen, as this role
studio-organizations.config
.self_service — may a person create an organization
Cloud leaves the first unset and the second true; an installation inside one
company does the opposite. Nothing else in the model, and no authorization
path, is aware of which of the two it is running as.
WHY AUTO-JOIN IS NOT ACCESS DERIVED FROM AUTHENTICATION
The membership is written at the one moment a person begins to exist, and it is
a row. It can be revoked — suspending somebody in Studio without removing them
from the corporate directory, which is frequently what a company actually wants
— and `membership.source` records that it came from a first login rather than
from an invitation or an operator. Access that followed the token could do
neither. ADR-0011 §1 stands: authentication still grants nothing by itself; the
deployment's statement does, and the statement is data.
The join happens on provisioning, not on every request. If it ran per request a
revoked membership would come straight back, which would make revocation a lie.
`on_first_login.role` may not be `owner`: a deployment does not hand ownership
to everybody who signs in. A configuration that says so is refused with a log
rather than obeyed.
Both new settings fail soft. A join that cannot be written is logged, because a
person who exists but has not joined is a person the next request can still
join, while failing there would leave them unable to sign in at all.
`GET /studio-organizations/v1/capabilities` reports `self_service`, so the
portal's no-organization screen can offer creation where creation is possible
and say "wait for an invitation" where it is not — rather than offering a
control that answers 403.
VERIFIED ON A STAND, by running one deployment as both:
cloud phase capabilities → self_service: true; an organization is created
on-prem phase capabilities → self_service: false
a brand-new subject's first request → member / first_login
creation → 403 SELF_SERVICE_DISABLED, for the administrator too
membership revoked → no memberships, and /me still answers 200
Signed-off-by: Andrej Kuchma <Andrej.Kuchma@constructor.tech>
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Advanced Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Comment |
|
Folded into #132, which carries this branch's commits unchanged along with the four pieces that finish the scope. Closing this one in favour of that is safe — nothing here is lost. |
Fifth in ADR-0018's order. Stacked on #125, targeting
feature/create-your-organization.The split stops being two products
ADR-0018 §4 claimed the domain model is identical whether Studio runs as a cloud service or inside one company, and that the difference is only how a person comes by their first organization. This makes that true. Two settings, no deployment mode, no branches behind a flag:
Nothing else in the model — and no authorization path — knows which of the two it is running as.
This is the part worth arguing with, because ADR-0011 §1 forbids the thing it superficially resembles.
The membership is written at the one moment a person begins to exist, and it is a row:
membership.sourcerecords that it came from a first login, not from an invitation or an operator.Access that followed the token could do neither. The deployment is making a statement — the users of this identity provider are the members of this organization — and the statement is recorded as data. §1 stands: authentication still grants nothing by itself.
The join happens on provisioning, not per request. If it ran per request, a revoked membership would come straight back and revocation would be a lie. Verified below.
on_first_login.rolemay not beowner: a deployment does not hand ownership to everybody who signs in. A config that says so is refused with a log rather than obeyed.Both settings fail soft. A join that cannot be written is logged — a person who exists but has not joined is one the next request can still join, whereas failing there would leave them unable to sign in at all.
For the portal
GET /studio-organizations/v1/capabilities→{ self_service }, so the no-organization screen can offer creation where it is possible and say "wait for an invitation" where it is not, instead of offering a control that answers 403.Verified
On a stand, by running one deployment as both — the same binary, the same database, only the two settings changed:
That last line is the one the ADR's argument rests on, so it is tested rather than asserted.
Tests: 502/502 with a real Postgres,
clippy --all-targets -- -D warningsandcargo fmt --checkclean.Not here
The portal side — a create control on the no-organization screen, and an invitations view. The backend now answers everything both need; the UI is the next piece, and it is where ADR-0018 §2 finally becomes visible to a person.