tests: add http policy integration tests
Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
This commit is contained in:
+19
-6
@@ -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 = '.'
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
+113
@@ -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,
|
||||
}
|
||||
}
|
||||
Vendored
+1
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user