Fix Notarius snapshots in relative workspaces

This commit is contained in:
2026-09-16 18:48:23 +00:00
parent a0312ed034
commit 13b83d7ee2
3 changed files with 177 additions and 0 deletions

View File

@@ -32,6 +32,11 @@ func materializeExtractReferenceSnapshots(
paths artifacts.SessionPaths,
references resolvedExtractReferences,
) ([]notarius.ReferenceBinding, error) {
var err error
paths, err = absoluteExtractSnapshotPaths(paths)
if err != nil {
return nil, err
}
if len(references.Bindings) != len(references.Identities) {
return nil, fmt.Errorf("reference bindings and identities have different lengths")
}
@@ -106,6 +111,11 @@ func cleanupInvalidExtractSnapshot(paths artifacts.SessionPaths, identity extrac
}
func verifyExtractReferenceSnapshots(paths artifacts.SessionPaths, references resolvedExtractReferences) error {
var err error
paths, err = absoluteExtractSnapshotPaths(paths)
if err != nil {
return err
}
verified := make(map[string]extractReferenceIdentity, len(references.Identities))
for _, identity := range references.Identities {
destination, err := extractReferenceSnapshotDestination(paths, identity)
@@ -129,6 +139,21 @@ func verifyExtractReferenceSnapshots(paths artifacts.SessionPaths, references re
return nil
}
// Resolve both roots against the caller's working directory before comparing
// paths. A session-prefixed relative destination is not relative to Root.
func absoluteExtractSnapshotPaths(paths artifacts.SessionPaths) (artifacts.SessionPaths, error) {
var err error
paths.Root, err = filepath.Abs(paths.Root)
if err != nil {
return paths, fmt.Errorf("resolve snapshot session root: %w", err)
}
paths.CacheDir, err = filepath.Abs(paths.CacheDir)
if err != nil {
return paths, fmt.Errorf("resolve snapshot cache root: %w", err)
}
return paths, nil
}
func extractReferenceSnapshotDestination(paths artifacts.SessionPaths, identity extractReferenceIdentity) (string, error) {
basename := filepath.Base(filepath.FromSlash(identity.Path))
return artifacts.SessionNotariusReferenceSnapshotPath(paths, identity.Checksum, basename)

View File

@@ -433,6 +433,77 @@ func TestExtractReferenceSnapshotsDeduplicateEquivalentDestinations(t *testing.T
}
}
func TestExtractReferenceSnapshotsWithRelativeWorkspace(t *testing.T) {
env, m, _ := setupExtractEnv(t)
prepared := recordPreparedExtractInput(t, env, m, artifactpolicy.SourceInputParty, "party bytes\n")
env.Config.Pipeline.Notarius.References = map[string]string{"party": artifactpolicy.SourceInputParty}
absPaths := sessionPathsForEnv(env, m.SessionID)
references, err := resolveExtractReferences(absPaths, m, env.Config.Pipeline.Notarius, m.SessionID)
if err != nil {
t.Fatal(err)
}
t.Chdir(env.Config.Pipeline.Workspace.Root)
paths := artifacts.NewLocalStore(".").SessionPathsFor(m.Campaign, m.SessionID)
first, err := materializeExtractReferenceSnapshots(paths, references)
if err != nil {
t.Fatal(err)
}
if !filepath.IsAbs(first[0].Path) {
t.Fatalf("adapter reference remains relative: %q", first[0].Path)
}
m.RunID = "20260809T020203Z-abcdef13"
second, err := materializeExtractReferenceSnapshots(paths, references)
if err != nil || !reflect.DeepEqual(first, second) {
t.Fatalf("second invocation bindings = %#v, error = %v", second, err)
}
if err := verifyExtractReferenceSnapshots(paths, references); err != nil {
t.Fatal(err)
}
if _, err := os.Stat(filepath.Join(absPaths.Root, paths.Root)); !errors.Is(err, os.ErrNotExist) {
t.Fatalf("duplicated session prefix exists or cannot be inspected: %v", err)
}
// A controlled executable proves the production adapter accepts the paths.
// It intentionally produces no receipt; receipt validation is covered elsewhere.
marker := filepath.Join(t.TempDir(), "started")
binary := filepath.Join(t.TempDir(), "notarius")
if err := os.WriteFile(binary, []byte("#!/bin/sh\nprintf started > '"+marker+"'\n"), 0o755); err != nil {
t.Fatal(err)
}
diagnostics := t.TempDir()
_, err = notarius.NewSubprocessRunner().Run(context.Background(), notarius.RunRequest{
Binary: binary, PipelineID: "dnd-session", ConfigPath: env.Config.Pipeline.Notarius.ConfigPath,
InputPath: prepared, OutputRoot: t.TempDir(), WorkingDirectory: env.Config.Pipeline.Workspace.Root,
Timeout: time.Second * 10, References: first,
ReceiptPath: filepath.Join(diagnostics, "receipt.json"), LogPath: filepath.Join(diagnostics, "stderr.log"),
})
if err == nil {
t.Fatal("expected missing receipt error")
}
if _, err := os.Stat(marker); err != nil {
t.Fatalf("adapter did not launch with the snapshot bindings: %v", err)
}
// A symlink on the actual cache path must not be missed by a duplicated walk.
if err := os.Remove(first[0].Path); err != nil {
t.Fatal(err)
}
digestDir := filepath.Dir(first[0].Path)
if err := os.Remove(digestDir); err != nil {
t.Fatal(err)
}
outside := t.TempDir()
if err := os.Symlink(outside, digestDir); err != nil {
t.Fatal(err)
}
if _, err := materializeExtractReferenceSnapshots(paths, references); err == nil {
t.Fatal("accepted symlink on actual cache path")
}
if _, err := os.Stat(filepath.Join(outside, filepath.Base(first[0].Path))); !errors.Is(err, os.ErrNotExist) {
t.Fatalf("unsafe cache path wrote outside session: %v", err)
}
}
func TestExtractReferenceSnapshotsRejectUnsafeConflicts(t *testing.T) {
for _, test := range []struct {
name string