Files
buildx/tests/policy_build.go
Tonis Tiigi 25db0fb050 tests: verify exec traffic is policy-checked in caps proxy test
Extend testBuildPolicyCapsProxy so that instead of only checking that
the network proxy was enabled, the build runs a command that makes an
HTTP request to a local test server. The policy denies that URL as an
HTTP source, so the test now verifies that exec traffic actually flows
through the proxy and is subject to source policy, including the deny
message and DENY decision in the build output.

Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
2026-06-10 08:58:48 -07:00

1643 lines
39 KiB
Go

package tests
import (
"archive/tar"
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"net/url"
"os"
"path/filepath"
"strings"
"testing"
"github.com/containerd/continuity/fs/fstest"
"github.com/containerd/platforms"
"github.com/distribution/reference"
"github.com/docker/buildx/util/gitutil"
"github.com/docker/buildx/util/gitutil/gittestutil"
"github.com/moby/buildkit/client"
"github.com/moby/buildkit/identity"
"github.com/moby/buildkit/util/contentutil"
bkgitutil "github.com/moby/buildkit/util/gitutil"
"github.com/moby/buildkit/util/testutil"
"github.com/moby/buildkit/util/testutil/httpserver"
"github.com/moby/buildkit/util/testutil/integration"
digest "github.com/opencontainers/go-digest"
"github.com/stretchr/testify/require"
)
var policyBuildTests = []func(t *testing.T, sb integration.Sandbox){
testBuildPolicyAllow,
testBuildPolicyDeny,
testBuildPolicyDenyProgressStream,
testBuildPolicyImageName,
testBuildPolicyEnv,
testBuildPolicyHTTP,
testBuildPolicyGit,
testBuildPolicyRemotePolicyFiles,
testBuildPolicyRemoteHTTPPolicyFiles,
testBuildPolicyConfigFlags,
testBuildPolicyCapsProxy,
testBuildPolicyCapsProxyUnsupported,
}
func policyCapsProxyFile(serverURL string) []byte {
return fmt.Appendf(nil, `
package docker
default allow = true
default deny_msg := []
default caps := {}
caps := {"exec.proxy": true} if input.env.capsRequest
allow := false if input.http.url == "%s/file"
deny_msg := ["exec proxy http source denied by policy"] if input.http.url == "%s/file"
decision := {"allow": allow, "deny_msg": deny_msg, "caps": caps}
`, serverURL, serverURL)
}
func testBuildPolicyCapsProxy(t *testing.T, sb integration.Sandbox) {
if buildkitTag() != "master" {
skipNoCompatBuildKit(t, sb, ">= 0.31.0-0", "network proxy requires BuildKit v0.31.0+")
}
resp := &httpserver.Response{Content: []byte("policy-caps-proxy")}
server := httpserver.NewTestServer(map[string]*httpserver.Response{
"/file": resp,
})
defer server.Close()
dockerfile := []byte(`
FROM busybox:latest
RUN wget -O- ` + server.URL + `/file
`)
dir := tmpdir(
t,
fstest.CreateFile("Dockerfile", dockerfile, 0600),
fstest.CreateFile("Dockerfile.rego", policyCapsProxyFile(server.URL), 0600),
)
cmd := buildxCmd(sb, withDir(dir), withArgs(
"build",
"--progress=plain",
"--output=type=cacheonly",
dir,
))
out, err := cmd.CombinedOutput()
require.Error(t, err, string(out))
require.Contains(t, string(out), "policy enabled network proxy")
require.Contains(t, string(out), "exec proxy http source denied by policy")
require.Contains(t, string(out), "policy decision for source "+server.URL+"/file")
require.Contains(t, string(out), "DENY")
}
func testBuildPolicyCapsProxyUnsupported(t *testing.T, sb integration.Sandbox) {
skipNoCompatBuildKit(t, sb, ">= 0.26.0-0", "policy input requires BuildKit v0.26.0+")
if buildkitTag() == "master" || matchesBuildKitVersion(t, sb, ">= 0.31.0-0") {
t.Skip("BuildKit daemon supports network proxy")
}
dockerfile := []byte(`
FROM scratch
COPY foo /foo
`)
dir := tmpdir(
t,
fstest.CreateFile("Dockerfile", dockerfile, 0600),
fstest.CreateFile("Dockerfile.rego", policyCapsProxyFile(""), 0600),
fstest.CreateFile("foo", []byte("foo"), 0600),
)
cmd := buildxCmd(sb, withDir(dir), withArgs(
"build",
"--progress=plain",
"--output=type=cacheonly",
dir,
))
out, err := cmd.CombinedOutput()
require.Error(t, err, string(out))
require.Contains(t, string(out), "network proxy requested by policy is not supported by the current BuildKit daemon")
}
func testBuildPolicyAllow(t *testing.T, sb integration.Sandbox) {
skipNoCompatBuildKit(t, sb, ">= 0.26.0-0", "policy input requires BuildKit v0.26.0+")
dockerfile := []byte(`
FROM busybox:latest
RUN echo policy-ok
`)
policyFile := []byte(`
package docker
default allow = false
allow if true
decision := {"allow": allow}
`)
dir := tmpdir(
t,
fstest.CreateFile("Dockerfile", dockerfile, 0600),
fstest.CreateFile("policy.rego", policyFile, 0600),
)
policyPath := filepath.Join(dir, "policy.rego")
cmd := buildxCmd(sb, withDir(dir), withArgs(
"build",
"--progress=plain",
"--policy", "filename="+policyPath,
"--output=type=cacheonly",
dir,
))
out, err := cmd.CombinedOutput()
require.NoError(t, err, string(out))
require.Contains(t, string(out), "loading policies "+policyPath)
}
func testBuildPolicyDeny(t *testing.T, sb integration.Sandbox) {
skipNoCompatBuildKit(t, sb, ">= 0.26.0-0", "policy input requires BuildKit v0.26.0+")
dockerfile := []byte(`
FROM busybox:latest
RUN echo policy-nope
`)
policyFile := []byte(`
package docker
default allow = false
deny_msg := ["denied by test"]
decision := {"allow": allow, "deny_msg": deny_msg}
`)
dir := tmpdir(
t,
fstest.CreateFile("Dockerfile", dockerfile, 0600),
fstest.CreateFile("policy.rego", policyFile, 0600),
)
policyPath := filepath.Join(dir, "policy.rego")
cmd := buildxCmd(sb, withDir(dir), withArgs(
"build",
"--progress=plain",
"--policy", "filename="+policyPath,
"--output=type=cacheonly",
dir,
))
out, err := cmd.CombinedOutput()
require.Error(t, err, string(out))
require.Contains(t, string(out), "not allowed by policy")
require.Contains(t, string(out), "loading policies "+policyPath)
require.Contains(t, string(out), "policy decision for source")
require.Contains(t, string(out), "DENY")
}
func testBuildPolicyDenyProgressStream(t *testing.T, sb integration.Sandbox) {
skipNoCompatBuildKit(t, sb, ">= 0.26.0-0", "policy input requires BuildKit v0.26.0+")
dockerfile := []byte(`
FROM busybox:latest
RUN echo policy-nope
`)
policyFile := []byte(`
package docker
default allow = false
allow if not input.image
decision := {"allow": allow}
`)
dir := tmpdir(
t,
fstest.CreateFile("Dockerfile", dockerfile, 0600),
fstest.CreateFile("policy.rego", policyFile, 0600),
)
policyPath := filepath.Join(dir, "policy.rego")
cmd := buildxCmd(sb, withDir(dir), withArgs(
"build",
"--progress=rawjson",
"--policy", "filename="+policyPath,
"--output=type=cacheonly",
dir,
))
out, err := cmd.CombinedOutput()
outStr := string(out)
require.Error(t, err, outStr)
policyVertexName := "loading policies " + policyPath
sawPolicyVertex := false
sawPolicyCompleted := false
policyVertexDigest := ""
policyVertexError := ""
for line := range strings.SplitSeq(outStr, "\n") {
line = strings.TrimSpace(line)
if !strings.HasPrefix(line, "{") {
continue
}
var st client.SolveStatus
require.NoError(t, json.Unmarshal([]byte(line), &st), line)
for _, v := range st.Vertexes {
if v == nil {
continue
}
if v.Name == policyVertexName {
sawPolicyVertex = true
if v.Digest != "" {
policyVertexDigest = string(v.Digest)
}
}
if policyVertexDigest == "" || string(v.Digest) != policyVertexDigest {
continue
}
if v.Completed != nil {
sawPolicyCompleted = true
}
if v.Error != "" {
policyVertexError = v.Error
}
}
}
require.True(t, sawPolicyVertex, outStr)
require.True(t, sawPolicyCompleted, outStr)
require.Contains(t, policyVertexError, "not allowed by policy", outStr)
require.Contains(t, outStr, "not allowed by policy")
}
func testBuildPolicyImageName(t *testing.T, sb integration.Sandbox) {
skipNoCompatBuildKit(t, sb, ">= 0.26.0-0", "policy input requires BuildKit v0.26.0+")
registry, err := sb.NewRegistry()
if errors.Is(err, integration.ErrRequirements) {
t.Skip(err.Error())
}
require.NoError(t, err)
baseRef := registry + "/buildx/policy-base:" + identity.NewID()
baseDir := tmpdir(
t,
fstest.CreateFile("Dockerfile", []byte("FROM busybox:latest\nLABEL com.example.policy=label\nENV POLICY_ENV=envval\n"), 0600),
)
baseCmd := buildxCmd(sb, withDir(baseDir), withArgs(
"build",
"--progress=plain",
"--output=type=image,name="+baseRef+",push=true",
baseDir,
))
baseOut, err := baseCmd.CombinedOutput()
require.NoError(t, err, string(baseOut))
baseDesc, provider, err := contentutil.ProviderFromRef(baseRef)
require.NoError(t, err)
_, err = testutil.ReadImages(sb.Context(), provider, baseDesc)
require.NoError(t, err)
baseCanonicalRef := baseRef + "@" + baseDesc.Digest.String()
parsedBase, err := reference.ParseNormalizedNamed(baseRef)
require.NoError(t, err)
baseFullRepo := parsedBase.Name()
platformStr := platforms.Format(platforms.Normalize(platforms.DefaultSpec()))
testCases := []struct {
name string
policy string
dockerfileName string
dockerfileBody string
buildArgs []string
wantErrContains string
}{
{
name: "repo-allow-busybox",
policy: `
package docker
default allow = false
allow if not input.image
allow if input.image.repo == "busybox"
decision := {"allow": allow}
`,
dockerfileName: "Dockerfile",
dockerfileBody: "FROM busybox:latest\nRUN echo repo-ok\n",
},
{
name: "repo-deny-alpine",
policy: `
package docker
default allow = false
allow if not input.image
allow if input.image.repo == "busybox"
decision := {"allow": allow}
`,
dockerfileName: "Dockerfile",
dockerfileBody: "FROM alpine:latest\nRUN echo repo-deny\n",
wantErrContains: "not allowed by policy",
},
{
name: "tag-allow-alpine-latest",
policy: `
package docker
default allow = false
allow if not input.image
allow if input.image.tag == "latest"
decision := {"allow": allow}
`,
dockerfileName: "Dockerfile",
dockerfileBody: "FROM alpine:latest\nRUN echo tag-ok\n",
},
{
name: "tag-deny-busybox-latest",
policy: `
package docker
default allow = false
allow if not input.image
allow if input.image.tag == "stable"
decision := {"allow": allow}
`,
dockerfileName: "Dockerfile",
dockerfileBody: "FROM busybox:latest\nRUN echo tag-deny\n",
wantErrContains: "not allowed by policy",
},
{
name: "host-allow-docker-io",
policy: `
package docker
default allow = false
allow if not input.image
allow if input.image.host == "docker.io"
decision := {"allow": allow}
`,
dockerfileName: "Dockerfile",
dockerfileBody: "FROM busybox:latest\nRUN echo host-ok\n",
},
{
name: "host-deny-local-registry",
policy: `
package docker
default allow = false
allow if not input.image
allow if input.image.host == "docker.io"
decision := {"allow": allow}
`,
dockerfileName: "Dockerfile",
dockerfileBody: fmt.Sprintf("FROM %s\nRUN echo host-deny\n", baseRef),
wantErrContains: "not allowed by policy",
},
{
name: "full-repo-allow-library-busybox",
policy: fmt.Sprintf(`
package docker
default allow = false
allow if not input.image
allow if input.image.fullRepo == "%s"
decision := {"allow": allow}
`, baseFullRepo),
dockerfileName: "Dockerfile",
dockerfileBody: fmt.Sprintf("FROM %s\nRUN echo full-repo-ok\n", baseRef),
},
{
name: "full-repo-deny-alpine",
policy: fmt.Sprintf(`
package docker
default allow = false
allow if not input.image
allow if input.image.fullRepo == "%s"
decision := {"allow": allow}
`, baseFullRepo),
dockerfileName: "Dockerfile",
dockerfileBody: "FROM alpine:latest\nRUN echo full-repo-deny\n",
wantErrContains: "not allowed by policy",
},
{
name: "platform-allow-default",
policy: fmt.Sprintf(`
package docker
default allow = false
allow if not input.image
allow if input.image.platform == "%s"
decision := {"allow": allow}
`, platformStr),
dockerfileName: "Dockerfile",
dockerfileBody: "FROM busybox:latest\nRUN echo platform-ok\n",
},
{
name: "canonical-allow",
policy: `
package docker
default allow = false
allow if not input.image
allow if input.image.isCanonical
decision := {"allow": allow}
`,
dockerfileName: "Dockerfile",
dockerfileBody: fmt.Sprintf("FROM %s\nRUN echo canonical-ok\n", baseCanonicalRef),
},
{
name: "canonical-deny",
policy: `
package docker
default allow = false
allow if not input.image
allow if input.image.isCanonical
decision := {"allow": allow}
`,
dockerfileName: "Dockerfile",
dockerfileBody: fmt.Sprintf("FROM %s\nRUN echo canonical-deny\n", baseRef),
wantErrContains: "not allowed by policy",
},
{
name: "checksum-allow",
policy: fmt.Sprintf(`
package docker
default allow = false
allow if not input.image
allow if input.image.checksum == "%s"
decision := {"allow": allow}
`, baseDesc.Digest.String()),
dockerfileName: "Dockerfile",
dockerfileBody: fmt.Sprintf("FROM %s\nRUN echo checksum-ok\n", baseCanonicalRef),
},
{
name: "checksum-deny",
policy: `
package docker
default allow = false
allow if not input.image
allow if input.image.checksum == "sha256:0000000000000000000000000000000000000000000000000000000000000000"
decision := {"allow": allow}
`,
dockerfileName: "Dockerfile",
dockerfileBody: fmt.Sprintf("FROM %s\nRUN echo checksum-deny\n", baseCanonicalRef),
wantErrContains: "not allowed by policy",
},
{
name: "config-label-allow",
policy: `
package docker
default allow = false
allow if not input.image
allow if input.image.labels["com.example.policy"] == "label"
decision := {"allow": allow}
`,
dockerfileName: "Dockerfile",
dockerfileBody: fmt.Sprintf("FROM %s\nRUN echo label-ok\n", baseRef),
},
{
name: "config-env-allow",
policy: `
package docker
default allow = false
allow if not input.image
allow if input.image.env[_] == "POLICY_ENV=envval"
decision := {"allow": allow}
`,
dockerfileName: "Dockerfile",
dockerfileBody: fmt.Sprintf("FROM %s\nRUN echo env-ok\n", baseRef),
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
buildDir := tmpdir(
t,
fstest.CreateFile(tc.dockerfileName, []byte(tc.dockerfileBody), 0600),
fstest.CreateFile("policy.rego", []byte(tc.policy), 0600),
)
policyPath := filepath.Join(buildDir, "policy.rego")
args := []string{
"build",
"--progress=plain",
"--policy", "filename=" + policyPath,
"--output=type=cacheonly",
}
args = append(args, tc.buildArgs...)
args = append(args, buildDir)
cmd := buildxCmd(sb, withDir(buildDir), withArgs(
args...,
))
out, err := cmd.CombinedOutput()
if tc.wantErrContains == "" {
require.NoError(t, err, string(out))
require.Contains(t, string(out), "loading policies "+policyPath)
} else {
require.Error(t, err, string(out))
require.Contains(t, string(out), tc.wantErrContains)
}
})
}
}
func testBuildPolicyEnv(t *testing.T, sb integration.Sandbox) {
skipNoCompatBuildKit(t, sb, ">= 0.26.0-0", "policy input requires BuildKit v0.26.0+")
testCases := []struct {
name string
policy string
dockerfileName string
dockerfileBody string
buildArgs []string
wantErrContains string
}{
{
name: "env-arg-allow",
policy: `
package docker
default allow = false
allow if input.env.args["POLICY_CASE"] == "arg"
decision := {"allow": allow}
`,
dockerfileName: "Dockerfile",
dockerfileBody: "FROM busybox:latest\nRUN echo env-arg-ok\n",
buildArgs: []string{"--build-arg", "POLICY_CASE=arg"},
},
{
name: "env-arg-deny",
policy: `
package docker
default allow = false
allow if input.env.args["POLICY_CASE"] == "arg"
decision := {"allow": allow}
`,
dockerfileName: "Dockerfile",
dockerfileBody: "FROM busybox:latest\nRUN echo env-arg-deny\n",
wantErrContains: "not allowed by policy",
},
{
name: "env-label-allow",
policy: `
package docker
default allow = false
allow if input.env.labels["com.example.policy"] == "label"
decision := {"allow": allow}
`,
dockerfileName: "Dockerfile",
dockerfileBody: "FROM busybox:latest\nRUN echo env-label-ok\n",
buildArgs: []string{"--label", "com.example.policy=label"},
},
{
name: "env-target-deny",
policy: `
package docker
default allow = false
allow if input.env.target == "final"
decision := {"allow": allow}
`,
dockerfileName: "Dockerfile",
dockerfileBody: "FROM busybox:latest AS build\nRUN echo stage-build\n\nFROM busybox:latest AS final\nRUN echo stage-final\n",
buildArgs: []string{"--target", "build"},
wantErrContains: "not allowed by policy",
},
{
name: "env-filename-allow",
policy: `
package docker
default allow = false
allow if input.env.filename == "Dockerfile.custom"
decision := {"allow": allow}
`,
dockerfileName: "Dockerfile.custom",
dockerfileBody: "FROM busybox:latest\nRUN echo filename-ok\n",
buildArgs: []string{"-f", "Dockerfile.custom"},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
buildDir := tmpdir(
t,
fstest.CreateFile(tc.dockerfileName, []byte(tc.dockerfileBody), 0600),
fstest.CreateFile("policy.rego", []byte(tc.policy), 0600),
)
policyPath := filepath.Join(buildDir, "policy.rego")
args := []string{
"build",
"--progress=plain",
"--policy", "filename=" + policyPath,
"--output=type=cacheonly",
}
args = append(args, tc.buildArgs...)
args = append(args, buildDir)
cmd := buildxCmd(sb, withDir(buildDir), withArgs(
args...,
))
out, err := cmd.CombinedOutput()
if tc.wantErrContains == "" {
require.NoError(t, err, string(out))
require.Contains(t, string(out), "loading policies "+policyPath)
} else {
require.Error(t, err, string(out))
require.Contains(t, string(out), tc.wantErrContains)
}
})
}
}
func testBuildPolicyHTTP(t *testing.T, sb integration.Sandbox) {
skipNoCompatBuildKit(t, sb, ">= 0.26.0-0", "policy input requires BuildKit v0.26.0+")
resp := &httpserver.Response{Content: []byte("policy-http")}
server := httpserver.NewTestServer(map[string]*httpserver.Response{
"/file": resp,
})
defer server.Close()
parsedURL, err := url.Parse(server.URL)
require.NoError(t, err)
baseURL := server.URL + "/file"
queryURL := baseURL + "?policy=allow&case=http"
checksum := digest.FromBytes(resp.Content).String()
testCases := []struct {
name string
policy string
addURL string
wantErrContains string
requiresHTTPChecksum bool
}{
{
name: "http-url-allow",
policy: fmt.Sprintf(`
package docker
default allow = false
allow if not input.http
allow if input.http.url == "%s"
decision := {"allow": allow}
`, queryURL),
addURL: queryURL,
},
{
name: "http-schema-allow",
policy: `
package docker
default allow = false
allow if not input.http
allow if input.http.schema == "http"
decision := {"allow": allow}
`,
addURL: baseURL,
},
{
name: "http-host-allow",
policy: fmt.Sprintf(`
package docker
default allow = false
allow if not input.http
allow if input.http.host == "%s"
decision := {"allow": allow}
`, parsedURL.Host),
addURL: baseURL,
},
{
name: "http-path-allow",
policy: `
package docker
default allow = false
allow if not input.http
allow if input.http.path == "/file"
decision := {"allow": allow}
`,
addURL: baseURL,
},
{
name: "http-query-allow",
policy: `
package docker
default allow = false
allow if not input.http
allow if input.http.query["policy"][_] == "allow"
decision := {"allow": allow}
`,
addURL: queryURL,
},
{
name: "http-checksum-allow",
policy: fmt.Sprintf(`
package docker
default allow = false
allow if not input.http
allow if input.http.checksum == "%s"
decision := {"allow": allow}
`, checksum),
addURL: baseURL,
requiresHTTPChecksum: true,
},
{
name: "http-checksum-deny",
policy: `
package docker
default allow = false
allow if not input.http
allow if input.http.checksum == "sha256:0000000000000000000000000000000000000000000000000000000000000000"
decision := {"allow": allow}
`,
addURL: baseURL,
wantErrContains: "not allowed by policy",
requiresHTTPChecksum: true,
},
{
name: "http-host-deny",
policy: `
package docker
default allow = false
allow if not input.http
allow if input.http.host == "example.invalid"
decision := {"allow": allow}
`,
addURL: baseURL,
wantErrContains: "not allowed by policy",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if tc.requiresHTTPChecksum {
if !isRemoteWorker(sb) {
t.Skip("http checksum policy input requires remote driver")
}
skipNoCompatBuildKit(t, sb, ">= 0.26.3-0", "http checksum policy input")
}
dockerfile := fmt.Appendf(nil, "FROM busybox:latest\nADD %s /tmp/file\n", tc.addURL)
dir := tmpdir(
t,
fstest.CreateFile("Dockerfile", dockerfile, 0600),
fstest.CreateFile("policy.rego", []byte(tc.policy), 0600),
)
policyPath := filepath.Join(dir, "policy.rego")
policyArg := "filename=" + policyPath
if tc.name == "git-schema-allow" {
policyArg = "log-level=debug," + policyArg
}
cmd := buildxCmd(sb, withDir(dir), withArgs(
"build",
"--progress=plain",
"--policy", policyArg,
"--output=type=cacheonly",
dir,
))
out, err := cmd.CombinedOutput()
if tc.wantErrContains == "" {
require.NoError(t, err, string(out))
require.Contains(t, string(out), "loading policies "+policyPath)
} else {
require.Error(t, err, string(out))
require.Contains(t, string(out), tc.wantErrContains)
}
})
}
}
func testBuildPolicyGit(t *testing.T, sb integration.Sandbox) {
skipNoCompatBuildKit(t, sb, ">= 0.26.0-0", "policy input requires BuildKit v0.26.0+")
gitDir := t.TempDir()
require.NoError(t, os.WriteFile(filepath.Join(gitDir, "Dockerfile"), []byte("FROM busybox:latest\nRUN echo git\n"), 0600))
require.NoError(t, os.WriteFile(filepath.Join(gitDir, "a"), []byte("a"), 0600))
git, err := gitutil.New(bkgitutil.WithDir(gitDir))
require.NoError(t, err)
gittestutil.GitInit(git, t)
gittestutil.GitAdd(git, t, "Dockerfile", "a")
gittestutil.GitCommit(git, t, "initial commit")
gittestutil.GitTagAnnotated(git, t, "v0.1", "v0.1release")
require.NoError(t, os.WriteFile(filepath.Join(gitDir, "b"), []byte("b"), 0600))
gittestutil.GitAdd(git, t, "b")
gittestutil.GitCommit(git, t, "b")
_, err = git.Run(context.TODO(), "checkout", "-B", "v2")
require.NoError(t, err)
commitHeadB, err := git.Run(context.TODO(), "rev-parse", "HEAD")
require.NoError(t, err)
commitHead := strings.TrimSpace(string(commitHeadB))
commitTagB, err := git.Run(context.TODO(), "rev-parse", "v0.1")
require.NoError(t, err)
commitTag := strings.TrimSpace(string(commitTagB))
commitTagCommitB, err := git.Run(context.TODO(), "rev-parse", "v0.1^{commit}")
require.NoError(t, err)
commitTagCommit := strings.TrimSpace(string(commitTagCommitB))
baseURL := gittestutil.GitServeHTTP(git, t)
tagURL := baseURL + "#v0.1"
branchURL := baseURL + "#v2"
parsedURL, err := url.Parse(baseURL)
require.NoError(t, err)
testCases := []struct {
name string
policy string
context string
wantErrContains string
requiresGitResolve bool
}{
{
name: "git-schema-allow",
policy: `
package docker
default allow = false
allow if not input.git
allow if input.git.schema != ""
decision := {"allow": allow}
`,
context: baseURL,
},
{
name: "git-host-allow",
policy: fmt.Sprintf(`
package docker
default allow = false
allow if not input.git
allow if input.git.host == "%s"
decision := {"allow": allow}
`, parsedURL.Host),
context: baseURL,
},
{
name: "git-remote-allow",
policy: fmt.Sprintf(`
package docker
default allow = false
allow if not input.git
allow if endswith(input.git.remote, "%s")
decision := {"allow": allow}
`, parsedURL.Path),
context: baseURL,
},
{
name: "git-ref-tag-allow",
policy: `
package docker
default allow = false
allow if not input.git
allow if input.git.ref == "refs/tags/v0.1"
decision := {"allow": allow}
`,
context: tagURL,
requiresGitResolve: true,
},
{
name: "git-branch-allow",
policy: `
package docker
default allow = false
allow if not input.git
allow if input.git.branch == "v2"
decision := {"allow": allow}
`,
context: branchURL,
requiresGitResolve: true,
},
{
name: "git-tagname-allow",
policy: `
package docker
default allow = false
allow if not input.git
allow if input.git.tagName == "v0.1"
decision := {"allow": allow}
`,
context: tagURL,
requiresGitResolve: true,
},
{
name: "git-checksum-allow",
policy: fmt.Sprintf(`
package docker
default allow = false
allow if not input.git
allow if input.git.checksum == "%s"
decision := {"allow": allow}
`, commitTag),
context: tagURL,
requiresGitResolve: true,
},
{
name: "git-commit-checksum-allow",
policy: fmt.Sprintf(`
package docker
default allow = false
allow if not input.git
allow if input.git.commitChecksum == "%s"
decision := {"allow": allow}
`, commitTagCommit),
context: tagURL,
requiresGitResolve: true,
},
{
name: "git-annotated-tag-allow",
policy: `
package docker
default allow = false
allow if not input.git
allow if input.git.isAnnotatedTag == true
decision := {"allow": allow}
`,
context: tagURL,
requiresGitResolve: true,
},
{
name: "git-commit-message-allow",
policy: `
package docker
default allow = false
allow if not input.git
allow if input.git.commit.message == "initial commit"
decision := {"allow": allow}
`,
context: tagURL,
requiresGitResolve: true,
},
{
name: "git-tag-object-allow",
policy: `
package docker
default allow = false
allow if not input.git
allow if input.git.tag.tag == "v0.1"
decision := {"allow": allow}
`,
context: tagURL,
requiresGitResolve: true,
},
{
name: "git-checksum-deny",
policy: `
package docker
default allow = false
allow if not input.git
allow if input.git.checksum == "deadbeef"
decision := {"allow": allow}
`,
context: tagURL,
wantErrContains: "not allowed by policy",
requiresGitResolve: true,
},
{
name: "git-commit-ref-allow",
policy: `
package docker
default allow = false
allow if not input.git
allow if input.git.isCommitRef == true
decision := {"allow": allow}
`,
context: baseURL + "#" + commitHead,
requiresGitResolve: true,
},
{
name: "git-host-deny",
policy: `
package docker
default allow = false
allow if not input.git
allow if input.git.host == "example.invalid"
decision := {"allow": allow}
`,
context: tagURL,
wantErrContains: "not allowed by policy",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
if tc.requiresGitResolve {
if !isRemoteWorker(sb) {
t.Skip("git policy metadata requires remote driver")
}
}
dir := tmpdir(
t,
fstest.CreateFile("policy.rego", []byte(tc.policy), 0600),
)
policyPath := "cwd://policy.rego"
cmd := buildxCmd(sb, withDir(dir), withArgs(
"build",
"--progress=plain",
"--policy", "filename="+policyPath,
"--output=type=cacheonly",
tc.context,
))
out, err := cmd.CombinedOutput()
if tc.wantErrContains == "" {
require.NoError(t, err, string(out))
require.Contains(t, string(out), "loading policies "+policyPath)
} else {
require.Error(t, err, string(out))
require.Contains(t, string(out), tc.wantErrContains)
}
})
}
}
func testBuildPolicyConfigFlags(t *testing.T, sb integration.Sandbox) {
skipNoCompatBuildKit(t, sb, ">= 0.26.0-0", "policy input requires BuildKit v0.26.0+")
dockerfile := []byte("FROM busybox:latest\nRUN echo policy-flags\n")
defaultPolicy := []byte(`
package docker
default allow = false
allow if input.env.args["DEFAULT_OK"] == "1"
decision := {"allow": allow}
`)
extraPolicy := []byte(`
package docker
default allow = false
allow if input.env.labels["com.example.extra"] == "1"
decision := {"allow": allow}
`)
denyPolicy := []byte(`
package docker
default allow = false
decision := {"allow": allow}
`)
t.Run("additional-policy-requires-default", func(t *testing.T) {
dir := tmpdir(
t,
fstest.CreateFile("Dockerfile", dockerfile, 0600),
fstest.CreateFile("Dockerfile.rego", defaultPolicy, 0600),
fstest.CreateFile("extra.rego", extraPolicy, 0600),
)
extraPath := filepath.Join(dir, "extra.rego")
cmd := buildxCmd(sb, withDir(dir), withArgs(
"build",
"--progress=plain",
"--policy", "filename="+extraPath,
"--build-arg", "DEFAULT_OK=1",
"--label", "com.example.extra=1",
"--output=type=cacheonly",
dir,
))
out, err := cmd.CombinedOutput()
require.NoError(t, err, string(out))
cmd = buildxCmd(sb, withDir(dir), withArgs(
"build",
"--progress=plain",
"--policy", "filename="+extraPath,
"--label", "com.example.extra=1",
"--output=type=cacheonly",
dir,
))
out, err = cmd.CombinedOutput()
require.Error(t, err, string(out))
require.Contains(t, string(out), "not allowed by policy")
cmd = buildxCmd(sb, withDir(dir), withArgs(
"build",
"--progress=plain",
"--policy", "filename="+extraPath,
"--build-arg", "DEFAULT_OK=1",
"--output=type=cacheonly",
dir,
))
out, err = cmd.CombinedOutput()
require.Error(t, err, string(out))
require.Contains(t, string(out), "not allowed by policy")
})
t.Run("reset-ignores-default", func(t *testing.T) {
dir := tmpdir(
t,
fstest.CreateFile("Dockerfile", dockerfile, 0600),
fstest.CreateFile("Dockerfile.rego", defaultPolicy, 0600),
fstest.CreateFile("extra.rego", extraPolicy, 0600),
)
extraPath := filepath.Join(dir, "extra.rego")
cmd := buildxCmd(sb, withDir(dir), withArgs(
"build",
"--progress=plain",
"--policy", "reset=true,filename="+extraPath,
"--label", "com.example.extra=1",
"--output=type=cacheonly",
dir,
))
out, err := cmd.CombinedOutput()
require.NoError(t, err, string(out))
cmd = buildxCmd(sb, withDir(dir), withArgs(
"build",
"--progress=plain",
"--policy", "reset=true,filename="+extraPath,
"--output=type=cacheonly",
dir,
))
out, err = cmd.CombinedOutput()
require.Error(t, err, string(out))
require.Contains(t, string(out), "not allowed by policy")
})
t.Run("disabled-skips-default", func(t *testing.T) {
dir := tmpdir(
t,
fstest.CreateFile("Dockerfile", dockerfile, 0600),
fstest.CreateFile("Dockerfile.rego", denyPolicy, 0600),
)
cmd := buildxCmd(sb, withDir(dir), withArgs(
"build",
"--progress=plain",
"--policy", "disabled=true",
"--output=type=cacheonly",
dir,
))
out, err := cmd.CombinedOutput()
require.NoError(t, err, string(out))
})
t.Run("disabled-cannot-combine-with-extra", func(t *testing.T) {
dir := tmpdir(
t,
fstest.CreateFile("Dockerfile", dockerfile, 0600),
fstest.CreateFile("extra.rego", denyPolicy, 0600),
)
extraPath := filepath.Join(dir, "extra.rego")
cmd := buildxCmd(sb, withDir(dir), withArgs(
"build",
"--progress=plain",
"--policy", "filename="+extraPath,
"--policy", "disabled=true",
"--output=type=cacheonly",
dir,
))
out, err := cmd.CombinedOutput()
require.Error(t, err, string(out))
require.Contains(t, string(out), "disabled policy cannot be combined with other policy flags")
})
}
func testBuildPolicyRemotePolicyFiles(t *testing.T, sb integration.Sandbox) {
skipNoCompatBuildKit(t, sb, ">= 0.26.0-0", "policy input requires BuildKit v0.26.0+")
gitDir := t.TempDir()
require.NoError(t, os.WriteFile(filepath.Join(gitDir, "Dockerfile"), []byte("FROM busybox:latest\nRUN echo remote-policy\n"), 0600))
require.NoError(t, os.MkdirAll(filepath.Join(gitDir, "policy"), 0700))
require.NoError(t, os.MkdirAll(filepath.Join(gitDir, "hack"), 0700))
require.NoError(t, os.WriteFile(filepath.Join(gitDir, "policy", "import.rego"), []byte(`
package docker
import data.hack.allow as allowlib
default allow = false
allow if allowlib.required
decision := {"allow": allow}
`), 0600))
require.NoError(t, os.WriteFile(filepath.Join(gitDir, "hack", "allow.rego"), []byte(`
package hack.allow
required := true
`), 0600))
require.NoError(t, os.WriteFile(filepath.Join(gitDir, "policy", "json.rego"), []byte(`
package docker
default allow = false
cfg := load_json("policy/config.json")
allow if cfg.allow == true
decision := {"allow": allow}
`), 0600))
require.NoError(t, os.WriteFile(filepath.Join(gitDir, "policy", "config.json"), []byte(`{"allow": true}`), 0600))
require.NoError(t, os.WriteFile(filepath.Join(gitDir, "policy", "deny.rego"), []byte(`
package docker
default allow = false
decision := {"allow": allow}
`), 0600))
require.NoError(t, os.WriteFile(filepath.Join(gitDir, "remote-only.rego"), []byte(`
package docker
default allow = false
decision := {"allow": allow}
`), 0600))
require.NoError(t, os.WriteFile(filepath.Join(gitDir, "Dockerfile.rego"), []byte(`
package docker
default allow = true
decision := {"allow": allow}
`), 0600))
git, err := gitutil.New(bkgitutil.WithDir(gitDir))
require.NoError(t, err)
gittestutil.GitInit(git, t)
gittestutil.GitAdd(git, t, ".")
gittestutil.GitCommit(git, t, "initial commit")
baseURL := gittestutil.GitServeHTTP(git, t)
workDir := t.TempDir()
require.NoError(t, os.WriteFile(filepath.Join(workDir, "local-allow.rego"), []byte(`
package docker
default allow = true
decision := {"allow": allow}
`), 0600))
require.NoError(t, os.WriteFile(filepath.Join(workDir, "local-only.rego"), []byte(`
package docker
default allow = true
decision := {"allow": allow}
`), 0600))
t.Run("remote-policy-import", func(t *testing.T) {
cmd := buildxCmd(sb, withDir(workDir), withArgs(
"build",
"--progress=plain",
"--policy", "filename=policy/import.rego",
"--output=type=cacheonly",
baseURL,
))
out, err := cmd.CombinedOutput()
require.NoError(t, err, string(out))
require.Contains(t, string(out), "loading policies")
require.Contains(t, string(out), "policy/import.rego")
})
t.Run("remote-policy-load-json", func(t *testing.T) {
cmd := buildxCmd(sb, withDir(workDir), withArgs(
"build",
"--progress=plain",
"--policy", "filename=policy/json.rego",
"--output=type=cacheonly",
baseURL,
))
out, err := cmd.CombinedOutput()
require.NoError(t, err, string(out))
require.Contains(t, string(out), "loading policies")
require.Contains(t, string(out), "policy/json.rego")
})
t.Run("remote-policy-cwd-override", func(t *testing.T) {
cmd := buildxCmd(sb, withDir(workDir), withArgs(
"build",
"--progress=plain",
"--policy", "filename=cwd://local-allow.rego",
"--output=type=cacheonly",
baseURL,
))
out, err := cmd.CombinedOutput()
require.NoError(t, err, string(out))
require.Contains(t, string(out), "loading policies")
require.Contains(t, string(out), "cwd://local-allow.rego")
})
t.Run("remote-policy-no-local-fallback", func(t *testing.T) {
cmd := buildxCmd(sb, withDir(workDir), withArgs(
"build",
"--progress=plain",
"--policy", "filename=does-not-exist.rego",
"--output=type=cacheonly",
baseURL,
))
out, err := cmd.CombinedOutput()
require.Error(t, err, string(out))
require.Contains(t, string(out), "policy file does-not-exist.rego not found")
})
t.Run("remote-policy-prefers-remote-over-cwd", func(t *testing.T) {
cmd := buildxCmd(sb, withDir(workDir), withArgs(
"build",
"--progress=plain",
"--policy", "filename=remote-only.rego",
"--output=type=cacheonly",
baseURL,
))
out, err := cmd.CombinedOutput()
require.Error(t, err, string(out))
require.Contains(t, string(out), "not allowed by policy")
require.Contains(t, string(out), "loading policies")
require.Contains(t, string(out), "remote-only.rego")
})
t.Run("remote-policy-cwd-prefix-uses-local", func(t *testing.T) {
cmd := buildxCmd(sb, withDir(workDir), withArgs(
"build",
"--progress=plain",
"--policy", "filename=cwd://local-only.rego",
"--output=type=cacheonly",
baseURL,
))
out, err := cmd.CombinedOutput()
require.NoError(t, err, string(out))
require.Contains(t, string(out), "loading policies")
require.Contains(t, string(out), "cwd://local-only.rego")
})
t.Run("remote-policy-default-from-remote-git", func(t *testing.T) {
cmd := buildxCmd(sb, withDir(workDir), withArgs(
"build",
"--progress=plain",
"--output=type=cacheonly",
baseURL,
))
out, err := cmd.CombinedOutput()
require.NoError(t, err, string(out))
require.Contains(t, string(out), "loading policies Dockerfile.rego")
})
}
func testBuildPolicyRemoteHTTPPolicyFiles(t *testing.T, sb integration.Sandbox) {
skipNoCompatBuildKit(t, sb, ">= 0.26.0-0", "policy input requires BuildKit v0.26.0+")
makeTar := func(t *testing.T, files map[string]string) []byte {
t.Helper()
var buf bytes.Buffer
tw := tar.NewWriter(&buf)
for name, data := range files {
dt := []byte(data)
require.NoError(t, tw.WriteHeader(&tar.Header{
Name: name,
Mode: 0600,
Size: int64(len(dt)),
}))
_, err := tw.Write(dt)
require.NoError(t, err)
}
require.NoError(t, tw.Close())
return buf.Bytes()
}
archiveURLPath := "/context.tar"
singleURLPath := "/Dockerfile"
server := httpserver.NewTestServer(map[string]*httpserver.Response{
archiveURLPath: {
Content: makeTar(t, map[string]string{
"Dockerfile": `FROM busybox:latest
RUN echo http-archive-policy
`,
"policy/allow.rego": `
package docker
default allow = true
decision := {"allow": allow}
`,
}),
},
singleURLPath: {
Content: []byte("FROM busybox:latest\nRUN echo http-single-file\n"),
},
})
defer server.Close()
workDir := t.TempDir()
require.NoError(t, os.WriteFile(filepath.Join(workDir, "Dockerfile.rego"), []byte(`
package docker
default allow = false
decision := {"allow": allow}
`), 0600))
t.Run("http-archive-policy-file", func(t *testing.T) {
cmd := buildxCmd(sb, withDir(workDir), withArgs(
"build",
"--progress=plain",
"--policy", "filename=policy/allow.rego",
"--output=type=cacheonly",
server.URL+archiveURLPath,
))
out, err := cmd.CombinedOutput()
require.NoError(t, err, string(out))
require.Contains(t, string(out), "loading policies")
require.Contains(t, string(out), "policy/allow.rego")
})
t.Run("http-single-file-required-policy-not-found", func(t *testing.T) {
cmd := buildxCmd(sb, withDir(workDir), withArgs(
"build",
"--progress=plain",
"--policy", "filename=policy/allow.rego",
"--output=type=cacheonly",
server.URL+singleURLPath,
))
out, err := cmd.CombinedOutput()
require.Error(t, err, string(out))
require.Contains(t, string(out), "policy file policy/allow.rego not found")
})
t.Run("http-single-file-no-local-default-fallback", func(t *testing.T) {
cmd := buildxCmd(sb, withDir(workDir), withArgs(
"build",
"--progress=plain",
"--output=type=cacheonly",
server.URL+singleURLPath,
))
out, err := cmd.CombinedOutput()
require.NoError(t, err, string(out))
})
t.Run("http-archive-default-from-remote-context", func(t *testing.T) {
serverWithDefault := httpserver.NewTestServer(map[string]*httpserver.Response{
archiveURLPath: {
Content: makeTar(t, map[string]string{
"Dockerfile": `FROM busybox:latest
RUN echo http-archive-default
`,
"Dockerfile.rego": `
package docker
default allow = true
decision := {"allow": allow}
`,
}),
},
})
defer serverWithDefault.Close()
cmd := buildxCmd(sb, withDir(workDir), withArgs(
"build",
"--progress=plain",
"--output=type=cacheonly",
serverWithDefault.URL+archiveURLPath,
))
out, err := cmd.CombinedOutput()
require.NoError(t, err, string(out))
require.Contains(t, string(out), "loading policies Dockerfile.rego")
})
}