From 29f24222fda69265d40f8bda237bc17d9a6405bd Mon Sep 17 00:00:00 2001 From: chengcheng Date: Thu, 16 Jul 2026 11:54:01 +0800 Subject: [PATCH] =?UTF-8?q?fix(ssf):=20=E4=BF=AE=E5=A4=8D=20Verification?= =?UTF-8?q?=20=E7=8A=B6=E6=80=81=E8=BF=81=E7=A7=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 为已执行旧版 0062 的环境补充不可变修复迁移,恢复 Stream 状态字段和 Verification challenge 表;同时为 timestamptz 参数增加显式类型,避免 pgx 在真实 PostgreSQL 中推断冲突。\n\n验证:pnpm test;pnpm lint;pnpm build;真实 PostgreSQL 集成测试通过。 --- apps/api/cmd/migrate/main_test.go | 18 +++++++++ apps/api/internal/store/security_events.go | 4 +- .../store/security_events_integration_test.go | 3 ++ ...curity_event_verification_state_repair.sql | 37 +++++++++++++++++++ 4 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 apps/api/migrations/0065_security_event_verification_state_repair.sql diff --git a/apps/api/cmd/migrate/main_test.go b/apps/api/cmd/migrate/main_test.go index 8cd63c8..e55c714 100644 --- a/apps/api/cmd/migrate/main_test.go +++ b/apps/api/cmd/migrate/main_test.go @@ -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) + } + } +} diff --git a/apps/api/internal/store/security_events.go b/apps/api/internal/store/security_events.go index 5ae18ed..bff029d 100644 --- a/apps/api/internal/store/security_events.go +++ b/apps/api/internal/store/security_events.go @@ -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 } diff --git a/apps/api/internal/store/security_events_integration_test.go b/apps/api/internal/store/security_events_integration_test.go index f2d9952..402737e 100644 --- a/apps/api/internal/store/security_events_integration_test.go +++ b/apps/api/internal/store/security_events_integration_test.go @@ -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) diff --git a/apps/api/migrations/0065_security_event_verification_state_repair.sql b/apps/api/migrations/0065_security_event_verification_state_repair.sql new file mode 100644 index 0000000..fd9afcf --- /dev/null +++ b/apps/api/migrations/0065_security_event_verification_state_repair.sql @@ -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);