fix(auth): close Windows remediation review findings

This commit is contained in:
2026-08-18 12:46:54 +02:00
parent fa499a9bdd
commit cd5f505c8a
10 changed files with 310 additions and 33 deletions
+2
View File
@@ -171,6 +171,7 @@ func TestCreateReferencesAuthFilesByDefaultAndArchivesThemOnlyWithSecretCustody(
if err := os.WriteFile(usersPath, []byte("users:\n - passwordHash: must-not-be-archived-by-default\n"), 0o600); err != nil {
t.Fatal(err)
}
prepareBackupFixturePrivatePaths(t, []string{authDirectory}, []string{authPath, usersPath})
fixture.installation.Authentication.ConfigDirectory = authDirectory
defaultOutput := filepath.Join(t.TempDir(), "default.zip")
@@ -781,6 +782,7 @@ func newBackupFixture(t *testing.T, profile string) *backupFixture {
},
}
fixture.writeEnvironment(t)
prepareBackupFixturePrivatePaths(t, []string{descriptorDirectory, secretDirectory}, []string{environment, secretPath})
currentImage := fixture.installation.CurrentImageOverridePath()
if err := os.MkdirAll(filepath.Dir(currentImage), 0o700); err != nil {
t.Fatal(err)
@@ -0,0 +1,9 @@
//go:build !windows
package backup
import "testing"
func prepareBackupFixturePrivatePaths(t *testing.T, directories, files []string) {
t.Helper()
}
@@ -0,0 +1,23 @@
//go:build windows
package backup
import (
"testing"
"github.com/aritmolab/thothii/tools/tht/internal/safeio"
)
func prepareBackupFixturePrivatePaths(t *testing.T, directories, files []string) {
t.Helper()
for _, path := range directories {
if err := safeio.ProtectPrivateDirectory(path); err != nil {
t.Fatalf("ProtectPrivateDirectory(%q): %v", path, err)
}
}
for _, path := range files {
if err := safeio.ProtectPrivateRegular(path); err != nil {
t.Fatalf("ProtectPrivateRegular(%q): %v", path, err)
}
}
}
+15 -1
View File
@@ -10,6 +10,7 @@ import (
"io"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"time"
@@ -43,6 +44,7 @@ func TestPreflightReturnsValidatedMetadataAndCallsAllTargetChecksWithoutExtracti
if err != nil {
t.Fatal(err)
}
defer result.CloseArchive()
if result.Manifest.InstallationID != "local-dev" || result.Manifest.Entries[0].Path != "configuration/operator.env" {
t.Fatalf("validated metadata = %#v", result)
}
@@ -197,10 +199,11 @@ func TestPreflightRequiresExplicitProtectionForExternalSecretPayloadsWithoutLeak
if err == nil || strings.Contains(err.Error(), string(secret)) || !strings.Contains(err.Error(), "confirmation") {
t.Fatalf("secret policy error = %v", err)
}
_, err = Preflight(context.Background(), installation, PreflightRequest{Archive: archive, Confirm: true, AllowExternalSecrets: true}, permissivePreflightDependencies())
result, err := Preflight(context.Background(), installation, PreflightRequest{Archive: archive, Confirm: true, AllowExternalSecrets: true}, permissivePreflightDependencies())
if err != nil {
t.Fatal(err)
}
defer result.CloseArchive()
}
func TestPreflightRejectsInsufficientDiskAndEachTargetCompatibilityFailure(t *testing.T) {
@@ -392,6 +395,17 @@ func TestPreflightRevalidationRefusesAnArchivePathThatWasReplaced(t *testing.T)
writePreflightArchive(t, replacement, preflightArchiveSpec{
entries: []preflightArchiveEntry{{path: "configuration/operator.env", body: []byte("unchecked")}},
})
if runtime.GOOS == "windows" {
// The retained validated handle denies replacement on Windows; that is the
// stronger invariant, so revalidation must still succeed for the unchanged path.
if err := os.Rename(replacement, archive); err == nil {
t.Fatal("Windows replaced a retained archive path")
}
if _, err := result.RevalidateArchive(); err != nil {
t.Fatalf("RevalidateArchive() on retained Windows archive = %v", err)
}
return
}
if err := os.Rename(replacement, archive); err != nil {
t.Fatal(err)
}
+106 -14
View File
@@ -308,6 +308,64 @@ func TestRestoreStagesArchiveAfterCheckpointAndRejectsMutation(t *testing.T) {
}
}
type restoreLifecycleTestOutcome struct {
result RestoreResult
err error
}
func releaseLifecycleStage(ctx context.Context, release chan<- struct{}, done <-chan restoreLifecycleTestOutcome) (restoreLifecycleTestOutcome, bool, error) {
select {
case outcome := <-done:
return outcome, true, nil
default:
}
select {
case release <- struct{}{}:
return restoreLifecycleTestOutcome{}, false, nil
default:
}
timer := time.NewTimer(2 * time.Second)
defer timer.Stop()
var ctxErr error
select {
case outcome := <-done:
return outcome, true, nil
case release <- struct{}{}:
return restoreLifecycleTestOutcome{}, false, nil
case <-ctx.Done():
ctxErr = ctx.Err()
ctx = nil
case <-timer.C:
if ctxErr != nil {
return restoreLifecycleTestOutcome{}, false, ctxErr
}
return restoreLifecycleTestOutcome{}, false, errors.New("timed out releasing lifecycle stage")
}
select {
case outcome := <-done:
return outcome, true, nil
case release <- struct{}{}:
return restoreLifecycleTestOutcome{}, false, nil
case <-timer.C:
return restoreLifecycleTestOutcome{}, false, ctxErr
}
}
func TestReleaseLifecycleStageReturnsPrematureWorkerOutcome(t *testing.T) {
want := errors.New("worker ended before the next lifecycle stage")
done := make(chan restoreLifecycleTestOutcome, 1)
done <- restoreLifecycleTestOutcome{err: want}
got, terminal, err := releaseLifecycleStage(context.Background(), make(chan struct{}), done)
if err != nil {
t.Fatalf("releaseLifecycleStage() error = %v", err)
}
if !terminal || !errors.Is(got.err, want) {
t.Fatalf("releaseLifecycleStage() = outcome %#v, terminal %t; want original worker error", got, terminal)
}
}
func TestRestoreLifecycleLockExcludesCompetingTransactionsUntilTerminalCleanup(t *testing.T) {
targetFailure := errors.New("target restore failed")
recoveryFailure := errors.New("recovery restore failed")
@@ -402,15 +460,15 @@ func TestRestoreLifecycleLockExcludesCompetingTransactionsUntilTerminalCleanup(t
installationID: fixture.installationID,
entries: []preflightArchiveEntry{{path: "configuration/operator.env", body: []byte("checkpoint")}},
})
stages := make(chan string)
continueStage := make(chan struct{})
stages := make(chan string, 1)
continueStage := make(chan struct{}, 1)
caller, cancel := context.WithCancel(context.Background())
defer cancel()
gate := func(stage string) {
stages <- stage
<-continueStage
}
runner.beforeFinalMaintenanceRelease = func() { gate("final-barrier-release") }
caller, cancel := context.WithCancel(context.Background())
defer cancel()
deps := restoreTestDependencies(t, runner)
deps.prepareRecovery = func(ctx context.Context, target config.Installation, _ string) (PreflightResult, error) {
return Preflight(ctx, target, PreflightRequest{Archive: recoveryArchive, Confirm: true, AllowExternalSecrets: true}, permissivePreflightDependencies())
@@ -426,32 +484,66 @@ func TestRestoreLifecycleLockExcludesCompetingTransactionsUntilTerminalCleanup(t
}
scenario.configure(&deps, runner, gate, cancel)
type outcome struct {
result RestoreResult
err error
}
done := make(chan outcome, 1)
done := make(chan restoreLifecycleTestOutcome, 1)
go func() {
result, err := restoreWithDependencies(caller, installation, RestoreRequest{Archive: archive, Confirm: true}, deps)
done <- outcome{result: result, err: err}
done <- restoreLifecycleTestOutcome{result: result, err: err}
}()
var failures []error
for _, wantStage := range scenario.stages {
var terminal *restoreLifecycleTestOutcome
for stageIndex, wantStage := range scenario.stages {
receivedStage := false
select {
case stage := <-stages:
receivedStage = true
if stage != wantStage {
failures = append(failures, fmt.Errorf("lifecycle stage = %q, want %q", stage, wantStage))
}
case outcome := <-done:
terminal = &outcome
failures = append(failures, fmt.Errorf("restore ended before lifecycle stage %q: %w", wantStage, outcome.err))
case <-time.After(2 * time.Second):
failures = append(failures, fmt.Errorf("timed out waiting for lifecycle stage %q", wantStage))
select {
case outcome := <-done:
terminal = &outcome
failures = append(failures, fmt.Errorf("restore ended before lifecycle stage %q: %w", wantStage, outcome.err))
default:
failures = append(failures, fmt.Errorf("timed out waiting for lifecycle stage %q", wantStage))
cancel()
}
}
if terminal != nil || !receivedStage {
break
}
if err := competingRestoreAndBackupEntry(installation, archive, t); err != nil {
failures = append(failures, fmt.Errorf("%s: %w", wantStage, err))
}
continueStage <- struct{}{}
outcome, workerDone, err := releaseLifecycleStage(caller, continueStage, done)
if err != nil {
failures = append(failures, fmt.Errorf("release lifecycle stage %q: %w", wantStage, err))
cancel()
break
}
if workerDone {
terminal = &outcome
if stageIndex+1 < len(scenario.stages) {
failures = append(failures, fmt.Errorf("restore ended before lifecycle stage %q: %w", scenario.stages[stageIndex+1], outcome.err))
}
break
}
}
var result restoreLifecycleTestOutcome
if terminal != nil {
result = *terminal
} else {
select {
case result = <-done:
case <-time.After(2 * time.Second):
failures = append(failures, errors.New("timed out waiting for restore worker outcome"))
cancel()
}
}
result := <-done
for _, wantErr := range scenario.wantErrors {
if !errors.Is(result.err, wantErr) {
failures = append(failures, fmt.Errorf("restore error = %v, want %v", result.err, wantErr))