From 7498fda030f478250e77e54e73174f30e722c5e4 Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Fri, 11 Sep 2026 15:03:39 +0000 Subject: [PATCH] Apply deployment permissions to checkpoints --- docs/roadmap/implementation.md | 2 + internal/cli/state_hardening_test.go | 1 - internal/cli/state_permissions_unix_test.go | 61 ++++++++++++ .../filesystem_permissions_unix_test.go | 98 +++++++++++++++++++ .../framework/checkpoint/filesystem_test.go | 22 ----- internal/framework/checkpoint/recorder.go | 2 +- 6 files changed, 162 insertions(+), 24 deletions(-) create mode 100644 internal/cli/state_permissions_unix_test.go create mode 100644 internal/framework/checkpoint/filesystem_permissions_unix_test.go diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index d153dc53..d0c24c88 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -179,6 +179,8 @@ passes. This stage is small enough for one implementation prompt. ## Stage 3: Convert Checkpoints And Prove Assembled Cache Behavior +**Completed:** 2026-09-11 + ### Goal Move checkpoints to the new shared writer, remove stale private-cache diff --git a/internal/cli/state_hardening_test.go b/internal/cli/state_hardening_test.go index c32e0abf..b387e4bc 100644 --- a/internal/cli/state_hardening_test.go +++ b/internal/cli/state_hardening_test.go @@ -57,7 +57,6 @@ func TestRunStateSurfaceMatrix(t *testing.T) { } } assertAnyFile(t, roots.checkpoints) - assertRestrictedTree(t, roots.checkpoints) if debug { bundle := onlyChildDir(t, roots.debug) assertFile(t, filepath.Join(bundle, "summary", "invocation.json")) diff --git a/internal/cli/state_permissions_unix_test.go b/internal/cli/state_permissions_unix_test.go new file mode 100644 index 00000000..cef86186 --- /dev/null +++ b/internal/cli/state_permissions_unix_test.go @@ -0,0 +1,61 @@ +//go:build linux || darwin + +package cli + +import ( + "os" + "path/filepath" + "syscall" + "testing" +) + +func TestRunCreatesReusableGroupAccessibleCacheState(t *testing.T) { + previousUmask := syscall.Umask(0o007) + defer syscall.Umask(previousUmask) + + roots := newStateTestRoots(t) + harness := newStateTestHarness() + result := runStateTest(t, roots, harness.options(), false, false, "auto") + if result.code != 0 { + t.Fatalf("initial run code=%d stderr=%q", result.code, result.stderr) + } + assertGroupAccessibleCacheTree(t, roots.plans) + assertGroupAccessibleCacheTree(t, roots.checkpoints) + + extractCalls := harness.extractCalls + result = runStateTest(t, roots, harness.options(), false, true, "auto") + if result.code != 0 { + t.Fatalf("resume run code=%d stderr=%q", result.code, result.stderr) + } + if harness.extractCalls != extractCalls { + t.Fatalf("extract calls after reuse = %d, want %d", harness.extractCalls, extractCalls) + } +} + +func assertGroupAccessibleCacheTree(t *testing.T, root string) { + t.Helper() + if err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + want := os.FileMode(0o660) + if info.IsDir() { + want = 0o770 + } + if got := info.Mode().Perm(); got != want { + return &cachePermissionError{path: path, got: got, want: want} + } + return nil + }); err != nil { + t.Fatal(err) + } +} + +type cachePermissionError struct { + path string + got, want os.FileMode +} + +func (err *cachePermissionError) Error() string { + return err.path + " mode mismatch" +} diff --git a/internal/framework/checkpoint/filesystem_permissions_unix_test.go b/internal/framework/checkpoint/filesystem_permissions_unix_test.go new file mode 100644 index 00000000..9886bf11 --- /dev/null +++ b/internal/framework/checkpoint/filesystem_permissions_unix_test.go @@ -0,0 +1,98 @@ +//go:build linux || darwin + +package checkpoint + +import ( + "os" + "path/filepath" + "syscall" + "testing" + + "gitea.maximumdirect.net/eric/notarius/internal/core/source" +) + +func TestFilesystemCheckpointPermissionsFollowUmask(t *testing.T) { + for _, test := range []struct { + name string + umask int + dirMode os.FileMode + fileMode os.FileMode + }{ + {name: "private", umask: 0o077, dirMode: 0o700, fileMode: 0o600}, + {name: "group", umask: 0o007, dirMode: 0o770, fileMode: 0o660}, + } { + t.Run(test.name, func(t *testing.T) { + previousUmask := syscall.Umask(test.umask) + defer syscall.Umask(previousUmask) + + root := filepath.Join(t.TempDir(), "checkpoints") + identity, err := NewIdentity(representativeIdentityInput()) + if err != nil { + t.Fatal(err) + } + recorder, err := NewFilesystemRecorder(root, identity) + if err != nil { + t.Fatal(err) + } + if err := recorder.SourceSucceeded("source-module", sourceDocumentForPermissions()); err != nil { + t.Fatal(err) + } + + relative, err := identity.RelativePath() + if err != nil { + t.Fatal(err) + } + identityRoot := filepath.Join(root, filepath.FromSlash(relative)) + sourceRoot := filepath.Join(identityRoot, "source") + for path, want := range map[string]os.FileMode{ + root: test.dirMode, + identityRoot: test.dirMode, + sourceRoot: test.dirMode, + filepath.Join(sourceRoot, "manifest.json"): test.fileMode, + filepath.Join(sourceRoot, "source-document.json"): test.fileMode, + } { + assertCheckpointPermissions(t, path, want) + } + }) + } +} + +func TestFilesystemCheckpointLeavesExistingRootUntouched(t *testing.T) { + previousUmask := syscall.Umask(0o077) + defer syscall.Umask(previousUmask) + + root := filepath.Join(t.TempDir(), "checkpoints") + syscall.Umask(0) + if err := os.Mkdir(root, 0o751); err != nil { + t.Fatal(err) + } + syscall.Umask(0o077) + identity, err := NewIdentity(representativeIdentityInput()) + if err != nil { + t.Fatal(err) + } + recorder, err := NewFilesystemRecorder(root, identity) + if err != nil { + t.Fatal(err) + } + if err := recorder.SourceSucceeded("source-module", sourceDocumentForPermissions()); err != nil { + t.Fatal(err) + } + assertCheckpointPermissions(t, root, 0o751) +} + +func sourceDocumentForPermissions() *source.SourceDocument { + document := checkpointDocument() + return &document +} + +func assertCheckpointPermissions(t *testing.T, path string, want os.FileMode) { + t.Helper() + info, err := os.Stat(path) + if err != nil { + t.Fatal(err) + } + if got := info.Mode().Perm(); got != want { + t.Fatalf("%s mode = %#o, want %#o", path, got, want) + } +} diff --git a/internal/framework/checkpoint/filesystem_test.go b/internal/framework/checkpoint/filesystem_test.go index d9914f87..9b90af9e 100644 --- a/internal/framework/checkpoint/filesystem_test.go +++ b/internal/framework/checkpoint/filesystem_test.go @@ -5,7 +5,6 @@ import ( "encoding/json" "os" "path/filepath" - "runtime" "strings" "testing" "unicode/utf8" @@ -106,27 +105,6 @@ func TestFilesystemCheckpointRoundTripsAllStages(t *testing.T) { }) } - if runtime.GOOS != "windows" { - t.Run("restrictive permissions", func(t *testing.T) { - root := filepath.Join(fixture.root, mustRelativePath(t, fixture.identity)) - err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error { - if err != nil { - return err - } - want := os.FileMode(0o600) - if info.IsDir() { - want = 0o700 - } - if got := info.Mode().Perm(); got != want { - t.Errorf("%s permissions = %o, want %o", path, got, want) - } - return nil - }) - if err != nil { - t.Fatal(err) - } - }) - } } func TestFilesystemCheckpointRejectsMissingAndCorruptState(t *testing.T) { diff --git a/internal/framework/checkpoint/recorder.go b/internal/framework/checkpoint/recorder.go index aff1deac..f86adb51 100644 --- a/internal/framework/checkpoint/recorder.go +++ b/internal/framework/checkpoint/recorder.go @@ -217,7 +217,7 @@ func (r *FilesystemRecorder) writeJSON(name string, payload any) error { if r == nil || strings.TrimSpace(r.root) == "" { return nil } - return fileio.WriteJSON(r.root, name, payload, 0o700, 0o600) + return fileio.WriteJSONWithProcessPermissions(r.root, name, payload) } func (r *FilesystemRecorder) timestamp() time.Time {