Skip to main content

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

  1. Single vocabulary — Route metadata uses @Permissions(...) with stable strings: legacy role-style aliases during migration, then resource_type:action keys aligned with Access Policy (access-policy getUserContext returns resourceType:action).
  2. EnforcementPermissionsGuard in @ghasi/nestjs-common reads @Permissions (reflector key permissions) and legacy @Roles (reflector key ROLES) until all handlers migrate. It unions JWT user.permissions with normalized user.roles / userType unless GHASI_JWT_PERMISSIONS_ONLY=true (staging: JWT claim only).
  3. JWT — Optional permissions array on access tokens (Keycloak protocol mapper or downstream). IAM POST /internal/iam/verify parses permissions from the payload and, when IAM_ENRICH_PERMISSIONS_FROM_ACCESS_POLICY=true and ACCESS_POLICY_SERVICE_URL (or ACCESS_POLICY_URL) is set, merges GET /internal/access/context effective permissions. JWT size: Use node-scoped tokens, permission sets, or PDP evaluate for ABAC-heavy routes if claims grow large.
  4. 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 to evaluate (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

AreaLocation
Guard / decoratorspackages/@ghasi/nestjs-commonpermissions.guard.ts, permissions.decorator.ts
JWT typespackages/@ghasi/shared-typesCurrentUser.permissions, JwtPayload.permissions
Passportpackages/@ghasi/auth-guardjwt.strategy.ts attaches permissions from token
IAM verifyapps/services/iamuser-permissions.enrich.ts, iam-internal.controller.ts
Catalogue auditspecs/access/permission-catalogue-audit.md

Staging validation

  • Set IAM_ENRICH_PERMISSIONS_FROM_ACCESS_POLICY=true with access-policy URL; confirm verify returns merged permissions.
  • Set GHASI_JWT_PERMISSIONS_ONLY=true on a service to assert routes deny when the token lacks permissions (no role fallback).