fix(backup): use platform private staging controls
This commit is contained in:
@@ -20,6 +20,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/aritmolab/thothii/tools/tht/internal/config"
|
"github.com/aritmolab/thothii/tools/tht/internal/config"
|
||||||
|
"github.com/aritmolab/thothii/tools/tht/internal/safeio"
|
||||||
)
|
)
|
||||||
|
|
||||||
var archiveDrivePath = regexp.MustCompile(`^[A-Za-z]:/`)
|
var archiveDrivePath = regexp.MustCompile(`^[A-Za-z]:/`)
|
||||||
@@ -267,7 +268,7 @@ func (result PreflightResult) StageArchive(ctx context.Context) (_ *stagedArchiv
|
|||||||
if result.stagingRoot == "" || result.freeBytes == nil {
|
if result.stagingRoot == "" || result.freeBytes == nil {
|
||||||
return nil, errors.New("backup archive has no controlled staging reservation")
|
return nil, errors.New("backup archive has no controlled staging reservation")
|
||||||
}
|
}
|
||||||
if err := ensurePrivateStagingRoot(result.stagingRoot); err != nil {
|
if err := safeio.EnsurePrivateDirectory(result.stagingRoot); err != nil {
|
||||||
return nil, fmt.Errorf("create private restore staging root: %w", err)
|
return nil, fmt.Errorf("create private restore staging root: %w", err)
|
||||||
}
|
}
|
||||||
freeBytes, err := result.freeBytes(result.stagingRoot)
|
freeBytes, err := result.freeBytes(result.stagingRoot)
|
||||||
@@ -281,7 +282,7 @@ func (result PreflightResult) StageArchive(ctx context.Context) (_ *stagedArchiv
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.New("create private restore staging directory")
|
return nil, errors.New("create private restore staging directory")
|
||||||
}
|
}
|
||||||
if err := os.Chmod(directory, 0o700); err != nil {
|
if err := safeio.ProtectPrivateDirectory(directory); err != nil {
|
||||||
_ = os.Remove(directory)
|
_ = os.Remove(directory)
|
||||||
return nil, errors.New("protect private restore staging directory")
|
return nil, errors.New("protect private restore staging directory")
|
||||||
}
|
}
|
||||||
@@ -342,27 +343,6 @@ func (result PreflightResult) StageArchive(ctx context.Context) (_ *stagedArchiv
|
|||||||
return staged, nil
|
return staged, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ensurePrivateStagingRoot(root string) error {
|
|
||||||
if !filepath.IsAbs(root) || filepath.Clean(root) != root {
|
|
||||||
return errors.New("restore staging root is invalid")
|
|
||||||
}
|
|
||||||
if err := os.Mkdir(root, 0o700); err != nil && !errors.Is(err, os.ErrExist) {
|
|
||||||
return errors.New("restore staging root is unavailable")
|
|
||||||
}
|
|
||||||
info, err := os.Lstat(root)
|
|
||||||
if err != nil || !info.IsDir() || info.Mode()&os.ModeSymlink != 0 {
|
|
||||||
return errors.New("restore staging root is unsafe")
|
|
||||||
}
|
|
||||||
if err := os.Chmod(root, 0o700); err != nil {
|
|
||||||
return errors.New("restore staging root cannot be protected")
|
|
||||||
}
|
|
||||||
info, err = os.Lstat(root)
|
|
||||||
if err != nil || !info.IsDir() || info.Mode()&os.ModeSymlink != 0 || info.Mode().Perm() != 0o700 {
|
|
||||||
return errors.New("restore staging root protection is invalid")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close removes only the staging file and directory created by StageArchive.
|
// Close removes only the staging file and directory created by StageArchive.
|
||||||
func (staged *stagedArchive) Close() error {
|
func (staged *stagedArchive) Close() error {
|
||||||
if staged == nil {
|
if staged == nil {
|
||||||
|
|||||||
@@ -524,7 +524,10 @@ type preflightRawArchiveEntry struct {
|
|||||||
|
|
||||||
func preflightTestInstallation(t *testing.T) config.Installation {
|
func preflightTestInstallation(t *testing.T) config.Installation {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
root := t.TempDir()
|
root, err := filepath.EvalSymlinks(t.TempDir())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
return config.Installation{
|
return config.Installation{
|
||||||
Path: filepath.Join(root, "deploy", "local-dev", "thothii-installation.yaml"),
|
Path: filepath.Join(root, "deploy", "local-dev", "thothii-installation.yaml"),
|
||||||
ProjectDirectory: root,
|
ProjectDirectory: root,
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
//go:build !windows
|
||||||
|
|
||||||
|
package backup
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/aritmolab/thothii/tools/tht/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestStageArchiveRejectsSymlinkedInstallationAncestor(t *testing.T) {
|
||||||
|
root, err := filepath.EvalSymlinks(t.TempDir())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
realProject := filepath.Join(root, "real-project")
|
||||||
|
linkedProject := filepath.Join(root, "linked-project")
|
||||||
|
if err := os.Mkdir(realProject, 0o700); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.Symlink(realProject, linkedProject); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
installation := config.Installation{
|
||||||
|
Path: filepath.Join(linkedProject, "deploy", "local-dev", "thothii-installation.yaml"),
|
||||||
|
ProjectDirectory: linkedProject,
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll(installation.ControlDirectory(), 0o700); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
archive := filepath.Join(root, "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()
|
||||||
|
|
||||||
|
if _, err := result.StageArchive(context.Background()); err == nil || !strings.Contains(err.Error(), "staging root") {
|
||||||
|
t.Fatalf("StageArchive() error = %v, want unsafe symlinked staging-root rejection", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package backup
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/aritmolab/thothii/tools/tht/internal/safeio"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestStageArchiveProtectsWindowsStagingDirectoriesWithOwnerOnlyACLs(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.ValidatePrivateDirectory(result.stagingRoot); err != nil {
|
||||||
|
t.Fatalf("staging root ACL = %v, want owner-only", err)
|
||||||
|
}
|
||||||
|
if err := safeio.ValidatePrivateDirectory(staged.directory); err != nil {
|
||||||
|
t.Fatalf("staged archive directory ACL = %v, want owner-only", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user