From 192e924dfbb2110f168bbbd34a906c4e5adfe343 Mon Sep 17 00:00:00 2001 From: chengcheng Date: Thu, 16 Jul 2026 12:06:43 +0800 Subject: [PATCH] =?UTF-8?q?fix(ssf):=20=E6=81=A2=E5=A4=8D=E5=81=A5?= =?UTF-8?q?=E5=BA=B7=E8=BF=9E=E6=8E=A5=E7=94=9F=E5=91=BD=E5=91=A8=E6=9C=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 连续两次有效 Verification 将 Stream 状态恢复为 push_healthy 时,同一事务同步把 degraded 连接恢复为 enabled、清除陈旧错误并推进版本,避免管理页永久误报降级。\n\n验证:真实 PostgreSQL 集成测试;pnpm test;pnpm lint;pnpm build;真实 Transmitter/Introspection 故障与恢复演练。 --- apps/api/internal/store/security_events.go | 12 ++++++++++ .../store/security_events_integration_test.go | 23 +++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/apps/api/internal/store/security_events.go b/apps/api/internal/store/security_events.go index bff029d..4ccac66 100644 --- a/apps/api/internal/store/security_events.go +++ b/apps/api/internal/store/security_events.go @@ -210,6 +210,18 @@ WHERE issuer=$1 AND audience=$2 AND stream_id=$3::uuid WHERE issuer=$1 AND audience=$2 AND state_hash=$3`, issuer, audience, stateHash); err != nil { return false, err } + if _, err := tx.Exec(ctx, ` +UPDATE gateway_security_event_connections connection +SET lifecycle_status='enabled',last_error_category=NULL,version=version+1,updated_at=now() +WHERE connection.transmitter_issuer=$1 AND connection.audience=$2 + AND connection.lifecycle_status='degraded' + AND EXISTS( + SELECT 1 FROM gateway_security_event_stream_state state + WHERE state.issuer=$1 AND state.audience=$2 + AND state.mode='push_healthy' AND state.stream_status='enabled' + )`, issuer, audience); err != nil { + return false, err + } return true, tx.Commit(ctx) } diff --git a/apps/api/internal/store/security_events_integration_test.go b/apps/api/internal/store/security_events_integration_test.go index 402737e..b84ed96 100644 --- a/apps/api/internal/store/security_events_integration_test.go +++ b/apps/api/internal/store/security_events_integration_test.go @@ -42,6 +42,7 @@ func TestSecurityEventWatermarkVerificationAndFallbackLifecycle(t *testing.T) { audience := "urn:easyai:ssf:receiver:" + uuid.NewString() streamID, tenantID, subject := uuid.NewString(), uuid.NewString(), uuid.NewString() t.Cleanup(func() { + _, _ = db.pool.Exec(context.Background(), `DELETE FROM gateway_security_event_connections WHERE transmitter_issuer=$1`, issuer) _, _ = db.pool.Exec(context.Background(), `DELETE FROM gateway_security_event_receipts WHERE issuer=$1`, issuer) _, _ = db.pool.Exec(context.Background(), `DELETE FROM gateway_oidc_revocation_watermarks WHERE issuer=$1`, subjectIssuer) _, _ = db.pool.Exec(context.Background(), `DELETE FROM gateway_security_event_stream_state WHERE issuer=$1`, issuer) @@ -132,4 +133,26 @@ func TestSecurityEventWatermarkVerificationAndFallbackLifecycle(t *testing.T) { if err != nil || !evaluation.RequireIntrospection || evaluation.Mode != "introspection_fallback" { t.Fatalf("fallback evaluation=%#v error=%v", evaluation, err) } + connectionID := uuid.NewString() + if _, err := db.pool.Exec(ctx, `DELETE FROM gateway_security_event_connections`); err != nil { + t.Fatal(err) + } + if _, err := db.pool.Exec(ctx, `INSERT INTO gateway_security_event_connections( + connection_id,transmitter_issuer,endpoint_url,audience,stream_id,credential_ref, + lifecycle_status,last_error_category,idempotency_key) + VALUES($1,$2,'https://gateway.test/api/v1/security-events/ssf',$3,$4,'test-secret-ref', + 'degraded','bootstrap_verification_stale',$5)`, connectionID, issuer, audience, streamID, uuid.NewString()); err != nil { + t.Fatal(err) + } + confirmAt("recovery-verification-one", now.Add(362*time.Second)) + confirmAt("recovery-verification-two", now.Add(363*time.Second)) + var lifecycle string + var lastError *string + if err := db.pool.QueryRow(ctx, `SELECT lifecycle_status,last_error_category + FROM gateway_security_event_connections WHERE connection_id=$1`, connectionID).Scan(&lifecycle, &lastError); err != nil { + t.Fatal(err) + } + if lifecycle != "enabled" || lastError != nil { + t.Fatalf("recovered connection lifecycle=%q lastError=%v", lifecycle, lastError) + } }