diff --git a/bake/bake.go b/bake/bake.go index b626186712ba..9ae0f011adc7 100644 --- a/bake/bake.go +++ b/bake/bake.go @@ -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" @@ -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 { @@ -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) { diff --git a/bake/entitlements.go b/bake/entitlements.go index b42ba1ab7bb9..43ad6c82f84b 100644 --- a/bake/entitlements.go +++ b/bake/entitlements.go @@ -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{}{} } @@ -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{}{} } } @@ -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 @@ -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 @@ -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] ") diff --git a/bake/entitlements_test.go b/bake/entitlements_test.go index d1f67fe43362..c96795a98939 100644 --- a/bake/entitlements_test.go +++ b/bake/entitlements_test.go @@ -7,6 +7,7 @@ import ( "os" "path/filepath" "slices" + "strings" "testing" "github.com/docker/buildx/build" @@ -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 @@ -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{ @@ -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 diff --git a/commands/bake.go b/commands/bake.go index 906825327840..5f2381deb3f3 100644 --- a/commands/bake.go +++ b/commands/bake.go @@ -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 { diff --git a/tests/bake.go b/tests/bake.go index f44da2b88d05..2e664eab89e6 100644 --- a/tests/bake.go +++ b/tests/bake.go @@ -78,6 +78,7 @@ var bakeTests = []func(t *testing.T, sb integration.Sandbox){ testBakeDefinitionNotExistingSubdirNoParallel, testBakeDefinitionNotExistingOutsideNoParallel, testBakeDefinitionExistingOutsideNoParallel, + testBakeRawJSONEntitlementsNoParallel, testBakeDefinitionSymlinkOutsideNoParallel, testBakeDefinitionSymlinkOutsideGrantedNoParallel, testBakeSSHPathNoParallel, @@ -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) { diff --git a/util/buildflags/secrets.go b/util/buildflags/secrets.go index 994fc416a129..4ce97e6d86dc 100644 --- a/util/buildflags/secrets.go +++ b/util/buildflags/secrets.go @@ -2,6 +2,7 @@ package buildflags import ( "encoding/json" + "os" "strings" "github.com/pkg/errors" @@ -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 }