Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions bake/bake.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"github.com/docker/buildx/bake/hclparser"
"github.com/docker/buildx/build"
"github.com/docker/buildx/util/buildflags"
"github.com/docker/buildx/util/ocilayout"
"github.com/docker/buildx/util/osutil"
"github.com/docker/buildx/util/platformutil"
"github.com/docker/buildx/util/progress"
Expand Down Expand Up @@ -1671,7 +1672,7 @@ func remoteURLWithSubdir(remoteURL, subdir string) string {
return base + "#" + ref + ":" + subdir
}

func collectLocalPaths(t build.Inputs) []string {
func collectLocalPaths(t build.Inputs) ([]string, error) {
var out []string
if t.ContextState == nil {
if v, ok := isLocalPath(t.ContextPath); ok {
Expand All @@ -1687,11 +1688,18 @@ func collectLocalPaths(t build.Inputs) []string {
if v.State != nil {
continue
}
if ref, ok, err := ocilayout.Parse(v.Path); ok {
if err != nil {
return nil, errors.Wrapf(err, "invalid OCI layout context %q", v.Path)
}
out = append(out, ref.Path)
continue
}
if v, ok := isLocalPath(v.Path); ok {
out = append(out, v)
}
}
return out
return out, nil
}

func isLocalPath(p string) (string, bool) {
Expand Down
32 changes: 26 additions & 6 deletions bake/entitlements.go
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,11 @@ func (c EntitlementConf) check(bo build.Options, expected *EntitlementConf) erro
rwPaths := map[string]struct{}{}
roPaths := map[string]struct{}{}

for _, p := range collectLocalPaths(bo.Inputs) {
localPaths, err := collectLocalPaths(bo.Inputs)
if err != nil {
return err
}
for _, p := range localPaths {
roPaths[p] = struct{}{}
}

Expand Down Expand Up @@ -200,8 +204,8 @@ func (c EntitlementConf) check(bo build.Options, expected *EntitlementConf) erro
}

for _, secret := range bo.SecretSpecs {
if secret.FilePath != "" {
roPaths[secret.FilePath] = struct{}{}
if filePath := secret.ResolveSource().FilePath; filePath != "" {
roPaths[filePath] = struct{}{}
}
}

Expand All @@ -216,7 +220,6 @@ func (c EntitlementConf) check(bo build.Options, expected *EntitlementConf) erro
}
}

var err error
expected.FSRead, err = findMissingPaths(c.FSRead, roPaths)
if err != nil {
return err
Expand All @@ -231,9 +234,19 @@ func (c EntitlementConf) check(bo build.Options, expected *EntitlementConf) erro
}

func (c EntitlementConf) Prompt(ctx context.Context, isRemote bool, out io.Writer) error {
return c.prompt(ctx, isRemote, out, true)
}

func (c EntitlementConf) Check(isRemote bool) error {
return c.prompt(context.Background(), isRemote, io.Discard, false)
}

func (c EntitlementConf) prompt(ctx context.Context, isRemote bool, out io.Writer, interactive bool) error {
var term bool
if _, err := console.ConsoleFromFile(os.Stdin); err == nil {
term = true
if interactive {
if _, err := console.ConsoleFromFile(os.Stdin); err == nil {
term = true
}
}

var msgs []string
Expand Down Expand Up @@ -365,6 +378,13 @@ func (c EntitlementConf) Prompt(ctx context.Context, isRemote bool, out io.Write
if fsEntitlementsEnabled && !fsEntitlementsSet && len(msgsFS) != 0 {
fmt.Fprintf(out, "To disable filesystem entitlements checks, you can set BUILDX_BAKE_ENTITLEMENTS_FS=0 .\n\n")
}
if !interactive {
requiredFlags := flags
if fsEntitlementsEnabled {
requiredFlags = slices.Concat(requiredFlags, flagsFS)
}
return errors.Errorf("additional privileges requested: pass %q to grant requested privileges", strings.Join(requiredFlags, " "))
}

if term {
fmt.Fprintf(out, "Do you want to grant requested privileges and continue? [y/N] ")
Expand Down
127 changes: 127 additions & 0 deletions bake/entitlements_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"os"
"path/filepath"
"slices"
"strings"
"testing"

"github.com/docker/buildx/build"
Expand Down Expand Up @@ -107,6 +108,13 @@ func TestValidateEntitlements(t *testing.T) {
require.NoError(t, err)
expWd, err := filepath.EvalSymlinks(wd)
require.NoError(t, err)
credentialPath := filepath.Join(dir1, "credential")
require.NoError(t, os.WriteFile(credentialPath, []byte("test credential"), 0600))
expCredentialPath, err := filepath.EvalSymlinks(credentialPath)
require.NoError(t, err)
layoutLink := filepath.Join(dir1, "layout-link")
require.NoError(t, os.Symlink(dir2, layoutLink))
t.Setenv("BUILDX_TEST_SECRET_SOURCE", "test credential")

tcases := []struct {
name string
Expand Down Expand Up @@ -223,6 +231,70 @@ func TestValidateEntitlements(t *testing.T) {
FSRead: []string{wd, dir1},
},
},
{
name: "secret-id-file-fallback-requires-read",
opt: build.Options{
SecretSpecs: []*buildflags.Secret{{ID: credentialPath}},
},
conf: EntitlementConf{FSRead: []string{wd}},
expected: EntitlementConf{
FSRead: []string{expCredentialPath},
},
},
{
name: "secret-id-file-fallback-allowed",
opt: build.Options{
SecretSpecs: []*buildflags.Secret{{ID: credentialPath}},
},
conf: EntitlementConf{FSRead: []string{wd, dir1}},
},
{
name: "secret-id-env-fallback-needs-no-read",
opt: build.Options{
SecretSpecs: []*buildflags.Secret{{ID: "BUILDX_TEST_SECRET_SOURCE"}},
},
conf: EntitlementConf{FSRead: []string{wd}},
},
{
name: "explicit-secret-env-needs-no-read",
opt: build.Options{
SecretSpecs: []*buildflags.Secret{{ID: credentialPath, Env: "BUILDX_TEST_SECRET_SOURCE"}},
},
conf: EntitlementConf{FSRead: []string{wd}},
},
{
name: "oci-layout-named-context-requires-read",
opt: build.Options{
Inputs: build.Inputs{NamedContexts: map[string]build.NamedContext{
"layout": {Path: "oci-layout://" + dir1 + ":latest"},
}},
},
conf: EntitlementConf{FSRead: []string{wd}},
expected: EntitlementConf{
FSRead: []string{expDir1},
},
},
{
name: "oci-layout-named-context-allowed",
opt: build.Options{
Inputs: build.Inputs{NamedContexts: map[string]build.NamedContext{
"layout": {Path: "oci-layout://" + dir1 + ":latest"},
}},
},
conf: EntitlementConf{FSRead: []string{wd, dir1}},
},
{
name: "oci-layout-named-context-symlink-requires-destination",
opt: build.Options{
Inputs: build.Inputs{NamedContexts: map[string]build.NamedContext{
"layout": {Path: "oci-layout://" + layoutLink + ":latest"},
}},
},
conf: EntitlementConf{FSRead: []string{wd, dir1}},
expected: EntitlementConf{
FSRead: []string{expDir2},
},
},
{
name: "SecretFromEscapeLink",
opt: build.Options{
Expand Down Expand Up @@ -441,6 +513,61 @@ func TestPromptLocalOutputDeleteCannotBeDisabledWithFSEntitlements(t *testing.T)
require.Contains(t, out.String(), "--allow=buildx.local.delete")
}

func TestCheckEntitlements(t *testing.T) {
t.Run("all entitlement types", func(t *testing.T) {
t.Setenv("BUILDX_BAKE_ENTITLEMENTS_FS", "1")

err := EntitlementConf{
NetworkHost: true,
SecurityInsecure: true,
Devices: &EntitlementsDevicesConf{
Devices: map[string]struct{}{"vendor.com/device=foo": {}},
},
FSRead: []string{t.TempDir()},
FSWrite: []string{t.TempDir()},
SSH: true,
LocalOutputDelete: true,
}.Check(false)
require.Error(t, err)
require.True(t, strings.HasPrefix(err.Error(), `additional privileges requested: pass "`), err.Error())
for _, flag := range []string{
"--allow=network.host",
"--allow=security.insecure",
"--allow=device=vendor.com/device=foo",
"--allow=fs.read=",
"--allow=fs.write=",
"--allow=ssh",
"--allow=buildx.local.delete",
} {
require.ErrorContains(t, err, flag)
}
})

t.Run("filesystem checks disabled", func(t *testing.T) {
t.Setenv("BUILDX_BAKE_ENTITLEMENTS_FS", "0")

err := EntitlementConf{
FSRead: []string{t.TempDir()},
FSWrite: []string{t.TempDir()},
SSH: true,
}.Check(false)
require.NoError(t, err)
})

t.Run("filesystem setting does not disable other checks", func(t *testing.T) {
t.Setenv("BUILDX_BAKE_ENTITLEMENTS_FS", "0")

err := EntitlementConf{
NetworkHost: true,
FSRead: []string{t.TempDir()},
SSH: true,
}.Check(false)
require.ErrorContains(t, err, "--allow=network.host")
require.NotContains(t, err.Error(), "--allow=fs.read=")
require.NotContains(t, err.Error(), "--allow=ssh")
})
}

func TestGroupSamePaths(t *testing.T) {
tests := []struct {
name string
Expand Down
4 changes: 2 additions & 2 deletions commands/bake.go
Original file line number Diff line number Diff line change
Expand Up @@ -351,8 +351,8 @@ func runBake(ctx context.Context, dockerCli command.Cli, targets []string, in ba
return err
}
if progressMode == progressui.RawJSONMode {
if exp.LocalOutputDelete {
return errors.Errorf("additional privileges requested: pass %q to grant requested privileges", "--allow="+string(bake.EntitlementKeyBuildxLocalDelete))
if err := exp.Check(url != ""); err != nil {
return err
}
} else {
if err := exp.Prompt(ctx, url != "", &syncWriter{w: dockerCli.Err(), wait: printer.Wait}); err != nil {
Expand Down
43 changes: 43 additions & 0 deletions tests/bake.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ var bakeTests = []func(t *testing.T, sb integration.Sandbox){
testBakeDefinitionNotExistingSubdirNoParallel,
testBakeDefinitionNotExistingOutsideNoParallel,
testBakeDefinitionExistingOutsideNoParallel,
testBakeRawJSONEntitlementsNoParallel,
testBakeDefinitionSymlinkOutsideNoParallel,
testBakeDefinitionSymlinkOutsideGrantedNoParallel,
testBakeSSHPathNoParallel,
Expand Down Expand Up @@ -2151,6 +2152,48 @@ target "default" {
}
}

func testBakeRawJSONEntitlementsNoParallel(t *testing.T, sb integration.Sandbox) {
t.Setenv("BUILDX_BAKE_ENTITLEMENTS_FS", "1")
dockerfile := []byte(`
FROM scratch
COPY foo /foo
`)
dirSrc := tmpdir(
t,
fstest.CreateFile("Dockerfile", dockerfile, 0600),
fstest.CreateFile("foo", []byte("foo"), 0600),
)
dirDest := t.TempDir()
bakefile := fmt.Appendf(nil, `
target "default" {
context = %q
output = ["type=local,dest=%s"]
}
`, dirSrc, dirDest)
dirSpec := tmpdir(
t,
fstest.CreateFile("docker-bake.hcl", bakefile, 0600),
)

cmd := buildxCmd(sb, withDir(dirSpec), withArgs("bake", "--progress=rawjson"))
out, err := cmd.CombinedOutput()
require.Error(t, err, string(out))
require.Contains(t, string(out), "additional privileges requested")
require.Contains(t, string(out), "--allow=fs.read=")
require.Contains(t, string(out), "--allow=fs.write=")
require.NoFileExists(t, filepath.Join(dirDest, "foo"))

cmd = buildxCmd(sb, withDir(dirSpec), withArgs(
"bake",
"--progress=rawjson",
"--allow=fs.read="+dirSrc,
"--allow=fs.write="+dirDest,
))
out, err = cmd.CombinedOutput()
require.NoError(t, err, string(out))
require.FileExists(t, filepath.Join(dirDest, "foo"))
}

func testBakeDefinitionSymlinkOutsideNoParallel(t *testing.T, sb integration.Sandbox) {
for _, ent := range []bool{true, false} {
t.Run(fmt.Sprintf("ent=%v", ent), func(t *testing.T) {
Expand Down
15 changes: 15 additions & 0 deletions util/buildflags/secrets.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package buildflags

import (
"encoding/json"
"os"
"strings"

"github.com/pkg/errors"
Expand Down Expand Up @@ -35,6 +36,20 @@ type Secret struct {
Env string `json:"env,omitempty"`
}

// ResolveSource returns a copy with the source selected using BuildKit's
// default secret source rules.
func (s *Secret) ResolveSource() *Secret {
resolved := *s
if resolved.Env == "" && resolved.FilePath == "" {
if _, ok := os.LookupEnv(resolved.ID); ok {
resolved.Env = resolved.ID
} else {
resolved.FilePath = resolved.ID
}
}
return &resolved
}

func (s *Secret) Equal(other *Secret) bool {
return s.ID == other.ID && s.FilePath == other.FilePath && s.Env == other.Env
}
Expand Down
Loading