mirror of
https://gitea.com/Lydanne/buildx.git
synced 2025-05-22 19:57:44 +08:00

Currently, to compare the local path used by bake against the paths allowed by entitlements, symlinks were evaluated for path normalization so that the local path used by build was allowed to not exist while the path allowed by entitlement needed to exist. If the path used by the build did not exist, then the deepest existing parent path was used instead. This was concistent with entitlement rules as that parent path would be the actual path access is needed. This raised an issue with `--set` if one provides a non-existing path as an argument, as these paths are supposed to be allowed automatically. With the above restrictions set to allowed paths, this meant the build would fail as it can't grant entitlement to the non-existing paths. This changes the evaluation logic for allowing paths so that they do not need to exist. If such a case appears, then the path is evaluated to the last component that exists, and then the rest of the path is appended as is. This means that for example, if `output = /tmp/out/foo/` is set in HCL and `/tmp` is the last component that exists then invoking build with `--allow fs.write=/tmp/out/foo` will not fail with stat error anymore but will fail in entitlements validation as build would also need to write `/tmp/out` that is not inside the allowed `/tmp/out/foo` path. The same would apply to `--set` as well so that if it points to a non-existing path, then an additional `--allow` rule is needed providing access to writing to the last existing component of that path. This may or may not be unexpected. Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
445 lines
10 KiB
Go
445 lines
10 KiB
Go
package bake
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"slices"
|
|
"testing"
|
|
|
|
"github.com/docker/buildx/build"
|
|
"github.com/docker/buildx/controller/pb"
|
|
"github.com/docker/buildx/util/osutil"
|
|
"github.com/moby/buildkit/client/llb"
|
|
"github.com/moby/buildkit/util/entitlements"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestEvaluateToExistingPath(t *testing.T) {
|
|
tempDir, err := osutil.GetLongPathName(t.TempDir())
|
|
require.NoError(t, err)
|
|
|
|
// Setup temporary directory structure for testing
|
|
existingFile := filepath.Join(tempDir, "existing_file")
|
|
require.NoError(t, os.WriteFile(existingFile, []byte("test"), 0644))
|
|
|
|
existingDir := filepath.Join(tempDir, "existing_dir")
|
|
require.NoError(t, os.Mkdir(existingDir, 0755))
|
|
|
|
symlinkToFile := filepath.Join(tempDir, "symlink_to_file")
|
|
require.NoError(t, os.Symlink(existingFile, symlinkToFile))
|
|
|
|
symlinkToDir := filepath.Join(tempDir, "symlink_to_dir")
|
|
require.NoError(t, os.Symlink(existingDir, symlinkToDir))
|
|
|
|
nonexistentPath := filepath.Join(tempDir, "nonexistent", "path", "file.txt")
|
|
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
expected string
|
|
expectErr bool
|
|
}{
|
|
{
|
|
name: "Existing file",
|
|
input: existingFile,
|
|
expected: existingFile,
|
|
expectErr: false,
|
|
},
|
|
{
|
|
name: "Existing directory",
|
|
input: existingDir,
|
|
expected: existingDir,
|
|
expectErr: false,
|
|
},
|
|
{
|
|
name: "Symlink to file",
|
|
input: symlinkToFile,
|
|
expected: existingFile,
|
|
expectErr: false,
|
|
},
|
|
{
|
|
name: "Symlink to directory",
|
|
input: symlinkToDir,
|
|
expected: existingDir,
|
|
expectErr: false,
|
|
},
|
|
{
|
|
name: "Non-existent path",
|
|
input: nonexistentPath,
|
|
expected: tempDir,
|
|
expectErr: false,
|
|
},
|
|
{
|
|
name: "Non-existent intermediate path",
|
|
input: filepath.Join(tempDir, "nonexistent", "file.txt"),
|
|
expected: tempDir,
|
|
expectErr: false,
|
|
},
|
|
{
|
|
name: "Root path",
|
|
input: "/",
|
|
expected: func() string {
|
|
root, _ := filepath.Abs("/")
|
|
return root
|
|
}(),
|
|
expectErr: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result, _, err := evaluateToExistingPath(tt.input)
|
|
|
|
if tt.expectErr {
|
|
require.Error(t, err)
|
|
} else {
|
|
require.NoError(t, err)
|
|
require.Equal(t, tt.expected, result)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDedupePaths(t *testing.T) {
|
|
wd := osutil.GetWd()
|
|
tcases := []struct {
|
|
in map[string]struct{}
|
|
out map[string]struct{}
|
|
}{
|
|
{
|
|
in: map[string]struct{}{
|
|
"/a/b/c": {},
|
|
"/a/b/d": {},
|
|
"/a/b/e": {},
|
|
},
|
|
out: map[string]struct{}{
|
|
"/a/b/c": {},
|
|
"/a/b/d": {},
|
|
"/a/b/e": {},
|
|
},
|
|
},
|
|
{
|
|
in: map[string]struct{}{
|
|
"/a/b/c": {},
|
|
"/a/b/c/d": {},
|
|
"/a/b/c/d/e": {},
|
|
"/a/b/../b/c": {},
|
|
},
|
|
out: map[string]struct{}{
|
|
"/a/b/c": {},
|
|
},
|
|
},
|
|
{
|
|
in: map[string]struct{}{
|
|
filepath.Join(wd, "a/b/c"): {},
|
|
filepath.Join(wd, "../aa"): {},
|
|
filepath.Join(wd, "a/b"): {},
|
|
filepath.Join(wd, "a/b/d"): {},
|
|
filepath.Join(wd, "../aa/b"): {},
|
|
filepath.Join(wd, "../../bb"): {},
|
|
},
|
|
out: map[string]struct{}{
|
|
"a/b": {},
|
|
"../aa": {},
|
|
filepath.Join(wd, "../../bb"): {},
|
|
},
|
|
},
|
|
}
|
|
|
|
for i, tc := range tcases {
|
|
t.Run(fmt.Sprintf("case%d", i), func(t *testing.T) {
|
|
out, err := dedupPaths(tc.in)
|
|
if err != nil {
|
|
require.NoError(t, err)
|
|
}
|
|
// convert to relative paths as that is shown to user
|
|
arr := make([]string, 0, len(out))
|
|
for k := range out {
|
|
arr = append(arr, k)
|
|
}
|
|
require.NoError(t, err)
|
|
arr = toRelativePaths(arr, wd)
|
|
m := make(map[string]struct{})
|
|
for _, v := range arr {
|
|
m[filepath.ToSlash(v)] = struct{}{}
|
|
}
|
|
o := make(map[string]struct{}, len(tc.out))
|
|
for k := range tc.out {
|
|
o[filepath.ToSlash(k)] = struct{}{}
|
|
}
|
|
require.Equal(t, o, m)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidateEntitlements(t *testing.T) {
|
|
dir1 := t.TempDir()
|
|
dir2 := t.TempDir()
|
|
|
|
// the paths returned by entitlements validation will have symlinks resolved
|
|
expDir1, err := filepath.EvalSymlinks(dir1)
|
|
require.NoError(t, err)
|
|
expDir2, err := filepath.EvalSymlinks(dir2)
|
|
require.NoError(t, err)
|
|
|
|
escapeLink := filepath.Join(dir1, "escape_link")
|
|
require.NoError(t, os.Symlink("../../aa", escapeLink))
|
|
|
|
wd, err := os.Getwd()
|
|
require.NoError(t, err)
|
|
expWd, err := filepath.EvalSymlinks(wd)
|
|
require.NoError(t, err)
|
|
|
|
tcases := []struct {
|
|
name string
|
|
conf EntitlementConf
|
|
opt build.Options
|
|
expected EntitlementConf
|
|
}{
|
|
{
|
|
name: "No entitlements",
|
|
opt: build.Options{
|
|
Inputs: build.Inputs{
|
|
ContextState: &llb.State{},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "NetworkHostMissing",
|
|
opt: build.Options{
|
|
Allow: []entitlements.Entitlement{
|
|
entitlements.EntitlementNetworkHost,
|
|
},
|
|
},
|
|
expected: EntitlementConf{
|
|
NetworkHost: true,
|
|
FSRead: []string{expWd},
|
|
},
|
|
},
|
|
{
|
|
name: "NetworkHostSet",
|
|
conf: EntitlementConf{
|
|
NetworkHost: true,
|
|
},
|
|
opt: build.Options{
|
|
Allow: []entitlements.Entitlement{
|
|
entitlements.EntitlementNetworkHost,
|
|
},
|
|
},
|
|
expected: EntitlementConf{
|
|
FSRead: []string{expWd},
|
|
},
|
|
},
|
|
{
|
|
name: "SecurityAndNetworkHostMissing",
|
|
opt: build.Options{
|
|
Allow: []entitlements.Entitlement{
|
|
entitlements.EntitlementNetworkHost,
|
|
entitlements.EntitlementSecurityInsecure,
|
|
},
|
|
},
|
|
expected: EntitlementConf{
|
|
NetworkHost: true,
|
|
SecurityInsecure: true,
|
|
FSRead: []string{expWd},
|
|
},
|
|
},
|
|
{
|
|
name: "SecurityMissingAndNetworkHostSet",
|
|
conf: EntitlementConf{
|
|
NetworkHost: true,
|
|
},
|
|
opt: build.Options{
|
|
Allow: []entitlements.Entitlement{
|
|
entitlements.EntitlementNetworkHost,
|
|
entitlements.EntitlementSecurityInsecure,
|
|
},
|
|
},
|
|
expected: EntitlementConf{
|
|
SecurityInsecure: true,
|
|
FSRead: []string{expWd},
|
|
},
|
|
},
|
|
{
|
|
name: "SSHMissing",
|
|
opt: build.Options{
|
|
SSHSpecs: []*pb.SSH{
|
|
{
|
|
ID: "test",
|
|
},
|
|
},
|
|
},
|
|
expected: EntitlementConf{
|
|
SSH: true,
|
|
FSRead: []string{expWd},
|
|
},
|
|
},
|
|
{
|
|
name: "ExportLocal",
|
|
opt: build.Options{
|
|
ExportsLocalPathsTemporary: []string{
|
|
dir1,
|
|
filepath.Join(dir1, "subdir"),
|
|
dir2,
|
|
},
|
|
},
|
|
expected: EntitlementConf{
|
|
FSWrite: func() []string {
|
|
exp := []string{expDir1, expDir2}
|
|
slices.Sort(exp)
|
|
return exp
|
|
}(),
|
|
FSRead: []string{expWd},
|
|
},
|
|
},
|
|
{
|
|
name: "SecretFromSubFile",
|
|
opt: build.Options{
|
|
SecretSpecs: []*pb.Secret{
|
|
{
|
|
FilePath: filepath.Join(dir1, "subfile"),
|
|
},
|
|
},
|
|
},
|
|
conf: EntitlementConf{
|
|
FSRead: []string{wd, dir1},
|
|
},
|
|
},
|
|
{
|
|
name: "SecretFromEscapeLink",
|
|
opt: build.Options{
|
|
SecretSpecs: []*pb.Secret{
|
|
{
|
|
FilePath: escapeLink,
|
|
},
|
|
},
|
|
},
|
|
conf: EntitlementConf{
|
|
FSRead: []string{wd, dir1},
|
|
},
|
|
expected: EntitlementConf{
|
|
FSRead: []string{filepath.Join(expDir1, "../..")},
|
|
},
|
|
},
|
|
{
|
|
name: "SecretFromEscapeLinkAllowRoot",
|
|
opt: build.Options{
|
|
SecretSpecs: []*pb.Secret{
|
|
{
|
|
FilePath: escapeLink,
|
|
},
|
|
},
|
|
},
|
|
conf: EntitlementConf{
|
|
FSRead: []string{"/"},
|
|
},
|
|
expected: EntitlementConf{
|
|
FSRead: func() []string {
|
|
// on windows root (/) is only allowed if it is the same volume as wd
|
|
if filepath.VolumeName(wd) == filepath.VolumeName(escapeLink) {
|
|
return nil
|
|
}
|
|
// if not, then escapeLink is not allowed
|
|
exp, _, err := evaluateToExistingPath(escapeLink)
|
|
require.NoError(t, err)
|
|
exp, err = filepath.EvalSymlinks(exp)
|
|
require.NoError(t, err)
|
|
return []string{exp}
|
|
}(),
|
|
},
|
|
},
|
|
{
|
|
name: "SecretFromEscapeLinkAllowAny",
|
|
opt: build.Options{
|
|
SecretSpecs: []*pb.Secret{
|
|
{
|
|
FilePath: escapeLink,
|
|
},
|
|
},
|
|
},
|
|
conf: EntitlementConf{
|
|
FSRead: []string{"*"},
|
|
},
|
|
expected: EntitlementConf{},
|
|
},
|
|
}
|
|
|
|
for _, tc := range tcases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
expected, err := tc.conf.Validate(map[string]build.Options{"test": tc.opt})
|
|
require.NoError(t, err)
|
|
require.Equal(t, tc.expected, expected)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGroupSamePaths(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
in1 []string
|
|
in2 []string
|
|
expected1 []string
|
|
expected2 []string
|
|
expectedC []string
|
|
}{
|
|
{
|
|
name: "All common paths",
|
|
in1: []string{"/path/a", "/path/b", "/path/c"},
|
|
in2: []string{"/path/a", "/path/b", "/path/c"},
|
|
expected1: []string{},
|
|
expected2: []string{},
|
|
expectedC: []string{"/path/a", "/path/b", "/path/c"},
|
|
},
|
|
{
|
|
name: "No common paths",
|
|
in1: []string{"/path/a", "/path/b"},
|
|
in2: []string{"/path/c", "/path/d"},
|
|
expected1: []string{"/path/a", "/path/b"},
|
|
expected2: []string{"/path/c", "/path/d"},
|
|
expectedC: []string{},
|
|
},
|
|
{
|
|
name: "Some common paths",
|
|
in1: []string{"/path/a", "/path/b", "/path/c"},
|
|
in2: []string{"/path/b", "/path/c", "/path/d"},
|
|
expected1: []string{"/path/a"},
|
|
expected2: []string{"/path/d"},
|
|
expectedC: []string{"/path/b", "/path/c"},
|
|
},
|
|
{
|
|
name: "Empty inputs",
|
|
in1: []string{},
|
|
in2: []string{},
|
|
expected1: []string{},
|
|
expected2: []string{},
|
|
expectedC: []string{},
|
|
},
|
|
{
|
|
name: "One empty input",
|
|
in1: []string{"/path/a", "/path/b"},
|
|
in2: []string{},
|
|
expected1: []string{"/path/a", "/path/b"},
|
|
expected2: []string{},
|
|
expectedC: []string{},
|
|
},
|
|
{
|
|
name: "Unsorted inputs with common paths",
|
|
in1: []string{"/path/c", "/path/a", "/path/b"},
|
|
in2: []string{"/path/b", "/path/c", "/path/a"},
|
|
expected1: []string{},
|
|
expected2: []string{},
|
|
expectedC: []string{"/path/a", "/path/b", "/path/c"},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
out1, out2, common := groupSamePaths(tt.in1, tt.in2)
|
|
require.Equal(t, tt.expected1, out1, "in1 should match expected1")
|
|
require.Equal(t, tt.expected2, out2, "in2 should match expected2")
|
|
require.Equal(t, tt.expectedC, common, "common should match expectedC")
|
|
})
|
|
}
|
|
}
|