feat(identity): 实现接入码配对后台流程
Gateway 使用一次性接入码创建 Exchange,将 Exchange Token、机器凭据与 Session Key 仅写入 SecretStore,并持久化脱敏配对进度。\n\n资源就绪后先保存凭据和 Manifest,再自动准备 SSF,最后确认远端 Exchange;SecretStore 失败时保持 ready,重试会触发新 Secret 轮换,不回放旧值。\n\n验证:go test ./...;go vet ./...
This commit is contained in:
@@ -48,6 +48,9 @@ type Revision struct {
|
||||
LegacyJWTEnabled bool `json:"legacyJwtEnabled"`
|
||||
TokenIntrospection bool `json:"tokenIntrospection"`
|
||||
SessionRevocation bool `json:"sessionRevocation"`
|
||||
SecurityEventIssuer string `json:"securityEventIssuer,omitempty"`
|
||||
SecurityEventConfigURL string `json:"securityEventConfigurationUrl,omitempty"`
|
||||
SecurityEventAudience string `json:"securityEventAudience,omitempty"`
|
||||
MachineCredentialRef string `json:"-"`
|
||||
SessionEncryptionKeyRef string `json:"-"`
|
||||
SessionIdleSeconds int `json:"sessionIdleSeconds"`
|
||||
@@ -136,6 +139,11 @@ func ApplyManifest(revision Revision, input ManifestApplication) (Revision, erro
|
||||
}
|
||||
revision.TokenIntrospection = capabilities["token_introspection"]
|
||||
revision.SessionRevocation = capabilities["session_revocation"]
|
||||
if input.Manifest.SecurityEvents != nil {
|
||||
revision.SecurityEventIssuer = input.Manifest.SecurityEvents.TransmitterIssuer
|
||||
revision.SecurityEventConfigURL = input.Manifest.SecurityEvents.ConfigurationEndpoint
|
||||
revision.SecurityEventAudience = input.Manifest.SecurityEvents.Audience
|
||||
}
|
||||
revision.MachineCredentialRef = strings.TrimSpace(input.MachineCredentialRef)
|
||||
revision.SessionEncryptionKeyRef = strings.TrimSpace(input.SessionEncryptionKeyRef)
|
||||
revision.LastTraceID = strings.TrimSpace(input.TraceID)
|
||||
|
||||
@@ -0,0 +1,301 @@
|
||||
package identity
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type PairingStatus string
|
||||
|
||||
const (
|
||||
PairingMetadataPending PairingStatus = "metadata_pending"
|
||||
PairingPreparing PairingStatus = "preparing"
|
||||
PairingReady PairingStatus = "ready"
|
||||
PairingCredentialsSaved PairingStatus = "credentials_saved"
|
||||
PairingCompleted PairingStatus = "completed"
|
||||
PairingFailed PairingStatus = "failed"
|
||||
PairingExpired PairingStatus = "expired"
|
||||
)
|
||||
|
||||
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"`
|
||||
}
|
||||
|
||||
type PairingExchangeUpdate struct {
|
||||
Status PairingStatus
|
||||
RemoteVersion int64
|
||||
LastErrorCategory string
|
||||
AuthCenterAuditID string
|
||||
}
|
||||
|
||||
type PairingRepository interface {
|
||||
CreateIdentityConfigurationRevision(context.Context, Revision) (Revision, 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)
|
||||
}
|
||||
|
||||
type PairingSecretStore interface {
|
||||
Put(context.Context, string, []byte) error
|
||||
Get(context.Context, string) ([]byte, error)
|
||||
Delete(context.Context, string) error
|
||||
}
|
||||
|
||||
type OnboardingRemote interface {
|
||||
Claim(context.Context, string) (ClaimedExchange, error)
|
||||
SubmitMetadata(context.Context, ClaimedExchange, ConsumerMetadata, string) (Exchange, error)
|
||||
Get(context.Context, string, string) (Exchange, error)
|
||||
DeliverCredential(context.Context, Exchange, string, string) (CredentialDelivery, error)
|
||||
Complete(context.Context, string, string, string, int64) error
|
||||
}
|
||||
|
||||
type SecurityEventPreparer interface {
|
||||
PrepareSecurityEvents(context.Context, Revision, []byte) error
|
||||
}
|
||||
|
||||
type PairingService struct {
|
||||
repository PairingRepository
|
||||
secrets PairingSecretStore
|
||||
remote func(string) (OnboardingRemote, error)
|
||||
security SecurityEventPreparer
|
||||
}
|
||||
|
||||
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 (service *PairingService) Start(ctx context.Context, input PairingInput, traceID string) (PairingExchange, error) {
|
||||
draft, err := NewDraft(input)
|
||||
if err != nil {
|
||||
return PairingExchange{}, err
|
||||
}
|
||||
draft.LastTraceID = strings.TrimSpace(traceID)
|
||||
draft, err = service.repository.CreateIdentityConfigurationRevision(ctx, draft)
|
||||
if err != nil {
|
||||
return PairingExchange{}, err
|
||||
}
|
||||
remote, err := service.remote(draft.AuthCenterURL)
|
||||
if err != nil {
|
||||
return PairingExchange{}, err
|
||||
}
|
||||
claimed, err := remote.Claim(ctx, strings.TrimSpace(input.OnboardingCode))
|
||||
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 {
|
||||
return PairingExchange{}, err
|
||||
}
|
||||
claimed.ExchangeToken = ""
|
||||
pairing := PairingExchange{
|
||||
ID: pairingID, RevisionID: draft.ID, RemoteExchangeID: claimed.ExchangeID, ExchangeTokenRef: tokenReference,
|
||||
Status: PairingMetadataPending, RemoteVersion: claimed.Version, ExpiresAt: claimed.ExpiresAt,
|
||||
Version: 1, LastTraceID: strings.TrimSpace(traceID),
|
||||
}
|
||||
pairing, err = service.repository.CreateIdentityPairingExchange(ctx, pairing)
|
||||
if err != nil {
|
||||
_ = service.secrets.Delete(ctx, tokenReference)
|
||||
return PairingExchange{}, err
|
||||
}
|
||||
return pairing, nil
|
||||
}
|
||||
|
||||
func (service *PairingService) Continue(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 {
|
||||
return pairing, nil
|
||||
}
|
||||
revision, err := service.repository.IdentityConfigurationRevision(ctx, pairing.RevisionID)
|
||||
if err != nil {
|
||||
return PairingExchange{}, err
|
||||
}
|
||||
token, err := service.secrets.Get(ctx, pairing.ExchangeTokenRef)
|
||||
if err != nil {
|
||||
return PairingExchange{}, err
|
||||
}
|
||||
remote, err := service.remote(revision.AuthCenterURL)
|
||||
if err != nil {
|
||||
clear(token)
|
||||
return PairingExchange{}, err
|
||||
}
|
||||
switch pairing.Status {
|
||||
case PairingMetadataPending:
|
||||
metadata, metadataErr := PairingInput{
|
||||
AuthCenterURL: revision.AuthCenterURL, PublicBaseURL: revision.PublicBaseURL,
|
||||
WebBaseURL: revision.WebBaseURL, LocalTenantKey: revision.LocalTenantKey,
|
||||
}.ConsumerMetadata(true)
|
||||
if metadataErr != nil {
|
||||
clear(token)
|
||||
return PairingExchange{}, metadataErr
|
||||
}
|
||||
view, submitErr := remote.SubmitMetadata(ctx, ClaimedExchange{
|
||||
ExchangeID: pairing.RemoteExchangeID, ExchangeToken: string(token), ExpiresAt: pairing.ExpiresAt, Version: pairing.RemoteVersion,
|
||||
}, metadata, "gateway-pairing-metadata-"+pairing.ID)
|
||||
clear(token)
|
||||
if submitErr != nil {
|
||||
return PairingExchange{}, submitErr
|
||||
}
|
||||
if _, err := service.updateFromRemote(ctx, pairing, view); err != nil {
|
||||
return PairingExchange{}, err
|
||||
}
|
||||
case PairingPreparing:
|
||||
view, getErr := remote.Get(ctx, pairing.RemoteExchangeID, string(token))
|
||||
clear(token)
|
||||
if getErr != nil {
|
||||
return PairingExchange{}, getErr
|
||||
}
|
||||
updated, err := service.updateFromRemote(ctx, pairing, view)
|
||||
if err != nil {
|
||||
return PairingExchange{}, err
|
||||
}
|
||||
if updated.Status == PairingPreparing {
|
||||
return updated, nil
|
||||
}
|
||||
case PairingReady:
|
||||
delivery, deliveryErr := remote.DeliverCredential(ctx, Exchange{
|
||||
ExchangeID: pairing.RemoteExchangeID, ApplicationID: revision.ApplicationID,
|
||||
Status: ExchangeReady, Version: pairing.RemoteVersion, ExpiresAt: pairing.ExpiresAt,
|
||||
}, string(token), "gateway-pairing-credential-"+uuid.NewString())
|
||||
clear(token)
|
||||
if deliveryErr != nil {
|
||||
return PairingExchange{}, deliveryErr
|
||||
}
|
||||
updatedRevision, saveErr := service.saveDelivery(ctx, revision, pairing, delivery)
|
||||
if saveErr != nil {
|
||||
return PairingExchange{}, saveErr
|
||||
}
|
||||
_ = updatedRevision
|
||||
if _, err := service.repository.UpdateIdentityPairingExchange(ctx, pairing.ID, pairing.Version, PairingExchangeUpdate{
|
||||
Status: PairingCredentialsSaved, RemoteVersion: delivery.Version,
|
||||
}); err != nil {
|
||||
return PairingExchange{}, err
|
||||
}
|
||||
case PairingCredentialsSaved:
|
||||
if revision.SessionRevocation {
|
||||
if service.security == nil {
|
||||
clear(token)
|
||||
return PairingExchange{}, errors.New("security event preparation is unavailable")
|
||||
}
|
||||
secret, secretErr := service.secrets.Get(ctx, revision.MachineCredentialRef)
|
||||
if secretErr != nil {
|
||||
clear(token)
|
||||
return PairingExchange{}, secretErr
|
||||
}
|
||||
prepareErr := service.security.PrepareSecurityEvents(ctx, revision, secret)
|
||||
clear(secret)
|
||||
if prepareErr != nil {
|
||||
clear(token)
|
||||
return PairingExchange{}, prepareErr
|
||||
}
|
||||
}
|
||||
completeErr := remote.Complete(ctx, pairing.RemoteExchangeID, string(token), "gateway-pairing-complete-"+pairing.ID, pairing.RemoteVersion)
|
||||
clear(token)
|
||||
if completeErr != nil {
|
||||
return PairingExchange{}, completeErr
|
||||
}
|
||||
completed, err := service.repository.UpdateIdentityPairingExchange(ctx, pairing.ID, pairing.Version, PairingExchangeUpdate{
|
||||
Status: PairingCompleted, RemoteVersion: pairing.RemoteVersion,
|
||||
})
|
||||
if err != nil {
|
||||
return PairingExchange{}, err
|
||||
}
|
||||
_ = service.secrets.Delete(ctx, pairing.ExchangeTokenRef)
|
||||
return completed, nil
|
||||
default:
|
||||
clear(token)
|
||||
return PairingExchange{}, ErrRevisionConflict
|
||||
}
|
||||
}
|
||||
return service.repository.IdentityPairingExchange(ctx, pairingID)
|
||||
}
|
||||
|
||||
func (service *PairingService) saveDelivery(ctx context.Context, revision Revision, pairing PairingExchange, delivery CredentialDelivery) (Revision, error) {
|
||||
if err := delivery.Manifest.Validate(); err != nil {
|
||||
return Revision{}, err
|
||||
}
|
||||
machineReference := ""
|
||||
if delivery.Manifest.Clients.MachineToMachine != nil {
|
||||
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
|
||||
secret := []byte(delivery.MachineCredential.ClientSecret)
|
||||
delivery.MachineCredential.ClientSecret = ""
|
||||
if err := service.secrets.Put(ctx, machineReference, secret); err != nil {
|
||||
clear(secret)
|
||||
return Revision{}, err
|
||||
}
|
||||
clear(secret)
|
||||
}
|
||||
sessionReference := ""
|
||||
if delivery.Manifest.Clients.BrowserLogin != nil {
|
||||
sessionReference = "identity-session-" + revision.ID
|
||||
key := make([]byte, 32)
|
||||
if _, err := rand.Read(key); err != nil {
|
||||
_ = service.secrets.Delete(ctx, machineReference)
|
||||
return Revision{}, err
|
||||
}
|
||||
if err := service.secrets.Put(ctx, sessionReference, key); err != nil {
|
||||
clear(key)
|
||||
_ = service.secrets.Delete(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,
|
||||
})
|
||||
if err != nil {
|
||||
if machineReference != "" {
|
||||
_ = service.secrets.Delete(ctx, machineReference)
|
||||
}
|
||||
if sessionReference != "" {
|
||||
_ = service.secrets.Delete(ctx, sessionReference)
|
||||
}
|
||||
return Revision{}, err
|
||||
}
|
||||
return updated, nil
|
||||
}
|
||||
|
||||
func (service *PairingService) updateFromRemote(ctx context.Context, pairing PairingExchange, remote Exchange) (PairingExchange, error) {
|
||||
status := PairingPreparing
|
||||
switch remote.Status {
|
||||
case ExchangeReady, ExchangeCredentialDelivered:
|
||||
status = PairingReady
|
||||
case ExchangeCompleted:
|
||||
status = PairingFailed
|
||||
remote.LastErrorCategory = "remote_state_invalid"
|
||||
case ExchangeFailed:
|
||||
status = PairingFailed
|
||||
case ExchangeExpired:
|
||||
status = PairingExpired
|
||||
}
|
||||
return service.repository.UpdateIdentityPairingExchange(ctx, pairing.ID, pairing.Version, PairingExchangeUpdate{
|
||||
Status: status, RemoteVersion: remote.Version, LastErrorCategory: remote.LastErrorCategory, AuthCenterAuditID: remote.AuditID,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
package identity
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type pairingRepositoryFake struct {
|
||||
revision Revision
|
||||
exchange PairingExchange
|
||||
}
|
||||
|
||||
func (f *pairingRepositoryFake) CreateIdentityConfigurationRevision(_ context.Context, revision Revision) (Revision, error) {
|
||||
f.revision = revision
|
||||
return revision, nil
|
||||
}
|
||||
func (f *pairingRepositoryFake) IdentityConfigurationRevision(context.Context, string) (Revision, error) {
|
||||
return f.revision, nil
|
||||
}
|
||||
func (f *pairingRepositoryFake) ApplyIdentityManifest(_ context.Context, _ string, _ int64, applied ManifestApplication) (Revision, error) {
|
||||
revision, err := ApplyManifest(f.revision, applied)
|
||||
if err != nil {
|
||||
return Revision{}, err
|
||||
}
|
||||
revision.Version++
|
||||
f.revision = revision
|
||||
return revision, nil
|
||||
}
|
||||
func (f *pairingRepositoryFake) CreateIdentityPairingExchange(_ context.Context, exchange PairingExchange) (PairingExchange, error) {
|
||||
f.exchange = exchange
|
||||
return exchange, nil
|
||||
}
|
||||
func (f *pairingRepositoryFake) IdentityPairingExchange(context.Context, string) (PairingExchange, error) {
|
||||
return f.exchange, nil
|
||||
}
|
||||
func (f *pairingRepositoryFake) UpdateIdentityPairingExchange(_ context.Context, id string, expected int64, update PairingExchangeUpdate) (PairingExchange, error) {
|
||||
if f.exchange.ID != id || f.exchange.Version != expected {
|
||||
return PairingExchange{}, ErrRevisionConflict
|
||||
}
|
||||
f.exchange.Status, f.exchange.RemoteVersion = update.Status, update.RemoteVersion
|
||||
f.exchange.LastErrorCategory, f.exchange.AuthCenterAuditID = update.LastErrorCategory, update.AuthCenterAuditID
|
||||
f.exchange.Version++
|
||||
return f.exchange, nil
|
||||
}
|
||||
|
||||
type secretStoreFake struct {
|
||||
values map[string][]byte
|
||||
failMachine bool
|
||||
}
|
||||
|
||||
func (f *secretStoreFake) Put(_ context.Context, reference string, value []byte) error {
|
||||
if f.failMachine && len(reference) > len("identity-machine-") && reference[:len("identity-machine-")] == "identity-machine-" {
|
||||
return errors.New("secret store unavailable")
|
||||
}
|
||||
if f.values == nil {
|
||||
f.values = map[string][]byte{}
|
||||
}
|
||||
f.values[reference] = append([]byte(nil), value...)
|
||||
return nil
|
||||
}
|
||||
func (f *secretStoreFake) Get(_ context.Context, reference string) ([]byte, error) {
|
||||
value, ok := f.values[reference]
|
||||
if !ok {
|
||||
return nil, errors.New("secret not found")
|
||||
}
|
||||
return append([]byte(nil), value...), nil
|
||||
}
|
||||
func (f *secretStoreFake) Delete(_ context.Context, reference string) error {
|
||||
delete(f.values, reference)
|
||||
return nil
|
||||
}
|
||||
|
||||
type onboardingRemoteFake struct {
|
||||
claimed ClaimedExchange
|
||||
view Exchange
|
||||
delivery CredentialDelivery
|
||||
deliveryCalls int
|
||||
completed bool
|
||||
}
|
||||
|
||||
func (f *onboardingRemoteFake) Claim(context.Context, string) (ClaimedExchange, error) {
|
||||
return f.claimed, nil
|
||||
}
|
||||
func (f *onboardingRemoteFake) SubmitMetadata(context.Context, ClaimedExchange, ConsumerMetadata, string) (Exchange, error) {
|
||||
f.view.Status, f.view.Version = ExchangePreparing, 2
|
||||
return f.view, nil
|
||||
}
|
||||
func (f *onboardingRemoteFake) Get(context.Context, string, string) (Exchange, error) {
|
||||
f.view.Status, f.view.Version = ExchangeReady, 3
|
||||
return f.view, nil
|
||||
}
|
||||
func (f *onboardingRemoteFake) DeliverCredential(context.Context, Exchange, string, string) (CredentialDelivery, error) {
|
||||
f.deliveryCalls++
|
||||
f.delivery.MachineCredential.ClientSecret = "rotated-machine-secret-value-000000000000-" + string(rune('0'+f.deliveryCalls))
|
||||
f.delivery.Version = int64(3 + f.deliveryCalls)
|
||||
return f.delivery, nil
|
||||
}
|
||||
func (f *onboardingRemoteFake) Complete(context.Context, string, string, string, int64) error {
|
||||
f.completed = true
|
||||
return nil
|
||||
}
|
||||
|
||||
type securityEventPreparerFake struct{ called bool }
|
||||
|
||||
func (f *securityEventPreparerFake) PrepareSecurityEvents(context.Context, Revision, []byte) error {
|
||||
f.called = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestPairingRetriesWithRotatedCredentialAfterSecretStoreFailure(t *testing.T) {
|
||||
repository := &pairingRepositoryFake{}
|
||||
secrets := &secretStoreFake{values: map[string][]byte{}, failMachine: true}
|
||||
remote := &onboardingRemoteFake{
|
||||
claimed: ClaimedExchange{ExchangeID: "11111111-1111-1111-1111-111111111111", ExchangeToken: "exchange-token-value", Version: 1, ExpiresAt: time.Now().Add(time.Hour)},
|
||||
view: Exchange{ExchangeID: "11111111-1111-1111-1111-111111111111", ApplicationID: "22222222-2222-2222-2222-222222222222", Version: 1, ExpiresAt: time.Now().Add(time.Hour)},
|
||||
delivery: CredentialDelivery{Manifest: ManifestV1{
|
||||
SchemaVersion: 1, Issuer: "https://auth.example.com/issuer/shared", TenantID: "33333333-3333-3333-3333-333333333333",
|
||||
ApplicationID: "22222222-2222-2222-2222-222222222222", Audience: "urn:easyai:resource:22222222-2222-2222-2222-222222222222",
|
||||
Capabilities: []string{"oidc_login", "api_access", "machine_to_machine", "token_introspection", "session_revocation"}, Scopes: []string{"openid", "gateway.access"},
|
||||
Clients: ManifestClients{BrowserLogin: &ManifestClient{ClientID: "browser"}, MachineToMachine: &ManifestClient{ClientID: "service"}},
|
||||
SecurityEvents: &ManifestSecurityEvents{TransmitterIssuer: "https://auth.example.com/ssf", ConfigurationEndpoint: "https://auth.example.com/.well-known/ssf-configuration/ssf", Audience: "urn:easyai:ssf:receiver:22222222-2222-2222-2222-222222222222"},
|
||||
}, MachineCredential: &MachineCredential{ClientID: "service", ClientSecret: "placeholder", IssuedAt: time.Now()}},
|
||||
}
|
||||
preparer := &securityEventPreparerFake{}
|
||||
service := NewPairingService(repository, secrets, func(string) (OnboardingRemote, error) { return remote, nil }, preparer)
|
||||
pairing, err := service.Start(context.Background(), PairingInput{
|
||||
AuthCenterURL: "https://auth.example.com", OnboardingCode: "one-time-code-value",
|
||||
PublicBaseURL: "https://api.example.com", WebBaseURL: "https://gateway.example.com", LocalTenantKey: "default",
|
||||
}, "trace-pairing")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, ok := secrets.values[pairing.ExchangeTokenRef]; !ok {
|
||||
t.Fatal("exchange token was not saved in SecretStore")
|
||||
}
|
||||
|
||||
if _, err := service.Continue(context.Background(), pairing.ID); err == nil {
|
||||
t.Fatal("machine SecretStore failure was ignored")
|
||||
}
|
||||
if remote.completed || repository.revision.ApplicationID != "" {
|
||||
t.Fatal("failed secret save advanced the exchange or revision")
|
||||
}
|
||||
secrets.failMachine = false
|
||||
completed, err := service.Continue(context.Background(), pairing.ID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !remote.completed || completed.Status != PairingCompleted || remote.deliveryCalls != 2 || !preparer.called {
|
||||
t.Fatalf("pairing did not recover safely: pairing=%#v calls=%d completed=%v", completed, remote.deliveryCalls, remote.completed)
|
||||
}
|
||||
if repository.revision.MachineCredentialRef == "" || repository.revision.SessionEncryptionKeyRef == "" {
|
||||
t.Fatalf("revision did not retain SecretStore references: %#v", repository.revision)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user