Authorization & Access Control - Domain Specification
First Introduced: MVP.0 Status: Specification Complete Last Updated: 2025-10-25
Overview
Authorization & Access Control define what actions users can take and where they can take them within the platform. This domain combines Membership-based scoping (Org and Account) with Role-Based Access Control (RBAC) to form a clear, auditable, and extensible model. It ensures that every API call, UI action, and data mutation respects the correct organizational and account boundaries while maintaining a comprehensive audit trail.
Responsibilities
This domain IS responsible for:
- Role-based permission management (RBAC)
- Permission evaluation and enforcement
- Session lifecycle management (creation, validation, expiration, revocation)
- Authentication token handling (JWT issuance and validation)
- Permission-to-role mapping with allow/deny semantics
- Scope-based access control (Org-wide vs. Account-level)
- Session security and rotation
- OAuth/OIDC integration (Google SSO)
This domain is NOT responsible for:
- Defining organizational boundaries (→ Identity & Tenancy domain)
- User identity management (→ Identity & Tenancy domain)
- Resource ownership tracking (→ Individual resource domains)
- Audit event logging (→ Analytics domain)
- Fine-grained resource-level policies (deferred to ABAC implementation)
Relationships
Depends On:
- Identity & Tenancy - Uses Org, Account, User, Membership entities for scoping
Depended On By:
- Supply - Enforces access to Spaces/Units
- Availability - Controls calendar modifications
- Pricing - Restricts pricing rule changes
- Bookings - Manages booking operations
- Payments - Controls financial operations
- Content - Manages media and description access
- Channels - Controls channel publishing rights
- ALL other domains - Every domain consumes permission checks
Related Domains:
- Delegation & Collaboration - Extends cross-org access model
- Analytics & Audit - Consumes authorization events
Core Entities
Entity: Role
Purpose: Named bundle of permissions representing a functional role within an organization (e.g., admin, owner, manager, viewer).
Key Attributes:
- id(UUID, primary key)
- name(VARCHAR, unique, required) - Internal role identifier
- display_name(VARCHAR) - User-friendly name shown in UI
- description(TEXT) - Role purpose and capabilities
- is_system(BOOLEAN, default true) - System-defined vs. custom role
- org_id(UUID, nullable, foreign key → organizations.id) - NULL for global roles
- created_at,- updated_at(timestamps)
Relationships:
- Role → RolePermission (1:*) - Maps to permissions
- Role → Membership (1:*) - Assigned to users via memberships
- Role → Org (*, many-to-one, optional) - Org-specific custom roles
Lifecycle:
- Created: Seeded on platform initialization; custom roles created by admins
- Updated: Permission mappings changed (audit logged)
- Deleted: Soft delete only (retain for audit); system roles never deleted
Business Rules:
- System roles (is_system=true) cannot be deleted
- Role names must be unique within scope (global or per-org)
- At least one admin role must exist in every Org
- Role changes trigger session rotation for affected users
Entity: Permission
Purpose: Atomic action registry defining all possible operations in the system (e.g., space.read, booking.manage).
Key Attributes:
- id(UUID, primary key)
- resource(VARCHAR, required) - Resource type (e.g., 'space', 'booking', 'pricing')
- action(VARCHAR, required) - Operation verb (e.g., 'read', 'create', 'update', 'delete', 'manage')
- description(TEXT) - Human-readable explanation
- created_at(timestamp)
Relationships:
- Permission → RolePermission (1:*) - Links to roles
Lifecycle:
- Created: Seeded on platform initialization; extended for new features
- Updated: Description changes only
- Deleted: Never deleted (versioned deprecation)
Business Rules:
- (resource, action)pair must be unique
- Permission names follow pattern: {resource}.{action}
- Wildcard permissions supported: *.read,space.*,*.*
- Permissions are version-controlled and centrally maintained
Entity: RolePermission
Purpose: Junction table linking Roles to Permissions with allow/deny semantics.
Key Attributes:
- role_id(UUID, foreign key → roles.id, required)
- permission_id(UUID, foreign key → permissions.id, required)
- effect(ENUM: 'allow' | 'deny', required) - Access effect
- created_at(timestamp)
Primary Key: (role_id, permission_id)
Relationships:
- RolePermission → Role (*, many-to-one)
- RolePermission → Permission (*, many-to-one)
Lifecycle:
- Created: When building role permission sets
- Updated: Effect can be changed from allow to deny or vice versa
- Deleted: Cascade delete when role is removed
Business Rules:
- Deny always overrides allow (deny-wins semantics)
- Same permission can be both allowed and denied (explicit deny takes precedence)
- Permission checks evaluate: (1) find all denies → reject if any; (2) find all allows → accept if any; (3) default deny
Entity: Session
Purpose: Server-side session record tracking authenticated user sessions with security metadata.
Key Attributes:
- id(UUID, primary key) - Session identifier (returned to client as JWT)
- user_id(UUID, foreign key → users.id, required)
- token_hash(VARCHAR, unique) - SHA-256 hash of session token for validation
- issued_at(TIMESTAMP, default NOW()) - Session creation time
- expires_at(TIMESTAMP, required) - Absolute expiration (24 hours from issued_at)
- last_activity_at(TIMESTAMP) - Last request timestamp (for idle timeout)
- revoked_at(TIMESTAMP, nullable) - Session revocation time
- user_agent(TEXT) - Browser/client user agent string
- ip_address(INET) - Client IP address at session creation
- created_at(timestamp)
Relationships:
- Session → User (*, many-to-one)
Lifecycle:
- Created: On successful authentication (Google SSO callback)
- Updated: last_activity_atrefreshed on each request
- Revoked: Set revoked_aton logout, role change, or security event
- Expired: Automatically invalid when expires_at < NOW()orrevoked_at IS NOT NULL
Business Rules:
- Sessions expire 24 hours after creation (absolute timeout)
- Active sessions limited to 10 per user (oldest revoked automatically)
- Session tokens stored client-side as HttpOnly, Secure, SameSite cookies
- User agent changes trigger security warning (potential hijacking)
- Sessions revoked on: logout, role change, password change (future), privilege escalation
Entity: PolicyRule (Future ABAC)
Purpose: Conditional access rules for attribute-based access control (ABAC), enabling context-aware permissions.
Key Attributes:
- id(UUID, primary key)
- org_id(UUID, foreign key → organizations.id, required)
- name(VARCHAR, required)
- description(TEXT)
- subject(JSONB, required) - Who:- {role, user_id, account_id}
- resource(JSONB, required) - What:- {type, id, attributes}
- action(VARCHAR, required) - Which operation
- condition(JSONB) - When/Where:- {channel, region, date_range, time_of_day}
- effect(ENUM: 'allow' | 'deny', required)
- priority(INT, default 100) - Evaluation order (higher = evaluated first)
- is_active(BOOLEAN, default false) - Disabled for MVP
- created_at,- updated_at(timestamps)
Relationships:
- PolicyRule → Org (*, many-to-one)
Lifecycle:
- Created: Schema ready but rules not created in MVP
- Updated: Condition or effect changes
- Deleted: Soft delete via archival
Business Rules:
- is_active=falseby default (not evaluated in MVP)
- Future activation requires policy evaluation engine
- Policies evaluated after RBAC (can override role permissions)
- Higher priority policies evaluated first
- Conditions support: channel, region, time, IP range, resource attributes
Workflows
Workflow: Google SSO Authentication
- User initiates login → Frontend redirects to /auth/login
- Generate PKCE parameters:
- code_verifier= random 43-128 character string (base64url)
- code_challenge= SHA256(code_verifier) encoded as base64url
- state= random 32-byte hex string
 
