diff --git a/act/runner/reusable_workflow.go b/act/runner/reusable_workflow.go index e1109a4f..00e98027 100644 --- a/act/runner/reusable_workflow.go +++ b/act/runner/reusable_workflow.go @@ -305,30 +305,45 @@ func setReusedWorkflowCallerResult(rc *RunContext, runner Runner) common.Executo // getGitCloneToken returns GITEA_TOKEN when shouldCloneURLUseToken returns true, // otherwise returns an empty string func getGitCloneToken(conf *Config, cloneURL string) string { - if !shouldCloneURLUseToken(conf.GitHubInstance, cloneURL) { + if !shouldCloneURLUseToken(conf.GitHubInstance, conf.trustedActionInstance(), cloneURL) { return "" } return conf.GetToken() } // For Gitea -// shouldCloneURLUseToken returns true when the following conditions are met: -// 1. cloneURL is from the same Gitea instance that the runner is registered to -// 2. the cloneURL does not have basic auth embedded -func shouldCloneURLUseToken(instanceURL, cloneURL string) bool { - if !strings.HasPrefix(instanceURL, "http://") && - !strings.HasPrefix(instanceURL, "https://") { - instanceURL = "https://" + instanceURL +// trustedActionInstance returns the self-hosted DEFAULT_ACTIONS_URL host that may carry the +// task token, or "" when actions resolve to github.com / a GithubMirror (never trusted). +func (c Config) trustedActionInstance() string { + if c.DefaultActionInstanceIsSelfHosted { + return c.DefaultActionInstance } - - u1, err1 := url.Parse(instanceURL) - u2, err2 := url.Parse(cloneURL) - if err1 != nil || err2 != nil { - return false - } - if u2.User != nil { - return false - } - - return u1.Host == u2.Host + return "" +} + +// For Gitea +// shouldCloneURLUseToken returns true when the following conditions are met: +// 1. cloneURL's host matches this Gitea instance: either the registered instance +// (instanceURL) or, for DEFAULT_ACTIONS_URL=self on a different hostname, the +// self-hosted action instance (trustedActionInstance, "" when not trusted) +// 2. the cloneURL does not have basic auth embedded +func shouldCloneURLUseToken(instanceURL, trustedActionInstance, cloneURL string) bool { + u2, err := url.Parse(cloneURL) + if err != nil || u2.User != nil { + return false + } + + for _, candidate := range []string{instanceURL, trustedActionInstance} { + if candidate == "" { + continue + } + if !strings.HasPrefix(candidate, "http://") && + !strings.HasPrefix(candidate, "https://") { + candidate = "https://" + candidate + } + if u1, err := url.Parse(candidate); err == nil && u1.Host == u2.Host { + return true + } + } + return false } diff --git a/act/runner/reusable_workflow_test.go b/act/runner/reusable_workflow_test.go index 4e5d074c..401ee39d 100644 --- a/act/runner/reusable_workflow_test.go +++ b/act/runner/reusable_workflow_test.go @@ -136,12 +136,30 @@ func TestGetGitCloneTokenWithSchemalessGiteaInstance(t *testing.T) { require.Equal(t, "token-value", token) } +func TestGetGitCloneTokenSelfHostedActionsDifferentHost(t *testing.T) { + // The runner registered with one hostname while DEFAULT_ACTIONS_URL=self resolves + // actions against AppURL on a different hostname for the same instance. + conf := &Config{ + GitHubInstance: "gitea.local", + DefaultActionInstance: "https://gitea.my-nas.lan", + DefaultActionInstanceIsSelfHosted: true, + Secrets: map[string]string{ + "GITEA_TOKEN": "token-value", + }, + } + + token := getGitCloneToken(conf, "https://gitea.my-nas.lan/owner/action") + + require.Equal(t, "token-value", token) +} + func TestShouldCloneURLUseToken(t *testing.T) { tests := []struct { - name string - instanceURL string - cloneURL string - want bool + name string + instanceURL string + trustedActionInstance string + cloneURL string + want bool }{ { name: "same host with schemaless instance", @@ -173,11 +191,37 @@ func TestShouldCloneURLUseToken(t *testing.T) { cloneURL: "://gitea.example.net/actions/tools", want: false, }, + { + // self-hosted DEFAULT_ACTIONS_URL on a different hostname than the + // registered instance: the token must still be attached. + name: "self-hosted action instance on different host", + instanceURL: "gitea.local", + trustedActionInstance: "https://gitea.my-nas.lan", + cloneURL: "https://gitea.my-nas.lan/owner/action", + want: true, + }, + { + // embedded basic auth must still be rejected even when the host matches + // the trusted action instance. + name: "self-hosted action instance with embedded basic auth", + instanceURL: "gitea.local", + trustedActionInstance: "https://gitea.my-nas.lan", + cloneURL: "https://user:pass@gitea.my-nas.lan/owner/action", + want: false, + }, + { + // github.com / mirror hosts are never trusted: trustedActionInstance is + // empty in github mode, so an off-instance clone URL gets no token. + name: "github mode does not trust mirror host", + instanceURL: "gitea.local", + cloneURL: "https://mirror.example.com/owner/action", + want: false, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - require.Equal(t, tt.want, shouldCloneURLUseToken(tt.instanceURL, tt.cloneURL)) + require.Equal(t, tt.want, shouldCloneURLUseToken(tt.instanceURL, tt.trustedActionInstance, tt.cloneURL)) }) } } diff --git a/act/runner/runner.go b/act/runner/runner.go index 2e6eb5d7..b8a12cde 100644 --- a/act/runner/runner.go +++ b/act/runner/runner.go @@ -73,18 +73,24 @@ type Config struct { ContainerNetworkCreateOptions container.NewDockerNetworkCreateExecutorInput // the default network create options ActionCache ActionCache // Use a custom ActionCache Implementation - PresetGitHubContext *model.GithubContext // the preset github context, overrides some fields like DefaultBranch, Env, Secrets etc. - EventJSON string // the content of JSON file to use for event.json in containers, overrides EventPath - ContainerNamePrefix string // the prefix of container name - ContainerMaxLifetime time.Duration // the max lifetime of job containers - CleanWorkdir bool // remove host executor workdir on teardown - DefaultActionInstance string // the default actions web site - PlatformPicker func(labels []string) string // platform picker, it will take precedence over Platforms if isn't nil - JobLoggerLevel *log.Level // the level of job logger - ValidVolumes []string // only volumes (and bind mounts) in this slice can be mounted on the job container or service containers - InsecureSkipTLS bool // whether to skip verifying TLS certificate of the Gitea instance - MaxParallel int // max parallel jobs to run across all workflows (0 = no limit, uses CPU count) - AllocatePTY bool // allocate a pseudo-TTY for each step's process + PresetGitHubContext *model.GithubContext // the preset github context, overrides some fields like DefaultBranch, Env, Secrets etc. + EventJSON string // the content of JSON file to use for event.json in containers, overrides EventPath + ContainerNamePrefix string // the prefix of container name + ContainerMaxLifetime time.Duration // the max lifetime of job containers + CleanWorkdir bool // remove host executor workdir on teardown + DefaultActionInstance string // the default actions web site + // DefaultActionInstanceIsSelfHosted reports whether DefaultActionInstance is this + // self-hosted Gitea (DEFAULT_ACTIONS_URL=self). It gates token trust: only then may the + // task token be attached to action clone URLs on DefaultActionInstance's host, which can + // differ from GitHubInstance when the runner registered with a different hostname than + // AppURL. It is never set for github.com or a GithubMirror, so the token stays on-instance. + DefaultActionInstanceIsSelfHosted bool + PlatformPicker func(labels []string) string // platform picker, it will take precedence over Platforms if isn't nil + JobLoggerLevel *log.Level // the level of job logger + ValidVolumes []string // only volumes (and bind mounts) in this slice can be mounted on the job container or service containers + InsecureSkipTLS bool // whether to skip verifying TLS certificate of the Gitea instance + MaxParallel int // max parallel jobs to run across all workflows (0 = no limit, uses CPU count) + AllocatePTY bool // allocate a pseudo-TTY for each step's process } // GetToken: Adapt to Gitea diff --git a/act/runner/step_action_remote.go b/act/runner/step_action_remote.go index 4f6dff1c..b61276a0 100644 --- a/act/runner/step_action_remote.go +++ b/act/runner/step_action_remote.go @@ -121,7 +121,7 @@ func (sar *stepActionRemote) prepareActionExecutor() common.Executor { // top-level token; keep the shouldCloneURLUseToken host gate to avoid leaking it. cloneURL := sar.remoteAction.CloneURL(defaultActionURL) token := "" - if shouldCloneURLUseToken(sar.RunContext.Config.GitHubInstance, cloneURL) { + if shouldCloneURLUseToken(sar.RunContext.Config.GitHubInstance, sar.RunContext.Config.trustedActionInstance(), cloneURL) { token = github.Token } gitClone := stepActionRemoteNewCloneExecutor(git.NewGitCloneExecutorInput{ diff --git a/internal/app/cmd/exec.go b/internal/app/cmd/exec.go index fb6dc856..053930bd 100644 --- a/internal/app/cmd/exec.go +++ b/internal/app/cmd/exec.go @@ -443,10 +443,11 @@ func runExec(ctx context.Context, execArgs *executeArgs) func(cmd *cobra.Command NoSkipCheckout: execArgs.noSkipCheckout, // PresetGitHubContext: preset, // EventJSON: string(eventJSON), - ContainerNamePrefix: "GITEA-ACTIONS-TASK-" + eventName, - ContainerMaxLifetime: maxLifetime, - ContainerNetworkMode: container.NetworkMode(execArgs.network), - DefaultActionInstance: execArgs.defaultActionsURL, + ContainerNamePrefix: "GITEA-ACTIONS-TASK-" + eventName, + ContainerMaxLifetime: maxLifetime, + ContainerNetworkMode: container.NetworkMode(execArgs.network), + DefaultActionInstance: execArgs.defaultActionsURL, + DefaultActionInstanceIsSelfHosted: execArgs.defaultActionsURL != "" && execArgs.defaultActionsURL != "https://github.com", PlatformPicker: func(_ []string) string { return execArgs.image }, diff --git a/internal/app/run/runner.go b/internal/app/run/runner.go index b7b456df..a751c2de 100644 --- a/internal/app/run/runner.go +++ b/internal/app/run/runner.go @@ -299,6 +299,15 @@ func (r *Runner) getDefaultActionsURL(task *runnerv1.Task) string { return giteaDefaultActionsURL } +// isSelfHostedActionsURL reports whether actions resolve to this self-hosted Gitea +// (DEFAULT_ACTIONS_URL=self), i.e. gitea_default_actions_url is AppURL rather than +// github.com (which may be mirror-substituted by getDefaultActionsURL). Only then may the +// task token be attached to action clone URLs on the actions instance host. +func (r *Runner) isSelfHostedActionsURL(task *runnerv1.Task) bool { + giteaDefaultActionsURL := task.Context.Fields["gitea_default_actions_url"].GetStringValue() + return giteaDefaultActionsURL != "" && giteaDefaultActionsURL != "https://github.com" +} + func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report.Reporter) (err error) { defer func() { if r := recover(); r != nil { @@ -446,14 +455,15 @@ func (r *Runner) run(ctx context.Context, task *runnerv1.Task, reporter *report. EnableIPv4: r.cfg.Container.NetworkCreateOptions.EnableIPv4, EnableIPv6: r.cfg.Container.NetworkCreateOptions.EnableIPv6, }, - ContainerOptions: r.cfg.Container.Options, - ContainerDaemonSocket: r.cfg.Container.DockerHost, - Privileged: r.cfg.Container.Privileged, - DefaultActionInstance: r.getDefaultActionsURL(task), - PlatformPicker: r.labels.PickPlatform, - Vars: task.Vars, - ValidVolumes: r.cfg.Container.ValidVolumes, - InsecureSkipTLS: r.cfg.Runner.Insecure, + ContainerOptions: r.cfg.Container.Options, + ContainerDaemonSocket: r.cfg.Container.DockerHost, + Privileged: r.cfg.Container.Privileged, + DefaultActionInstance: r.getDefaultActionsURL(task), + DefaultActionInstanceIsSelfHosted: r.isSelfHostedActionsURL(task), + PlatformPicker: r.labels.PickPlatform, + Vars: task.Vars, + ValidVolumes: r.cfg.Container.ValidVolumes, + InsecureSkipTLS: r.cfg.Runner.Insecure, } rr, err := runner.New(runnerConfig)