fix(ssf): 支持断开后安全重连
重建 Stream 时仅重置流级 Verification 状态,保留历史收据和撤销水位;允许显式恢复失败连接,并在进程重启后按持久化时间继续 bootstrap。\n\n已通过 pnpm test、pnpm lint、pnpm build、go vet、PostgreSQL 集成测试以及本地真实断开重连和停机重试验收。
This commit is contained in:
@@ -108,19 +108,38 @@ WHERE session.gateway_user_id = gateway_user.id
|
||||
}
|
||||
|
||||
func (s *Store) EnsureSecurityEventStreamState(ctx context.Context, issuer, audience, streamID string) error {
|
||||
tag, err := s.pool.Exec(ctx, `
|
||||
INSERT INTO gateway_security_event_stream_state(issuer,audience,stream_id,mode)
|
||||
VALUES($1,$2,$3::uuid,'bootstrap')
|
||||
ON CONFLICT(issuer,audience) DO UPDATE
|
||||
SET stream_id=EXCLUDED.stream_id,updated_at=now()
|
||||
WHERE gateway_security_event_stream_state.stream_id=EXCLUDED.stream_id`, issuer, audience, streamID)
|
||||
tx, err := s.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if tag.RowsAffected() == 0 {
|
||||
return errors.New("security event stream id cannot be changed")
|
||||
defer func() { _ = tx.Rollback(ctx) }()
|
||||
if _, err := tx.Exec(ctx, `
|
||||
INSERT INTO gateway_security_event_stream_state(issuer,audience,stream_id,mode)
|
||||
VALUES($1,$2,$3::uuid,'bootstrap')
|
||||
ON CONFLICT(issuer,audience) DO NOTHING`, issuer, audience, streamID); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
var currentStreamID string
|
||||
if err := tx.QueryRow(ctx, `SELECT stream_id::text FROM gateway_security_event_stream_state
|
||||
WHERE issuer=$1 AND audience=$2 FOR UPDATE`, issuer, audience).Scan(¤tStreamID); err != nil {
|
||||
return err
|
||||
}
|
||||
if currentStreamID != streamID {
|
||||
if _, err := tx.Exec(ctx, `DELETE FROM gateway_security_event_verification_challenges
|
||||
WHERE issuer=$1 AND audience=$2`, issuer, audience); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := tx.Exec(ctx, `
|
||||
UPDATE gateway_security_event_stream_state
|
||||
SET stream_id=$3::uuid,mode='bootstrap',stream_status='unknown',
|
||||
last_verification_at=NULL,bootstrap_until=NULL,consecutive_verifications=0,
|
||||
pending_state_hash=NULL,pending_state_created_at=NULL,fallback_since=NULL,
|
||||
last_error_category=NULL,created_at=now(),updated_at=now()
|
||||
WHERE issuer=$1 AND audience=$2`, issuer, audience, streamID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return tx.Commit(ctx)
|
||||
}
|
||||
|
||||
func (s *Store) BeginSecurityEventVerification(ctx context.Context, issuer, audience string, stateHash []byte, now time.Time) error {
|
||||
|
||||
@@ -155,4 +155,31 @@ func TestSecurityEventWatermarkVerificationAndFallbackLifecycle(t *testing.T) {
|
||||
if lifecycle != "enabled" || lastError != nil {
|
||||
t.Fatalf("recovered connection lifecycle=%q lastError=%v", lifecycle, lastError)
|
||||
}
|
||||
replacementStreamID := uuid.NewString()
|
||||
if err := db.EnsureSecurityEventStreamState(ctx, issuer, audience, replacementStreamID); err != nil {
|
||||
t.Fatalf("rebind retained receiver state: %v", err)
|
||||
}
|
||||
var reboundStreamID, reboundMode, reboundStatus string
|
||||
var reboundVerification *time.Time
|
||||
if err := db.pool.QueryRow(ctx, `SELECT stream_id::text,mode,stream_status,last_verification_at
|
||||
FROM gateway_security_event_stream_state WHERE issuer=$1 AND audience=$2`, issuer, audience).
|
||||
Scan(&reboundStreamID, &reboundMode, &reboundStatus, &reboundVerification); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if reboundStreamID != replacementStreamID || reboundMode != "bootstrap" || reboundStatus != "unknown" || reboundVerification != nil {
|
||||
t.Fatalf("rebound state stream=%q mode=%q status=%q verification=%v",
|
||||
reboundStreamID, reboundMode, reboundStatus, reboundVerification)
|
||||
}
|
||||
var retainedReceipts, retainedWatermarks int
|
||||
if err := db.pool.QueryRow(ctx, `SELECT count(*) FROM gateway_security_event_receipts
|
||||
WHERE issuer=$1`, issuer).Scan(&retainedReceipts); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := db.pool.QueryRow(ctx, `SELECT count(*) FROM gateway_oidc_revocation_watermarks
|
||||
WHERE issuer=$1 AND tenant_id=$2 AND subject=$3`, subjectIssuer, tenantID, subject).Scan(&retainedWatermarks); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if retainedReceipts == 0 || retainedWatermarks != 1 {
|
||||
t.Fatalf("rebind discarded security history receipts=%d watermarks=%d", retainedReceipts, retainedWatermarks)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user