403 lines · 13.8 KB · Go
RawDownload
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)	}}