Project version protections through item placement

This commit is contained in:
2026-10-16 19:11:08 +00:00
parent 190431740a
commit c19825620c
10 changed files with 276 additions and 50 deletions

View File

@@ -193,6 +193,43 @@ func TestLayeredMountedVersionProtectionCoversInheritanceAndUpperTiers(t *testin
})
}
func TestLayeredItemVersionProtectionTracksAssignedPeerIndexes(t *testing.T) {
t.Parallel()
definition := Definition[map[string]any]{
Version: &VersionRule{Path: Keys("schema", "version"), Check: func(Version, *Validation) error { return nil }},
sequenceRegistrations: []sequenceRegistration{{path: Keys("catalog", "entries"), identityField: "id"}},
}
item := func(label string, value map[string]any) Input {
return Input{Source: Mapping(label, value), Destination: Keys("catalog", "entries"), placementMode: placementModeSequenceItem}
}
options := func(inputs ...Input) LayeredOptions[map[string]any] {
return LayeredOptions[map[string]any]{Definition: definition, SourceVersionMode: sourceVersionModeIfPresent, Underlays: []Underlay{{Inputs: inputs}}}
}
main := item("main", map[string]any{"id": "main", "schema": map[string]any{"version": "v1"}})
t.Run("same assigned main path is protected", func(t *testing.T) {
_, err := LoadLayered(main, testSecretBinding(), options(item("underlay", map[string]any{"id": "underlay", "schema": nil})))
if got := groupedErrorCode(t, err); got != CodeUnderlayVersion {
t.Fatalf("protected item claim code = %s, want %s", got, CodeUnderlayVersion)
}
})
t.Run("appended peer does not claim main path", func(t *testing.T) {
loaded, err := LoadLayered(main, testSecretBinding(), options(
item("first", map[string]any{"id": "first", "schema": map[string]any{"other": "value"}}),
item("peer", map[string]any{"id": "peer", "schema": nil}),
))
if err != nil {
t.Fatalf("unrelated appended peer rejected: %v", err)
}
entries := layeredMapValue(t, testLoaded(loaded).Config, "catalog", "entries").([]any)
if len(entries) != 1 || entries[0].(map[string]any)["id"] != "main" {
t.Fatalf("main item projection = %#v", entries)
}
})
}
type destinationDecodeProbe string
var destinationDecodeProbeValues []string

View File

