mirror of
https://github.com/go-gitea/gitea.git
synced 2025-04-15 05:37:46 +00:00
Merge 70947980e8
into 93a2def96b
This commit is contained in:
commit
64e91a591c
2
go.mod
2
go.mod
@ -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
|
||||
|
4
go.sum
4
go.sum
@ -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=
|
||||
|
217
models/actions/permissions.go
Normal file
217
models/actions/permissions.go
Normal file
@ -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)
|
||||
}
|
@ -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
|
||||
@ -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)
|
||||
@ -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,
|
||||
@ -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
|
||||
|
@ -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"`
|
||||
@ -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)
|
||||
|
@ -381,6 +381,7 @@ func prepareMigrationTasks() []*migration {
|
||||
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 ExclusiveOrder to Label table", v1_24.AddExclusiveOrderColumnToLabelTable),
|
||||
newMigration(320, "Add Permissions to Actions Task", v1_24.AddPermissions),
|
||||
}
|
||||
return preparedMigrations
|
||||
}
|
||||
|
43
models/migrations/v1_24/v320.go
Normal file
43
models/migrations/v1_24/v320.go
Normal file
@ -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))
|
||||
}
|
217
modules/actions/permissions.go
Normal file
217
modules/actions/permissions.go
Normal file
@ -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)
|
||||
}
|
154
routers/api/v1/actions/oidc.go
Normal file
154
routers/api/v1/actions/oidc.go
Normal file
@ -0,0 +1,154 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// OIDC provider for Gitea Actions
|
||||
package actions
|
||||
|
||||
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/context"
|
||||
"code.gitea.io/gitea/services/oauth2_provider"
|
||||
|
||||
"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 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.
|
||||
|
||||
/*
|
||||
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_provider.SignToken(idToken, oauth2_provider.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),
|
||||
})
|
||||
}
|
@ -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,6 +1127,8 @@ func Routes() *web.Router {
|
||||
})
|
||||
}, tokenRequiresScopes(auth_model.AccessTokenScopeCategoryUser), reqToken())
|
||||
|
||||
m.Get("/actions/id-token/request", actions_router.GenerateOIDCToken)
|
||||
|
||||
// Repositories (requires repo scope, org scope)
|
||||
m.Post("/org/{org}/repos",
|
||||
// FIXME: we need org in context
|
||||
|
@ -353,6 +353,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
|
||||
|
@ -58,9 +58,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
|
||||
@ -88,6 +86,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())
|
||||
|
Loading…
Reference in New Issue
Block a user