diff --git a/policy/validate.go b/policy/validate.go index 48b2519b9..4f0be6c47 100644 --- a/policy/validate.go +++ b/policy/validate.go @@ -236,7 +236,7 @@ func (p *Policy) CheckPolicy(ctx context.Context, req *policysession.CheckPolicy if err := AddUnknownsWithLogger(p.opt.Log, next, unk); err != nil { return nil, nil, err } - if next.Image != nil || next.Git != nil { + if next.Image != nil || next.Git != nil || hasHTTPUnknowns(unk) { p.log(logrus.InfoLevel, "policy decision for source %s: resolve missing fields %+v", src.Source.Identifier, summarizeUnknownsForLog(unk)) return nil, next, nil } @@ -347,14 +347,15 @@ func SourceToInputWithLogger(ctx context.Context, getVerifier PolicyVerifierProv Path: u.Path, Query: u.Query(), } + if src.HTTP != nil { + inp.HTTP.Checksum = src.HTTP.Checksum + } + if inp.HTTP.Checksum == "" { + unknowns = append(unknowns, "input.http.checksum") + } if _, ok := src.Source.Attrs[pb.AttrHTTPAuthHeaderSecret]; ok { inp.HTTP.HasAuth = true } - if src.Image == nil { - unknowns = append(unknowns, "input.http.checksum") - } else { - inp.HTTP.Checksum = src.Image.Digest - } case "git": if !gitutil.IsGitTransport(refstr) { refstr = "https://" + refstr @@ -625,6 +626,9 @@ func AddUnknownsWithLogger(logf func(logrus.Level, string), req *gwpb.ResolveSou } req.Image.AttestationChain = true + case "http.checksum": + // HTTP checksums are resolved by BuildKit for the HTTP source itself. + case "git.ref", "git.checksum", "git.commitChecksum", "git.isAnnotatedTag", "git.isSHA256", "git.tagName", "git.branch": case "git.commit", "git.tag": @@ -678,6 +682,15 @@ func summarizeUnknownsForLog(unk []string) []string { return out } +func hasHTTPUnknowns(unk []string) bool { + for _, u := range unk { + if strings.HasPrefix(u, "input.http.") { + return true + } + } + return false +} + func trimKey(s string) string { const ( dot = '.' diff --git a/tests/policy_build.go b/tests/policy_build.go index f90bea10b..c134e534e 100644 --- a/tests/policy_build.go +++ b/tests/policy_build.go @@ -3,6 +3,7 @@ package tests import ( "errors" "fmt" + "net/url" "path/filepath" "testing" @@ -12,7 +13,9 @@ import ( "github.com/moby/buildkit/identity" "github.com/moby/buildkit/util/contentutil" "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" ) @@ -21,6 +24,7 @@ var policyBuildTests = []func(t *testing.T, sb integration.Sandbox){ testBuildPolicyDeny, testBuildPolicyImageName, testBuildPolicyEnv, + testBuildPolicyHTTP, } func testBuildPolicyAllow(t *testing.T, sb integration.Sandbox) { @@ -537,3 +541,185 @@ decision := {"allow": allow} }) } } + +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 { + sbDriver, _, _ := driverName(sb.Name()) + if sbDriver != "remote" { + t.Skip("http checksum policy input requires remote driver") + } + skipNoCompatBuildKit(t, sb, ">= 0.26.3-0", "http checksum policy input") + } + dir := tmpdir( + t, + fstest.CreateFile("Dockerfile", []byte(fmt.Sprintf("FROM busybox:latest\nADD %s /tmp/file\n", tc.addURL)), 0600), + fstest.CreateFile("policy.rego", []byte(tc.policy), 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() + 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) + } + }) + } +} diff --git a/vendor/github.com/moby/buildkit/util/testutil/httpserver/server.go b/vendor/github.com/moby/buildkit/util/testutil/httpserver/server.go new file mode 100644 index 000000000..589304c70 --- /dev/null +++ b/vendor/github.com/moby/buildkit/util/testutil/httpserver/server.go @@ -0,0 +1,113 @@ +package httpserver + +import ( + "bytes" + "io" + "net/http" + "net/http/httptest" + "slices" + "sync" + "time" +) + +type TestServer struct { + *httptest.Server + mu sync.Mutex + routes map[string]*Response + stats map[string]*Stat +} + +func NewTestServer(routes map[string]*Response) *TestServer { + ts := &TestServer{ + routes: routes, + stats: map[string]*Stat{}, + } + ts.Server = httptest.NewServer(ts) + return ts +} + +func (s *TestServer) SetRoute(name string, resp *Response) { + s.mu.Lock() + defer s.mu.Unlock() + s.routes[name] = resp +} + +func (s *TestServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { + s.mu.Lock() + resp, ok := s.routes[r.URL.Path] + if !ok { + w.WriteHeader(http.StatusNotFound) + s.mu.Unlock() + return + } + + if _, ok := s.stats[r.URL.Path]; !ok { + s.stats[r.URL.Path] = &Stat{} + } + + s.stats[r.URL.Path].AllRequests++ + s.stats[r.URL.Path].Requests = append(s.stats[r.URL.Path].Requests, newRequest(r)) + + if resp.LastModified != nil { + w.Header().Set("Last-Modified", resp.LastModified.Format(time.RFC850)) + } + + if resp.ContentEncoding != "" { + w.Header().Set("Content-Encoding", resp.ContentEncoding) + } + + if resp.ContentDisposition != "" { + w.Header().Set("Content-Disposition", resp.ContentDisposition) + } + + if resp.Etag != "" { + w.Header().Set("ETag", resp.Etag) + if match := r.Header.Get("If-None-Match"); match == resp.Etag { + w.WriteHeader(http.StatusNotModified) + s.stats[r.URL.Path].CachedRequests++ + s.mu.Unlock() + return + } + } + + s.mu.Unlock() + + w.WriteHeader(http.StatusOK) + io.Copy(w, bytes.NewReader(resp.Content)) +} + +func (s *TestServer) Stats(name string) (st Stat) { + if st, ok := s.stats[name]; ok { + return *st + } + return +} + +type Response struct { + Content []byte + Etag string + LastModified *time.Time + ContentEncoding string + ContentDisposition string +} + +type Stat struct { + AllRequests, CachedRequests int + Requests []Request +} + +type Request struct { + Method string + Header http.Header +} + +func newRequest(r *http.Request) Request { + headers := http.Header{} + for k, v := range r.Header { + headers[k] = slices.Clone(v) + } + return Request{ + Method: r.Method, + Header: headers, + } +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 16d39ba07..02afd71de 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -733,6 +733,7 @@ github.com/moby/buildkit/util/system github.com/moby/buildkit/util/testutil github.com/moby/buildkit/util/testutil/dockerd github.com/moby/buildkit/util/testutil/dockerd/client +github.com/moby/buildkit/util/testutil/httpserver github.com/moby/buildkit/util/testutil/integration github.com/moby/buildkit/util/testutil/workers github.com/moby/buildkit/util/tracing