Make ordinary workspaces group shareable
This commit is contained in:
@@ -10,11 +10,6 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
promotedDirectoryMode = 0o755
|
||||
promotedFileMode = 0o644
|
||||
)
|
||||
|
||||
// ErrAtomicDirectoryPromotionUnsupported indicates that the current operating
|
||||
// system lacks the atomic no-replace primitive required by PromoteDirectory.
|
||||
var ErrAtomicDirectoryPromotionUnsupported = errors.New("atomic no-replace directory promotion is unsupported")
|
||||
@@ -98,7 +93,7 @@ func promoteDirectoryWithHooks(
|
||||
if err := copyRegularTree(sourceRoot, src, temporary, hooks); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.Chmod(temporary, promotedDirectoryMode); err != nil {
|
||||
if err := os.Chmod(temporary, WorkspaceDirectoryMode); err != nil {
|
||||
return fmt.Errorf("set temporary root permissions: %w", err)
|
||||
}
|
||||
if err := syncDirectory(temporary); err != nil {
|
||||
@@ -223,13 +218,13 @@ func copyRegularDirectory(
|
||||
return fmt.Errorf("source directory %q changed while being copied", sourcePath)
|
||||
}
|
||||
|
||||
if err := os.Mkdir(dst, promotedDirectoryMode); err != nil {
|
||||
if err := os.Mkdir(dst, WorkspaceDirectoryMode); err != nil {
|
||||
return fmt.Errorf("create destination directory %q: %w", dst, err)
|
||||
}
|
||||
if err := copyRegularTree(child, sourcePath, dst, hooks); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.Chmod(dst, promotedDirectoryMode); err != nil {
|
||||
if err := os.Chmod(dst, WorkspaceDirectoryMode); err != nil {
|
||||
return fmt.Errorf("set destination directory permissions %q: %w", dst, err)
|
||||
}
|
||||
if err := syncDirectory(dst); err != nil {
|
||||
@@ -264,7 +259,7 @@ func copyRegularFile(
|
||||
return fmt.Errorf("source file %q changed while being copied", sourcePath)
|
||||
}
|
||||
|
||||
out, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_EXCL, promotedFileMode)
|
||||
out, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_EXCL, WorkspaceFileMode)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create destination file %q: %w", dst, err)
|
||||
}
|
||||
@@ -278,7 +273,7 @@ func copyRegularFile(
|
||||
if _, err := io.Copy(out, in); err != nil {
|
||||
return fmt.Errorf("copy source file %q: %w", sourcePath, err)
|
||||
}
|
||||
if err := out.Chmod(promotedFileMode); err != nil {
|
||||
if err := out.Chmod(WorkspaceFileMode); err != nil {
|
||||
return fmt.Errorf("set destination file permissions %q: %w", dst, err)
|
||||
}
|
||||
if err := out.Sync(); err != nil {
|
||||
|
||||
@@ -41,8 +41,8 @@ func TestPromoteDirectoryCopiesNestedRegularTree(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("Stat(%q) error = %v", path, err)
|
||||
}
|
||||
if got := info.Mode().Perm(); got != promotedDirectoryMode {
|
||||
t.Fatalf("directory mode for %q = %o, want %o", path, got, promotedDirectoryMode)
|
||||
if got := info.Mode().Perm(); got != WorkspaceDirectoryMode.Perm() {
|
||||
t.Fatalf("directory mode for %q = %o, want %o", path, got, WorkspaceDirectoryMode.Perm())
|
||||
}
|
||||
}
|
||||
for _, path := range []string{filepath.Join(dst, "a-first.txt"), filepath.Join(dst, "nested", "binary.dat"), filepath.Join(dst, "z-last.txt")} {
|
||||
@@ -50,8 +50,8 @@ func TestPromoteDirectoryCopiesNestedRegularTree(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("Stat(%q) error = %v", path, err)
|
||||
}
|
||||
if got := info.Mode().Perm(); got != promotedFileMode {
|
||||
t.Fatalf("file mode for %q = %o, want %o", path, got, promotedFileMode)
|
||||
if got := info.Mode().Perm(); got != WorkspaceFileMode.Perm() {
|
||||
t.Fatalf("file mode for %q = %o, want %o", path, got, WorkspaceFileMode.Perm())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ func WriteFileAtomic(dst string, data []byte, perm os.FileMode) error {
|
||||
if strings.TrimSpace(dst) == "" {
|
||||
return fmt.Errorf("destination path is required")
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {
|
||||
if err := EnsureWorkspaceDirectory(filepath.Dir(dst)); err != nil {
|
||||
return fmt.Errorf("create destination directory: %w", err)
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ func CopyFileAtomicWithChecksum(src, dst string, perm os.FileMode) (string, erro
|
||||
}
|
||||
defer func() { _ = in.Close() }()
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {
|
||||
if err := EnsureWorkspaceDirectory(filepath.Dir(dst)); err != nil {
|
||||
return "", fmt.Errorf("create destination directory: %w", err)
|
||||
}
|
||||
|
||||
@@ -115,7 +115,7 @@ func InstallDownloadedTempFile(tmpPath, dst string, perm os.FileMode) error {
|
||||
if strings.TrimSpace(tmpPath) == "" || strings.TrimSpace(dst) == "" {
|
||||
return fmt.Errorf("temp and destination paths are required")
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {
|
||||
if err := EnsureWorkspaceDirectory(filepath.Dir(dst)); err != nil {
|
||||
return fmt.Errorf("create destination directory: %w", err)
|
||||
}
|
||||
if err := os.Chmod(tmpPath, perm); err != nil {
|
||||
|
||||
58
internal/fileops/modes.go
Normal file
58
internal/fileops/modes.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package fileops
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// WorkspaceDirectoryMode is the POSIX mode for ordinary, shareable workspace
|
||||
// directories. The set-group-ID bit preserves the configured workspace group
|
||||
// for nested files and directories.
|
||||
const WorkspaceDirectoryMode os.FileMode = os.ModeSetgid | 0o775
|
||||
|
||||
// WorkspaceFileMode is the POSIX mode for ordinary, shareable workspace files.
|
||||
const WorkspaceFileMode os.FileMode = 0o664
|
||||
|
||||
// EnsureWorkspaceDirectory creates directory and makes every directory created
|
||||
// for it conform to the ordinary workspace sharing contract. Existing
|
||||
// directories retain their owner and group; only their mode is updated.
|
||||
func EnsureWorkspaceDirectory(directory string) error {
|
||||
if strings.TrimSpace(directory) == "" {
|
||||
return fmt.Errorf("workspace directory is required")
|
||||
}
|
||||
|
||||
directory = filepath.Clean(directory)
|
||||
missing := make([]string, 0)
|
||||
for current := directory; ; current = filepath.Dir(current) {
|
||||
info, err := os.Lstat(current)
|
||||
if err == nil {
|
||||
if !info.IsDir() {
|
||||
return fmt.Errorf("workspace directory %q is not a directory", current)
|
||||
}
|
||||
break
|
||||
}
|
||||
if !os.IsNotExist(err) {
|
||||
return fmt.Errorf("inspect workspace directory %q: %w", current, err)
|
||||
}
|
||||
missing = append(missing, current)
|
||||
parent := filepath.Dir(current)
|
||||
if parent == current {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(directory, WorkspaceDirectoryMode); err != nil {
|
||||
return fmt.Errorf("create workspace directory %q: %w", directory, err)
|
||||
}
|
||||
for index := len(missing) - 1; index >= 0; index-- {
|
||||
if err := os.Chmod(missing[index], WorkspaceDirectoryMode); err != nil {
|
||||
return fmt.Errorf("set workspace directory permissions %q: %w", missing[index], err)
|
||||
}
|
||||
}
|
||||
if err := os.Chmod(directory, WorkspaceDirectoryMode); err != nil {
|
||||
return fmt.Errorf("set workspace directory permissions %q: %w", directory, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
87
internal/fileops/modes_posix_test.go
Normal file
87
internal/fileops/modes_posix_test.go
Normal file
@@ -0,0 +1,87 @@
|
||||
//go:build !windows
|
||||
|
||||
package fileops
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestWorkspaceModesOverrideRestrictiveUmask(t *testing.T) {
|
||||
restoreUmask := syscall.Umask(0o077)
|
||||
t.Cleanup(func() { syscall.Umask(restoreUmask) })
|
||||
|
||||
root := t.TempDir()
|
||||
nested := filepath.Join(root, "campaign", "session", "artifacts")
|
||||
if err := EnsureWorkspaceDirectory(nested); err != nil {
|
||||
t.Fatalf("EnsureWorkspaceDirectory() error = %v", err)
|
||||
}
|
||||
for _, path := range []string{
|
||||
filepath.Join(root, "campaign"),
|
||||
filepath.Join(root, "campaign", "session"),
|
||||
nested,
|
||||
} {
|
||||
assertWorkspaceDirectoryMode(t, path)
|
||||
}
|
||||
|
||||
file := filepath.Join(nested, "result.json")
|
||||
if err := WriteFileAtomic(file, []byte("first"), WorkspaceFileMode); err != nil {
|
||||
t.Fatalf("WriteFileAtomic(first) error = %v", err)
|
||||
}
|
||||
if err := WriteFileAtomic(file, []byte("replacement"), WorkspaceFileMode); err != nil {
|
||||
t.Fatalf("WriteFileAtomic(replacement) error = %v", err)
|
||||
}
|
||||
assertWorkspaceFileMode(t, file)
|
||||
}
|
||||
|
||||
func TestPromoteDirectoryUsesWorkspaceModesUnderRestrictiveUmask(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
src := filepath.Join(root, "source")
|
||||
if err := os.MkdirAll(filepath.Join(src, "nested"), 0o755); err != nil {
|
||||
t.Fatalf("MkdirAll(source) error = %v", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(src, "nested", "result.json"), []byte("result"), 0o600); err != nil {
|
||||
t.Fatalf("WriteFile(source) error = %v", err)
|
||||
}
|
||||
|
||||
destinationParent := filepath.Join(root, "workspace", "artifacts", "notarius")
|
||||
if err := EnsureWorkspaceDirectory(destinationParent); err != nil {
|
||||
t.Fatalf("EnsureWorkspaceDirectory(destination parent) error = %v", err)
|
||||
}
|
||||
restoreUmask := syscall.Umask(0o077)
|
||||
t.Cleanup(func() { syscall.Umask(restoreUmask) })
|
||||
destination := filepath.Join(destinationParent, "run-1")
|
||||
if err := PromoteDirectory(src, destination); err != nil {
|
||||
t.Fatalf("PromoteDirectory() error = %v", err)
|
||||
}
|
||||
assertWorkspaceDirectoryMode(t, destination)
|
||||
assertWorkspaceDirectoryMode(t, filepath.Join(destination, "nested"))
|
||||
assertWorkspaceFileMode(t, filepath.Join(destination, "nested", "result.json"))
|
||||
}
|
||||
|
||||
func assertWorkspaceDirectoryMode(t *testing.T, path string) {
|
||||
t.Helper()
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
t.Fatalf("Stat(%q) error = %v", path, err)
|
||||
}
|
||||
if got, want := info.Mode().Perm(), WorkspaceDirectoryMode.Perm(); got != want {
|
||||
t.Fatalf("directory mode for %q = %o, want %o", path, got, want)
|
||||
}
|
||||
if info.Mode()&os.ModeSetgid == 0 {
|
||||
t.Fatalf("directory mode for %q does not include setgid: %v", path, info.Mode())
|
||||
}
|
||||
}
|
||||
|
||||
func assertWorkspaceFileMode(t *testing.T, path string) {
|
||||
t.Helper()
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
t.Fatalf("Stat(%q) error = %v", path, err)
|
||||
}
|
||||
if got, want := info.Mode().Perm(), WorkspaceFileMode.Perm(); got != want {
|
||||
t.Fatalf("file mode for %q = %o, want %o", path, got, want)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user