Skip to content
Open
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
15 changes: 15 additions & 0 deletions executor.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ type (
Insecure bool
Download bool
Offline bool
Trust []string
Timeout time.Duration
CacheExpiryDuration time.Duration
Watch bool
Expand Down Expand Up @@ -225,6 +226,20 @@ func (o *offlineOption) ApplyToExecutor(e *Executor) {
e.Offline = o.offline
}

// WithTrust configures the [Executor] with a list of trusted hosts for remote
// Taskfiles. Hosts in this list will not prompt for user confirmation.
func WithTrust(trust []string) ExecutorOption {
return &trustOption{trust}
}

type trustOption struct {
trust []string
}

func (o *trustOption) ApplyToExecutor(e *Executor) {
e.Trust = o.trust
}

// WithTimeout sets the [Executor]'s timeout for fetching remote taskfiles. By
// default, the timeout is set to 10 seconds.
func WithTimeout(timeout time.Duration) ExecutorOption {
Expand Down
3 changes: 3 additions & 0 deletions internal/flags/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ var (
Experiments bool
Download bool
Offline bool
Trust []string
ClearCache bool
Timeout time.Duration
CacheExpiryDuration time.Duration
Expand Down Expand Up @@ -152,6 +153,7 @@ func init() {
if experiments.RemoteTaskfiles.Enabled() {
pflag.BoolVar(&Download, "download", false, "Downloads a cached version of a remote Taskfile.")
pflag.BoolVar(&Offline, "offline", getConfig(config, func() *bool { return config.Remote.Offline }, false), "Forces Task to only use local or cached Taskfiles.")
pflag.StringSliceVar(&Trust, "trust", config.Remote.Trust, "List of trusted hosts for remote Taskfiles (can be specified multiple times).")
pflag.DurationVar(&Timeout, "timeout", getConfig(config, func() *time.Duration { return config.Remote.Timeout }, time.Second*10), "Timeout for downloading remote Taskfiles.")
pflag.BoolVar(&ClearCache, "clear-cache", false, "Clear the remote cache.")
pflag.DurationVar(&CacheExpiryDuration, "expiry", getConfig(config, func() *time.Duration { return config.Remote.CacheExpiry }, 0), "Expiry duration for cached remote Taskfiles.")
Expand Down Expand Up @@ -238,6 +240,7 @@ func (o *flagsOption) ApplyToExecutor(e *task.Executor) {
task.WithInsecure(Insecure),
task.WithDownload(Download),
task.WithOffline(Offline),
task.WithTrust(Trust),
task.WithTimeout(Timeout),
task.WithCacheExpiryDuration(CacheExpiryDuration),
task.WithWatch(Watch),
Expand Down
1 change: 1 addition & 0 deletions setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ func (e *Executor) readTaskfile(node taskfile.Node) error {
taskfile.WithInsecure(e.Insecure),
taskfile.WithDownload(e.Download),
taskfile.WithOffline(e.Offline),
taskfile.WithTrust(e.Trust),
taskfile.WithTempDir(e.TempDir.Remote),
taskfile.WithCacheExpiryDuration(e.CacheExpiryDuration),
taskfile.WithDebugFunc(debugFunc),
Expand Down
23 changes: 23 additions & 0 deletions task_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
rand "math/rand/v2"
"net/http"
"net/http/httptest"
"net/url"
"os"
"path/filepath"
"regexp"
Expand Down Expand Up @@ -784,6 +785,11 @@ func TestIncludesRemote(t *testing.T) {

var buff SyncBuffer

// Extract host from server URL for trust testing
parsedURL, err := url.Parse(srv.URL)
require.NoError(t, err)
trustedHost := parsedURL.Host
Copy link
Contributor

Choose a reason for hiding this comment

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

Can't I trust URLs? I trust github.com/myself but I don't trust github.com/shady.

Copy link
Author

Choose a reason for hiding this comment

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

I was thinking about that, but using URLs requires more assumptions how to compare provided URL with the trust config.

  1. Exact match, so full URL comparison. In my case it would require configuring every single remote taskfiles (more than dozen now) which is not a big deal, but may not be a best DX.
  2. Prefix match. A problem: I want to trust https://github.com/myself but not https://github.com/myselfHackedByShady - which could be easily solved by setting https://github.com/myself/ and not https://github.com/myself. So maybe this is the best way.
  3. Glob-like style: https://github.com/myself/* or extended version https://github.com/myself/**/*
  4. Regex: https:\/\/github\.com\/myself\/.*


executors := []struct {
name string
executor *task.Executor
Expand Down Expand Up @@ -823,6 +829,23 @@ func TestIncludesRemote(t *testing.T) {
task.WithOffline(true),
),
},
{
name: "with trust, no prompts",
executor: task.NewExecutor(
task.WithDir(dir),
task.WithStdout(&buff),
task.WithStderr(&buff),
task.WithTimeout(time.Minute),
task.WithInsecure(true),
task.WithStdout(&buff),
task.WithStderr(&buff),
task.WithVerbose(true),

// With trust
task.WithTrust([]string{trustedHost}),
task.WithDownload(true),
),
},
}

for _, e := range executors {
Expand Down
43 changes: 41 additions & 2 deletions taskfile/reader.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package taskfile
import (
"context"
"fmt"
"net/url"
"os"
"sync"
"time"
Expand Down Expand Up @@ -43,6 +44,7 @@ type (
insecure bool
download bool
offline bool
trust []string
tempDir string
cacheExpiryDuration time.Duration
debugFunc DebugFunc
Expand All @@ -59,6 +61,7 @@ func NewReader(opts ...ReaderOption) *Reader {
insecure: false,
download: false,
offline: false,
trust: nil,
tempDir: os.TempDir(),
cacheExpiryDuration: 0,
debugFunc: nil,
Expand Down Expand Up @@ -119,6 +122,20 @@ func (o *offlineOption) ApplyToReader(r *Reader) {
r.offline = o.offline
}

// WithTrust configures the [Reader] with a list of trusted hosts for remote
// Taskfiles. Hosts in this list will not prompt for user confirmation.
func WithTrust(trust []string) ReaderOption {
return &trustOption{trust: trust}
}

type trustOption struct {
trust []string
}

func (o *trustOption) ApplyToReader(r *Reader) {
r.trust = o.trust
}

// WithTempDir sets the temporary directory that will be used by the [Reader].
// By default, the reader uses [os.TempDir].
func WithTempDir(tempDir string) ReaderOption {
Expand Down Expand Up @@ -206,6 +223,28 @@ func (r *Reader) promptf(format string, a ...any) error {
return nil
}

// isTrusted checks if a URI's host matches any of the trusted hosts patterns.
func (r *Reader) isTrusted(uri string) bool {
if len(r.trust) == 0 {
return false
}

// Parse the URI to extract the host
parsedURL, err := url.Parse(uri)
if err != nil {
return false
}
host := parsedURL.Host

// Check against each trusted pattern (exact match including port if provided)
for _, pattern := range r.trust {
if host == pattern {
return true
}
}
return false
}

func (r *Reader) include(ctx context.Context, node Node) error {
// Create a new vertex for the Taskfile
vertex := &ast.TaskfileVertex{
Expand Down Expand Up @@ -459,9 +498,9 @@ func (r *Reader) readRemoteNodeContent(ctx context.Context, node RemoteNode) ([]

// If there is no manual checksum pin, run the automatic checks
if node.Checksum() == "" {
// Prompt the user if required
// Prompt the user if required (unless host is trusted)
prompt := cache.ChecksumPrompt(checksum)
if prompt != "" {
if prompt != "" && !r.isTrusted(node.Location()) {
if err := func() error {
r.promptMutex.Lock()
defer r.promptMutex.Unlock()
Expand Down
26 changes: 26 additions & 0 deletions taskrc/ast/taskrc.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ type Remote struct {
Offline *bool `yaml:"offline"`
Timeout *time.Duration `yaml:"timeout"`
CacheExpiry *time.Duration `yaml:"cache-expiry"`
Trust []string `yaml:"trust"`
}

// Merge combines the current TaskRC with another TaskRC, prioritizing non-nil fields from the other TaskRC.
Expand All @@ -43,6 +44,31 @@ func (t *TaskRC) Merge(other *TaskRC) {
t.Remote.Timeout = cmp.Or(other.Remote.Timeout, t.Remote.Timeout)
t.Remote.CacheExpiry = cmp.Or(other.Remote.CacheExpiry, t.Remote.CacheExpiry)

// Merge Trust lists - combine both lists with other's entries taking precedence
// Remove duplicates by using a map
if len(other.Remote.Trust) > 0 {
seen := make(map[string]bool)
merged := []string{}

// Add other's hosts first
for _, host := range other.Remote.Trust {
if !seen[host] {
seen[host] = true
merged = append(merged, host)
}
}

// Then add base's hosts that aren't duplicates
for _, host := range t.Remote.Trust {
if !seen[host] {
seen[host] = true
merged = append(merged, host)
}
}

t.Remote.Trust = merged
}

Comment on lines +47 to +71
Copy link
Member

Choose a reason for hiding this comment

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

Since the order doesn’t matter (a host is trusted regardless of its position in the slice), we can simplify it to:

Suggested change
// Merge Trust lists - combine both lists with other's entries taking precedence
// Remove duplicates by using a map
if len(other.Remote.Trust) > 0 {
seen := make(map[string]bool)
merged := []string{}
// Add other's hosts first
for _, host := range other.Remote.Trust {
if !seen[host] {
seen[host] = true
merged = append(merged, host)
}
}
// Then add base's hosts that aren't duplicates
for _, host := range t.Remote.Trust {
if !seen[host] {
seen[host] = true
merged = append(merged, host)
}
}
t.Remote.Trust = merged
}
if len(other.Remote.Trust) > 0 {
merged := slices.Concat(other.Remote.Trust, t.Remote.Trust)
slices.Sort(merged)
t.Remote.Trust = slices.Compact(merged)
}

t.Verbose = cmp.Or(other.Verbose, t.Verbose)
t.Concurrency = cmp.Or(other.Concurrency, t.Concurrency)
}
Loading
Loading