- Store PKCE state in Redis with 5-minute TTL: auth:state:{state}→{code_verifier, redirect_uri}
- Redirect to Google with authorization URL:
- client_id,- redirect_uri,- response_type=code
- scope=openid email profile
- state,- code_challenge,- code_challenge_method=S256
 
- User authenticates with Google and approves consent
- Google redirects to callback URL with codeandstate
- Validate state parameter → fetch from Redis, verify match, delete key
- Exchange authorization code for tokens:
- POST to Google token endpoint with code,client_id,client_secret,redirect_uri,code_verifier
- Receive id_token,access_token,refresh_token
 
- POST to Google token endpoint with 
- Verify ID token (JWT signature, issuer, audience, expiration)
- Extract user claims from ID token: sub,email,name,picture
- Upsert user record in database:
- Find by email or create new
- Update google_sub,display_name,avatar_url,last_login_at
 
- Create session record:
- Generate session UUID
- Store user_id,expires_at(NOW + 24 hours),user_agent,ip_address
 
- Generate JWT token with payload: {user_id, session_id, email}, signed with JWT_SECRET, expires 24h
- Cache session in Redis: session:{session_id}→{user_id, email, permissions}, TTL 24 hours
- Set secure cookie with session_id: HttpOnly, Secure, SameSite=Lax, maxAge=86400000
- Return success with user profile and organization memberships
- Redirect to dashboard
Postconditions:
- User authenticated with valid session
- JWT token returned to client
- Session cached for fast validation
- User can access authorized resources
Workflow: Permission Check
- Extract JWT from Authorization header or session cookie
- Validate JWT signature and expiration
- Extract session_id from JWT payload
- Check session cache in Redis: session:{session_id}
- If cache miss:
- Query sessions table for id,user_id,expires_at,revoked_at
- Reject if expired (expires_at < NOW()) or revoked (revoked_at IS NOT NULL)
- Load user memberships for current Org
- Query role permissions for user's role
- Build permission set and cache in Redis (5-minute TTL)
 
