RBAC Phase 1: membership authority endpoint + fail-closed API auth (#14)
ci / shared (push) Failing after 14s
ci / test (push) Successful in 21m13s
ci / image (push) Skipped

Implements the model-B2 membership authority (ratified compliance auth design) and inbound token verification.

- GET /v1/users/{id}/memberships: Keycloak supplies user->tenant links + org_roles (attribute projection until the realm migrates to Organizations); registered tenants override status/plan/products from registry tables (source: registry|keycloak). Closes the M5.2-deferred org_roles stub.
- New internal/authn: OIDC discovery + JWKS verification (go-oidc/v3), audience tenant-registry. /healthz + /readyz stay PUBLIC_EXPLICIT; all other routes INTERNAL_SERVICE_ONLY. AUTH_ENABLED=true refuses to start on incomplete config (fail-closed); false (dev default) keeps current behavior.
- Full suite green incl. postgres testcontainers; openapi.yaml updated (contract test passes).

Activation is a separate step (Auth-5): requires the orca-infra env merge and a portal service token (portal currently calls with no auth).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Sharang Parnerkar <30073382+mighty840@users.noreply.github.com>
Reviewed-on: #14
This commit was merged in pull request #14.
This commit is contained in:
2026-08-24 18:21:46 +00:00
co-authored by Sharang Parnerkar
parent 31cb06cf3d
commit ce75ed04c2
14 changed files with 797 additions and 7 deletions
+56 -1
View File
@@ -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.