修复 credentials_saved 状态无法恢复、配对与激活并发冲突,以及 SSF 和身份 Secret 生命周期不完整的问题。新增持久化协调器、取消与清理状态机、事务级并发门禁、受控 SSF 凭据交接、禁用后的延迟 Secret 清理,并对生产环境统一认证及 Discovery 端点强制 HTTPS。 验证:go test ./...;go test -race ./internal/auth ./internal/identity ./internal/identityruntime ./internal/securityevents ./internal/httpapi ./internal/store -count=1;go vet ./...;真实 PostgreSQL 并发及清理成功/冲突回滚测试;pnpm openapi。
90 lines
3.3 KiB
SQL
90 lines
3.3 KiB
SQL
SET LOCAL lock_timeout = '10s';
|
|
SET LOCAL statement_timeout = '60s';
|
|
|
|
-- 0071 was briefly released with only the singleton reservation lease fields.
|
|
-- Add the durable lifecycle columns without rewriting that already-applied
|
|
-- migration, then reconcile the singleton with the canonical outstanding
|
|
-- pairing (when one exists).
|
|
ALTER TABLE gateway_identity_pairing_start_reservation
|
|
ADD COLUMN IF NOT EXISTS state text,
|
|
ADD COLUMN IF NOT EXISTS revision_id uuid,
|
|
ADD COLUMN IF NOT EXISTS updated_at timestamptz;
|
|
|
|
UPDATE gateway_identity_pairing_start_reservation
|
|
SET state=CASE WHEN revision_id IS NULL THEN 'starting' ELSE 'paired' END,
|
|
updated_at=COALESCE(updated_at,created_at,now())
|
|
WHERE state IS NULL
|
|
OR state NOT IN ('starting','paired')
|
|
OR (state='starting' AND revision_id IS NOT NULL)
|
|
OR (state='paired' AND revision_id IS NULL)
|
|
OR updated_at IS NULL;
|
|
|
|
WITH canonical_pairing AS (
|
|
SELECT exchange.id AS attempt_id,
|
|
revision.id AS revision_id,
|
|
exchange.expires_at
|
|
FROM gateway_identity_configuration_revisions revision
|
|
JOIN gateway_identity_onboarding_exchanges exchange
|
|
ON exchange.revision_id=revision.id
|
|
WHERE revision.state IN ('draft','validated','failed')
|
|
AND NOT (exchange.status='cancelled' AND exchange.cleanup_status='completed')
|
|
ORDER BY revision.created_at DESC,exchange.created_at DESC
|
|
LIMIT 1
|
|
)
|
|
INSERT INTO gateway_identity_pairing_start_reservation(
|
|
singleton,attempt_id,state,revision_id,expires_at,updated_at
|
|
)
|
|
SELECT true,attempt_id,'paired',revision_id,expires_at,now()
|
|
FROM canonical_pairing
|
|
ON CONFLICT (singleton) DO UPDATE
|
|
SET attempt_id=EXCLUDED.attempt_id,
|
|
state=EXCLUDED.state,
|
|
revision_id=EXCLUDED.revision_id,
|
|
expires_at=EXCLUDED.expires_at,
|
|
updated_at=now();
|
|
|
|
ALTER TABLE gateway_identity_pairing_start_reservation
|
|
ALTER COLUMN state SET DEFAULT 'starting',
|
|
ALTER COLUMN state SET NOT NULL,
|
|
ALTER COLUMN updated_at SET DEFAULT now(),
|
|
ALTER COLUMN updated_at SET NOT NULL;
|
|
|
|
ALTER TABLE gateway_identity_pairing_start_reservation
|
|
DROP CONSTRAINT IF EXISTS gateway_identity_pairing_start_state_value_check,
|
|
DROP CONSTRAINT IF EXISTS gateway_identity_pairing_start_state_check;
|
|
|
|
ALTER TABLE gateway_identity_pairing_start_reservation
|
|
ADD CONSTRAINT gateway_identity_pairing_start_state_value_check
|
|
CHECK (state IN ('starting','paired')),
|
|
ADD CONSTRAINT gateway_identity_pairing_start_state_check CHECK (
|
|
(state='starting' AND revision_id IS NULL) OR
|
|
(state='paired' AND revision_id IS NOT NULL)
|
|
);
|
|
|
|
DO $migration$
|
|
BEGIN
|
|
IF NOT EXISTS (
|
|
SELECT 1
|
|
FROM pg_constraint
|
|
WHERE conrelid='gateway_identity_pairing_start_reservation'::regclass
|
|
AND contype='f'
|
|
AND pg_get_constraintdef(oid) LIKE 'FOREIGN KEY (revision_id)%'
|
|
) THEN
|
|
ALTER TABLE gateway_identity_pairing_start_reservation
|
|
ADD CONSTRAINT gateway_identity_pairing_start_revision_fk
|
|
FOREIGN KEY (revision_id)
|
|
REFERENCES gateway_identity_configuration_revisions(id)
|
|
ON DELETE RESTRICT;
|
|
END IF;
|
|
END
|
|
$migration$;
|
|
|
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_gateway_identity_pairing_start_revision_unique
|
|
ON gateway_identity_pairing_start_reservation(revision_id)
|
|
WHERE revision_id IS NOT NULL;
|
|
|
|
DROP INDEX IF EXISTS idx_gateway_identity_pairing_start_expiry;
|
|
|
|
CREATE INDEX idx_gateway_identity_pairing_start_expiry
|
|
ON gateway_identity_pairing_start_reservation(state,expires_at);
|