Skip to content

OIDC provider #25664

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Member

Choose a reason for hiding this comment

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

Maybe a update on gitea/act side is better.

Copy link
Author

Choose a reason for hiding this comment

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

As mentioned in the PR description, there's a PR against gitea/act at https://gitea.com/gitea/act/pulls/73 since several months ago.


exclude github.com/gofrs/uuid v3.2.0+incompatible

Expand Down
6 changes: 4 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand All @@ -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=
Expand Down
217 changes: 217 additions & 0 deletions models/actions/permissions.go
Original file line number Diff line number Diff line change
@@ -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)
}
48 changes: 46 additions & 2 deletions models/actions/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -71,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)
Expand Down Expand Up @@ -280,7 +313,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,
Expand All @@ -292,7 +325,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
Expand Down
15 changes: 10 additions & 5 deletions models/actions/run_job.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down Expand Up @@ -71,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)
Expand Down
2 changes: 2 additions & 0 deletions models/migrations/migrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,8 @@ var migrations = []Migration{
NewMigration("Add ScheduleID for ActionRun", v1_21.AddScheduleIDForActionRun),
// v276 -> v277
NewMigration("Add RemoteAddress to mirrors", v1_21.AddRemoteAddressToMirrors),
// vXXX -> vYYY
NewMigration("Add Permissions to Actions Task", v1_21.AddPermissions),
}

// GetCurrentDBVersion returns the current db version
Expand Down
Loading