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:
@@ -5,6 +5,7 @@ import (
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
@@ -20,22 +21,45 @@ const (
|
||||
PairingCompleted PairingStatus = "completed"
|
||||
PairingFailed PairingStatus = "failed"
|
||||
PairingExpired PairingStatus = "expired"
|
||||
PairingCancelled PairingStatus = "cancelled"
|
||||
)
|
||||
|
||||
type PairingCleanupStatus string
|
||||
|
||||
const (
|
||||
PairingCleanupNone PairingCleanupStatus = "none"
|
||||
PairingCleanupPending PairingCleanupStatus = "pending"
|
||||
PairingCleanupCompleted PairingCleanupStatus = "completed"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrPairingInProgress = errors.New("an identity pairing is already in progress")
|
||||
ErrPairingNotCancellable = errors.New("identity pairing cannot be cancelled")
|
||||
ErrPairingConflictNotResolvable = errors.New("identity pairing has no resolvable security event conflict")
|
||||
)
|
||||
|
||||
const (
|
||||
identityPairingStartReservationTTL = 2 * time.Minute
|
||||
identityPairingStartReleaseTimeout = 5 * time.Second
|
||||
)
|
||||
|
||||
type PairingExchange struct {
|
||||
ID string `json:"id"`
|
||||
RevisionID string `json:"revisionId"`
|
||||
RemoteExchangeID string `json:"remoteExchangeId"`
|
||||
ExchangeTokenRef string `json:"-"`
|
||||
Status PairingStatus `json:"status"`
|
||||
RemoteVersion int64 `json:"remoteVersion"`
|
||||
ExpiresAt time.Time `json:"expiresAt"`
|
||||
Version int64 `json:"version"`
|
||||
LastErrorCategory string `json:"lastErrorCategory,omitempty"`
|
||||
AuthCenterAuditID string `json:"authCenterAuditId,omitempty"`
|
||||
LastTraceID string `json:"lastTraceId,omitempty"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
ID string `json:"id"`
|
||||
RevisionID string `json:"revisionId"`
|
||||
RemoteExchangeID string `json:"remoteExchangeId"`
|
||||
ExchangeTokenRef string `json:"-"`
|
||||
Status PairingStatus `json:"status"`
|
||||
CleanupStatus PairingCleanupStatus `json:"cleanupStatus"`
|
||||
RemoteVersion int64 `json:"remoteVersion"`
|
||||
ExpiresAt time.Time `json:"expiresAt"`
|
||||
Version int64 `json:"version"`
|
||||
LastErrorCategory string `json:"lastErrorCategory,omitempty"`
|
||||
AuthCenterAuditID string `json:"authCenterAuditId,omitempty"`
|
||||
LastTraceID string `json:"lastTraceId,omitempty"`
|
||||
CancelledAt *time.Time `json:"cancelledAt,omitempty"`
|
||||
CleanupCompletedAt *time.Time `json:"cleanupCompletedAt,omitempty"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
type PairingExchangeUpdate struct {
|
||||
@@ -46,12 +70,20 @@ type PairingExchangeUpdate struct {
|
||||
}
|
||||
|
||||
type PairingRepository interface {
|
||||
CreateIdentityConfigurationRevision(context.Context, Revision) (Revision, error)
|
||||
ReserveIdentityPairingStart(context.Context, string, time.Time) error
|
||||
ReleaseIdentityPairingStart(context.Context, string) error
|
||||
CommitIdentityPairingStart(context.Context, Revision, PairingExchange) (PairingExchange, error)
|
||||
IdentityPairingStartBlocked(context.Context) (bool, error)
|
||||
IdentityConfigurationRevision(context.Context, string) (Revision, error)
|
||||
ApplyIdentityManifest(context.Context, string, int64, ManifestApplication) (Revision, error)
|
||||
CreateIdentityPairingExchange(context.Context, PairingExchange) (PairingExchange, error)
|
||||
IdentityPairingExchange(context.Context, string) (PairingExchange, error)
|
||||
UpdateIdentityPairingExchange(context.Context, string, int64, PairingExchangeUpdate) (PairingExchange, error)
|
||||
RecordIdentityPairingRetryFailure(context.Context, string, PairingStatus, string) (PairingExchange, error)
|
||||
CancelIdentityPairingExchange(context.Context, string, int64, string, string) (PairingExchange, error)
|
||||
CompleteIdentityPairingCleanup(context.Context, string, int64) (PairingExchange, error)
|
||||
RecordIdentityPairingCleanupFailure(context.Context, string, string) (PairingExchange, error)
|
||||
QueueIdentitySecretCleanup(context.Context, string, time.Time) error
|
||||
RemovePendingIdentitySecretCleanup(context.Context, string) (bool, error)
|
||||
}
|
||||
|
||||
type PairingSecretStore interface {
|
||||
@@ -70,6 +102,11 @@ type OnboardingRemote interface {
|
||||
|
||||
type SecurityEventPreparer interface {
|
||||
PrepareSecurityEvents(context.Context, Revision, []byte) error
|
||||
CleanupPreparedSecurityEvents(context.Context, Revision) error
|
||||
}
|
||||
|
||||
type SecurityEventConflictResolver interface {
|
||||
RetireConflictingSecurityEvents(context.Context, Revision) error
|
||||
}
|
||||
|
||||
type PairingService struct {
|
||||
@@ -77,22 +114,37 @@ type PairingService struct {
|
||||
secrets PairingSecretStore
|
||||
remote func(string) (OnboardingRemote, error)
|
||||
security SecurityEventPreparer
|
||||
appEnv string
|
||||
operations sync.Map
|
||||
}
|
||||
|
||||
func NewPairingService(repository PairingRepository, secrets PairingSecretStore, remote func(string) (OnboardingRemote, error), security SecurityEventPreparer) *PairingService {
|
||||
return &PairingService{repository: repository, secrets: secrets, remote: remote, security: security}
|
||||
func NewPairingService(repository PairingRepository, secrets PairingSecretStore, remote func(string) (OnboardingRemote, error), security SecurityEventPreparer, appEnvs ...string) *PairingService {
|
||||
appEnv := "production"
|
||||
if len(appEnvs) > 0 {
|
||||
appEnv = appEnvs[0]
|
||||
}
|
||||
return &PairingService{repository: repository, secrets: secrets, remote: remote, security: security, appEnv: appEnv}
|
||||
}
|
||||
|
||||
func (service *PairingService) Start(ctx context.Context, input PairingInput, traceID string) (PairingExchange, error) {
|
||||
draft, err := NewDraft(input)
|
||||
draft, err := NewDraft(input, service.appEnv)
|
||||
if err != nil {
|
||||
return PairingExchange{}, err
|
||||
}
|
||||
draft.LastTraceID = strings.TrimSpace(traceID)
|
||||
draft, err = service.repository.CreateIdentityConfigurationRevision(ctx, draft)
|
||||
if err != nil {
|
||||
pairingID := uuid.NewString()
|
||||
tokenReference := "identity-exchange-" + pairingID
|
||||
if err := service.repository.ReserveIdentityPairingStart(ctx, pairingID, time.Now().UTC().Add(identityPairingStartReservationTTL)); err != nil {
|
||||
return PairingExchange{}, err
|
||||
}
|
||||
committed := false
|
||||
defer func() {
|
||||
if !committed {
|
||||
releaseContext, cancel := context.WithTimeout(context.Background(), identityPairingStartReleaseTimeout)
|
||||
defer cancel()
|
||||
_ = service.repository.ReleaseIdentityPairingStart(releaseContext, pairingID)
|
||||
}
|
||||
}()
|
||||
remote, err := service.remote(draft.AuthCenterURL)
|
||||
if err != nil {
|
||||
return PairingExchange{}, err
|
||||
@@ -101,32 +153,211 @@ func (service *PairingService) Start(ctx context.Context, input PairingInput, tr
|
||||
if err != nil {
|
||||
return PairingExchange{}, err
|
||||
}
|
||||
pairingID := uuid.NewString()
|
||||
tokenReference := "identity-exchange-" + pairingID
|
||||
if err := service.secrets.Put(ctx, tokenReference, []byte(claimed.ExchangeToken)); err != nil {
|
||||
token := []byte(claimed.ExchangeToken)
|
||||
claimed.ExchangeToken = ""
|
||||
if err := service.stageIdentitySecret(ctx, tokenReference, token); err != nil {
|
||||
clear(token)
|
||||
return PairingExchange{}, err
|
||||
}
|
||||
claimed.ExchangeToken = ""
|
||||
clear(token)
|
||||
pairing := PairingExchange{
|
||||
ID: pairingID, RevisionID: draft.ID, RemoteExchangeID: claimed.ExchangeID, ExchangeTokenRef: tokenReference,
|
||||
Status: PairingMetadataPending, RemoteVersion: claimed.Version, ExpiresAt: claimed.ExpiresAt,
|
||||
Status: PairingMetadataPending, CleanupStatus: PairingCleanupNone, RemoteVersion: claimed.Version, ExpiresAt: claimed.ExpiresAt,
|
||||
Version: 1, LastTraceID: strings.TrimSpace(traceID),
|
||||
}
|
||||
pairing, err = service.repository.CreateIdentityPairingExchange(ctx, pairing)
|
||||
pairing, err = service.repository.CommitIdentityPairingStart(ctx, draft, pairing)
|
||||
if err != nil {
|
||||
_ = service.secrets.Delete(ctx, tokenReference)
|
||||
// Commit may have succeeded even when its response was lost. The
|
||||
// staging row is the source of truth: rollback leaves it queued, while
|
||||
// a successful commit atomically adopts it. Re-queueing here could
|
||||
// delete a Secret that the new Pairing already references.
|
||||
return PairingExchange{}, err
|
||||
}
|
||||
committed = true
|
||||
return pairing, nil
|
||||
}
|
||||
|
||||
func (service *PairingService) Cancel(ctx context.Context, pairingID string, expectedVersion int64, traceID, auditID string) (PairingExchange, error) {
|
||||
return service.repository.CancelIdentityPairingExchange(ctx, pairingID, expectedVersion, strings.TrimSpace(traceID), strings.TrimSpace(auditID))
|
||||
}
|
||||
|
||||
func (service *PairingService) RetireConflictingSecurityEvents(ctx context.Context, pairingID string, expectedVersion int64) (PairingExchange, error) {
|
||||
unlock := service.lockPairingOperation(pairingID)
|
||||
defer unlock()
|
||||
pairing, err := service.repository.IdentityPairingExchange(ctx, pairingID)
|
||||
if err != nil {
|
||||
return PairingExchange{}, err
|
||||
}
|
||||
if pairing.Version != expectedVersion {
|
||||
return pairing, ErrRevisionConflict
|
||||
}
|
||||
if pairing.Status != PairingCredentialsSaved || pairing.CleanupStatus != PairingCleanupNone ||
|
||||
pairing.LastErrorCategory != "security_event_connection_conflict" {
|
||||
return pairing, ErrPairingConflictNotResolvable
|
||||
}
|
||||
revision, err := service.repository.IdentityConfigurationRevision(ctx, pairing.RevisionID)
|
||||
if err != nil {
|
||||
return pairing, err
|
||||
}
|
||||
if revision.State != RevisionDraft || !revision.SessionRevocation {
|
||||
return pairing, ErrPairingConflictNotResolvable
|
||||
}
|
||||
resolver, ok := service.security.(SecurityEventConflictResolver)
|
||||
if !ok {
|
||||
return pairing, errors.New("security event conflict recovery is unavailable")
|
||||
}
|
||||
if err := resolver.RetireConflictingSecurityEvents(ctx, revision); err != nil {
|
||||
return pairing, err
|
||||
}
|
||||
return service.repository.RecordIdentityPairingRetryFailure(ctx, pairing.ID, pairing.Status, "security_event_retirement_pending")
|
||||
}
|
||||
|
||||
// RestoreCompletedSecurityEvents reconstructs the prepared Receiver after a
|
||||
// process restart while a completed Revision is still awaiting validation or
|
||||
// activation. The machine Secret is read only from SecretStore and cleared
|
||||
// immediately after use.
|
||||
func (service *PairingService) RestoreCompletedSecurityEvents(ctx context.Context, pairingID string) error {
|
||||
unlock := service.lockPairingOperation(pairingID)
|
||||
defer unlock()
|
||||
pairing, err := service.repository.IdentityPairingExchange(ctx, pairingID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if pairing.Status != PairingCompleted {
|
||||
return ErrRevisionConflict
|
||||
}
|
||||
revision, err := service.repository.IdentityConfigurationRevision(ctx, pairing.RevisionID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if revision.State == RevisionActive {
|
||||
return nil
|
||||
}
|
||||
if revision.State != RevisionDraft && revision.State != RevisionValidated {
|
||||
return ErrRevisionConflict
|
||||
}
|
||||
if !revision.SessionRevocation {
|
||||
return nil
|
||||
}
|
||||
if service.security == nil || revision.MachineCredentialRef == "" {
|
||||
return errors.New("completed security event configuration is unavailable")
|
||||
}
|
||||
secret, err := service.secrets.Get(ctx, revision.MachineCredentialRef)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer clear(secret)
|
||||
if err := service.security.PrepareSecurityEvents(ctx, revision, secret); err != nil {
|
||||
return err
|
||||
}
|
||||
currentRevision, revisionErr := service.repository.IdentityConfigurationRevision(ctx, pairing.RevisionID)
|
||||
if revisionErr != nil {
|
||||
return revisionErr
|
||||
}
|
||||
if currentRevision.State == RevisionActive {
|
||||
return nil
|
||||
}
|
||||
currentPairing, pairingErr := service.repository.IdentityPairingExchange(ctx, pairingID)
|
||||
if pairingErr != nil {
|
||||
return pairingErr
|
||||
}
|
||||
if currentPairing.Status == PairingCompleted &&
|
||||
(currentRevision.State == RevisionDraft || currentRevision.State == RevisionValidated) {
|
||||
return nil
|
||||
}
|
||||
if cleanupErr := service.security.CleanupPreparedSecurityEvents(ctx, currentRevision); cleanupErr != nil {
|
||||
return cleanupErr
|
||||
}
|
||||
return ErrRevisionConflict
|
||||
}
|
||||
|
||||
func (service *PairingService) Cleanup(ctx context.Context, pairingID string) (PairingExchange, error) {
|
||||
unlock := service.lockPairingOperation(pairingID)
|
||||
defer unlock()
|
||||
pairing, err := service.repository.IdentityPairingExchange(ctx, pairingID)
|
||||
if err != nil {
|
||||
return PairingExchange{}, err
|
||||
}
|
||||
if pairing.Status != PairingCancelled {
|
||||
return pairing, ErrPairingNotCancellable
|
||||
}
|
||||
if pairing.CleanupStatus == PairingCleanupCompleted {
|
||||
return pairing, nil
|
||||
}
|
||||
if pairing.CleanupStatus != PairingCleanupPending {
|
||||
return pairing, ErrRevisionConflict
|
||||
}
|
||||
revision, err := service.repository.IdentityConfigurationRevision(ctx, pairing.RevisionID)
|
||||
if err != nil {
|
||||
return service.recordCleanupFailure(ctx, pairing, "cleanup_revision_unavailable", err)
|
||||
}
|
||||
if revision.SessionRevocation {
|
||||
if service.security == nil {
|
||||
return service.recordCleanupFailure(ctx, pairing, "cleanup_security_event_unavailable", errors.New("security event cleanup is unavailable"))
|
||||
}
|
||||
if err := service.security.CleanupPreparedSecurityEvents(ctx, revision); err != nil {
|
||||
return service.recordCleanupFailure(ctx, pairing, pairingCleanupFailureCategory(err), err)
|
||||
}
|
||||
}
|
||||
for _, reference := range []string{pairing.ExchangeTokenRef, revision.MachineCredentialRef, revision.SessionEncryptionKeyRef} {
|
||||
if reference == "" {
|
||||
continue
|
||||
}
|
||||
if err := service.secrets.Delete(ctx, reference); err != nil {
|
||||
return service.recordCleanupFailure(ctx, pairing, "cleanup_secret_store_failed", err)
|
||||
}
|
||||
}
|
||||
completed, err := service.repository.CompleteIdentityPairingCleanup(ctx, pairing.ID, pairing.Version)
|
||||
if err != nil {
|
||||
return service.recordCleanupFailure(ctx, pairing, "cleanup_finalize_failed", err)
|
||||
}
|
||||
return completed, nil
|
||||
}
|
||||
|
||||
func (service *PairingService) recordCleanupFailure(ctx context.Context, pairing PairingExchange, category string, cause error) (PairingExchange, error) {
|
||||
if errors.Is(cause, context.Canceled) || errors.Is(cause, context.DeadlineExceeded) {
|
||||
return pairing, cause
|
||||
}
|
||||
recorded, err := service.repository.RecordIdentityPairingCleanupFailure(ctx, pairing.ID, category)
|
||||
if err != nil {
|
||||
return pairing, cause
|
||||
}
|
||||
return recorded, cause
|
||||
}
|
||||
|
||||
func (service *PairingService) Continue(ctx context.Context, pairingID string) (PairingExchange, error) {
|
||||
unlock := service.lockPairingOperation(pairingID)
|
||||
defer unlock()
|
||||
pairing, err := service.continuePairing(ctx, pairingID)
|
||||
if err == nil || errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
|
||||
return pairing, err
|
||||
}
|
||||
current, currentErr := service.repository.IdentityPairingExchange(ctx, pairingID)
|
||||
if currentErr != nil || !isPairingInProgress(current.Status) {
|
||||
return pairing, err
|
||||
}
|
||||
category := pairingRetryFailureCategory(current.Status, err)
|
||||
recorded, recordErr := service.repository.RecordIdentityPairingRetryFailure(ctx, current.ID, current.Status, category)
|
||||
if recordErr != nil {
|
||||
return current, err
|
||||
}
|
||||
return recorded, err
|
||||
}
|
||||
|
||||
func (service *PairingService) lockPairingOperation(pairingID string) func() {
|
||||
value, _ := service.operations.LoadOrStore(pairingID, &sync.Mutex{})
|
||||
mutex := value.(*sync.Mutex)
|
||||
mutex.Lock()
|
||||
return mutex.Unlock
|
||||
}
|
||||
|
||||
func (service *PairingService) continuePairing(ctx context.Context, pairingID string) (PairingExchange, error) {
|
||||
for step := 0; step < 6; step++ {
|
||||
pairing, err := service.repository.IdentityPairingExchange(ctx, pairingID)
|
||||
if err != nil {
|
||||
return PairingExchange{}, err
|
||||
}
|
||||
if pairing.Status == PairingCompleted || pairing.Status == PairingFailed || pairing.Status == PairingExpired {
|
||||
if pairing.Status == PairingCompleted || pairing.Status == PairingFailed || pairing.Status == PairingExpired || pairing.Status == PairingCancelled {
|
||||
return pairing, nil
|
||||
}
|
||||
if !pairing.ExpiresAt.After(time.Now()) {
|
||||
@@ -134,7 +365,7 @@ func (service *PairingService) Continue(ctx context.Context, pairingID string) (
|
||||
Status: PairingExpired, RemoteVersion: pairing.RemoteVersion, LastErrorCategory: "exchange_expired",
|
||||
})
|
||||
if updateErr == nil {
|
||||
_ = service.secrets.Delete(ctx, pairing.ExchangeTokenRef)
|
||||
service.deleteRetiredIdentitySecret(ctx, pairing.ExchangeTokenRef)
|
||||
}
|
||||
return expired, updateErr
|
||||
}
|
||||
@@ -156,7 +387,7 @@ func (service *PairingService) Continue(ctx context.Context, pairingID string) (
|
||||
metadata, metadataErr := PairingInput{
|
||||
AuthCenterURL: revision.AuthCenterURL, PublicBaseURL: revision.PublicBaseURL,
|
||||
WebBaseURL: revision.WebBaseURL, LocalTenantKey: revision.LocalTenantKey,
|
||||
}.ConsumerMetadata(true)
|
||||
}.ConsumerMetadata(true, service.appEnv)
|
||||
if metadataErr != nil {
|
||||
clear(token)
|
||||
return PairingExchange{}, metadataErr
|
||||
@@ -224,7 +455,7 @@ func (service *PairingService) Continue(ctx context.Context, pairingID string) (
|
||||
completeErr := remote.Complete(ctx, pairing.RemoteExchangeID, string(token), "gateway-pairing-complete-"+pairing.ID, pairing.RemoteVersion)
|
||||
clear(token)
|
||||
if completeErr != nil {
|
||||
return PairingExchange{}, completeErr
|
||||
return PairingExchange{}, pairingStepError{category: "exchange_completion_failed", cause: completeErr}
|
||||
}
|
||||
completed, err := service.repository.UpdateIdentityPairingExchange(ctx, pairing.ID, pairing.Version, PairingExchangeUpdate{
|
||||
Status: PairingCompleted, RemoteVersion: pairing.RemoteVersion,
|
||||
@@ -232,7 +463,7 @@ func (service *PairingService) Continue(ctx context.Context, pairingID string) (
|
||||
if err != nil {
|
||||
return PairingExchange{}, err
|
||||
}
|
||||
_ = service.secrets.Delete(ctx, pairing.ExchangeTokenRef)
|
||||
service.deleteRetiredIdentitySecret(ctx, pairing.ExchangeTokenRef)
|
||||
return completed, nil
|
||||
default:
|
||||
clear(token)
|
||||
@@ -242,8 +473,71 @@ func (service *PairingService) Continue(ctx context.Context, pairingID string) (
|
||||
return service.repository.IdentityPairingExchange(ctx, pairingID)
|
||||
}
|
||||
|
||||
func isPairingInProgress(status PairingStatus) bool {
|
||||
switch status {
|
||||
case PairingMetadataPending, PairingPreparing, PairingReady, PairingCredentialsSaved:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
type safeErrorCategory interface {
|
||||
SafeErrorCategory() string
|
||||
}
|
||||
|
||||
type pairingStepError struct {
|
||||
category string
|
||||
cause error
|
||||
}
|
||||
|
||||
func (err pairingStepError) Error() string { return "identity pairing step failed: " + err.category }
|
||||
func (err pairingStepError) Unwrap() error { return err.cause }
|
||||
func (err pairingStepError) SafeErrorCategory() string { return err.category }
|
||||
|
||||
func pairingRetryFailureCategory(status PairingStatus, err error) string {
|
||||
var categorized safeErrorCategory
|
||||
if errors.As(err, &categorized) && categorized.SafeErrorCategory() == "exchange_completion_failed" {
|
||||
return "exchange_completion_failed"
|
||||
}
|
||||
switch status {
|
||||
case PairingMetadataPending:
|
||||
return "metadata_submission_failed"
|
||||
case PairingPreparing:
|
||||
return "exchange_status_unavailable"
|
||||
case PairingReady:
|
||||
return "credential_delivery_failed"
|
||||
case PairingCredentialsSaved:
|
||||
if errors.As(err, &categorized) {
|
||||
switch categorized.SafeErrorCategory() {
|
||||
case "configuration_invalid", "connection_conflict", "discovery_failed", "management_token_failed",
|
||||
"stream_create_failed", "stream_response_invalid", "receiver_activation_failed", "preparation_failed", "retirement_pending",
|
||||
"credential_handoff_unsafe", "connection_binding_missing", "connection_binding_unavailable",
|
||||
"connection_binding_invalid", "connection_binding_mismatch":
|
||||
return "security_event_" + categorized.SafeErrorCategory()
|
||||
}
|
||||
}
|
||||
return "security_event_preparation_failed"
|
||||
default:
|
||||
return "pairing_step_failed"
|
||||
}
|
||||
}
|
||||
|
||||
func pairingCleanupFailureCategory(err error) string {
|
||||
var categorized safeErrorCategory
|
||||
if errors.As(err, &categorized) {
|
||||
switch categorized.SafeErrorCategory() {
|
||||
case "configuration_invalid", "retirement_pending", "connection_conflict", "connection_cleanup_failed", "secret_cleanup_failed",
|
||||
"discovery_failed", "management_token_failed", "stream_create_failed", "stream_response_invalid",
|
||||
"receiver_activation_failed", "preparation_failed", "credential_handoff_unsafe":
|
||||
return "cleanup_security_event_" + categorized.SafeErrorCategory()
|
||||
}
|
||||
}
|
||||
return "cleanup_security_event_failed"
|
||||
}
|
||||
|
||||
func (service *PairingService) saveDelivery(ctx context.Context, revision Revision, pairing PairingExchange, delivery CredentialDelivery) (Revision, error) {
|
||||
if err := delivery.Manifest.Validate(); err != nil {
|
||||
if err := delivery.Manifest.Validate(service.appEnv); err != nil {
|
||||
return Revision{}, err
|
||||
}
|
||||
machineReference := ""
|
||||
@@ -251,10 +545,10 @@ func (service *PairingService) saveDelivery(ctx context.Context, revision Revisi
|
||||
if delivery.MachineCredential == nil || delivery.MachineCredential.ClientID != delivery.Manifest.Clients.MachineToMachine.ClientID {
|
||||
return Revision{}, errors.New("onboarding machine credential is invalid")
|
||||
}
|
||||
machineReference = "identity-machine-" + revision.ID
|
||||
machineReference = "identity-machine-" + uuid.NewString()
|
||||
secret := []byte(delivery.MachineCredential.ClientSecret)
|
||||
delivery.MachineCredential.ClientSecret = ""
|
||||
if err := service.secrets.Put(ctx, machineReference, secret); err != nil {
|
||||
if err := service.stageIdentitySecret(ctx, machineReference, secret); err != nil {
|
||||
clear(secret)
|
||||
return Revision{}, err
|
||||
}
|
||||
@@ -262,49 +556,77 @@ func (service *PairingService) saveDelivery(ctx context.Context, revision Revisi
|
||||
}
|
||||
sessionReference := ""
|
||||
if delivery.Manifest.Clients.BrowserLogin != nil {
|
||||
sessionReference = "identity-session-" + revision.ID
|
||||
sessionReference = "identity-session-" + uuid.NewString()
|
||||
key := make([]byte, 32)
|
||||
if _, err := rand.Read(key); err != nil {
|
||||
_ = service.secrets.Delete(ctx, machineReference)
|
||||
_ = service.retireIdentitySecret(ctx, machineReference)
|
||||
return Revision{}, err
|
||||
}
|
||||
if err := service.secrets.Put(ctx, sessionReference, key); err != nil {
|
||||
if err := service.stageIdentitySecret(ctx, sessionReference, key); err != nil {
|
||||
clear(key)
|
||||
_ = service.secrets.Delete(ctx, machineReference)
|
||||
_ = service.retireIdentitySecret(ctx, machineReference)
|
||||
return Revision{}, err
|
||||
}
|
||||
clear(key)
|
||||
}
|
||||
updated, err := service.repository.ApplyIdentityManifest(ctx, revision.ID, revision.Version, ManifestApplication{
|
||||
Manifest: delivery.Manifest, MachineCredentialRef: machineReference, SessionEncryptionKeyRef: sessionReference,
|
||||
TraceID: pairing.LastTraceID, AuditID: pairing.AuthCenterAuditID,
|
||||
TraceID: pairing.LastTraceID, AuditID: pairing.AuthCenterAuditID, AppEnv: service.appEnv,
|
||||
})
|
||||
if err != nil {
|
||||
if machineReference != "" {
|
||||
_ = service.secrets.Delete(ctx, machineReference)
|
||||
}
|
||||
if sessionReference != "" {
|
||||
_ = service.secrets.Delete(ctx, sessionReference)
|
||||
}
|
||||
// ApplyIdentityManifest adopts the staged references in the same DB
|
||||
// transaction as the Revision update. On rollback the staging rows
|
||||
// remain; on an ambiguous successful commit they are gone. Do not
|
||||
// re-queue here or a valid active credential could be destroyed.
|
||||
return Revision{}, err
|
||||
}
|
||||
return updated, nil
|
||||
}
|
||||
|
||||
func (service *PairingService) stageIdentitySecret(ctx context.Context, reference string, value []byte) error {
|
||||
if err := service.repository.QueueIdentitySecretCleanup(ctx, reference, time.Now().UTC().Add(10*time.Minute)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := service.secrets.Put(ctx, reference, value); err != nil {
|
||||
deleteErr := service.secrets.Delete(ctx, reference)
|
||||
if deleteErr == nil {
|
||||
_, _ = service.repository.RemovePendingIdentitySecretCleanup(ctx, reference)
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (service *PairingService) retireIdentitySecret(ctx context.Context, reference string) error {
|
||||
if reference == "" {
|
||||
return nil
|
||||
}
|
||||
return service.repository.QueueIdentitySecretCleanup(ctx, reference, time.Now().UTC())
|
||||
}
|
||||
|
||||
func (service *PairingService) deleteRetiredIdentitySecret(ctx context.Context, reference string) {
|
||||
if err := service.secrets.Delete(ctx, reference); err == nil {
|
||||
_, _ = service.repository.RemovePendingIdentitySecretCleanup(ctx, reference)
|
||||
}
|
||||
}
|
||||
|
||||
func (service *PairingService) updateFromRemote(ctx context.Context, pairing PairingExchange, remote Exchange) (PairingExchange, error) {
|
||||
status := PairingPreparing
|
||||
category := ""
|
||||
switch remote.Status {
|
||||
case ExchangeReady, ExchangeCredentialDelivered:
|
||||
status = PairingReady
|
||||
case ExchangeCompleted:
|
||||
status = PairingFailed
|
||||
remote.LastErrorCategory = "remote_state_invalid"
|
||||
category = "remote_state_invalid"
|
||||
case ExchangeFailed:
|
||||
status = PairingFailed
|
||||
category = "remote_exchange_failed"
|
||||
case ExchangeExpired:
|
||||
status = PairingExpired
|
||||
category = "exchange_expired"
|
||||
}
|
||||
return service.repository.UpdateIdentityPairingExchange(ctx, pairing.ID, pairing.Version, PairingExchangeUpdate{
|
||||
Status: status, RemoteVersion: remote.Version, LastErrorCategory: remote.LastErrorCategory, AuthCenterAuditID: remote.AuditID,
|
||||
Status: status, RemoteVersion: remote.Version, LastErrorCategory: category, AuthCenterAuditID: remote.AuditID,
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user