feat(oidc): add oauth_client_user_tenants assignments + admin API #126
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!126
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/bunyip-61-oauth-client-user-tenants"
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-61. First of three children under the BUNYIP-60 umbrella (per-RP tenant claim on bunyip's at+jwt).
Context. Today bunyip mints at+jwt with only the standard OIDC claims. mokosh-server (and every future PSA RP) falls back to
OIDC_DEFAULT_TENANT_IDenv (defaultUuid::from_u128(1)) for every bunyip-issued token. Result: every user authenticated through bunyip lands in the same tenant and sees each other's data. The fix is to source a per-RP tenant claim from a bunyip-owned(user, oauth_client) -> tenant_idtable; this PR lands that table and the admin API. BUNYIP-62 wires/authorizeto gate on it; BUNYIP-63 emits the claim on the token mints.Schema (migration
20260612000010_create_oauth_client_user_tenants.sql):oauth_client_user_tenants(id, oauth_client_id, user_id, tenant_id, role, created_at, created_by). UNIQUE on the (client, user, tenant) triple (a user can legitimately have multiple tenants per RP), composite index on(oauth_client_id, user_id)covers the/authorizehot path that BUNYIP-62 will run.oauth_clientsgrows a nullabletenant_claim_name TEXT. NULL means "no tenant claim, no enforcement, identical to pre-BUNYIP-61 behaviour" so existing seeded clients are unchanged. The cutover from NULL to non-null (mokosh_tenant_idetc.) is a separate deploy step gated on the assignment table being backfilled.Domain types (
crates/bunyip-domain/src/models/oauth_client_user_tenant.rs):OAuthClientUserTenantrow model (sqlx FromRow + Serialize for the admin list response).CreateUserTenantAssignmentrequest DTO.Repository (
crates/bunyip-domain/src/repositories/oauth_client_user_tenant.rs):assignments_for(client, user)is the hot path BUNYIP-62 consumes. Stable order so a tenant picker UI renders deterministically across reloads.list_for_clientpowers the admin list endpoint. No pagination in v1; realistic order of magnitude is tens to low hundreds per client. Adding?after=...later does not break the flat-array body shape.assign(...)upserts (ON CONFLICT DO UPDATE on the role column). Retrying a seed script that crashed mid-batch does not 500.unassign(id)is idempotent so a network retry after a successful delete returns 204, not 404.Admin API (
bunyip-api/src/handlers/admin_oauth_tenants.rs, wired inroutes/admin.rs):/v1/admin/oauth-clients/{client_id}/user-tenants-> list/v1/admin/oauth-clients/{client_id}/user-tenants-> assign (upsert)/v1/admin/oauth-clients/{client_id}/user-tenants/{assignment_id}-> unassignAudit (
crates/bunyip-domain/src/models/audit.rs):OauthUserTenantAssigned/OauthUserTenantUnassignedactions, registered as admin actions and serialized tooauth_user_tenant_assigned/oauth_user_tenant_unassigned.No UI changes; v1 of the umbrella ships admin-API-only. A picker UI lives on bunyip-web later; for now an admin (or a one-off seed script) hits the API directly.
just check-containerclean (modulo the pre-existingtest_config_defaultsparallel-env-var flake on main; 188 other tests pass; fmt + clippy + build clean).#BUNYIP-61
#BUNYIP-60
Closes BUNYIP-61. First of three children under the BUNYIP-60 umbrella (per-RP tenant claim on bunyip's at+jwt). Context. Today bunyip mints at+jwt with only the standard OIDC claims. mokosh-server (and every future PSA RP) falls back to `OIDC_DEFAULT_TENANT_ID` env (default `Uuid::from_u128(1)`) for every bunyip-issued token. Result: every user authenticated through bunyip lands in the same tenant and sees each other's data. The fix is to source a per-RP tenant claim from a bunyip-owned `(user, oauth_client) -> tenant_id` table; this PR lands that table and the admin API. BUNYIP-62 wires `/authorize` to gate on it; BUNYIP-63 emits the claim on the token mints. Schema (migration `20260612000010_create_oauth_client_user_tenants.sql`): - New `oauth_client_user_tenants(id, oauth_client_id, user_id, tenant_id, role, created_at, created_by)`. UNIQUE on the (client, user, tenant) triple (a user can legitimately have multiple tenants per RP), composite index on `(oauth_client_id, user_id)` covers the `/authorize` hot path that BUNYIP-62 will run. - `oauth_clients` grows a nullable `tenant_claim_name TEXT`. NULL means "no tenant claim, no enforcement, identical to pre-BUNYIP-61 behaviour" so existing seeded clients are unchanged. The cutover from NULL to non-null (`mokosh_tenant_id` etc.) is a separate deploy step gated on the assignment table being backfilled. Domain types (`crates/bunyip-domain/src/models/oauth_client_user_tenant.rs`): - `OAuthClientUserTenant` row model (sqlx FromRow + Serialize for the admin list response). - `CreateUserTenantAssignment` request DTO. Repository (`crates/bunyip-domain/src/repositories/oauth_client_user_tenant.rs`): - `assignments_for(client, user)` is the hot path BUNYIP-62 consumes. Stable order so a tenant picker UI renders deterministically across reloads. - `list_for_client` powers the admin list endpoint. No pagination in v1; realistic order of magnitude is tens to low hundreds per client. Adding `?after=...` later does not break the flat-array body shape. - `assign(...)` upserts (ON CONFLICT DO UPDATE on the role column). Retrying a seed script that crashed mid-batch does not 500. - `unassign(id)` is idempotent so a network retry after a successful delete returns 204, not 404. Admin API (`bunyip-api/src/handlers/admin_oauth_tenants.rs`, wired in `routes/admin.rs`): - GET `/v1/admin/oauth-clients/{client_id}/user-tenants` -> list - POST `/v1/admin/oauth-clients/{client_id}/user-tenants` -> assign (upsert) - DELETE `/v1/admin/oauth-clients/{client_id}/user-tenants/{assignment_id}` -> unassign - Every mutation writes an audit-log row carrying the (user, tenant, role, assignment_id) so security review can reconstruct "who connected what when" even after the row itself is gone. Audit (`crates/bunyip-domain/src/models/audit.rs`): - `OauthUserTenantAssigned` / `OauthUserTenantUnassigned` actions, registered as admin actions and serialized to `oauth_user_tenant_assigned` / `oauth_user_tenant_unassigned`. No UI changes; v1 of the umbrella ships admin-API-only. A picker UI lives on bunyip-web later; for now an admin (or a one-off seed script) hits the API directly. `just check-container` clean (modulo the pre-existing `test_config_defaults` parallel-env-var flake on main; 188 other tests pass; fmt + clippy + build clean). #BUNYIP-61 #BUNYIP-60