Constrain restore installation to session root
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
@@ -46,6 +47,14 @@ func executeRestorePlan(
|
||||
}
|
||||
|
||||
sessionRoot := artifacts.SessionWorkDirForCampaign(cfg.Pipeline.Workspace.Root, cfg.Session.Campaign, cfg.Session.SessionID)
|
||||
if err := fileops.EnsureWorkspaceDirectory(sessionRoot); err != nil {
|
||||
return nil, fmt.Errorf("create session root for restore: %w", err)
|
||||
}
|
||||
root, err := os.OpenRoot(sessionRoot)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("open session root for restore: %w", err)
|
||||
}
|
||||
defer func() { _ = root.Close() }()
|
||||
manifestActions := make([]RestoreAction, 0, 1)
|
||||
actions := make([]RestoreAction, 0, len(plan.Actions))
|
||||
for _, action := range plan.Actions {
|
||||
@@ -67,7 +76,7 @@ func executeRestorePlan(
|
||||
|
||||
result := &RestoreExecutionResult{}
|
||||
for _, action := range actions {
|
||||
if err := executeRestoreDownloadAction(ctx, cfg, sessionRoot, current, action, store); err != nil {
|
||||
if err := executeRestoreDownloadAction(ctx, cfg, sessionRoot, root, current, action, store); err != nil {
|
||||
if report != nil {
|
||||
report.markFailed(action, err)
|
||||
}
|
||||
@@ -86,6 +95,7 @@ func executeRestoreDownloadAction(
|
||||
ctx context.Context,
|
||||
cfg *config.Config,
|
||||
sessionRoot string,
|
||||
root *os.Root,
|
||||
current *RemoteCurrentState,
|
||||
action RestoreAction,
|
||||
store storage.ObjectStore,
|
||||
@@ -97,6 +107,14 @@ func executeRestoreDownloadAction(
|
||||
if strings.TrimSpace(action.LocalPath) != "" && filepath.Clean(action.LocalPath) != safeLocalPath {
|
||||
return fmt.Errorf("restore plan local path mismatch for %q", action.LocalRelativePath)
|
||||
}
|
||||
parent, leaf, err := openRestoreDestinationParent(root, action.LocalRelativePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = parent.Close() }()
|
||||
if err := validateRestoreDestinationLeaf(parent, leaf); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if restoreActionIsAudio(action) {
|
||||
return executeRestoreAudioAction(ctx, cfg, safeLocalPath, action, store)
|
||||
@@ -135,12 +153,18 @@ func executeRestoreDownloadAction(
|
||||
if closeErr != nil {
|
||||
return fmt.Errorf("close restored manifest: %w", closeErr)
|
||||
}
|
||||
if err := validateRestoreDestinationLeaf(parent, leaf); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := (&manifest.LocalStore{}).Save(ctx, safeLocalPath, restored); err != nil {
|
||||
return fmt.Errorf("install rebased manifest atomically: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := validateRestoreDestinationLeaf(parent, leaf); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := fileops.InstallSiblingTempFile(temporaryPath, safeLocalPath, fileops.WorkspaceFileMode); err != nil {
|
||||
return fmt.Errorf("install file atomically: %w", err)
|
||||
}
|
||||
@@ -148,6 +172,71 @@ func executeRestoreDownloadAction(
|
||||
return nil
|
||||
}
|
||||
|
||||
func openRestoreDestinationParent(root *os.Root, relative string) (*os.Root, string, error) {
|
||||
if root == nil {
|
||||
return nil, "", fmt.Errorf("restore session root is required")
|
||||
}
|
||||
name := filepath.Clean(filepath.FromSlash(strings.TrimSpace(relative)))
|
||||
if name == "." || name == ".." || filepath.IsAbs(name) || strings.HasPrefix(name, ".."+string(filepath.Separator)) {
|
||||
return nil, "", fmt.Errorf("restore path %q is not a normalized relative path", relative)
|
||||
}
|
||||
parts := strings.Split(name, string(filepath.Separator))
|
||||
current := root
|
||||
for _, part := range parts[:len(parts)-1] {
|
||||
info, err := current.Lstat(part)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
if err := current.Mkdir(part, fileops.WorkspaceDirectoryMode.Perm()); err != nil && !errors.Is(err, os.ErrExist) {
|
||||
if current != root {
|
||||
_ = current.Close()
|
||||
}
|
||||
return nil, "", fmt.Errorf("create restore destination directory %q: %w", part, err)
|
||||
}
|
||||
info, err = current.Lstat(part)
|
||||
}
|
||||
if err != nil {
|
||||
if current != root {
|
||||
_ = current.Close()
|
||||
}
|
||||
return nil, "", fmt.Errorf("inspect restore destination ancestor %q: %w", part, err)
|
||||
}
|
||||
if !info.IsDir() || info.Mode()&os.ModeSymlink != 0 {
|
||||
if current != root {
|
||||
_ = current.Close()
|
||||
}
|
||||
return nil, "", fmt.Errorf("restore destination ancestor %q is not a directory", part)
|
||||
}
|
||||
if err := current.Chmod(part, fileops.WorkspaceDirectoryMode); err != nil {
|
||||
if current != root {
|
||||
_ = current.Close()
|
||||
}
|
||||
return nil, "", fmt.Errorf("set restore destination directory permissions %q: %w", part, err)
|
||||
}
|
||||
next, err := current.OpenRoot(part)
|
||||
if current != root {
|
||||
_ = current.Close()
|
||||
}
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("open restore destination ancestor %q: %w", part, err)
|
||||
}
|
||||
current = next
|
||||
}
|
||||
return current, parts[len(parts)-1], nil
|
||||
}
|
||||
|
||||
func validateRestoreDestinationLeaf(parent *os.Root, leaf string) error {
|
||||
info, err := parent.Lstat(leaf)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("inspect restore destination %q: %w", leaf, err)
|
||||
}
|
||||
if !info.Mode().IsRegular() || info.Mode()&os.ModeSymlink != 0 {
|
||||
return fmt.Errorf("restore destination %q is not an eligible regular file", leaf)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func isRestoreManifest(relativePath string) bool {
|
||||
clean := filepath.ToSlash(filepath.Clean(strings.TrimSpace(relativePath)))
|
||||
return clean == config.PathManifestFile || clean == config.PathPreviousDirSegment+"/"+config.PathManifestFile
|
||||
|
||||
Reference in New Issue
Block a user