fix(identity): 完善统一认证配对恢复与安全退役
修复 credentials_saved 状态无法恢复、配对与激活并发冲突,以及 SSF 和身份 Secret 生命周期不完整的问题。新增持久化协调器、取消与清理状态机、事务级并发门禁、受控 SSF 凭据交接、禁用后的延迟 Secret 清理,并对生产环境统一认证及 Discovery 端点强制 HTTPS。 验证:go test ./...;go test -race ./internal/auth ./internal/identity ./internal/identityruntime ./internal/securityevents ./internal/httpapi ./internal/store -count=1;go vet ./...;真实 PostgreSQL 并发及清理成功/冲突回滚测试;pnpm openapi。
This commit is contained in:
@@ -2,6 +2,7 @@ package store
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"time"
|
||||
@@ -13,6 +14,7 @@ import (
|
||||
var (
|
||||
ErrSecurityEventConnectionNotFound = errors.New("security event connection not found")
|
||||
ErrSecurityEventConnectionConflict = errors.New("security event connection conflicts with current state")
|
||||
ErrIdentitySecretCleanupConflict = errors.New("identity secret cleanup conflicts with current state")
|
||||
)
|
||||
|
||||
type SecurityEventConnection struct {
|
||||
@@ -44,6 +46,121 @@ type SecurityEventConnectionIdempotency struct {
|
||||
Response json.RawMessage
|
||||
}
|
||||
|
||||
type IdentitySecretCleanupClaim struct {
|
||||
Reference string
|
||||
ClaimToken string
|
||||
LeaseExpiresAt time.Time
|
||||
}
|
||||
|
||||
func (s *Store) QueueIdentitySecretCleanup(ctx context.Context, reference string, notBefore time.Time) error {
|
||||
tag, err := s.pool.Exec(ctx, `INSERT INTO gateway_identity_secret_cleanup_queue(secret_ref,not_before)
|
||||
VALUES($1,$2) ON CONFLICT(secret_ref) DO UPDATE
|
||||
SET not_before=LEAST(gateway_identity_secret_cleanup_queue.not_before,EXCLUDED.not_before),updated_at=now()
|
||||
WHERE gateway_identity_secret_cleanup_queue.status='pending'`, reference, notBefore)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if tag.RowsAffected() != 1 {
|
||||
return ErrIdentitySecretCleanupConflict
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Store) RemovePendingIdentitySecretCleanup(ctx context.Context, reference string) (bool, error) {
|
||||
tag, err := s.pool.Exec(ctx, `DELETE FROM gateway_identity_secret_cleanup_queue
|
||||
WHERE secret_ref=$1 AND status='pending'`, reference)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return tag.RowsAffected() == 1, nil
|
||||
}
|
||||
|
||||
func (s *Store) ClaimIdentitySecretCleanups(ctx context.Context, limit int, lease time.Duration) ([]IdentitySecretCleanupClaim, error) {
|
||||
if limit <= 0 || limit > 1000 {
|
||||
limit = 100
|
||||
}
|
||||
if lease <= 0 || lease > time.Hour {
|
||||
lease = 2 * time.Minute
|
||||
}
|
||||
leaseSeconds := int64((lease + time.Second - 1) / time.Second)
|
||||
rows, err := s.pool.Query(ctx, `WITH candidates AS MATERIALIZED (
|
||||
SELECT secret_ref FROM gateway_identity_secret_cleanup_queue
|
||||
WHERE (status='pending' AND not_before <= now())
|
||||
OR (status='claimed' AND lease_expires_at <= now())
|
||||
ORDER BY COALESCE(lease_expires_at,not_before),created_at
|
||||
FOR UPDATE SKIP LOCKED
|
||||
LIMIT $1
|
||||
)
|
||||
UPDATE gateway_identity_secret_cleanup_queue queued
|
||||
SET status='claimed',claim_token=gen_random_uuid(),
|
||||
lease_expires_at=now()+make_interval(secs => $2::int),updated_at=now()
|
||||
FROM candidates
|
||||
WHERE queued.secret_ref=candidates.secret_ref
|
||||
RETURNING queued.secret_ref,queued.claim_token::text,queued.lease_expires_at`, limit, leaseSeconds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
claims := make([]IdentitySecretCleanupClaim, 0)
|
||||
for rows.Next() {
|
||||
var claim IdentitySecretCleanupClaim
|
||||
if err := rows.Scan(&claim.Reference, &claim.ClaimToken, &claim.LeaseExpiresAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
claims = append(claims, claim)
|
||||
}
|
||||
return claims, rows.Err()
|
||||
}
|
||||
|
||||
func (s *Store) CompleteIdentitySecretCleanup(ctx context.Context, reference, claimToken string) (bool, error) {
|
||||
if _, err := uuid.Parse(claimToken); err != nil {
|
||||
return false, ErrIdentitySecretCleanupConflict
|
||||
}
|
||||
tag, err := s.pool.Exec(ctx, `DELETE FROM gateway_identity_secret_cleanup_queue
|
||||
WHERE secret_ref=$1 AND status='claimed' AND claim_token=$2::uuid`, reference, claimToken)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return tag.RowsAffected() == 1, nil
|
||||
}
|
||||
|
||||
func adoptPendingIdentitySecrets(ctx context.Context, tx pgx.Tx, references ...string) error {
|
||||
unique := make(map[string]struct{}, len(references))
|
||||
for _, reference := range references {
|
||||
if reference == "" {
|
||||
return ErrIdentitySecretCleanupConflict
|
||||
}
|
||||
unique[reference] = struct{}{}
|
||||
}
|
||||
values := make([]string, 0, len(unique))
|
||||
for reference := range unique {
|
||||
values = append(values, reference)
|
||||
}
|
||||
tag, err := tx.Exec(ctx, `DELETE FROM gateway_identity_secret_cleanup_queue
|
||||
WHERE secret_ref=ANY($1) AND status='pending'`, values)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if tag.RowsAffected() != int64(len(values)) {
|
||||
return ErrIdentitySecretCleanupConflict
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func queueIdentitySecretCleanupTx(ctx context.Context, tx pgx.Tx, reference string, notBefore time.Time) error {
|
||||
tag, err := tx.Exec(ctx, `INSERT INTO gateway_identity_secret_cleanup_queue(secret_ref,not_before)
|
||||
VALUES($1,$2) ON CONFLICT(secret_ref) DO UPDATE
|
||||
SET not_before=LEAST(gateway_identity_secret_cleanup_queue.not_before,EXCLUDED.not_before),updated_at=now()
|
||||
WHERE gateway_identity_secret_cleanup_queue.status='pending'`, reference, notBefore)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if tag.RowsAffected() != 1 {
|
||||
return ErrIdentitySecretCleanupConflict
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Store) SecurityEventConnectionIdempotency(ctx context.Context, operation, key string) (SecurityEventConnectionIdempotency, error) {
|
||||
var value SecurityEventConnectionIdempotency
|
||||
err := s.pool.QueryRow(ctx, `SELECT request_hash,response FROM gateway_security_event_connection_idempotency
|
||||
@@ -64,20 +181,22 @@ VALUES($1,$2,$3,$4::jsonb) ON CONFLICT(operation,idempotency_key) DO NOTHING`, o
|
||||
}
|
||||
|
||||
func (s *Store) CreateSecurityEventConnection(ctx context.Context, input CreateSecurityEventConnectionInput) (SecurityEventConnection, error) {
|
||||
var value SecurityEventConnection
|
||||
err := s.pool.QueryRow(ctx, `
|
||||
tx, err := s.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return SecurityEventConnection{}, err
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
_, err = tx.Exec(ctx, `
|
||||
INSERT INTO gateway_security_event_connections(
|
||||
connection_id,transmitter_issuer,endpoint_url,credential_ref,management_client_id,management_credential_ref,lifecycle_status,idempotency_key
|
||||
) VALUES($1::uuid,$2,$3,$4,$5,$6,'connecting',$7)
|
||||
RETURNING connection_id::text,transmitter_issuer,endpoint_url,audience,stream_id::text,credential_ref,
|
||||
next_credential_ref,management_client_id,management_credential_ref,lifecycle_status,version,last_error_category,idempotency_key,created_at,updated_at`,
|
||||
`,
|
||||
input.ConnectionID, input.TransmitterIssuer, input.EndpointURL, input.CredentialRef,
|
||||
input.ManagementClientID, input.ManagementCredentialRef, input.IdempotencyKey,
|
||||
).Scan(&value.ConnectionID, &value.TransmitterIssuer, &value.EndpointURL, &value.Audience, &value.StreamID,
|
||||
&value.CredentialRef, &value.NextCredentialRef, &value.ManagementClientID, &value.ManagementCredentialRef, &value.LifecycleStatus, &value.Version,
|
||||
&value.LastErrorCategory, &value.IdempotencyKey, &value.CreatedAt, &value.UpdatedAt)
|
||||
)
|
||||
if err != nil {
|
||||
if isUniqueViolation(err) {
|
||||
_ = tx.Rollback(ctx)
|
||||
existing, getErr := s.SecurityEventConnection(ctx)
|
||||
if getErr == nil && existing.IdempotencyKey == input.IdempotencyKey && existing.TransmitterIssuer == input.TransmitterIssuer {
|
||||
return existing, nil
|
||||
@@ -86,7 +205,13 @@ RETURNING connection_id::text,transmitter_issuer,endpoint_url,audience,stream_id
|
||||
}
|
||||
return SecurityEventConnection{}, err
|
||||
}
|
||||
return value, nil
|
||||
if err := adoptPendingIdentitySecrets(ctx, tx, input.CredentialRef, input.ManagementCredentialRef); err != nil {
|
||||
return SecurityEventConnection{}, err
|
||||
}
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return SecurityEventConnection{}, err
|
||||
}
|
||||
return s.SecurityEventConnection(ctx)
|
||||
}
|
||||
|
||||
func (s *Store) SecurityEventConnection(ctx context.Context) (SecurityEventConnection, error) {
|
||||
@@ -126,49 +251,56 @@ WHERE connection_id=$1::uuid AND version=$5`, connectionID, audience, streamID,
|
||||
return s.SecurityEventConnection(ctx)
|
||||
}
|
||||
|
||||
func (s *Store) SetSecurityEventManagementCredential(ctx context.Context, connectionID, clientID, reference string) (SecurityEventConnection, error) {
|
||||
tag, err := s.pool.Exec(ctx, `UPDATE gateway_security_event_connections
|
||||
SET management_client_id=$2,management_credential_ref=$3,last_error_category=NULL,updated_at=now()
|
||||
WHERE connection_id=$1::uuid`, connectionID, clientID, reference)
|
||||
func (s *Store) SetSecurityEventManagementCredential(ctx context.Context, connectionID, clientID, reference string, expectedVersion int64) (SecurityEventConnection, error) {
|
||||
tx, err := s.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return SecurityEventConnection{}, err
|
||||
}
|
||||
if tag.RowsAffected() == 0 {
|
||||
defer tx.Rollback(ctx)
|
||||
var oldReference sql.NullString
|
||||
var lifecycle string
|
||||
var version int64
|
||||
if err := tx.QueryRow(ctx, `SELECT management_credential_ref,lifecycle_status,version FROM gateway_security_event_connections
|
||||
WHERE connection_id=$1::uuid FOR UPDATE`, connectionID).Scan(&oldReference, &lifecycle, &version); errors.Is(err, pgx.ErrNoRows) {
|
||||
return SecurityEventConnection{}, ErrSecurityEventConnectionNotFound
|
||||
}
|
||||
return s.SecurityEventConnection(ctx)
|
||||
}
|
||||
|
||||
func (s *Store) UpdateSecurityEventConnectionLifecycle(ctx context.Context, connectionID, lifecycle string, errorCategory *string) (SecurityEventConnection, error) {
|
||||
tag, err := s.pool.Exec(ctx, `UPDATE gateway_security_event_connections
|
||||
SET lifecycle_status=$2,last_error_category=$3,version=version+1,updated_at=now() WHERE connection_id=$1::uuid`, connectionID, lifecycle, errorCategory)
|
||||
if err != nil {
|
||||
} else if err != nil {
|
||||
return SecurityEventConnection{}, err
|
||||
}
|
||||
if tag.RowsAffected() == 0 {
|
||||
return SecurityEventConnection{}, ErrSecurityEventConnectionNotFound
|
||||
if version != expectedVersion || lifecycle == "retiring" || lifecycle == "disconnect_pending" {
|
||||
return SecurityEventConnection{}, ErrSecurityEventConnectionConflict
|
||||
}
|
||||
return s.SecurityEventConnection(ctx)
|
||||
}
|
||||
|
||||
func (s *Store) SetSecurityEventNextCredential(ctx context.Context, connectionID, reference string) (SecurityEventConnection, error) {
|
||||
tag, err := s.pool.Exec(ctx, `UPDATE gateway_security_event_connections
|
||||
SET next_credential_ref=$2,lifecycle_status='rotating',last_error_category=NULL,version=version+1,updated_at=now()
|
||||
WHERE connection_id=$1::uuid`, connectionID, reference)
|
||||
if err != nil {
|
||||
return SecurityEventConnection{}, err
|
||||
}
|
||||
if tag.RowsAffected() == 0 {
|
||||
return SecurityEventConnection{}, ErrSecurityEventConnectionNotFound
|
||||
}
|
||||
return s.SecurityEventConnection(ctx)
|
||||
}
|
||||
|
||||
func (s *Store) PromoteSecurityEventCredential(ctx context.Context, connectionID, nextReference string) (SecurityEventConnection, error) {
|
||||
tag, err := s.pool.Exec(ctx, `UPDATE gateway_security_event_connections
|
||||
SET credential_ref=$2,next_credential_ref=NULL,lifecycle_status='bootstrap',last_error_category=NULL,
|
||||
tag, err := tx.Exec(ctx, `UPDATE gateway_security_event_connections
|
||||
SET management_client_id=$2,management_credential_ref=$3,last_error_category=NULL,
|
||||
version=version+1,updated_at=now()
|
||||
WHERE connection_id=$1::uuid AND next_credential_ref=$2`, connectionID, nextReference)
|
||||
WHERE connection_id=$1::uuid AND version=$4
|
||||
AND lifecycle_status NOT IN ('retiring','disconnect_pending')`, connectionID, clientID, reference, expectedVersion)
|
||||
if err != nil {
|
||||
return SecurityEventConnection{}, err
|
||||
}
|
||||
if tag.RowsAffected() == 0 {
|
||||
return SecurityEventConnection{}, ErrSecurityEventConnectionConflict
|
||||
}
|
||||
if err := adoptPendingIdentitySecrets(ctx, tx, reference); err != nil {
|
||||
return SecurityEventConnection{}, err
|
||||
}
|
||||
if oldReference.Valid && oldReference.String != reference {
|
||||
if err := queueIdentitySecretCleanupTx(ctx, tx, oldReference.String, time.Now().UTC()); err != nil {
|
||||
return SecurityEventConnection{}, err
|
||||
}
|
||||
}
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return SecurityEventConnection{}, err
|
||||
}
|
||||
return s.SecurityEventConnection(ctx)
|
||||
}
|
||||
|
||||
func (s *Store) TransitionSecurityEventConnectionLifecycle(ctx context.Context, connectionID, expectedLifecycle string, expectedVersion int64, lifecycle string, errorCategory *string) (SecurityEventConnection, error) {
|
||||
tag, err := s.pool.Exec(ctx, `UPDATE gateway_security_event_connections
|
||||
SET lifecycle_status=$4,last_error_category=$5,version=version+1,updated_at=now()
|
||||
WHERE connection_id=$1::uuid AND lifecycle_status=$2 AND version=$3
|
||||
AND (lifecycle_status NOT IN ('retiring','disconnect_pending')
|
||||
OR $4 IN ('retiring','disconnect_pending'))`,
|
||||
connectionID, expectedLifecycle, expectedVersion, lifecycle, errorCategory)
|
||||
if err != nil {
|
||||
return SecurityEventConnection{}, err
|
||||
}
|
||||
@@ -178,16 +310,168 @@ WHERE connection_id=$1::uuid AND next_credential_ref=$2`, connectionID, nextRefe
|
||||
return s.SecurityEventConnection(ctx)
|
||||
}
|
||||
|
||||
func (s *Store) DeleteSecurityEventConnection(ctx context.Context, connectionID string) error {
|
||||
if _, err := uuid.Parse(connectionID); err != nil {
|
||||
return ErrSecurityEventConnectionNotFound
|
||||
func (s *Store) SetSecurityEventNextCredential(ctx context.Context, connectionID, reference string, expectedVersion int64) (SecurityEventConnection, error) {
|
||||
tx, err := s.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return SecurityEventConnection{}, err
|
||||
}
|
||||
tag, err := s.pool.Exec(ctx, `DELETE FROM gateway_security_event_connections WHERE connection_id=$1::uuid`, connectionID)
|
||||
defer tx.Rollback(ctx)
|
||||
tag, err := tx.Exec(ctx, `UPDATE gateway_security_event_connections
|
||||
SET next_credential_ref=$2,lifecycle_status='rotating',last_error_category=NULL,version=version+1,updated_at=now()
|
||||
WHERE connection_id=$1::uuid AND version=$3 AND next_credential_ref IS NULL
|
||||
AND lifecycle_status IN ('connecting','verifying','bootstrap','enabled','degraded','error')`, connectionID, reference, expectedVersion)
|
||||
if err != nil {
|
||||
return SecurityEventConnection{}, err
|
||||
}
|
||||
if tag.RowsAffected() == 0 {
|
||||
return SecurityEventConnection{}, ErrSecurityEventConnectionConflict
|
||||
}
|
||||
if err := adoptPendingIdentitySecrets(ctx, tx, reference); err != nil {
|
||||
return SecurityEventConnection{}, err
|
||||
}
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return SecurityEventConnection{}, err
|
||||
}
|
||||
return s.SecurityEventConnection(ctx)
|
||||
}
|
||||
|
||||
func (s *Store) PromoteSecurityEventCredential(ctx context.Context, connectionID, nextReference string) (SecurityEventConnection, error) {
|
||||
tx, err := s.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return SecurityEventConnection{}, err
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
var oldReference string
|
||||
if err := tx.QueryRow(ctx, `SELECT credential_ref FROM gateway_security_event_connections
|
||||
WHERE connection_id=$1::uuid AND next_credential_ref=$2 AND lifecycle_status='rotating' FOR UPDATE`, connectionID, nextReference).Scan(&oldReference); errors.Is(err, pgx.ErrNoRows) {
|
||||
return SecurityEventConnection{}, ErrSecurityEventConnectionConflict
|
||||
} else if err != nil {
|
||||
return SecurityEventConnection{}, err
|
||||
}
|
||||
tag, err := tx.Exec(ctx, `UPDATE gateway_security_event_connections
|
||||
SET credential_ref=$2,next_credential_ref=NULL,lifecycle_status='bootstrap',last_error_category=NULL,
|
||||
version=version+1,updated_at=now()
|
||||
WHERE connection_id=$1::uuid AND next_credential_ref=$2 AND lifecycle_status='rotating'`, connectionID, nextReference)
|
||||
if err != nil {
|
||||
return SecurityEventConnection{}, err
|
||||
}
|
||||
if tag.RowsAffected() == 0 {
|
||||
return SecurityEventConnection{}, ErrSecurityEventConnectionConflict
|
||||
}
|
||||
if oldReference != nextReference {
|
||||
if err := queueIdentitySecretCleanupTx(ctx, tx, oldReference, time.Now().UTC()); err != nil {
|
||||
return SecurityEventConnection{}, err
|
||||
}
|
||||
}
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return SecurityEventConnection{}, err
|
||||
}
|
||||
return s.SecurityEventConnection(ctx)
|
||||
}
|
||||
|
||||
// DiscardPreparedSecurityEventConnection atomically retires the local Secrets
|
||||
// and removes only the exact, still-unbound connection generation observed by
|
||||
// the caller. A concurrent Bind or credential update changes the version and
|
||||
// therefore preserves both the connection and its Secrets.
|
||||
func (s *Store) DiscardPreparedSecurityEventConnection(ctx context.Context, connectionID, ownerKey string, expectedVersion int64) error {
|
||||
if _, err := uuid.Parse(connectionID); err != nil || ownerKey == "" || expectedVersion <= 0 {
|
||||
return ErrSecurityEventConnectionConflict
|
||||
}
|
||||
tx, err := s.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if tag.RowsAffected() == 0 {
|
||||
return ErrSecurityEventConnectionNotFound
|
||||
defer tx.Rollback(ctx)
|
||||
|
||||
var credentialReference string
|
||||
var nextReference, managementReference sql.NullString
|
||||
err = tx.QueryRow(ctx, `SELECT credential_ref,next_credential_ref,management_credential_ref
|
||||
FROM gateway_security_event_connections
|
||||
WHERE connection_id=$1::uuid AND idempotency_key=$2 AND version=$3 AND stream_id IS NULL
|
||||
FOR UPDATE`, connectionID, ownerKey, expectedVersion).Scan(
|
||||
&credentialReference, &nextReference, &managementReference,
|
||||
)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return ErrSecurityEventConnectionConflict
|
||||
}
|
||||
return nil
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
references := map[string]struct{}{credentialReference: {}}
|
||||
if nextReference.Valid {
|
||||
references[nextReference.String] = struct{}{}
|
||||
}
|
||||
if managementReference.Valid {
|
||||
references[managementReference.String] = struct{}{}
|
||||
}
|
||||
now := time.Now().UTC()
|
||||
for reference := range references {
|
||||
if err := queueIdentitySecretCleanupTx(ctx, tx, reference, now); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
tag, err := tx.Exec(ctx, `DELETE FROM gateway_security_event_connections
|
||||
WHERE connection_id=$1::uuid AND idempotency_key=$2 AND version=$3 AND stream_id IS NULL`,
|
||||
connectionID, ownerKey, expectedVersion)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if tag.RowsAffected() != 1 {
|
||||
return ErrSecurityEventConnectionConflict
|
||||
}
|
||||
return tx.Commit(ctx)
|
||||
}
|
||||
|
||||
// FinalizeRetiringSecurityEventConnection atomically transfers every Secret
|
||||
// reference owned by the exact retiring generation to the durable cleanup
|
||||
// queue before removing that generation. The SecretStore worker can therefore
|
||||
// resume after a crash without the database losing its last cleanup intent.
|
||||
func (s *Store) FinalizeRetiringSecurityEventConnection(ctx context.Context, connectionID string, expectedVersion int64) error {
|
||||
if _, err := uuid.Parse(connectionID); err != nil || expectedVersion <= 0 {
|
||||
return ErrSecurityEventConnectionConflict
|
||||
}
|
||||
tx, err := s.pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
|
||||
var credentialReference string
|
||||
var nextReference, managementReference sql.NullString
|
||||
err = tx.QueryRow(ctx, `SELECT credential_ref,next_credential_ref,management_credential_ref
|
||||
FROM gateway_security_event_connections
|
||||
WHERE connection_id=$1::uuid AND lifecycle_status='retiring' AND version=$2
|
||||
FOR UPDATE`, connectionID, expectedVersion).Scan(
|
||||
&credentialReference, &nextReference, &managementReference,
|
||||
)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return ErrSecurityEventConnectionConflict
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
references := map[string]struct{}{credentialReference: {}}
|
||||
if nextReference.Valid {
|
||||
references[nextReference.String] = struct{}{}
|
||||
}
|
||||
if managementReference.Valid {
|
||||
references[managementReference.String] = struct{}{}
|
||||
}
|
||||
now := time.Now().UTC()
|
||||
for reference := range references {
|
||||
if err := queueIdentitySecretCleanupTx(ctx, tx, reference, now); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
tag, err := tx.Exec(ctx, `DELETE FROM gateway_security_event_connections
|
||||
WHERE connection_id=$1::uuid AND lifecycle_status='retiring' AND version=$2`, connectionID, expectedVersion)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if tag.RowsAffected() != 1 {
|
||||
return ErrSecurityEventConnectionConflict
|
||||
}
|
||||
return tx.Commit(ctx)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user