Skip to content

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

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

Closed
wants to merge 2 commits into from
Closed
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
2 changes: 1 addition & 1 deletion taskfile/node.go
Original file line number Diff line number Diff line change
@@ -21,7 +21,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
}

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

@@ -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
rawUrl string
ref string
path string
fullURL string
baseURL string
ref string
filepath string
}

func NewGitNode(
@@ -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}
}

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.String()

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 {
@@ -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
}
@@ -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
}
@@ -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
@@ -12,9 +12,9 @@ func TestGitNode_ssh(t *testing.T) {
node, err := NewGitNode("git@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, "ssh://git@github.com/foo/bar.git//Taskfile.yml?ref=main", node.rawUrl)
assert.Equal(t, "ssh://git@github.com/foo/bar.git", node.URL.String())
assert.Equal(t, "Taskfile.yml", node.filepath)
assert.Equal(t, "ssh://git@github.com/foo/bar.git//Taskfile.yml?ref=main", node.fullURL)
assert.Equal(t, "ssh://git@github.com/foo/bar.git", node.baseURL)
entrypoint, err := node.ResolveEntrypoint("common.yml")
assert.NoError(t, err)
assert.Equal(t, "ssh://git@github.com/foo/bar.git//common.yml?ref=main", entrypoint)
@@ -26,9 +26,9 @@ func TestGitNode_sshWithDir(t *testing.T) {
node, err := NewGitNode("git@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, "ssh://git@github.com/foo/bar.git//directory/Taskfile.yml?ref=main", node.rawUrl)
assert.Equal(t, "ssh://git@github.com/foo/bar.git", node.URL.String())
assert.Equal(t, "directory/Taskfile.yml", node.filepath)
assert.Equal(t, "ssh://git@github.com/foo/bar.git//directory/Taskfile.yml?ref=main", node.fullURL)
assert.Equal(t, "ssh://git@github.com/foo/bar.git", node.baseURL)
entrypoint, err := node.ResolveEntrypoint("common.yml")
assert.NoError(t, err)
assert.Equal(t, "ssh://git@github.com/foo/bar.git//directory/common.yml?ref=main", entrypoint)
@@ -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:token@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:token@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:token@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:token@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
@@ -16,7 +16,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.
timeout time.Duration
}
@@ -34,13 +34,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,
}, nil
}
@@ -54,27 +54,27 @@ func (node *HTTPNode) Remote() bool {
}

func (node *HTTPNode) Read(ctx context.Context) ([]byte, error) {
url, err := RemoteExists(ctx, node.URL, node.timeout)
url, err := RemoteExists(ctx, 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,
}
}
@@ -93,7 +93,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) {
@@ -118,5 +118,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
}
76 changes: 76 additions & 0 deletions taskfile/node_http_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package taskfile

import (
"testing"
"time"

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

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

node, err := NewHTTPNode("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()

node, err := NewHTTPNode("https://user:password@example.com/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:xxxxx@example.com/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:password@example.com/common.yaml", location)
})
}

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

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(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)
})
}
}