feat(oidc): /authorize tenant gate + picker (BUNYIP-62) #127
No reviewers
Labels
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
psa-systems/bunyip!127
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/bunyip-62-authorize-tenant-gate-and-picker"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes BUNYIP-62. Second of three children under the BUNYIP-60 umbrella. Stacks on BUNYIP-61.
After BUNYIP-61 the
oauth_client_user_tenantstable and theoauth_clients.tenant_claim_namecolumn exist but/authorizedoes not consult either. This PR wires the enforcement and adds the tenant picker UI for the multi-tenant-per-user case.Behaviour, as a function of
oauth_clients.tenant_claim_name:tenant_claim_name IS NULL-> existing single-tenant flow, no change. Backward compatible for every seeded client today (mokosh, drillmark, bunyip-web SPA).tenant_claim_name IS NOT NULL:error=access_denied, error_description="no tenant assigned for this client". The user has signed in to bunyip OK but has not been granted access to this RP for any tenant.selected_tenant_idcolumn.selected_tenant_idquery param absent -> render the tenant picker page (HTML, hand-rolled), preserve every AuthorizeQuery field as a hidden input, submit GET back to/oauth2/authorizewith the chosenselected_tenant_idappended.selected_tenant_idquery param present -> validate the pick is one of the user's assignments and continue. A forged pick returns 400access_denied.Schema (migration
20260612000020_oauth_codes_selected_tenant_id.sql):oauth_authorization_codesgrows a nullableselected_tenant_id UUID. Nullable so legacy clients (tenant_claim_name NULL) keep working. /token (BUNYIP-63) will read this column when minting at+jwt and id_token to emit the tenant claim.Code changes:
crates/bunyip-oidc/src/handlers/oidc.rs::AuthorizeQuery: addselected_tenant_id: Option<Uuid>(#[serde(default)]). Newrender_tenant_picker(q, client, assignments, silent_cookies)helper produces the HTML picker page; preserves the silent-SSO Set-Cookie headers across the round-trip so a user who landed in the picker on first hit keeps their newly-minted session. Hand-rolledhtml_escapecovers the five characters that matter for attribute + content contexts (client.name and assignment role values are user-controlled input that flows into the page).crates/bunyip-oidc/src/handlers/oidc.rs::authorize: between client load andissue_authorization_code, branch onclient.tenant_claim_nameandassignments_for(client, user)count as described above. Existing seeded clients (tenant_claim_name NULL) skip the entire block.crates/bunyip-oidc/src/services/oidc_provider.rs::OAuthClient: addpub tenant_claim_name: Option<String>, derivesqlx::FromRow.load_clientand the in-rotationquery_asswitch from compile-timesqlx::query_as!to runtimesqlx::query_as::<_, OAuthClient>so future per-RP column additions do not force a workspace.sqlx/cache regen on every change.crates/bunyip-oidc/src/services/oidc_provider.rs::issue_authorization_code: gains aselected_tenant_id: Option<Uuid>parameter. Runtime query so the new column does not need a cache regen here either. Test-onlytest_client()updated to settenant_claim_name: None.No new dependencies. No UI changes beyond the picker HTML page (admin UI for managing assignments still goes through the BUNYIP-61 admin API).
just check-containerclean (modulo the pre-existingtest_config_defaultsparallel-env-var flake on main; 188 other tests pass; fmt + clippy + build clean).#BUNYIP-62
#BUNYIP-60