Role and Policy Lifecycle Model (Review Baseline)¶
Purpose¶
Define a single lifecycle model for:
- built-in platform and tenant/project roles,
- tenant-defined custom roles,
- policy constraints that are OPA-ready but OPA-deferred.
This document is the pre-implementation baseline for feedback before endpoint/schema changes.
Scope¶
In scope:
- role taxonomy and boundaries (platform vs tenant/project),
- role lifecycle rules (create/update/delete/assignment/audit),
- policy lifecycle rules and decision interface,
- phased rollout plan (MVP+1).
Out of scope:
- full OPA/OPAL deployment,
- enterprise IdP federation admin UX implementation details,
- final API contract shapes for role CRUD.
Design Principles¶
- Ownership remains
tenant -> project -> resource. - Role grants are permission-based, not UI-label-based.
- Built-in roles are immutable and non-deletable.
- Membership and role changes are soft-delete/auditable, never silent overwrite.
- Authorization decision call shape must be stable before swapping to OPA.
Current vs Target¶
Current runtime:
- Platform role in
users.role(user/admin) gates platform admin surfaces. - Tenant/project membership tables exist and are the ownership/authz baseline.
Target extension (this model):
- Keep platform role layer for platform operations.
- Add explicit tenant/project role lifecycle with custom roles.
- Keep the same decision contract so in-code evaluation and OPA can share inputs/outputs.
Platform Role Mapping and Cutover¶
Transitional Mapping (while users.role is runtime source)¶
users.role='admin'maps toplatform_superadmin.users.role='user'maps toplatform_user.platform_opsis introduced via explicit binding/claims in Phase 2 and is not inferred fromusers.role.
Cutover Rule¶
- Phase 1:
users.roleremains authoritative for platform role checks. - Phase 2: platform role bindings become authoritative;
users.roleis compatibility/read-model only. - During transition, when both sources exist:
- binding-based platform role wins,
- mismatch must emit an audit warning with correlation_id.
Role Taxonomy¶
Platform Roles (Global)¶
Built-in:
platform_superadminplatform_opsplatform_user(default non-platform-admin)
Tenant Roles (Tenant Scope)¶
Built-in:
tenant_ownertenant_admintenant_membertenant_billing_managertenant_billing_viewertenant_viewer
Project Roles (Project Scope)¶
Built-in:
project_ownerproject_adminproject_memberproject_viewer
Service Identity Role¶
service_accountis a project-scoped actor type.- Service accounts never inherit platform roles.
Built-in Role Permission Baseline (MVP+1)¶
Permissions are resource.action keys.
| Role | Baseline permissions |
|---|---|
platform_superadmin |
authorization.override.all |
platform_ops |
platform.ops.read, platform.ops.runbook.read, platform.node.read, platform.node.probe, platform.audit.read |
platform_user |
no platform-admin permissions |
tenant_owner |
tenant.user.invite, tenant.user.remove, tenant.role.assign, tenant.policy.write, tenant.project.create, tenant.billing.read, tenant.billing.write |
tenant_admin |
tenant.user.invite, tenant.user.remove, tenant.role.assign, tenant.project.read, tenant.project.update, tenant.billing.read |
tenant_member |
tenant.read, project.read, tenant.user.read |
tenant_billing_manager |
tenant.billing.read, tenant.billing.write, tenant.invoice.read |
tenant_billing_viewer |
tenant.billing.read, tenant.invoice.read |
tenant_viewer |
tenant.read |
project_owner |
project.role.assign, allocation.create, allocation.release, allocation.read, storage.read, storage.write, terminal.connect |
project_admin |
project.member.invite, allocation.create, allocation.release, allocation.read, storage.read, storage.write, terminal.connect |
project_member |
allocation.create, allocation.release, allocation.read, storage.read, storage.write, terminal.connect |
project_viewer |
allocation.read, storage.read |
Notes:
- This table is the implementation baseline and must be represented in code as permission sets.
- Handler checks evaluate permission keys, not role-name string comparisons.
authorization.override.allis a reserved internal permission. It is an explicit allow-all override for platform-superadmin evaluation and is not treated as a prefix wildcard match.tenant.role.assignauthorizes assignment attempts, but assignment ceiling is enforced separately by assignment handler rules (grantor highest active role vs target role).is_assignable_to_service_accountsbaseline for built-ins:- true:
project_member,project_viewer - false: all platform roles, all tenant roles,
project_owner,project_admin - Current runtime-enforced platform action keys are:
platform.adminplatform.ops.readplatform.ops.runbook.readplatform.node.readplatform.audit.readRemaining keys in this table are staged for incremental handler adoption.
Role Inheritance and Scope Rules¶
- Inheritance is only within the same scope tier:
tenant_ownerincludestenant_adminincludestenant_member.project_ownerincludesproject_adminincludesproject_memberincludesproject_viewer.- No automatic tenant-to-project runtime grant:
- tenant role alone does not grant project runtime access unless a project membership exists.
- Platform override:
platform_superadminmay bypass tenant/project checks for explicit platform-admin endpoints.- Assignment ceiling rule:
- Role assignment cannot exceed grantor authority.
tenant_admincannot granttenant_owner.- only
tenant_owner(or platform override) can granttenant_owner.
Built-in vs Custom Roles¶
Built-in Roles¶
is_builtin = true, immutable ID and immutable baseline permission set.- Cannot be deleted.
- Can be disabled only by platform policy control.
Custom Roles (Tenant-Defined)¶
- Scope-limited to tenant or project.
- Editable permission set with versioning.
- Soft-delete only (
deleted_at,deleted_by_user_id,delete_reason). - No hard delete in runtime paths.
Lifecycle Rules¶
Role Definition Lifecycle¶
create-> role versionv1.update-> append new role version.disable-> deny new assignments immediately; active bindings remain valid during configured grace period.delete-> soft-delete marker only (custom roles only).
Disable controls:
- Default mode: graceful disable (
block_new_only) with policy-controlled grace window. - Emergency mode: immediate disable (
block_all_now) for security incidents. - Rollback: role can be re-enabled by same authority scope; rollback must be audit-logged with reason.
- Planned policy key:
authorization.role_disable_grace_window_seconds(not seeded yet in MVP baseline). - Until the key is seeded, runtime behavior is deterministic:
block_new_onlyrequests are rejected asinvalid_request(unconfigured policy),- only
block_all_nowis permitted. - Tracking requirement: add this key to
doc/architecture/Seed_Data_Spec.md,scripts/seed.sql, and queue before enabling graceful disable in production.
Assignment Semantics (Versioning)¶
- Assignments are pinned to
role_version_idat grant time. - Role updates do not auto-upgrade existing assignments.
- Optional bulk-upgrade operation can rebind assignments to a newer version (audit required).
- Bulk-upgrade authority:
- tenant-scope roles:
tenant_owneror platform override. - project-scope roles:
project_owneror platform override. - Bulk-upgrade execution requires explicit target role, from-version, to-version, reason, and correlation_id in audit metadata.
Membership Binding Lifecycle¶
- Membership grant creates active binding row.
- Membership revoke sets
deleted_atfields (soft delete). - Authorization reads only active rows (
deleted_at is null). - Every grant/revoke/change writes audit log with
correlation_id.
Break-Glass Lifecycle¶
- Break-glass elevation is time-bound with explicit reason.
- In MVP+1, only
platform_superadmincan grant break-glass elevation. - Expiry auto-revokes elevation.
- All break-glass actions are high-severity audit events.
- Break-glass implementation phase: Phase 3 (same phase as OPA cutover preparation).
Custom Role Governance¶
- Tenant-scoped custom roles can be created/updated/deleted by
tenant_owner. - Project-scoped custom roles can be created/updated/deleted by
project_owner. platform_superadmincan perform override operations.
Policy Lifecycle (OPA-Ready, OPA-Deferred)¶
Policy Scope¶
- Runtime scope chain stays
global -> tenant -> project(most-specific wins). - Role grants baseline permission.
- Policy adds constraints or denies based on context attributes.
Decision Interface (Stable Contract)¶
Input:
- actor (
user_idorservice_account_id, actor type), - platform role,
- tenant/project memberships and resolved permissions,
- action,
- resource descriptor (
resource_name, type, owner tenant/project), - request attributes (region, sku, time, flags).
Output:
allow/deny,reason_code,applied_scope(global|tenant|project),policy_source(in_code|policy_values|opa).
Reason codes (baseline enum):
permission_deniedmembership_missingscope_mismatchpolicy_constraint_deniedrole_disabledactor_disabled
Implementation rule:
- this input/output shape is mandatory in MVP+1 even while evaluation remains in Go.
Deterministic Authorization Merge Algorithm¶
- Deny if actor is disabled (
actor_disabled). - If
authorization.override.allis present and action is override-eligible, allow immediately and record policy sourcein_code. - Resolve active tenant/project memberships for requested scope; deny on missing required scope (
membership_missing). - Resolve active bound roles in scope and expand inherited roles within same scope tier.
- Build effective permission set as union of resolved role permission sets.
- If action not present in effective set, deny (
permission_denied). - Apply policy constraints on granted action.
- Scope precedence: project -> tenant -> global (most-specific wins).
- Conflict rule: explicit policy deny overrides role grant (
policy_constraint_denied). - Precedence rule for superadmin override: when step 2 matches (
authorization.override.all+override_eligible=true), that allow decision is final and is not overridden by step 9.
Override-eligible source of truth:
- Override eligibility is defined by an explicit action metadata registry (
override_eligible: true|false, default false). - Runtime handlers must resolve action identity from the same action registry consumed by authorization evaluation.
- Superadmin bypass is allowed only for actions explicitly marked
override_eligible=true; no implicit endpoint-path heuristics are permitted. - Registry home: code-owned static registry at
packages/shared/authz/action_registry.go; changes are code-reviewed and not runtime-configurable in MVP+1.
Service Account Permission Rules¶
- Service accounts use the same permission key model (
resource.action). - Service accounts may be bound only to project-scope built-in/custom roles.
- Service accounts cannot be granted platform roles or break-glass elevation.
- Service accounts cannot call platform-admin endpoints.
Data Model Direction¶
Implemented baseline tables:
role_definitions(id,scope_type,scope_id,name,is_builtin,is_assignable_to_service_accounts,state,current_version_id).role_definition_versions(id,role_definition_id,version,created_at,created_by_user_id).role_permissions(role_version_id,permission_key,effect).platform_role_bindings(id,principal_type,principal_id,role_definition_id,created_at,deleted_at).tenant_role_bindings(id,tenant_id,principal_type,principal_id,role_version_id,created_at,deleted_at).project_role_bindings(id,project_id,principal_type,principal_id,role_version_id,created_at,deleted_at).
Constraint direction:
- Tenant/project bindings: unique active binding per principal/scope/role version.
- Platform bindings: unique active binding per (
principal_type,principal_id,role_definition_id). - FK from tenant/project bindings to role version and owner scope.
- Custom role rows must reference tenant/project scope owner.
- Bindings extend membership anchors; they do not replace
tenant_membershipsorproject_memberships. role_definitions.current_version_idmust reference a version row for the samerole_definition_id.platform_role_bindingsreferencesrole_definition_iddirectly because platform built-in roles are immutable and do not participate in role-version lifecycle.platform_role_bindingsis the Phase 2 authority source for platform roles.
Rollout Plan¶
Phase 1 (Near-Term)¶
- Keep current platform role gating (
users.role) intact. - Add explicit role-permission mapping in code for built-in tenant/project roles.
- Seed test users for:
- platform superadmin,
- tenant admin,
- project member,
- project viewer.
Phase 2 (Extension in Same Track)¶
- Add platform role bindings as authority source and enable
platform_opsruntime role path. - Seed platform-ops test users via binding path.
- Use platform-role management APIs for bind/revoke/list as primary control plane path. Controlled scripts remain break-glass fallback only:
scripts/ops/bind_platform_role.shscripts/ops/revoke_platform_role.sh(correlation-id required, audit write required; no ad-hoc SQL).- Add custom role definitions and bindings.
- Add role versioning and soft-delete lifecycle.
- Keep decision interface unchanged.
Phase 3 (Later)¶
- Introduce OPA engine behind existing decision interface.
- Run shadow-mode decision parity checks.
- Flip enforcement after parity SLO is met.
Observability and Audit Requirements¶
Every authorization failure or privileged role mutation should include:
correlation_idactor_typeactor_idplatform_roletenant_idproject_idresource_name(when applicable)reason_code
Decision Status for This Baseline¶
platform_opsis part of MVP+1 built-ins.- Billing split is active in model (
tenant_billing_managerandtenant_billing_viewer). - Phase 2 supports both tenant-level and project-level custom roles.
References¶
doc/architecture/Tenant_Project_Ownership_Baseline.mddoc/architecture/User_Onboarding_Model.mddoc/architecture/Service_Account_Model.mddoc/architecture/adrs/ADR-004-identity-authz-model.mddoc/governance/Assumptions_Register.md(A-015)