Skip to content
Merged
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
TrustedHosts []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
}

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

type trustedHostsOption struct {
trustedHosts []string
}

func (o *trustedHostsOption) ApplyToExecutor(e *Executor) {
e.TrustedHosts = o.trustedHosts
}

// 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
TrustedHosts []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(&TrustedHosts, "trusted-hosts", config.Remote.TrustedHosts, "List of trusted hosts for remote Taskfiles (comma-separated).")
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.WithTrustedHosts(TrustedHosts),
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.WithTrustedHosts(e.TrustedHosts),
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 @@ -786,6 +787,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
Contributor 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 @@ -825,6 +831,23 @@ func TestIncludesRemote(t *testing.T) {
task.WithOffline(true),
),
},
{
name: "with trusted hosts, 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 trusted hosts
task.WithTrustedHosts([]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
trustedHosts []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,
trustedHosts: 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
}

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

type trustedHostsOption struct {
trustedHosts []string
}

func (o *trustedHostsOption) ApplyToReader(r *Reader) {
r.trustedHosts = o.trustedHosts
}

// 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.trustedHosts) == 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.trustedHosts {
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
16 changes: 12 additions & 4 deletions taskrc/ast/taskrc.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package ast
import (
"cmp"
"maps"
"slices"
"time"

"github.com/Masterminds/semver/v3"
Expand All @@ -17,10 +18,11 @@ type TaskRC struct {
}

type Remote struct {
Insecure *bool `yaml:"insecure"`
Offline *bool `yaml:"offline"`
Timeout *time.Duration `yaml:"timeout"`
CacheExpiry *time.Duration `yaml:"cache-expiry"`
Insecure *bool `yaml:"insecure"`
Offline *bool `yaml:"offline"`
Timeout *time.Duration `yaml:"timeout"`
CacheExpiry *time.Duration `yaml:"cache-expiry"`
TrustedHosts []string `yaml:"trusted-hosts"`
}

// Merge combines the current TaskRC with another TaskRC, prioritizing non-nil fields from the other TaskRC.
Expand All @@ -43,6 +45,12 @@ 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)

if len(other.Remote.TrustedHosts) > 0 {
merged := slices.Concat(other.Remote.TrustedHosts, t.Remote.TrustedHosts)
slices.Sort(merged)
t.Remote.TrustedHosts = slices.Compact(merged)
}

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