ADR 0001: Permission-based HTTP authorization
Status
Accepted — implemented April 2026.
Context
Services used coarse @Roles() metadata with inconsistent string literals (DOCTOR vs physician vs admin) matched against JWT user.roles after Keycloak realm expansion (packages/@ghasi/auth-guard/src/keycloak-realm-roles.ts). Admin-driven RBAC and a stable vocabulary require stable permission keys and Access Policy as PDP. ModuleEntitlementGuard (licensing) stays orthogonal.
Decision
- Single vocabulary — Route metadata uses
@Permissions(...)with stable strings: legacy role-style aliases during migration, thenresource_type:actionkeys aligned with Access Policy (access-policygetUserContextreturnsresourceType:action). - Enforcement —
PermissionsGuardin@ghasi/nestjs-commonreads@Permissions(reflector keypermissions) and legacy@Roles(reflector keyROLES) until all handlers migrate. It unions JWTuser.permissionswith normalizeduser.roles/userTypeunlessGHASI_JWT_PERMISSIONS_ONLY=true(staging: JWT claim only). - JWT — Optional
permissionsarray on access tokens (Keycloak protocol mapper or downstream). IAMPOST /internal/iam/verifyparsespermissionsfrom the payload and, whenIAM_ENRICH_PERMISSIONS_FROM_ACCESS_POLICY=trueandACCESS_POLICY_SERVICE_URL(orACCESS_POLICY_URL) is set, mergesGET /internal/access/contexteffective permissions. JWT size: Use node-scoped tokens, permission sets, or PDPevaluatefor ABAC-heavy routes if claims grow large. - Keycloak — Realm roles remain coarse personas per
specs/reference/PLATFORM_REALM_ROLES.md; fine permissions live in Access Policy and the JWT claim, not duplicated as hundreds of Keycloak client roles.
Non-goals
- No change to public API shapes or business logic beyond authorization metadata and guards.
AbacGuard/@Policies()remain non-enforcing until wired toevaluate(separate ADR).
Consequences
- Positive: Controllers depend on stable keys; admin UI can map tenant roles to permissions via existing access-policy APIs.
- Operational: Version permission catalogue when adding routes; log denials with required permission keys (message:
insufficient permission).
Implementation notes
| Area | Location |
|---|---|
| Guard / decorators | packages/@ghasi/nestjs-common — permissions.guard.ts, permissions.decorator.ts |
| JWT types | packages/@ghasi/shared-types — CurrentUser.permissions, JwtPayload.permissions |
| Passport | packages/@ghasi/auth-guard — jwt.strategy.ts attaches permissions from token |
| IAM verify | apps/services/iam — user-permissions.enrich.ts, iam-internal.controller.ts |
| Catalogue audit | specs/access/permission-catalogue-audit.md |
Staging validation
- Set
IAM_ENRICH_PERMISSIONS_FROM_ACCESS_POLICY=truewith access-policy URL; confirmverifyreturns mergedpermissions. - Set
GHASI_JWT_PERMISSIONS_ONLY=trueon a service to assert routes deny when the token lackspermissions(no role fallback).