Watch
1
0
Fork
You've already forked pkg-proxy
1
mirror of https://github.com/git-pkgs/proxy.git synced 2026-11-09 15:49:16 -05:00
pkg-proxy/internal/handler/npm_attestations_test.go

191 lines
7.7 KiB
Go
Raw Permalink Normal View History

package handler
import (
"net/http"
"slices"
"strings"
"testing"
"time"
"github.com/git-pkgs/cooldown"
)
const testAttestations = `{"attestations":[{"predicateType":"https://slsa.dev/provenance/v1","bundle":{}}]}`
// attestedPackument returns a packument whose version lists attestations at
// attestationsURL, resolved against the upstream's URL when it starts with
// "/", and was published age ago.
func attestedPackument(name, version, attestationsURL string, age time.Duration) func(string) string {
published := time.Now().Add(-age).UTC().Format(time.RFC3339)
return func(baseURL string) string {
if strings.HasPrefix(attestationsURL, "/") {
attestationsURL = baseURL + attestationsURL
}
dist := `{"tarball":"` + baseURL + "/" + name + `/-/x-` + version + `.tgz"`
if attestationsURL != "" {
dist += `,"attestations":{"url":"` + attestationsURL + `","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}}`
}
dist += "}"
return `{"name":"` + name + `","dist-tags":{"latest":"` + version + `"},` +
`"versions":{"` + version + `":{"version":"` + version + `","dist":` + dist + `}},` +
`"time":{"` + version + `":"` + published + `"}}`
}
}
func attestationsFile(string) string { return testAttestations }
func newAttestationsProxy(t *testing.T) *Proxy {
t.Helper()
proxy, _, _, _ := setupTestProxy(t)
proxy.HTTPClient = http.DefaultClient
proxy.CacheMetadata = true
proxy.MetadataTTL = time.Hour
return proxy
}
func TestNPMAttestationsServedAndCached(t *testing.T) {
upstream := newNPMUpstream(t, map[string]func(string) string{
"/sigstore": attestedPackument("sigstore", "3.0.0", "/-/npm/v1/attestations/sigstore@3.0.0", 30*24*time.Hour),
"/-/npm/v1/attestations/sigstore@3.0.0": attestationsFile,
}, http.StatusOK)
h := NewNPMHandler(newAttestationsProxy(t), "http://proxy.test", upstream.URL)
for range 2 {
w := serveNPM(t, h, http.MethodGet, "/-/npm/v1/attestations/sigstore@3.0.0", nil, nil)
if w.Code != http.StatusOK || w.Body.String() != testAttestations {
t.Fatalf("status = %d, body = %s", w.Code, w.Body.String())
}
}
fetched := 0
for _, p := range upstream.requested() {
if p == "/-/npm/v1/attestations/sigstore@3.0.0" {
fetched++
}
}
if fetched != 1 {
t.Errorf("attestations fetched %d times, want once: %v", fetched, upstream.requested())
}
}
func TestNPMAttestationsRoutedPackageStaysOnRoute(t *testing.T) {
public := newNPMUpstream(t, map[string]func(string) string{}, http.StatusOK)
private := newNPMUpstream(t, map[string]func(string) string{
routedNPMPath: attestedPackument("@example/widgets", "1.0.0", "/registry/-/npm/v1/attestations/@example%2fwidgets@1.0.0", 0),
"/registry/-/npm/v1/attestations/@example/widgets@1.0.0": attestationsFile,
}, http.StatusOK)
h := newRoutedNPMHandler(newAttestationsProxy(t), public, private)
for _, target := range []string{
"/-/npm/v1/attestations/@example%2fwidgets@1.0.0",
"/-/npm/v1/attestations/@example/widgets@1.0.0",
} {
w := serveNPM(t, h, http.MethodGet, target, nil, nil)
if w.Code != http.StatusOK || w.Body.String() != testAttestations {
t.Errorf("%s: status = %d, body = %s", target, w.Code, w.Body.String())
}
}
if got := public.requested(); len(got) != 0 {
t.Errorf("default registry was queried for a routed package: %v", got)
}
}
// TestNPMAttestationsOnlyAskThePackageRegistry checks that an attestations
// URL on another host is not followed: the proxy asks the package's registry
// at its standard path instead.
func TestNPMAttestationsOnlyAskThePackageRegistry(t *testing.T) {
other := newNPMUpstream(t, map[string]func(string) string{"/-/npm/v1/attestations/demo@1.0.0": attestationsFile}, http.StatusOK)
upstream := newNPMUpstream(t, map[string]func(string) string{
"/demo": attestedPackument("demo", "1.0.0", other.URL+"/-/npm/v1/attestations/demo@1.0.0", 0),
"/-/npm/v1/attestations/demo@1.0.0": attestationsFile,
}, http.StatusOK)
h := NewNPMHandler(newAttestationsProxy(t), "http://proxy.test", upstream.URL)
w := serveNPM(t, h, http.MethodGet, "/-/npm/v1/attestations/demo@1.0.0", nil, nil)
if w.Code != http.StatusOK {
t.Fatalf("status = %d, body = %s", w.Code, w.Body.String())
}
if got := other.requested(); len(got) != 0 {
t.Errorf("host named in the packument was queried: %v", got)
}
if !slices.Contains(upstream.requested(), "/-/npm/v1/attestations/demo@1.0.0") {
t.Errorf("registry not asked for attestations: %v", upstream.requested())
}
}
func TestNPMAttestationsWithheld(t *testing.T) {
tests := []struct {
name string
packument func(string) string
setup func(t *testing.T, p *Proxy)
wantStatus int
}{
{"version has none", attestedPackument("demo", "1.0.0", "", 0), nil, http.StatusNotFound},
{"version in cooldown", attestedPackument("demo", "1.0.0", "/-/npm/v1/attestations/demo@1.0.0", time.Hour),
func(_ *testing.T, p *Proxy) { p.Cooldown = &cooldown.Config{Default: "7d"} }, http.StatusNotFound},
{"version denied", attestedPackument("demo", "1.0.0", "/-/npm/v1/attestations/demo@1.0.0", 0),
func(t *testing.T, p *Proxy) { setTestDenylist(t, p, "pkg:npm/demo@1.0.0") }, http.StatusForbidden},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
upstream := newNPMUpstream(t, map[string]func(string) string{
"/demo": tt.packument,
"/-/npm/v1/attestations/demo@1.0.0": attestationsFile,
}, http.StatusOK)
proxy := newAttestationsProxy(t)
if tt.setup != nil {
tt.setup(t, proxy)
}
h := NewNPMHandler(proxy, "http://proxy.test", upstream.URL)
w := serveNPM(t, h, http.MethodGet, "/-/npm/v1/attestations/demo@1.0.0", nil, nil)
if w.Code != tt.wantStatus {
t.Errorf("status = %d, want %d; body: %s", w.Code, tt.wantStatus, w.Body.String())
}
if slices.Contains(upstream.requested(), "/-/npm/v1/attestations/demo@1.0.0") {
t.Error("attestations were fetched for a withheld version")
}
})
}
}
func TestNPMAttestationsRejectsInvalidPaths(t *testing.T) {
upstream := newNPMUpstream(t, map[string]func(string) string{}, http.StatusOK)
h := NewNPMHandler(newAttestationsProxy(t), "http://proxy.test", upstream.URL)
for _, target := range []string{
"/-/npm/v1/attestations/demo",
"/-/npm/v1/attestations/demo@",
"/-/npm/v1/attestations/@demo@1.0.0",
"/-/npm/v1/attestations/@scope/a/b@1.0.0",
"/-/npm/v1/attestations/demo@1.0.0/x",
"/-/npm/v1/attestations/..%2f..%2fetc@1.0.0",
} {
w := serveNPM(t, h, http.MethodGet, target, nil, nil)
if w.Code != http.StatusBadRequest {
t.Errorf("%s: status = %d, want 400", target, w.Code)
}
}
if got := upstream.requested(); len(got) != 0 {
t.Errorf("upstream queried for invalid paths: %v", got)
}
}
func TestNPMMetadataRewritesAttestationsURL(t *testing.T) {
const attestations = "/registry/-/npm/v1/attestations/@example%2fwidgets@1.0.0"
upstream := newNPMUpstream(t, map[string]func(string) string{
routedNPMPath: attestedPackument("@example/widgets", "1.0.0", attestations, 0),
routedNPMPath + "/1.0.0": func(baseURL string) string {
return `{"name":"@example/widgets","version":"1.0.0","dist":{"attestations":{"url":"` + baseURL + attestations +
`"},"tarball":"` + baseURL + `/registry/@example/widgets/-/widgets-1.0.0.tgz"}}`
},
}, http.StatusOK)
h := newRoutedNPMHandler(newAttestationsProxy(t), upstream, upstream)
for _, target := range []string{"/@example%2fwidgets", "/@example%2fwidgets/1.0.0"} {
w := serveNPM(t, h, http.MethodGet, target, nil, nil)
want := `"url":"http://proxy.test/npm/-/npm/v1/attestations/@example%2Fwidgets@1.0.0"`
if w.Code != http.StatusOK || !strings.Contains(w.Body.String(), want) {
t.Errorf("%s: status %d, attestations URL not rewritten: %s", target, w.Code, w.Body.String())
}
}
}