- Query sessions table for 
- Extract org_id and account_id from request context (header or session)
- Verify membership scope:
- If membership has account_id=NULL→ Org-wide access (all accounts)
- If membership has account_id={value}→ Verify request targets that account
 
- If membership has 
- Check requested permission: {resource}.{action}
- Evaluate deny rules first:
- Find any role permissions with effect='deny'matching(resource, action)
- If found → DENY access, log denial, return 403
 
- Find any role permissions with 
- Evaluate allow rules:
- Find any role permissions with effect='allow'matching(resource, action)
- Support wildcards: *.action,resource.*,*.*
- If found → ALLOW access, proceed
 
- Find any role permissions with 
- Default deny: If no allow found → DENY access, log denial, return 403
Postconditions:
- Request either proceeds (allowed) or rejected (denied)
- All denials logged to audit trail
- Permission cache warmed for subsequent requests
Workflow: Session Rotation on Role Change
- Admin updates user role in Membership record
- Identify affected user → user_id,org_id
- Invalidate permission cache: Delete Redis key permissions:{user_id}:{org_id}
- Find all active sessions for user:
- Query sessions: user_id = ? AND expires_at > NOW() AND revoked_at IS NULL
 
- Query sessions: 
- For each active session:
- Set revoked_at = NOW()
- Delete Redis cache: session:{session_id}
 
- Set 
- Create new session for user:
- Generate new session UUID
- Copy user_agent,ip_addressfrom old session
- Set new expires_at
 
- Log rotation event to audit trail:
- Event type: session.rotated
- Metadata: {reason: 'role_change', old_role_id, new_role_id, old_session_ids, new_session_id}
 
