Check native overlay link targets
This commit is contained in:
@@ -1529,6 +1529,10 @@ base import graph is composed and its schema version is checked, but before defa
|
|||||||
and typed decoding. Overlay bytes, nodes, and source records count toward the same
|
and typed decoding. Overlay bytes, nodes, and source records count toward the same
|
||||||
aggregate limits as base inputs. Its canonical path must remain within the configured
|
aggregate limits as base inputs. Its canonical path must remain within the configured
|
||||||
root and must not duplicate a file already present in the base graph.
|
root and must not duplicate a file already present in the base graph.
|
||||||
|
For native filesystems that implement `fs.ReadLinkFS`, these checks resolve
|
||||||
|
link targets in the selected namespace. For other native filesystems, the
|
||||||
|
checks use namespace-relative paths and reads follow that filesystem's
|
||||||
|
ordinary behavior.
|
||||||
|
|
||||||
Collection destinations follow the
|
Collection destinations follow the
|
||||||
[collection operation and presence rules](#strict-document-composition) above.
|
[collection operation and presence rules](#strict-document-composition) above.
|
||||||
|
|||||||
@@ -218,3 +218,43 @@ func TestLayeredOverlayNativeContext(t *testing.T) {
|
|||||||
}
|
}
|
||||||
t.Fatalf("diagnostics = %#v", err.(*configkit.LoadError).Diagnostics())
|
t.Fatalf("diagnostics = %#v", err.(*configkit.LoadError).Diagnostics())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestLayeredOverlayNativeLinks(t *testing.T) {
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name, mainPath, target string
|
||||||
|
want configkit.Code
|
||||||
|
}{
|
||||||
|
{"outside root", "scope/main.yml", "../../outside.yml", configkit.CodeOverlayOutsideRoot},
|
||||||
|
{"overlay aliases main", "scope/main.yml", "../main.yml", configkit.CodeOverlayRepeated},
|
||||||
|
{"main aliases overlay target", "scope/alias.yml", "../target.yml", configkit.CodeOverlayRepeated},
|
||||||
|
{"contained overlay", "scope/main.yml", "../target.yml", ""},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
native := configkit.NewFilesystem("bundle", fstest.MapFS{
|
||||||
|
"scope/main.yml": {Data: []byte("count: 1\n")},
|
||||||
|
"scope/alias.yml": {Data: []byte("target.yml"), Mode: os.ModeSymlink},
|
||||||
|
"scope/target.yml": {Data: []byte("count: 7\n")},
|
||||||
|
"scope/overlays/test.yml": {Data: []byte(tc.target), Mode: os.ModeSymlink},
|
||||||
|
"outside.yml": {Data: []byte("count: 9\n")},
|
||||||
|
})
|
||||||
|
main := configkit.Input{Source: native.File(tc.mainPath), Filesystem: &configkit.FilesystemContext{Root: "scope", FS: &native}}
|
||||||
|
result, err := configkit.LoadLayered(main, configkit.LayeredOptions[layeredFacadeConfig]{Overlay: "test"})
|
||||||
|
if tc.want != "" {
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("overlay link unexpectedly loaded")
|
||||||
|
}
|
||||||
|
diagnostics := err.(*configkit.LoadError).Diagnostics()
|
||||||
|
if len(diagnostics) != 1 || diagnostics[0].Code != tc.want {
|
||||||
|
t.Fatalf("diagnostics = %#v, want %v", diagnostics, tc.want)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if result.Config.Count != 7 || result.Sources()[1].Path != "scope/target.yml" {
|
||||||
|
t.Fatalf("config = %#v, sources = %#v", result.Config, result.Sources())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -76,12 +76,21 @@ func loadSelectedOverlay(request selectedOverlayRequest, access *filesystemAcces
|
|||||||
lookupDirectory = access.rootPath
|
lookupDirectory = access.rootPath
|
||||||
}
|
}
|
||||||
overlayPath := access.join(lookupDirectory, "overlays/"+request.name+".yml")
|
overlayPath := access.join(lookupDirectory, "overlays/"+request.name+".yml")
|
||||||
canonical, err := access.canonical(overlayPath)
|
links, _ := access.native.(fs.ReadLinkFS)
|
||||||
|
var canonical string
|
||||||
|
var err error
|
||||||
|
if links != nil {
|
||||||
|
canonical, err = resolveNativeImportPath(access, links, overlayPath)
|
||||||
|
} else {
|
||||||
|
canonical, err = access.canonical(overlayPath)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
code := CodeOverlayInvalid
|
code := CodeOverlayInvalid
|
||||||
message := "selected overlay could not be resolved"
|
message := "selected overlay could not be resolved"
|
||||||
if errors.Is(err, fs.ErrNotExist) {
|
if errors.Is(err, fs.ErrNotExist) {
|
||||||
code, message = CodeOverlayMissing, "selected overlay does not exist"
|
code, message = CodeOverlayMissing, "selected overlay does not exist"
|
||||||
|
} else if errors.Is(err, errImportPathOutsideRoot) {
|
||||||
|
code, message = CodeOverlayOutsideRoot, "selected overlay resolves outside the configuration root"
|
||||||
} else if errors.Is(err, fs.ErrPermission) {
|
} else if errors.Is(err, fs.ErrPermission) {
|
||||||
code, message = CodeImportPermission, "selected overlay cannot be accessed"
|
code, message = CodeImportPermission, "selected overlay cannot be accessed"
|
||||||
}
|
}
|
||||||
@@ -90,12 +99,8 @@ func loadSelectedOverlay(request selectedOverlayRequest, access *filesystemAcces
|
|||||||
if !access.contains(canonical) {
|
if !access.contains(canonical) {
|
||||||
return cloneSourceInfos(sources), []Diagnostic{importDiagnostic(CodeOverlayOutsideRoot, "selected overlay resolves outside the configuration root", nil)}
|
return cloneSourceInfos(sources), []Diagnostic{importDiagnostic(CodeOverlayOutsideRoot, "selected overlay resolves outside the configuration root", nil)}
|
||||||
}
|
}
|
||||||
for _, existing := range sources {
|
if overlayRepeatsSource(access, links, canonical, sources) {
|
||||||
if existing.Path != "" && existing.Path == canonical &&
|
return cloneSourceInfos(sources), []Diagnostic{importDiagnostic(CodeOverlayRepeated, "selected overlay is already present in the base source graph", nil)}
|
||||||
((access.native == nil && existing.Kind == SourceKindFile) ||
|
|
||||||
(access.native != nil && existing.Kind == SourceKindFilesystem && existing.namespace == access.token)) {
|
|
||||||
return cloneSourceInfos(sources), []Diagnostic{importDiagnostic(CodeOverlayRepeated, "selected overlay is already present in the base source graph", nil)}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if len(sources)+1 > limits.sources {
|
if len(sources)+1 > limits.sources {
|
||||||
return cloneSourceInfos(sources), []Diagnostic{importDiagnostic(CodeOverlayLimitExceeded, "selected overlay exceeds the source-count limit", nil)}
|
return cloneSourceInfos(sources), []Diagnostic{importDiagnostic(CodeOverlayLimitExceeded, "selected overlay exceeds the source-count limit", nil)}
|
||||||
@@ -174,6 +179,37 @@ func loadSelectedOverlay(request selectedOverlayRequest, access *filesystemAcces
|
|||||||
return sources, nil
|
return sources, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func overlayRepeatsSource(access *filesystemAccess, links fs.ReadLinkFS, canonical string, sources []SourceInfo) bool {
|
||||||
|
// Other inputs can have a wider root. Compare their identities in the
|
||||||
|
// shared native namespace while keeping overlay lookup rooted separately.
|
||||||
|
namespaceAccess := *access
|
||||||
|
namespaceAccess.rootPath = "."
|
||||||
|
for _, source := range sources {
|
||||||
|
if source.Path == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if access.native == nil {
|
||||||
|
if source.Kind == SourceKindFile && source.Path == canonical {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if source.Kind != SourceKindFilesystem || source.namespace != access.token {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if source.Path == canonical {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if links != nil {
|
||||||
|
target, err := resolveNativeImportPath(&namespaceAccess, links, source.Path)
|
||||||
|
if err == nil && target == canonical {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func readOverlayFile(access *filesystemAccess, name, sourceID string, remaining int64) ([]byte, *Diagnostic) {
|
func readOverlayFile(access *filesystemAccess, name, sourceID string, remaining int64) ([]byte, *Diagnostic) {
|
||||||
limit := min(int64(maxSourceBytes), remaining)
|
limit := min(int64(maxSourceBytes), remaining)
|
||||||
if limit < 0 {
|
if limit < 0 {
|
||||||
|
|||||||
Reference in New Issue
Block a user