fix(ssf): 修复 Verification 状态迁移

为已执行旧版 0062 的环境补充不可变修复迁移,恢复 Stream 状态字段和 Verification challenge 表;同时为 timestamptz 参数增加显式类型,避免 pgx 在真实 PostgreSQL 中推断冲突。\n\n验证:pnpm test;pnpm lint;pnpm build;真实 PostgreSQL 集成测试通过。
This commit is contained in:
chengcheng 2026-07-16 11:54:01 +08:00
parent 2ccf041b35
commit 29f24222fd
4 changed files with 60 additions and 2 deletions

View File

@ -21,3 +21,21 @@ func TestSecurityEventIdempotencyRepairMigrationExists(t *testing.T) {
}
}
}
func TestSecurityEventVerificationStateRepairMigrationExists(t *testing.T) {
payload, err := os.ReadFile("../../migrations/0065_security_event_verification_state_repair.sql")
if err != nil {
t.Fatalf("read verification state repair migration: %v", err)
}
sql := string(payload)
for _, statement := range []string{
"ADD COLUMN IF NOT EXISTS stream_status",
"ADD COLUMN IF NOT EXISTS created_at",
"CREATE TABLE IF NOT EXISTS gateway_security_event_verification_challenges",
"CREATE INDEX IF NOT EXISTS idx_gateway_security_event_challenges_expiry",
} {
if !strings.Contains(sql, statement) {
t.Fatalf("verification state repair migration is missing %q", statement)
}
}
}

View File

@ -144,7 +144,7 @@ WHERE issuer=$1 AND audience=$2`, issuer, audience, stateHash, now)
}
if _, err := tx.Exec(ctx, `
INSERT INTO gateway_security_event_verification_challenges(issuer,audience,state_hash,created_at,expires_at)
VALUES($1,$2,$3,$4,$4 + interval '180 seconds')
VALUES($1,$2,$3,$4::timestamptz,$4::timestamptz + interval '180 seconds')
ON CONFLICT(issuer,audience,state_hash) DO NOTHING`, issuer, audience, stateHash, now); err != nil {
return err
}
@ -278,7 +278,7 @@ UPDATE gateway_security_event_stream_state
SET mode='introspection_fallback',fallback_since=COALESCE(fallback_since,$3),
consecutive_verifications=0,last_error_category='verification_stale',updated_at=now()
WHERE issuer=$1 AND audience=$2 AND mode <> 'introspection_fallback'
AND COALESCE(last_verification_at,created_at) < $3 - make_interval(secs => $4::double precision)`,
AND COALESCE(last_verification_at,created_at) < $3::timestamptz - make_interval(secs => $4::double precision)`,
issuer, audience, now, staleAfter.Seconds())
return err
}

View File

@ -51,6 +51,9 @@ func TestSecurityEventWatermarkVerificationAndFallbackLifecycle(t *testing.T) {
t.Fatal(err)
}
now := time.Now().UTC().Truncate(time.Second)
if err := db.AdvanceSecurityEventStreamState(ctx, issuer, audience, now, 180*time.Second); err != nil {
t.Fatalf("advance fresh stream state: %v", err)
}
evaluation, err := db.EvaluateOIDCSecurityEvent(ctx, issuer, audience, subjectIssuer, tenantID, subject, now, now, 180*time.Second)
if err != nil || !evaluation.RequireIntrospection || evaluation.Mode != "bootstrap" {
t.Fatalf("bootstrap evaluation=%#v error=%v", evaluation, err)

View File

@ -0,0 +1,37 @@
-- 0062 was released before the verification challenge table and the complete
-- stream-state columns were added. Installations that already recorded 0062
-- need a new immutable migration to repair that drift.
ALTER TABLE gateway_security_event_stream_state
ADD COLUMN IF NOT EXISTS stream_status text NOT NULL DEFAULT 'unknown',
ADD COLUMN IF NOT EXISTS created_at timestamptz NOT NULL DEFAULT now();
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conrelid = 'gateway_security_event_stream_state'::regclass
AND conname = 'gateway_security_event_stream_state_status'
) THEN
ALTER TABLE gateway_security_event_stream_state
ADD CONSTRAINT gateway_security_event_stream_state_status
CHECK (stream_status IN ('unknown','enabled','paused','disabled'));
END IF;
END
$$;
CREATE TABLE IF NOT EXISTS gateway_security_event_verification_challenges (
issuer text NOT NULL,
audience text NOT NULL,
state_hash bytea NOT NULL,
created_at timestamptz NOT NULL,
expires_at timestamptz NOT NULL,
PRIMARY KEY (issuer, audience, state_hash),
FOREIGN KEY (issuer, audience)
REFERENCES gateway_security_event_stream_state(issuer, audience) ON DELETE CASCADE,
CONSTRAINT gateway_security_event_verification_challenge_hash
CHECK (octet_length(state_hash) = 32)
);
CREATE INDEX IF NOT EXISTS idx_gateway_security_event_challenges_expiry
ON gateway_security_event_verification_challenges(expires_at);