mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 17:12:04 +00:00
aab8b9f13f
Three new idempotent transactional migrations that materialize the
Phase 1 domain types into Postgres tables. Repository implementations
+ integration tests land as Phase 2b in the next commit.
migrations/000034_oidc_providers.up.sql:
oidc_providers table with the full OIDCProvider field set
(issuer_url + client_id + client_secret_encrypted v2 blob +
redirect_uri + groups_claim_path + groups_claim_format +
fetch_userinfo + scopes[] + allowed_email_domains[] +
iat_window_seconds + jwks_cache_ttl_seconds + tenant_id).
group_role_mappings table linking provider+group_name to role_id.
Closed-enum CHECK on groups_claim_format ('string-array' or
'json-path').
Defense-in-depth bounds CHECKs on iat_window_seconds (1..600) and
jwks_cache_ttl_seconds (>= 60); app-layer Validate() also
enforces these.
ON DELETE CASCADE on group_role_mappings.provider_id so deleting a
provider cleans up its mappings.
ON DELETE RESTRICT on group_role_mappings.role_id so an in-use role
can't be silently dropped.
migrations/000035_sessions.up.sql:
session_signing_keys table with key_material_encrypted v2 blob +
retired_at nullable + the retired-after-created CHECK.
Partial index on (tenant_id, created_at DESC) WHERE retired_at IS
NULL backs the GetActive hot path.
sessions table covers BOTH the post-login row (1h-idle/8h-absolute
cookie lifecycle) AND the Phase 5 pre-login row (10-minute TTL,
is_pre_login=true). csrf_token_hash holds the SHA-256 of the
CSRF token plaintext (the plaintext lives in a separate
JS-readable cookie, hashed here so a DB-read leak can't replay).
Two CHECK constraints pin the expiry order (absolute > idle, idle >
created); these match the Phase 1 domain Validate() pre-write
invariants but enforce them at the DB layer too so direct SQL
inserts can't silently land malformed rows.
Partial indexes on actor_id (active sessions only), the active
session lookup, the pre-login GC sweep (created_at), and the
absolute-expired GC sweep (absolute_expires_at) cover the four
hot paths Phase 4's service consumes.
ON DELETE RESTRICT on sessions.signing_key_id so a signing key
referenced by an active session can't be dropped (the retention
window keeps retired keys valid; full purge waits until every
session signed under that key has expired).
migrations/000036_users.up.sql:
users table for federated-human identity (per-(provider, subject)
tuple via UNIQUE constraint, not global - identity is per-IdP by
design).
webauthn_credentials JSONB DEFAULT '[]' reserved for v3 (Decision
12); Bundle 2 always stores [].
Email index for the GUI's "find user by email" surface (not unique
because the same email can appear in multiple providers per the
per-IdP identity model).
ON DELETE RESTRICT on users.oidc_provider_id keeps Phase 3's "delete
provider only when no users authenticated via it" rule enforced
at the DB layer; the OIDCProviderRepository.Delete impl will
translate SQLSTATE 23503 into a 409 sentinel.
All three migrations:
Wrapped in BEGIN/COMMIT so partial-fail leaves no half-state.
IF NOT EXISTS / IF EXISTS / ON CONFLICT DO NOTHING for idempotency
(the certctl-server boot path applies every migration on every
start per CLAUDE.md "Idempotent migrations" architecture rule).
TIMESTAMPTZ for time columns (no TIMESTAMP WITHOUT TIME ZONE).
TEXT primary keys with prefixes per CLAUDE.md "Architecture
Decisions" (op- / grm- / sk- / ses- / u-).
Multi-tenant ready: tenant_id column with DEFAULT 't-default' on
every row, FK to tenants(id) ON DELETE CASCADE. Bundle 2 ships
single-tenant; managed-service activation adds tenants without a
schema migration.
Down migrations exist in lockstep, drop tables in FK-safe order
(group_role_mappings -> oidc_providers; sessions ->
session_signing_keys; users alone). Down-migrations are destructive;
docstrings call this out.
Verifications:
Migration count: ls migrations/*.up.sql | wc -l = 36 (33 from
Bundle 1 + 3 new).
BEGIN/COMMIT pair counts: each new migration is 1:1.
No Docker in this sandbox, so the migrations are not applied
end-to-end here; CI's testcontainers harness runs them via
postgres.RunMigrations on every push. Phase 2b's repository
integration tests will exercise the schema against Postgres 16
Alpine.
17 lines
625 B
PL/PgSQL
17 lines
625 B
PL/PgSQL
-- 000036_users.down.sql
|
|
-- Reverses 000036_users.up.sql. Destructive: every federated-human
|
|
-- user record is dropped. Operators MUST take a backup before
|
|
-- running this; SSO logins fail until a fresh login re-creates rows.
|
|
--
|
|
-- The actor_roles table (Bundle 1's RBAC) does NOT cascade-delete
|
|
-- here because actor_roles.actor_id is a TEXT column without an FK
|
|
-- to users. Down-migrating users orphans actor_roles rows whose
|
|
-- actor_id matches a deleted user; those rows become unreachable
|
|
-- via the normal UI but are not auto-cleaned.
|
|
BEGIN;
|
|
|
|
DROP INDEX IF EXISTS idx_users_email;
|
|
DROP TABLE IF EXISTS users;
|
|
|
|
COMMIT;
|