-- Migration 0042: Identity and Access Control (IAM) Tables -- AEG-VS-01-02: Data Contract for Identity/Access Management -- Created: 2026-08-17 -- Status: READY FOR REVIEW BEGIN; -- 1. IDENTITY TABLE (PIT: point-in-time identity) CREATE TABLE IF NOT EXISTS public.identity ( identity_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- Identity attributes username VARCHAR(255) NOT NULL UNIQUE, email VARCHAR(255) NOT NULL UNIQUE, display_name VARCHAR(255), -- State machine (UNDEFINED → ACTIVE → REQUIRES_MFA_SETUP → MFA_CONFIGURED → MFA_SUSPENDED → INACTIVE → REVOKED) state VARCHAR(50) NOT NULL DEFAULT 'UNDEFINED' CHECK (state IN ('UNDEFINED', 'ACTIVE', 'REQUIRES_MFA_SETUP', 'MFA_CONFIGURED', 'MFA_SUSPENDED', 'INACTIVE', 'REVOKED')), -- MFA requirement flag mfa_required BOOLEAN NOT NULL DEFAULT false, mfa_enforced_at TIMESTAMP WITH TIME ZONE, -- Lifecycle tracking (immutable append-only) created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, -- Audit columns (for correction events) published_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, valid_time_start TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, valid_time_end TIMESTAMP WITH TIME ZONE, revision_version INT NOT NULL DEFAULT 1, -- Idempotency & correlation correlation_id UUID UNIQUE, source_event_id UUID UNIQUE, checksum VARCHAR(64) ); CREATE INDEX idx_identity_username ON public.identity(username); CREATE INDEX idx_identity_email ON public.identity(email); CREATE INDEX idx_identity_state ON public.identity(state); CREATE INDEX idx_identity_published_at ON public.identity(published_at); -- 2. ROLE TABLE (Core & Domain-Specific Roles) CREATE TABLE IF NOT EXISTS public.role ( role_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- Role definition role_name VARCHAR(100) NOT NULL UNIQUE, description TEXT, -- Hierarchy (0 = GUEST, 1 = USER, 2 = OPERATOR, 3 = ADMIN, 4 = SUPER_ADMIN, 100+ = domain-specific) hierarchy_level INT NOT NULL DEFAULT 0, -- Role type (CORE / DOMAIN_SPECIFIC / TEMPORARY / SERVICE) role_type VARCHAR(50) NOT NULL DEFAULT 'CORE' CHECK (role_type IN ('CORE', 'DOMAIN_SPECIFIC', 'TEMPORARY', 'SERVICE')), -- Expiration (for TEMPORARY roles like quarterly reviewer) expires_at TIMESTAMP WITH TIME ZONE, -- Lifecycle created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, published_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, valid_time_start TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, valid_time_end TIMESTAMP WITH TIME ZONE, revision_version INT NOT NULL DEFAULT 1, -- Idempotency correlation_id UUID UNIQUE, checksum VARCHAR(64) ); CREATE INDEX idx_role_name ON public.role(role_name); CREATE INDEX idx_role_hierarchy ON public.role(hierarchy_level); CREATE INDEX idx_role_type ON public.role(role_type); -- 3. ROLE_ASSIGNMENT TABLE (With Maker-Checker Workflow) CREATE TABLE IF NOT EXISTS public.role_assignment ( role_assignment_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- Association identity_id UUID NOT NULL REFERENCES public.identity(identity_id) ON DELETE CASCADE, role_id UUID NOT NULL REFERENCES public.role(role_id) ON DELETE CASCADE, -- Maker-Checker workflow -- State: PENDING_APPROVAL → APPROVED_BY_1 → APPROVED_BY_2 → ACTIVE → EXPIRED / REVOKED assignment_state VARCHAR(50) NOT NULL DEFAULT 'PENDING_APPROVAL' CHECK (assignment_state IN ('PENDING_APPROVAL', 'APPROVED_BY_1', 'APPROVED_BY_2', 'ACTIVE', 'EXPIRED', 'REVOKED', 'REJECTED')), -- Approval tracking approval_count INT DEFAULT 0, required_approval_count INT NOT NULL DEFAULT 2, -- Configurable per role approved_by_identity_ids UUID[] DEFAULT '{}', approval_reason TEXT, -- Effective date (when role becomes ACTIVE) effective_at TIMESTAMP WITH TIME ZONE, -- Lifecycle created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, published_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, valid_time_start TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, valid_time_end TIMESTAMP WITH TIME ZONE, revision_version INT NOT NULL DEFAULT 1, -- Idempotency & correlation correlation_id UUID UNIQUE NOT NULL DEFAULT gen_random_uuid(), checksum VARCHAR(64) ); CREATE INDEX idx_role_assignment_identity ON public.role_assignment(identity_id); CREATE INDEX idx_role_assignment_role ON public.role_assignment(role_id); CREATE INDEX idx_role_assignment_state ON public.role_assignment(assignment_state); CREATE INDEX idx_role_assignment_correlation ON public.role_assignment(correlation_id); -- Partial unique constraint: One active role per identity CREATE UNIQUE INDEX idx_role_assignment_unique_active ON public.role_assignment(identity_id, role_id) WHERE assignment_state NOT IN ('REVOKED', 'REJECTED'); -- 4. PERMISSION TABLE (Granular Permissions) CREATE TABLE IF NOT EXISTS public.permission ( permission_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- Permission definition permission_name VARCHAR(100) NOT NULL UNIQUE, description TEXT, -- Resource and action (e.g., "MODEL:READ", "DATASET:WRITE", "AUDIT_LOG:READ") resource VARCHAR(50) NOT NULL, action VARCHAR(50) NOT NULL, -- Permission category (DATA_ACCESS / WORKFLOW_APPROVAL / ADMIN / AUDIT) permission_category VARCHAR(50) NOT NULL CHECK (permission_category IN ('DATA_ACCESS', 'WORKFLOW_APPROVAL', 'ADMIN', 'AUDIT')), -- Lifecycle created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, published_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, valid_time_start TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, valid_time_end TIMESTAMP WITH TIME ZONE, revision_version INT NOT NULL DEFAULT 1, -- Idempotency correlation_id UUID UNIQUE, checksum VARCHAR(64) ); CREATE UNIQUE INDEX idx_permission_resource_action ON public.permission(resource, action); CREATE INDEX idx_permission_category ON public.permission(permission_category); -- 5. ROLE_PERMISSION MAPPING (M:N - Roles to Permissions) CREATE TABLE IF NOT EXISTS public.role_permission ( role_permission_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), role_id UUID NOT NULL REFERENCES public.role(role_id) ON DELETE CASCADE, permission_id UUID NOT NULL REFERENCES public.permission(permission_id) ON DELETE CASCADE, -- Lifecycle created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, published_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, valid_time_start TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, valid_time_end TIMESTAMP WITH TIME ZONE, -- Mapping enforced: one permission per role UNIQUE(role_id, permission_id) ); CREATE INDEX idx_role_permission_role ON public.role_permission(role_id); CREATE INDEX idx_role_permission_permission ON public.role_permission(permission_id); -- 6. MFA_DEVICE TABLE (Multi-Factor Authentication) CREATE TABLE IF NOT EXISTS public.mfa_device ( mfa_device_id UUID PRIMARY KEY DEFAULT gen_random_uuid(), identity_id UUID NOT NULL REFERENCES public.identity(identity_id) ON DELETE CASCADE, -- Device type (TOTP / WEBAUTHN / SMS / EMAIL) device_type VARCHAR(50) NOT NULL CHECK (device_type IN ('TOTP', 'WEBAUTHN', 'SMS', 'EMAIL')), -- Device identifier (for recovery/management) device_name VARCHAR(255), device_identifier VARCHAR(255) UNIQUE, -- Secret (encrypted, stored as hash only for recovery codes) secret_hash VARCHAR(255), -- State (PENDING_VERIFICATION → VERIFIED → REVOKED) state VARCHAR(50) NOT NULL DEFAULT 'PENDING_VERIFICATION' CHECK (state IN ('PENDING_VERIFICATION', 'VERIFIED', 'REVOKED')), -- Last used (for anomaly detection) last_used_at TIMESTAMP WITH TIME ZONE, -- Lifecycle created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, published_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, valid_time_start TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, valid_time_end TIMESTAMP WITH TIME ZONE, -- Idempotency correlation_id UUID UNIQUE, checksum VARCHAR(64) ); CREATE INDEX idx_mfa_device_identity ON public.mfa_device(identity_id); CREATE INDEX idx_mfa_device_state ON public.mfa_device(state); COMMIT;