Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remote Taskfiles: redact credentials of remote URLs in prompts and logs #2045

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions internal/logger/logger.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"slices"
"strconv"
"strings"
"testing"

"github.com/fatih/color"

Expand Down Expand Up @@ -107,6 +108,12 @@ func envColor(env string, defaultColor color.Attribute) []color.Attribute {
return attributes
}

// NewTestLogger returns a noop test logger.
func NewTestLogger(tb testing.TB) *Logger {
tb.Helper()
return &Logger{Stdout: io.Discard, Stderr: io.Discard, Verbose: false}
}

// Logger is just a wrapper that prints stuff to STDOUT or STDERR,
// with optional color.
type Logger struct {
Expand Down
2 changes: 1 addition & 1 deletion taskfile/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ type Node interface {
Remote() bool
ResolveEntrypoint(entrypoint string) (string, error)
ResolveDir(dir string) (string, error)
FilenameAndLastDir() (string, string)
FilenameAndLastDir() (lastDir string, file string) // TODO the return order is implemented opposite to the naming
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like this caused some bugs? #1317 (comment)

}

func NewRootNode(
Expand Down
51 changes: 25 additions & 26 deletions taskfile/node_git.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"context"
"fmt"
"io"
"net/url"
"path/filepath"
"strings"

Expand All @@ -22,10 +21,10 @@ import (
// An GitNode is a node that reads a Taskfile from a remote location via Git.
type GitNode struct {
*BaseNode
URL *url.URL
iwittkau marked this conversation as resolved.
Show resolved Hide resolved
rawUrl string
ref string
path string
fullURL string
baseURL string
ref string
filepath string
}

func NewGitNode(
Expand All @@ -34,37 +33,37 @@ func NewGitNode(
insecure bool,
opts ...NodeOption,
) (*GitNode, error) {
base := NewBaseNode(dir, opts...)
u, err := giturls.Parse(entrypoint)
gitURL, err := giturls.Parse(entrypoint)
if err != nil {
return nil, err
}
if gitURL.Scheme == "http" && !insecure {
return nil, &errors.TaskfileNotSecureError{URI: entrypoint}
iwittkau marked this conversation as resolved.
Show resolved Hide resolved
}

basePath, path := func() (string, string) {
x := strings.Split(u.Path, "//")
urlPath, filepath := func() (string, string) {
x := strings.Split(gitURL.Path, "//")
return x[0], x[1]
}()
ref := u.Query().Get("ref")

rawUrl := u.String()
ref := gitURL.Query().Get("ref")
fullURL := gitURL.Redacted()

u.RawQuery = ""
u.Path = basePath
gitURL.RawQuery = ""
gitURL.Path = urlPath
baseURL := gitURL.Redacted()
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We actually have to use the unredacted URL here.


if u.Scheme == "http" && !insecure {
return nil, &errors.TaskfileNotSecureError{URI: entrypoint}
}
return &GitNode{
BaseNode: base,
URL: u,
rawUrl: rawUrl,
BaseNode: NewBaseNode(dir, opts...),
fullURL: fullURL,
baseURL: baseURL,
ref: ref,
path: path,
filepath: filepath,
}, nil
}

func (node *GitNode) Location() string {
return node.rawUrl
return node.fullURL
}

func (node *GitNode) Remote() bool {
Expand All @@ -75,15 +74,15 @@ func (node *GitNode) Read(_ context.Context) ([]byte, error) {
fs := memfs.New()
storer := memory.NewStorage()
_, err := git.Clone(storer, fs, &git.CloneOptions{
URL: node.URL.String(),
URL: node.baseURL,
ReferenceName: plumbing.ReferenceName(node.ref),
SingleBranch: true,
Depth: 1,
})
if err != nil {
return nil, err
}
file, err := fs.Open(node.path)
file, err := fs.Open(node.filepath)
if err != nil {
return nil, err
}
Expand All @@ -97,8 +96,8 @@ func (node *GitNode) Read(_ context.Context) ([]byte, error) {
}

func (node *GitNode) ResolveEntrypoint(entrypoint string) (string, error) {
dir, _ := filepath.Split(node.path)
resolvedEntrypoint := fmt.Sprintf("%s//%s", node.URL, filepath.Join(dir, entrypoint))
dir, _ := filepath.Split(node.filepath)
resolvedEntrypoint := fmt.Sprintf("%s//%s", node.baseURL, filepath.Join(dir, entrypoint))
if node.ref != "" {
return fmt.Sprintf("%s?ref=%s", resolvedEntrypoint, node.ref), nil
}
Expand All @@ -122,5 +121,5 @@ func (node *GitNode) ResolveDir(dir string) (string, error) {
}

func (node *GitNode) FilenameAndLastDir() (string, string) {
return filepath.Base(node.path), filepath.Base(filepath.Dir(node.path))
return filepath.Base(filepath.Dir(node.filepath)), filepath.Base(node.filepath)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

taskfile/cache.go uses it like this:

lastDir, filename := node.FilenameAndLastDir()

}
38 changes: 19 additions & 19 deletions taskfile/node_git_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,9 @@ func TestGitNode_ssh(t *testing.T) {
node, err := NewGitNode("[email protected]:foo/bar.git//Taskfile.yml?ref=main", "", false)
assert.NoError(t, err)
assert.Equal(t, "main", node.ref)
assert.Equal(t, "Taskfile.yml", node.path)
assert.Equal(t, "ssh://[email protected]/foo/bar.git//Taskfile.yml?ref=main", node.rawUrl)
assert.Equal(t, "ssh://[email protected]/foo/bar.git", node.URL.String())
assert.Equal(t, "Taskfile.yml", node.filepath)
assert.Equal(t, "ssh://[email protected]/foo/bar.git//Taskfile.yml?ref=main", node.fullURL)
assert.Equal(t, "ssh://[email protected]/foo/bar.git", node.baseURL)
entrypoint, err := node.ResolveEntrypoint("common.yml")
assert.NoError(t, err)
assert.Equal(t, "ssh://[email protected]/foo/bar.git//common.yml?ref=main", entrypoint)
Expand All @@ -26,9 +26,9 @@ func TestGitNode_sshWithDir(t *testing.T) {
node, err := NewGitNode("[email protected]:foo/bar.git//directory/Taskfile.yml?ref=main", "", false)
assert.NoError(t, err)
assert.Equal(t, "main", node.ref)
assert.Equal(t, "directory/Taskfile.yml", node.path)
assert.Equal(t, "ssh://[email protected]/foo/bar.git//directory/Taskfile.yml?ref=main", node.rawUrl)
assert.Equal(t, "ssh://[email protected]/foo/bar.git", node.URL.String())
assert.Equal(t, "directory/Taskfile.yml", node.filepath)
assert.Equal(t, "ssh://[email protected]/foo/bar.git//directory/Taskfile.yml?ref=main", node.fullURL)
assert.Equal(t, "ssh://[email protected]/foo/bar.git", node.baseURL)
entrypoint, err := node.ResolveEntrypoint("common.yml")
assert.NoError(t, err)
assert.Equal(t, "ssh://[email protected]/foo/bar.git//directory/common.yml?ref=main", entrypoint)
Expand All @@ -37,49 +37,49 @@ func TestGitNode_sshWithDir(t *testing.T) {
func TestGitNode_https(t *testing.T) {
t.Parallel()

node, err := NewGitNode("https://github.com/foo/bar.git//Taskfile.yml?ref=main", "", false)
node, err := NewGitNode("https://git:token@github.com/foo/bar.git//Taskfile.yml?ref=main", "", false)
assert.NoError(t, err)
assert.Equal(t, "main", node.ref)
assert.Equal(t, "Taskfile.yml", node.path)
assert.Equal(t, "https://github.com/foo/bar.git//Taskfile.yml?ref=main", node.rawUrl)
assert.Equal(t, "https://github.com/foo/bar.git", node.URL.String())
assert.Equal(t, "Taskfile.yml", node.filepath)
assert.Equal(t, "https://git:xxxxx@github.com/foo/bar.git//Taskfile.yml?ref=main", node.fullURL)
assert.Equal(t, "https://git:xxxxx@github.com/foo/bar.git", node.baseURL)
entrypoint, err := node.ResolveEntrypoint("common.yml")
assert.NoError(t, err)
assert.Equal(t, "https://github.com/foo/bar.git//common.yml?ref=main", entrypoint)
assert.Equal(t, "https://git:xxxxx@github.com/foo/bar.git//common.yml?ref=main", entrypoint)
}

func TestGitNode_httpsWithDir(t *testing.T) {
t.Parallel()

node, err := NewGitNode("https://github.com/foo/bar.git//directory/Taskfile.yml?ref=main", "", false)
node, err := NewGitNode("https://git:token@github.com/foo/bar.git//directory/Taskfile.yml?ref=main", "", false)
assert.NoError(t, err)
assert.Equal(t, "main", node.ref)
assert.Equal(t, "directory/Taskfile.yml", node.path)
assert.Equal(t, "https://github.com/foo/bar.git//directory/Taskfile.yml?ref=main", node.rawUrl)
assert.Equal(t, "https://github.com/foo/bar.git", node.URL.String())
assert.Equal(t, "directory/Taskfile.yml", node.filepath)
assert.Equal(t, "https://git:xxxxx@github.com/foo/bar.git//directory/Taskfile.yml?ref=main", node.fullURL)
assert.Equal(t, "https://git:xxxxx@github.com/foo/bar.git", node.baseURL)
entrypoint, err := node.ResolveEntrypoint("common.yml")
assert.NoError(t, err)
assert.Equal(t, "https://github.com/foo/bar.git//directory/common.yml?ref=main", entrypoint)
assert.Equal(t, "https://git:xxxxx@github.com/foo/bar.git//directory/common.yml?ref=main", entrypoint)
}

func TestGitNode_FilenameAndDir(t *testing.T) {
t.Parallel()

node, err := NewGitNode("https://github.com/foo/bar.git//directory/Taskfile.yml?ref=main", "", false)
assert.NoError(t, err)
filename, dir := node.FilenameAndLastDir()
dir, filename := node.FilenameAndLastDir()
assert.Equal(t, "Taskfile.yml", filename)
assert.Equal(t, "directory", dir)

node, err = NewGitNode("https://github.com/foo/bar.git//Taskfile.yml?ref=main", "", false)
assert.NoError(t, err)
filename, dir = node.FilenameAndLastDir()
dir, filename = node.FilenameAndLastDir()
assert.Equal(t, "Taskfile.yml", filename)
assert.Equal(t, ".", dir)

node, err = NewGitNode("https://github.com/foo/bar.git//multiple/directory/Taskfile.yml?ref=main", "", false)
assert.NoError(t, err)
filename, dir = node.FilenameAndLastDir()
dir, filename = node.FilenameAndLastDir()
assert.Equal(t, "Taskfile.yml", filename)
assert.Equal(t, "directory", dir)
}
28 changes: 16 additions & 12 deletions taskfile/node_http.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import (
// An HTTPNode is a node that reads a Taskfile from a remote location via HTTP.
type HTTPNode struct {
*BaseNode
URL *url.URL // stores url pointing actual remote file. (e.g. with Taskfile.yml)
url *url.URL // stores url pointing actual remote file. (e.g. with Taskfile.yml)
entrypoint string // stores entrypoint url. used for building graph vertices.
logger *logger.Logger
timeout time.Duration
Expand All @@ -37,13 +37,13 @@ func NewHTTPNode(
return nil, err
}
if url.Scheme == "http" && !insecure {
return nil, &errors.TaskfileNotSecureError{URI: entrypoint}
return nil, &errors.TaskfileNotSecureError{URI: url.Redacted()}
}

return &HTTPNode{
BaseNode: base,
URL: url,
entrypoint: entrypoint,
url: url,
entrypoint: url.Redacted(),
timeout: timeout,
logger: l,
}, nil
Expand All @@ -58,27 +58,27 @@ func (node *HTTPNode) Remote() bool {
}

func (node *HTTPNode) Read(ctx context.Context) ([]byte, error) {
url, err := RemoteExists(ctx, node.logger, node.URL, node.timeout)
url, err := RemoteExists(ctx, node.logger, node.url, node.timeout)
if err != nil {
return nil, err
}
node.URL = url
req, err := http.NewRequest("GET", node.URL.String(), nil)
node.url = url
req, err := http.NewRequest("GET", node.url.String(), nil)
if err != nil {
return nil, errors.TaskfileFetchFailedError{URI: node.URL.String()}
return nil, errors.TaskfileFetchFailedError{URI: node.url.Redacted()}
}

resp, err := http.DefaultClient.Do(req.WithContext(ctx))
if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
return nil, &errors.TaskfileNetworkTimeoutError{URI: node.URL.String(), Timeout: node.timeout}
return nil, &errors.TaskfileNetworkTimeoutError{URI: node.url.Redacted(), Timeout: node.timeout}
}
return nil, errors.TaskfileFetchFailedError{URI: node.URL.String()}
return nil, errors.TaskfileFetchFailedError{URI: node.url.Redacted()}
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, errors.TaskfileFetchFailedError{
URI: node.URL.String(),
URI: node.url.Redacted(),
HTTPStatusCode: resp.StatusCode,
}
}
Expand All @@ -97,7 +97,7 @@ func (node *HTTPNode) ResolveEntrypoint(entrypoint string) (string, error) {
if err != nil {
return "", err
}
return node.URL.ResolveReference(ref).String(), nil
return node.url.ResolveReference(ref).String(), nil
}

func (node *HTTPNode) ResolveDir(dir string) (string, error) {
Expand All @@ -122,5 +122,9 @@ func (node *HTTPNode) ResolveDir(dir string) (string, error) {

func (node *HTTPNode) FilenameAndLastDir() (string, string) {
dir, filename := filepath.Split(node.entrypoint)
dir = filepath.Base(dir)
if dir == node.url.Host {
dir = "."
}
return filepath.Base(dir), filename
}
81 changes: 81 additions & 0 deletions taskfile/node_http_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package taskfile

import (
"testing"
"time"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/go-task/task/v3/internal/logger"
)

func TestHTTPNode_https(t *testing.T) {
t.Parallel()

l := logger.NewTestLogger(t)
node, err := NewHTTPNode(l, "https://raw.githubusercontent.com/my-org/my-repo/main/Taskfile.yml", "", false, time.Second)
require.NoError(t, err)
assert.Equal(t, time.Second, node.timeout)
assert.Equal(t, "https://raw.githubusercontent.com/my-org/my-repo/main/Taskfile.yml", node.url.String())
entrypoint, err := node.ResolveEntrypoint("common.yml")
require.NoError(t, err)
assert.Equal(t, "https://raw.githubusercontent.com/my-org/my-repo/main/common.yml", entrypoint)
}

func TestHTTPNode_redaction(t *testing.T) {
t.Parallel()

l := logger.NewTestLogger(t)
node, err := NewHTTPNode(l, "https://user:[email protected]/Taskfile.yml", "", false, time.Second)

t.Run("the location is redacted", func(t *testing.T) {
t.Parallel()
require.NoError(t, err)
assert.Equal(t, "https://user:[email protected]/Taskfile.yml", node.Location())
})

t.Run("resolved entrypoints contain the username and password", func(t *testing.T) {
t.Parallel()
location, err := node.ResolveEntrypoint("common.yaml")
require.NoError(t, err)
assert.Equal(t, "https://user:[email protected]/common.yaml", location)
})
}

func TestHTTPNode_FilenameAndDir(t *testing.T) {
t.Parallel()

l := logger.NewTestLogger(t)
tests := map[string]struct {
entrypoint string
filename string
dir string
}{
"file at root": {
entrypoint: "https://example.com/Taskfile.yaml",
filename: "Taskfile.yaml",
dir: ".",
},
"file in folder": {
entrypoint: "https://example.com/taskfiles/Taskfile.yaml",
filename: "Taskfile.yaml",
dir: "taskfiles",
},
"nested structure": {
entrypoint: "https://raw.githubusercontent.com/my-org/my-repo/main/Taskfile.yaml",
filename: "Taskfile.yaml",
dir: "main",
},
}
for name, tt := range tests {
t.Run(name, func(t *testing.T) {
t.Parallel()
node, err := NewHTTPNode(l, tt.entrypoint, "", false, time.Second)
require.NoError(t, err)
dir, filename := node.FilenameAndLastDir()
assert.Equal(t, tt.filename, filename)
assert.Equal(t, tt.dir, dir)
})
}
}
Loading