feat(server): activate projected authentication safely

This commit is contained in:
User
2026-08-22 01:01:36 +02:00
parent 903c0b4de5
commit 3d9a9f0675
32 changed files with 1837 additions and 38 deletions
+69 -3
View File
@@ -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")