ADR-032: RBAC Model
Status
Implemented
Date
2025-01-16 (Retrospective)
Decision Makers
- Security Team - Authorization design
- Architecture Team - Access control patterns
Layer
Auth
Related ADRs
- ADR-005: OAuth2/OIDC Authentication
Supersedes
None
Depends On
- ADR-005: OAuth2/OIDC Authentication
Context
Authorization requires fine-grained access control:
- Role-Based Access: Different capabilities for different roles
- Permission Granularity: Read, write, delete, admin
- Entity-Level: Some roles only access certain entities
- Integration: Work with OAuth claims
- Auditability: Track who can do what
Requirements:
- Simple role model initially
- Permission-based checks in code
- Extensible for future needs
- Mapped from IdP claims
- UI-aware access control
Decision
We implement Role-Based Access Control (RBAC) with permissions:
Key Design Decisions
- Role Hierarchy: Admin > Editor > Viewer
- Permission Sets: Roles map to permission sets
- Claim Mapping: OAuth claims to internal roles
- Decorator Pattern: Route protection via decorators
- UI Integration: Frontend respects permissions
Role Definitions
| Role | Permissions | Description |
|---|---|---|
admin | All | Full system access |
editor | read, write | Create and modify entities |
viewer | read | Read-only access |
service | api | Programmatic access only |
Permission Types
class Permission(Enum):
READ = "read"
WRITE = "write"
DELETE = "delete"
ADMIN = "admin"
API = "api" # For service accounts
Role-Permission Mapping
ROLE_PERMISSIONS = {
UserRole.ADMIN: {Permission.READ, Permission.WRITE, Permission.DELETE, Permission.ADMIN},
UserRole.EDITOR: {Permission.READ, Permission.WRITE},
UserRole.VIEWER: {Permission.READ},
UserRole.SERVICE: {Permission.READ, Permission.WRITE, Permission.API},
}
Route Protection
from core.auth import auth_required, Permission
@router.delete("/requirements/{id}")
@auth_required(permissions=[Permission.DELETE])
async def delete_requirement(
id: str,
user: UserInfo = Depends(get_current_user),
db: Session = Depends(get_db)
):
# Only users with DELETE permission reach here
...
Claim Mapping
def map_oauth_claims_to_role(claims: dict) -> UserRole:
"""Map IdP claims to internal roles."""
groups = claims.get("groups", [])
if "ops-admins" in groups:
return UserRole.ADMIN
elif "ops-editors" in groups:
return UserRole.EDITOR
else:
return UserRole.VIEWER
Consequences
Positive
- Simple Model: Easy to understand and audit
- IdP Integration: Roles from OAuth claims
- Decorator Pattern: Clean route protection
- Extensible: Can add permissions/roles easily
- UI Awareness: Frontend can check permissions
Negative
- Coarse Grained: No entity-level permissions yet
- Static Roles: Roles defined in code
- Group Dependency: Requires IdP group configuration
- No ABAC: Attribute-based control not supported
Neutral
- Claim Mapping: Must match IdP configuration
- Role Proliferation: Resist creating many roles
Implementation Status
- Core implementation complete
- Tests written and passing
- Documentation updated
- Migration/upgrade path defined
- Monitoring/observability in place
- Scoped Role Bindings (Issue #450) - Implemented 2025-01-16
Implementation Details
Core Components
- Models:
backend/core/auth/models.py(includes operational verbs) - Decorators:
backend/core/auth/decorators.py - Middleware:
backend/core/auth/middleware.py - Frontend:
frontend/src/contexts/AuthContext.tsx
Scoped RBAC Components (Issue #450)
- Scope Models:
backend/models/auth_dynamic.pyAuthScope: Hierarchical scope definitions (team, environment, service, organization, project)AuthRoleScopeBinding: Links users to roles within specific scopes
- Permission Checker:
backend/core/auth/scoped_rbac.pyScopedPermissionChecker: Checks permissions within resource contextscreate_scope_resolver: Factory for extracting scope from requestsscoped_auth: Decorator for scoped endpoint protection
- Migration:
backend/alembic/versions/adr_032_scoped_role_bindings.py- Creates
auth_scopesandauth_role_scope_bindingstables - Seeds default environment scopes (development, staging, production)
- Creates
Operational Verbs Added (Issue #450)
class Permission(str, Enum):
# Basic CRUD permissions
READ = "read"
WRITE = "write"
DELETE = "delete"
EXECUTE = "execute"
ADMIN = "admin"
# Operational verbs (ADR-032: Scoped Role Bindings)
APPROVE = "approve" # Approve deployments, changes, runbooks
DEPLOY = "deploy" # Deploy to environments
SCALE = "scale" # Scale services up/down
SILENCE_ALERT = "silence_alert" # Silence alerts during maintenance
Scoped Authorization Pattern
from core.auth.scoped_rbac import ScopedPermissionChecker, create_scope_resolver
# Create a scope resolver for team resources
team_resolver = create_scope_resolver(
scope_type="team",
extract_scope=lambda r: r.path_params.get("team_id"),
)
# Check scoped permissions
checker = ScopedPermissionChecker(db)
has_access = checker.check_scoped_permission(
user=user,
required_permission=Permission.WRITE,
resource_context={"team": "team-alpha", "environment": "staging"},
)
Audit Logging
All permission checks are automatically logged with structured fields:
user_email: User making the requestpermission: Permission being checkedresource_context: Scope path being accessedresult: granted/denieddenial_reason: Reason for denial (if applicable)scope_path: Full hierarchy path for audit trail
LLM Council Review
Review Date: 2025-01-16 Confidence Level: High (100%) Verdict: INSUFFICIENT - REQUIRES MAJOR CHANGES
Quality Metrics
- Consensus Strength Score (CSS): 0.95
- Deliberation Depth Index (DDI): 0.90
Council Feedback Summary
Unanimous consensus that the RBAC model is insufficient for a production SRE platform. The foundational flaw is that permissions are global rather than scoped, creating unacceptable "blast radius" risks.
Key Concerns Identified:
- No Resource Scoping: User with WRITE can modify ANY resource (Team A modifies Team B's entities)
- No Environment Scoping: Can't have ADMIN in Dev but VIEWER in Production
- Insufficient Action Granularity: WRITE covers both "edit description" and "redeploy cluster"
- Missing Audit Logging: No structured logging of permission checks and access
- Service Account Risk: Generic
Permission.APIrole too broad for service accounts
Required Modifications:
- Implement Scoped Role Bindings:
User -> Role -> Scope- Example:
(User: Alice, Role: Editor, Scope: Team-Payments, Env: Staging)
- Example:
- Resource-Aware Decorators:
@auth_required(permission=Action.RESTART, scope_resolver=get_service_team) - Operational Verbs: Add SRE-specific actions:
EXECUTE,APPROVE,ACKNOWLEDGE,DEPLOY,SCALE,SILENCE_ALERT - Mandatory Audit Logging: Emit structured logs for every permission check
- Token Scopes: Service accounts should use fine-grained scopes (e.g.,
metrics:write) - Break-Glass Access: Support time-bound elevation for on-call engineers
Modifications Applied
- Documented scoped role binding requirement
- Added resource-aware authorization pattern
- Documented operational verb additions
- Added audit logging mandate
Issue #450 Implementation (2025-01-16)
All council concerns have been addressed:
| Concern | Resolution |
|---|---|
| No Resource Scoping | AuthScope + AuthRoleScopeBinding enable team/service/project scopes |
| No Environment Scoping | Environment-specific scope bindings allow Admin in staging, Viewer in prod |
| Missing Operational Verbs | Added APPROVE, DEPLOY, SCALE, SILENCE_ALERT to Permission enum |
| No Audit Logging | ScopedPermissionChecker logs all checks with structured fields |
| Coarse Permission Granularity | Scoped bindings limit blast radius of any permission |
Verdict Update: APPROVED - Scoped RBAC implementation addresses all blocking concerns
Council Ranking
- claude-opus-4.5: Best Response (scoping)
- gpt-5.2: Strong (audit requirements)
- gemini-3-pro: Good (operational verbs)
References
ADR-032 | Auth Layer | Implemented