test(auth): gate local and OIDC authentication release

This commit is contained in:
2026-08-18 06:02:25 +02:00
parent 7cf7d9db6b
commit 8a3fa5031d
23 changed files with 1711 additions and 168 deletions
+26 -21
View File
@@ -9,6 +9,7 @@ import (
"time"
"github.com/aritmolab/thothii/tools/tht/internal/config"
"github.com/aritmolab/thothii/tools/tht/internal/lifecycle"
)
var ErrRestoreConfirmationRequired = errors.New("restore requires --yes")
@@ -29,19 +30,17 @@ type RestoreResult struct {
Verified bool
}
type restoreLock interface{ Release() error }
type restoreVerify func(context.Context, config.Installation, archiveRunner) error
type restoreDependencies struct {
preflight func(context.Context, config.Installation, PreflightRequest) (PreflightResult, error)
// checkpointLocked creates the secret-aware recovery archive while the caller already owns
// the installation lifecycle lock. It must not call public Create, which would re-acquire the
// non-reentrant lock and deadlock the restore transaction.
checkpointLocked func(context.Context, config.Installation, CreateRequest) (Result, 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, bool) error
cleanupCheckpoint func(string) error
acquireLock func(config.Installation) (restoreLock, error)
acquireTransaction func(config.Installation) (*lifecycle.Transaction, error)
runner archiveRunner
sleep func(duration time.Duration)
restoreFile func(context.Context, config.Installation, ArchiveEntryMetadata, io.Reader) error
@@ -84,36 +83,32 @@ func restoreWithDependencies(ctx context.Context, installation config.Installati
if request.Archive == "" {
return RestoreResult{}, errors.New("restore archive is required")
}
if deps.preflight == nil || deps.checkpointLocked == nil || deps.prepareRecovery == nil || deps.recover == nil || deps.cleanupCheckpoint == nil || deps.acquireLock == nil || deps.runner == nil || deps.restoreFile == nil || deps.restoreVolume == nil || deps.resetAuthenticationState == nil || deps.verify == nil {
if deps.preflight == nil || deps.checkpoint == nil || deps.prepareRecovery == nil || deps.recover == nil || deps.cleanupCheckpoint == nil || deps.acquireTransaction == nil || deps.runner == nil || deps.restoreFile == nil || deps.restoreVolume == nil || deps.resetAuthenticationState == nil || deps.verify == nil {
return RestoreResult{}, errors.New("restore dependencies are incomplete")
}
preflight, err := deps.preflight(ctx, installation, PreflightRequest{Archive: request.Archive, Confirm: true, AllowExternalSecrets: true})
if err != nil {
return RestoreResult{}, err
}
archive, err := preflight.RevalidateArchive()
if err != nil {
_ = preflight.CloseArchive()
return RestoreResult{}, err
}
// Lock ordering is lifecycle lock -> Compose/operator maintenance barrier. The core operator
// command never acquires the host lifecycle lock, so this order cannot form a lock cycle with
// Docker Compose or the durable maintenance marker.
lock, err := deps.acquireLock(installation)
transaction, err := deps.acquireTransaction(installation)
if err != nil {
_ = preflight.CloseArchive()
return result, err
}
defer func() {
if releaseErr := lock.Release(); releaseErr != nil {
if releaseErr := transaction.Release(); releaseErr != nil {
result = RestoreResult{}
resultErr = errors.Join(resultErr, fmt.Errorf("release restore lifecycle lock: %w", releaseErr))
}
}()
// Target-dependent preflight is intentionally inside the lifecycle transaction. This binds
// target ownership, volume, image, and free-space checks to the later mutation.
preflight, err := deps.preflight(ctx, installation, PreflightRequest{Archive: request.Archive, Confirm: true, AllowExternalSecrets: true})
if err != nil {
return RestoreResult{}, err
}
defer preflight.CloseArchive()
checkpoint, err := deps.checkpointLocked(ctx, installation, CreateRequest{IncludeSecrets: true, Confirm: true})
checkpoint, err := deps.checkpoint(ctx, transaction, installation, CreateRequest{IncludeSecrets: true, Confirm: true})
if err != nil {
return result, fmt.Errorf("create recovery checkpoint: %w", err)
}
@@ -217,8 +212,18 @@ func restoreWithDependencies(ctx context.Context, installation config.Installati
return result, err
}
}
staged, err := preflight.StageArchive(ctx)
if err != nil {
return result, err
}
defer func() {
if closeErr := staged.Close(); closeErr != nil {
result = RestoreResult{}
resultErr = errors.Join(resultErr, closeErr)
}
}()
state.mutated = true
if err := restoreVerifiedEntries(ctx, installation, preflight, archive, deps.restoreFile, deps.restoreVolume); err != nil {
if err := restoreVerifiedEntries(ctx, installation, preflight, staged.file, deps.restoreFile, deps.restoreVolume); err != nil {
return result, err
}
if err := deps.resetAuthenticationState(ctx, installation, deps.runner); err != nil {