- Event type: 
- Notify user (future): Push notification or email about role change
- Return new JWT token to client
Postconditions:
- Old sessions invalidated immediately
- New session created with updated permissions
- User must re-authenticate or use new token
- Role change audit trail complete
Business Rules
- Deny-Wins Semantics: Any explicit deny permission overrides all allow permissions
- Default Deny: If no explicit allow found, access is denied
- Org Isolation: Permission checks must validate org_id matches user's active membership
- Account Scoping: Account-level memberships restrict access to that account's resources only
- Session Expiration: Sessions expire 24 hours after creation (absolute timeout)
- Session Revocation: Sessions must be revoked on: logout, role change, privilege escalation, security events
- Permission Caching: Permission sets cached 5 minutes; invalidated on role/membership changes
- Wildcard Permissions: Supported patterns: *.action,resource.*,*.*(admin only)
- System Roles: Core roles (admin,ops,owner_admin,manager,viewer) cannot be deleted
- Audit Requirement: All access denials and role changes must be logged to audit trail
Version Evolution
MVP.0: Core RBAC with Google SSO
Scope:
- 4 system roles: admin,owner,channel_publisher,content_manager,viewer
- Core permissions: 30+ covering account, space, unit, media, availability, pricing, booking, payment, settings, channel operations
- Google OAuth 2.0 with OIDC + PKCE
- Server-side session management (PostgreSQL + Redis)
- JWT token-based authentication
- Role-permission mapping with allow/deny
- Org-wide vs. Account-level membership scoping
- Permission caching (Redis, 5-minute TTL)
- Session cookies (HttpOnly, Secure, SameSite=Lax)
- Basic audit logging of denials and role changes
Included Permissions:
- account.*- Account management
- space.{read,create,update,delete}- Space operations
- unit.{read,create,update,delete}- Unit operations
- media.{read,write,delete}- Media asset management
- availability.{read,create,update,delete}- Calendar operations
- pricing.{read,create,update,delete}- Pricing rule management
- booking.{read,create,update,delete,manage}- Booking operations
- payment.{read,create,update,delete}- Payment operations
- financials.read- Financial reporting
- users.{read,create,update,delete}- Team management
- settings.{read,update}- Organization settings
- channel.{read,manage}- Channel publishing
Role Assignments:
- admin: All permissions (*.*)
- owner: Space, unit, pricing, booking, payment management; financial read; settings
- channel_publisher: Space/unit read, channel management, booking read
- content_manager: Space/unit/media write, availability management
- viewer: Read-only access to all resources
Deferred:
- ABAC policy rules (schema present, is_active=false)
- Idle session timeout (future: 30-minute inactivity)
- Multi-factor authentication (MFA)
- Session device management UI
- Refresh token support (sessions require re-authentication)
- Property-level permission scoping (currently org/account level only)
V1: Enhanced Session Management
Additions:
- Idle timeout (30 minutes of inactivity)
- Session device management (view active sessions, revoke remotely)
- Refresh token support (60-day lifetime, rotated on use)
- Enhanced user agent tracking and anomaly detection
- IP-based session validation with geolocation alerts
- Session activity feed (last 10 actions per session)
Changes:
- sessionstable adds- last_activity_at,- device_name,- location
- Session rotation triggers on IP change beyond threshold
- Grace period for mobile users switching networks
V2: Multi-Provider Authentication
Additions:
- Microsoft OIDC integration
- GitHub OAuth integration
- Email/password authentication with email verification
- Provider configuration table (auth_providers)
- Multi-provider account linking
- Provider-specific token storage (encrypted refresh tokens)
Changes:
- userstable adds- email_verified,- email_verified_at
- New table: user_identities(provider-specific profiles)
- New table: email_verification_tokens
- Authentication flow branches by provider type
V3: Attribute-Based Access Control (ABAC)
Additions:
- PolicyRule evaluation engine
- Context-aware permissions (channel, region, time, IP range)
- Policy management UI (create, test, activate policies)
- Policy priority and conflict resolution
- Condition DSL for complex rules
Changes:
- policy_rules.is_activedefaults to- truefor new policies
- Permission evaluation order: RBAC → ABAC overrides
- Policy cache (Redis) with 5-minute TTL
- Audit logging for policy evaluation results
Example Policy:
{
  "name": "Restrict booking creation to business hours",
  "subject": {"role": "manager"},
  "resource": {"type": "booking"},
  "action": "create",
  "condition": {
    "time_range": {"start": "09:00", "end": "17:00", "timezone": "America/New_York"}
  },
  "effect": "deny",
  "priority": 200
}
Implementation Notes
MVP.0 Scope
Authentication:
- Google OAuth 2.0 only
- PKCE flow for authorization code security
- State parameter validation (5-minute TTL in Redis)
- ID token verification (signature, claims, expiration)
Session Management:
- 24-hour absolute timeout (no idle timeout in MVP)
- Server-side storage (PostgreSQL primary, Redis cache)
- Session limit: 10 active sessions per user
- Cookie-based session ID transmission
Permission Evaluation:
- In-memory permission check after cache load
- Deny-wins semantics
- Wildcard support for admin role
- Cache invalidation on role/membership changes
Performance:
- Redis cache hit rate target: >95%
- Permission check latency: <10ms (cached), <50ms (cache miss)
- Session validation: <5ms (Redis), <20ms (PostgreSQL fallback)
Database Indexes
Critical for performance:
-- Roles
CREATE UNIQUE INDEX idx_roles_name ON roles(name);
CREATE INDEX idx_roles_org ON roles(org_id) WHERE org_id IS NOT NULL;
-- Permissions
CREATE UNIQUE INDEX idx_permissions_resource_action ON permissions(resource, action);
CREATE INDEX idx_permissions_resource ON permissions(resource);
-- RolePermissions
CREATE INDEX idx_role_permissions_role ON role_permissions(role_id);
CREATE INDEX idx_role_permissions_permission ON role_permissions(permission_id);
-- Sessions
CREATE INDEX idx_sessions_user ON sessions(user_id);
CREATE INDEX idx_sessions_expires ON sessions(expires_at) WHERE revoked_at IS NULL;
CREATE INDEX idx_sessions_token_hash ON sessions(token_hash);
-- PolicyRules (future)
CREATE INDEX idx_policy_rules_org ON policy_rules(org_id);
CREATE INDEX idx_policy_rules_active ON policy_rules(is_active, priority) WHERE is_active = true;
Constraints
Data integrity:
-- Roles
ALTER TABLE roles ADD CONSTRAINT roles_name_unique UNIQUE (name);
ALTER TABLE roles ADD CONSTRAINT roles_org_name_unique UNIQUE (org_id, name) WHERE org_id IS NOT NULL;
-- Permissions
ALTER TABLE permissions ADD CONSTRAINT permissions_resource_action_unique UNIQUE (resource, action);
-- RolePermissions
ALTER TABLE role_permissions ADD CONSTRAINT role_permissions_effect_check CHECK (effect IN ('allow', 'deny'));
-- Sessions
ALTER TABLE sessions ADD CONSTRAINT sessions_expires_valid CHECK (expires_at > issued_at);
Security Considerations
JWT Tokens:
- Signed with HS256 using secret key (minimum 256 bits)
- Short-lived: 24 hours
- Payload: {user_id, session_id, email, exp, iat}
- Stored in HttpOnly cookie (prevents XSS)
PKCE Flow:
- code_verifier: 43-128 random characters
- code_challenge: SHA256(code_verifier) base64url-encoded
- State parameter: 32-byte random hex
- All temporary state stored in Redis with 5-minute TTL
Session Security:
- Token hash stored (SHA-256), not plaintext
- User agent binding (detect hijacking)
- IP address logging (audit trail)
- Automatic revocation on suspicious activity
Physical Schema
See 001_initial_schema.sql for complete CREATE TABLE statements.
Summary:
- 4 tables: roles, permissions, role_permissions, sessions
- 1 future table: policy_rules (schema present, not enforced)
- 10+ indexes for query performance
- 8+ constraints for data integrity
- Redis cache keys: session:{uuid},permissions:{user_id}:{org_id},auth:state:{state}
Operational Notes
Monitoring:
- Track permission cache hit rate (target >95%)
- Monitor session creation rate and concurrent sessions
- Alert on high denial rates (potential attack or misconfiguration)
- Track session revocation reasons (manual vs. automatic)
Cache Management:
- Session cache: 24-hour TTL, matches session expiration
- Permission cache: 5-minute TTL, invalidate on role changes
- PKCE state: 5-minute TTL, one-time use
- Redis memory limit: size sessions + permissions cache appropriately
Cleanup Jobs:
- Daily: Delete expired sessions older than 7 days
- Weekly: Archive revoked sessions to cold storage
- Monthly: Rotate JWT signing keys (with grace period)
Audit Requirements:
- Log all access denials with user, resource, action, timestamp
- Log all role changes with old/new roles, changed by, reason
- Log all session revocations with reason and metadata
- Retain audit logs for 7 years (compliance)
Related Documents
- MVP Mapping - MVP version alignment
- Identity & Tenancy Domain - User and membership model
- Authentication Flow Guide - Detailed OAuth/OIDC implementation
- Permission Seeding - Initial roles and permissions
- Session Security Guide - Best practices
- MVP.0 Overview
- V1 Product Vision