From 3a1668071323a59f5c67eb4c635b10bbeeae10dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20L=2E=20Hansen?= Date: Mon, 3 Jul 2023 17:14:07 -0700 Subject: [PATCH 01/23] Refactor token signing method in OAuth2 service --- services/auth/source/oauth2/token.go | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/services/auth/source/oauth2/token.go b/services/auth/source/oauth2/token.go index 3405619d3fa59..fda27b777fc3e 100644 --- a/services/auth/source/oauth2/token.go +++ b/services/auth/source/oauth2/token.go @@ -64,9 +64,7 @@ func ParseToken(jwtToken string, signingKey JWTSigningKey) (*Token, error) { // SignToken signs the token with the JWT secret func (token *Token) SignToken(signingKey JWTSigningKey) (string, error) { token.IssuedAt = jwt.NewNumericDate(time.Now()) - jwtToken := jwt.NewWithClaims(signingKey.SigningMethod(), token) - signingKey.PreProcessToken(jwtToken) - return jwtToken.SignedString(signingKey.SignKey()) + return SignToken(token, signingKey) } // OIDCToken represents an OpenID Connect id_token @@ -94,6 +92,10 @@ type OIDCToken struct { // SignToken signs an id_token with the (symmetric) client secret key func (token *OIDCToken) SignToken(signingKey JWTSigningKey) (string, error) { token.IssuedAt = jwt.NewNumericDate(time.Now()) + return SignToken(token, signingKey) +} + +func SignToken(token jwt.Claims, signingKey JWTSigningKey) (string, error) { jwtToken := jwt.NewWithClaims(signingKey.SigningMethod(), token) signingKey.PreProcessToken(jwtToken) return jwtToken.SignedString(signingKey.SignKey()) From 31e8ad3c04d465a4ae8880dfe512e473aee36e88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20L=2E=20Hansen?= Date: Sun, 6 Aug 2023 14:12:42 -0700 Subject: [PATCH 02/23] feat: Add basic permissions support for actions This depends on a few changes in act and act_runner: https://gitea.com/gitea/act_runner/pulls/272 https://gitea.com/gitea/act/pulls/73 --- go.mod | 2 +- go.sum | 6 +- models/actions/permissions.go | 217 ++++++++++++++++++++++++++++ models/actions/run.go | 16 +- models/actions/run_job.go | 11 +- models/migrations/migrations.go | 2 + models/migrations/v1_21/v276.go | 18 +++ services/actions/notifier_helper.go | 7 + 8 files changed, 269 insertions(+), 10 deletions(-) create mode 100644 models/actions/permissions.go create mode 100644 models/migrations/v1_21/v276.go diff --git a/go.mod b/go.mod index a6e83df05a720..1da5dc444f410 100644 --- a/go.mod +++ b/go.mod @@ -303,7 +303,7 @@ replace github.com/hashicorp/go-version => github.com/6543/go-version v1.3.1 replace github.com/shurcooL/vfsgen => github.com/lunny/vfsgen v0.0.0-20220105142115-2c99e1ffdfa0 -replace github.com/nektos/act => gitea.com/gitea/act v0.243.4 +replace github.com/nektos/act => gitea.com/sorenisanerd/act v0.246.2-0.20230806181409-a9e947b70bf6 exclude github.com/gofrs/uuid v3.2.0+incompatible diff --git a/go.sum b/go.sum index 5e32daf0c14de..0e2e4d69b11ae 100644 --- a/go.sum +++ b/go.sum @@ -51,11 +51,11 @@ codeberg.org/gusted/mcaptcha v0.0.0-20220723083913-4f3072e1d570 h1:TXbikPqa7YRtf codeberg.org/gusted/mcaptcha v0.0.0-20220723083913-4f3072e1d570/go.mod h1:IIAjsijsd8q1isWX8MACefDEgTQslQ4stk2AeeTt3kM= dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078 h1:cliQ4HHsCo6xi2oWZYKWW4bly/Ory9FuTpFPRxj/mAg= git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078/go.mod h1:g/V2Hjas6Z1UHUp4yIx6bATpNzJ7DYtD0FG3+xARWxs= -gitea.com/gitea/act v0.243.4 h1:MuBHBLCJfpa6mzwwvs4xqQynrSP2RRzpHpWfTV16PmI= -gitea.com/gitea/act v0.243.4/go.mod h1:mabw6AZAiDgxGlK83orWLrNERSPvgBJzEUS3S7u2bHI= gitea.com/go-chi/binding v0.0.0-20230415142243-04b515c6d669 h1:RUBX+MK/TsDxpHmymaOaydfigEbbzqUnG1OTZU/HAeo= gitea.com/go-chi/binding v0.0.0-20230415142243-04b515c6d669/go.mod h1:77TZu701zMXWJFvB8gvTbQ92zQ3DQq/H7l5wAEjQRKc= gitea.com/go-chi/cache v0.0.0-20210110083709-82c4c9ce2d5e/go.mod h1:k2V/gPDEtXGjjMGuBJiapffAXTv76H4snSmlJRLUhH0= @@ -69,6 +69,8 @@ gitea.com/lunny/dingtalk_webhook v0.0.0-20171025031554-e3534c89ef96 h1:+wWBi6Qfr gitea.com/lunny/dingtalk_webhook v0.0.0-20171025031554-e3534c89ef96/go.mod h1:VyMQP6ue6MKHM8UsOXfNfuMKD0oSAWZdXVcpHIN2yaY= gitea.com/lunny/levelqueue v0.4.2-0.20230414023320-3c0159fe0fe4 h1:IFT+hup2xejHqdhS7keYWioqfmxdnfblFDTGoOwcZ+o= gitea.com/lunny/levelqueue v0.4.2-0.20230414023320-3c0159fe0fe4/go.mod h1:HBqmLbz56JWpfEGG0prskAV97ATNRoj5LDmPicD22hU= +gitea.com/sorenisanerd/act v0.246.2-0.20230806181409-a9e947b70bf6 h1:ANNwt5ZqFG7FhDjdwCfsfoi7zlEV7uAfbrYTV5R8CNg= +gitea.com/sorenisanerd/act v0.246.2-0.20230806181409-a9e947b70bf6/go.mod h1:tfannUyz3cgmq1P1o69KW1AMB1aSlNOMzlswHkRjzcQ= gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:lSA0F4e9A2NcQSqGqTOXqu2aRi/XEQxDCBwM8yJtE6s= gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:EXuID2Zs0pAQhH8yz+DNjUbjppKQzKFAn28TMYPB6IU= gitee.com/travelliu/dm v1.8.11192/go.mod h1:DHTzyhCrM843x9VdKVbZ+GKXGRbKM2sJ4LxihRxShkE= diff --git a/models/actions/permissions.go b/models/actions/permissions.go new file mode 100644 index 0000000000000..a5c0058a3b8ea --- /dev/null +++ b/models/actions/permissions.go @@ -0,0 +1,217 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package actions + +import ( + "errors" + "fmt" + + "gopkg.in/yaml.v3" +) + +type Permission int + +const ( + PermissionUnspecified Permission = iota + PermissionNone + PermissionRead + PermissionWrite +) + +// Per https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idpermissions +type Permissions struct { + Actions Permission `yaml:"actions"` + Checks Permission `yaml:"checks"` + Contents Permission `yaml:"contents"` + Deployments Permission `yaml:"deployments"` + IDToken Permission `yaml:"id-token"` + Issues Permission `yaml:"issues"` + Discussions Permission `yaml:"discussions"` + Packages Permission `yaml:"packages"` + Pages Permission `yaml:"pages"` + PullRequests Permission `yaml:"pull-requests"` + RepositoryProjects Permission `yaml:"repository-projects"` + SecurityEvents Permission `yaml:"security-events"` + Statuses Permission `yaml:"statuses"` +} + +// WorkflowPermissions parses a workflow and returns +// a Permissions struct representing the permissions set +// at the workflow (i.e. file) level +func WorkflowPermissions(contents []byte) (Permissions, error) { + p := struct { + Permissions Permissions `yaml:"permissions"` + }{} + err := yaml.Unmarshal(contents, &p) + return p.Permissions, err +} + +// Given the contents of a workflow, JobPermissions +// returns a Permissions object representing the permissions +// of THE FIRST job in the file. +func JobPermissions(contents []byte) (Permissions, error) { + p := struct { + Jobs []struct { + Permissions Permissions `yaml:"permissions"` + } `yaml:"jobs"` + }{} + err := yaml.Unmarshal(contents, &p) + if len(p.Jobs) > 0 { + return p.Jobs[0].Permissions, err + } + return Permissions{}, errors.New("no jobs detected in workflow") +} + +func (p *Permission) UnmarshalYAML(unmarshal func(interface{}) error) error { + var data string + if err := unmarshal(&data); err != nil { + return err + } + + switch data { + case "none": + *p = PermissionNone + case "read": + *p = PermissionRead + case "write": + *p = PermissionWrite + default: + return fmt.Errorf("invalid permission: %s", data) + } + + return nil +} + +// DefaultAccessPermissive is the default "permissive" set granted to actions on repositories +// per https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token +// That page also lists a "metadata" permission that I can't find mentioned anywhere else. +// However, it seems to always have "read" permission, so it doesn't really matter. +// Interestingly, it doesn't list "Discussions", so we assume "write" for permissive and "none" for restricted. +var DefaultAccessPermissive = Permissions{ + Actions: PermissionWrite, + Checks: PermissionWrite, + Contents: PermissionWrite, + Deployments: PermissionWrite, + IDToken: PermissionNone, + Issues: PermissionWrite, + Discussions: PermissionWrite, + Packages: PermissionWrite, + Pages: PermissionWrite, + PullRequests: PermissionWrite, + RepositoryProjects: PermissionWrite, + SecurityEvents: PermissionWrite, + Statuses: PermissionWrite, +} + +// DefaultAccessRestricted is the default "restrictive" set granted. See docs for +// DefaultAccessPermissive above. +// +// This is not currently used, since Gitea does not have a permissive/restricted setting. +var DefaultAccessRestricted = Permissions{ + Actions: PermissionNone, + Checks: PermissionNone, + Contents: PermissionWrite, + Deployments: PermissionNone, + IDToken: PermissionNone, + Issues: PermissionNone, + Discussions: PermissionNone, + Packages: PermissionRead, + Pages: PermissionNone, + PullRequests: PermissionNone, + RepositoryProjects: PermissionNone, + SecurityEvents: PermissionNone, + Statuses: PermissionNone, +} + +var ReadAllPermissions = Permissions{ + Actions: PermissionRead, + Checks: PermissionRead, + Contents: PermissionRead, + Deployments: PermissionRead, + IDToken: PermissionRead, + Issues: PermissionRead, + Discussions: PermissionRead, + Packages: PermissionRead, + Pages: PermissionRead, + PullRequests: PermissionRead, + RepositoryProjects: PermissionRead, + SecurityEvents: PermissionRead, + Statuses: PermissionRead, +} + +var WriteAllPermissions = Permissions{ + Actions: PermissionWrite, + Checks: PermissionWrite, + Contents: PermissionWrite, + Deployments: PermissionWrite, + IDToken: PermissionWrite, + Issues: PermissionWrite, + Discussions: PermissionWrite, + Packages: PermissionWrite, + Pages: PermissionWrite, + PullRequests: PermissionWrite, + RepositoryProjects: PermissionWrite, + SecurityEvents: PermissionWrite, + Statuses: PermissionWrite, +} + +// FromYAML takes a yaml.Node representing a permissions +// definition and parses it into a Permissions struct +func (p *Permissions) FromYAML(rawPermissions *yaml.Node) error { + switch rawPermissions.Kind { + case yaml.ScalarNode: + var val string + err := rawPermissions.Decode(&val) + if err != nil { + return err + } + if val == "read-all" { + *p = ReadAllPermissions + } + if val == "write-all" { + *p = WriteAllPermissions + } + return fmt.Errorf("unexpected `permissions` value: %v", rawPermissions) + case yaml.MappingNode: + var perms Permissions + err := rawPermissions.Decode(&perms) + if err != nil { + return err + } + return nil + case 0: + *p = Permissions{} + return nil + default: + return fmt.Errorf("invalid permissions value: %v", rawPermissions) + } +} + +func merge[T comparable](a, b T) T { + var zero T + if a == zero { + return b + } + return a +} + +// Merge merges two Permission values +// +// Already set values take precedence over `other`. +// I.e. you want to call jobLevel.Permissions.Merge(topLevel.Permissions) +func (p *Permissions) Merge(other Permissions) { + p.Actions = merge(p.Actions, other.Actions) + p.Checks = merge(p.Checks, other.Checks) + p.Contents = merge(p.Contents, other.Contents) + p.Deployments = merge(p.Deployments, other.Deployments) + p.IDToken = merge(p.IDToken, other.IDToken) + p.Issues = merge(p.Issues, other.Issues) + p.Discussions = merge(p.Discussions, other.Discussions) + p.Packages = merge(p.Packages, other.Packages) + p.Pages = merge(p.Pages, other.Pages) + p.PullRequests = merge(p.PullRequests, other.PullRequests) + p.RepositoryProjects = merge(p.RepositoryProjects, other.RepositoryProjects) + p.SecurityEvents = merge(p.SecurityEvents, other.SecurityEvents) + p.Statuses = merge(p.Statuses, other.Statuses) +} diff --git a/models/actions/run.go b/models/actions/run.go index 8078613fb8f5b..568150870e180 100644 --- a/models/actions/run.go +++ b/models/actions/run.go @@ -45,6 +45,7 @@ type ActionRun struct { EventPayload string `xorm:"LONGTEXT"` TriggerEvent string // the trigger event defined in the `on` configuration of the triggered workflow Status Status `xorm:"index"` + Permissions Permissions `xorm:"-"` Version int `xorm:"version default 0"` // Status could be updated concomitantly, so an optimistic lock is needed Started timeutil.TimeStamp Stopped timeutil.TimeStamp @@ -280,7 +281,7 @@ func InsertRun(ctx context.Context, run *ActionRun, jobs []*jobparser.SingleWork hasWaiting = true } job.Name, _ = util.SplitStringAtByteN(job.Name, 255) - runJobs = append(runJobs, &ActionRunJob{ + runJob := &ActionRunJob{ RunID: run.ID, RepoID: run.RepoID, OwnerID: run.OwnerID, @@ -292,7 +293,18 @@ func InsertRun(ctx context.Context, run *ActionRun, jobs []*jobparser.SingleWork Needs: needs, RunsOn: job.RunsOn(), Status: status, - }) + } + + // Parse the job's permissions + if err := job.RawPermissions.Decode(&runJob.Permissions); err != nil { + return err + } + + // Merge the job's permissions with the workflow permissions. + // Job permissions take precedence. + runJob.Permissions.Merge(run.Permissions) + + runJobs = append(runJobs, runJob) } if err := db.Insert(ctx, runJobs); err != nil { return err diff --git a/models/actions/run_job.go b/models/actions/run_job.go index 4b8664077dca9..c1e65d42cd1e5 100644 --- a/models/actions/run_job.go +++ b/models/actions/run_job.go @@ -28,11 +28,12 @@ type ActionRunJob struct { Name string `xorm:"VARCHAR(255)"` Attempt int64 WorkflowPayload []byte - JobID string `xorm:"VARCHAR(255)"` // job id in workflow, not job's id - Needs []string `xorm:"JSON TEXT"` - RunsOn []string `xorm:"JSON TEXT"` - TaskID int64 // the latest task of the job - Status Status `xorm:"index"` + JobID string `xorm:"VARCHAR(255)"` // job id in workflow, not job's id + Needs []string `xorm:"JSON TEXT"` + RunsOn []string `xorm:"JSON TEXT"` + Permissions Permissions `xorm:"JSON TEXT"` + TaskID int64 // the latest task of the job + Status Status `xorm:"index"` Started timeutil.TimeStamp Stopped timeutil.TimeStamp Created timeutil.TimeStamp `xorm:"created"` diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index f0a8b05d5337d..1c49d5072dfb7 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -532,6 +532,8 @@ var migrations = []Migration{ NewMigration("Add Actions artifacts expiration date", v1_21.AddExpiredUnixColumnInActionArtifactTable), // v275 -> v276 NewMigration("Add ScheduleID for ActionRun", v1_21.AddScheduleIDForActionRun), + // v276 -> v277 + NewMigration("Add Permissions to Actions Task", v1_21.AddPermissions), } // GetCurrentDBVersion returns the current db version diff --git a/models/migrations/v1_21/v276.go b/models/migrations/v1_21/v276.go new file mode 100644 index 0000000000000..08f0a16a44cc9 --- /dev/null +++ b/models/migrations/v1_21/v276.go @@ -0,0 +1,18 @@ +// Copyright 2023 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_21 //nolint + +import ( + actions_model "code.gitea.io/gitea/models/actions" + + "xorm.io/xorm" +) + +func AddPermissions(x *xorm.Engine) error { + type ActionRunJob struct { + Permissions actions_model.Permissions `xorm:"JSON TEXT"` + } + + return x.Sync(new(ActionRunJob)) +} diff --git a/services/actions/notifier_helper.go b/services/actions/notifier_helper.go index ff00e48c644d1..94deaeb4efb8a 100644 --- a/services/actions/notifier_helper.go +++ b/services/actions/notifier_helper.go @@ -268,6 +268,13 @@ func handleWorkflows( } } + wp, err := actions_model.WorkflowPermissions(dwf.Content) + if err != nil { + log.Error("WorkflowPermissions: %v", err) + continue + } + run.Permissions = wp + if err := actions_model.InsertRun(ctx, run, jobs); err != nil { log.Error("InsertRun: %v", err) continue From 8271be528b5dead2dfcb853cfda0b41fb04c02c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20L=2E=20Hansen?= Date: Sun, 6 Aug 2023 14:17:14 -0700 Subject: [PATCH 03/23] Extract {base_,}{sha,ref} and EventName logic --- models/actions/run.go | 32 +++++++++++++++++++++++++++++ routers/api/actions/runner/utils.go | 27 ++---------------------- 2 files changed, 34 insertions(+), 25 deletions(-) diff --git a/models/actions/run.go b/models/actions/run.go index 568150870e180..40370b7642295 100644 --- a/models/actions/run.go +++ b/models/actions/run.go @@ -72,6 +72,38 @@ func (run *ActionRun) Link() string { return fmt.Sprintf("%s/actions/runs/%d", run.Repo.Link(), run.Index) } +func (run *ActionRun) RefShaBaseRefAndHeadRef() (string, string, string, string) { + var ref, sha, baseRef, headRef string + + ref = run.Ref + sha = run.CommitSHA + + if pullPayload, err := run.GetPullRequestEventPayload(); err == nil && pullPayload.PullRequest != nil && pullPayload.PullRequest.Base != nil && pullPayload.PullRequest.Head != nil { + baseRef = pullPayload.PullRequest.Base.Ref + headRef = pullPayload.PullRequest.Head.Ref + + // if the TriggerEvent is pull_request_target, ref and sha need to be set according to the base of pull request + // In GitHub's documentation, ref should be the branch or tag that triggered workflow. But when the TriggerEvent is pull_request_target, + // the ref will be the base branch. + if run.TriggerEvent == "pull_request_target" { + ref = git.BranchPrefix + pullPayload.PullRequest.Base.Name + sha = pullPayload.PullRequest.Base.Sha + } + } + return ref, sha, baseRef, headRef +} + +func (run *ActionRun) EventName() string { + // TriggerEvent is added in https://github.com/go-gitea/gitea/pull/25229 + // This fallback is for the old ActionRun that doesn't have the TriggerEvent field + // and should be removed in 1.22 + eventName := run.TriggerEvent + if eventName == "" { + eventName = run.Event.Event() + } + return eventName +} + // RefLink return the url of run's ref func (run *ActionRun) RefLink() string { refName := git.RefName(run.Ref) diff --git a/routers/api/actions/runner/utils.go b/routers/api/actions/runner/utils.go index 24432ab6b202d..1cfe0a4435b53 100644 --- a/routers/api/actions/runner/utils.go +++ b/routers/api/actions/runner/utils.go @@ -117,31 +117,8 @@ func generateTaskContext(t *actions_model.ActionTask) *structpb.Struct { event := map[string]any{} _ = json.Unmarshal([]byte(t.Job.Run.EventPayload), &event) - // TriggerEvent is added in https://github.com/go-gitea/gitea/pull/25229 - // This fallback is for the old ActionRun that doesn't have the TriggerEvent field - // and should be removed in 1.22 - eventName := t.Job.Run.TriggerEvent - if eventName == "" { - eventName = t.Job.Run.Event.Event() - } - - baseRef := "" - headRef := "" - ref := t.Job.Run.Ref - sha := t.Job.Run.CommitSHA - if pullPayload, err := t.Job.Run.GetPullRequestEventPayload(); err == nil && pullPayload.PullRequest != nil && pullPayload.PullRequest.Base != nil && pullPayload.PullRequest.Head != nil { - baseRef = pullPayload.PullRequest.Base.Ref - headRef = pullPayload.PullRequest.Head.Ref - - // if the TriggerEvent is pull_request_target, ref and sha need to be set according to the base of pull request - // In GitHub's documentation, ref should be the branch or tag that triggered workflow. But when the TriggerEvent is pull_request_target, - // the ref will be the base branch. - if t.Job.Run.TriggerEvent == actions_module.GithubEventPullRequestTarget { - ref = git.BranchPrefix + pullPayload.PullRequest.Base.Name - sha = pullPayload.PullRequest.Base.Sha - } - } - + eventName := t.Job.Run.EventName() + ref, sha, baseRef, headRef := t.Job.Run.RefShaBaseRefAndHeadRef() refName := git.RefName(ref) taskContext, err := structpb.NewStruct(map[string]any{ From 724e138bd865b8fef1117a830ec05fb3ac0fc22f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20L=2E=20Hansen?= Date: Sun, 6 Aug 2023 14:19:42 -0700 Subject: [PATCH 04/23] feat: Add OIDC provider for actions --- models/actions/run_job.go | 4 + routers/api/actions/runner/utils.go | 15 ++- routers/api/v1/api.go | 2 + routers/api/v1/oidc.go | 154 ++++++++++++++++++++++++++++ services/auth/oauth2.go | 1 + 5 files changed, 174 insertions(+), 2 deletions(-) create mode 100644 routers/api/v1/oidc.go diff --git a/models/actions/run_job.go b/models/actions/run_job.go index c1e65d42cd1e5..c4ff88e92c018 100644 --- a/models/actions/run_job.go +++ b/models/actions/run_job.go @@ -72,6 +72,10 @@ func (job *ActionRunJob) LoadAttributes(ctx context.Context) error { return job.Run.LoadAttributes(ctx) } +func (job *ActionRunJob) MayCreateIDToken() bool { + return job.Permissions.IDToken == PermissionWrite +} + func GetRunJobByID(ctx context.Context, id int64) (*ActionRunJob, error) { var job ActionRunJob has, err := db.GetEngine(ctx).Where("id=?", id).Get(&job) diff --git a/routers/api/actions/runner/utils.go b/routers/api/actions/runner/utils.go index 1cfe0a4435b53..cd6003d4997d8 100644 --- a/routers/api/actions/runner/utils.go +++ b/routers/api/actions/runner/utils.go @@ -121,7 +121,7 @@ func generateTaskContext(t *actions_model.ActionTask) *structpb.Struct { ref, sha, baseRef, headRef := t.Job.Run.RefShaBaseRefAndHeadRef() refName := git.RefName(ref) - taskContext, err := structpb.NewStruct(map[string]any{ + contextMap := map[string]any{ // standard contexts, see https://docs.github.com/en/actions/learn-github-actions/contexts#github-context "action": "", // string, The name of the action currently running, or the id of a step. GitHub removes special characters, and uses the name __run when the current step runs a script without an id. If you use the same action more than once in the same job, the name will include a suffix with the sequence number with underscore before it. For example, the first script you run will have the name __run, and the second script will be named __run_2. Similarly, the second invocation of actions/checkout will be actionscheckout2. "action_path": "", // string, The path where an action is located. This property is only supported in composite actions. You can use this path to access files located in the same repository as the action. @@ -160,7 +160,18 @@ func generateTaskContext(t *actions_model.ActionTask) *structpb.Struct { // additional contexts "gitea_default_actions_url": setting.Actions.DefaultActionsURL.URL(), - }) + } + + if t.Job.MayCreateIDToken() { + // The "a=1" is a dummy variable. If an audience is passed to + // github/core.js's getIdToken(), it appends it to the URL as "&audience=". + // If the URL doesn't at least have a '?', the "&audience=" part will be + // interpreted as part of the path. + contextMap["actions_id_token_request_url"] = fmt.Sprintf("%sapi/v1/actions/id-token/request?a=1", setting.AppURL) + contextMap["actions_id_token_request_token"] = t.Token + } + + taskContext, err := structpb.NewStruct(contextMap) if err != nil { log.Error("structpb.NewStruct failed: %v", err) } diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index ca74a23a4b89e..3d40e43ed6528 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -1020,6 +1020,8 @@ func Routes() *web.Route { }, reqToken()) }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser), reqToken()) + m.Get("/actions/id-token/request", generateOIDCToken) + // Repositories (requires repo scope, org scope) m.Post("/org/{org}/repos", tokenRequiresScopes(auth_model.AccessTokenScopeCategoryOrganization, auth_model.AccessTokenScopeCategoryRepository), diff --git a/routers/api/v1/oidc.go b/routers/api/v1/oidc.go new file mode 100644 index 0000000000000..13a014dd7df6b --- /dev/null +++ b/routers/api/v1/oidc.go @@ -0,0 +1,154 @@ +// Copyright 2016 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +// OIDC provider for Gitea Actions +package v1 + +import ( + "fmt" + "net/http" + + actions_model "code.gitea.io/gitea/models/actions" + "code.gitea.io/gitea/modules/context" + "code.gitea.io/gitea/modules/git" + "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/timeutil" + auth_service "code.gitea.io/gitea/services/auth" + "code.gitea.io/gitea/services/auth/source/oauth2" + + "github.com/golang-jwt/jwt/v5" +) + +type IDTokenResponse struct { + Value string `json:"value"` + Count int `json:"count"` +} + +type IDTokenErrorResponse struct { + ErrorDescription string `json:"error_description"` +} + +type IDToken struct { + jwt.RegisteredClaims + + Ref string `json:"ref,omitempty"` + SHA string `json:"sha,omitempty"` + Repository string `json:"repository,omitempty"` + RepositoryOwner string `json:"repository_owner,omitempty"` + RepositoryOwnerID int `json:"repository_owner_id,omitempty"` + RunID int `json:"run_id,omitempty"` + RunNumber int `json:"run_number,omitempty"` + RunAttempt int `json:"run_attempt,omitempty"` + RepositoryVisibility string `json:"repository_visibility,omitempty"` + RepositoryID int `json:"repository_id,omitempty"` + ActorID int `json:"actor_id,omitempty"` + Actor string `json:"actor,omitempty"` + Workflow string `json:"workflow,omitempty"` + EventName string `json:"event_name,omitempty"` + RefType string `json:"ref_type,omitempty"` + HeadRef string `json:"head_ref,omitempty"` + BaseRef string `json:"base_ref,omitempty"` + + // Github's OIDC tokens have all of these, but I wasn't sure how + // to populate them. Leaving them here to make future work easier. + + /* + WorkflowRef string `json:"workflow_ref,omitempty"` + WorkflowSHA string `json:"workflow_sha,omitempty"` + JobWorkflowRef string `json:"job_workflow_ref,omitempty"` + JobWorkflowSHA string `json:"job_workflow_sha,omitempty"` + RunnerEnvironment string `json:"runner_environment,omitempty"` + */ +} + +func generateOIDCToken(ctx *context.APIContext) { + if ctx.Doer == nil || ctx.Data["AuthedMethod"] != (&auth_service.OAuth2{}).Name() || ctx.Data["IsActionsToken"] != true { + ctx.PlainText(http.StatusUnauthorized, "no valid authorization") + return + } + + task := ctx.Data["ActionsTask"].(*actions_model.ActionTask) + if err := task.LoadJob(ctx); err != nil { + ctx.PlainText(http.StatusUnauthorized, "no valid authorization") + return + } + + if mayCreateToken := task.Job.MayCreateIDToken(); !mayCreateToken { + ctx.PlainText(http.StatusUnauthorized, "no valid authorization") + return + } + + if err := task.Job.LoadAttributes(ctx); err != nil { + ctx.PlainText(http.StatusUnauthorized, "no valid authorization") + return + } + + if err := task.Job.Run.LoadAttributes(ctx); err != nil { + ctx.PlainText(http.StatusUnauthorized, "no valid authorization") + return + } + + if err := task.Job.Run.Repo.LoadAttributes(ctx); err != nil { + ctx.PlainText(http.StatusUnauthorized, "no valid authorization") + return + } + + eventName := task.Job.Run.EventName() + ref, sha, baseRef, headRef := task.Job.Run.RefShaBaseRefAndHeadRef() + + jwtAudience := jwt.ClaimStrings{task.Job.Run.Repo.Owner.HTMLURL()} + requestedAudience := ctx.Req.URL.Query().Get("audience") + if requestedAudience != "" { + jwtAudience = append(jwtAudience, requestedAudience) + } + + // generate OIDC token + issueTime := timeutil.TimeStampNow() + expirationTime := timeutil.TimeStampNow().Add(15 * 60) + notBeforeTime := timeutil.TimeStampNow().Add(-15 * 60) + idToken := &IDToken{ + RegisteredClaims: jwt.RegisteredClaims{ + Issuer: setting.AppURL, + Audience: jwtAudience, + ExpiresAt: jwt.NewNumericDate(expirationTime.AsTime()), + NotBefore: jwt.NewNumericDate(notBeforeTime.AsTime()), + IssuedAt: jwt.NewNumericDate(issueTime.AsTime()), + Subject: fmt.Sprintf("repo:%s:ref:%s", task.Job.Run.Repo.FullName(), ref), + }, + Ref: ref, + SHA: sha, + Repository: task.Job.Run.Repo.FullName(), + RepositoryOwner: task.Job.Run.Repo.OwnerName, + RepositoryOwnerID: int(task.Job.Run.Repo.OwnerID), + RunID: int(task.Job.RunID), + RunNumber: int(task.Job.Run.Index), + RunAttempt: int(task.Job.Attempt), + RepositoryID: int(task.Job.Run.RepoID), + ActorID: int(task.Job.Run.TriggerUserID), + Actor: task.Job.Run.TriggerUser.Name, + Workflow: task.Job.Run.WorkflowID, + EventName: eventName, + RefType: git.RefName(task.Job.Run.Ref).RefType(), + BaseRef: baseRef, + HeadRef: headRef, + } + + if task.Job.Run.Repo.IsPrivate { + idToken.RepositoryVisibility = "private" + } else { + idToken.RepositoryVisibility = "public" + } + + signedIDToken, err := oauth2.SignToken(idToken, oauth2.DefaultSigningKey) + if err != nil { + ctx.JSON(http.StatusInternalServerError, &IDTokenErrorResponse{ + ErrorDescription: "unable to sign token", + }) + return + } + + ctx.JSON(http.StatusOK, IDTokenResponse{ + Value: signedIDToken, + Count: len(signedIDToken), + }) +} diff --git a/services/auth/oauth2.go b/services/auth/oauth2.go index 6572d661e87f9..9631becf33f1f 100644 --- a/services/auth/oauth2.go +++ b/services/auth/oauth2.go @@ -103,6 +103,7 @@ func (o *OAuth2) userIDFromToken(ctx context.Context, tokenSHA string, store Dat store.GetData()["IsActionsToken"] = true store.GetData()["ActionsTaskID"] = task.ID + store.GetData()["ActionsTask"] = task return user_model.ActionsUserID } From fc80a8a1cc7ea0bf7be0bef4738059d3340e5929 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20L=2E=20Hansen?= Date: Sat, 16 Sep 2023 13:00:39 -0700 Subject: [PATCH 05/23] Copy Permissions struct actions_model Referencing it could cause inconsistencies if it changes later. --- models/migrations/v1_21/v276.go | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/models/migrations/v1_21/v276.go b/models/migrations/v1_21/v276.go index 08f0a16a44cc9..48dd70cfa61c0 100644 --- a/models/migrations/v1_21/v276.go +++ b/models/migrations/v1_21/v276.go @@ -4,14 +4,39 @@ package v1_21 //nolint import ( - actions_model "code.gitea.io/gitea/models/actions" - "xorm.io/xorm" ) +// Permission copied from models.actions.Permission +type Permission int + +const ( + PermissionUnspecified Permission = iota + PermissionNone + PermissionRead + PermissionWrite +) + +// Permissions copied from models.actions.Permissions +type Permissions struct { + Actions Permission `yaml:"actions"` + Checks Permission `yaml:"checks"` + Contents Permission `yaml:"contents"` + Deployments Permission `yaml:"deployments"` + IDToken Permission `yaml:"id-token"` + Issues Permission `yaml:"issues"` + Discussions Permission `yaml:"discussions"` + Packages Permission `yaml:"packages"` + Pages Permission `yaml:"pages"` + PullRequests Permission `yaml:"pull-requests"` + RepositoryProjects Permission `yaml:"repository-projects"` + SecurityEvents Permission `yaml:"security-events"` + Statuses Permission `yaml:"statuses"` +} + func AddPermissions(x *xorm.Engine) error { type ActionRunJob struct { - Permissions actions_model.Permissions `xorm:"JSON TEXT"` + Permissions Permissions `xorm:"JSON TEXT"` } return x.Sync(new(ActionRunJob)) From e192676cd573c00011f92bcbc00f6c160ea71788 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20L=2E=20Hansen?= Date: Sat, 16 Sep 2023 13:01:01 -0700 Subject: [PATCH 06/23] *looks at a calendar* --- routers/api/v1/oidc.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/routers/api/v1/oidc.go b/routers/api/v1/oidc.go index 13a014dd7df6b..0e9390a8c16a9 100644 --- a/routers/api/v1/oidc.go +++ b/routers/api/v1/oidc.go @@ -1,4 +1,4 @@ -// Copyright 2016 The Gitea Authors. All rights reserved. +// Copyright 2023 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT // OIDC provider for Gitea Actions From 747dcc12009c578834c15704d1431a23258f5e61 Mon Sep 17 00:00:00 2001 From: Jack Jackson Date: Wed, 19 Mar 2025 20:12:54 -0700 Subject: [PATCH 07/23] Add Migration with updated name --- models/migrations/v1_24/v317.go | 44 +++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 models/migrations/v1_24/v317.go diff --git a/models/migrations/v1_24/v317.go b/models/migrations/v1_24/v317.go new file mode 100644 index 0000000000000..83c06291f0dc5 --- /dev/null +++ b/models/migrations/v1_24/v317.go @@ -0,0 +1,44 @@ + +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_24 //nolint + +import ( + "xorm.io/xorm" +) + +// Permission copied from models.actions.Permission +type Permission int + +const ( + PermissionUnspecified Permission = iota + PermissionNone + PermissionRead + PermissionWrite +) + +// Permissions copied from models.actions.Permissions +type Permissions struct { + Actions Permission `yaml:"actions"` + Checks Permission `yaml:"checks"` + Contents Permission `yaml:"contents"` + Deployments Permission `yaml:"deployments"` + IDToken Permission `yaml:"id-token"` + Issues Permission `yaml:"issues"` + Discussions Permission `yaml:"discussions"` + Packages Permission `yaml:"packages"` + Pages Permission `yaml:"pages"` + PullRequests Permission `yaml:"pull-requests"` + RepositoryProjects Permission `yaml:"repository-projects"` + SecurityEvents Permission `yaml:"security-events"` + Statuses Permission `yaml:"statuses"` +} + +func AddPermissions(x *xorm.Engine) error { + type ActionRunJob struct { + Permissions Permissions `xorm:"JSON TEXT"` + } + + return x.Sync(new(ActionRunJob)) +} \ No newline at end of file From 32e618a64b55416c9c9094d3eb55d6629c6f3124 Mon Sep 17 00:00:00 2001 From: Jack Jackson Date: Wed, 19 Mar 2025 20:14:18 -0700 Subject: [PATCH 08/23] Remove old (inconsistently-named) migration --- models/migrations/v1_21/permissions.go | 43 -------------------------- 1 file changed, 43 deletions(-) delete mode 100644 models/migrations/v1_21/permissions.go diff --git a/models/migrations/v1_21/permissions.go b/models/migrations/v1_21/permissions.go deleted file mode 100644 index 48dd70cfa61c0..0000000000000 --- a/models/migrations/v1_21/permissions.go +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright 2023 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package v1_21 //nolint - -import ( - "xorm.io/xorm" -) - -// Permission copied from models.actions.Permission -type Permission int - -const ( - PermissionUnspecified Permission = iota - PermissionNone - PermissionRead - PermissionWrite -) - -// Permissions copied from models.actions.Permissions -type Permissions struct { - Actions Permission `yaml:"actions"` - Checks Permission `yaml:"checks"` - Contents Permission `yaml:"contents"` - Deployments Permission `yaml:"deployments"` - IDToken Permission `yaml:"id-token"` - Issues Permission `yaml:"issues"` - Discussions Permission `yaml:"discussions"` - Packages Permission `yaml:"packages"` - Pages Permission `yaml:"pages"` - PullRequests Permission `yaml:"pull-requests"` - RepositoryProjects Permission `yaml:"repository-projects"` - SecurityEvents Permission `yaml:"security-events"` - Statuses Permission `yaml:"statuses"` -} - -func AddPermissions(x *xorm.Engine) error { - type ActionRunJob struct { - Permissions Permissions `xorm:"JSON TEXT"` - } - - return x.Sync(new(ActionRunJob)) -} From 647bdf56b65514912b6a21214b96078cf842cba1 Mon Sep 17 00:00:00 2001 From: Jack Jackson Date: Wed, 19 Mar 2025 20:18:34 -0700 Subject: [PATCH 09/23] Update Copyright dates --- models/actions/permissions.go | 2 +- models/actions/run.go | 2 +- models/actions/run_job.go | 2 +- models/auth/oauth2.go | 2 +- models/migrations/migrations.go | 2 +- routers/api/v1/oidc.go | 2 +- services/actions/notifier_helper.go | 2 +- services/oauth2_provider/token.go | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/models/actions/permissions.go b/models/actions/permissions.go index a5c0058a3b8ea..1baf50e873e8a 100644 --- a/models/actions/permissions.go +++ b/models/actions/permissions.go @@ -1,4 +1,4 @@ -// Copyright 2023 The Gitea Authors. All rights reserved. +// Copyright 2025 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT package actions diff --git a/models/actions/run.go b/models/actions/run.go index edc7f4589796d..28a9622cad75b 100644 --- a/models/actions/run.go +++ b/models/actions/run.go @@ -1,4 +1,4 @@ -// Copyright 2022 The Gitea Authors. All rights reserved. +// Copyright 2025 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT package actions diff --git a/models/actions/run_job.go b/models/actions/run_job.go index 53266e7ac0bf0..e44d0d73c04aa 100644 --- a/models/actions/run_job.go +++ b/models/actions/run_job.go @@ -1,4 +1,4 @@ -// Copyright 2022 The Gitea Authors. All rights reserved. +// Copyright 2025 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT package actions diff --git a/models/auth/oauth2.go b/models/auth/oauth2.go index c270e4856e767..09b857d592b32 100644 --- a/models/auth/oauth2.go +++ b/models/auth/oauth2.go @@ -1,4 +1,4 @@ -// Copyright 2019 The Gitea Authors. All rights reserved. +// Copyright 2025 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT package auth diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index b14ee13268d1c..a50931af892e4 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -1,5 +1,5 @@ // Copyright 2015 The Gogs Authors. All rights reserved. -// Copyright 2017 The Gitea Authors. All rights reserved. +// Copyright 2025 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT package migrations diff --git a/routers/api/v1/oidc.go b/routers/api/v1/oidc.go index 0e9390a8c16a9..0eb30b1052ddd 100644 --- a/routers/api/v1/oidc.go +++ b/routers/api/v1/oidc.go @@ -1,4 +1,4 @@ -// Copyright 2023 The Gitea Authors. All rights reserved. +// Copyright 2025 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT // OIDC provider for Gitea Actions diff --git a/services/actions/notifier_helper.go b/services/actions/notifier_helper.go index f6d088fc314c4..f603c5fcf628e 100644 --- a/services/actions/notifier_helper.go +++ b/services/actions/notifier_helper.go @@ -1,4 +1,4 @@ -// Copyright 2022 The Gitea Authors. All rights reserved. +// Copyright 2025 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT package actions diff --git a/services/oauth2_provider/token.go b/services/oauth2_provider/token.go index 0e59dd72fc726..985d242cbb8f3 100644 --- a/services/oauth2_provider/token.go +++ b/services/oauth2_provider/token.go @@ -1,4 +1,4 @@ -// Copyright 2021 The Gitea Authors. All rights reserved. +// Copyright 2025 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT package oauth2_provider //nolint From 062561631e89eb80c0e92593cb20b97b88ec42e6 Mon Sep 17 00:00:00 2001 From: Jack Jackson Date: Wed, 19 Mar 2025 21:50:10 -0700 Subject: [PATCH 10/23] Lint-fix --- models/actions/run.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/actions/run.go b/models/actions/run.go index 28a9622cad75b..a2734e3f5e05d 100644 --- a/models/actions/run.go +++ b/models/actions/run.go @@ -358,7 +358,7 @@ func InsertRun(ctx context.Context, run *ActionRun, jobs []*jobparser.SingleWork Needs: needs, RunsOn: job.RunsOn(), Status: status, - } + }) // Parse the job's permissions if err := job.RawPermissions.Decode(&runJob.Permissions); err != nil { From f66c4b9657f98da02f3744199a53945af6271696 Mon Sep 17 00:00:00 2001 From: Jack Jackson Date: Wed, 19 Mar 2025 23:07:18 -0700 Subject: [PATCH 11/23] More lintfixes This will not build until [this PR](https://gitea.com/gitea/act/pulls/73) (which adds `SingleWorkflow.RawPermissions`) is merged. --- models/actions/run.go | 7 ++++--- routers/api/v1/oidc.go | 1 - 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/models/actions/run.go b/models/actions/run.go index a2734e3f5e05d..870b17b47a95c 100644 --- a/models/actions/run.go +++ b/models/actions/run.go @@ -345,8 +345,8 @@ func InsertRun(ctx context.Context, run *ActionRun, jobs []*jobparser.SingleWork } else { hasWaiting = true } - job.Name, _ = util.EllipsisDisplayString(job.Name, 255) - runJobs = append(runJobs, &ActionRunJob{ + job.Name = util.EllipsisDisplayString(job.Name, 255) + runJob := &ActionRunJob{ RunID: run.ID, RepoID: run.RepoID, OwnerID: run.OwnerID, @@ -358,7 +358,8 @@ func InsertRun(ctx context.Context, run *ActionRun, jobs []*jobparser.SingleWork Needs: needs, RunsOn: job.RunsOn(), Status: status, - }) + } + runJobs = append(runJobs, runJob) // Parse the job's permissions if err := job.RawPermissions.Decode(&runJob.Permissions); err != nil { diff --git a/routers/api/v1/oidc.go b/routers/api/v1/oidc.go index 0eb30b1052ddd..f3232a47da535 100644 --- a/routers/api/v1/oidc.go +++ b/routers/api/v1/oidc.go @@ -9,7 +9,6 @@ import ( "net/http" actions_model "code.gitea.io/gitea/models/actions" - "code.gitea.io/gitea/modules/context" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" From 3cde888b8c49eb37cb414850d59cb415c96f3ee1 Mon Sep 17 00:00:00 2001 From: Jack Jackson Date: Wed, 19 Mar 2025 23:27:55 -0700 Subject: [PATCH 12/23] Move `permissions.go` to `modules/actions` As requested [here](https://github.com/go-gitea/gitea/pull/33945#discussion_r2004893403). --- {models => modules}/actions/permissions.go | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {models => modules}/actions/permissions.go (100%) diff --git a/models/actions/permissions.go b/modules/actions/permissions.go similarity index 100% rename from models/actions/permissions.go rename to modules/actions/permissions.go From 828e2d53b24dfdf35a8ea387bef46196d00e1b31 Mon Sep 17 00:00:00 2001 From: Jack Jackson Date: Thu, 20 Mar 2025 13:51:33 -0700 Subject: [PATCH 13/23] Correction to package-name for migration --- models/migrations/v1_24/v318.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/migrations/v1_24/v318.go b/models/migrations/v1_24/v318.go index 7ce27ca7bdb58..cf7540085f802 100644 --- a/models/migrations/v1_24/v318.go +++ b/models/migrations/v1_24/v318.go @@ -1,7 +1,7 @@ // Copyright 2025 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT -package runner +package v1_24 //nolint import ( "xorm.io/xorm" From 8221de275e1b775aa7af7d8b0c9a851aec5d1ec3 Mon Sep 17 00:00:00 2001 From: Jack Jackson Date: Fri, 28 Mar 2025 11:53:34 -0700 Subject: [PATCH 14/23] Add required models/actions/permissions file Not sure how this got dropped! --- models/actions/permissions.go | 217 ++++++++++++++++++++++++++++++++++ 1 file changed, 217 insertions(+) create mode 100644 models/actions/permissions.go diff --git a/models/actions/permissions.go b/models/actions/permissions.go new file mode 100644 index 0000000000000..1baf50e873e8a --- /dev/null +++ b/models/actions/permissions.go @@ -0,0 +1,217 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package actions + +import ( + "errors" + "fmt" + + "gopkg.in/yaml.v3" +) + +type Permission int + +const ( + PermissionUnspecified Permission = iota + PermissionNone + PermissionRead + PermissionWrite +) + +// Per https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idpermissions +type Permissions struct { + Actions Permission `yaml:"actions"` + Checks Permission `yaml:"checks"` + Contents Permission `yaml:"contents"` + Deployments Permission `yaml:"deployments"` + IDToken Permission `yaml:"id-token"` + Issues Permission `yaml:"issues"` + Discussions Permission `yaml:"discussions"` + Packages Permission `yaml:"packages"` + Pages Permission `yaml:"pages"` + PullRequests Permission `yaml:"pull-requests"` + RepositoryProjects Permission `yaml:"repository-projects"` + SecurityEvents Permission `yaml:"security-events"` + Statuses Permission `yaml:"statuses"` +} + +// WorkflowPermissions parses a workflow and returns +// a Permissions struct representing the permissions set +// at the workflow (i.e. file) level +func WorkflowPermissions(contents []byte) (Permissions, error) { + p := struct { + Permissions Permissions `yaml:"permissions"` + }{} + err := yaml.Unmarshal(contents, &p) + return p.Permissions, err +} + +// Given the contents of a workflow, JobPermissions +// returns a Permissions object representing the permissions +// of THE FIRST job in the file. +func JobPermissions(contents []byte) (Permissions, error) { + p := struct { + Jobs []struct { + Permissions Permissions `yaml:"permissions"` + } `yaml:"jobs"` + }{} + err := yaml.Unmarshal(contents, &p) + if len(p.Jobs) > 0 { + return p.Jobs[0].Permissions, err + } + return Permissions{}, errors.New("no jobs detected in workflow") +} + +func (p *Permission) UnmarshalYAML(unmarshal func(interface{}) error) error { + var data string + if err := unmarshal(&data); err != nil { + return err + } + + switch data { + case "none": + *p = PermissionNone + case "read": + *p = PermissionRead + case "write": + *p = PermissionWrite + default: + return fmt.Errorf("invalid permission: %s", data) + } + + return nil +} + +// DefaultAccessPermissive is the default "permissive" set granted to actions on repositories +// per https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token +// That page also lists a "metadata" permission that I can't find mentioned anywhere else. +// However, it seems to always have "read" permission, so it doesn't really matter. +// Interestingly, it doesn't list "Discussions", so we assume "write" for permissive and "none" for restricted. +var DefaultAccessPermissive = Permissions{ + Actions: PermissionWrite, + Checks: PermissionWrite, + Contents: PermissionWrite, + Deployments: PermissionWrite, + IDToken: PermissionNone, + Issues: PermissionWrite, + Discussions: PermissionWrite, + Packages: PermissionWrite, + Pages: PermissionWrite, + PullRequests: PermissionWrite, + RepositoryProjects: PermissionWrite, + SecurityEvents: PermissionWrite, + Statuses: PermissionWrite, +} + +// DefaultAccessRestricted is the default "restrictive" set granted. See docs for +// DefaultAccessPermissive above. +// +// This is not currently used, since Gitea does not have a permissive/restricted setting. +var DefaultAccessRestricted = Permissions{ + Actions: PermissionNone, + Checks: PermissionNone, + Contents: PermissionWrite, + Deployments: PermissionNone, + IDToken: PermissionNone, + Issues: PermissionNone, + Discussions: PermissionNone, + Packages: PermissionRead, + Pages: PermissionNone, + PullRequests: PermissionNone, + RepositoryProjects: PermissionNone, + SecurityEvents: PermissionNone, + Statuses: PermissionNone, +} + +var ReadAllPermissions = Permissions{ + Actions: PermissionRead, + Checks: PermissionRead, + Contents: PermissionRead, + Deployments: PermissionRead, + IDToken: PermissionRead, + Issues: PermissionRead, + Discussions: PermissionRead, + Packages: PermissionRead, + Pages: PermissionRead, + PullRequests: PermissionRead, + RepositoryProjects: PermissionRead, + SecurityEvents: PermissionRead, + Statuses: PermissionRead, +} + +var WriteAllPermissions = Permissions{ + Actions: PermissionWrite, + Checks: PermissionWrite, + Contents: PermissionWrite, + Deployments: PermissionWrite, + IDToken: PermissionWrite, + Issues: PermissionWrite, + Discussions: PermissionWrite, + Packages: PermissionWrite, + Pages: PermissionWrite, + PullRequests: PermissionWrite, + RepositoryProjects: PermissionWrite, + SecurityEvents: PermissionWrite, + Statuses: PermissionWrite, +} + +// FromYAML takes a yaml.Node representing a permissions +// definition and parses it into a Permissions struct +func (p *Permissions) FromYAML(rawPermissions *yaml.Node) error { + switch rawPermissions.Kind { + case yaml.ScalarNode: + var val string + err := rawPermissions.Decode(&val) + if err != nil { + return err + } + if val == "read-all" { + *p = ReadAllPermissions + } + if val == "write-all" { + *p = WriteAllPermissions + } + return fmt.Errorf("unexpected `permissions` value: %v", rawPermissions) + case yaml.MappingNode: + var perms Permissions + err := rawPermissions.Decode(&perms) + if err != nil { + return err + } + return nil + case 0: + *p = Permissions{} + return nil + default: + return fmt.Errorf("invalid permissions value: %v", rawPermissions) + } +} + +func merge[T comparable](a, b T) T { + var zero T + if a == zero { + return b + } + return a +} + +// Merge merges two Permission values +// +// Already set values take precedence over `other`. +// I.e. you want to call jobLevel.Permissions.Merge(topLevel.Permissions) +func (p *Permissions) Merge(other Permissions) { + p.Actions = merge(p.Actions, other.Actions) + p.Checks = merge(p.Checks, other.Checks) + p.Contents = merge(p.Contents, other.Contents) + p.Deployments = merge(p.Deployments, other.Deployments) + p.IDToken = merge(p.IDToken, other.IDToken) + p.Issues = merge(p.Issues, other.Issues) + p.Discussions = merge(p.Discussions, other.Discussions) + p.Packages = merge(p.Packages, other.Packages) + p.Pages = merge(p.Pages, other.Pages) + p.PullRequests = merge(p.PullRequests, other.PullRequests) + p.RepositoryProjects = merge(p.RepositoryProjects, other.RepositoryProjects) + p.SecurityEvents = merge(p.SecurityEvents, other.SecurityEvents) + p.Statuses = merge(p.Statuses, other.Statuses) +} From 50c2a219cd52499c2ae99896c1f7db754dd819ac Mon Sep 17 00:00:00 2001 From: Jack Jackson Date: Fri, 28 Mar 2025 12:04:00 -0700 Subject: [PATCH 15/23] Use personal tag of act --- go.mod | 5 ++++- go.sum | 6 ++---- routers/api/v1/oidc.go | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 094aeeb6bc84d..96070ccc65bdb 100644 --- a/go.mod +++ b/go.mod @@ -317,7 +317,10 @@ replace github.com/hashicorp/go-version => github.com/6543/go-version v1.3.1 replace github.com/shurcooL/vfsgen => github.com/lunny/vfsgen v0.0.0-20220105142115-2c99e1ffdfa0 -replace github.com/nektos/act => gitea.com/gitea/act v0.261.4 +// TEMPORARY replacement - looks like no tag was generated for this commit (https://gitea.com/gitea/act/commit/65c232c4a5a40e59e257ab5d956b32585f0405d7), +// so I'm pointing to my own version of it. Should there be an automated action that creates a new tag for every push? +// (I've made one in `$DAY_JOB`, happy to contribute/discuss if that would be helpful!) +replace github.com/nektos/act => gitea.com/scubbo/act v0.262.0 // TODO: the only difference is in `PutObject`: the fork doesn't use `NewVerifyingReader(r, sha256.New(), oid, expectedSize)`, need to figure out why replace github.com/charmbracelet/git-lfs-transfer => gitea.com/gitea/git-lfs-transfer v0.2.0 diff --git a/go.sum b/go.sum index 4aa8a33267b1f..2ba87495fbefa 100644 --- a/go.sum +++ b/go.sum @@ -16,8 +16,6 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078 h1:cliQ4HHsCo6xi2oWZYKWW4bly/Ory9FuTpFPRxj/mAg= git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078/go.mod h1:g/V2Hjas6Z1UHUp4yIx6bATpNzJ7DYtD0FG3+xARWxs= -gitea.com/gitea/act v0.261.4 h1:Tf9eLlvsYFtKcpuxlMvf9yT3g4Hshb2Beqw6C1STuH8= -gitea.com/gitea/act v0.261.4/go.mod h1:Pg5C9kQY1CEA3QjthjhlrqOC/QOT5NyWNjOjRHw23Ok= gitea.com/gitea/git-lfs-transfer v0.2.0 h1:baHaNoBSRaeq/xKayEXwiDQtlIjps4Ac/Ll4KqLMB40= gitea.com/gitea/git-lfs-transfer v0.2.0/go.mod h1:UrXUCm3xLQkq15fu7qlXHUMlrhdlXHoi13KH2Dfiits= gitea.com/go-chi/binding v0.0.0-20240430071103-39a851e106ed h1:EZZBtilMLSZNWtHHcgq2mt6NSGhJSZBuduAlinMEmso= @@ -32,8 +30,8 @@ gitea.com/lunny/dingtalk_webhook v0.0.0-20171025031554-e3534c89ef96 h1:+wWBi6Qfr gitea.com/lunny/dingtalk_webhook v0.0.0-20171025031554-e3534c89ef96/go.mod h1:VyMQP6ue6MKHM8UsOXfNfuMKD0oSAWZdXVcpHIN2yaY= gitea.com/lunny/levelqueue v0.4.2-0.20230414023320-3c0159fe0fe4 h1:IFT+hup2xejHqdhS7keYWioqfmxdnfblFDTGoOwcZ+o= gitea.com/lunny/levelqueue v0.4.2-0.20230414023320-3c0159fe0fe4/go.mod h1:HBqmLbz56JWpfEGG0prskAV97ATNRoj5LDmPicD22hU= -gitea.com/sorenisanerd/act v0.246.2-0.20230806181409-a9e947b70bf6 h1:ANNwt5ZqFG7FhDjdwCfsfoi7zlEV7uAfbrYTV5R8CNg= -gitea.com/sorenisanerd/act v0.246.2-0.20230806181409-a9e947b70bf6/go.mod h1:tfannUyz3cgmq1P1o69KW1AMB1aSlNOMzlswHkRjzcQ= +gitea.com/scubbo/act v0.262.0 h1:mFJFwCs3eKCc0jTzwuXqesOZhuPL1+IFyE0NJ6Rkl5Y= +gitea.com/scubbo/act v0.262.0/go.mod h1:Pg5C9kQY1CEA3QjthjhlrqOC/QOT5NyWNjOjRHw23Ok= gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:lSA0F4e9A2NcQSqGqTOXqu2aRi/XEQxDCBwM8yJtE6s= gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:EXuID2Zs0pAQhH8yz+DNjUbjppKQzKFAn28TMYPB6IU= github.com/42wim/httpsig v1.2.2 h1:ofAYoHUNs/MJOLqQ8hIxeyz2QxOz8qdSVvp3PX/oPgA= diff --git a/routers/api/v1/oidc.go b/routers/api/v1/oidc.go index f3232a47da535..aa42d20dfd002 100644 --- a/routers/api/v1/oidc.go +++ b/routers/api/v1/oidc.go @@ -8,12 +8,12 @@ import ( "fmt" "net/http" - actions_model "code.gitea.io/gitea/models/actions" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" auth_service "code.gitea.io/gitea/services/auth" "code.gitea.io/gitea/services/auth/source/oauth2" + "code.gitea.io/gitea/services/context" "github.com/golang-jwt/jwt/v5" ) From 95c31fb62b38c5b1604c02efe1d7467bad27f047 Mon Sep 17 00:00:00 2001 From: Jack Jackson Date: Fri, 28 Mar 2025 12:08:13 -0700 Subject: [PATCH 16/23] Remove routers/api/actions/runner/utils As it's been [moved](https://github.com/go-gitea/gitea/commit/da33b708af1a98c2ead4b4714fcc3055b2d4c6bd) in a recent commit. --- routers/api/actions/runner/utils.go | 221 ---------------------------- 1 file changed, 221 deletions(-) delete mode 100644 routers/api/actions/runner/utils.go diff --git a/routers/api/actions/runner/utils.go b/routers/api/actions/runner/utils.go deleted file mode 100644 index cae2508a03273..0000000000000 --- a/routers/api/actions/runner/utils.go +++ /dev/null @@ -1,221 +0,0 @@ -// Copyright 2022 The Gitea Authors. All rights reserved. -// SPDX-License-Identifier: MIT - -package runner - -import ( - "context" - "fmt" - - actions_model "code.gitea.io/gitea/models/actions" - secret_model "code.gitea.io/gitea/models/secret" - actions_module "code.gitea.io/gitea/modules/actions" - "code.gitea.io/gitea/modules/container" - "code.gitea.io/gitea/modules/git" - "code.gitea.io/gitea/modules/json" - "code.gitea.io/gitea/modules/log" - secret_module "code.gitea.io/gitea/modules/secret" - "code.gitea.io/gitea/modules/setting" - "code.gitea.io/gitea/services/actions" - - runnerv1 "code.gitea.io/actions-proto-go/runner/v1" - "google.golang.org/protobuf/types/known/structpb" -) - -func pickTask(ctx context.Context, runner *actions_model.ActionRunner) (*runnerv1.Task, bool, error) { - t, ok, err := actions_model.CreateTaskForRunner(ctx, runner) - if err != nil { - return nil, false, fmt.Errorf("CreateTaskForRunner: %w", err) - } - if !ok { - return nil, false, nil - } - - actions.CreateCommitStatus(ctx, t.Job) - - task := &runnerv1.Task{ - Id: t.ID, - WorkflowPayload: t.Job.WorkflowPayload, - Context: generateTaskContext(t), - Secrets: getSecretsOfTask(ctx, t), - Vars: getVariablesOfTask(ctx, t), - } - - if needs, err := findTaskNeeds(ctx, t); err != nil { - log.Error("Cannot find needs for task %v: %v", t.ID, err) - // Go on with empty needs. - // If return error, the task will be wild, which means the runner will never get it when it has been assigned to the runner. - // In contrast, missing needs is less serious. - // And the task will fail and the runner will report the error in the logs. - } else { - task.Needs = needs - } - - return task, true, nil -} - -func getSecretsOfTask(ctx context.Context, task *actions_model.ActionTask) map[string]string { - secrets := map[string]string{} - - secrets["GITHUB_TOKEN"] = task.Token - secrets["GITEA_TOKEN"] = task.Token - - if task.Job.Run.IsForkPullRequest && task.Job.Run.TriggerEvent != actions_module.GithubEventPullRequestTarget { - // ignore secrets for fork pull request, except GITHUB_TOKEN and GITEA_TOKEN which are automatically generated. - // for the tasks triggered by pull_request_target event, they could access the secrets because they will run in the context of the base branch - // see the documentation: https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target - return secrets - } - - ownerSecrets, err := secret_model.FindSecrets(ctx, secret_model.FindSecretsOptions{OwnerID: task.Job.Run.Repo.OwnerID}) - if err != nil { - log.Error("find secrets of owner %v: %v", task.Job.Run.Repo.OwnerID, err) - // go on - } - repoSecrets, err := secret_model.FindSecrets(ctx, secret_model.FindSecretsOptions{RepoID: task.Job.Run.RepoID}) - if err != nil { - log.Error("find secrets of repo %v: %v", task.Job.Run.RepoID, err) - // go on - } - - for _, secret := range append(ownerSecrets, repoSecrets...) { - if v, err := secret_module.DecryptSecret(setting.SecretKey, secret.Data); err != nil { - log.Error("decrypt secret %v %q: %v", secret.ID, secret.Name, err) - // go on - } else { - secrets[secret.Name] = v - } - } - - return secrets -} - -func getVariablesOfTask(ctx context.Context, task *actions_model.ActionTask) map[string]string { - variables := map[string]string{} - - // Org / User level - ownerVariables, err := actions_model.FindVariables(ctx, actions_model.FindVariablesOpts{OwnerID: task.Job.Run.Repo.OwnerID}) - if err != nil { - log.Error("find variables of org: %d, error: %v", task.Job.Run.Repo.OwnerID, err) - } - - // Repo level - repoVariables, err := actions_model.FindVariables(ctx, actions_model.FindVariablesOpts{RepoID: task.Job.Run.RepoID}) - if err != nil { - log.Error("find variables of repo: %d, error: %v", task.Job.Run.RepoID, err) - } - - // Level precedence: Repo > Org / User - for _, v := range append(ownerVariables, repoVariables...) { - variables[v.Name] = v.Data - } - - return variables -} - -func generateTaskContext(t *actions_model.ActionTask) *structpb.Struct { - event := map[string]any{} - _ = json.Unmarshal([]byte(t.Job.Run.EventPayload), &event) - - eventName := t.Job.Run.EventName() - ref, sha, baseRef, headRef := t.Job.Run.RefShaBaseRefAndHeadRef() - refName := git.RefName(ref) - - contextMap := map[string]any{ - // standard contexts, see https://docs.github.com/en/actions/learn-github-actions/contexts#github-context - "action": "", // string, The name of the action currently running, or the id of a step. GitHub removes special characters, and uses the name __run when the current step runs a script without an id. If you use the same action more than once in the same job, the name will include a suffix with the sequence number with underscore before it. For example, the first script you run will have the name __run, and the second script will be named __run_2. Similarly, the second invocation of actions/checkout will be actionscheckout2. - "action_path": "", // string, The path where an action is located. This property is only supported in composite actions. You can use this path to access files located in the same repository as the action. - "action_ref": "", // string, For a step executing an action, this is the ref of the action being executed. For example, v2. - "action_repository": "", // string, For a step executing an action, this is the owner and repository name of the action. For example, actions/checkout. - "action_status": "", // string, For a composite action, the current result of the composite action. - "actor": t.Job.Run.TriggerUser.Name, // string, The username of the user that triggered the initial workflow run. If the workflow run is a re-run, this value may differ from github.triggering_actor. Any workflow re-runs will use the privileges of github.actor, even if the actor initiating the re-run (github.triggering_actor) has different privileges. - "api_url": setting.AppURL + "api/v1", // string, The URL of the GitHub REST API. - "base_ref": baseRef, // string, The base_ref or target branch of the pull request in a workflow run. This property is only available when the event that triggers a workflow run is either pull_request or pull_request_target. - "env": "", // string, Path on the runner to the file that sets environment variables from workflow commands. This file is unique to the current step and is a different file for each step in a job. For more information, see "Workflow commands for GitHub Actions." - "event": event, // object, The full event webhook payload. You can access individual properties of the event using this context. This object is identical to the webhook payload of the event that triggered the workflow run, and is different for each event. The webhooks for each GitHub Actions event is linked in "Events that trigger workflows." For example, for a workflow run triggered by the push event, this object contains the contents of the push webhook payload. - "event_name": eventName, // string, The name of the event that triggered the workflow run. - "event_path": "", // string, The path to the file on the runner that contains the full event webhook payload. - "graphql_url": "", // string, The URL of the GitHub GraphQL API. - "head_ref": headRef, // string, The head_ref or source branch of the pull request in a workflow run. This property is only available when the event that triggers a workflow run is either pull_request or pull_request_target. - "job": fmt.Sprint(t.JobID), // string, The job_id of the current job. - "ref": ref, // string, The fully-formed ref of the branch or tag that triggered the workflow run. For workflows triggered by push, this is the branch or tag ref that was pushed. For workflows triggered by pull_request, this is the pull request merge branch. For workflows triggered by release, this is the release tag created. For other triggers, this is the branch or tag ref that triggered the workflow run. This is only set if a branch or tag is available for the event type. The ref given is fully-formed, meaning that for branches the format is refs/heads/, for pull requests it is refs/pull//merge, and for tags it is refs/tags/. For example, refs/heads/feature-branch-1. - "ref_name": refName.ShortName(), // string, The short ref name of the branch or tag that triggered the workflow run. This value matches the branch or tag name shown on GitHub. For example, feature-branch-1. - "ref_protected": false, // boolean, true if branch protections are configured for the ref that triggered the workflow run. - "ref_type": refName.RefType(), // string, The type of ref that triggered the workflow run. Valid values are branch or tag. - "path": "", // string, Path on the runner to the file that sets system PATH variables from workflow commands. This file is unique to the current step and is a different file for each step in a job. For more information, see "Workflow commands for GitHub Actions." - "repository": t.Job.Run.Repo.OwnerName + "/" + t.Job.Run.Repo.Name, // string, The owner and repository name. For example, Codertocat/Hello-World. - "repository_owner": t.Job.Run.Repo.OwnerName, // string, The repository owner's name. For example, Codertocat. - "repositoryUrl": t.Job.Run.Repo.HTMLURL(), // string, The Git URL to the repository. For example, git://github.com/codertocat/hello-world.git. - "retention_days": "", // string, The number of days that workflow run logs and artifacts are kept. - "run_id": fmt.Sprint(t.Job.RunID), // string, A unique number for each workflow run within a repository. This number does not change if you re-run the workflow run. - "run_number": fmt.Sprint(t.Job.Run.Index), // string, A unique number for each run of a particular workflow in a repository. This number begins at 1 for the workflow's first run, and increments with each new run. This number does not change if you re-run the workflow run. - "run_attempt": fmt.Sprint(t.Job.Attempt), // string, A unique number for each attempt of a particular workflow run in a repository. This number begins at 1 for the workflow run's first attempt, and increments with each re-run. - "secret_source": "Actions", // string, The source of a secret used in a workflow. Possible values are None, Actions, Dependabot, or Codespaces. - "server_url": setting.AppURL, // string, The URL of the GitHub server. For example: https://github.com. - "sha": sha, // string, The commit SHA that triggered the workflow. The value of this commit SHA depends on the event that triggered the workflow. For more information, see "Events that trigger workflows." For example, ffac537e6cbbf934b08745a378932722df287a53. - "token": t.Token, // string, A token to authenticate on behalf of the GitHub App installed on your repository. This is functionally equivalent to the GITHUB_TOKEN secret. For more information, see "Automatic token authentication." - "triggering_actor": "", // string, The username of the user that initiated the workflow run. If the workflow run is a re-run, this value may differ from github.actor. Any workflow re-runs will use the privileges of github.actor, even if the actor initiating the re-run (github.triggering_actor) has different privileges. - "workflow": t.Job.Run.WorkflowID, // string, The name of the workflow. If the workflow file doesn't specify a name, the value of this property is the full path of the workflow file in the repository. - "workspace": "", // string, The default working directory on the runner for steps, and the default location of your repository when using the checkout action. - - // additional contexts - "gitea_default_actions_url": setting.Actions.DefaultActionsURL.URL(), - } - - if t.Job.MayCreateIDToken() { - // The "a=1" is a dummy variable. If an audience is passed to - // github/core.js's getIdToken(), it appends it to the URL as "&audience=". - // If the URL doesn't at least have a '?', the "&audience=" part will be - // interpreted as part of the path. - contextMap["actions_id_token_request_url"] = fmt.Sprintf("%sapi/v1/actions/id-token/request?a=1", setting.AppURL) - contextMap["actions_id_token_request_token"] = t.Token - } - - taskContext, err := structpb.NewStruct(contextMap) - if err != nil { - log.Error("structpb.NewStruct failed: %v", err) - } - - return taskContext -} - -func findTaskNeeds(ctx context.Context, task *actions_model.ActionTask) (map[string]*runnerv1.TaskNeed, error) { - if err := task.LoadAttributes(ctx); err != nil { - return nil, fmt.Errorf("LoadAttributes: %w", err) - } - if len(task.Job.Needs) == 0 { - return nil, nil - } - needs := container.SetOf(task.Job.Needs...) - - jobs, _, err := actions_model.FindRunJobs(ctx, actions_model.FindRunJobOptions{RunID: task.Job.RunID}) - if err != nil { - return nil, fmt.Errorf("FindRunJobs: %w", err) - } - - ret := make(map[string]*runnerv1.TaskNeed, len(needs)) - for _, job := range jobs { - if !needs.Contains(job.JobID) { - continue - } - if job.TaskID == 0 || !job.Status.IsDone() { - // it shouldn't happen, or the job has been rerun - continue - } - outputs := make(map[string]string) - got, err := actions_model.FindTaskOutputByTaskID(ctx, job.TaskID) - if err != nil { - return nil, fmt.Errorf("FindTaskOutputByTaskID: %w", err) - } - for _, v := range got { - outputs[v.OutputKey] = v.OutputValue - } - ret[job.JobID] = &runnerv1.TaskNeed{ - Outputs: outputs, - Result: runnerv1.Result(job.Status), - } - } - - return ret, nil -} - From 8f02684cc5d807201f5d909bc51127842400a19f Mon Sep 17 00:00:00 2001 From: Jack Jackson Date: Fri, 28 Mar 2025 12:25:50 -0700 Subject: [PATCH 17/23] Use appropriate type for RefType --- routers/api/v1/oidc.go | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/routers/api/v1/oidc.go b/routers/api/v1/oidc.go index aa42d20dfd002..3ecf044c02ede 100644 --- a/routers/api/v1/oidc.go +++ b/routers/api/v1/oidc.go @@ -8,6 +8,7 @@ import ( "fmt" "net/http" + actions_model "code.gitea.io/gitea/models/actions" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" @@ -30,23 +31,23 @@ type IDTokenErrorResponse struct { type IDToken struct { jwt.RegisteredClaims - Ref string `json:"ref,omitempty"` - SHA string `json:"sha,omitempty"` - Repository string `json:"repository,omitempty"` - RepositoryOwner string `json:"repository_owner,omitempty"` - RepositoryOwnerID int `json:"repository_owner_id,omitempty"` - RunID int `json:"run_id,omitempty"` - RunNumber int `json:"run_number,omitempty"` - RunAttempt int `json:"run_attempt,omitempty"` - RepositoryVisibility string `json:"repository_visibility,omitempty"` - RepositoryID int `json:"repository_id,omitempty"` - ActorID int `json:"actor_id,omitempty"` - Actor string `json:"actor,omitempty"` - Workflow string `json:"workflow,omitempty"` - EventName string `json:"event_name,omitempty"` - RefType string `json:"ref_type,omitempty"` - HeadRef string `json:"head_ref,omitempty"` - BaseRef string `json:"base_ref,omitempty"` + Ref string `json:"ref,omitempty"` + SHA string `json:"sha,omitempty"` + Repository string `json:"repository,omitempty"` + RepositoryOwner string `json:"repository_owner,omitempty"` + RepositoryOwnerID int `json:"repository_owner_id,omitempty"` + RunID int `json:"run_id,omitempty"` + RunNumber int `json:"run_number,omitempty"` + RunAttempt int `json:"run_attempt,omitempty"` + RepositoryVisibility string `json:"repository_visibility,omitempty"` + RepositoryID int `json:"repository_id,omitempty"` + ActorID int `json:"actor_id,omitempty"` + Actor string `json:"actor,omitempty"` + Workflow string `json:"workflow,omitempty"` + EventName string `json:"event_name,omitempty"` + RefType git.RefType `json:"ref_type,omitempty"` + HeadRef string `json:"head_ref,omitempty"` + BaseRef string `json:"base_ref,omitempty"` // Github's OIDC tokens have all of these, but I wasn't sure how // to populate them. Leaving them here to make future work easier. From c02825447b9fa48e17d9c2ba5b13274ba0ef789b Mon Sep 17 00:00:00 2001 From: Jack Jackson Date: Sat, 5 Apr 2025 11:56:25 -0700 Subject: [PATCH 18/23] Use renamed oauth2_provider for signing token --- routers/api/v1/oidc.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/routers/api/v1/oidc.go b/routers/api/v1/oidc.go index 3ecf044c02ede..fadde203f7b19 100644 --- a/routers/api/v1/oidc.go +++ b/routers/api/v1/oidc.go @@ -13,8 +13,8 @@ import ( "code.gitea.io/gitea/modules/setting" "code.gitea.io/gitea/modules/timeutil" auth_service "code.gitea.io/gitea/services/auth" - "code.gitea.io/gitea/services/auth/source/oauth2" "code.gitea.io/gitea/services/context" + "code.gitea.io/gitea/services/oauth2_provider" "github.com/golang-jwt/jwt/v5" ) @@ -139,7 +139,7 @@ func generateOIDCToken(ctx *context.APIContext) { idToken.RepositoryVisibility = "public" } - signedIDToken, err := oauth2.SignToken(idToken, oauth2.DefaultSigningKey) + signedIDToken, err := oauth2_provider.SignToken(idToken, oauth2_provider.DefaultSigningKey) if err != nil { ctx.JSON(http.StatusInternalServerError, &IDTokenErrorResponse{ ErrorDescription: "unable to sign token", From d77b25024390154b35222bf6af8ee03809655b16 Mon Sep 17 00:00:00 2001 From: Jack Jackson Date: Sat, 5 Apr 2025 11:57:30 -0700 Subject: [PATCH 19/23] Reformatting --- models/actions/permissions.go | 2 +- modules/actions/permissions.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/models/actions/permissions.go b/models/actions/permissions.go index 1baf50e873e8a..3a0f0206865de 100644 --- a/models/actions/permissions.go +++ b/models/actions/permissions.go @@ -63,7 +63,7 @@ func JobPermissions(contents []byte) (Permissions, error) { return Permissions{}, errors.New("no jobs detected in workflow") } -func (p *Permission) UnmarshalYAML(unmarshal func(interface{}) error) error { +func (p *Permission) UnmarshalYAML(unmarshal func(any) error) error { var data string if err := unmarshal(&data); err != nil { return err diff --git a/modules/actions/permissions.go b/modules/actions/permissions.go index 1baf50e873e8a..3a0f0206865de 100644 --- a/modules/actions/permissions.go +++ b/modules/actions/permissions.go @@ -63,7 +63,7 @@ func JobPermissions(contents []byte) (Permissions, error) { return Permissions{}, errors.New("no jobs detected in workflow") } -func (p *Permission) UnmarshalYAML(unmarshal func(interface{}) error) error { +func (p *Permission) UnmarshalYAML(unmarshal func(any) error) error { var data string if err := unmarshal(&data); err != nil { return err From f01436909646342ea0baee74e8ae8b4e5c8c0fd0 Mon Sep 17 00:00:00 2001 From: Jack Jackson Date: Sat, 5 Apr 2025 12:05:11 -0700 Subject: [PATCH 20/23] Use gitea/act --- go.mod | 5 +---- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 96070ccc65bdb..ed6f0535ccaaf 100644 --- a/go.mod +++ b/go.mod @@ -317,10 +317,7 @@ replace github.com/hashicorp/go-version => github.com/6543/go-version v1.3.1 replace github.com/shurcooL/vfsgen => github.com/lunny/vfsgen v0.0.0-20220105142115-2c99e1ffdfa0 -// TEMPORARY replacement - looks like no tag was generated for this commit (https://gitea.com/gitea/act/commit/65c232c4a5a40e59e257ab5d956b32585f0405d7), -// so I'm pointing to my own version of it. Should there be an automated action that creates a new tag for every push? -// (I've made one in `$DAY_JOB`, happy to contribute/discuss if that would be helpful!) -replace github.com/nektos/act => gitea.com/scubbo/act v0.262.0 +replace github.com/nektos/act => gitea.com/gitea/act v0.261.5 // TODO: the only difference is in `PutObject`: the fork doesn't use `NewVerifyingReader(r, sha256.New(), oid, expectedSize)`, need to figure out why replace github.com/charmbracelet/git-lfs-transfer => gitea.com/gitea/git-lfs-transfer v0.2.0 diff --git a/go.sum b/go.sum index 2ba87495fbefa..6be3eb345af70 100644 --- a/go.sum +++ b/go.sum @@ -16,6 +16,8 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078 h1:cliQ4HHsCo6xi2oWZYKWW4bly/Ory9FuTpFPRxj/mAg= git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078/go.mod h1:g/V2Hjas6Z1UHUp4yIx6bATpNzJ7DYtD0FG3+xARWxs= +gitea.com/gitea/act v0.261.5 h1:o4cWLYTy1T5819CCZoBpc9rf0Y8Xev8MatMJUsM7IUY= +gitea.com/gitea/act v0.261.5/go.mod h1:Pg5C9kQY1CEA3QjthjhlrqOC/QOT5NyWNjOjRHw23Ok= gitea.com/gitea/git-lfs-transfer v0.2.0 h1:baHaNoBSRaeq/xKayEXwiDQtlIjps4Ac/Ll4KqLMB40= gitea.com/gitea/git-lfs-transfer v0.2.0/go.mod h1:UrXUCm3xLQkq15fu7qlXHUMlrhdlXHoi13KH2Dfiits= gitea.com/go-chi/binding v0.0.0-20240430071103-39a851e106ed h1:EZZBtilMLSZNWtHHcgq2mt6NSGhJSZBuduAlinMEmso= @@ -30,8 +32,6 @@ gitea.com/lunny/dingtalk_webhook v0.0.0-20171025031554-e3534c89ef96 h1:+wWBi6Qfr gitea.com/lunny/dingtalk_webhook v0.0.0-20171025031554-e3534c89ef96/go.mod h1:VyMQP6ue6MKHM8UsOXfNfuMKD0oSAWZdXVcpHIN2yaY= gitea.com/lunny/levelqueue v0.4.2-0.20230414023320-3c0159fe0fe4 h1:IFT+hup2xejHqdhS7keYWioqfmxdnfblFDTGoOwcZ+o= gitea.com/lunny/levelqueue v0.4.2-0.20230414023320-3c0159fe0fe4/go.mod h1:HBqmLbz56JWpfEGG0prskAV97ATNRoj5LDmPicD22hU= -gitea.com/scubbo/act v0.262.0 h1:mFJFwCs3eKCc0jTzwuXqesOZhuPL1+IFyE0NJ6Rkl5Y= -gitea.com/scubbo/act v0.262.0/go.mod h1:Pg5C9kQY1CEA3QjthjhlrqOC/QOT5NyWNjOjRHw23Ok= gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:lSA0F4e9A2NcQSqGqTOXqu2aRi/XEQxDCBwM8yJtE6s= gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:EXuID2Zs0pAQhH8yz+DNjUbjppKQzKFAn28TMYPB6IU= github.com/42wim/httpsig v1.2.2 h1:ofAYoHUNs/MJOLqQ8hIxeyz2QxOz8qdSVvp3PX/oPgA= From f9521905ffcfa1d56c65f0a200b7b0e01fb4b13c Mon Sep 17 00:00:00 2001 From: Jack Jackson Date: Sun, 6 Apr 2025 22:02:55 -0700 Subject: [PATCH 21/23] PR comments --- models/actions/run.go | 2 +- models/auth/oauth2.go | 2 +- models/migrations/migrations.go | 2 +- services/auth/oauth2.go | 1 - services/oauth2_provider/token.go | 2 +- 5 files changed, 4 insertions(+), 5 deletions(-) diff --git a/models/actions/run.go b/models/actions/run.go index 779450678835e..fa932014d5b44 100644 --- a/models/actions/run.go +++ b/models/actions/run.go @@ -1,4 +1,4 @@ -// Copyright 2025 The Gitea Authors. All rights reserved. +// Copyright 2022 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT package actions diff --git a/models/auth/oauth2.go b/models/auth/oauth2.go index 09b857d592b32..c270e4856e767 100644 --- a/models/auth/oauth2.go +++ b/models/auth/oauth2.go @@ -1,4 +1,4 @@ -// Copyright 2025 The Gitea Authors. All rights reserved. +// Copyright 2019 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT package auth diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index e06ce9750cefc..9f0e1d81b8168 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -1,5 +1,5 @@ // Copyright 2015 The Gogs Authors. All rights reserved. -// Copyright 2025 The Gitea Authors. All rights reserved. +// Copyright 2017 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT package migrations diff --git a/services/auth/oauth2.go b/services/auth/oauth2.go index 5cc277ac636f4..66cc68680936c 100644 --- a/services/auth/oauth2.go +++ b/services/auth/oauth2.go @@ -138,7 +138,6 @@ func (o *OAuth2) userIDFromToken(ctx context.Context, tokenSHA string, store Dat store.GetData()["IsActionsToken"] = true store.GetData()["ActionsTaskID"] = task.ID - store.GetData()["ActionsTask"] = task return user_model.ActionsUserID } diff --git a/services/oauth2_provider/token.go b/services/oauth2_provider/token.go index d757496354b63..3e73946186e6f 100644 --- a/services/oauth2_provider/token.go +++ b/services/oauth2_provider/token.go @@ -1,4 +1,4 @@ -// Copyright 2025 The Gitea Authors. All rights reserved. +// Copyright 2021 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT package oauth2_provider //nolint From 7b98be867a2ef2c82016b9e52f2d487af1a19129 Mon Sep 17 00:00:00 2001 From: Jack Jackson Date: Mon, 7 Apr 2025 12:11:27 -0700 Subject: [PATCH 22/23] Reverting Copyright date updates --- models/actions/run_job.go | 2 +- services/actions/notifier_helper.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/models/actions/run_job.go b/models/actions/run_job.go index 6d6967e6689ef..3f57aeb6dd665 100644 --- a/models/actions/run_job.go +++ b/models/actions/run_job.go @@ -1,4 +1,4 @@ -// Copyright 2025 The Gitea Authors. All rights reserved. +// Copyright 2022 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT package actions diff --git a/services/actions/notifier_helper.go b/services/actions/notifier_helper.go index f603c5fcf628e..f6d088fc314c4 100644 --- a/services/actions/notifier_helper.go +++ b/services/actions/notifier_helper.go @@ -1,4 +1,4 @@ -// Copyright 2025 The Gitea Authors. All rights reserved. +// Copyright 2022 The Gitea Authors. All rights reserved. // SPDX-License-Identifier: MIT package actions From 702f6401fbebfd1991b02ac88b6ba3c5d740b243 Mon Sep 17 00:00:00 2001 From: Jack Jackson Date: Mon, 7 Apr 2025 13:53:28 -0700 Subject: [PATCH 23/23] Move OIDC API into subfolder --- routers/api/v1/{ => actions}/oidc.go | 4 ++-- routers/api/v1/api.go | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) rename routers/api/v1/{ => actions}/oidc.go (98%) diff --git a/routers/api/v1/oidc.go b/routers/api/v1/actions/oidc.go similarity index 98% rename from routers/api/v1/oidc.go rename to routers/api/v1/actions/oidc.go index fadde203f7b19..cc9b35d1f38b0 100644 --- a/routers/api/v1/oidc.go +++ b/routers/api/v1/actions/oidc.go @@ -2,7 +2,7 @@ // SPDX-License-Identifier: MIT // OIDC provider for Gitea Actions -package v1 +package actions import ( "fmt" @@ -61,7 +61,7 @@ type IDToken struct { */ } -func generateOIDCToken(ctx *context.APIContext) { +func GenerateOIDCToken(ctx *context.APIContext) { if ctx.Doer == nil || ctx.Data["AuthedMethod"] != (&auth_service.OAuth2{}).Name() || ctx.Data["IsActionsToken"] != true { ctx.PlainText(http.StatusUnauthorized, "no valid authorization") return diff --git a/routers/api/v1/api.go b/routers/api/v1/api.go index 403583798192d..9de67874db940 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -82,6 +82,7 @@ import ( "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/web" + actions_router "code.gitea.io/gitea/routers/api/v1/actions" "code.gitea.io/gitea/routers/api/v1/activitypub" "code.gitea.io/gitea/routers/api/v1/admin" "code.gitea.io/gitea/routers/api/v1/misc" @@ -1126,7 +1127,7 @@ func Routes() *web.Router { }) }, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser), reqToken()) - m.Get("/actions/id-token/request", generateOIDCToken) + m.Get("/actions/id-token/request", actions_router.GenerateOIDCToken) // Repositories (requires repo scope, org scope) m.Post("/org/{org}/repos",