@@ -163,26 +163,54 @@ func nestedDirectiveDiagnostics(node *document.Node, current Path) []Diagnostic
}
func fragmentVersionClaim(root *document.Node, rule *VersionRule) *document.Node {
return fragmentVersionClaimProjected(root, rule, -1, nil)
}
func fragmentVersionClaimProjected(root *document.Node, rule *VersionRule, sourceIndex int, ranges []sequenceRange) *document.Node {
if rule == nil {
return nil
}
current := root
currentPath := Path{}
if len(rule.Path.segments) == 0 {
return current
}
for _, segment := range rule.Path.segments {
if current.Kind != document.Mapping {
switch current.Kind {
case document.Mapping:
if segment.kind == pathSegmentIndex {
return current
}
index := documentEntryIndex(current.Mapping, segment.key)
if index < 0 {
return nil
}
current = current.Mapping[index].Value
currentPath = currentPath.Key(segment.key)
case document.Sequence:
if segment.kind != pathSegmentIndex {
return current
}
index := segment.index
for _, assigned := range ranges {
if assigned.sourceIndex != sourceIndex || assigned.path.String() != currentPath.String() {
continue
}
if index < assigned.start || index >= assigned.end {
return nil
}
index -= assigned.start
break
}
if index < 0 || index >= len(current.Sequence) {
return nil
}
current = current.Sequence[index]
currentPath = currentPath.Index(index)
default:
// A scalar or null at an ancestor replaces the protected subtree.
return current
}
if segment.kind == pathSegmentIndex {
return nil
}
key := segment.key
index := documentEntryIndex(current.Mapping, key)
if index < 0 {
return nil
}
current = current.Mapping[index].Value
}
return current
}

View File

@@ -230,6 +230,11 @@ func loadImportedWithAccessTrackedAndProcessingPolicy(access *filesystemAccess,
if len(failures) != 0 {
return nil, cloneSourceInfos(state.sources), prependDiagnosticPaths(failures, destination.Index(0))
}
if versionState != nil {
if diagnostic := importedVersionClaim(documents, completed, versionState.localRule, 0); diagnostic != nil {
return nil, cloneSourceInfos(state.sources), []Diagnostic{*diagnostic}
}
}
completed.acquisition = []acquisitionScope{cloneAcquisitionScope(localScope)}
projectNestedEvidence(completed, localScope)
_, placed, err := placeDocumentWithMode(completed.root, destination, placementModeSequenceItem, body.Location, maxDocumentDepth, maxDocumentNodes)
@@ -260,6 +265,11 @@ func loadImportedWithAccessTrackedAndProcessingPolicy(access *filesystemAccess,
documents = append(documents, contribution.document)
}
composed, diagnostics := composeDocumentsWithPolicy(policy, documents...)
if composed != nil {
if diagnostic := importedVersionClaim(documents, composed, rule, 0); diagnostic != nil {
return nil, cloneSourceInfos(state.sources), []Diagnostic{*diagnostic}
}
}
if composed != nil {
projectNestedEvidence(composed, state.scope)
projectAcquiredDestinations(composed, state.scope, state.sources)
@@ -416,7 +426,11 @@ func (s *importTraversal) visitPath(basePath string, declaration importDeclarati
var diagnostics []Diagnostic
var processing *sourceVersionProcessingResult
if s.versionState != nil && s.versionState.mode != sourceVersionModeDefault {
processed, failures := s.versionState.process(root, sourceVersionContribution, destination, sourceID)
versionDestination := destination
if s.itemScope {
versionDestination = appendDestination(s.itemPrefix.Index(0), destination)
}
processed, failures := s.versionState.process(root, sourceVersionContribution, versionDestination, sourceID, declaration.placement)
if len(failures) != 0 {
return failures
}
@@ -452,18 +466,6 @@ func (s *importTraversal) visitPath(basePath string, declaration importDeclarati
return []Diagnostic{importDiagnostic(CodeImportLimitExceeded, "configuration exceeds the aggregate node limit", &location)}
}
s.accounting.nodes += placementNodes
protectionRule := s.rule
if s.versionState != nil && s.versionState.effectiveRule != nil {
protectionRule = s.versionState.effectiveRule
}
if versionNode := fragmentVersionClaim(placed, protectionRule); versionNode != nil {
versionLocation := publicLocation(versionNode.Location)
return []Diagnostic{{
Code: CodeImportVersion, Severity: SeverityError, Path: protectionRule.Path.clone(),
Message: "imported fragments cannot contribute to the schema-version path", Primary: &versionLocation,
}}
}
route := importRoute{canonical: canonical, locations: currentRoute.locations}
s.scope.contributions = append(s.scope.contributions, acquiredContribution{
source: cloneSourceInfo(info), payload: body, document: placed,
@@ -530,17 +532,6 @@ func (s *importTraversal) visitItemSource(canonical string, declaration importDe
return []Diagnostic{importDiagnostic(CodeImportLimitExceeded, "configuration exceeds the aggregate node limit", &declaration.location)}
}
s.accounting.nodes += placementNodes
protectionRule := s.rule
if s.versionState != nil && s.versionState.effectiveRule != nil {
protectionRule = s.versionState.effectiveRule
}
if versionNode := fragmentVersionClaim(placed, protectionRule); versionNode != nil {
versionLocation := publicLocation(versionNode.Location)
return []Diagnostic{{
Code: CodeImportVersion, Severity: SeverityError, Path: protectionRule.Path.clone(),
Message: "imported fragments cannot contribute to the schema-version path", Primary: &versionLocation,
}}
}
s.scope.contributions = append(s.scope.contributions, acquiredContribution{
source: cloneSourceInfo(info), payload: completed.root, document: placed,
destination: pl.clone(), relativeDestination: relativeDestination.clone(), processing: processing,
@@ -549,6 +540,25 @@ func (s *importTraversal) visitItemSource(canonical string, declaration importDe
return nil
}
func importedVersionClaim(documents []*document.Node, composed *composedDocument, rule *VersionRule, mainIndex int) *Diagnostic {
if composed == nil || rule == nil {
return nil
}
for index, contribution := range documents {
if index == mainIndex {
continue
}
if node := fragmentVersionClaimProjected(contribution, rule, index, composed.sequenceRanges); node != nil {
location := publicLocation(node.Location)
return &Diagnostic{
Code: CodeImportVersion, Severity: SeverityError, Path: rule.Path.clone(),
Message: "imported fragments cannot contribute to the schema-version path", Primary: &location,
}
}
}
return nil
}
func prependDiagnosticPaths(diagnostics []Diagnostic, prefix Path) []Diagnostic {
cloned := append([]Diagnostic(nil), diagnostics...)
for index := range cloned {
@@ -647,6 +657,7 @@ func projectAcquiredDestinations(composed *composedDocument, scope acquisitionSc
}
contributionPrefix = appendDestination(container, Path{}.Index(start))
contributionInsideItem = true
projectSourceVersionEvidence(contribution.processing, contributionPrefix)
} else if contributionInsideItem {
exactDestination := appendDestination(contributionPrefix, contribution.destination)
contribution.source.Destination = exactDestination.clone()
@@ -656,6 +667,7 @@ func projectAcquiredDestinations(composed *composedDocument, scope acquisitionSc
sources[position].Destination = exactDestination
sources[position].destinationUnresolved = false
}
projectSourceVersionEvidence(contribution.processing, exactDestination)
}
if contribution.nested != nil && contribution.nested.acquisition != nil {
for _, nestedScope := range contribution.nested.acquisition {
@@ -667,6 +679,12 @@ func projectAcquiredDestinations(composed *composedDocument, scope acquisitionSc
projectScope(composed, scope, Path{}, false, "")
}
func projectSourceVersionEvidence(processing *sourceVersionProcessingResult, destination Path) {
if processing != nil && processing.evidence != nil {
processing.evidence.destination = destination.clone()
}
}
func (s *importTraversal) readImport(name, sourceID string, optional bool) ([]byte, bool, *Diagnostic) {
remaining := s.limits.bytes - s.accounting.bytes
limit := min(int64(maxSourceBytes), remaining)

View File

@@ -154,7 +154,7 @@ func loadWithSecretLookup[T any](source Source, secrets SecretBinding, lookup se
definition: definition, sequencePolicy: policy, overlay: options.Overlay,
overrides: options.Overrides, ordinary: true,
sourceVersionMode: options.SourceVersionMode,
effectiveVersion: effectiveSourceVersionRule(options.Definition.Version, Path{}, options.SourceVersionMode),
effectiveVersion: effectiveSourceVersionRuleForPlacement(options.Definition.Version, options.destination, options.SourceVersionMode, options.placementMode),
}
return executeLoadRequest(request, secrets, lookup)
}

View File

@@ -111,7 +111,7 @@ func assembleBaseInputTrackedForRole[T any](source Source, options Options[T], d
if parsed.source != nil {
sourceID = parsed.source.ID
}
processed, failures := versionState.process(parsed.document, role, destination, sourceID)
processed, failures := versionState.process(parsed.document, role, destination, sourceID, options.placementMode)
if len(failures) != 0 {
return input, failures
}

View File

@@ -49,7 +49,7 @@ func loadGroupedWithSecretLookup[T any](request loadRequest[T], secrets SecretBi
}})
request.main.destination = mainOptions.destination
diagnostics = append(diagnostics, mainDiagnostics...)
request.effectiveVersion = effectiveSourceVersionRule(request.definition.Version, request.main.destination, request.sourceVersionMode)
request.effectiveVersion = effectiveSourceVersionRuleForPlacement(request.definition.Version, request.main.destination, request.sourceVersionMode, request.main.placementMode)
for groupIndex := range request.underlays {
for inputIndex := range request.underlays[groupIndex].inputs {
input := &request.underlays[groupIndex].inputs[inputIndex]
@@ -180,13 +180,6 @@ func acquireUnderlayGroupWithPolicyVersion(group loadGroup, index int, rule *Ver
if acquired.document == nil {
continue
}
if node := fragmentVersionClaim(acquired.document, rule); node != nil {
location := publicLocation(node.Location)
return nil, sources, contexts, []Diagnostic{{
Code: CodeUnderlayVersion, Severity: SeverityError, Path: rule.Path.clone(),
Message: "underlay inputs cannot contribute to the schema-version path", Primary: &location,
}}
}
scopes = append(scopes, cloneAcquisitionScopes(acquired.acquisition)...)
}
var documents []*document.Node
@@ -202,6 +195,17 @@ func acquireUnderlayGroupWithPolicyVersion(group loadGroup, index int, rule *Ver
if len(diagnostics) != 0 {
return nil, sources, contexts, diagnostics
}
if composition != nil && rule != nil {
for sourceIndex, document := range documents {
if node := fragmentVersionClaimProjected(document, rule, sourceIndex, composition.sequenceRanges); node != nil {
location := publicLocation(node.Location)
return nil, sources, contexts, []Diagnostic{{
Code: CodeUnderlayVersion, Severity: SeverityError, Path: rule.Path.clone(),
Message: "underlay inputs cannot contribute to the schema-version path", Primary: &location,
}}
}
}
}
composition.inputBytes = accounting.bytes - initialBytes
composition.inputNodes = accounting.nodes - initialNodes
composition.acquisition = cloneAcquisitionScopes(scopes)

View File

@@ -136,7 +136,7 @@ func loadSelectedOverlayWithAccessAndVersion(access *filesystemAccess, base *ass
}
overlayPayload := overlayRoot
if versionState != nil && versionState.mode != sourceVersionModeDefault {
processed, failures := versionState.process(overlayRoot, sourceVersionContribution, Path{}, sourceID)
processed, failures := versionState.process(overlayRoot, sourceVersionContribution, Path{}, sourceID, placementModeMapping)
if len(failures) != 0 {
return sources, failures
}

View File

@@ -91,11 +91,15 @@ func sourceVersionCheckForEvidence(evidence *sourceVersionEvidence, sensitivity
}
}
func (state *sourceVersionState) process(root *document.Node, role sourceVersionRole, destination Path, sourceID string) (sourceVersionProcessingResult, []Diagnostic) {
func (state *sourceVersionState) process(root *document.Node, role sourceVersionRole, destination Path, sourceID string, placement placementMode) (sourceVersionProcessingResult, []Diagnostic) {
if state == nil || state.mode == sourceVersionModeDefault {
return sourceVersionProcessingResult{}, nil
}
result := processSourceVersion(root, state.localRule, role, destination, state.mode)
projectedDestination := destination.clone()
if placement == placementModeSequenceItem {
projectedDestination = projectedDestination.Index(0)
}
result := processSourceVersion(root, state.localRule, role, projectedDestination, state.mode)
if result.evidence != nil {
result.evidence.sourceID = sourceID
state.evidence = append(state.evidence, result.evidence)
@@ -119,6 +123,10 @@ type sourceVersionProcessingResult struct {
}
func effectiveSourceVersionRule(local *VersionRule, destination Path, mode sourceVersionMode) *VersionRule {
return effectiveSourceVersionRuleForPlacement(local, destination, mode, placementModeMapping)
}
func effectiveSourceVersionRuleForPlacement(local *VersionRule, destination Path, mode sourceVersionMode, placement placementMode) *VersionRule {
if local == nil {
return nil
}
@@ -126,9 +134,13 @@ func effectiveSourceVersionRule(local *VersionRule, destination Path, mode sourc
return local
}
effective := *local
prefix := destination.clone()
if placement == placementModeSequenceItem {
prefix = prefix.Index(0)
}
effective.Path = Path{
segments: append(clonePathSegments(destination.segments, len(local.Path.segments)), local.Path.segments...),
invalid: destination.invalid || local.Path.invalid,
segments: append(clonePathSegments(prefix.segments, len(local.Path.segments)), local.Path.segments...),
invalid: prefix.invalid || local.Path.invalid,
}
return &effective
}

View File

@@ -37,6 +37,40 @@ func TestSourceVersionAbsentPathKeepsUnresolvedSegmentIntent(t *testing.T) {
}
}
func TestFragmentVersionClaimUsesAssignedSequenceProjection(t *testing.T) {
t.Parallel()
fragment := sourceVersionTestDocument(t, "items: [{version: v1}]\n")
rangeForPeer := []sequenceRange{{sourceIndex: 1, path: Keys("items"), start: 1, end: 2}}
if node := fragmentVersionClaimProjected(fragment, &VersionRule{Path: Keys("items").Index(0).Key("version")}, 1, rangeForPeer); node != nil {
t.Fatalf("peer append claimed main index 0: %#v", node)
}
if node := fragmentVersionClaimProjected(fragment, &VersionRule{Path: Keys("items").Index(1).Key("version")}, 1, rangeForPeer); node == nil || node.Text != "v1" {
t.Fatalf("assigned peer index 1 claim = %#v, want its version node", node)
}
nested := sourceVersionTestDocument(t, "items: [{nested: [{version: nested-v}]}]\n")
nestedRanges := []sequenceRange{
{sourceIndex: 1, path: Keys("items"), start: 1, end: 2},
{sourceIndex: 1, path: Keys("items").Index(0).Key("nested"), start: 2, end: 3},
}
if node := fragmentVersionClaimProjected(nested, &VersionRule{Path: Keys("items").Index(1).Key("nested").Index(2).Key("version")}, 1, nestedRanges); node == nil || node.Text != "nested-v" {
t.Fatalf("nested assigned claim = %#v, want nested version", node)
}
ancestor := sourceVersionTestDocument(t, "items: {version: replacement}\n")
if node := fragmentVersionClaim(ancestor, &VersionRule{Path: Keys("items").Index(0).Key("version")}); node == nil || node.Kind != document.Mapping {
t.Fatalf("destructive sequence ancestor claim = %#v, want mapping node", node)
}
effective := effectiveSourceVersionRuleForPlacement(&VersionRule{Path: Keys("version")}, Keys("catalog", "entries"), sourceVersionModeRequired, placementModeSequenceItem)
if got, want := effective.Path.String(), "/catalog/entries/0/version"; got != want {
t.Fatalf("item effective version path = %q, want %q", got, want)
}
defaultRule := effectiveSourceVersionRuleForPlacement(&VersionRule{Path: Keys("version")}, Keys("catalog", "entries"), sourceVersionModeDefault, placementModeSequenceItem)
if got, want := defaultRule.Path.String(), "/version"; got != want {
t.Fatalf("default effective version path = %q, want %q", got, want)
}
}
type sourceVersionMountedService struct {
Version string `yaml:"version"`
}
@@ -473,6 +507,83 @@ func TestSourceVersionLoadChecksMountedMainLocallyAndProtectsEffectivePath(t *te
}
}
func TestSourceVersionItemPlacementKeepsLocalChecksAndProtectsProjectedPath(t *testing.T) {
t.Parallel()
for _, test := range []struct {
name string
load func(Definition[map[string]any]) error
}{
{
name: "ordinary",
load: func(definition Definition[map[string]any]) error {
_, err := Load(Mapping("main", map[string]any{"version": "v1", "name": "ordinary"}), testSecretBinding(), Options[map[string]any]{
Definition: definition, SourceVersionMode: sourceVersionModeRequired,
destination: Keys("catalog", "entries"), placementMode: placementModeSequenceItem,
Overrides: []Override{{Path: Keys("catalog", "entries"), Value: []any{}, Flag: "--entries"}},
})
return err
},
},
{
name: "layered",
load: func(definition Definition[map[string]any]) error {
_, err := LoadLayered(Input{Source: Mapping("main", map[string]any{"version": "v1", "name": "layered"}), Destination: Keys("catalog", "entries"), placementMode: placementModeSequenceItem}, testSecretBinding(), LayeredOptions[map[string]any]{
Definition: definition, SourceVersionMode: sourceVersionModeRequired,
Overrides: []Override{{Path: Keys("catalog", "entries"), Value: []any{}, Flag: "--entries"}},
})
return err
},
},
} {
t.Run(test.name, func(t *testing.T) {
t.Parallel()
checks := 0
definition := Definition[map[string]any]{Version: &VersionRule{
Path: Keys("version"),
Check: func(version Version, _ *Validation) error {
checks++
if !version.Present || version.Text != "v1" {
t.Errorf("source-local version = %#v, want v1", version)
}
return nil
},
}}
err := test.load(definition)
var loadErr *LoadError
if !errors.As(err, &loadErr) {
t.Fatalf("item placement override error = %T %v, want *LoadError", err, err)
}
requireSourceVersionDiagnostic(t, loadErr.Diagnostics(), CodeOverrideVersion, "/catalog/entries")
if checks != 1 {
t.Fatalf("source-version checker called %d times, want once", checks)
}
})
}
}
func TestSourceVersionItemEvidenceUsesProjectedSensitivityPath(t *testing.T) {
t.Parallel()
const canary = "PROJECTED-VERSION-EVIDENCE-CANARY"
secretPath := Keys("catalog", "entries").Index(0).Key("version")
loaded, err := Load(Mapping("main", map[string]any{"version": canary}), testSecretBinding(), Options[map[string]any]{
Definition: Definition[map[string]any]{
Version: &VersionRule{Path: Keys("version"), Check: func(Version, *Validation) error { return nil }},
Secret: func(path Path) bool { return path.String() == secretPath.String() },
},
SourceVersionMode: sourceVersionModeRequired,
destination: Keys("catalog", "entries"), placementMode: placementModeSequenceItem,
})
if err != nil {
t.Fatalf("item source-version load: %v", err)
}
sources := loaded.Inspection.Sources()
if len(sources) != 1 || sources[0].VersionCheck == nil || !sources[0].VersionCheck.Redacted || sources[0].VersionCheck.Version.Text != "" {
t.Fatalf("projected source-version evidence was not redacted: %#v", sources)
}
}
func TestSourceVersionLoadChecksNullAndNoSourceMain(t *testing.T) {
t.Parallel()