354 lines
11 KiB
Go
354 lines
11 KiB
Go
package engine
|
|
|
|
import (
|
|
"io/fs"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"testing/fstest"
|
|
"time"
|
|
)
|
|
|
|
func TestSelectDirectoriesApplyHiddenAndFollowControls(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
root := t.TempDir()
|
|
for name, content := range map[string]string{
|
|
"direct.yaml": "value: direct\n",
|
|
".private.yaml": "value: hidden\n",
|
|
".private/nested.yaml": "value: hidden directory\n",
|
|
"target/linked.yaml": "value: linked\n",
|
|
"target/source.txt": "value: source\n",
|
|
} {
|
|
filename := filepath.Join(root, filepath.FromSlash(name))
|
|
if err := os.MkdirAll(filepath.Dir(filename), 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filename, []byte(content), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if err := os.Symlink("target", filepath.Join(root, "visible-alias")); err != nil {
|
|
t.Skipf("symlinks are unavailable: %v", err)
|
|
}
|
|
if err := os.Symlink("target/linked.yaml", filepath.Join(root, "linked-alias.txt")); err != nil {
|
|
t.Skipf("symlinks are unavailable: %v", err)
|
|
}
|
|
if err := os.Symlink("target/source.txt", filepath.Join(root, "selected.yaml")); err != nil {
|
|
t.Skipf("symlinks are unavailable: %v", err)
|
|
}
|
|
if err := os.Symlink("target", filepath.Join(root, ".hidden-alias")); err != nil {
|
|
t.Skipf("symlinks are unavailable: %v", err)
|
|
}
|
|
|
|
access, err := openHostAccess(root)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer access.close()
|
|
|
|
selection := selectDirectoryWithOptions(t, access, root, ` - dir: .
|
|
recursive: true
|
|
follow: false
|
|
hidden: false
|
|
pattern: '*.yaml'
|
|
`)
|
|
if got, want := selectedPathsRelativeTo(selection, root), []string{"direct.yaml", "target/linked.yaml"}; !equalStrings(got, want) {
|
|
t.Fatalf("filtered paths = %#v, want %#v", got, want)
|
|
}
|
|
selection = selectDirectoryWithOptions(t, access, root, ` - dir: visible-alias
|
|
follow: false
|
|
pattern: '*.yaml'
|
|
`)
|
|
if got, want := selectedPathsRelativeTo(selection, root), []string{"visible-alias/linked.yaml"}; !equalStrings(got, want) {
|
|
t.Fatalf("explicit symlink root paths = %#v, want %#v", got, want)
|
|
}
|
|
|
|
selection = selectDirectoryWithOptions(t, access, root, ` - dir: .
|
|
recursive: true
|
|
follow: true
|
|
hidden: false
|
|
pattern: '*.yaml'
|
|
`)
|
|
if got, want := selectedPathsRelativeTo(selection, root), []string{"direct.yaml", "selected.yaml", "target/linked.yaml", "visible-alias/linked.yaml"}; !equalStrings(got, want) {
|
|
t.Fatalf("followed paths = %#v, want %#v", got, want)
|
|
}
|
|
|
|
selection = selectDirectoryWithOptions(t, access, root, ` - dir: .
|
|
recursive: true
|
|
follow: true
|
|
hidden: true
|
|
pattern: '*.yaml'
|
|
`)
|
|
if got, want := selectedPathsRelativeTo(selection, root), []string{
|
|
".hidden-alias/linked.yaml", ".private.yaml", ".private/nested.yaml", "direct.yaml",
|
|
"selected.yaml", "target/linked.yaml", "visible-alias/linked.yaml",
|
|
}; !equalStrings(got, want) {
|
|
t.Fatalf("hidden-enabled paths = %#v, want %#v", got, want)
|
|
}
|
|
}
|
|
|
|
func TestSelectDirectoriesRejectEscapingAndCyclicLinks(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Run("escape", func(t *testing.T) {
|
|
root := t.TempDir()
|
|
outside := t.TempDir()
|
|
if err := os.WriteFile(filepath.Join(outside, "outside.yaml"), []byte("value: outside\n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.Symlink(outside, filepath.Join(root, "escape")); err != nil {
|
|
t.Skipf("symlinks are unavailable: %v", err)
|
|
}
|
|
access, err := openHostAccess(root)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer access.close()
|
|
_, diagnostics := selectDirectoryWithDiagnostics(t, access, root, ` - dir: .
|
|
recursive: true
|
|
follow: true
|
|
pattern: '*.yaml'
|
|
`)
|
|
if len(diagnostics) != 1 || diagnostics[0].Code != CodeImportOutsideRoot {
|
|
t.Fatalf("escape diagnostics = %#v, want import.outside_root", diagnostics)
|
|
}
|
|
selection := selectDirectoryWithOptions(t, access, root, ` - dir: .
|
|
recursive: true
|
|
follow: false
|
|
pattern: '*.yaml'
|
|
`)
|
|
if len(selection.candidates) != 0 {
|
|
t.Fatalf("follow:false selected an escaping symlink: %#v", selection.candidates)
|
|
}
|
|
})
|
|
|
|
for _, test := range []struct {
|
|
name string
|
|
}{
|
|
{name: "self"},
|
|
{name: "ancestor"},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
root := t.TempDir()
|
|
if test.name == "self" {
|
|
if err := os.Symlink(".", filepath.Join(root, "loop")); err != nil {
|
|
t.Skipf("symlinks are unavailable: %v", err)
|
|
}
|
|
} else {
|
|
if err := os.Mkdir(filepath.Join(root, "nested"), 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.Symlink("..", filepath.Join(root, "nested", "up")); err != nil {
|
|
t.Skipf("symlinks are unavailable: %v", err)
|
|
}
|
|
}
|
|
access, err := openHostAccess(root)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer access.close()
|
|
_, diagnostics := selectDirectoryWithDiagnostics(t, access, root, ` - dir: .
|
|
recursive: true
|
|
follow: true
|
|
`)
|
|
if len(diagnostics) != 1 || diagnostics[0].Code != CodeImportCycle {
|
|
t.Fatalf("cycle diagnostics = %#v, want import.cycle", diagnostics)
|
|
}
|
|
if diagnostics[0].Message != "directory traversal cycle detected" {
|
|
t.Fatalf("cycle diagnostic exposes unexpected detail: %q", diagnostics[0].Message)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSelectDirectoriesRetainSeparateNoncyclicAliases(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
root := t.TempDir()
|
|
if err := os.Mkdir(filepath.Join(root, "target"), 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(root, "target", "item.yaml"), []byte("value: item\n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, alias := range []string{"first", "second"} {
|
|
if err := os.Symlink("target", filepath.Join(root, alias)); err != nil {
|
|
t.Skipf("symlinks are unavailable: %v", err)
|
|
}
|
|
}
|
|
access, err := openHostAccess(root)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer access.close()
|
|
|
|
selection := selectDirectoryWithOptions(t, access, root, ` - dir: .
|
|
recursive: true
|
|
follow: true
|
|
pattern: '*.yaml'
|
|
`)
|
|
want := []string{"first/item.yaml", "second/item.yaml", "target/item.yaml"}
|
|
if got := selectedPathsRelativeTo(selection, root); !equalStrings(got, want) {
|
|
t.Fatalf("alias candidates = %#v, want %#v", got, want)
|
|
}
|
|
}
|
|
|
|
func TestSelectDirectoriesUseNativeReadLinkCapability(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
filesystem := &linkAwareMapFS{
|
|
files: fstest.MapFS{
|
|
"real": &fstest.MapFile{Mode: fs.ModeDir | 0o700},
|
|
"real/item.yaml": &fstest.MapFile{Data: []byte("value: item\n")},
|
|
"alias": &fstest.MapFile{Mode: fs.ModeSymlink, Data: []byte("real")},
|
|
".hidden": &fstest.MapFile{Mode: fs.ModeDir | 0o700},
|
|
".hidden/secret.yaml": &fstest.MapFile{Data: []byte("value: secret\n")},
|
|
},
|
|
links: map[string]string{"alias": "real"},
|
|
}
|
|
access := newNativeAccess(filesystem, ".", &namespaceToken{})
|
|
selection := selectDirectoryWithOptions(t, access, ".", ` - dir: .
|
|
recursive: true
|
|
follow: true
|
|
hidden: false
|
|
pattern: '*.yaml'
|
|
`)
|
|
if got, want := selectedPaths(selection), []string{"alias/item.yaml", "real/item.yaml"}; !equalStrings(got, want) {
|
|
t.Fatalf("native link candidates = %#v, want %#v", got, want)
|
|
}
|
|
cyclic := &linkAwareMapFS{
|
|
files: fstest.MapFS{
|
|
"loop": &fstest.MapFile{Mode: fs.ModeSymlink, Data: []byte(".")},
|
|
},
|
|
links: map[string]string{"loop": "."},
|
|
}
|
|
_, diagnostics := selectDirectoryWithDiagnostics(t, newNativeAccess(cyclic, ".", &namespaceToken{}), ".", ` - dir: .
|
|
recursive: true
|
|
follow: true
|
|
`)
|
|
if len(diagnostics) != 1 || diagnostics[0].Code != CodeImportCycle {
|
|
t.Fatalf("native loop diagnostics = %#v, want import.cycle", diagnostics)
|
|
}
|
|
|
|
plain := newNativeAccess(fstest.MapFS{"plain.yaml": &fstest.MapFile{Data: []byte("value: plain\n")}}, ".", &namespaceToken{})
|
|
selection = selectDirectoryWithOptions(t, plain, ".", " - dir: .\n")
|
|
if got, want := selectedPaths(selection), []string{"plain.yaml"}; !equalStrings(got, want) {
|
|
t.Fatalf("plain native filesystem selection = %#v, want %#v", got, want)
|
|
}
|
|
}
|
|
|
|
func selectDirectoryWithOptions(t *testing.T, access *filesystemAccess, base, declaration string) importSelection {
|
|
t.Helper()
|
|
selection, diagnostics := selectDirectoryWithDiagnostics(t, access, base, declaration)
|
|
if len(diagnostics) != 0 {
|
|
t.Fatalf("directory selection diagnostics = %#v", diagnostics)
|
|
}
|
|
return selection
|
|
}
|
|
|
|
func selectDirectoryWithDiagnostics(t *testing.T, access *filesystemAccess, base, declaration string) (importSelection, []Diagnostic) {
|
|
t.Helper()
|
|
specs, diagnostics := parseImportSpecsYAML(t, declaration)
|
|
if len(diagnostics) != 0 {
|
|
t.Fatalf("parse diagnostics = %#v", diagnostics)
|
|
}
|
|
policy, diagnostics := resolveImportPolicy(specs[0], importDefaults{})
|
|
if len(diagnostics) != 0 {
|
|
t.Fatalf("policy diagnostics = %#v", diagnostics)
|
|
}
|
|
return selectDirectoryImportCandidates(access, base, specs[0], policy, defaultImportLimits(), &importAccounting{})
|
|
}
|
|
|
|
func selectedPaths(selection importSelection) []string {
|
|
paths := make([]string, 0, len(selection.candidates))
|
|
for _, candidate := range selection.candidates {
|
|
paths = append(paths, candidate.path)
|
|
}
|
|
return paths
|
|
}
|
|
|
|
func selectedPathsRelativeTo(selection importSelection, root string) []string {
|
|
paths := make([]string, 0, len(selection.candidates))
|
|
for _, candidate := range selection.candidates {
|
|
relative, err := filepath.Rel(root, candidate.path)
|
|
if err != nil {
|
|
paths = append(paths, candidate.path)
|
|
continue
|
|
}
|
|
paths = append(paths, filepath.ToSlash(relative))
|
|
}
|
|
return paths
|
|
}
|
|
|
|
func equalStrings(left, right []string) bool {
|
|
if len(left) != len(right) {
|
|
return false
|
|
}
|
|
for index := range left {
|
|
if left[index] != right[index] {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
type linkAwareMapFS struct {
|
|
files fstest.MapFS
|
|
links map[string]string
|
|
}
|
|
|
|
func (f *linkAwareMapFS) Open(name string) (fs.File, error) {
|
|
resolved, err := f.resolve(name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return f.files.Open(resolved)
|
|
}
|
|
|
|
func (f *linkAwareMapFS) ReadLink(name string) (string, error) {
|
|
target, ok := f.links[name]
|
|
if !ok {
|
|
return "", &fs.PathError{Op: "readlink", Path: name, Err: fs.ErrInvalid}
|
|
}
|
|
return target, nil
|
|
}
|
|
|
|
func (f *linkAwareMapFS) Lstat(name string) (fs.FileInfo, error) {
|
|
if _, ok := f.links[name]; ok {
|
|
return linkMapFileInfo{name: path.Base(name)}, nil
|
|
}
|
|
return fs.Stat(f.files, name)
|
|
}
|
|
|
|
func (f *linkAwareMapFS) resolve(name string) (string, error) {
|
|
if !fs.ValidPath(name) {
|
|
return "", fs.ErrInvalid
|
|
}
|
|
resolved := ""
|
|
seen := make(map[string]struct{})
|
|
for _, segment := range strings.Split(name, "/") {
|
|
candidate := path.Join(resolved, segment)
|
|
if target, ok := f.links[candidate]; ok {
|
|
if _, exists := seen[candidate]; exists {
|
|
return "", fs.ErrInvalid
|
|
}
|
|
seen[candidate] = struct{}{}
|
|
candidate = path.Clean(path.Join(path.Dir(candidate), target))
|
|
}
|
|
resolved = candidate
|
|
}
|
|
return resolved, nil
|
|
}
|
|
|
|
type linkMapFileInfo struct{ name string }
|
|
|
|
func (i linkMapFileInfo) Name() string { return i.name }
|
|
func (linkMapFileInfo) Size() int64 { return 0 }
|
|
func (linkMapFileInfo) Mode() fs.FileMode { return fs.ModeSymlink | 0o777 }
|
|
func (linkMapFileInfo) ModTime() time.Time { return time.Time{} }
|
|
func (linkMapFileInfo) IsDir() bool { return false }
|
|
func (linkMapFileInfo) Sys() any { return nil }
|