| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140 |
- """Add enterprise identity, OIDC, lifecycle, session and emergency ledgers."""
- from alembic import op
- revision = "20260802_470"
- down_revision = "20260802_460"
- branch_labels = None
- depends_on = None
- def upgrade() -> None:
- op.execute(
- """
- CREATE TABLE public.identity_idp_config_versions (
- uid UUID PRIMARY KEY, provider_uid UUID NOT NULL, version INTEGER NOT NULL CHECK (version > 0),
- status VARCHAR(20) NOT NULL CHECK (status IN ('draft','active','disabled','superseded')),
- issuer TEXT NOT NULL, client_id VARCHAR(300) NOT NULL,
- secret_ref VARCHAR(160) NOT NULL CHECK (secret_ref ~ '^env:[A-Z][A-Z0-9_]*$'),
- authorization_endpoint TEXT NOT NULL, token_endpoint TEXT NOT NULL, jwks_uri TEXT NOT NULL,
- redirect_uris JSONB NOT NULL CHECK (jsonb_typeof(redirect_uris) = 'array'),
- algorithms JSONB NOT NULL CHECK (jsonb_typeof(algorithms) = 'array'),
- mapping_version VARCHAR(120) NOT NULL, config JSONB NOT NULL DEFAULT '{}'::jsonb,
- created_by UUID NOT NULL REFERENCES public.users(id), created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
- activated_at TIMESTAMPTZ, disabled_at TIMESTAMPTZ,
- UNIQUE (provider_uid, version), CHECK (jsonb_typeof(config) = 'object')
- );
- CREATE UNIQUE INDEX uq_identity_active_provider ON public.identity_idp_config_versions(provider_uid) WHERE status = 'active';
- CREATE INDEX idx_identity_idp_status_time ON public.identity_idp_config_versions(status, created_at DESC);
- CREATE TABLE public.identity_oidc_flows (
- uid UUID PRIMARY KEY, state_hash CHAR(64) NOT NULL UNIQUE, provider_uid UUID NOT NULL,
- provider_version INTEGER NOT NULL, nonce_hash CHAR(64) NOT NULL, pkce_verifier_hash CHAR(64) NOT NULL,
- redirect_uri TEXT NOT NULL, status VARCHAR(20) NOT NULL DEFAULT 'pending' CHECK (status IN ('pending','consumed','expired','failed')),
- expires_at TIMESTAMPTZ NOT NULL, consumed_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
- CHECK (expires_at > created_at)
- );
- CREATE INDEX idx_identity_oidc_flow_expiry ON public.identity_oidc_flows(status, expires_at);
- CREATE TABLE public.enterprise_identity_links (
- uid UUID PRIMARY KEY, provider_uid UUID NOT NULL, enterprise_subject VARCHAR(500) NOT NULL,
- user_uid UUID NOT NULL REFERENCES public.users(id), username VARCHAR(64) NOT NULL,
- display_name VARCHAR(300) NOT NULL, department_external_id VARCHAR(300) NOT NULL,
- groups JSONB NOT NULL DEFAULT '[]'::jsonb, role_names JSONB NOT NULL DEFAULT '[]'::jsonb,
- authorization_scope JSONB NOT NULL DEFAULT '{}'::jsonb, mapping_version VARCHAR(120) NOT NULL,
- claims_digest CHAR(64) NOT NULL, status VARCHAR(20) NOT NULL CHECK (status IN ('active','disabled','left')),
- token_version INTEGER NOT NULL DEFAULT 1 CHECK (token_version > 0),
- created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
- UNIQUE (provider_uid, enterprise_subject), UNIQUE (provider_uid, user_uid),
- CHECK (jsonb_typeof(groups) = 'array'), CHECK (jsonb_typeof(role_names) = 'array'),
- CHECK (jsonb_typeof(authorization_scope) = 'object')
- );
- CREATE INDEX idx_enterprise_identity_status ON public.enterprise_identity_links(status, updated_at DESC);
- CREATE TABLE public.identity_organization_nodes (
- uid UUID PRIMARY KEY, provider_uid UUID NOT NULL, external_id VARCHAR(300) NOT NULL,
- node_type VARCHAR(20) NOT NULL CHECK (node_type IN ('department','group')),
- parent_external_id VARCHAR(300), display_name VARCHAR(300) NOT NULL,
- status VARCHAR(20) NOT NULL CHECK (status IN ('active','disabled')),
- attributes JSONB NOT NULL DEFAULT '{}'::jsonb, updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
- UNIQUE (provider_uid, node_type, external_id), CHECK (jsonb_typeof(attributes) = 'object')
- );
- CREATE INDEX idx_identity_org_parent ON public.identity_organization_nodes(provider_uid, parent_external_id);
- CREATE TABLE public.identity_directory_events (
- uid UUID PRIMARY KEY, provider_uid UUID NOT NULL, source VARCHAR(80) NOT NULL, source_event_id VARCHAR(300) NOT NULL,
- cursor_value VARCHAR(500) NOT NULL, cursor_sequence BIGINT NOT NULL CHECK(cursor_sequence > 0),
- event_type VARCHAR(20) NOT NULL CHECK (event_type IN ('JOINER','MOVER','LEAVER','DISABLE','RESTORE','DEPARTMENT','GROUP')),
- enterprise_subject VARCHAR(500), payload_digest CHAR(64) NOT NULL,
- status VARCHAR(20) NOT NULL CHECK (status IN ('applied','rejected','conflict')),
- safe_result JSONB NOT NULL DEFAULT '{}'::jsonb, processed_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
- UNIQUE (provider_uid, source, source_event_id), UNIQUE(provider_uid,source,cursor_sequence),
- CHECK (jsonb_typeof(safe_result) = 'object')
- );
- CREATE INDEX idx_identity_directory_cursor ON public.identity_directory_events(provider_uid,source,cursor_sequence DESC);
- CREATE TABLE public.identity_directory_checkpoints (
- provider_uid UUID NOT NULL,source VARCHAR(80) NOT NULL,cursor_value VARCHAR(500) NOT NULL,
- cursor_sequence BIGINT NOT NULL CHECK(cursor_sequence > 0),updated_at TIMESTAMPTZ NOT NULL,
- PRIMARY KEY(provider_uid,source)
- );
- CREATE TABLE public.identity_directory_conflicts (
- uid UUID PRIMARY KEY,provider_uid UUID NOT NULL,source VARCHAR(80) NOT NULL,source_event_id VARCHAR(300) NOT NULL,
- cursor_value VARCHAR(500) NOT NULL,cursor_sequence BIGINT NOT NULL,payload_digest CHAR(64) NOT NULL,
- reason VARCHAR(120) NOT NULL,created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
- );
- CREATE TABLE public.identity_sessions (
- uid UUID PRIMARY KEY, family_uid UUID NOT NULL, identity_link_uid UUID REFERENCES public.enterprise_identity_links(uid),
- user_uid UUID NOT NULL REFERENCES public.users(id), identity_source VARCHAR(20) NOT NULL CHECK (identity_source IN ('oidc','local','emergency')),
- token_version INTEGER NOT NULL CHECK (token_version > 0), refresh_hash CHAR(64) NOT NULL UNIQUE,
- status VARCHAR(20) NOT NULL CHECK (status IN ('active','rotated','revoked','expired')),
- rotated_from_uid UUID REFERENCES public.identity_sessions(uid), revoke_reason VARCHAR(120), risk_reason VARCHAR(300),
- emergency_request_uid UUID,
- created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, last_seen_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
- expires_at TIMESTAMPTZ NOT NULL, rotated_at TIMESTAMPTZ, revoked_at TIMESTAMPTZ, CHECK (expires_at > created_at),
- CHECK ((identity_source='emergency') = (emergency_request_uid IS NOT NULL))
- );
- CREATE INDEX idx_identity_session_user_status ON public.identity_sessions(user_uid, status, created_at DESC);
- CREATE INDEX idx_identity_session_family ON public.identity_sessions(family_uid, created_at);
- CREATE TABLE public.identity_exchange_codes (
- uid UUID PRIMARY KEY, code_hash CHAR(64) NOT NULL UNIQUE, session_uid UUID NOT NULL REFERENCES public.identity_sessions(uid),
- redirect_uri TEXT NOT NULL, status VARCHAR(20) NOT NULL CHECK (status IN ('pending','consumed','expired')),
- expires_at TIMESTAMPTZ NOT NULL, consumed_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
- CHECK (expires_at > created_at)
- );
- CREATE INDEX idx_identity_exchange_expiry ON public.identity_exchange_codes(status, expires_at);
- CREATE TABLE public.identity_emergency_requests (
- uid UUID PRIMARY KEY, requester_uid UUID NOT NULL REFERENCES public.users(id),
- emergency_account_uid UUID NOT NULL REFERENCES public.users(id), reason TEXT NOT NULL,
- status VARCHAR(20) NOT NULL CHECK (status IN ('pending','active','closed','expired','reviewed','rejected')),
- starts_at TIMESTAMPTZ NOT NULL, expires_at TIMESTAMPTZ NOT NULL,
- activated_at TIMESTAMPTZ, closed_at TIMESTAMPTZ, reviewer_uid UUID REFERENCES public.users(id),
- review_outcome TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
- CHECK (requester_uid <> emergency_account_uid), CHECK (expires_at > starts_at)
- );
- CREATE INDEX idx_identity_emergency_status_expiry ON public.identity_emergency_requests(status, expires_at);
- CREATE TABLE public.identity_emergency_approvals (
- request_uid UUID NOT NULL REFERENCES public.identity_emergency_requests(uid),
- approver_uid UUID NOT NULL REFERENCES public.users(id), approved_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
- PRIMARY KEY (request_uid, approver_uid)
- );
- ALTER TABLE public.identity_sessions ADD CONSTRAINT fk_identity_session_emergency
- FOREIGN KEY(emergency_request_uid) REFERENCES public.identity_emergency_requests(uid);
- CREATE TABLE public.identity_audit_events (
- uid UUID PRIMARY KEY, event_type VARCHAR(80) NOT NULL, outcome VARCHAR(20) NOT NULL CHECK (outcome IN ('success','failure','denied','warning')),
- actor_uid UUID REFERENCES public.users(id), provider_uid UUID, enterprise_subject_digest CHAR(64),
- resource_type VARCHAR(80) NOT NULL, resource_uid VARCHAR(300), correlation_uid UUID,
- safe_detail JSONB NOT NULL DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
- CHECK (jsonb_typeof(safe_detail) = 'object')
- );
- CREATE INDEX idx_identity_audit_time ON public.identity_audit_events(created_at DESC, uid DESC);
- CREATE INDEX idx_identity_audit_actor ON public.identity_audit_events(actor_uid, created_at DESC);
- """
- )
- def downgrade() -> None:
- raise RuntimeError("Forward-only migration: enterprise identity and authentication evidence cannot be removed")
|