471 lines · 14.8 KB · Go
RawDownload
package pluginstore import (	"encoding/base64"	"fmt"	"net/http"	"net/url"	"os"	"strings"	"time") const (	RequestKindRegistry = "registry"	RequestKindMetadata = "metadata"	RequestKindArtifact = "artifact" 	AuthTypeNone        = "none"	AuthTypeBearer      = "bearer"	AuthTypeBasic       = "basic"	AuthTypeHeader      = "header"	AuthTypeGitHubToken = "github-token") type AuthConfig struct {	Match          string   `yaml:"match,omitempty" json:"match,omitempty"`	ApplyTo        []string `yaml:"apply-to,omitempty" json:"apply_to,omitempty"`	Type           string   `yaml:"type,omitempty" json:"type,omitempty"`	TokenEnv       string   `yaml:"token-env,omitempty" json:"token_env,omitempty"`	UsernameEnv    string   `yaml:"username-env,omitempty" json:"username_env,omitempty"`	PasswordEnv    string   `yaml:"password-env,omitempty" json:"password_env,omitempty"`	HeaderName     string   `yaml:"header-name,omitempty" json:"header_name,omitempty"`	HeaderValueEnv string   `yaml:"header-value-env,omitempty" json:"header_value_env,omitempty"`	AllowInsecure  bool     `yaml:"allow-insecure,omitempty" json:"allow_insecure,omitempty"`} // Secret holds short-lived credential material that can be overwritten after use.type Secret []byte // Clear overwrites the secret and releases its backing slice reference.func (s *Secret) Clear() {	if s == nil {		return	}	for index := range *s {		(*s)[index] = 0	}	*s = nil} type ResolvedAuthConfig struct {	Match       string   `yaml:"match,omitempty" json:"match,omitempty"`	ApplyTo     []string `yaml:"apply-to,omitempty" json:"apply_to,omitempty"`	Type        string   `yaml:"type,omitempty" json:"type,omitempty"`	Token       Secret   `yaml:"token,omitempty" json:"token,omitempty"`	Username    Secret   `yaml:"username,omitempty" json:"username,omitempty"`	Password    Secret   `yaml:"password,omitempty" json:"password,omitempty"`	HeaderName  string   `yaml:"header-name,omitempty" json:"header_name,omitempty"`	HeaderValue Secret   `yaml:"header-value,omitempty" json:"header_value,omitempty"`} func (c *ResolvedAuthConfig) Clear() {	if c == nil {		return	}	c.Token.Clear()	c.Username.Clear()	c.Password.Clear()	c.HeaderValue.Clear()	c.ApplyTo = nil} func ClearResolvedAuthConfigs(auth []ResolvedAuthConfig) {	for index := range auth {		auth[index].Clear()	}} func ResolvedAuthForRequest(auth []ResolvedAuthConfig, requestURL string, kind string) (ResolvedAuthConfig, bool) {	item, ok := matchingResolvedAuthConfig(auth, requestURL, kind)	if !ok {		return ResolvedAuthConfig{}, false	}	return cloneResolvedAuthConfig(item), true} func ValidateResolvedAuthConfig(item ResolvedAuthConfig) error {	parsed, errParse := url.Parse(strings.TrimSpace(item.Match))	if errParse != nil || parsed.Scheme == "" || parsed.Host == "" {		return fmt.Errorf("plugin store resolved auth match is invalid")	}	if !strings.EqualFold(parsed.Scheme, "https") {		return fmt.Errorf("plugin store resolved auth match must use https")	}	if parsed.User != nil || parsed.RawQuery != "" || parsed.Fragment != "" {		return fmt.Errorf("plugin store resolved auth match must not contain credentials, query, or fragment")	}	for _, kind := range item.ApplyTo {		switch strings.ToLower(strings.TrimSpace(kind)) {		case RequestKindRegistry, RequestKindMetadata, RequestKindArtifact:		default:			return fmt.Errorf("plugin store resolved auth has unsupported apply_to %q", kind)		}	}	switch strings.ToLower(strings.TrimSpace(item.Type)) {	case "", AuthTypeNone:		return nil	case AuthTypeBearer, AuthTypeGitHubToken:		if len(item.Token) == 0 {			return fmt.Errorf("plugin store resolved auth token is empty")		}	case AuthTypeBasic:		if len(item.Username) == 0 || len(item.Password) == 0 {			return fmt.Errorf("plugin store resolved basic auth is incomplete")		}	case AuthTypeHeader:		if strings.TrimSpace(item.HeaderName) == "" || strings.ContainsAny(item.HeaderName, "\r\n:") {			return fmt.Errorf("plugin store resolved auth header name is invalid")		}		if len(item.HeaderValue) == 0 || secretContainsCRLF(item.HeaderValue) {			return fmt.Errorf("plugin store resolved auth header value is invalid")		}	default:		return fmt.Errorf("unsupported plugin store resolved auth type %q", item.Type)	}	return nil} func NormalizeAuthConfigs(auth []AuthConfig) []AuthConfig {	if len(auth) == 0 {		return nil	}	out := make([]AuthConfig, 0, len(auth))	for _, item := range auth {		item.Match = strings.TrimSpace(item.Match)		item.Type = strings.ToLower(strings.TrimSpace(item.Type))		item.TokenEnv = strings.TrimSpace(item.TokenEnv)		item.UsernameEnv = strings.TrimSpace(item.UsernameEnv)		item.PasswordEnv = strings.TrimSpace(item.PasswordEnv)		item.HeaderName = strings.TrimSpace(item.HeaderName)		item.HeaderValueEnv = strings.TrimSpace(item.HeaderValueEnv)		if item.Type == "" {			item.Type = AuthTypeNone		}		if item.Match == "" {			continue		}		if len(item.ApplyTo) > 0 {			applyTo := make([]string, 0, len(item.ApplyTo))			seen := map[string]struct{}{}			for _, value := range item.ApplyTo {				value = strings.ToLower(strings.TrimSpace(value))				if value == "" {					continue				}				if _, exists := seen[value]; exists {					continue				}				seen[value] = struct{}{}				applyTo = append(applyTo, value)			}			item.ApplyTo = applyTo		}		out = append(out, item)	}	return out} func AuthConfigured(auth []AuthConfig, requestURL string, kind string) bool {	item, ok := matchingAuthConfig(auth, requestURL, kind)	if !ok {		return false	}	switch strings.ToLower(strings.TrimSpace(item.Type)) {	case AuthTypeNone:		return false	case AuthTypeBearer, AuthTypeGitHubToken:		return strings.TrimSpace(os.Getenv(item.TokenEnv)) != ""	case AuthTypeBasic:		return strings.TrimSpace(os.Getenv(item.UsernameEnv)) != "" && strings.TrimSpace(os.Getenv(item.PasswordEnv)) != ""	case AuthTypeHeader:		return item.HeaderName != "" && strings.TrimSpace(os.Getenv(item.HeaderValueEnv)) != ""	default:		return false	}} func PluginAuthConfigured(source Source, plugin Plugin, auth []AuthConfig) bool {	if AuthConfigured(auth, source.URL, RequestKindRegistry) {		return true	}	switch PluginInstallType(plugin) {	case InstallTypeDirect:		for _, artifact := range PluginArtifacts(plugin) {			if AuthConfigured(auth, artifact.URL, RequestKindArtifact) {				return true			}		}	case InstallTypeGitHubRelease:		return pluginGitHubReleaseAuthConfigured(plugin, auth)	}	return false} func pluginGitHubReleaseAuthConfigured(plugin Plugin, auth []AuthConfig) bool {	owner, repo, errRepository := GitHubRepositoryParts(plugin.Repository)	if errRepository != nil {		return false	}	releasesURL := fmt.Sprintf(		"https://api.github.com/repos/%s/%s/releases/",		url.PathEscape(owner),		url.PathEscape(repo),	)	return AuthConfigured(auth, releasesURL+"latest", RequestKindMetadata) ||		AuthConfigured(auth, releasesURL+"tags/", RequestKindMetadata)} func applyPluginStoreAuth(headers http.Header, auth []AuthConfig, requestURL string, kind string) error {	_, errApply := applyPluginStoreAuthForClient(headers, nil, auth, requestURL, kind)	return errApply} func applyPluginStoreAuthForClient(headers http.Header, resolved []ResolvedAuthConfig, auth []AuthConfig, requestURL string, kind string) (bool, error) {	if item, ok := matchingResolvedAuthConfig(resolved, requestURL, kind); ok {		applied, errApply := applyResolvedPluginStoreAuth(headers, item)		return applied, errApply	}	item, ok := matchingAuthConfig(auth, requestURL, kind)	if !ok {		return false, nil	}	switch strings.ToLower(strings.TrimSpace(item.Type)) {	case "", AuthTypeNone:		return false, nil	case AuthTypeBearer:		token, errToken := envValueRequired(item.TokenEnv, "token-env")		if errToken != nil {			return false, errToken		}		headers.Set("Authorization", "Bearer "+token)	case AuthTypeBasic:		username, errUsername := envValueRequired(item.UsernameEnv, "username-env")		if errUsername != nil {			return false, errUsername		}		password, errPassword := envValueRequired(item.PasswordEnv, "password-env")		if errPassword != nil {			return false, errPassword		}		encoded := base64.StdEncoding.EncodeToString([]byte(username + ":" + password))		headers.Set("Authorization", "Basic "+encoded)	case AuthTypeHeader:		if strings.TrimSpace(item.HeaderName) == "" {			return false, fmt.Errorf("plugin store auth missing header-name")		}		value, errValue := envValueRequired(item.HeaderValueEnv, "header-value-env")		if errValue != nil {			return false, errValue		}		headers.Set(item.HeaderName, value)	case AuthTypeGitHubToken:		token, errToken := envValueRequired(item.TokenEnv, "token-env")		if errToken != nil {			return false, errToken		}		headers.Set("Authorization", "Bearer "+token)	default:		return false, fmt.Errorf("unsupported plugin store auth type %q", item.Type)	}	return true, nil} func applyResolvedPluginStoreAuth(headers http.Header, item ResolvedAuthConfig) (bool, error) {	switch strings.ToLower(strings.TrimSpace(item.Type)) {	case "", AuthTypeNone:		return false, nil	case AuthTypeBearer, AuthTypeGitHubToken:		if len(item.Token) == 0 {			return false, fmt.Errorf("plugin store resolved auth token is empty")		}		headers.Set("Authorization", "Bearer "+string(item.Token))	case AuthTypeBasic:		if len(item.Username) == 0 || len(item.Password) == 0 {			return false, fmt.Errorf("plugin store resolved basic auth is incomplete")		}		credential := make([]byte, 0, len(item.Username)+1+len(item.Password))		credential = append(credential, item.Username...)		credential = append(credential, ':')		credential = append(credential, item.Password...)		encoded := base64.StdEncoding.EncodeToString(credential)		for index := range credential {			credential[index] = 0		}		headers.Set("Authorization", "Basic "+encoded)	case AuthTypeHeader:		if strings.TrimSpace(item.HeaderName) == "" {			return false, fmt.Errorf("plugin store resolved auth missing header-name")		}		if len(item.HeaderValue) == 0 {			return false, fmt.Errorf("plugin store resolved auth header value is empty")		}		headers.Set(item.HeaderName, string(item.HeaderValue))	default:		return false, fmt.Errorf("unsupported plugin store resolved auth type %q", item.Type)	}	return true, nil} func validatePluginStoreRequestURL(auth []AuthConfig, requestURL string, kind string) error {	parsed, errParse := url.Parse(strings.TrimSpace(requestURL))	if errParse != nil || parsed.Scheme == "" || parsed.Host == "" {		return fmt.Errorf("invalid plugin store url")	}	if parsed.User != nil {		return fmt.Errorf("plugin store url must not contain credentials")	}	if hasSensitiveQueryParameter(parsed) {		return fmt.Errorf("plugin store url contains sensitive query parameter")	}	if strings.EqualFold(parsed.Scheme, "http") && !allowInsecurePluginStoreURL(auth, requestURL, kind) {		return fmt.Errorf("insecure plugin store url requires matching allow-insecure auth rule")	}	return nil} func allowInsecurePluginStoreURL(auth []AuthConfig, requestURL string, kind string) bool {	item, ok := matchingAuthConfig(auth, requestURL, kind)	return ok && item.AllowInsecure} func validateResolvedAuthExpiry(auth []ResolvedAuthConfig, expiresAt time.Time, now time.Time, requestURL string, kind string) error {	if expiresAt.IsZero() {		return nil	}	if _, ok := matchingResolvedAuthConfig(auth, requestURL, kind); !ok {		return nil	}	if !now.Before(expiresAt) {		return fmt.Errorf("plugin store resolved auth expired")	}	return nil} func matchingAuthConfig(auth []AuthConfig, requestURL string, kind string) (AuthConfig, bool) {	requestURL = strings.TrimSpace(requestURL)	kind = strings.ToLower(strings.TrimSpace(kind))	for _, item := range NormalizeAuthConfigs(auth) {		if !pluginStoreURLMatchesAuthRule(requestURL, item.Match) {			continue		}		if !authAppliesTo(item, kind) {			continue		}		return item, true	}	return AuthConfig{}, false} func matchingResolvedAuthConfig(auth []ResolvedAuthConfig, requestURL string, kind string) (ResolvedAuthConfig, bool) {	requestURL = strings.TrimSpace(requestURL)	kind = strings.ToLower(strings.TrimSpace(kind))	for _, item := range auth {		if !pluginStoreURLMatchesAuthRule(requestURL, strings.TrimSpace(item.Match)) {			continue		}		if !resolvedAuthAppliesTo(item, kind) {			continue		}		return item, true	}	return ResolvedAuthConfig{}, false} func resolvedAuthAppliesTo(item ResolvedAuthConfig, kind string) bool {	if len(item.ApplyTo) == 0 {		return true	}	for _, value := range item.ApplyTo {		if strings.EqualFold(strings.TrimSpace(value), kind) {			return true		}	}	return false} func cloneResolvedAuthConfig(item ResolvedAuthConfig) ResolvedAuthConfig {	item.ApplyTo = append([]string(nil), item.ApplyTo...)	item.Token = append(Secret(nil), item.Token...)	item.Username = append(Secret(nil), item.Username...)	item.Password = append(Secret(nil), item.Password...)	item.HeaderValue = append(Secret(nil), item.HeaderValue...)	return item} func resolvedAuthConfigured(item ResolvedAuthConfig) bool {	switch strings.ToLower(strings.TrimSpace(item.Type)) {	case AuthTypeBearer, AuthTypeGitHubToken:		return len(item.Token) > 0	case AuthTypeBasic:		return len(item.Username) > 0 && len(item.Password) > 0	case AuthTypeHeader:		return strings.TrimSpace(item.HeaderName) != "" && len(item.HeaderValue) > 0	default:		return false	}} func secretContainsCRLF(secret Secret) bool {	for _, value := range secret {		if value == '\r' || value == '\n' {			return true		}	}	return false} func pluginStoreURLMatchesAuthRule(requestURL string, matchURL string) bool {	request, errRequest := url.Parse(strings.TrimSpace(requestURL))	if errRequest != nil || request.Scheme == "" || request.Host == "" {		return false	}	rule, errRule := url.Parse(strings.TrimSpace(matchURL))	if errRule != nil || rule.Scheme == "" || rule.Host == "" {		return false	}	if !strings.EqualFold(request.Scheme, rule.Scheme) || !strings.EqualFold(request.Host, rule.Host) {		return false	}	return pluginStorePathMatchesAuthRule(request.Path, rule.Path)} func pluginStorePathMatchesAuthRule(requestPath string, rulePath string) bool {	if rulePath == "" || rulePath == "/" {		return true	}	if requestPath == "" {		requestPath = "/"	}	if requestPath == rulePath {		return true	}	if strings.HasSuffix(rulePath, "/") {		return strings.HasPrefix(requestPath, rulePath)	}	return strings.HasPrefix(requestPath, rulePath+"/")} func authAppliesTo(item AuthConfig, kind string) bool {	if len(item.ApplyTo) == 0 {		return true	}	for _, value := range item.ApplyTo {		if strings.EqualFold(strings.TrimSpace(value), kind) {			return true		}	}	return false} func envValueRequired(envName string, field string) (string, error) {	envName = strings.TrimSpace(envName)	if envName == "" {		return "", fmt.Errorf("plugin store auth missing %s", field)	}	value := strings.TrimSpace(os.Getenv(envName))	if value == "" {		return "", fmt.Errorf("plugin store auth env %s is empty", envName)	}	return value, nil}