Use stable verified Notarius reference snapshots
This commit is contained in:
@@ -123,7 +123,14 @@ has been introduced. Existing credential-redaction tests remain green.
|
|||||||
|
|
||||||
## Stage 3 — Replace Run-Local Reference Snapshots with Stable Verified Snapshots
|
## Stage 3 — Replace Run-Local Reference Snapshots with Stable Verified Snapshots
|
||||||
|
|
||||||
Status: Not started
|
Status: Complete (2026-09-16)
|
||||||
|
|
||||||
|
Completion: Replaced run-ID reference copies with confined, content-addressed
|
||||||
|
session snapshots; added canonical cache path ownership, verified reuse,
|
||||||
|
reconstruction and regular-file repair, unsafe-conflict failures, narrow cleanup
|
||||||
|
of invalid new writes, and shared post-run verification. Removed the obsolete
|
||||||
|
run-local reference helper. Verified the required artifacts/stage/fileops/
|
||||||
|
pathsafe tests and race tests for artifacts/stage.
|
||||||
|
|
||||||
Read: `internal/artifacts/paths.go` and its tests;
|
Read: `internal/artifacts/paths.go` and its tests;
|
||||||
`internal/stage/extract_references.go`, `extract.go`, `extract_resume.go`,
|
`internal/stage/extract_references.go`, `extract.go`, `extract_resume.go`,
|
||||||
|
|||||||
@@ -182,12 +182,16 @@ func TestExtractLifecyclePrepareBindsVerifiedReferenceSnapshots(t *testing.T) {
|
|||||||
for index, expected := range want {
|
for index, expected := range want {
|
||||||
binding := request.References[index]
|
binding := request.References[index]
|
||||||
canonical := filepath.Join(paths.InputsDir, expected.filename)
|
canonical := filepath.Join(paths.InputsDir, expected.filename)
|
||||||
snapshot := filepath.Join(
|
descriptor, ok := artifactpolicy.DescribePreparedInputSource(expected.sourceID)
|
||||||
artifacts.SessionRunNotariusReferencesDirForCampaign(
|
if !ok {
|
||||||
cfg.Pipeline.Workspace.Root, cfg.Session.Campaign, cfg.Session.SessionID, loaded.RunID,
|
t.Fatalf("DescribePreparedInputSource(%q) ok = false", expected.sourceID)
|
||||||
),
|
}
|
||||||
expected.filename,
|
snapshot, err := artifacts.SessionNotariusReferenceSnapshotPath(
|
||||||
|
paths, lifecycleInputChecksum(t, loaded, descriptor.ManifestKind), expected.filename,
|
||||||
)
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("SessionNotariusReferenceSnapshotPath(%q) error = %v", expected.sourceID, err)
|
||||||
|
}
|
||||||
if binding.Selector != expected.selector || binding.Path != snapshot || binding.Path == canonical || binding.Path == originalPaths[expected.sourceID] {
|
if binding.Selector != expected.selector || binding.Path != snapshot || binding.Path == canonical || binding.Path == originalPaths[expected.sourceID] {
|
||||||
t.Fatalf("reference[%d] = %#v, want selector %q snapshot %q and not prepared/source paths", index, binding, expected.selector, snapshot)
|
t.Fatalf("reference[%d] = %#v, want selector %q snapshot %q and not prepared/source paths", index, binding, expected.selector, snapshot)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import (
|
|||||||
"gitea.maximumdirect.net/eric/narratio/internal/pathsafe"
|
"gitea.maximumdirect.net/eric/narratio/internal/pathsafe"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const sha256HexLength = 64
|
||||||
|
|
||||||
// SessionPaths contains canonical local paths for one session work directory.
|
// SessionPaths contains canonical local paths for one session work directory.
|
||||||
type SessionPaths struct {
|
type SessionPaths struct {
|
||||||
WorkspaceRoot string
|
WorkspaceRoot string
|
||||||
@@ -28,6 +30,7 @@ type SessionPaths struct {
|
|||||||
CurrentDir string
|
CurrentDir string
|
||||||
RunsDir string
|
RunsDir string
|
||||||
PreviousDir string
|
PreviousDir string
|
||||||
|
CacheDir string
|
||||||
PreviousManifestPath string
|
PreviousManifestPath string
|
||||||
PreviousArtifactsDir string
|
PreviousArtifactsDir string
|
||||||
ManifestPath string
|
ManifestPath string
|
||||||
@@ -108,17 +111,37 @@ func SessionRunNotariusLogPathForCampaign(rootDir, campaign, sessionID, runID st
|
|||||||
return filepath.Join(SessionRunExtractDirForCampaign(rootDir, campaign, sessionID, runID), "notarius.stderr.log")
|
return filepath.Join(SessionRunExtractDirForCampaign(rootDir, campaign, sessionID, runID), "notarius.stderr.log")
|
||||||
}
|
}
|
||||||
|
|
||||||
// SessionRunNotariusReferencesDirForCampaign returns the invocation-local
|
|
||||||
// directory containing verified reference snapshots supplied to Notarius.
|
|
||||||
func SessionRunNotariusReferencesDirForCampaign(rootDir, campaign, sessionID, runID string) string {
|
|
||||||
return filepath.Join(SessionRunExtractDirForCampaign(rootDir, campaign, sessionID, runID), "references")
|
|
||||||
}
|
|
||||||
|
|
||||||
// SessionRunNotariusOutputRootForCampaign returns the invocation-local Notarius output root.
|
// SessionRunNotariusOutputRootForCampaign returns the invocation-local Notarius output root.
|
||||||
func SessionRunNotariusOutputRootForCampaign(rootDir, campaign, sessionID, runID string) string {
|
func SessionRunNotariusOutputRootForCampaign(rootDir, campaign, sessionID, runID string) string {
|
||||||
return filepath.Join(SessionRunExtractDirForCampaign(rootDir, campaign, sessionID, runID), "notarius-output")
|
return filepath.Join(SessionRunExtractDirForCampaign(rootDir, campaign, sessionID, runID), "notarius-output")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SessionNotariusReferenceCacheDir returns the stable cache root for verified
|
||||||
|
// reference snapshots supplied to Notarius.
|
||||||
|
func SessionNotariusReferenceCacheDir(paths SessionPaths) string {
|
||||||
|
return filepath.Join(paths.CacheDir, "notarius", "references")
|
||||||
|
}
|
||||||
|
|
||||||
|
// SessionNotariusReferenceSnapshotPath returns the content-addressed path for
|
||||||
|
// one verified prepared reference snapshot.
|
||||||
|
func SessionNotariusReferenceSnapshotPath(paths SessionPaths, checksum, preparedBasename string) (string, error) {
|
||||||
|
if len(checksum) != sha256HexLength {
|
||||||
|
return "", fmt.Errorf("reference checksum must be a full lowercase SHA-256 digest")
|
||||||
|
}
|
||||||
|
for _, character := range checksum {
|
||||||
|
if (character < '0' || character > '9') && (character < 'a' || character > 'f') {
|
||||||
|
return "", fmt.Errorf("reference checksum must be a full lowercase SHA-256 digest")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := pathsafe.ValidateOpaqueSegment(preparedBasename); err != nil {
|
||||||
|
return "", fmt.Errorf("prepared reference basename: %w", err)
|
||||||
|
}
|
||||||
|
return pathsafe.JoinSlashRelativeUnderRoot(
|
||||||
|
SessionNotariusReferenceCacheDir(paths),
|
||||||
|
checksum+"/"+preparedBasename,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// SessionNotariusBundleDirForCampaign returns one immutable durable bundle destination.
|
// SessionNotariusBundleDirForCampaign returns one immutable durable bundle destination.
|
||||||
func SessionNotariusBundleDirForCampaign(rootDir, campaign, sessionID, runID string) string {
|
func SessionNotariusBundleDirForCampaign(rootDir, campaign, sessionID, runID string) string {
|
||||||
return filepath.Join(
|
return filepath.Join(
|
||||||
@@ -268,6 +291,7 @@ func buildSessionPathsFromRoot(workspaceRoot, campaign, sessionID, root string)
|
|||||||
CurrentDir: filepath.Join(root, config.PathCurrentDirSegment),
|
CurrentDir: filepath.Join(root, config.PathCurrentDirSegment),
|
||||||
RunsDir: filepath.Join(root, config.PathRunsDirSegment),
|
RunsDir: filepath.Join(root, config.PathRunsDirSegment),
|
||||||
PreviousDir: filepath.Join(root, config.PathPreviousDirSegment),
|
PreviousDir: filepath.Join(root, config.PathPreviousDirSegment),
|
||||||
|
CacheDir: filepath.Join(root, config.PathCacheDirSegment),
|
||||||
PreviousManifestPath: filepath.Join(root, config.PathPreviousDirSegment, config.PathManifestFile),
|
PreviousManifestPath: filepath.Join(root, config.PathPreviousDirSegment, config.PathManifestFile),
|
||||||
PreviousArtifactsDir: filepath.Join(root, config.PathPreviousDirSegment, config.PathArtifactsDirSegment),
|
PreviousArtifactsDir: filepath.Join(root, config.PathPreviousDirSegment, config.PathArtifactsDirSegment),
|
||||||
ManifestPath: filepath.Join(root, config.PathManifestFile),
|
ManifestPath: filepath.Join(root, config.PathManifestFile),
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package artifacts
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -54,7 +55,6 @@ func TestSessionNotariusPathsForCampaign(t *testing.T) {
|
|||||||
{name: "extract directory", got: SessionRunExtractDirForCampaign(root, campaign, sessionID, runID), want: extractDir},
|
{name: "extract directory", got: SessionRunExtractDirForCampaign(root, campaign, sessionID, runID), want: extractDir},
|
||||||
{name: "receipt", got: SessionRunNotariusReceiptPathForCampaign(root, campaign, sessionID, runID), want: filepath.Join(extractDir, "notarius.receipt.json")},
|
{name: "receipt", got: SessionRunNotariusReceiptPathForCampaign(root, campaign, sessionID, runID), want: filepath.Join(extractDir, "notarius.receipt.json")},
|
||||||
{name: "stderr", got: SessionRunNotariusLogPathForCampaign(root, campaign, sessionID, runID), want: filepath.Join(extractDir, "notarius.stderr.log")},
|
{name: "stderr", got: SessionRunNotariusLogPathForCampaign(root, campaign, sessionID, runID), want: filepath.Join(extractDir, "notarius.stderr.log")},
|
||||||
{name: "references", got: SessionRunNotariusReferencesDirForCampaign(root, campaign, sessionID, runID), want: filepath.Join(extractDir, "references")},
|
|
||||||
{name: "output root", got: SessionRunNotariusOutputRootForCampaign(root, campaign, sessionID, runID), want: filepath.Join(extractDir, "notarius-output")},
|
{name: "output root", got: SessionRunNotariusOutputRootForCampaign(root, campaign, sessionID, runID), want: filepath.Join(extractDir, "notarius-output")},
|
||||||
{name: "durable bundle", got: SessionNotariusBundleDirForCampaign(root, campaign, sessionID, runID), want: filepath.Join(root, "work", campaign, sessionID, "artifacts", "notarius", runID)},
|
{name: "durable bundle", got: SessionNotariusBundleDirForCampaign(root, campaign, sessionID, runID), want: filepath.Join(root, "work", campaign, sessionID, "artifacts", "notarius", runID)},
|
||||||
}
|
}
|
||||||
@@ -67,6 +67,43 @@ func TestSessionNotariusPathsForCampaign(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSessionNotariusReferenceSnapshotPath(t *testing.T) {
|
||||||
|
paths := buildSessionPaths("/tmp/workspace", "forsaken", "2026-04-19")
|
||||||
|
checksum := strings.Repeat("a", 64)
|
||||||
|
got, err := SessionNotariusReferenceSnapshotPath(paths, checksum, "party.yml")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("SessionNotariusReferenceSnapshotPath() error = %v", err)
|
||||||
|
}
|
||||||
|
wantRoot := filepath.Join(paths.Root, "cache", "notarius", "references")
|
||||||
|
if root := SessionNotariusReferenceCacheDir(paths); root != wantRoot {
|
||||||
|
t.Fatalf("SessionNotariusReferenceCacheDir() = %q, want %q", root, wantRoot)
|
||||||
|
}
|
||||||
|
if want := filepath.Join(wantRoot, checksum, "party.yml"); got != want {
|
||||||
|
t.Fatalf("SessionNotariusReferenceSnapshotPath() = %q, want %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSessionNotariusReferenceSnapshotPathRejectsUnsafeIdentity(t *testing.T) {
|
||||||
|
paths := buildSessionPaths("/tmp/workspace", "forsaken", "2026-04-19")
|
||||||
|
validChecksum := strings.Repeat("a", 64)
|
||||||
|
for _, test := range []struct {
|
||||||
|
name, checksum, basename string
|
||||||
|
}{
|
||||||
|
{name: "short digest", checksum: "abc", basename: "party.yml"},
|
||||||
|
{name: "uppercase digest", checksum: strings.Repeat("A", 64), basename: "party.yml"},
|
||||||
|
{name: "nonhex digest", checksum: strings.Repeat("g", 64), basename: "party.yml"},
|
||||||
|
{name: "empty basename", checksum: validChecksum},
|
||||||
|
{name: "nested basename", checksum: validChecksum, basename: "nested/party.yml"},
|
||||||
|
{name: "parent basename", checksum: validChecksum, basename: ".."},
|
||||||
|
} {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
if got, err := SessionNotariusReferenceSnapshotPath(paths, test.checksum, test.basename); err == nil {
|
||||||
|
t.Fatalf("SessionNotariusReferenceSnapshotPath() = %q, want error", got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestSessionPreviousPathsForCampaign(t *testing.T) {
|
func TestSessionPreviousPathsForCampaign(t *testing.T) {
|
||||||
root := "/tmp/workspace"
|
root := "/tmp/workspace"
|
||||||
previousDir := SessionPreviousDirForCampaign(root, "forsaken", "2026-04-19")
|
previousDir := SessionPreviousDirForCampaign(root, "forsaken", "2026-04-19")
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ const (
|
|||||||
PathCurrentDirSegment = "current"
|
PathCurrentDirSegment = "current"
|
||||||
PathRunsDirSegment = "runs"
|
PathRunsDirSegment = "runs"
|
||||||
PathPreviousDirSegment = "previous"
|
PathPreviousDirSegment = "previous"
|
||||||
|
PathCacheDirSegment = "cache"
|
||||||
PathManifestFile = "manifest.json"
|
PathManifestFile = "manifest.json"
|
||||||
PathLockFile = ".lock"
|
PathLockFile = ".lock"
|
||||||
PathTranscriptBase = artifactmodel.TranscriptPathBase
|
PathTranscriptBase = artifactmodel.TranscriptPathBase
|
||||||
|
|||||||
@@ -124,20 +124,13 @@ func (extractStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (re
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("extract: resolve durable bundle path: %w", err)
|
return nil, fmt.Errorf("extract: resolve durable bundle path: %w", err)
|
||||||
}
|
}
|
||||||
referenceSnapshotRoot, err := absolutePath(artifacts.SessionRunNotariusReferencesDirForCampaign(workspaceRoot, campaign, sessionID, runID))
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("extract: resolve reference snapshot path: %w", err)
|
|
||||||
}
|
|
||||||
directories := []string{filepath.Dir(receiptPath), outputRoot, filepath.Dir(durableBundle)}
|
directories := []string{filepath.Dir(receiptPath), outputRoot, filepath.Dir(durableBundle)}
|
||||||
if len(references.Bindings) > 0 {
|
|
||||||
directories = append(directories, referenceSnapshotRoot)
|
|
||||||
}
|
|
||||||
for _, directory := range directories {
|
for _, directory := range directories {
|
||||||
if err := fileops.EnsureWorkspaceDirectory(directory); err != nil {
|
if err := fileops.EnsureWorkspaceDirectory(directory); err != nil {
|
||||||
return nil, fmt.Errorf("extract: create directory %q: %w", directory, err)
|
return nil, fmt.Errorf("extract: create directory %q: %w", directory, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
referenceBindings, err := materializeExtractReferenceSnapshots(paths, references, referenceSnapshotRoot)
|
referenceBindings, err := materializeExtractReferenceSnapshots(paths, references)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("extract: materialize Notarius reference snapshots: %w", err)
|
return nil, fmt.Errorf("extract: materialize Notarius reference snapshots: %w", err)
|
||||||
}
|
}
|
||||||
@@ -182,7 +175,7 @@ func (extractStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (re
|
|||||||
if adapterResult.Receipt.RunID == "" || adapterResult.Receipt.PipelineID != notariusConfig.PipelineID {
|
if adapterResult.Receipt.RunID == "" || adapterResult.Receipt.PipelineID != notariusConfig.PipelineID {
|
||||||
return nil, fmt.Errorf("extract: notarius receipt identity is missing or incompatible")
|
return nil, fmt.Errorf("extract: notarius receipt identity is missing or incompatible")
|
||||||
}
|
}
|
||||||
if err := verifyExtractReferenceSnapshots(referenceSnapshotRoot, references); err != nil {
|
if err := verifyExtractReferenceSnapshots(paths, references); err != nil {
|
||||||
return nil, fmt.Errorf("extract: verify Notarius reference snapshots: %w", err)
|
return nil, fmt.Errorf("extract: verify Notarius reference snapshots: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package stage
|
|||||||
import (
|
import (
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
@@ -16,6 +17,7 @@ import (
|
|||||||
"gitea.maximumdirect.net/eric/narratio/internal/fileops"
|
"gitea.maximumdirect.net/eric/narratio/internal/fileops"
|
||||||
"gitea.maximumdirect.net/eric/narratio/internal/manifest"
|
"gitea.maximumdirect.net/eric/narratio/internal/manifest"
|
||||||
"gitea.maximumdirect.net/eric/narratio/internal/notariusref"
|
"gitea.maximumdirect.net/eric/narratio/internal/notariusref"
|
||||||
|
"gitea.maximumdirect.net/eric/narratio/internal/pathsafe"
|
||||||
)
|
)
|
||||||
|
|
||||||
type extractReferenceIdentity struct {
|
type extractReferenceIdentity struct {
|
||||||
@@ -29,83 +31,179 @@ type extractReferenceIdentity struct {
|
|||||||
func materializeExtractReferenceSnapshots(
|
func materializeExtractReferenceSnapshots(
|
||||||
paths artifacts.SessionPaths,
|
paths artifacts.SessionPaths,
|
||||||
references resolvedExtractReferences,
|
references resolvedExtractReferences,
|
||||||
snapshotRoot string,
|
|
||||||
) ([]notarius.ReferenceBinding, error) {
|
) ([]notarius.ReferenceBinding, error) {
|
||||||
if len(references.Bindings) != len(references.Identities) {
|
if len(references.Bindings) != len(references.Identities) {
|
||||||
return nil, fmt.Errorf("reference bindings and identities have different lengths")
|
return nil, fmt.Errorf("reference bindings and identities have different lengths")
|
||||||
}
|
}
|
||||||
bindings := make([]notarius.ReferenceBinding, len(references.Bindings))
|
bindings := make([]notarius.ReferenceBinding, len(references.Bindings))
|
||||||
copied := make(map[string]string, len(references.Bindings))
|
materialized := make(map[string]extractReferenceIdentity, len(references.Bindings))
|
||||||
destinationOwners := make(map[string]string, len(references.Bindings))
|
|
||||||
for index, binding := range references.Bindings {
|
for index, binding := range references.Bindings {
|
||||||
identity := references.Identities[index]
|
identity := references.Identities[index]
|
||||||
destination, ok := copied[identity.SourceID]
|
destination, err := extractReferenceSnapshotDestination(paths, identity)
|
||||||
if !ok {
|
if err != nil {
|
||||||
filename := filepath.Base(filepath.FromSlash(identity.Path))
|
return nil, fmt.Errorf("reference %q source %q snapshot path: %w", identity.Selector, identity.SourceID, err)
|
||||||
if filename == "" || filename == "." || filename == string(filepath.Separator) {
|
|
||||||
return nil, fmt.Errorf("reference %q source %q has invalid prepared filename", identity.Selector, identity.SourceID)
|
|
||||||
}
|
|
||||||
destination = filepath.Join(snapshotRoot, filename)
|
|
||||||
if owner, duplicate := destinationOwners[destination]; duplicate && owner != identity.SourceID {
|
|
||||||
return nil, fmt.Errorf("reference sources %q and %q resolve to the same snapshot path", owner, identity.SourceID)
|
|
||||||
}
|
|
||||||
destinationOwners[destination] = identity.SourceID
|
|
||||||
|
|
||||||
file, err := os.Open(filepath.Join(paths.Root, filepath.FromSlash(identity.Path)))
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("open reference %q source %q for snapshot: %w", identity.Selector, identity.SourceID, err)
|
|
||||||
}
|
|
||||||
checksum, copyErr := fileops.WriteReaderAtomicWithChecksum(destination, file, fileops.WorkspaceFileMode)
|
|
||||||
closeErr := file.Close()
|
|
||||||
if copyErr != nil {
|
|
||||||
return nil, fmt.Errorf("snapshot reference %q source %q: %w", identity.Selector, identity.SourceID, copyErr)
|
|
||||||
}
|
|
||||||
if closeErr != nil {
|
|
||||||
return nil, fmt.Errorf("close reference %q source %q: %w", identity.Selector, identity.SourceID, closeErr)
|
|
||||||
}
|
|
||||||
info, err := os.Lstat(destination)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("inspect reference %q snapshot: %w", identity.Selector, err)
|
|
||||||
}
|
|
||||||
if !info.Mode().IsRegular() || !strings.EqualFold(checksum, identity.Checksum) || info.Size() != identity.SizeBytes {
|
|
||||||
return nil, fmt.Errorf("reference %q source %q changed while its verified snapshot was created", identity.Selector, identity.SourceID)
|
|
||||||
}
|
|
||||||
copied[identity.SourceID] = destination
|
|
||||||
}
|
}
|
||||||
|
if previous, ok := materialized[destination]; ok {
|
||||||
|
if previous.Checksum != identity.Checksum || previous.SizeBytes != identity.SizeBytes {
|
||||||
|
return nil, fmt.Errorf("references %q and %q have conflicting identities for snapshot %q", previous.Selector, identity.Selector, destination)
|
||||||
|
}
|
||||||
|
bindings[index] = notarius.ReferenceBinding{Selector: binding.Selector, Path: destination}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := materializeExtractReferenceSnapshot(paths, identity, destination); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
materialized[destination] = identity
|
||||||
bindings[index] = notarius.ReferenceBinding{Selector: binding.Selector, Path: destination}
|
bindings[index] = notarius.ReferenceBinding{Selector: binding.Selector, Path: destination}
|
||||||
}
|
}
|
||||||
return bindings, nil
|
return bindings, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func verifyExtractReferenceSnapshots(snapshotRoot string, references resolvedExtractReferences) error {
|
func materializeExtractReferenceSnapshot(paths artifacts.SessionPaths, identity extractReferenceIdentity, destination string) error {
|
||||||
verified := make(map[string]struct{}, len(references.Identities))
|
if err := checkExtractSnapshotDirectory(paths.Root, filepath.Dir(destination), true); err != nil {
|
||||||
for _, identity := range references.Identities {
|
return fmt.Errorf("reference %q snapshot cache: %w", identity.Selector, err)
|
||||||
if _, ok := verified[identity.SourceID]; ok {
|
}
|
||||||
continue
|
info, err := os.Lstat(destination)
|
||||||
|
if err == nil && !info.Mode().IsRegular() {
|
||||||
|
return unsafeExtractSnapshotPath(destination, "the destination is not a regular file")
|
||||||
|
}
|
||||||
|
if err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||||
|
return fmt.Errorf("inspect reference %q snapshot %q: %w", identity.Selector, destination, err)
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
if verifyErr := verifyExtractReferenceFile(destination, identity); verifyErr == nil {
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
filename := filepath.Base(filepath.FromSlash(identity.Path))
|
}
|
||||||
file, err := os.Open(filepath.Join(snapshotRoot, filename))
|
|
||||||
if err != nil {
|
source, err := pathsafe.JoinSlashRelativeUnderRoot(paths.Root, identity.Path)
|
||||||
return fmt.Errorf("reopen reference %q source %q snapshot: %w", identity.Selector, identity.SourceID, err)
|
if err != nil {
|
||||||
}
|
return fmt.Errorf("resolve reference %q prepared source: %w", identity.Selector, err)
|
||||||
digest := sha256.New()
|
}
|
||||||
size, readErr := io.Copy(digest, file)
|
if err := verifyExtractReferenceFile(source, identity); err != nil {
|
||||||
closeErr := file.Close()
|
return fmt.Errorf("reference %q prepared source %q no longer matches its manifest identity; rerun prepare with --force: %w", identity.Selector, source, err)
|
||||||
if readErr != nil {
|
}
|
||||||
return fmt.Errorf("revalidate reference %q source %q snapshot: %w", identity.Selector, identity.SourceID, readErr)
|
checksum, err := fileops.CopyFileAtomicWithChecksum(source, destination, fileops.WorkspaceFileMode)
|
||||||
}
|
if err != nil {
|
||||||
if closeErr != nil {
|
return fmt.Errorf("snapshot reference %q source %q to %q: %w", identity.Selector, identity.SourceID, destination, err)
|
||||||
return fmt.Errorf("close reference %q source %q snapshot: %w", identity.Selector, identity.SourceID, closeErr)
|
}
|
||||||
}
|
if checksum != identity.Checksum {
|
||||||
checksum := hex.EncodeToString(digest.Sum(nil))
|
return cleanupInvalidExtractSnapshot(paths, identity, destination,
|
||||||
if !strings.EqualFold(checksum, identity.Checksum) || size != identity.SizeBytes {
|
fmt.Errorf("copied checksum %q does not match manifest checksum %q", checksum, identity.Checksum))
|
||||||
return fmt.Errorf("reference %q source %q snapshot changed while Notarius was running", identity.Selector, identity.SourceID)
|
}
|
||||||
}
|
if err := verifyExtractReferenceFile(destination, identity); err != nil {
|
||||||
verified[identity.SourceID] = struct{}{}
|
return cleanupInvalidExtractSnapshot(paths, identity, destination, err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func cleanupInvalidExtractSnapshot(paths artifacts.SessionPaths, identity extractReferenceIdentity, destination string, verifyErr error) error {
|
||||||
|
err := fmt.Errorf("reference %q source %q changed while snapshot %q was created; rerun prepare with --force: %w", identity.Selector, identity.SourceID, destination, verifyErr)
|
||||||
|
if cleanupErr := fileops.RemoveFileUnderRoot(artifacts.SessionNotariusReferenceCacheDir(paths), destination); cleanupErr != nil {
|
||||||
|
return errors.Join(err, fmt.Errorf("remove invalid newly written snapshot %q: %w", destination, cleanupErr))
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func verifyExtractReferenceSnapshots(paths artifacts.SessionPaths, references resolvedExtractReferences) error {
|
||||||
|
verified := make(map[string]extractReferenceIdentity, len(references.Identities))
|
||||||
|
for _, identity := range references.Identities {
|
||||||
|
destination, err := extractReferenceSnapshotDestination(paths, identity)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("reference %q source %q snapshot path: %w", identity.Selector, identity.SourceID, err)
|
||||||
|
}
|
||||||
|
if previous, ok := verified[destination]; ok {
|
||||||
|
if previous.Checksum != identity.Checksum || previous.SizeBytes != identity.SizeBytes {
|
||||||
|
return fmt.Errorf("references %q and %q have conflicting identities for snapshot %q", previous.Selector, identity.Selector, destination)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := checkExtractSnapshotDirectory(paths.Root, filepath.Dir(destination), false); err != nil {
|
||||||
|
return fmt.Errorf("reference %q snapshot cache: %w", identity.Selector, err)
|
||||||
|
}
|
||||||
|
if err := verifyExtractReferenceFile(destination, identity); err != nil {
|
||||||
|
return fmt.Errorf("reference %q source %q snapshot changed while Notarius was running at %q: %w", identity.Selector, identity.SourceID, destination, err)
|
||||||
|
}
|
||||||
|
verified[destination] = identity
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractReferenceSnapshotDestination(paths artifacts.SessionPaths, identity extractReferenceIdentity) (string, error) {
|
||||||
|
basename := filepath.Base(filepath.FromSlash(identity.Path))
|
||||||
|
return artifacts.SessionNotariusReferenceSnapshotPath(paths, identity.Checksum, basename)
|
||||||
|
}
|
||||||
|
|
||||||
|
func verifyExtractReferenceFile(path string, identity extractReferenceIdentity) error {
|
||||||
|
info, err := os.Lstat(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !info.Mode().IsRegular() {
|
||||||
|
return unsafeExtractSnapshotPath(path, "the path is not a regular file")
|
||||||
|
}
|
||||||
|
file, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
digest := sha256.New()
|
||||||
|
size, readErr := io.Copy(digest, file)
|
||||||
|
closeErr := file.Close()
|
||||||
|
if readErr != nil {
|
||||||
|
return readErr
|
||||||
|
}
|
||||||
|
if closeErr != nil {
|
||||||
|
return closeErr
|
||||||
|
}
|
||||||
|
checksum := hex.EncodeToString(digest.Sum(nil))
|
||||||
|
if checksum != identity.Checksum || size != identity.SizeBytes || info.Size() != identity.SizeBytes {
|
||||||
|
return fmt.Errorf("content identity is checksum %q size %d, want checksum %q size %d", checksum, size, identity.Checksum, identity.SizeBytes)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkExtractSnapshotDirectory(sessionRoot, directory string, create bool) error {
|
||||||
|
relative, err := pathsafe.SlashRelativeFromRoot(sessionRoot, directory)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("resolve snapshot directory %q below session root: %w", directory, err)
|
||||||
|
}
|
||||||
|
root, err := os.OpenRoot(sessionRoot)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("open session root %q: %w", sessionRoot, err)
|
||||||
|
}
|
||||||
|
defer func() { _ = root.Close() }()
|
||||||
|
|
||||||
|
current := ""
|
||||||
|
for _, component := range strings.Split(relative, "/") {
|
||||||
|
if current == "" {
|
||||||
|
current = component
|
||||||
|
} else {
|
||||||
|
current += "/" + component
|
||||||
|
}
|
||||||
|
info, err := root.Lstat(current)
|
||||||
|
if errors.Is(err, os.ErrNotExist) && create {
|
||||||
|
if err := root.Mkdir(current, fileops.WorkspaceDirectoryMode.Perm()); err != nil && !errors.Is(err, os.ErrExist) {
|
||||||
|
return fmt.Errorf("create snapshot cache directory %q: %w", filepath.Join(sessionRoot, filepath.FromSlash(current)), err)
|
||||||
|
}
|
||||||
|
info, err = root.Lstat(current)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("inspect snapshot cache directory %q: %w", filepath.Join(sessionRoot, filepath.FromSlash(current)), err)
|
||||||
|
}
|
||||||
|
if !info.IsDir() || info.Mode()&os.ModeSymlink != 0 {
|
||||||
|
return unsafeExtractSnapshotPath(filepath.Join(sessionRoot, filepath.FromSlash(current)), "a path component is not a real directory")
|
||||||
|
}
|
||||||
|
if err := root.Chmod(current, fileops.WorkspaceDirectoryMode); err != nil {
|
||||||
|
return fmt.Errorf("set snapshot cache directory permissions %q: %w", filepath.Join(sessionRoot, filepath.FromSlash(current)), err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func unsafeExtractSnapshotPath(path, reason string) error {
|
||||||
|
return fmt.Errorf("unsafe Notarius reference snapshot cache path %q: %s; remove or replace it only while the session is inactive", path, reason)
|
||||||
|
}
|
||||||
|
|
||||||
type resolvedExtractReferences struct {
|
type resolvedExtractReferences struct {
|
||||||
Bindings []notarius.ReferenceBinding
|
Bindings []notarius.ReferenceBinding
|
||||||
Identities []extractReferenceIdentity
|
Identities []extractReferenceIdentity
|
||||||
|
|||||||
@@ -187,12 +187,16 @@ func TestExtractStageResolvesAllPreparedReferencesInSelectorOrder(t *testing.T)
|
|||||||
}
|
}
|
||||||
for index, selector := range wantSelectors {
|
for index, selector := range wantSelectors {
|
||||||
binding := fake.Requests[0].References[index]
|
binding := fake.Requests[0].References[index]
|
||||||
wantPath := filepath.Join(
|
checksum, err := artifacts.SHA256File(preparedPaths[selector])
|
||||||
artifacts.SessionRunNotariusReferencesDirForCampaign(
|
if err != nil {
|
||||||
env.Config.Pipeline.Workspace.Root, m.Campaign, m.SessionID, m.RunID,
|
t.Fatalf("SHA256File(%q) error = %v", selector, err)
|
||||||
),
|
}
|
||||||
filepath.Base(preparedPaths[selector]),
|
wantPath, err := artifacts.SessionNotariusReferenceSnapshotPath(
|
||||||
|
sessionPathsForEnv(env, m.SessionID), checksum, filepath.Base(preparedPaths[selector]),
|
||||||
)
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("SessionNotariusReferenceSnapshotPath(%q) error = %v", selector, err)
|
||||||
|
}
|
||||||
if binding.Selector != selector || binding.Path != wantPath || binding.Path == preparedPaths[selector] || !filepath.IsAbs(binding.Path) {
|
if binding.Selector != selector || binding.Path != wantPath || binding.Path == preparedPaths[selector] || !filepath.IsAbs(binding.Path) {
|
||||||
t.Fatalf("reference[%d] = %#v, want selector %q verified snapshot %q", index, binding, selector, wantPath)
|
t.Fatalf("reference[%d] = %#v, want selector %q verified snapshot %q", index, binding, selector, wantPath)
|
||||||
}
|
}
|
||||||
@@ -229,11 +233,11 @@ func TestExtractStageResolvesAllPreparedReferencesInSelectorOrder(t *testing.T)
|
|||||||
}
|
}
|
||||||
|
|
||||||
type referenceMutationRunner struct {
|
type referenceMutationRunner struct {
|
||||||
delegate notarius.Runner
|
delegate notarius.Runner
|
||||||
preparedPath string
|
preparedPath string
|
||||||
preparedBytes []byte
|
preparedBytes []byte
|
||||||
mutateSnapshot bool
|
mutate func(string) error
|
||||||
snapshotBytes []byte
|
snapshotBytes []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *referenceMutationRunner) Run(ctx context.Context, req notarius.RunRequest) (notarius.RunResult, error) {
|
func (r *referenceMutationRunner) Run(ctx context.Context, req notarius.RunRequest) (notarius.RunResult, error) {
|
||||||
@@ -248,8 +252,8 @@ func (r *referenceMutationRunner) Run(ctx context.Context, req notarius.RunReque
|
|||||||
return notarius.RunResult{}, err
|
return notarius.RunResult{}, err
|
||||||
}
|
}
|
||||||
r.snapshotBytes = data
|
r.snapshotBytes = data
|
||||||
if r.mutateSnapshot {
|
if r.mutate != nil {
|
||||||
if err := os.WriteFile(req.References[0].Path, []byte("tampered snapshot\n"), 0o664); err != nil {
|
if err := r.mutate(req.References[0].Path); err != nil {
|
||||||
return notarius.RunResult{}, err
|
return notarius.RunResult{}, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -272,21 +276,221 @@ func TestExtractStageInvokesNotariusWithVerifiedReferenceSnapshot(t *testing.T)
|
|||||||
t.Fatalf("Notarius reference bytes = %q, want verified snapshot", runner.snapshotBytes)
|
t.Fatalf("Notarius reference bytes = %q, want verified snapshot", runner.snapshotBytes)
|
||||||
}
|
}
|
||||||
if len(fake.Requests) != 1 || fake.Requests[0].References[0].Path == preparedPath {
|
if len(fake.Requests) != 1 || fake.Requests[0].References[0].Path == preparedPath {
|
||||||
t.Fatalf("adapter requests = %#v, want a run-local reference snapshot", fake.Requests)
|
t.Fatalf("adapter requests = %#v, want a stable reference snapshot", fake.Requests)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestExtractStageRejectsReferenceSnapshotChangedDuringNotariusRun(t *testing.T) {
|
func TestExtractStageRejectsReferenceSnapshotChangedDuringNotariusRun(t *testing.T) {
|
||||||
env, m, fake := setupExtractEnv(t)
|
for _, test := range []struct {
|
||||||
|
name string
|
||||||
|
mutate func(string) error
|
||||||
|
}{
|
||||||
|
{name: "content", mutate: func(path string) error { return os.WriteFile(path, []byte("tampered snapshot\n"), 0o664) }},
|
||||||
|
{name: "missing", mutate: os.Remove},
|
||||||
|
{name: "symlink", mutate: func(path string) error {
|
||||||
|
if err := os.Remove(path); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return os.Symlink(path+".outside", path)
|
||||||
|
}},
|
||||||
|
{name: "directory", mutate: func(path string) error {
|
||||||
|
if err := os.Remove(path); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return os.Mkdir(path, 0o775)
|
||||||
|
}},
|
||||||
|
} {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
env, m, fake := setupExtractEnv(t)
|
||||||
|
env.Config.Pipeline.Notarius.References = map[string]string{"party": artifactpolicy.SourceInputParty}
|
||||||
|
preparedPath := recordPreparedExtractInput(t, env, m, artifactpolicy.SourceInputParty, "verified party\n")
|
||||||
|
env.Notarius = &referenceMutationRunner{
|
||||||
|
delegate: fake, preparedPath: preparedPath, preparedBytes: []byte("changed after snapshot\n"), mutate: test.mutate,
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := (extractStage{}).Run(context.Background(), env, m)
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "snapshot changed while Notarius was running") || result != nil {
|
||||||
|
t.Fatalf("Run() result = %#v error = %v, want changed-snapshot failure", result, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtractReferenceSnapshotsAreStableReusableAndRecoverable(t *testing.T) {
|
||||||
|
env, m, _ := setupExtractEnv(t)
|
||||||
env.Config.Pipeline.Notarius.References = map[string]string{"party": artifactpolicy.SourceInputParty}
|
env.Config.Pipeline.Notarius.References = map[string]string{"party": artifactpolicy.SourceInputParty}
|
||||||
preparedPath := recordPreparedExtractInput(t, env, m, artifactpolicy.SourceInputParty, "verified party\n")
|
preparedPath := recordPreparedExtractInput(t, env, m, artifactpolicy.SourceInputParty, "stable party\n")
|
||||||
env.Notarius = &referenceMutationRunner{
|
paths := sessionPathsForEnv(env, m.SessionID)
|
||||||
delegate: fake, preparedPath: preparedPath, preparedBytes: []byte("changed after snapshot\n"), mutateSnapshot: true,
|
references, err := resolveExtractReferences(paths, m, env.Config.Pipeline.Notarius, m.SessionID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("resolveExtractReferences() error = %v", err)
|
||||||
|
}
|
||||||
|
first, err := materializeExtractReferenceSnapshots(paths, references)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("materializeExtractReferenceSnapshots(first) error = %v", err)
|
||||||
|
}
|
||||||
|
if len(first) != 1 {
|
||||||
|
t.Fatalf("first bindings = %#v", first)
|
||||||
|
}
|
||||||
|
firstPath := first[0].Path
|
||||||
|
|
||||||
|
oldTime := time.Unix(123, 0)
|
||||||
|
if err := os.Chtimes(firstPath, oldTime, oldTime); err != nil {
|
||||||
|
t.Fatalf("Chtimes(snapshot) error = %v", err)
|
||||||
|
}
|
||||||
|
m.RunID = "20260810T020304Z-fedcba98"
|
||||||
|
second, err := materializeExtractReferenceSnapshots(paths, references)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("materializeExtractReferenceSnapshots(second run) error = %v", err)
|
||||||
|
}
|
||||||
|
if second[0].Path != firstPath {
|
||||||
|
t.Fatalf("snapshot path changed across run IDs: %q != %q", second[0].Path, firstPath)
|
||||||
|
}
|
||||||
|
info, err := os.Stat(firstPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !info.ModTime().Equal(oldTime) {
|
||||||
|
t.Fatalf("verified snapshot was rewritten: modtime = %v, want %v", info.ModTime(), oldTime)
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := (extractStage{}).Run(context.Background(), env, m)
|
if err := os.Remove(firstPath); err != nil {
|
||||||
if err == nil || !strings.Contains(err.Error(), "snapshot changed while Notarius was running") || result != nil {
|
t.Fatalf("Remove(snapshot) error = %v", err)
|
||||||
t.Fatalf("Run() result = %#v error = %v, want changed-snapshot failure", result, err)
|
}
|
||||||
|
if _, err := materializeExtractReferenceSnapshots(paths, references); err != nil {
|
||||||
|
t.Fatalf("reconstruct missing snapshot error = %v", err)
|
||||||
|
}
|
||||||
|
assertFileContents(t, firstPath, "stable party\n")
|
||||||
|
|
||||||
|
if err := os.WriteFile(firstPath, []byte("corrupt snapshot\n"), 0o664); err != nil {
|
||||||
|
t.Fatalf("WriteFile(corrupt snapshot) error = %v", err)
|
||||||
|
}
|
||||||
|
if _, err := materializeExtractReferenceSnapshots(paths, references); err != nil {
|
||||||
|
t.Fatalf("repair corrupt snapshot error = %v", err)
|
||||||
|
}
|
||||||
|
assertFileContents(t, firstPath, "stable party\n")
|
||||||
|
|
||||||
|
if err := os.WriteFile(preparedPath, []byte("changed party bytes\n"), 0o664); err != nil {
|
||||||
|
t.Fatalf("WriteFile(changed prepared source) error = %v", err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(firstPath, []byte("corrupt before source mismatch\n"), 0o664); err != nil {
|
||||||
|
t.Fatalf("WriteFile(corrupt snapshot) error = %v", err)
|
||||||
|
}
|
||||||
|
if _, err := materializeExtractReferenceSnapshots(paths, references); err == nil || !strings.Contains(err.Error(), "prepared source") {
|
||||||
|
t.Fatalf("materialize with changed prepared source error = %v, want prepared-source mismatch", err)
|
||||||
|
}
|
||||||
|
assertFileContents(t, firstPath, "corrupt before source mismatch\n")
|
||||||
|
|
||||||
|
changedChecksum, err := artifacts.SHA256File(preparedPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("SHA256File(changed prepared source) error = %v", err)
|
||||||
|
}
|
||||||
|
descriptor, _ := artifactpolicy.DescribePreparedInputSource(artifactpolicy.SourceInputParty)
|
||||||
|
for index := range m.Inputs {
|
||||||
|
if m.Inputs[index].Kind == descriptor.ManifestKind {
|
||||||
|
m.Inputs[index].Checksum = changedChecksum
|
||||||
|
}
|
||||||
|
}
|
||||||
|
changed, err := resolveExtractReferences(paths, m, env.Config.Pipeline.Notarius, m.SessionID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("resolveExtractReferences(changed) error = %v", err)
|
||||||
|
}
|
||||||
|
changedBindings, err := materializeExtractReferenceSnapshots(paths, changed)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("materializeExtractReferenceSnapshots(changed) error = %v", err)
|
||||||
|
}
|
||||||
|
if changedBindings[0].Path == firstPath || filepath.Base(changedBindings[0].Path) != filepath.Base(firstPath) {
|
||||||
|
t.Fatalf("changed bytes paths = old %q new %q, want new digest path with same basename", firstPath, changedBindings[0].Path)
|
||||||
|
}
|
||||||
|
assertFileContents(t, firstPath, "corrupt before source mismatch\n")
|
||||||
|
assertFileContents(t, changedBindings[0].Path, "changed party bytes\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtractReferenceSnapshotsDeduplicateEquivalentDestinations(t *testing.T) {
|
||||||
|
env, m, _ := setupExtractEnv(t)
|
||||||
|
path := recordPreparedExtractInput(t, env, m, artifactpolicy.SourceInputParty, "shared bytes\n")
|
||||||
|
checksum, err := artifacts.SHA256File(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
identity := extractReferenceIdentity{
|
||||||
|
Selector: "party", SourceID: artifactpolicy.SourceInputParty, Path: "inputs/party.yml",
|
||||||
|
Checksum: checksum, SizeBytes: int64(len("shared bytes\n")),
|
||||||
|
}
|
||||||
|
references := resolvedExtractReferences{
|
||||||
|
Bindings: []notarius.ReferenceBinding{{Selector: "party"}, {Selector: "alternate"}},
|
||||||
|
Identities: []extractReferenceIdentity{
|
||||||
|
identity,
|
||||||
|
{Selector: "alternate", SourceID: artifactpolicy.SourceInputGlossary, Path: identity.Path, Checksum: checksum, SizeBytes: identity.SizeBytes},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
bindings, err := materializeExtractReferenceSnapshots(sessionPathsForEnv(env, m.SessionID), references)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("materializeExtractReferenceSnapshots() error = %v", err)
|
||||||
|
}
|
||||||
|
if len(bindings) != 2 || bindings[0].Path != bindings[1].Path {
|
||||||
|
t.Fatalf("equivalent bindings = %#v, want shared destination", bindings)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExtractReferenceSnapshotsRejectUnsafeConflicts(t *testing.T) {
|
||||||
|
for _, test := range []struct {
|
||||||
|
name string
|
||||||
|
setup func(*testing.T, artifacts.SessionPaths, string)
|
||||||
|
}{
|
||||||
|
{name: "leaf symlink", setup: func(t *testing.T, _ artifacts.SessionPaths, destination string) {
|
||||||
|
if err := os.MkdirAll(filepath.Dir(destination), 0o775); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.Symlink(filepath.Join(t.TempDir(), "outside"), destination); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}},
|
||||||
|
{name: "leaf directory", setup: func(t *testing.T, _ artifacts.SessionPaths, destination string) {
|
||||||
|
if err := os.MkdirAll(destination, 0o775); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}},
|
||||||
|
{name: "digest symlink", setup: func(t *testing.T, paths artifacts.SessionPaths, destination string) {
|
||||||
|
cacheRoot := artifacts.SessionNotariusReferenceCacheDir(paths)
|
||||||
|
if err := os.MkdirAll(cacheRoot, 0o775); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.Symlink(t.TempDir(), filepath.Dir(destination)); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}},
|
||||||
|
} {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
env, m, _ := setupExtractEnv(t)
|
||||||
|
env.Config.Pipeline.Notarius.References = map[string]string{"party": artifactpolicy.SourceInputParty}
|
||||||
|
recordPreparedExtractInput(t, env, m, artifactpolicy.SourceInputParty, "party bytes\n")
|
||||||
|
paths := sessionPathsForEnv(env, m.SessionID)
|
||||||
|
references, err := resolveExtractReferences(paths, m, env.Config.Pipeline.Notarius, m.SessionID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
destination, err := extractReferenceSnapshotDestination(paths, references.Identities[0])
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
test.setup(t, paths, destination)
|
||||||
|
_, err = materializeExtractReferenceSnapshots(paths, references)
|
||||||
|
if err == nil || !strings.Contains(err.Error(), artifacts.SessionNotariusReferenceCacheDir(paths)) || !strings.Contains(err.Error(), "session is inactive") {
|
||||||
|
t.Fatalf("materializeExtractReferenceSnapshots() error = %v, want actionable unsafe-path failure for %q", err, destination)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertFileContents(t *testing.T, path, want string) {
|
||||||
|
t.Helper()
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ReadFile(%q) error = %v", path, err)
|
||||||
|
}
|
||||||
|
if string(data) != want {
|
||||||
|
t.Fatalf("ReadFile(%q) = %q, want %q", path, data, want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user