Project version protections through item placement
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# Implementation Plan: Sequence Placement and Composition
|
# Implementation Plan: Sequence Placement and Composition
|
||||||
|
|
||||||
Status: implementation in progress; stages 1–9 completed.
|
Status: implementation in progress; stages 1–10 completed.
|
||||||
|
|
||||||
## Purpose and authority
|
## Purpose and authority
|
||||||
|
|
||||||
@@ -595,6 +595,22 @@ Validation: source-version/version/inheritance/override/resolution tests,
|
|||||||
`go test -race ./internal/engine`, and `./scripts/check.sh`.
|
`go test -race ./internal/engine`, and `./scripts/check.sh`.
|
||||||
Completion: item placement changes coordinates, not version or structural policy.
|
Completion: item placement changes coordinates, not version or structural policy.
|
||||||
|
|
||||||
|
Completion evidence: enabled source-version rules now derive their effective
|
||||||
|
paths through the main item's assigned index, while default-mode coordinates
|
||||||
|
remain unchanged. Structural import and underlay checks use assigned sequence
|
||||||
|
ranges so they protect the actual main item without rejecting unrelated peers;
|
||||||
|
source-local checking still runs once per original source, and projected header
|
||||||
|
evidence is used for sensitivity. Tests cover ordinary and layered placement,
|
||||||
|
protected versus peer indexes, nested projections, and redacted version
|
||||||
|
evidence. These checks passed with Go `go1.26.9 linux/amd64`:
|
||||||
|
|
||||||
|
- `GOTOOLCHAIN=local GOPROXY=off go test ./internal/engine -run 'SourceVersion|Version|Inheritance|Override|Resolution' -count=1`
|
||||||
|
- `GOTOOLCHAIN=local GOPROXY=off go test -race ./internal/engine`
|
||||||
|
- `GOTOOLCHAIN=local ./scripts/check.sh`
|
||||||
|
- `git diff --check`
|
||||||
|
|
||||||
|
No environmental limitation affected validation; checks ran on Linux/amd64.
|
||||||
|
|
||||||
## Stage 11 — Expose root-owned controls and detached placement evidence
|
## Stage 11 — Expose root-owned controls and detached placement evidence
|
||||||
|
|
||||||
Scope: root facade declarations, conversions, and public integration tests.
|
Scope: root facade declarations, conversions, and public integration tests.
|
||||||
|
|||||||
@@ -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
|
type destinationDecodeProbe string
|
||||||
|
|
||||||
var destinationDecodeProbeValues []string
|
var destinationDecodeProbeValues []string
|
||||||
|
|||||||
@@ -163,26 +163,54 @@ func nestedDirectiveDiagnostics(node *document.Node, current Path) []Diagnostic
|
|||||||
}
|
}
|
||||||
|
|
||||||
func fragmentVersionClaim(root *document.Node, rule *VersionRule) *document.Node {
|
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 {
|
if rule == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
current := root
|
current := root
|
||||||
|
currentPath := Path{}
|
||||||
if len(rule.Path.segments) == 0 {
|
if len(rule.Path.segments) == 0 {
|
||||||
return current
|
return current
|
||||||
}
|
}
|
||||||
for _, segment := range rule.Path.segments {
|
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
|
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
|
return current
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -230,6 +230,11 @@ func loadImportedWithAccessTrackedAndProcessingPolicy(access *filesystemAccess,
|
|||||||
if len(failures) != 0 {
|
if len(failures) != 0 {
|
||||||
return nil, cloneSourceInfos(state.sources), prependDiagnosticPaths(failures, destination.Index(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)}
|
completed.acquisition = []acquisitionScope{cloneAcquisitionScope(localScope)}
|
||||||
projectNestedEvidence(completed, localScope)
|
projectNestedEvidence(completed, localScope)
|
||||||
_, placed, err := placeDocumentWithMode(completed.root, destination, placementModeSequenceItem, body.Location, maxDocumentDepth, maxDocumentNodes)
|
_, placed, err := placeDocumentWithMode(completed.root, destination, placementModeSequenceItem, body.Location, maxDocumentDepth, maxDocumentNodes)
|
||||||
@@ -260,6 +265,11 @@ func loadImportedWithAccessTrackedAndProcessingPolicy(access *filesystemAccess,
|
|||||||
documents = append(documents, contribution.document)
|
documents = append(documents, contribution.document)
|
||||||
}
|
}
|
||||||
composed, diagnostics := composeDocumentsWithPolicy(policy, documents...)
|
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 {
|
if composed != nil {
|
||||||
projectNestedEvidence(composed, state.scope)
|
projectNestedEvidence(composed, state.scope)
|
||||||
projectAcquiredDestinations(composed, state.scope, state.sources)
|
projectAcquiredDestinations(composed, state.scope, state.sources)
|
||||||
@@ -416,7 +426,11 @@ func (s *importTraversal) visitPath(basePath string, declaration importDeclarati
|
|||||||
var diagnostics []Diagnostic
|
var diagnostics []Diagnostic
|
||||||
var processing *sourceVersionProcessingResult
|
var processing *sourceVersionProcessingResult
|
||||||
if s.versionState != nil && s.versionState.mode != sourceVersionModeDefault {
|
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 {
|
if len(failures) != 0 {
|
||||||
return failures
|
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)}
|
return []Diagnostic{importDiagnostic(CodeImportLimitExceeded, "configuration exceeds the aggregate node limit", &location)}
|
||||||
}
|
}
|
||||||
s.accounting.nodes += placementNodes
|
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}
|
route := importRoute{canonical: canonical, locations: currentRoute.locations}
|
||||||
s.scope.contributions = append(s.scope.contributions, acquiredContribution{
|
s.scope.contributions = append(s.scope.contributions, acquiredContribution{
|
||||||
source: cloneSourceInfo(info), payload: body, document: placed,
|
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)}
|
return []Diagnostic{importDiagnostic(CodeImportLimitExceeded, "configuration exceeds the aggregate node limit", &declaration.location)}
|
||||||
}
|
}
|
||||||
s.accounting.nodes += placementNodes
|
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{
|
s.scope.contributions = append(s.scope.contributions, acquiredContribution{
|
||||||
source: cloneSourceInfo(info), payload: completed.root, document: placed,
|
source: cloneSourceInfo(info), payload: completed.root, document: placed,
|
||||||
destination: pl.clone(), relativeDestination: relativeDestination.clone(), processing: processing,
|
destination: pl.clone(), relativeDestination: relativeDestination.clone(), processing: processing,
|
||||||
@@ -549,6 +540,25 @@ func (s *importTraversal) visitItemSource(canonical string, declaration importDe
|
|||||||
return nil
|
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 {
|
func prependDiagnosticPaths(diagnostics []Diagnostic, prefix Path) []Diagnostic {
|
||||||
cloned := append([]Diagnostic(nil), diagnostics...)
|
cloned := append([]Diagnostic(nil), diagnostics...)
|
||||||
for index := range cloned {
|
for index := range cloned {
|
||||||
@@ -647,6 +657,7 @@ func projectAcquiredDestinations(composed *composedDocument, scope acquisitionSc
|
|||||||
}
|
}
|
||||||
contributionPrefix = appendDestination(container, Path{}.Index(start))
|
contributionPrefix = appendDestination(container, Path{}.Index(start))
|
||||||
contributionInsideItem = true
|
contributionInsideItem = true
|
||||||
|
projectSourceVersionEvidence(contribution.processing, contributionPrefix)
|
||||||
} else if contributionInsideItem {
|
} else if contributionInsideItem {
|
||||||
exactDestination := appendDestination(contributionPrefix, contribution.destination)
|
exactDestination := appendDestination(contributionPrefix, contribution.destination)
|
||||||
contribution.source.Destination = exactDestination.clone()
|
contribution.source.Destination = exactDestination.clone()
|
||||||
@@ -656,6 +667,7 @@ func projectAcquiredDestinations(composed *composedDocument, scope acquisitionSc
|
|||||||
sources[position].Destination = exactDestination
|
sources[position].Destination = exactDestination
|
||||||
sources[position].destinationUnresolved = false
|
sources[position].destinationUnresolved = false
|
||||||
}
|
}
|
||||||
|
projectSourceVersionEvidence(contribution.processing, exactDestination)
|
||||||
}
|
}
|
||||||
if contribution.nested != nil && contribution.nested.acquisition != nil {
|
if contribution.nested != nil && contribution.nested.acquisition != nil {
|
||||||
for _, nestedScope := range contribution.nested.acquisition {
|
for _, nestedScope := range contribution.nested.acquisition {
|
||||||
@@ -667,6 +679,12 @@ func projectAcquiredDestinations(composed *composedDocument, scope acquisitionSc
|
|||||||
projectScope(composed, scope, Path{}, false, "")
|
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) {
|
func (s *importTraversal) readImport(name, sourceID string, optional bool) ([]byte, bool, *Diagnostic) {
|
||||||
remaining := s.limits.bytes - s.accounting.bytes
|
remaining := s.limits.bytes - s.accounting.bytes
|
||||||
limit := min(int64(maxSourceBytes), remaining)
|
limit := min(int64(maxSourceBytes), remaining)
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ func loadWithSecretLookup[T any](source Source, secrets SecretBinding, lookup se
|
|||||||
definition: definition, sequencePolicy: policy, overlay: options.Overlay,
|
definition: definition, sequencePolicy: policy, overlay: options.Overlay,
|
||||||
overrides: options.Overrides, ordinary: true,
|
overrides: options.Overrides, ordinary: true,
|
||||||
sourceVersionMode: options.SourceVersionMode,
|
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)
|
return executeLoadRequest(request, secrets, lookup)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ func assembleBaseInputTrackedForRole[T any](source Source, options Options[T], d
|
|||||||
if parsed.source != nil {
|
if parsed.source != nil {
|
||||||
sourceID = parsed.source.ID
|
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 {
|
if len(failures) != 0 {
|
||||||
return input, failures
|
return input, failures
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ func loadGroupedWithSecretLookup[T any](request loadRequest[T], secrets SecretBi
|
|||||||
}})
|
}})
|
||||||
request.main.destination = mainOptions.destination
|
request.main.destination = mainOptions.destination
|
||||||
diagnostics = append(diagnostics, mainDiagnostics...)
|
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 groupIndex := range request.underlays {
|
||||||
for inputIndex := range request.underlays[groupIndex].inputs {
|
for inputIndex := range request.underlays[groupIndex].inputs {
|
||||||
input := &request.underlays[groupIndex].inputs[inputIndex]
|
input := &request.underlays[groupIndex].inputs[inputIndex]
|
||||||
@@ -180,13 +180,6 @@ func acquireUnderlayGroupWithPolicyVersion(group loadGroup, index int, rule *Ver
|
|||||||
if acquired.document == nil {
|
if acquired.document == nil {
|
||||||
continue
|
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)...)
|
scopes = append(scopes, cloneAcquisitionScopes(acquired.acquisition)...)
|
||||||
}
|
}
|
||||||
var documents []*document.Node
|
var documents []*document.Node
|
||||||
@@ -202,6 +195,17 @@ func acquireUnderlayGroupWithPolicyVersion(group loadGroup, index int, rule *Ver
|
|||||||
if len(diagnostics) != 0 {
|
if len(diagnostics) != 0 {
|
||||||
return nil, sources, contexts, diagnostics
|
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.inputBytes = accounting.bytes - initialBytes
|
||||||
composition.inputNodes = accounting.nodes - initialNodes
|
composition.inputNodes = accounting.nodes - initialNodes
|
||||||
composition.acquisition = cloneAcquisitionScopes(scopes)
|
composition.acquisition = cloneAcquisitionScopes(scopes)
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ func loadSelectedOverlayWithAccessAndVersion(access *filesystemAccess, base *ass
|
|||||||
}
|
}
|
||||||
overlayPayload := overlayRoot
|
overlayPayload := overlayRoot
|
||||||
if versionState != nil && versionState.mode != sourceVersionModeDefault {
|
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 {
|
if len(failures) != 0 {
|
||||||
return sources, failures
|
return sources, failures
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
if state == nil || state.mode == sourceVersionModeDefault {
|
||||||
return sourceVersionProcessingResult{}, nil
|
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 {
|
if result.evidence != nil {
|
||||||
result.evidence.sourceID = sourceID
|
result.evidence.sourceID = sourceID
|
||||||
state.evidence = append(state.evidence, result.evidence)
|
state.evidence = append(state.evidence, result.evidence)
|
||||||
@@ -119,6 +123,10 @@ type sourceVersionProcessingResult struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func effectiveSourceVersionRule(local *VersionRule, destination Path, mode sourceVersionMode) *VersionRule {
|
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 {
|
if local == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -126,9 +134,13 @@ func effectiveSourceVersionRule(local *VersionRule, destination Path, mode sourc
|
|||||||
return local
|
return local
|
||||||
}
|
}
|
||||||
effective := *local
|
effective := *local
|
||||||
|
prefix := destination.clone()
|
||||||
|
if placement == placementModeSequenceItem {
|
||||||
|
prefix = prefix.Index(0)
|
||||||
|
}
|
||||||
effective.Path = Path{
|
effective.Path = Path{
|
||||||
segments: append(clonePathSegments(destination.segments, len(local.Path.segments)), local.Path.segments...),
|
segments: append(clonePathSegments(prefix.segments, len(local.Path.segments)), local.Path.segments...),
|
||||||
invalid: destination.invalid || local.Path.invalid,
|
invalid: prefix.invalid || local.Path.invalid,
|
||||||
}
|
}
|
||||||
return &effective
|
return &effective
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
type sourceVersionMountedService struct {
|
||||||
Version string `yaml:"version"`
|
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) {
|
func TestSourceVersionLoadChecksNullAndNoSourceMain(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user