easyai-ai-gateway/apps/api/internal/store/security_event_connections.go
chengcheng ffb85b73af feat(ssf): 托管机器凭据并支持动态恢复
Gateway 连接表单接收认证中心一次性交付的 machine Client 凭据,后端立即写入 SecretStore,数据库和公开响应只保留引用及公开 Client ID。SSF 管理 Token、Verification 和 RFC 7662 内省统一从动态凭据读取,支持现有连接无重启修复及进程重启恢复,环境变量仅保留为旧部署回退。\n\n验证:go test ./apps/api/...;pnpm nx test web;真实重启、OIDC 登录与 session-revoked 1.29 秒失效验收。
2026-07-17 09:13:57 +08:00

194 lines
9.1 KiB
Go

package store
import (
"context"
"encoding/json"
"errors"
"time"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
)
var (
ErrSecurityEventConnectionNotFound = errors.New("security event connection not found")
ErrSecurityEventConnectionConflict = errors.New("security event connection conflicts with current state")
)
type SecurityEventConnection struct {
ConnectionID string `json:"connectionId"`
TransmitterIssuer string `json:"transmitterIssuer"`
EndpointURL string `json:"endpointUrl"`
Audience *string `json:"audience,omitempty"`
StreamID *string `json:"streamId,omitempty"`
CredentialRef string `json:"-"`
NextCredentialRef *string `json:"-"`
ManagementClientID string `json:"managementClientId"`
ManagementCredentialRef *string `json:"-"`
LifecycleStatus string `json:"lifecycleStatus"`
Version int64 `json:"version"`
LastErrorCategory *string `json:"lastErrorCategory,omitempty"`
IdempotencyKey string `json:"-"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
LastVerificationAt *time.Time `json:"lastVerificationAt,omitempty"`
HealthMode string `json:"healthMode"`
}
type CreateSecurityEventConnectionInput struct {
ConnectionID, TransmitterIssuer, EndpointURL, CredentialRef, ManagementClientID, ManagementCredentialRef, IdempotencyKey string
}
type SecurityEventConnectionIdempotency struct {
RequestHash string
Response json.RawMessage
}
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
WHERE operation=$1 AND idempotency_key=$2`, operation, key).Scan(&value.RequestHash, &value.Response)
if errors.Is(err, pgx.ErrNoRows) {
return SecurityEventConnectionIdempotency{}, ErrSecurityEventConnectionNotFound
}
return value, err
}
func (s *Store) RecordSecurityEventConnectionIdempotency(ctx context.Context, operation, key, requestHash string, response []byte) error {
if !json.Valid(response) {
return errors.New("security event idempotency response is invalid")
}
_, err := s.pool.Exec(ctx, `INSERT INTO gateway_security_event_connection_idempotency(operation,idempotency_key,request_hash,response)
VALUES($1,$2,$3,$4::jsonb) ON CONFLICT(operation,idempotency_key) DO NOTHING`, operation, key, requestHash, response)
return err
}
func (s *Store) CreateSecurityEventConnection(ctx context.Context, input CreateSecurityEventConnectionInput) (SecurityEventConnection, error) {
var value SecurityEventConnection
err := s.pool.QueryRow(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) {
existing, getErr := s.SecurityEventConnection(ctx)
if getErr == nil && existing.IdempotencyKey == input.IdempotencyKey && existing.TransmitterIssuer == input.TransmitterIssuer {
return existing, nil
}
return SecurityEventConnection{}, ErrSecurityEventConnectionConflict
}
return SecurityEventConnection{}, err
}
return value, nil
}
func (s *Store) SecurityEventConnection(ctx context.Context) (SecurityEventConnection, error) {
var value SecurityEventConnection
err := s.pool.QueryRow(ctx, `
SELECT connection.connection_id::text,connection.transmitter_issuer,connection.endpoint_url,connection.audience,
connection.stream_id::text,connection.credential_ref,connection.next_credential_ref,
COALESCE(connection.management_client_id,''),connection.management_credential_ref,connection.lifecycle_status,
connection.version,connection.last_error_category,connection.idempotency_key,connection.created_at,connection.updated_at,
state.last_verification_at,COALESCE(state.mode,'disabled')
FROM gateway_security_event_connections connection
LEFT JOIN gateway_security_event_stream_state state
ON state.issuer=connection.transmitter_issuer AND state.audience=connection.audience
WHERE connection.singleton=true`).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,
&value.LastVerificationAt, &value.HealthMode,
)
if errors.Is(err, pgx.ErrNoRows) {
return SecurityEventConnection{}, ErrSecurityEventConnectionNotFound
}
return value, err
}
func (s *Store) BindSecurityEventConnection(ctx context.Context, connectionID, audience, streamID, lifecycle string, expectedVersion int64) (SecurityEventConnection, error) {
tag, err := s.pool.Exec(ctx, `UPDATE gateway_security_event_connections
SET audience=$2,stream_id=$3::uuid,lifecycle_status=$4,last_error_category=NULL,version=version+1,updated_at=now()
WHERE connection_id=$1::uuid AND version=$5`, connectionID, audience, streamID, lifecycle, expectedVersion)
if err != nil {
return SecurityEventConnection{}, err
}
if tag.RowsAffected() == 0 {
return SecurityEventConnection{}, ErrSecurityEventConnectionConflict
}
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)
if err != nil {
return SecurityEventConnection{}, err
}
if tag.RowsAffected() == 0 {
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 {
return SecurityEventConnection{}, err
}
if tag.RowsAffected() == 0 {
return SecurityEventConnection{}, ErrSecurityEventConnectionNotFound
}
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,
version=version+1,updated_at=now()
WHERE connection_id=$1::uuid AND next_credential_ref=$2`, connectionID, nextReference)
if err != nil {
return SecurityEventConnection{}, err
}
if tag.RowsAffected() == 0 {
return SecurityEventConnection{}, ErrSecurityEventConnectionConflict
}
return s.SecurityEventConnection(ctx)
}
func (s *Store) DeleteSecurityEventConnection(ctx context.Context, connectionID string) error {
if _, err := uuid.Parse(connectionID); err != nil {
return ErrSecurityEventConnectionNotFound
}
tag, err := s.pool.Exec(ctx, `DELETE FROM gateway_security_event_connections WHERE connection_id=$1::uuid`, connectionID)
if err != nil {
return err
}
if tag.RowsAffected() == 0 {
return ErrSecurityEventConnectionNotFound
}
return nil
}