# RBAC Policy Configuration Type Contracts # # Defines types for the file-based RBAC layer. # Identity (who the user is) comes from JWT claims / AuthContext. # This file defines what groups are allowed to do (policy rules + effects). let EffectTypes = ["redirect", "log", "audit_log", "notify", "response"] in let ResourceKinds = ["endpoint", "page", "asset"] in let Outcomes = ["allow", "deny"] in { # Validate that a value belongs to a fixed set OneOf = fun allowed => std.contract.from_predicate (fun v => std.array.elem v allowed), # Effect type discriminator contract EffectType | doc "Possible effect types: redirect | log | audit_log | notify | response" = OneOf EffectTypes, # Policy effect: describes what to do when a rule matches # The `type` field discriminates the variant PolicyEffect = { type | OneOf EffectTypes | doc "Effect variant discriminator", # --- redirect: issue HTTP redirect to another URL ------------------ to | String | doc "redirect: destination URL. Supports {request.path}, {user.email}, {user.id}" | optional, # --- log | audit_log: write to application or audit log ----------- level | String | doc "log|audit_log: severity level (debug|info|warn|error)" | default = "info", message | String | doc "log|audit_log|notify: message template with interpolation support" | optional, event | String | doc "audit_log: semantic event name (e.g. 'access_denied')" | optional, # --- notify: send to external channel ---------------------------- channel | String | doc "notify: destination channel (security|slack|webhook|email)" | optional, # --- response: return HTTP response directly --------------------- status | Number | doc "response: HTTP status code (401, 403, 302 ...)" | default = 403, body | { _ : Dyn } | doc "response: JSON response body" | optional, }, # A single access rule: matches resource kind, URL pattern, HTTP methods PolicyRule = { resource | OneOf ResourceKinds | doc "Resource kind: endpoint | page | asset", pattern | String | doc "Glob pattern matched against the request path (e.g. '/api/*', '/*')", outcome | OneOf Outcomes | doc "Decision when this rule matches: allow | deny", methods | Array String | doc "HTTP methods this rule applies to. Empty array = all methods" | default = [], effects | Array PolicyEffect | doc "Effects executed when this rule matches" | default = [], }, # Group definition with optional inheritance FileRbacGroup = { name | String | doc "Group identifier (must match role names from JWT, e.g. 'admin', 'moderator')", parent_groups | Array String | doc "Groups whose permissions are inherited (BFS expansion)" | default = [], permissions | Array PolicyRule | doc "Access rules for this group (first match wins)" | default = [], }, # Per-user permission override (highest priority, matched by email or sub) FileRbacUserOverride = { id | String | doc "User identifier: email address or JWT sub claim", permissions | Array PolicyRule | doc "User-specific rules (override all group rules)" | default = [], }, # Default rules for unauthenticated and fallback behaviour FileRbacDefaults = { unauthenticated_allow | Array PolicyRule | doc "Explicit allow rules for unauthenticated requests (first match wins)" | default = [], unauthenticated_deny | Array PolicyRule | doc "Explicit deny rules for unauthenticated requests (evaluated after allow)" | default = [], authenticated_fallback | OneOf Outcomes | doc "Outcome when an authenticated user matches no rule" | default = "deny", unauthenticated_fallback | OneOf Outcomes | doc "Outcome when an unauthenticated user matches no rule" | default = "deny", fallback_effects | Array PolicyEffect | doc "Effects applied when the fallback outcome is triggered" | default = [], }, # Root RBAC configuration record FileRbacConfig = { groups | Array FileRbacGroup | doc "Group definitions with their permissions" | default = [], user_overrides | Array FileRbacUserOverride | doc "Per-user permission overrides (highest priority)" | default = [], defaults | FileRbacDefaults | doc "Fallback rules for unmatched requests" | default = {}, }, }