feat(auth): membership authority endpoint + fail-closed API auth (RBAC Phase 1)
GET /v1/users/{id}/memberships answers 'which tenants does this JWT
subject belong to, with which org_roles and entitlements' (ratified
auth design, model B2). Keycloak supplies user->tenant links and roles
via the Adapter (attribute projection until the realm migrates to
Organizations); registered tenants override status/plan/products from
registry tables.
New internal/authn verifies Keycloak bearer tokens (OIDC discovery +
JWKS, audience AUTH_EXPECTED_AUDIENCE, default tenant-registry). With
AUTH_ENABLED=true all routes except /healthz + /readyz require a token
and the server refuses to start if the verifier cannot initialize;
false (dev default) keeps the API open. Completes the M5.2-deferred
org_roles lookup and replaces the 'M4.3 adds JWT validation' TODO in
the spec.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
31cb06cf3d
commit
4a49c630d4
+56
-1
@@ -8,7 +8,10 @@ info:
|
||||
`PLATFORM_ARCHITECTURE.md §5c` for the schema, and
|
||||
`PRODUCT_INTEGRATION_SPEC.md §8.4` for the audit shape.
|
||||
|
||||
This API is not yet authenticated — M4.3 adds Keycloak JWT validation.
|
||||
Auth (RBAC Phase 1): with AUTH_ENABLED=true every route except
|
||||
/healthz and /readyz requires a Keycloak-issued bearer token whose
|
||||
audience contains AUTH_EXPECTED_AUDIENCE (INTERNAL_SERVICE_ONLY
|
||||
posture). With AUTH_ENABLED=false (dev default) the API is open.
|
||||
contact:
|
||||
email: oncall@breakpilot.com
|
||||
license:
|
||||
@@ -250,6 +253,49 @@ paths:
|
||||
description: Revoked.
|
||||
"404": { $ref: "#/components/responses/NotFound" }
|
||||
|
||||
/v1/users/{id}/memberships:
|
||||
get:
|
||||
summary: Resolve which tenants a user belongs to (membership authority).
|
||||
description: |
|
||||
The B2 membership-authority endpoint (ratified compliance auth
|
||||
design). Product backends call this with the JWT `sub` instead of
|
||||
trusting token claims or client-supplied headers. Keycloak supplies
|
||||
the user→tenant links and org_roles; where the tenant is registered
|
||||
here, tenant_status / plan / products are overridden from registry
|
||||
tables (source: "registry"), otherwise the Keycloak attribute
|
||||
projection is returned as-is (source: "keycloak").
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
description: Keycloak user id (the JWT `sub`).
|
||||
schema: { type: string }
|
||||
responses:
|
||||
"200":
|
||||
description: Memberships (possibly empty) for the user.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [user_id, memberships]
|
||||
properties:
|
||||
user_id: { type: string }
|
||||
memberships:
|
||||
type: array
|
||||
items:
|
||||
allOf:
|
||||
- $ref: "#/components/schemas/Claims"
|
||||
- type: object
|
||||
required: [source]
|
||||
properties:
|
||||
source: { type: string, enum: [registry, keycloak] }
|
||||
"400": { $ref: "#/components/responses/BadRequest" }
|
||||
"404": { $ref: "#/components/responses/NotFound" }
|
||||
"503":
|
||||
description: Keycloak unreachable — membership cannot be resolved.
|
||||
content:
|
||||
application/json: { schema: { $ref: "#/components/schemas/Error" } }
|
||||
|
||||
/v1/internal/keycloak/claims:
|
||||
post:
|
||||
summary: Resolve the up-to-date claim bundle for a user/tenant.
|
||||
@@ -356,6 +402,15 @@ paths:
|
||||
"400": { $ref: "#/components/responses/BadRequest" }
|
||||
|
||||
components:
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
description: |
|
||||
Keycloak-issued token (client_credentials for services). Enforced
|
||||
on all non-health routes when AUTH_ENABLED=true.
|
||||
|
||||
responses:
|
||||
BadRequest:
|
||||
description: Input failed validation.
|
||||
|
||||
Reference in New Issue
Block a user