Skip to content
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

OIDC provider #33945

Open
wants to merge 30 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
3a16680
Refactor token signing method in OAuth2 service
sorenisanerd Jul 4, 2023
31e8ad3
feat: Add basic permissions support for actions
sorenisanerd Aug 6, 2023
8271be5
Extract {base_,}{sha,ref} and EventName logic
sorenisanerd Aug 6, 2023
724e138
feat: Add OIDC provider for actions
sorenisanerd Aug 6, 2023
fc80a8a
Copy Permissions struct actions_model
sorenisanerd Sep 16, 2023
e192676
*looks at a calendar*
sorenisanerd Sep 16, 2023
69ce797
Merge remote-tracking branch 'origin/main' into oidc-provider
sorenisanerd Sep 16, 2023
66dca88
Merge branch 'main' into oidc-provider
scubbo Mar 20, 2025
747dcc1
Add Migration with updated name
scubbo Mar 20, 2025
32e618a
Remove old (inconsistently-named) migration
scubbo Mar 20, 2025
647bdf5
Update Copyright dates
scubbo Mar 20, 2025
0625616
Lint-fix
scubbo Mar 20, 2025
f66c4b9
More lintfixes
scubbo Mar 20, 2025
3cde888
Move `permissions.go` to `modules/actions`
scubbo Mar 20, 2025
6395824
Merge remote-tracking branch 'origin/main' into oidc-provider
scubbo Mar 20, 2025
828e2d5
Correction to package-name for migration
scubbo Mar 20, 2025
3ac43c6
Merge branch 'main' into oidc-provider
scubbo Mar 25, 2025
74ace80
Merge branch 'main' into oidc-provider
scubbo Mar 28, 2025
8221de2
Add required models/actions/permissions file
scubbo Mar 28, 2025
50c2a21
Use personal tag of act
scubbo Mar 28, 2025
95c31fb
Remove routers/api/actions/runner/utils
scubbo Mar 28, 2025
8f02684
Use appropriate type for RefType
scubbo Mar 28, 2025
236745d
Merge branch 'main' into oidc-provider
scubbo Apr 5, 2025
c028254
Use renamed oauth2_provider for signing token
scubbo Apr 5, 2025
d77b250
Reformatting
scubbo Apr 5, 2025
f014369
Use gitea/act
scubbo Apr 5, 2025
f952190
PR comments
scubbo Apr 7, 2025
7b98be8
Reverting Copyright date updates
scubbo Apr 7, 2025
702f640
Move OIDC API into subfolder
scubbo Apr 7, 2025
c477e0e
Merge branch 'main' into oidc-provider
scubbo Apr 7, 2025
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 @@ -317,7 +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

replace github.com/nektos/act => gitea.com/gitea/act v0.261.4
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
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +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.4 h1:Tf9eLlvsYFtKcpuxlMvf9yT3g4Hshb2Beqw6C1STuH8=
gitea.com/gitea/act v0.261.4/go.mod h1:Pg5C9kQY1CEA3QjthjhlrqOC/QOT5NyWNjOjRHw23Ok=
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=
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 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(any) 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)
}
49 changes: 47 additions & 2 deletions models/actions/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,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 and Stopped is used for recording last run time, if rerun happened, they will be reset to 0
Started timeutil.TimeStamp
Expand Down Expand Up @@ -83,6 +84,38 @@ func (run *ActionRun) WorkflowLink() string {
return fmt.Sprintf("%s/actions/?workflow=%s", run.Repo.Link(), run.WorkflowID)
}

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 @@ -314,7 +347,7 @@ func InsertRun(ctx context.Context, run *ActionRun, jobs []*jobparser.SingleWork
hasWaiting = true
}
job.Name = util.EllipsisDisplayString(job.Name, 255)
runJobs = append(runJobs, &ActionRunJob{
runJob := &ActionRunJob{
RunID: run.ID,
RepoID: run.RepoID,
OwnerID: run.OwnerID,
Expand All @@ -326,7 +359,19 @@ 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 {
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 @@ -30,11 +30,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 @@ -84,6 +85,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
1 change: 1 addition & 0 deletions models/migrations/migrations.go
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,7 @@ func prepareMigrationTasks() []*migration {
newMigration(316, "Add description for secrets and variables", v1_24.AddDescriptionForSecretsAndVariables),
newMigration(317, "Add new index for action for heatmap", v1_24.AddNewIndexForUserDashboard),
newMigration(318, "Add anonymous_access_mode for repo_unit", v1_24.AddRepoUnitAnonymousAccessMode),
newMigration(319, "Add Permissions to Actions Task", v1_24.AddPermissions),
}
return preparedMigrations
}
Expand Down
43 changes: 43 additions & 0 deletions models/migrations/v1_24/v319.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// 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))
}
Loading
Loading