fix(windows): protect lifecycle and backup state

This commit is contained in:
2026-08-18 14:05:52 +02:00
parent b6396e66fa
commit ada3f9fc7f
7 changed files with 122 additions and 21 deletions
+6
View File
@@ -186,6 +186,12 @@ func createWithDependenciesTransaction(ctx context.Context, transaction *lifecyc
if err != nil {
return Result{}, err
}
if request.IncludeSecrets {
if err := safeio.ProtectPrivateRegular(reservation.path); err != nil {
_ = reservation.RemoveIfOwned()
return Result{}, errors.New("protect secret-bearing backup output")
}
}
published := false
defer func() {
if !published {
+24 -5
View File
@@ -11,6 +11,7 @@ import (
"io"
"os"
"path/filepath"
"runtime"
"sort"
"strings"
"testing"
@@ -19,6 +20,7 @@ import (
"github.com/aritmolab/thothii/tools/tht/internal/compose"
"github.com/aritmolab/thothii/tools/tht/internal/config"
"github.com/aritmolab/thothii/tools/tht/internal/lifecycle"
"github.com/aritmolab/thothii/tools/tht/internal/safeio"
)
var requiredTestVolumes = []string{"settings", "pi-state", "workspace-registry", "workspace-secrets", "sessions", "qdrant-data", "embedding-models"}
@@ -193,7 +195,7 @@ func TestCreateReferencesAuthFilesByDefaultAndArchivesThemOnlyWithSecretCustody(
t.Fatalf("default backup did not record only the auth.yaml configuration path: %#v", defaultArchive.manifest.Entries)
}
secretOutput := filepath.Join(t.TempDir(), "with-auth-secrets.zip")
secretOutput := canonicalBackupOutput(t, "with-auth-secrets.zip")
secretResult, err := createWithDependencies(context.Background(), fixture.installation, CreateRequest{Output: secretOutput, IncludeSecrets: true, Confirm: true}, testDependencies(t, newBackupRunner(fixture.installation, false)))
if err != nil {
t.Fatal(err)
@@ -410,7 +412,7 @@ func TestCreateExcludesExternalSecretPayloadsByDefaultButRecordsDigests(t *testi
func TestCreateRequiresConfirmationToIncludeSecretsAndUsesOwnerOnlyMode(t *testing.T) {
fixture := newBackupFixture(t, "local")
output := filepath.Join(t.TempDir(), "with-secrets.zip")
output := canonicalBackupOutput(t, "with-secrets.zip")
request := CreateRequest{Output: output, IncludeSecrets: true}
if _, err := createWithDependencies(context.Background(), fixture.installation, request, testDependencies(t, newBackupRunner(fixture.installation, false))); !errors.Is(err, ErrConfirmationRequired) {
t.Fatalf("Create() error = %v, want ErrConfirmationRequired", err)
@@ -428,8 +430,13 @@ func TestCreateRequiresConfirmationToIncludeSecretsAndUsesOwnerOnlyMode(t *testi
if err != nil {
t.Fatal(err)
}
if got := info.Mode().Perm(); got != 0o600 {
t.Fatalf("archive mode = %#o, want 0600", got)
if err := safeio.ValidatePrivateRegular(output); err != nil {
t.Fatalf("secret-bearing archive protection = %v", err)
}
if runtime.GOOS != "windows" {
if got := info.Mode().Perm(); got != 0o600 {
t.Fatalf("archive mode = %#o, want 0600", got)
}
}
archive := readFixtureArchive(t, output)
if !archive.manifest.IncludesSecrets || !bytes.Contains(bytes.Join(mapValues(archive.files), nil), []byte(fixture.secretValue)) {
@@ -550,7 +557,10 @@ func TestCreateTransactionCapabilityRefusesUnlockedOrForeignInstallation(t *test
t.Fatalf("Docker runner was called without a transaction capability: %v", runner.calls)
}
otherRoot := t.TempDir()
otherRoot, err := filepath.EvalSymlinks(t.TempDir())
if err != nil {
t.Fatal(err)
}
other := config.Installation{ProjectDirectory: otherRoot, Path: filepath.Join(otherRoot, "thothii-installation.yaml")}
transaction, err := lifecycle.AcquireTransaction(other)
if err != nil {
@@ -798,6 +808,15 @@ func (fixture *backupFixture) writeEnvironment(t *testing.T) {
}
}
func canonicalBackupOutput(t *testing.T, name string) string {
t.Helper()
directory, err := filepath.EvalSymlinks(t.TempDir())
if err != nil {
t.Fatal(err)
}
return filepath.Join(directory, name)
}
type fakeBackupRunner struct {
installation config.Installation
running bool
+12 -1
View File
@@ -16,6 +16,7 @@ import (
"time"
"github.com/aritmolab/thothii/tools/tht/internal/config"
"github.com/aritmolab/thothii/tools/tht/internal/safeio"
)
func TestPreflightReturnsValidatedMetadataAndCallsAllTargetChecksWithoutExtracting(t *testing.T) {
@@ -573,10 +574,20 @@ func preflightTestInstallation(t *testing.T) config.Installation {
if err != nil {
t.Fatal(err)
}
return config.Installation{
installation := config.Installation{
Path: filepath.Join(root, "deploy", "local-dev", "thothii-installation.yaml"),
ProjectDirectory: root,
}
controlParent := filepath.Dir(installation.ControlDirectory())
for _, directory := range []string{controlParent, installation.ControlDirectory()} {
if err := os.MkdirAll(directory, 0o700); err != nil {
t.Fatal(err)
}
if err := safeio.ProtectPrivateDirectory(directory); err != nil {
t.Fatalf("protect preflight fixture directory %q: %v", directory, err)
}
}
return installation
}
func permissivePreflightDependencies() PreflightDependencies {
+9 -2
View File
@@ -16,17 +16,21 @@ import (
"github.com/aritmolab/thothii/tools/tht/internal/compose"
"github.com/aritmolab/thothii/tools/tht/internal/config"
"github.com/aritmolab/thothii/tools/tht/internal/lifecycle"
"github.com/aritmolab/thothii/tools/tht/internal/safeio"
)
func TestRestorePublicPathUsesConcreteProductionPreflight(t *testing.T) {
root := t.TempDir()
root, err := filepath.EvalSymlinks(t.TempDir())
if err != nil {
t.Fatal(err)
}
installation := config.Installation{
Path: filepath.Join(root, "deploy", "local-dev", "thothii-installation.yaml"),
ProjectDirectory: root,
}
missing := filepath.Join(root, "missing.zip")
_, err := Restore(context.Background(), installation, RestoreRequest{Archive: missing, Confirm: true})
_, err = Restore(context.Background(), installation, RestoreRequest{Archive: missing, Confirm: true})
if err == nil || strings.Contains(err.Error(), "dependencies are unavailable") || !strings.Contains(err.Error(), "backup archive") {
t.Fatalf("Restore() error = %v, want production archive preflight", err)
@@ -924,6 +928,9 @@ func TestRecoveryCheckpointCleanupRemovesOnlyPrivateRegularFile(t *testing.T) {
if err := os.WriteFile(checkpoint, []byte("secret checkpoint"), 0o600); err != nil {
t.Fatal(err)
}
if err := safeio.ProtectPrivateRegular(checkpoint); err != nil {
t.Fatal(err)
}
if err := cleanupRecoveryCheckpoint(checkpoint); err != nil {
t.Fatal(err)
}