20260802_470_enterprise_identity.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. """Add enterprise identity, OIDC, lifecycle, session and emergency ledgers."""
  2. from alembic import op
  3. revision = "20260802_470"
  4. down_revision = "20260802_460"
  5. branch_labels = None
  6. depends_on = None
  7. def upgrade() -> None:
  8. op.execute(
  9. """
  10. CREATE TABLE public.identity_idp_config_versions (
  11. uid UUID PRIMARY KEY, provider_uid UUID NOT NULL, version INTEGER NOT NULL CHECK (version > 0),
  12. status VARCHAR(20) NOT NULL CHECK (status IN ('draft','active','disabled','superseded')),
  13. issuer TEXT NOT NULL, client_id VARCHAR(300) NOT NULL,
  14. secret_ref VARCHAR(160) NOT NULL CHECK (secret_ref ~ '^env:[A-Z][A-Z0-9_]*$'),
  15. authorization_endpoint TEXT NOT NULL, token_endpoint TEXT NOT NULL, jwks_uri TEXT NOT NULL,
  16. redirect_uris JSONB NOT NULL CHECK (jsonb_typeof(redirect_uris) = 'array'),
  17. algorithms JSONB NOT NULL CHECK (jsonb_typeof(algorithms) = 'array'),
  18. mapping_version VARCHAR(120) NOT NULL, config JSONB NOT NULL DEFAULT '{}'::jsonb,
  19. created_by UUID NOT NULL REFERENCES public.users(id), created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
  20. activated_at TIMESTAMPTZ, disabled_at TIMESTAMPTZ,
  21. UNIQUE (provider_uid, version), CHECK (jsonb_typeof(config) = 'object')
  22. );
  23. CREATE UNIQUE INDEX uq_identity_active_provider ON public.identity_idp_config_versions(provider_uid) WHERE status = 'active';
  24. CREATE INDEX idx_identity_idp_status_time ON public.identity_idp_config_versions(status, created_at DESC);
  25. CREATE TABLE public.identity_oidc_flows (
  26. uid UUID PRIMARY KEY, state_hash CHAR(64) NOT NULL UNIQUE, provider_uid UUID NOT NULL,
  27. provider_version INTEGER NOT NULL, nonce_hash CHAR(64) NOT NULL, pkce_verifier_hash CHAR(64) NOT NULL,
  28. redirect_uri TEXT NOT NULL, status VARCHAR(20) NOT NULL DEFAULT 'pending' CHECK (status IN ('pending','consumed','expired','failed')),
  29. expires_at TIMESTAMPTZ NOT NULL, consumed_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
  30. CHECK (expires_at > created_at)
  31. );
  32. CREATE INDEX idx_identity_oidc_flow_expiry ON public.identity_oidc_flows(status, expires_at);
  33. CREATE TABLE public.enterprise_identity_links (
  34. uid UUID PRIMARY KEY, provider_uid UUID NOT NULL, enterprise_subject VARCHAR(500) NOT NULL,
  35. user_uid UUID NOT NULL REFERENCES public.users(id), username VARCHAR(64) NOT NULL,
  36. display_name VARCHAR(300) NOT NULL, department_external_id VARCHAR(300) NOT NULL,
  37. groups JSONB NOT NULL DEFAULT '[]'::jsonb, role_names JSONB NOT NULL DEFAULT '[]'::jsonb,
  38. authorization_scope JSONB NOT NULL DEFAULT '{}'::jsonb, mapping_version VARCHAR(120) NOT NULL,
  39. claims_digest CHAR(64) NOT NULL, status VARCHAR(20) NOT NULL CHECK (status IN ('active','disabled','left')),
  40. token_version INTEGER NOT NULL DEFAULT 1 CHECK (token_version > 0),
  41. created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
  42. UNIQUE (provider_uid, enterprise_subject), UNIQUE (provider_uid, user_uid),
  43. CHECK (jsonb_typeof(groups) = 'array'), CHECK (jsonb_typeof(role_names) = 'array'),
  44. CHECK (jsonb_typeof(authorization_scope) = 'object')
  45. );
  46. CREATE INDEX idx_enterprise_identity_status ON public.enterprise_identity_links(status, updated_at DESC);
  47. CREATE TABLE public.identity_organization_nodes (
  48. uid UUID PRIMARY KEY, provider_uid UUID NOT NULL, external_id VARCHAR(300) NOT NULL,
  49. node_type VARCHAR(20) NOT NULL CHECK (node_type IN ('department','group')),
  50. parent_external_id VARCHAR(300), display_name VARCHAR(300) NOT NULL,
  51. status VARCHAR(20) NOT NULL CHECK (status IN ('active','disabled')),
  52. attributes JSONB NOT NULL DEFAULT '{}'::jsonb, updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
  53. UNIQUE (provider_uid, node_type, external_id), CHECK (jsonb_typeof(attributes) = 'object')
  54. );
  55. CREATE INDEX idx_identity_org_parent ON public.identity_organization_nodes(provider_uid, parent_external_id);
  56. CREATE TABLE public.identity_directory_events (
  57. uid UUID PRIMARY KEY, provider_uid UUID NOT NULL, source VARCHAR(80) NOT NULL, source_event_id VARCHAR(300) NOT NULL,
  58. cursor_value VARCHAR(500) NOT NULL, cursor_sequence BIGINT NOT NULL CHECK(cursor_sequence > 0),
  59. event_type VARCHAR(20) NOT NULL CHECK (event_type IN ('JOINER','MOVER','LEAVER','DISABLE','RESTORE','DEPARTMENT','GROUP')),
  60. enterprise_subject VARCHAR(500), payload_digest CHAR(64) NOT NULL,
  61. status VARCHAR(20) NOT NULL CHECK (status IN ('applied','rejected','conflict')),
  62. safe_result JSONB NOT NULL DEFAULT '{}'::jsonb, processed_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
  63. UNIQUE (provider_uid, source, source_event_id), UNIQUE(provider_uid,source,cursor_sequence),
  64. CHECK (jsonb_typeof(safe_result) = 'object')
  65. );
  66. CREATE INDEX idx_identity_directory_cursor ON public.identity_directory_events(provider_uid,source,cursor_sequence DESC);
  67. CREATE TABLE public.identity_directory_checkpoints (
  68. provider_uid UUID NOT NULL,source VARCHAR(80) NOT NULL,cursor_value VARCHAR(500) NOT NULL,
  69. cursor_sequence BIGINT NOT NULL CHECK(cursor_sequence > 0),updated_at TIMESTAMPTZ NOT NULL,
  70. PRIMARY KEY(provider_uid,source)
  71. );
  72. CREATE TABLE public.identity_directory_conflicts (
  73. uid UUID PRIMARY KEY,provider_uid UUID NOT NULL,source VARCHAR(80) NOT NULL,source_event_id VARCHAR(300) NOT NULL,
  74. cursor_value VARCHAR(500) NOT NULL,cursor_sequence BIGINT NOT NULL,payload_digest CHAR(64) NOT NULL,
  75. reason VARCHAR(120) NOT NULL,created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
  76. );
  77. CREATE TABLE public.identity_sessions (
  78. uid UUID PRIMARY KEY, family_uid UUID NOT NULL, identity_link_uid UUID REFERENCES public.enterprise_identity_links(uid),
  79. user_uid UUID NOT NULL REFERENCES public.users(id), identity_source VARCHAR(20) NOT NULL CHECK (identity_source IN ('oidc','local','emergency')),
  80. token_version INTEGER NOT NULL CHECK (token_version > 0), refresh_hash CHAR(64) NOT NULL UNIQUE,
  81. status VARCHAR(20) NOT NULL CHECK (status IN ('active','rotated','revoked','expired')),
  82. rotated_from_uid UUID REFERENCES public.identity_sessions(uid), revoke_reason VARCHAR(120), risk_reason VARCHAR(300),
  83. emergency_request_uid UUID,
  84. created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, last_seen_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
  85. expires_at TIMESTAMPTZ NOT NULL, rotated_at TIMESTAMPTZ, revoked_at TIMESTAMPTZ, CHECK (expires_at > created_at),
  86. CHECK ((identity_source='emergency') = (emergency_request_uid IS NOT NULL))
  87. );
  88. CREATE INDEX idx_identity_session_user_status ON public.identity_sessions(user_uid, status, created_at DESC);
  89. CREATE INDEX idx_identity_session_family ON public.identity_sessions(family_uid, created_at);
  90. CREATE TABLE public.identity_exchange_codes (
  91. uid UUID PRIMARY KEY, code_hash CHAR(64) NOT NULL UNIQUE, session_uid UUID NOT NULL REFERENCES public.identity_sessions(uid),
  92. redirect_uri TEXT NOT NULL, status VARCHAR(20) NOT NULL CHECK (status IN ('pending','consumed','expired')),
  93. expires_at TIMESTAMPTZ NOT NULL, consumed_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
  94. CHECK (expires_at > created_at)
  95. );
  96. CREATE INDEX idx_identity_exchange_expiry ON public.identity_exchange_codes(status, expires_at);
  97. CREATE TABLE public.identity_emergency_requests (
  98. uid UUID PRIMARY KEY, requester_uid UUID NOT NULL REFERENCES public.users(id),
  99. emergency_account_uid UUID NOT NULL REFERENCES public.users(id), reason TEXT NOT NULL,
  100. status VARCHAR(20) NOT NULL CHECK (status IN ('pending','active','closed','expired','reviewed','rejected')),
  101. starts_at TIMESTAMPTZ NOT NULL, expires_at TIMESTAMPTZ NOT NULL,
  102. activated_at TIMESTAMPTZ, closed_at TIMESTAMPTZ, reviewer_uid UUID REFERENCES public.users(id),
  103. review_outcome TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
  104. CHECK (requester_uid <> emergency_account_uid), CHECK (expires_at > starts_at)
  105. );
  106. CREATE INDEX idx_identity_emergency_status_expiry ON public.identity_emergency_requests(status, expires_at);
  107. CREATE TABLE public.identity_emergency_approvals (
  108. request_uid UUID NOT NULL REFERENCES public.identity_emergency_requests(uid),
  109. approver_uid UUID NOT NULL REFERENCES public.users(id), approved_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
  110. PRIMARY KEY (request_uid, approver_uid)
  111. );
  112. ALTER TABLE public.identity_sessions ADD CONSTRAINT fk_identity_session_emergency
  113. FOREIGN KEY(emergency_request_uid) REFERENCES public.identity_emergency_requests(uid);
  114. CREATE TABLE public.identity_audit_events (
  115. uid UUID PRIMARY KEY, event_type VARCHAR(80) NOT NULL, outcome VARCHAR(20) NOT NULL CHECK (outcome IN ('success','failure','denied','warning')),
  116. actor_uid UUID REFERENCES public.users(id), provider_uid UUID, enterprise_subject_digest CHAR(64),
  117. resource_type VARCHAR(80) NOT NULL, resource_uid VARCHAR(300), correlation_uid UUID,
  118. safe_detail JSONB NOT NULL DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
  119. CHECK (jsonb_typeof(safe_detail) = 'object')
  120. );
  121. CREATE INDEX idx_identity_audit_time ON public.identity_audit_events(created_at DESC, uid DESC);
  122. CREATE INDEX idx_identity_audit_actor ON public.identity_audit_events(actor_uid, created_at DESC);
  123. """
  124. )
  125. def downgrade() -> None:
  126. raise RuntimeError("Forward-only migration: enterprise identity and authentication evidence cannot be removed")