Add projects
This commit is contained in:
parent
2d3a9ad623
commit
8b607dd700
1802 changed files with 503346 additions and 2 deletions
403
backend/internal/pluginstore/auth_test.go
Normal file
403
backend/internal/pluginstore/auth_test.go
Normal file
|
|
@ -0,0 +1,403 @@
|
|||
package pluginstore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"crypto/tls"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestPluginStoreAuthMatchesURLHostAndPathBoundaries(t *testing.T) {
|
||||
t.Setenv("PLUGIN_STORE_TOKEN", "secret-token")
|
||||
auth := []AuthConfig{{
|
||||
Match: "https://downloads.example/private",
|
||||
ApplyTo: []string{RequestKindArtifact},
|
||||
Type: AuthTypeBearer,
|
||||
TokenEnv: "PLUGIN_STORE_TOKEN",
|
||||
}}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
url string
|
||||
wantAuth bool
|
||||
}{
|
||||
{name: "exact path", url: "https://downloads.example/private", wantAuth: true},
|
||||
{name: "child path", url: "https://downloads.example/private/plugin.zip", wantAuth: true},
|
||||
{name: "sibling prefix", url: "https://downloads.example/private2/plugin.zip", wantAuth: false},
|
||||
{name: "similar host", url: "https://downloads.example.evil/private/plugin.zip", wantAuth: false},
|
||||
{name: "different scheme", url: "http://downloads.example/private/plugin.zip", wantAuth: false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
headers := http.Header{}
|
||||
if errAuth := applyPluginStoreAuth(headers, auth, tt.url, RequestKindArtifact); errAuth != nil {
|
||||
t.Fatalf("applyPluginStoreAuth() error = %v", errAuth)
|
||||
}
|
||||
gotAuth := headers.Get("Authorization") != ""
|
||||
if gotAuth != tt.wantAuth {
|
||||
t.Fatalf("Authorization set = %v, want %v", gotAuth, tt.wantAuth)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPluginStoreGitHubTokenUsesExplicitTokenEnv(t *testing.T) {
|
||||
t.Setenv("PLUGIN_STORE_TOKEN", "secret-token")
|
||||
headers := http.Header{}
|
||||
auth := []AuthConfig{{
|
||||
Match: "https://api.github.com/repos/author-name/sample-provider/releases/",
|
||||
ApplyTo: []string{RequestKindArtifact},
|
||||
Type: AuthTypeGitHubToken,
|
||||
TokenEnv: "PLUGIN_STORE_TOKEN",
|
||||
}}
|
||||
|
||||
if errAuth := applyPluginStoreAuth(headers, auth, "https://api.github.com/repos/author-name/sample-provider/releases/assets/1", RequestKindArtifact); errAuth != nil {
|
||||
t.Fatalf("applyPluginStoreAuth() error = %v", errAuth)
|
||||
}
|
||||
if gotAuth := headers.Get("Authorization"); gotAuth != "Bearer secret-token" {
|
||||
t.Fatalf("Authorization = %q, want Bearer secret-token", gotAuth)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPluginAuthConfiguredCoversInstallRequestKinds(t *testing.T) {
|
||||
t.Setenv("PLUGIN_STORE_TOKEN", "secret-token")
|
||||
|
||||
source := Source{URL: "https://registry.example/registry.json"}
|
||||
directPlugin := Plugin{
|
||||
ID: "sample-provider",
|
||||
Version: "1.0.0",
|
||||
Install: InstallPlan{
|
||||
Type: InstallTypeDirect,
|
||||
Artifacts: []Artifact{{
|
||||
GOOS: "linux",
|
||||
GOARCH: "amd64",
|
||||
URL: "https://downloads.example/private/sample-provider.zip",
|
||||
SHA256: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
|
||||
}},
|
||||
},
|
||||
}
|
||||
gitHubPlugin := Plugin{
|
||||
ID: "sample-provider",
|
||||
Repository: "https://github.com/author-name/sample-provider",
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
plugin Plugin
|
||||
auth []AuthConfig
|
||||
}{
|
||||
{
|
||||
name: "registry",
|
||||
plugin: gitHubPlugin,
|
||||
auth: []AuthConfig{{
|
||||
Match: "https://registry.example/",
|
||||
ApplyTo: []string{RequestKindRegistry},
|
||||
Type: AuthTypeBearer,
|
||||
TokenEnv: "PLUGIN_STORE_TOKEN",
|
||||
}},
|
||||
},
|
||||
{
|
||||
name: "direct artifact",
|
||||
plugin: directPlugin,
|
||||
auth: []AuthConfig{{
|
||||
Match: "https://downloads.example/private/",
|
||||
ApplyTo: []string{RequestKindArtifact},
|
||||
Type: AuthTypeBearer,
|
||||
TokenEnv: "PLUGIN_STORE_TOKEN",
|
||||
}},
|
||||
},
|
||||
{
|
||||
name: "github metadata",
|
||||
plugin: gitHubPlugin,
|
||||
auth: []AuthConfig{{
|
||||
Match: "https://api.github.com/repos/author-name/sample-provider/releases/",
|
||||
ApplyTo: []string{RequestKindMetadata},
|
||||
Type: AuthTypeBearer,
|
||||
TokenEnv: "PLUGIN_STORE_TOKEN",
|
||||
}},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if !PluginAuthConfigured(source, tt.plugin, tt.auth) {
|
||||
t.Fatal("PluginAuthConfigured() = false, want true")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPluginStoreAuthHeaderIsReevaluatedAcrossRedirect(t *testing.T) {
|
||||
t.Setenv("PLUGIN_STORE_HEADER", "secret-token")
|
||||
|
||||
var initialHeader string
|
||||
var redirectedHeader string
|
||||
artifactData := []byte("artifact-data")
|
||||
sum := sha256.Sum256(artifactData)
|
||||
target := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
redirectedHeader = r.Header.Get("X-Plugin-Token")
|
||||
_, _ = w.Write(artifactData)
|
||||
}))
|
||||
t.Cleanup(target.Close)
|
||||
source := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
initialHeader = r.Header.Get("X-Plugin-Token")
|
||||
http.Redirect(w, r, target.URL+"/artifact.zip", http.StatusFound)
|
||||
}))
|
||||
t.Cleanup(source.Close)
|
||||
|
||||
client := Client{
|
||||
HTTPClient: source.Client(),
|
||||
Auth: []AuthConfig{
|
||||
{
|
||||
Match: source.URL + "/private/",
|
||||
ApplyTo: []string{RequestKindArtifact},
|
||||
Type: AuthTypeHeader,
|
||||
HeaderName: "X-Plugin-Token",
|
||||
HeaderValueEnv: "PLUGIN_STORE_HEADER",
|
||||
AllowInsecure: true,
|
||||
},
|
||||
{
|
||||
Match: target.URL + "/",
|
||||
ApplyTo: []string{RequestKindArtifact},
|
||||
Type: AuthTypeNone,
|
||||
AllowInsecure: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
data, errDownload := client.DownloadArtifact(context.Background(), Artifact{
|
||||
GOOS: "linux",
|
||||
GOARCH: "amd64",
|
||||
URL: source.URL + "/private/artifact.zip",
|
||||
SHA256: hex.EncodeToString(sum[:]),
|
||||
})
|
||||
if errDownload != nil {
|
||||
t.Fatalf("DownloadArtifact() error = %v", errDownload)
|
||||
}
|
||||
if string(data) != string(artifactData) {
|
||||
t.Fatalf("DownloadArtifact() = %q, want %q", data, artifactData)
|
||||
}
|
||||
if initialHeader != "secret-token" {
|
||||
t.Fatalf("initial auth header = %q, want secret-token", initialHeader)
|
||||
}
|
||||
if redirectedHeader != "" {
|
||||
t.Fatalf("redirected auth header = %q, want empty", redirectedHeader)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPluginStoreAuthHeaderIsAppliedToMatchingRedirect(t *testing.T) {
|
||||
t.Setenv("PLUGIN_STORE_HEADER", "secret-token")
|
||||
|
||||
var redirectedHeader string
|
||||
artifactData := []byte("artifact-data")
|
||||
sum := sha256.Sum256(artifactData)
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/private/start.zip" {
|
||||
http.Redirect(w, r, "/private/artifact.zip", http.StatusFound)
|
||||
return
|
||||
}
|
||||
redirectedHeader = r.Header.Get("X-Plugin-Token")
|
||||
_, _ = io.WriteString(w, string(artifactData))
|
||||
}))
|
||||
t.Cleanup(server.Close)
|
||||
|
||||
client := Client{
|
||||
HTTPClient: server.Client(),
|
||||
Auth: []AuthConfig{{
|
||||
Match: server.URL + "/private/",
|
||||
ApplyTo: []string{RequestKindArtifact},
|
||||
Type: AuthTypeHeader,
|
||||
HeaderName: "X-Plugin-Token",
|
||||
HeaderValueEnv: "PLUGIN_STORE_HEADER",
|
||||
AllowInsecure: true,
|
||||
}},
|
||||
}
|
||||
if _, errDownload := client.DownloadArtifact(context.Background(), Artifact{
|
||||
GOOS: "linux",
|
||||
GOARCH: "amd64",
|
||||
URL: server.URL + "/private/start.zip",
|
||||
SHA256: hex.EncodeToString(sum[:]),
|
||||
}); errDownload != nil {
|
||||
t.Fatalf("DownloadArtifact() error = %v", errDownload)
|
||||
}
|
||||
if redirectedHeader != "secret-token" {
|
||||
t.Fatalf("redirected auth header = %q, want secret-token", redirectedHeader)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolvedPluginStoreAuthTakesPriorityOverEnvironmentAuth(t *testing.T) {
|
||||
t.Setenv("PLUGIN_STORE_TOKEN", "environment-token")
|
||||
headers := http.Header{}
|
||||
resolved := []ResolvedAuthConfig{{
|
||||
Match: "https://downloads.example/private/",
|
||||
ApplyTo: []string{RequestKindArtifact},
|
||||
Type: AuthTypeBearer,
|
||||
Token: Secret("resolved-token"),
|
||||
}}
|
||||
auth := []AuthConfig{{
|
||||
Match: "https://downloads.example/private/",
|
||||
ApplyTo: []string{RequestKindArtifact},
|
||||
Type: AuthTypeBearer,
|
||||
TokenEnv: "PLUGIN_STORE_TOKEN",
|
||||
}}
|
||||
|
||||
applied, errApply := applyPluginStoreAuthForClient(headers, resolved, auth, "https://downloads.example/private/plugin.zip", RequestKindArtifact)
|
||||
if errApply != nil {
|
||||
t.Fatalf("applyPluginStoreAuthForClient() error = %v", errApply)
|
||||
}
|
||||
if !applied || headers.Get("Authorization") != "Bearer resolved-token" {
|
||||
t.Fatalf("Authorization = %q, want resolved token", headers.Get("Authorization"))
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolvedNoAuthRuleBlocksEnvironmentFallback(t *testing.T) {
|
||||
t.Setenv("PLUGIN_STORE_TOKEN", "environment-token")
|
||||
headers := http.Header{}
|
||||
resolved := []ResolvedAuthConfig{{
|
||||
Match: "https://downloads.example/private/", ApplyTo: []string{RequestKindArtifact}, Type: AuthTypeNone,
|
||||
}}
|
||||
auth := []AuthConfig{{
|
||||
Match: "https://downloads.example/private/", ApplyTo: []string{RequestKindArtifact}, Type: AuthTypeBearer, TokenEnv: "PLUGIN_STORE_TOKEN",
|
||||
}}
|
||||
|
||||
applied, errApply := applyPluginStoreAuthForClient(headers, resolved, auth, "https://downloads.example/private/plugin.zip", RequestKindArtifact)
|
||||
if errApply != nil {
|
||||
t.Fatalf("applyPluginStoreAuthForClient() error = %v", errApply)
|
||||
}
|
||||
if applied || headers.Get("Authorization") != "" {
|
||||
t.Fatalf("resolved none rule applied environment auth: %q", headers.Get("Authorization"))
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolvedPluginStoreAuthIsNotForwardedAcrossOriginRedirect(t *testing.T) {
|
||||
var redirectedAuth string
|
||||
target := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
redirectedAuth = r.Header.Get("Authorization")
|
||||
_, _ = io.WriteString(w, "artifact")
|
||||
}))
|
||||
t.Cleanup(target.Close)
|
||||
source := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, target.URL+"/artifact.zip", http.StatusFound)
|
||||
}))
|
||||
t.Cleanup(source.Close)
|
||||
client := Client{
|
||||
HTTPClient: &http.Client{Transport: &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}}, //nolint:gosec -- test servers use ephemeral certificates.
|
||||
ResolvedAuth: []ResolvedAuthConfig{{
|
||||
Match: source.URL + "/private/",
|
||||
ApplyTo: []string{RequestKindArtifact},
|
||||
Type: AuthTypeBearer,
|
||||
Token: Secret("temporary-token"),
|
||||
}},
|
||||
}
|
||||
|
||||
if _, errDownload := client.DownloadArtifact(context.Background(), Artifact{
|
||||
GOOS: "linux",
|
||||
GOARCH: "amd64",
|
||||
URL: source.URL + "/private/artifact.zip",
|
||||
SHA256: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
|
||||
}); errDownload != nil && !strings.Contains(errDownload.Error(), "sha256 mismatch") {
|
||||
t.Fatalf("DownloadArtifact() error = %v, want only checksum mismatch", errDownload)
|
||||
}
|
||||
if redirectedAuth != "" {
|
||||
t.Fatalf("redirected Authorization = %q, want empty", redirectedAuth)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthenticatedPluginStoreFailureDoesNotExposeResponseBody(t *testing.T) {
|
||||
server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "secret diagnostic body", http.StatusUnauthorized)
|
||||
}))
|
||||
t.Cleanup(server.Close)
|
||||
client := Client{
|
||||
HTTPClient: server.Client(),
|
||||
ResolvedAuth: []ResolvedAuthConfig{{
|
||||
Match: server.URL + "/",
|
||||
ApplyTo: []string{RequestKindArtifact},
|
||||
Type: AuthTypeBearer,
|
||||
Token: Secret("temporary-token"),
|
||||
}},
|
||||
}
|
||||
|
||||
_, errDownload := client.DownloadArtifact(context.Background(), Artifact{
|
||||
GOOS: "linux",
|
||||
GOARCH: "amd64",
|
||||
URL: server.URL + "/artifact.zip",
|
||||
SHA256: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
|
||||
})
|
||||
if errDownload == nil {
|
||||
t.Fatal("DownloadArtifact() error = nil, want unauthorized status")
|
||||
}
|
||||
if strings.Contains(errDownload.Error(), "secret diagnostic body") {
|
||||
t.Fatalf("DownloadArtifact() error leaked response body: %v", errDownload)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolvedAuthClearOverwritesSecrets(t *testing.T) {
|
||||
token := Secret("temporary-token")
|
||||
backing := token
|
||||
auth := ResolvedAuthConfig{Token: token, Username: Secret("user"), Password: Secret("pass"), HeaderValue: Secret("header")}
|
||||
auth.Clear()
|
||||
for index, value := range backing {
|
||||
if value != 0 {
|
||||
t.Fatalf("token byte %d = %d, want zero", index, value)
|
||||
}
|
||||
}
|
||||
if auth.Token != nil || auth.Username != nil || auth.Password != nil || auth.HeaderValue != nil {
|
||||
t.Fatalf("cleared auth retains secret references: %#v", auth)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPluginStoreRequestErrorRedactsQueryAndFragment(t *testing.T) {
|
||||
requestURL := "https://user:password@downloads.example/plugin.zip?trace=private-value#section"
|
||||
cause := context.Canceled
|
||||
errRequest := pluginStoreRequestError(requestURL, &url.Error{URL: requestURL, Err: cause})
|
||||
if strings.Contains(errRequest.Error(), "private-value") || strings.Contains(errRequest.Error(), "section") || strings.Contains(errRequest.Error(), "trace=") || strings.Contains(errRequest.Error(), "password") || strings.Contains(errRequest.Error(), "user@") {
|
||||
t.Fatalf("pluginStoreRequestError() leaked URL query or fragment: %v", errRequest)
|
||||
}
|
||||
if !strings.Contains(errRequest.Error(), "https://downloads.example/plugin.zip") {
|
||||
t.Fatalf("pluginStoreRequestError() = %v, want sanitized URL", errRequest)
|
||||
}
|
||||
if !errors.Is(errRequest, cause) {
|
||||
t.Fatalf("errors.Is(pluginStoreRequestError(), context.Canceled) = false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPluginStoreRequestURLRejectsCredentials(t *testing.T) {
|
||||
errValidate := validatePluginStoreRequestURL(nil, "https://user:password@downloads.example/plugin.zip", RequestKindArtifact)
|
||||
if errValidate == nil {
|
||||
t.Fatal("validatePluginStoreRequestURL() error = nil, want URL credentials rejection")
|
||||
}
|
||||
if strings.Contains(errValidate.Error(), "password") {
|
||||
t.Fatalf("validatePluginStoreRequestURL() error leaked URL credentials: %v", errValidate)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolvedAuthExpiryRejectsAuthenticatedRequest(t *testing.T) {
|
||||
auth := []ResolvedAuthConfig{{
|
||||
Match: "https://downloads.example/private/",
|
||||
ApplyTo: []string{RequestKindArtifact},
|
||||
Type: AuthTypeBearer,
|
||||
Token: Secret("temporary-token"),
|
||||
}}
|
||||
now := time.Now().UTC()
|
||||
client := Client{
|
||||
HTTPClient: failingHTTPDoer{},
|
||||
ResolvedAuth: auth,
|
||||
ResolvedAuthExpiresAt: now.Add(-time.Second),
|
||||
}
|
||||
_, errDownload := client.DownloadArtifact(context.Background(), Artifact{
|
||||
GOOS: "linux",
|
||||
GOARCH: "amd64",
|
||||
URL: "https://downloads.example/private/plugin.zip",
|
||||
SHA256: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
|
||||
})
|
||||
if errDownload == nil || !strings.Contains(errDownload.Error(), "resolved auth expired") {
|
||||
t.Fatalf("DownloadArtifact() error = %v, want resolved auth expiry", errDownload)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue