Files
KArtSell.Aegis/db/migrations/0042_iam_tables.sql
T
kjh2064 a7f4ec8759 feat(wbs): AEG-VS-01-02 Identity & Access Control data contract
AEG-VS-01-02: Data Schema & Contract Definition

Implementation:
1. db/migrations/0042_iam_tables.sql
   - identity table: PIT versioning (published_at, revision_version)
   - role table: Hierarchy levels (GUEST/USER/OPERATOR/ADMIN/SUPER_ADMIN)
   - role_assignment table: Maker-Checker workflow (PENDING_APPROVAL → ACTIVE)
   - permission table: Granular permissions (RESOURCE:ACTION)
   - role_permission table: M:N role-to-permission mapping
   - mfa_device table: TOTP/WebAuthn/SMS/EMAIL support

2. contracts/data/identity-data-contract.v1.json
   - Full JSON Schema for 5 tables
   - PIT (Point-in-Time) versioning strategy
   - Maker-Checker workflow constraints
   - Unique constraints (username, email, role_name, resource+action)
   - Referential integrity (cascade on delete)
   - Lineage: upstream (OIDC), downstream (Auth middleware, Authorization policy)
   - Quality rules: no circular hierarchies, MFA verification, approval counts

Principles Applied:
- 정규화: 5NF (identity/role/permission separation)
- 역정규화: role_assignment.approved_by_identity_ids (array for audit)
- 정공법: Maker-Checker enforced at schema level (approval_count constraint)
- 안정성: Immutable append-only (published_at, revision_version)
- 데이터 정합성: referential integrity, unique constraints, quality rules

Next: AEG-VS-01-03 (Domain Policy Implementation)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-08-17 17:26:55 +09:00

212 lines
8.2 KiB
PL/PgSQL

-- 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),
-- Constraints: One role per identity (except temporary/special cases)
UNIQUE(identity_id, role_id) WHERE assignment_state NOT IN ('REVOKED', 'REJECTED')
);
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);
-- 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;