fix(backup): privately stage restore archive

This commit is contained in:
2026-08-18 08:13:13 +02:00
parent df00f6bfa8
commit 54698e7340
8 changed files with 178 additions and 23 deletions
+1 -1
View File
@@ -287,7 +287,7 @@ func (result PreflightResult) StageArchive(ctx context.Context) (_ *stagedArchiv
return nil, errors.New("protect private restore staging directory")
}
path := filepath.Join(directory, "archive.zip")
file, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0o600)
file, err := safeio.CreateCanonicalNewPrivateFile(path)
if err != nil {
_ = os.Remove(directory)
return nil, errors.New("create private restore staging archive")
@@ -455,6 +455,37 @@ func TestPreflightStagesArchiveIntoImmutablePrivateBytes(t *testing.T) {
}
}
func TestStageArchiveCleansPrivateFileAfterStreamingFailure(t *testing.T) {
installation := preflightTestInstallation(t)
if err := os.MkdirAll(installation.ControlDirectory(), 0o700); err != nil {
t.Fatal(err)
}
archive := filepath.Join(t.TempDir(), "checked.zip")
writePreflightArchive(t, archive, preflightArchiveSpec{
entries: []preflightArchiveEntry{{path: "configuration/operator.env", body: []byte("before")}},
})
result, err := Preflight(context.Background(), installation, PreflightRequest{Archive: archive, Confirm: true}, permissivePreflightDependencies())
if err != nil {
t.Fatal(err)
}
defer result.CloseArchive()
result.freeBytes = func(string) (uint64, error) {
result.archive.digest = "after-preflight-mismatch"
return 1024, nil
}
if _, err := result.StageArchive(context.Background()); err == nil || !strings.Contains(err.Error(), "changed") {
t.Fatalf("StageArchive() error = %v, want streaming size refusal", err)
}
entries, err := os.ReadDir(result.stagingRoot)
if err != nil {
t.Fatal(err)
}
if len(entries) != 0 {
t.Fatalf("private staging leftovers = %v, want none", entries)
}
}
func TestPreflightStagingRejectsInPlaceArchiveHashMutation(t *testing.T) {
installation := preflightTestInstallation(t)
archive := filepath.Join(t.TempDir(), "checked.zip")
@@ -10,6 +10,7 @@ import (
"testing"
"github.com/aritmolab/thothii/tools/tht/internal/config"
"github.com/aritmolab/thothii/tools/tht/internal/safeio"
)
func TestStageArchiveRejectsSymlinkedInstallationAncestor(t *testing.T) {
@@ -46,3 +47,35 @@ func TestStageArchiveRejectsSymlinkedInstallationAncestor(t *testing.T) {
t.Fatalf("StageArchive() error = %v, want unsafe symlinked staging-root rejection", err)
}
}
func TestStageArchiveCreatesUnixPrivateRegularFile(t *testing.T) {
installation := preflightTestInstallation(t)
if err := os.MkdirAll(installation.ControlDirectory(), 0o700); err != nil {
t.Fatal(err)
}
archive := filepath.Join(t.TempDir(), "valid.zip")
writePreflightArchive(t, archive, preflightArchiveSpec{
entries: []preflightArchiveEntry{{path: "configuration/operator.env", body: []byte("safe")}},
})
result, err := Preflight(context.Background(), installation, PreflightRequest{Archive: archive, Confirm: true}, permissivePreflightDependencies())
if err != nil {
t.Fatal(err)
}
defer result.CloseArchive()
staged, err := result.StageArchive(context.Background())
if err != nil {
t.Fatal(err)
}
defer staged.Close()
if err := safeio.ValidatePrivateRegular(staged.path); err != nil {
t.Fatalf("staged archive privacy = %v, want owner-private regular file", err)
}
info, err := os.Lstat(staged.path)
if err != nil {
t.Fatal(err)
}
if got := info.Mode().Perm(); got != 0o600 {
t.Fatalf("staged archive mode = %#o, want 0600", got)
}
}
@@ -11,7 +11,7 @@ import (
"github.com/aritmolab/thothii/tools/tht/internal/safeio"
)
func TestStageArchiveProtectsWindowsStagingDirectoriesWithOwnerOnlyACLs(t *testing.T) {
func TestStageArchiveProtectsWindowsStagingArtifactsWithOwnerOnlyACLs(t *testing.T) {
installation := preflightTestInstallation(t)
if err := os.MkdirAll(installation.ControlDirectory(), 0o700); err != nil {
t.Fatal(err)
@@ -37,4 +37,7 @@ func TestStageArchiveProtectsWindowsStagingDirectoriesWithOwnerOnlyACLs(t *testi
if err := safeio.ValidatePrivateDirectory(staged.directory); err != nil {
t.Fatalf("staged archive directory ACL = %v, want owner-only", err)
}
if err := safeio.ValidatePrivateRegular(staged.path); err != nil {
t.Fatalf("staged archive ACL = %v, want owner-only", err)
}
}