diff --git a/executor.go b/executor.go index 49a367ee..2a3e495c 100644 --- a/executor.go +++ b/executor.go @@ -37,7 +37,7 @@ type ( Download bool Offline bool TrustedHosts []string - RemoteAuth taskfile.HostHeaders + RemoteAuth taskfile.HeadersByHost Timeout time.Duration CacheExpiryDuration time.Duration RemoteCacheDir string @@ -281,12 +281,12 @@ func (o *trustedHostsOption) ApplyToExecutor(e *Executor) { // WithRemoteAuth configures the [Executor] with the HTTP headers to send when // fetching a remote Taskfile, keyed by host. -func WithRemoteAuth(remoteAuth taskfile.HostHeaders) ExecutorOption { +func WithRemoteAuth(remoteAuth taskfile.HeadersByHost) ExecutorOption { return &remoteAuthOption{remoteAuth} } type remoteAuthOption struct { - remoteAuth taskfile.HostHeaders + remoteAuth taskfile.HeadersByHost } func (o *remoteAuthOption) ApplyToExecutor(e *Executor) { diff --git a/internal/flags/flags.go b/internal/flags/flags.go index a6b249b5..03346aa6 100644 --- a/internal/flags/flags.go +++ b/internal/flags/flags.go @@ -80,7 +80,7 @@ var ( Download bool Offline bool TrustedHosts []string - RemoteAuth taskfile.HostHeaders + RemoteAuth taskfile.HeadersByHost ClearCache bool Timeout time.Duration CacheExpiryDuration time.Duration @@ -318,11 +318,11 @@ func (o *flagsOption) ApplyToExecutor(e *task.Executor) { // remoteAuth flattens the configured entries into a lookup by host, the last // entry winning as it does when configuration files are merged. -func remoteAuth(config *taskrcast.TaskRC) taskfile.HostHeaders { +func remoteAuth(config *taskrcast.TaskRC) taskfile.HeadersByHost { if config == nil || len(config.Remote.Auth) == 0 { return nil } - byHost := make(taskfile.HostHeaders, len(config.Remote.Auth)) + byHost := make(taskfile.HeadersByHost, len(config.Remote.Auth)) for _, auth := range config.Remote.Auth { byHost[auth.Host] = auth.Headers } diff --git a/taskfile/http_auth.go b/taskfile/http_auth.go index 7159452b..69fe9e6b 100644 --- a/taskfile/http_auth.go +++ b/taskfile/http_auth.go @@ -12,9 +12,9 @@ import ( "github.com/go-task/task/v3/internal/templater" ) -// HostHeaders maps a host to the HTTP headers to send when fetching a remote +// HeadersByHost maps a host to the HTTP headers to send when fetching a remote // Taskfile from it. Values are templated, but no variables are available. -type HostHeaders map[string]map[string]string +type HeadersByHost map[string]map[string]string type authTransport struct { base http.RoundTripper @@ -38,7 +38,7 @@ func (t *authTransport) RoundTrip(req *http.Request) (*http.Response, error) { // authenticatedClient resolves on each read, not at build time, so a cached // run needs no credentials. func (node *HTTPNode) authenticatedClient() (*http.Client, error) { - headers, err := resolveAuthHeaders(node.authHeaders, node.url.Host) + headers, err := resolveAuthHeaders(node.authHeadersByHost, node.url.Host) if err != nil { return nil, err } @@ -61,9 +61,9 @@ func withAuthHeaders(client *http.Client, host string, headers map[string]string } // resolveAuthHeaders returns the expanded headers for host, or nil if none. -func resolveAuthHeaders(hostHeaders HostHeaders, host string) (map[string]string, error) { +func resolveAuthHeaders(headersByHost HeadersByHost, host string) (map[string]string, error) { var headers map[string]string - for pattern, patternHeaders := range hostHeaders { + for pattern, patternHeaders := range headersByHost { if hostMatches(pattern, host) { headers = patternHeaders break diff --git a/taskfile/http_auth_test.go b/taskfile/http_auth_test.go index 7092cabf..9dcadc0f 100644 --- a/taskfile/http_auth_test.go +++ b/taskfile/http_auth_test.go @@ -12,98 +12,98 @@ import ( func TestResolveAuthHeaders(t *testing.T) { //nolint:paralleltest // t.Setenv cannot be used in parallel tests tests := []struct { - name string - hostHeaders HostHeaders - host string - env map[string]string - want map[string]string - wantErr string + name string + headersByHost HeadersByHost + host string + env map[string]string + want map[string]string + wantErr string }{ { - name: "no configuration", - hostHeaders: nil, - host: "gitlab.com", + name: "no configuration", + headersByHost: nil, + host: "gitlab.com", }, { - name: "host does not match", - hostHeaders: HostHeaders{"gitlab.com": {"PRIVATE-TOKEN": "token"}}, - host: "example.com", + name: "host does not match", + headersByHost: HeadersByHost{"gitlab.com": {"PRIVATE-TOKEN": "token"}}, + host: "example.com", }, { - name: "port is part of the host", - hostHeaders: HostHeaders{"example.com": {"PRIVATE-TOKEN": "token"}}, - host: "example.com:8080", + name: "port is part of the host", + headersByHost: HeadersByHost{"example.com": {"PRIVATE-TOKEN": "token"}}, + host: "example.com:8080", }, { - name: "literal value", - hostHeaders: HostHeaders{"gitlab.com": {"PRIVATE-TOKEN": "token"}}, - host: "gitlab.com", - want: map[string]string{"PRIVATE-TOKEN": "token"}, + name: "literal value", + headersByHost: HeadersByHost{"gitlab.com": {"PRIVATE-TOKEN": "token"}}, + host: "gitlab.com", + want: map[string]string{"PRIVATE-TOKEN": "token"}, }, { - name: "environment variable", - hostHeaders: HostHeaders{"gitlab.com": {"PRIVATE-TOKEN": `{{env "TASK_TEST_TOKEN"}}`}}, //nolint:gosec // an env var reference, not a credential - host: "gitlab.com", - env: map[string]string{"TASK_TEST_TOKEN": "s3cret"}, - want: map[string]string{"PRIVATE-TOKEN": "s3cret"}, + name: "environment variable", + headersByHost: HeadersByHost{"gitlab.com": {"PRIVATE-TOKEN": `{{env "TASK_TEST_TOKEN"}}`}}, //nolint:gosec // an env var reference, not a credential + host: "gitlab.com", + env: map[string]string{"TASK_TEST_TOKEN": "s3cret"}, + want: map[string]string{"PRIVATE-TOKEN": "s3cret"}, }, { - name: "environment variable inside a longer value", - hostHeaders: HostHeaders{"gitlab.com": {"Authorization": `Bearer {{env "TASK_TEST_TOKEN"}}`}}, - host: "gitlab.com", - env: map[string]string{"TASK_TEST_TOKEN": "s3cret"}, - want: map[string]string{"Authorization": "Bearer s3cret"}, + name: "environment variable inside a longer value", + headersByHost: HeadersByHost{"gitlab.com": {"Authorization": `Bearer {{env "TASK_TEST_TOKEN"}}`}}, + host: "gitlab.com", + env: map[string]string{"TASK_TEST_TOKEN": "s3cret"}, + want: map[string]string{"Authorization": "Bearer s3cret"}, }, { - name: "undefined environment variable expands to nothing", - hostHeaders: HostHeaders{"gitlab.com": {"PRIVATE-TOKEN": `{{env "TASK_TEST_UNSET"}}`}}, //nolint:gosec // an env var reference, not a credential - host: "gitlab.com", - want: map[string]string{"PRIVATE-TOKEN": ""}, + name: "undefined environment variable expands to nothing", + headersByHost: HeadersByHost{"gitlab.com": {"PRIVATE-TOKEN": `{{env "TASK_TEST_UNSET"}}`}}, //nolint:gosec // an env var reference, not a credential + host: "gitlab.com", + want: map[string]string{"PRIVATE-TOKEN": ""}, }, { - name: "functions compose, so Basic auth needs no manual base64", - hostHeaders: HostHeaders{"gitlab.com": {"Authorization": `Basic {{ printf "%s:%s" (env "TASK_TEST_USER") (env "TASK_TEST_TOKEN") | b64enc }}`}}, - host: "gitlab.com", - env: map[string]string{"TASK_TEST_USER": "alice", "TASK_TEST_TOKEN": "s3cret"}, - want: map[string]string{"Authorization": "Basic YWxpY2U6czNjcmV0"}, + name: "functions compose, so Basic auth needs no manual base64", + headersByHost: HeadersByHost{"gitlab.com": {"Authorization": `Basic {{ printf "%s:%s" (env "TASK_TEST_USER") (env "TASK_TEST_TOKEN") | b64enc }}`}}, + host: "gitlab.com", + env: map[string]string{"TASK_TEST_USER": "alice", "TASK_TEST_TOKEN": "s3cret"}, + want: map[string]string{"Authorization": "Basic YWxpY2U6czNjcmV0"}, }, { // The .taskrc is read before any Taskfile, so no variable exists. - name: "a variable reference resolves to nothing", - hostHeaders: HostHeaders{"gitlab.com": {"PRIVATE-TOKEN": "{{.TASK_TEST_TOKEN}}"}}, //nolint:gosec // a template, not a credential - host: "gitlab.com", - env: map[string]string{"TASK_TEST_TOKEN": "s3cret"}, - want: map[string]string{"PRIVATE-TOKEN": ""}, + name: "a variable reference resolves to nothing", + headersByHost: HeadersByHost{"gitlab.com": {"PRIVATE-TOKEN": "{{.TASK_TEST_TOKEN}}"}}, //nolint:gosec // a template, not a credential + host: "gitlab.com", + env: map[string]string{"TASK_TEST_TOKEN": "s3cret"}, + want: map[string]string{"PRIVATE-TOKEN": ""}, }, { - name: "a literal value is left untouched", - hostHeaders: HostHeaders{"gitlab.com": {"PRIVATE-TOKEN": "p$ssw0rd"}}, //nolint:gosec // a test fixture - host: "gitlab.com", - want: map[string]string{"PRIVATE-TOKEN": "p$ssw0rd"}, + name: "a literal value is left untouched", + headersByHost: HeadersByHost{"gitlab.com": {"PRIVATE-TOKEN": "p$ssw0rd"}}, //nolint:gosec // a test fixture + host: "gitlab.com", + want: map[string]string{"PRIVATE-TOKEN": "p$ssw0rd"}, }, { - name: "malformed template", - hostHeaders: HostHeaders{"gitlab.com": {"PRIVATE-TOKEN": `{{env "TASK_TEST_TOKEN"`}}, //nolint:gosec // a template, not a credential - host: "gitlab.com", - wantErr: `remote auth for host "gitlab.com": template: :1: unclosed action`, + name: "malformed template", + headersByHost: HeadersByHost{"gitlab.com": {"PRIVATE-TOKEN": `{{env "TASK_TEST_TOKEN"`}}, //nolint:gosec // a template, not a credential + host: "gitlab.com", + wantErr: `remote auth for host "gitlab.com": template: :1: unclosed action`, }, { - name: "header name with a space", - hostHeaders: HostHeaders{"gitlab.com": {"PRIVATE TOKEN": "token"}}, - host: "gitlab.com", - wantErr: `remote auth for host "gitlab.com": invalid header name "PRIVATE TOKEN"`, + name: "header name with a space", + headersByHost: HeadersByHost{"gitlab.com": {"PRIVATE TOKEN": "token"}}, + host: "gitlab.com", + wantErr: `remote auth for host "gitlab.com": invalid header name "PRIVATE TOKEN"`, }, { - name: "header name outside the HTTP token grammar", - hostHeaders: HostHeaders{"gitlab.com": {"X-Foo(bar)": "token"}}, - host: "gitlab.com", - wantErr: `remote auth for host "gitlab.com": invalid header name "X-Foo(bar)"`, + name: "header name outside the HTTP token grammar", + headersByHost: HeadersByHost{"gitlab.com": {"X-Foo(bar)": "token"}}, + host: "gitlab.com", + wantErr: `remote auth for host "gitlab.com": invalid header name "X-Foo(bar)"`, }, { - name: "empty header name", - hostHeaders: HostHeaders{"gitlab.com": {"": "token"}}, - host: "gitlab.com", - wantErr: `remote auth for host "gitlab.com": invalid header name ""`, + name: "empty header name", + headersByHost: HeadersByHost{"gitlab.com": {"": "token"}}, + host: "gitlab.com", + wantErr: `remote auth for host "gitlab.com": invalid header name ""`, }, } @@ -112,7 +112,7 @@ func TestResolveAuthHeaders(t *testing.T) { //nolint:paralleltest // t.Setenv ca for name, value := range test.env { t.Setenv(name, value) } - headers, err := resolveAuthHeaders(test.hostHeaders, test.host) + headers, err := resolveAuthHeaders(test.headersByHost, test.host) if test.wantErr != "" { require.EqualError(t, err, test.wantErr) return @@ -178,7 +178,7 @@ func TestHTTPNodeAuthHeaders(t *testing.T) { //nolint:paralleltest // t.Setenv c t.Setenv("TASK_TEST_TOKEN", "s3cret") node, err := NewHTTPNode(srv.URL+"/Taskfile.yml", "", true, - WithAuthHeaders(HostHeaders{ + WithAuthHeaders(HeadersByHost{ mustHost(t, srv.URL): {"PRIVATE-TOKEN": `{{env "TASK_TEST_TOKEN"}}`}, //nolint:gosec // an env var reference, not a credential }), ) @@ -208,7 +208,7 @@ func TestHTTPNodeAuthHeadersNotSentOnRedirect(t *testing.T) { defer srv.Close() node, err := NewHTTPNode(srv.URL+"/Taskfile.yml", "", true, - WithAuthHeaders(HostHeaders{ + WithAuthHeaders(HeadersByHost{ mustHost(t, srv.URL): {"PRIVATE-TOKEN": "s3cret"}, }), ) @@ -226,7 +226,7 @@ func TestHTTPNodeAuthHeadersNotSentOnRedirect(t *testing.T) { // cached and offline runs do not require them. func TestHTTPNodeAuthHeadersResolvedLazily(t *testing.T) { //nolint:paralleltest // t.Setenv cannot be used in parallel tests node, err := NewHTTPNode("https://gitlab.com/Taskfile.yml", "", false, - WithAuthHeaders(HostHeaders{ + WithAuthHeaders(HeadersByHost{ "gitlab.com": {"PRIVATE-TOKEN": `{{env "TASK_TEST_LAZY"}}`}, //nolint:gosec // an env var reference, not a credential }), ) @@ -235,7 +235,7 @@ func TestHTTPNodeAuthHeadersResolvedLazily(t *testing.T) { //nolint:paralleltest // Defined only after the node was built: the value must still be picked up. t.Setenv("TASK_TEST_LAZY", "s3cret") - headers, err := resolveAuthHeaders(node.authHeaders, node.url.Host) + headers, err := resolveAuthHeaders(node.authHeadersByHost, node.url.Host) require.NoError(t, err) assert.Equal(t, map[string]string{"PRIVATE-TOKEN": "s3cret"}, headers) } diff --git a/taskfile/node_base.go b/taskfile/node_base.go index 9a8cafa7..82b5c1ac 100644 --- a/taskfile/node_base.go +++ b/taskfile/node_base.go @@ -7,13 +7,13 @@ type ( // designed to be embedded in other node types so that this boilerplate code // does not need to be repeated. baseNode struct { - parent Node - dir string - checksum string - caCert string - cert string - certKey string - authHeaders HostHeaders + parent Node + dir string + checksum string + caCert string + cert string + certKey string + authHeadersByHost HeadersByHost } ) @@ -78,8 +78,8 @@ func WithCertKey(certKey string) NodeOption { } // WithAuthHeaders sets the HTTP headers to send, keyed by host. -func WithAuthHeaders(authHeaders HostHeaders) NodeOption { +func WithAuthHeaders(authHeadersByHost HeadersByHost) NodeOption { return func(node *baseNode) { - node.authHeaders = authHeaders + node.authHeadersByHost = authHeadersByHost } } diff --git a/taskfile/reader.go b/taskfile/reader.go index 5ceec70e..edf0faa9 100644 --- a/taskfile/reader.go +++ b/taskfile/reader.go @@ -51,7 +51,7 @@ type ( caCert string cert string certKey string - authHeaders HostHeaders + authHeadersByHost HeadersByHost debugFunc DebugFunc promptFunc PromptFunc promptMutex sync.Mutex @@ -244,16 +244,16 @@ func (o *readerCertKeyOption) ApplyToReader(r *Reader) { } // WithReaderAuthHeaders sets the HTTP headers to send to each configured host. -func WithReaderAuthHeaders(authHeaders HostHeaders) ReaderOption { - return &readerAuthHeadersOption{authHeaders: authHeaders} +func WithReaderAuthHeaders(authHeadersByHost HeadersByHost) ReaderOption { + return &readerAuthHeadersOption{authHeadersByHost: authHeadersByHost} } type readerAuthHeadersOption struct { - authHeaders HostHeaders + authHeadersByHost HeadersByHost } func (o *readerAuthHeadersOption) ApplyToReader(r *Reader) { - r.authHeaders = o.authHeaders + r.authHeadersByHost = o.authHeadersByHost } // Read will read the Taskfile defined by the [Reader]'s [Node] and recurse @@ -371,7 +371,7 @@ func (r *Reader) include(ctx context.Context, node Node) error { WithCACert(r.caCert), WithCert(r.cert), WithCertKey(r.certKey), - WithAuthHeaders(r.authHeaders), + WithAuthHeaders(r.authHeadersByHost), ) if err != nil { if include.Optional {