feat(server): activate projected authentication safely
This commit is contained in:
@@ -8,6 +8,7 @@ import (
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/aritmolab/thothii/tools/tht/internal/authconfig"
|
||||
"github.com/aritmolab/thothii/tools/tht/internal/config"
|
||||
"github.com/aritmolab/thothii/tools/tht/internal/lifecycle"
|
||||
)
|
||||
@@ -32,13 +33,20 @@ type RestoreResult struct {
|
||||
|
||||
type restoreVerify func(context.Context, config.Installation, archiveRunner) error
|
||||
|
||||
type authProjectionRestoreTransaction interface {
|
||||
PublishCanonical() (authconfig.ProjectionStatus, error)
|
||||
RestorePriorIfCanonicalUnchanged() error
|
||||
Close() error
|
||||
}
|
||||
|
||||
type restoreDependencies struct {
|
||||
preflight func(context.Context, config.Installation, PreflightRequest) (PreflightResult, error)
|
||||
// checkpoint requires the opaque capability created by lifecycle acquisition. It must not call
|
||||
// public Create, which would re-acquire the non-reentrant lock and deadlock the transaction.
|
||||
checkpoint func(context.Context, *lifecycle.Transaction, config.Installation, CreateRequest) (Result, error)
|
||||
prepareRecovery func(context.Context, config.Installation, string) (PreflightResult, error)
|
||||
recover func(context.Context, config.Installation, PreflightResult, *stagedArchive, bool) error
|
||||
recover func(context.Context, config.Installation, PreflightResult, *stagedArchive, bool, authProjectionRestoreTransaction) error
|
||||
beginAuthProjection func(context.Context, config.Installation) (authProjectionRestoreTransaction, error)
|
||||
cleanupCheckpoint func(string) error
|
||||
acquireTransaction func(config.Installation) (*lifecycle.Transaction, error)
|
||||
runner archiveRunner
|
||||
@@ -107,6 +115,12 @@ func restoreWithDependencies(ctx context.Context, installation config.Installati
|
||||
return RestoreResult{}, err
|
||||
}
|
||||
defer preflight.CloseArchive()
|
||||
authRestoreRequired := installation.HasRuntimeAuthProjection() && manifestArchivesAuthentication(preflight.Manifest)
|
||||
if authRestoreRequired {
|
||||
if err := requireAuthProjectionRestorePrivilege(); err != nil {
|
||||
return result, err
|
||||
}
|
||||
}
|
||||
|
||||
checkpoint, err := deps.checkpoint(ctx, transaction, installation, CreateRequest{IncludeSecrets: true, Confirm: true})
|
||||
if err != nil {
|
||||
@@ -150,11 +164,24 @@ func restoreWithDependencies(ctx context.Context, installation config.Installati
|
||||
return result, errors.Join(err, cleanupErr)
|
||||
}
|
||||
|
||||
var authTransaction authProjectionRestoreTransaction
|
||||
if authRestoreRequired {
|
||||
if deps.beginAuthProjection == nil {
|
||||
cleanupErr := deps.cleanupCheckpoint(checkpoint.Path)
|
||||
return result, errors.Join(errors.New("restore authentication projection dependency is unavailable"), cleanupErr)
|
||||
}
|
||||
authTransaction, err = deps.beginAuthProjection(ctx, installation)
|
||||
if err != nil {
|
||||
cleanupErr := deps.cleanupCheckpoint(checkpoint.Path)
|
||||
return result, errors.Join(errors.New("restore authentication projection could not be blocked"), cleanupErr)
|
||||
}
|
||||
}
|
||||
|
||||
state := restoreTransactionState{}
|
||||
defer func() {
|
||||
if state.recoveryRequired(resultErr) {
|
||||
recoveryContext, cancel := boundedCleanupContext()
|
||||
recoveryErr := deps.recover(recoveryContext, installation, recovery, recoveryStage, state.wasRunning)
|
||||
recoveryErr := deps.recover(recoveryContext, installation, recovery, recoveryStage, state.wasRunning, authTransaction)
|
||||
cancel()
|
||||
if recoveryErr != nil {
|
||||
resultErr = errors.Join(resultErr, fmt.Errorf("restore recovery checkpoint: %w", recoveryErr))
|
||||
@@ -174,6 +201,20 @@ func restoreWithDependencies(ctx context.Context, installation config.Installati
|
||||
checkpointCleanupSucceeded = false
|
||||
cleanupErr = errors.Join(cleanupErr, fmt.Errorf("destroy recovery checkpoint: %w", checkpointErr))
|
||||
}
|
||||
authCleanupSucceeded := true
|
||||
if authTransaction != nil {
|
||||
if resultErr != nil && !state.mutated {
|
||||
if restoreErr := authTransaction.RestorePriorIfCanonicalUnchanged(); restoreErr != nil {
|
||||
authCleanupSucceeded = false
|
||||
cleanupErr = errors.Join(cleanupErr, errors.New("restore authentication projection could not restore its prior selector"))
|
||||
}
|
||||
}
|
||||
if closeErr := authTransaction.Close(); closeErr != nil {
|
||||
authCleanupSucceeded = false
|
||||
cleanupErr = errors.Join(cleanupErr, errors.New("restore authentication projection transaction could not be closed"))
|
||||
}
|
||||
authTransaction = nil
|
||||
}
|
||||
if !state.maintenanceAttempted {
|
||||
if cleanupErr != nil {
|
||||
result = RestoreResult{}
|
||||
@@ -204,7 +245,7 @@ func restoreWithDependencies(ctx context.Context, installation config.Installati
|
||||
}
|
||||
// A failed checkpoint recovery deliberately leaves admissions blocked. Starting or
|
||||
// deactivating at that point would expose an unverified, possibly partial restore.
|
||||
if state.mayDeactivateMaintenance() && restartCompleted {
|
||||
if state.mayDeactivateMaintenance() && restartCompleted && authCleanupSucceeded {
|
||||
deactivateErr, deactivated := retryBoundedCleanup(func(cleanupContext context.Context) error {
|
||||
return maintenance(cleanupContext, installation, deps.runner, false)
|
||||
})
|
||||
@@ -250,6 +291,11 @@ func restoreWithDependencies(ctx context.Context, installation config.Installati
|
||||
if err := deps.resetAuthenticationState(ctx, installation, deps.runner); err != nil {
|
||||
return result, fmt.Errorf("reset authentication state: %w", err)
|
||||
}
|
||||
if authTransaction != nil {
|
||||
if err := publishRestoredAuthentication(authTransaction); err != nil {
|
||||
return result, err
|
||||
}
|
||||
}
|
||||
if state.wasRunning {
|
||||
if err := composeStartAndVerify(ctx, installation, deps.runner); err != nil {
|
||||
return result, err
|
||||
@@ -265,6 +311,26 @@ func restoreWithDependencies(ctx context.Context, installation config.Installati
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func manifestArchivesAuthentication(manifest Manifest) bool {
|
||||
for _, entry := range manifest.Entries {
|
||||
if entry.Archived && entry.Kind == EntryExternalSecret && entry.Owner == "authentication-configuration" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func publishRestoredAuthentication(transaction authProjectionRestoreTransaction) error {
|
||||
status, err := transaction.PublishCanonical()
|
||||
if err != nil || status.State != "ready" || !status.Equal {
|
||||
if err != nil {
|
||||
return fmt.Errorf("publish restored authentication projection: %w", err)
|
||||
}
|
||||
return errors.New("publish restored authentication projection")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ensureCombinedRestoreCapacity(candidate, recovery PreflightResult) error {
|
||||
if candidate.freeBytes == nil || candidate.stagingRoot == "" || candidate.stagingRoot != recovery.stagingRoot {
|
||||
return errors.New("candidate and recovery archives do not share controlled restore staging")
|
||||
|
||||
Reference in New Issue
Block a user