fix(ssf): 修复 Verification 状态迁移
为已执行旧版 0062 的环境补充不可变修复迁移,恢复 Stream 状态字段和 Verification challenge 表;同时为 timestamptz 参数增加显式类型,避免 pgx 在真实 PostgreSQL 中推断冲突。\n\n验证:pnpm test;pnpm lint;pnpm build;真实 PostgreSQL 集成测试通过。
This commit is contained in:
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ WHERE issuer=$1 AND audience=$2`, issuer, audience, stateHash, now)
|
|||||||
}
|
}
|
||||||
if _, err := tx.Exec(ctx, `
|
if _, err := tx.Exec(ctx, `
|
||||||
INSERT INTO gateway_security_event_verification_challenges(issuer,audience,state_hash,created_at,expires_at)
|
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 {
|
ON CONFLICT(issuer,audience,state_hash) DO NOTHING`, issuer, audience, stateHash, now); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -278,7 +278,7 @@ UPDATE gateway_security_event_stream_state
|
|||||||
SET mode='introspection_fallback',fallback_since=COALESCE(fallback_since,$3),
|
SET mode='introspection_fallback',fallback_since=COALESCE(fallback_since,$3),
|
||||||
consecutive_verifications=0,last_error_category='verification_stale',updated_at=now()
|
consecutive_verifications=0,last_error_category='verification_stale',updated_at=now()
|
||||||
WHERE issuer=$1 AND audience=$2 AND mode <> 'introspection_fallback'
|
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())
|
issuer, audience, now, staleAfter.Seconds())
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,6 +51,9 @@ func TestSecurityEventWatermarkVerificationAndFallbackLifecycle(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
now := time.Now().UTC().Truncate(time.Second)
|
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)
|
evaluation, err := db.EvaluateOIDCSecurityEvent(ctx, issuer, audience, subjectIssuer, tenantID, subject, now, now, 180*time.Second)
|
||||||
if err != nil || !evaluation.RequireIntrospection || evaluation.Mode != "bootstrap" {
|
if err != nil || !evaluation.RequireIntrospection || evaluation.Mode != "bootstrap" {
|
||||||
t.Fatalf("bootstrap evaluation=%#v error=%v", evaluation, err)
|
t.Fatalf("bootstrap evaluation=%#v error=%v", evaluation, err)
|
||||||
|
|||||||
@@ -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);
|
||||||
Reference in New Issue
Block a user