Skip to content

ADR-053: Authorization Policy Model Port

Attribute Value
Status Proposed
Date 2026-06-12
Deciders Architecture Team
Extends ADR-050 (Definition/Instance Duality)
Related ADRs ADR-052, ADR-001

1. Context

LCM authenticates with Keycloak/OIDC and enforces coarse RBAC at the application layer. The legacy Track / Session / Pod managers carry a richer, per-resource authorization model: a reusable AuthorizationPolicy (a named set of AuthorizationRequirements) that can guard any definition or resource, evaluated with JQ runtime expressions against the entity under check to filter what each user may see (e.g. "the user's track claim must equal this track's id"). RBAC alone cannot express that data-dependent, per-entity filtering.

2. Decision

2.1 Port the policy model as definitions

Model AuthorizationPolicy as a seed ResourceDefinition composed of embedded AuthorizationRequirements (a child value structure, not an aggregate). Any definition or resource references one via authorization_policy_id.

  • Claim requirementclaim_type (regex-capable) + optional claim_value. The value may be a JQ runtime expression evaluated against the entity (keyed by upper-cased type name, e.g. TRACK). Absence ⇒ existence check only.
  • Composite requirementAll or Any over nested requirements (recursive).

2.2 Authentication stays Keycloak; authorization filters

Keycloak/OIDC remains the authentication layer (cookie/BFF for UI, JWT for API — unchanged). The policy model adds authorization filtering on top: the …ForCurrentUser query pattern loads candidates, then includes those with no policy or whose policy the principal satisfies. Admin principals bypass all checks.

flowchart TB
    U[ClaimsPrincipal] --> A{IsAdmin?}
    A -- yes --> G[Grant / include]
    A -- no --> P{Resource has authorization_policy_id?}
    P -- no --> G
    P -- yes --> L[Load AuthorizationPolicy]
    L --> R{Evaluate each requirement}
    R --> T{Requirement type}
    T -- Claim --> CV["Resolve claim value (JQ runtime expr, entity params)"]
    CV --> CM{User has matching claim?}
    T -- Composite --> CC{Condition}
    CC -- All --> ALL[every child passes]
    CC -- Any --> ANY[at least one child passes]
    CM -- yes --> G
    CM -- no --> D[Deny / filter out]
    ALL --> G
    ANY --> G

2.3 Persistence & evaluation

AuthorizationPolicy is state-based like every aggregate (ADR-051). Evaluation needs a JQ expression evaluator as a lcm_core service; claims come from the validated Keycloak token. The evaluator is invoked by query handlers to filter (not hard-fail) result sets.

3. Consequences

Positive — fine-grained, data-dependent, per-resource visibility the RBAC layer cannot express; reusable named policies; consistent with the legacy contract and seed assets.

Negative / trade-offs — adds a JQ evaluator dependency and an evaluation cost on list endpoints; policy authoring must map onto Keycloak claim shapes; two authorization concepts (Keycloak roles + policies) coexist and must be kept clearly scoped (authN vs per-resource authZ).