mirror of
https://gitea.com/Lydanne/buildx.git
synced 2025-05-18 00:47:48 +08:00
Merge pull request #2796 from tonistiigi/fs-entitlements
bake: add filesystem entitlements support
This commit is contained in:
commit
a34c641bc4
69
bake/bake.go
69
bake/bake.go
@ -1117,62 +1117,34 @@ func updateContext(t *build.Inputs, inp *Input) {
|
||||
t.ContextState = &st
|
||||
}
|
||||
|
||||
// validateContextsEntitlements is a basic check to ensure contexts do not
|
||||
// escape local directories when loaded from remote sources. This is to be
|
||||
// replaced with proper entitlements support in the future.
|
||||
func validateContextsEntitlements(t build.Inputs, inp *Input) error {
|
||||
if inp == nil || inp.State == nil {
|
||||
return nil
|
||||
}
|
||||
if v, ok := os.LookupEnv("BAKE_ALLOW_REMOTE_FS_ACCESS"); ok {
|
||||
if vv, _ := strconv.ParseBool(v); vv {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
func collectLocalPaths(t build.Inputs) []string {
|
||||
var out []string
|
||||
if t.ContextState == nil {
|
||||
if err := checkPath(t.ContextPath); err != nil {
|
||||
return err
|
||||
if v, ok := isLocalPath(t.ContextPath); ok {
|
||||
out = append(out, v)
|
||||
}
|
||||
if v, ok := isLocalPath(t.DockerfilePath); ok {
|
||||
out = append(out, v)
|
||||
}
|
||||
} else if strings.HasPrefix(t.ContextPath, "cwd://") {
|
||||
out = append(out, strings.TrimPrefix(t.ContextPath, "cwd://"))
|
||||
}
|
||||
for _, v := range t.NamedContexts {
|
||||
if v.State != nil {
|
||||
continue
|
||||
}
|
||||
if err := checkPath(v.Path); err != nil {
|
||||
return err
|
||||
if v, ok := isLocalPath(v.Path); ok {
|
||||
out = append(out, v)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
return out
|
||||
}
|
||||
|
||||
func checkPath(p string) error {
|
||||
func isLocalPath(p string) (string, bool) {
|
||||
if build.IsRemoteURL(p) || strings.HasPrefix(p, "target:") || strings.HasPrefix(p, "docker-image:") {
|
||||
return nil
|
||||
return "", false
|
||||
}
|
||||
p, err := filepath.EvalSymlinks(p)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
p, err = filepath.Abs(p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rel, err := filepath.Rel(wd, p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
parts := strings.Split(rel, string(os.PathSeparator))
|
||||
if parts[0] == ".." {
|
||||
return errors.Errorf("path %s is outside of the working directory, please set BAKE_ALLOW_REMOTE_FS_ACCESS=1", p)
|
||||
}
|
||||
return nil
|
||||
return strings.TrimPrefix(p, "cwd://"), true
|
||||
}
|
||||
|
||||
func toBuildOpt(t *Target, inp *Input) (*build.Options, error) {
|
||||
@ -1212,9 +1184,6 @@ func toBuildOpt(t *Target, inp *Input) (*build.Options, error) {
|
||||
// it's not outside the working directory and then resolve it to an
|
||||
// absolute path.
|
||||
bi.DockerfilePath = path.Clean(strings.TrimPrefix(bi.DockerfilePath, "cwd://"))
|
||||
if err := checkPath(bi.DockerfilePath); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var err error
|
||||
bi.DockerfilePath, err = filepath.Abs(bi.DockerfilePath)
|
||||
if err != nil {
|
||||
@ -1251,10 +1220,6 @@ func toBuildOpt(t *Target, inp *Input) (*build.Options, error) {
|
||||
}
|
||||
}
|
||||
|
||||
if err := validateContextsEntitlements(bi, inp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
t.Context = &bi.ContextPath
|
||||
|
||||
args := map[string]string{}
|
||||
@ -1315,6 +1280,8 @@ func toBuildOpt(t *Target, inp *Input) (*build.Options, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bo.SecretSpecs = secrets
|
||||
|
||||
secretAttachment, err := controllerapi.CreateSecrets(secrets)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -1328,6 +1295,8 @@ func toBuildOpt(t *Target, inp *Input) (*build.Options, error) {
|
||||
if len(sshSpecs) == 0 && (buildflags.IsGitSSH(bi.ContextPath) || (inp != nil && buildflags.IsGitSSH(inp.URL))) {
|
||||
sshSpecs = append(sshSpecs, &controllerapi.SSH{ID: "default"})
|
||||
}
|
||||
bo.SSHSpecs = sshSpecs
|
||||
|
||||
sshAttachment, err := controllerapi.CreateSSH(sshSpecs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -2,15 +2,21 @@ package bake
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"cmp"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/containerd/console"
|
||||
"github.com/docker/buildx/build"
|
||||
"github.com/docker/buildx/util/osutil"
|
||||
"github.com/moby/buildkit/util/entitlements"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
@ -67,10 +73,8 @@ func ParseEntitlements(in []string) (EntitlementConf, error) {
|
||||
conf.ImagePush = append(conf.ImagePush, v)
|
||||
conf.ImageLoad = append(conf.ImageLoad, v)
|
||||
default:
|
||||
return conf, errors.Errorf("uknown entitlement key %q", k)
|
||||
return conf, errors.Errorf("unknown entitlement key %q", k)
|
||||
}
|
||||
|
||||
// TODO: dedupe slices and parent paths
|
||||
}
|
||||
}
|
||||
return conf, nil
|
||||
@ -101,10 +105,73 @@ func (c EntitlementConf) check(bo build.Options, expected *EntitlementConf) erro
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rwPaths := map[string]struct{}{}
|
||||
roPaths := map[string]struct{}{}
|
||||
|
||||
for _, p := range collectLocalPaths(bo.Inputs) {
|
||||
roPaths[p] = struct{}{}
|
||||
}
|
||||
|
||||
for _, out := range bo.Exports {
|
||||
if out.Type == "local" {
|
||||
if dest, ok := out.Attrs["dest"]; ok {
|
||||
rwPaths[dest] = struct{}{}
|
||||
}
|
||||
}
|
||||
if out.Type == "tar" {
|
||||
if dest, ok := out.Attrs["dest"]; ok && dest != "-" {
|
||||
rwPaths[dest] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, ce := range bo.CacheTo {
|
||||
if ce.Type == "local" {
|
||||
if dest, ok := ce.Attrs["dest"]; ok {
|
||||
rwPaths[dest] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, ci := range bo.CacheFrom {
|
||||
if ci.Type == "local" {
|
||||
if src, ok := ci.Attrs["src"]; ok {
|
||||
roPaths[src] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, secret := range bo.SecretSpecs {
|
||||
if secret.FilePath != "" {
|
||||
roPaths[secret.FilePath] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
for _, ssh := range bo.SSHSpecs {
|
||||
for _, p := range ssh.Paths {
|
||||
roPaths[p] = struct{}{}
|
||||
}
|
||||
if len(ssh.Paths) == 0 {
|
||||
expected.SSH = true
|
||||
}
|
||||
}
|
||||
|
||||
var err error
|
||||
expected.FSRead, err = findMissingPaths(c.FSRead, roPaths)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
expected.FSWrite, err = findMissingPaths(c.FSWrite, rwPaths)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c EntitlementConf) Prompt(ctx context.Context, out io.Writer) error {
|
||||
func (c EntitlementConf) Prompt(ctx context.Context, isRemote bool, out io.Writer) error {
|
||||
var term bool
|
||||
if _, err := console.ConsoleFromFile(os.Stdin); err == nil {
|
||||
term = true
|
||||
@ -113,32 +180,78 @@ func (c EntitlementConf) Prompt(ctx context.Context, out io.Writer) error {
|
||||
var msgs []string
|
||||
var flags []string
|
||||
|
||||
// these warnings are currently disabled to give users time to update
|
||||
var msgsFS []string
|
||||
var flagsFS []string
|
||||
|
||||
if c.NetworkHost {
|
||||
msgs = append(msgs, " - Running build containers that can access host network")
|
||||
flags = append(flags, "network.host")
|
||||
flags = append(flags, string(EntitlementKeyNetworkHost))
|
||||
}
|
||||
if c.SecurityInsecure {
|
||||
msgs = append(msgs, " - Running privileged containers that can make system changes")
|
||||
flags = append(flags, "security.insecure")
|
||||
flags = append(flags, string(EntitlementKeySecurityInsecure))
|
||||
}
|
||||
|
||||
if len(msgs) == 0 {
|
||||
if c.SSH {
|
||||
msgsFS = append(msgsFS, " - Forwarding default SSH agent socket")
|
||||
flagsFS = append(flagsFS, string(EntitlementKeySSH))
|
||||
}
|
||||
|
||||
roPaths, rwPaths, commonPaths := groupSamePaths(c.FSRead, c.FSWrite)
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to get current working directory")
|
||||
}
|
||||
wd, err = filepath.EvalSymlinks(wd)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to evaluate working directory")
|
||||
}
|
||||
roPaths = toRelativePaths(roPaths, wd)
|
||||
rwPaths = toRelativePaths(rwPaths, wd)
|
||||
commonPaths = toRelativePaths(commonPaths, wd)
|
||||
|
||||
if len(commonPaths) > 0 {
|
||||
for _, p := range commonPaths {
|
||||
msgsFS = append(msgsFS, fmt.Sprintf(" - Read and write access to path %s", p))
|
||||
flagsFS = append(flagsFS, string(EntitlementKeyFS)+"="+p)
|
||||
}
|
||||
}
|
||||
|
||||
if len(roPaths) > 0 {
|
||||
for _, p := range roPaths {
|
||||
msgsFS = append(msgsFS, fmt.Sprintf(" - Read access to path %s", p))
|
||||
flagsFS = append(flagsFS, string(EntitlementKeyFSRead)+"="+p)
|
||||
}
|
||||
}
|
||||
|
||||
if len(rwPaths) > 0 {
|
||||
for _, p := range rwPaths {
|
||||
msgsFS = append(msgsFS, fmt.Sprintf(" - Write access to path %s", p))
|
||||
flagsFS = append(flagsFS, string(EntitlementKeyFSWrite)+"="+p)
|
||||
}
|
||||
}
|
||||
|
||||
if len(msgs) == 0 && len(msgsFS) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Fprintf(out, "Your build is requesting privileges for following possibly insecure capabilities:\n\n")
|
||||
for _, m := range msgs {
|
||||
for _, m := range slices.Concat(msgs, msgsFS) {
|
||||
fmt.Fprintf(out, "%s\n", m)
|
||||
}
|
||||
|
||||
for i, f := range flags {
|
||||
flags[i] = "--allow=" + f
|
||||
}
|
||||
for i, f := range flagsFS {
|
||||
flagsFS[i] = "--allow=" + f
|
||||
}
|
||||
|
||||
if term {
|
||||
fmt.Fprintf(out, "\nIn order to not see this message in the future pass %q to grant requested privileges.\n", strings.Join(flags, " "))
|
||||
fmt.Fprintf(out, "\nIn order to not see this message in the future pass %q to grant requested privileges.\n", strings.Join(slices.Concat(flags, flagsFS), " "))
|
||||
} else {
|
||||
fmt.Fprintf(out, "\nPass %q to grant requested privileges.\n", strings.Join(flags, " "))
|
||||
fmt.Fprintf(out, "\nPass %q to grant requested privileges.\n", strings.Join(slices.Concat(flags, flagsFS), " "))
|
||||
}
|
||||
|
||||
args := append([]string(nil), os.Args...)
|
||||
@ -149,7 +262,35 @@ func (c EntitlementConf) Prompt(ctx context.Context, out io.Writer) error {
|
||||
|
||||
if idx != -1 {
|
||||
fmt.Fprintf(out, "\nYour full command with requested privileges:\n\n")
|
||||
fmt.Fprintf(out, "%s %s %s\n\n", strings.Join(args[:idx+1], " "), strings.Join(flags, " "), strings.Join(args[idx+1:], " "))
|
||||
fmt.Fprintf(out, "%s %s %s\n\n", strings.Join(args[:idx+1], " "), strings.Join(slices.Concat(flags, flagsFS), " "), strings.Join(args[idx+1:], " "))
|
||||
}
|
||||
|
||||
fsEntitlementsEnabled := false
|
||||
if isRemote {
|
||||
if v, ok := os.LookupEnv("BAKE_ALLOW_REMOTE_FS_ACCESS"); ok {
|
||||
vv, err := strconv.ParseBool(v)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to parse BAKE_ALLOW_REMOTE_FS_ACCESS value %q", v)
|
||||
}
|
||||
fsEntitlementsEnabled = !vv
|
||||
} else {
|
||||
fsEntitlementsEnabled = true
|
||||
}
|
||||
}
|
||||
v, fsEntitlementsSet := os.LookupEnv("BUILDX_BAKE_ENTITLEMENTS_FS")
|
||||
if fsEntitlementsSet {
|
||||
vv, err := strconv.ParseBool(v)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to parse BUILDX_BAKE_ENTITLEMENTS_FS value %q", v)
|
||||
}
|
||||
fsEntitlementsEnabled = vv
|
||||
}
|
||||
|
||||
if !fsEntitlementsEnabled && len(msgs) == 0 {
|
||||
if !fsEntitlementsSet {
|
||||
fmt.Fprintf(out, "This warning will become an error in a future release. To enable filesystem entitlements checks at the moment, set BUILDX_BAKE_ENTITLEMENTS_FS=1 .\n\n")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if term {
|
||||
@ -173,3 +314,265 @@ func (c EntitlementConf) Prompt(ctx context.Context, out io.Writer) error {
|
||||
|
||||
return errors.Errorf("additional privileges requested")
|
||||
}
|
||||
|
||||
func isParentOrEqualPath(p, parent string) bool {
|
||||
if p == parent || parent == "/" {
|
||||
return true
|
||||
}
|
||||
if strings.HasPrefix(p, filepath.Clean(parent+string(filepath.Separator))) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func findMissingPaths(set []string, paths map[string]struct{}) ([]string, error) {
|
||||
paths, err := evaluateToExistingPaths(paths)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
paths, err = dedupPaths(paths)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
set, err = evaluatePaths(set)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
out := make([]string, 0, len(paths))
|
||||
loop0:
|
||||
for p := range paths {
|
||||
for _, c := range set {
|
||||
if isParentOrEqualPath(p, c) {
|
||||
continue loop0
|
||||
}
|
||||
}
|
||||
out = append(out, p)
|
||||
}
|
||||
if len(out) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
slices.Sort(out)
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func dedupPaths(in map[string]struct{}) (map[string]struct{}, error) {
|
||||
arr := make([]string, 0, len(in))
|
||||
for p := range in {
|
||||
arr = append(arr, filepath.Clean(p))
|
||||
}
|
||||
|
||||
slices.SortFunc(arr, func(a, b string) int {
|
||||
return cmp.Compare(len(a), len(b))
|
||||
})
|
||||
|
||||
m := make(map[string]struct{}, len(arr))
|
||||
loop0:
|
||||
for _, p := range arr {
|
||||
for parent := range m {
|
||||
if strings.HasPrefix(p, parent+string(filepath.Separator)) {
|
||||
continue loop0
|
||||
}
|
||||
}
|
||||
m[p] = struct{}{}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func toRelativePaths(in []string, wd string) []string {
|
||||
out := make([]string, 0, len(in))
|
||||
for _, p := range in {
|
||||
rel, err := filepath.Rel(wd, p)
|
||||
if err == nil {
|
||||
// allow up to one level of ".." in the path
|
||||
if !strings.HasPrefix(rel, ".."+string(filepath.Separator)+"..") {
|
||||
out = append(out, rel)
|
||||
continue
|
||||
}
|
||||
}
|
||||
out = append(out, p)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func groupSamePaths(in1, in2 []string) ([]string, []string, []string) {
|
||||
if in1 == nil || in2 == nil {
|
||||
return in1, in2, nil
|
||||
}
|
||||
|
||||
slices.Sort(in1)
|
||||
slices.Sort(in2)
|
||||
|
||||
common := []string{}
|
||||
i, j := 0, 0
|
||||
|
||||
for i < len(in1) && j < len(in2) {
|
||||
switch {
|
||||
case in1[i] == in2[j]:
|
||||
common = append(common, in1[i])
|
||||
i++
|
||||
j++
|
||||
case in1[i] < in2[j]:
|
||||
i++
|
||||
default:
|
||||
j++
|
||||
}
|
||||
}
|
||||
|
||||
in1 = removeCommonPaths(in1, common)
|
||||
in2 = removeCommonPaths(in2, common)
|
||||
|
||||
return in1, in2, common
|
||||
}
|
||||
|
||||
func removeCommonPaths(in, common []string) []string {
|
||||
filtered := make([]string, 0, len(in))
|
||||
commonIndex := 0
|
||||
for _, path := range in {
|
||||
if commonIndex < len(common) && path == common[commonIndex] {
|
||||
commonIndex++
|
||||
continue
|
||||
}
|
||||
filtered = append(filtered, path)
|
||||
}
|
||||
return filtered
|
||||
}
|
||||
|
||||
func evaluateToExistingPaths(in map[string]struct{}) (map[string]struct{}, error) {
|
||||
m := make(map[string]struct{}, len(in))
|
||||
for p := range in {
|
||||
v, err := evaluateToExistingPath(p)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to evaluate path %q", p)
|
||||
}
|
||||
v, err = osutil.GetLongPathName(v)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to evaluate path %q", p)
|
||||
}
|
||||
m[v] = struct{}{}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func evaluateToExistingPath(in string) (string, error) {
|
||||
in, err := filepath.Abs(in)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
volLen := volumeNameLen(in)
|
||||
pathSeparator := string(os.PathSeparator)
|
||||
|
||||
if volLen < len(in) && os.IsPathSeparator(in[volLen]) {
|
||||
volLen++
|
||||
}
|
||||
vol := in[:volLen]
|
||||
dest := vol
|
||||
linksWalked := 0
|
||||
var end int
|
||||
for start := volLen; start < len(in); start = end {
|
||||
for start < len(in) && os.IsPathSeparator(in[start]) {
|
||||
start++
|
||||
}
|
||||
end = start
|
||||
for end < len(in) && !os.IsPathSeparator(in[end]) {
|
||||
end++
|
||||
}
|
||||
|
||||
if end == start {
|
||||
break
|
||||
} else if in[start:end] == "." {
|
||||
continue
|
||||
} else if in[start:end] == ".." {
|
||||
var r int
|
||||
for r = len(dest) - 1; r >= volLen; r-- {
|
||||
if os.IsPathSeparator(dest[r]) {
|
||||
break
|
||||
}
|
||||
}
|
||||
if r < volLen || dest[r+1:] == ".." {
|
||||
if len(dest) > volLen {
|
||||
dest += pathSeparator
|
||||
}
|
||||
dest += ".."
|
||||
} else {
|
||||
dest = dest[:r]
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if len(dest) > volumeNameLen(dest) && !os.IsPathSeparator(dest[len(dest)-1]) {
|
||||
dest += pathSeparator
|
||||
}
|
||||
dest += in[start:end]
|
||||
|
||||
fi, err := os.Lstat(dest)
|
||||
if err != nil {
|
||||
// If the component doesn't exist, return the last valid path
|
||||
if os.IsNotExist(err) {
|
||||
for r := len(dest) - 1; r >= volLen; r-- {
|
||||
if os.IsPathSeparator(dest[r]) {
|
||||
return dest[:r], nil
|
||||
}
|
||||
}
|
||||
return vol, nil
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
||||
if fi.Mode()&fs.ModeSymlink == 0 {
|
||||
if !fi.Mode().IsDir() && end < len(in) {
|
||||
return "", syscall.ENOTDIR
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
linksWalked++
|
||||
if linksWalked > 255 {
|
||||
return "", errors.New("too many symlinks")
|
||||
}
|
||||
|
||||
link, err := os.Readlink(dest)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
in = link + in[end:]
|
||||
|
||||
v := volumeNameLen(link)
|
||||
if v > 0 {
|
||||
if v < len(link) && os.IsPathSeparator(link[v]) {
|
||||
v++
|
||||
}
|
||||
vol = link[:v]
|
||||
dest = vol
|
||||
end = len(vol)
|
||||
} else if len(link) > 0 && os.IsPathSeparator(link[0]) {
|
||||
dest = link[:1]
|
||||
end = 1
|
||||
vol = link[:1]
|
||||
volLen = 1
|
||||
} else {
|
||||
var r int
|
||||
for r = len(dest) - 1; r >= volLen; r-- {
|
||||
if os.IsPathSeparator(dest[r]) {
|
||||
break
|
||||
}
|
||||
}
|
||||
if r < volLen {
|
||||
dest = vol
|
||||
} else {
|
||||
dest = dest[:r]
|
||||
}
|
||||
end = 0
|
||||
}
|
||||
}
|
||||
return filepath.Clean(dest), nil
|
||||
}
|
||||
|
||||
func volumeNameLen(s string) int {
|
||||
return len(filepath.VolumeName(s))
|
||||
}
|
||||
|
426
bake/entitlements_test.go
Normal file
426
bake/entitlements_test.go
Normal file
@ -0,0 +1,426 @@
|
||||
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"
|
||||
"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, err := osutil.GetLongPathName(t.TempDir())
|
||||
require.NoError(t, err)
|
||||
dir2, err := osutil.GetLongPathName(t.TempDir())
|
||||
require.NoError(t, err)
|
||||
|
||||
escapeLink := filepath.Join(dir1, "escape_link")
|
||||
require.NoError(t, os.Symlink("../../aa", escapeLink))
|
||||
|
||||
wd := osutil.GetWd()
|
||||
|
||||
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{wd},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "NetworkHostSet",
|
||||
conf: EntitlementConf{
|
||||
NetworkHost: true,
|
||||
},
|
||||
opt: build.Options{
|
||||
Allow: []entitlements.Entitlement{
|
||||
entitlements.EntitlementNetworkHost,
|
||||
},
|
||||
},
|
||||
expected: EntitlementConf{
|
||||
FSRead: []string{wd},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "SecurityAndNetworkHostMissing",
|
||||
opt: build.Options{
|
||||
Allow: []entitlements.Entitlement{
|
||||
entitlements.EntitlementNetworkHost,
|
||||
entitlements.EntitlementSecurityInsecure,
|
||||
},
|
||||
},
|
||||
expected: EntitlementConf{
|
||||
NetworkHost: true,
|
||||
SecurityInsecure: true,
|
||||
FSRead: []string{wd},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "SecurityMissingAndNetworkHostSet",
|
||||
conf: EntitlementConf{
|
||||
NetworkHost: true,
|
||||
},
|
||||
opt: build.Options{
|
||||
Allow: []entitlements.Entitlement{
|
||||
entitlements.EntitlementNetworkHost,
|
||||
entitlements.EntitlementSecurityInsecure,
|
||||
},
|
||||
},
|
||||
expected: EntitlementConf{
|
||||
SecurityInsecure: true,
|
||||
FSRead: []string{wd},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "SSHMissing",
|
||||
opt: build.Options{
|
||||
SSHSpecs: []*pb.SSH{
|
||||
{
|
||||
ID: "test",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: EntitlementConf{
|
||||
SSH: true,
|
||||
FSRead: []string{wd},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ExportLocal",
|
||||
opt: build.Options{
|
||||
Exports: []client.ExportEntry{
|
||||
{
|
||||
Type: "local",
|
||||
Attrs: map[string]string{
|
||||
"dest": dir1,
|
||||
},
|
||||
},
|
||||
{
|
||||
Type: "local",
|
||||
Attrs: map[string]string{
|
||||
"dest": filepath.Join(dir1, "subdir"),
|
||||
},
|
||||
},
|
||||
{
|
||||
Type: "local",
|
||||
Attrs: map[string]string{
|
||||
"dest": dir2,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: EntitlementConf{
|
||||
FSWrite: func() []string {
|
||||
exp := []string{dir1, dir2}
|
||||
slices.Sort(exp)
|
||||
return exp
|
||||
}(),
|
||||
FSRead: []string{wd},
|
||||
},
|
||||
},
|
||||
{
|
||||
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(dir1, "../..")},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "SecretFromEscapeLinkAllowRoot",
|
||||
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")
|
||||
})
|
||||
}
|
||||
}
|
26
bake/entitlements_unix.go
Normal file
26
bake/entitlements_unix.go
Normal file
@ -0,0 +1,26 @@
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package bake
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func evaluatePaths(in []string) ([]string, error) {
|
||||
out := make([]string, 0, len(in))
|
||||
for _, p := range in {
|
||||
v, err := filepath.Abs(p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
v, err = filepath.EvalSymlinks(v)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to evaluate path %q", p)
|
||||
}
|
||||
out = append(out, v)
|
||||
}
|
||||
return out, nil
|
||||
}
|
39
bake/entitlements_windows.go
Normal file
39
bake/entitlements_windows.go
Normal file
@ -0,0 +1,39 @@
|
||||
package bake
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
func evaluatePaths(in []string) ([]string, error) {
|
||||
out := make([]string, 0, len(in))
|
||||
for _, p := range in {
|
||||
if p == "/" {
|
||||
out = append(out, getAllVolumes()...)
|
||||
continue
|
||||
}
|
||||
v, err := filepath.Abs(p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
v, err = filepath.EvalSymlinks(v)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to evaluate path %q", p)
|
||||
}
|
||||
out = append(out, v)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func getAllVolumes() []string {
|
||||
var volumes []string
|
||||
for _, drive := range "ABCDEFGHIJKLMNOPQRSTUVWXYZ" {
|
||||
p := string(drive) + ":" + string(filepath.Separator)
|
||||
if _, err := os.Stat(p); !os.IsNotExist(err) {
|
||||
volumes = append(volumes, p)
|
||||
}
|
||||
}
|
||||
return volumes
|
||||
}
|
@ -18,6 +18,7 @@ import (
|
||||
"github.com/containerd/containerd/images"
|
||||
"github.com/distribution/reference"
|
||||
"github.com/docker/buildx/builder"
|
||||
controllerapi "github.com/docker/buildx/controller/pb"
|
||||
"github.com/docker/buildx/driver"
|
||||
"github.com/docker/buildx/util/confutil"
|
||||
"github.com/docker/buildx/util/desktop"
|
||||
@ -76,6 +77,8 @@ type Options struct {
|
||||
NoCacheFilter []string
|
||||
Platforms []specs.Platform
|
||||
Pull bool
|
||||
SecretSpecs []*controllerapi.Secret
|
||||
SSHSpecs []*controllerapi.SSH
|
||||
ShmSize opts.MemBytes
|
||||
Tags []string
|
||||
Target string
|
||||
|
@ -107,6 +107,13 @@ func runBake(ctx context.Context, dockerCli command.Cli, targets []string, in ba
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "failed to get current working directory")
|
||||
}
|
||||
// filesystem access under the current working directory is allowed by default
|
||||
ent.FSRead = append(ent.FSRead, wd)
|
||||
ent.FSWrite = append(ent.FSWrite, wd)
|
||||
|
||||
ctx2, cancel := context.WithCancelCause(context.TODO())
|
||||
defer cancel(errors.WithStack(context.Canceled))
|
||||
@ -250,7 +257,7 @@ func runBake(ctx context.Context, dockerCli command.Cli, targets []string, in ba
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := exp.Prompt(ctx, &syncWriter{w: dockerCli.Err(), wait: printer.Wait}); err != nil {
|
||||
if err := exp.Prompt(ctx, url != "", &syncWriter{w: dockerCli.Err(), wait: printer.Wait}); err != nil {
|
||||
return err
|
||||
}
|
||||
if printer.IsDone() {
|
||||
|
@ -508,7 +508,8 @@ EOT
|
||||
withArgs(addr, "--set", "*.output=type=local,dest="+dirDest),
|
||||
)
|
||||
require.Error(t, err, out)
|
||||
require.Contains(t, out, "outside of the working directory, please set BAKE_ALLOW_REMOTE_FS_ACCESS")
|
||||
require.Contains(t, out, "Your build is requesting privileges for following possibly insecure capabilities")
|
||||
require.Contains(t, out, "Read access to path ../")
|
||||
|
||||
out, err = bakeCmd(
|
||||
sb,
|
||||
@ -555,7 +556,8 @@ EOT
|
||||
withArgs(addr, "--set", "*.output=type=local,dest="+dirDest),
|
||||
)
|
||||
require.Error(t, err, out)
|
||||
require.Contains(t, out, "outside of the working directory, please set BAKE_ALLOW_REMOTE_FS_ACCESS")
|
||||
require.Contains(t, out, "Your build is requesting privileges for following possibly insecure capabilities")
|
||||
require.Contains(t, out, "Read access to path ..")
|
||||
|
||||
out, err = bakeCmd(
|
||||
sb,
|
||||
|
Loading…
x
Reference in New Issue
Block a user