Files
qinglong/packages/ql3-cluster-postgres/src/migrations/pg-0006-identity-credential-administration.ts
T

317 lines
11 KiB
TypeScript

import { definePostgresSqlMigration } from './sqlMigration';
export const POSTGRESQL_IDENTITY_SUBJECT_MUTATION_TABLE =
'identity_subject_mutations';
export const POSTGRESQL_API_CREDENTIAL_MUTATION_TABLE =
'api_credential_mutations';
export const pg0006IdentityCredentialAdministrationMigration =
definePostgresSqlMigration({
id: 'pg-0006-identity-credential-administration',
statements: [
`
CREATE TABLE "ql3"."${POSTGRESQL_IDENTITY_SUBJECT_MUTATION_TABLE}" (
mutation_id uuid PRIMARY KEY,
operation varchar(16) NOT NULL
CONSTRAINT ql3_identity_subject_mutations_operation_check
CHECK (operation IN ('import', 'register', 'enable', 'disable')),
subject_type varchar(32) NOT NULL
CONSTRAINT ql3_identity_subject_mutations_subject_type_check
CHECK (subject_type IN ('user', 'api_app', 'mcp_client', 'agent', 'system', 'worker')),
subject_id varchar(255) NOT NULL
CONSTRAINT ql3_identity_subject_mutations_subject_id_check
CHECK (char_length(subject_id) >= 1),
subject_version integer NOT NULL
CONSTRAINT ql3_identity_subject_mutations_version_check
CHECK (subject_version BETWEEN 1 AND 2147483647),
expected_previous_version integer NOT NULL
CONSTRAINT ql3_identity_subject_mutations_previous_version_check
CHECK (expected_previous_version BETWEEN 0 AND 2147483646),
status varchar(16) NOT NULL
CONSTRAINT ql3_identity_subject_mutations_status_check
CHECK (status IN ('active', 'disabled')),
changed_by_type varchar(32) NOT NULL
CONSTRAINT ql3_identity_subject_mutations_changed_by_type_check
CHECK (changed_by_type IN ('user', 'system')),
changed_by_id varchar(255) NOT NULL
CONSTRAINT ql3_identity_subject_mutations_changed_by_id_check
CHECK (char_length(changed_by_id) >= 1),
audit_event_id uuid NOT NULL,
identity_created_at_ms bigint NOT NULL
CONSTRAINT ql3_identity_subject_mutations_identity_created_at_check
CHECK (identity_created_at_ms >= 0),
created_at_ms bigint NOT NULL
CONSTRAINT ql3_identity_subject_mutations_created_at_check
CHECK (created_at_ms >= 0),
CONSTRAINT ql3_identity_subject_mutations_version_fence_check
CHECK (subject_version = expected_previous_version + 1),
CONSTRAINT ql3_identity_subject_mutations_transition_check
CHECK (
operation = 'import'
OR (operation = 'register' AND expected_previous_version = 0 AND status = 'active')
OR (operation = 'enable' AND expected_previous_version >= 1 AND status = 'active')
OR (operation = 'disable' AND expected_previous_version >= 1 AND status = 'disabled')
),
CONSTRAINT ql3_identity_subject_mutations_audit_identity_check
CHECK (mutation_id = audit_event_id),
CONSTRAINT ql3_identity_subject_mutations_subject_fk
FOREIGN KEY (subject_type, subject_id)
REFERENCES "ql3"."identity_subjects" (subject_type, subject_id)
ON DELETE RESTRICT,
CONSTRAINT ql3_identity_subject_mutations_audit_fk
FOREIGN KEY (audit_event_id)
REFERENCES "ql3"."security_audit_events" (event_id)
ON DELETE RESTRICT,
CONSTRAINT ql3_identity_subject_mutations_subject_version_uidx
UNIQUE (subject_type, subject_id, subject_version)
)
`.trim(),
`
CREATE INDEX ql3_identity_subject_mutations_actor_idx
ON "ql3"."${POSTGRESQL_IDENTITY_SUBJECT_MUTATION_TABLE}"
(changed_by_type, changed_by_id, created_at_ms DESC, mutation_id)
`.trim(),
`
WITH source AS (
SELECT
subject.*,
md5('ql3-identity-import:' || subject.subject_type || ':' || subject.subject_id) AS digest
FROM "ql3"."identity_subjects" AS subject
), imported AS (
SELECT
source.*,
(
substr(digest, 1, 8) || '-' || substr(digest, 9, 4) || '-4' ||
substr(digest, 14, 3) || '-8' || substr(digest, 18, 3) || '-' ||
substr(digest, 21, 12)
)::uuid AS mutation_id
FROM source
)
INSERT INTO "ql3"."security_audit_events" (
event_id, request_id, operation_id, project_id, subject_type, subject_id,
authentication_id, outcome, reasons, project_version, binding_version,
occurred_at_ms
)
SELECT
mutation_id,
'migration:' || mutation_id::text,
'identity.import',
NULL,
'system',
'pg-0006',
'migration:pg-0006',
'allowed',
'["identity_admin"]'::jsonb,
NULL,
NULL,
created_at_ms
FROM imported
`.trim(),
`
WITH source AS (
SELECT
subject.*,
md5('ql3-identity-import:' || subject.subject_type || ':' || subject.subject_id) AS digest
FROM "ql3"."identity_subjects" AS subject
), imported AS (
SELECT
source.*,
(
substr(digest, 1, 8) || '-' || substr(digest, 9, 4) || '-4' ||
substr(digest, 14, 3) || '-8' || substr(digest, 18, 3) || '-' ||
substr(digest, 21, 12)
)::uuid AS mutation_id
FROM source
)
INSERT INTO "ql3"."${POSTGRESQL_IDENTITY_SUBJECT_MUTATION_TABLE}" (
mutation_id, operation, subject_type, subject_id, subject_version,
expected_previous_version, status, changed_by_type, changed_by_id,
audit_event_id, identity_created_at_ms, created_at_ms
)
SELECT
mutation_id,
'import',
subject_type,
subject_id,
version,
version - 1,
status,
'system',
'pg-0006',
mutation_id,
created_at_ms,
created_at_ms
FROM imported
`.trim(),
`
CREATE TABLE "ql3"."${POSTGRESQL_API_CREDENTIAL_MUTATION_TABLE}" (
mutation_id uuid PRIMARY KEY,
operation varchar(16) NOT NULL
CONSTRAINT ql3_api_credential_mutations_operation_check
CHECK (operation IN ('import', 'issue', 'rotate', 'revoke')),
credential_id varchar(64) NOT NULL
CONSTRAINT ql3_api_credential_mutations_id_check
CHECK (credential_id ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,63}$'),
credential_version integer NOT NULL
CONSTRAINT ql3_api_credential_mutations_version_check
CHECK (credential_version BETWEEN 1 AND 2147483647),
expected_previous_version integer NOT NULL
CONSTRAINT ql3_api_credential_mutations_previous_version_check
CHECK (expected_previous_version BETWEEN 0 AND 2147483646),
state varchar(16) NOT NULL
CONSTRAINT ql3_api_credential_mutations_state_check
CHECK (state IN ('active', 'revoked')),
subject_type varchar(32) NOT NULL
CONSTRAINT ql3_api_credential_mutations_subject_type_check
CHECK (subject_type IN ('user', 'api_app', 'mcp_client', 'agent')),
subject_id varchar(255) NOT NULL
CONSTRAINT ql3_api_credential_mutations_subject_id_check
CHECK (char_length(subject_id) >= 1),
subject_status varchar(16) NOT NULL
CONSTRAINT ql3_api_credential_mutations_subject_status_check
CHECK (subject_status IN ('active', 'disabled')),
changed_by_type varchar(32) NOT NULL
CONSTRAINT ql3_api_credential_mutations_changed_by_type_check
CHECK (changed_by_type IN ('user', 'system')),
changed_by_id varchar(255) NOT NULL
CONSTRAINT ql3_api_credential_mutations_changed_by_id_check
CHECK (char_length(changed_by_id) >= 1),
audit_event_id uuid NOT NULL,
created_at_ms bigint NOT NULL
CONSTRAINT ql3_api_credential_mutations_created_at_check
CHECK (created_at_ms >= 0),
CONSTRAINT ql3_api_credential_mutations_version_fence_check
CHECK (credential_version = expected_previous_version + 1),
CONSTRAINT ql3_api_credential_mutations_transition_check
CHECK (
operation = 'import'
OR (operation = 'issue' AND expected_previous_version = 0 AND state = 'active')
OR (operation = 'rotate' AND expected_previous_version >= 1 AND state = 'active')
OR (operation = 'revoke' AND expected_previous_version >= 1 AND state = 'revoked')
),
CONSTRAINT ql3_api_credential_mutations_audit_identity_check
CHECK (mutation_id = audit_event_id),
CONSTRAINT ql3_api_credential_mutations_credential_fk
FOREIGN KEY (credential_id, credential_version)
REFERENCES "ql3"."api_credentials" (credential_id, version)
ON DELETE RESTRICT,
CONSTRAINT ql3_api_credential_mutations_subject_fk
FOREIGN KEY (subject_type, subject_id)
REFERENCES "ql3"."identity_subjects" (subject_type, subject_id)
ON DELETE RESTRICT,
CONSTRAINT ql3_api_credential_mutations_audit_fk
FOREIGN KEY (audit_event_id)
REFERENCES "ql3"."security_audit_events" (event_id)
ON DELETE RESTRICT,
CONSTRAINT ql3_api_credential_mutations_credential_version_uidx
UNIQUE (credential_id, credential_version)
)
`.trim(),
`
CREATE INDEX ql3_api_credential_mutations_actor_idx
ON "ql3"."${POSTGRESQL_API_CREDENTIAL_MUTATION_TABLE}"
(changed_by_type, changed_by_id, created_at_ms DESC, mutation_id)
`.trim(),
`
WITH source AS (
SELECT
credential.*,
md5('ql3-credential-import:' || credential.credential_id || ':' || credential.version::text) AS digest
FROM "ql3"."api_credentials" AS credential
), imported AS (
SELECT
source.*,
(
substr(digest, 1, 8) || '-' || substr(digest, 9, 4) || '-4' ||
substr(digest, 14, 3) || '-8' || substr(digest, 18, 3) || '-' ||
substr(digest, 21, 12)
)::uuid AS mutation_id
FROM source
)
INSERT INTO "ql3"."security_audit_events" (
event_id, request_id, operation_id, project_id, subject_type, subject_id,
authentication_id, outcome, reasons, project_version, binding_version,
occurred_at_ms
)
SELECT
mutation_id,
'migration:' || mutation_id::text,
'credential.import',
NULL,
'system',
'pg-0006',
'migration:pg-0006',
'allowed',
'["credential_admin"]'::jsonb,
NULL,
NULL,
created_at_ms
FROM imported
`.trim(),
`
WITH source AS (
SELECT
credential.*,
md5('ql3-credential-import:' || credential.credential_id || ':' || credential.version::text) AS digest
FROM "ql3"."api_credentials" AS credential
), imported AS (
SELECT
source.*,
(
substr(digest, 1, 8) || '-' || substr(digest, 9, 4) || '-4' ||
substr(digest, 14, 3) || '-8' || substr(digest, 18, 3) || '-' ||
substr(digest, 21, 12)
)::uuid AS mutation_id
FROM source
)
INSERT INTO "ql3"."${POSTGRESQL_API_CREDENTIAL_MUTATION_TABLE}" (
mutation_id, operation, credential_id, credential_version,
expected_previous_version, state, subject_type, subject_id,
subject_status, changed_by_type, changed_by_id, audit_event_id, created_at_ms
)
SELECT
mutation_id,
'import',
credential_id,
version,
version - 1,
state,
subject_type,
subject_id,
(
SELECT identity.status
FROM "ql3"."identity_subjects" AS identity
WHERE identity.subject_type = imported.subject_type
AND identity.subject_id = imported.subject_id
),
'system',
'pg-0006',
mutation_id,
created_at_ms
FROM imported
`.trim(),
`
DO $ql3$
BEGIN
UPDATE "ql3"."schema_capabilities"
SET contract_version = 5,
migration_id = 'pg-0006-identity-credential-administration',
capabilities = '{"api_credential":1,"api_credential_admin":1,"identity_admin":1,"project_policy":1,"run_core":1,"run_retry_policy":1,"security_audit":1,"security_audit_query":1}'::jsonb,
updated_at_ms = floor(
extract(epoch FROM transaction_timestamp()) * 1000
)::bigint
WHERE contract_name = 'control-core'
AND contract_version = 4
AND migration_id = 'pg-0005-api-credential-security-audit'
AND capabilities = '{"api_credential":1,"project_policy":1,"run_core":1,"run_retry_policy":1,"security_audit":1}'::jsonb;
IF NOT FOUND THEN
RAISE EXCEPTION 'control-core capability is not at version 4'
USING ERRCODE = 'check_violation';
END IF;
END
$ql3$
`.trim(),
],
});