This commit is contained in:
Jack Jackson 2025-04-12 19:53:25 -04:00 committed by GitHub
commit 64e91a591c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 707 additions and 13 deletions

2
go.mod
View File

@ -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
View File

@ -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=

View 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)
}

View File

@ -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

View File

@ -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)

View File

@ -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
}

View 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))
}

View 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)
}

View 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),
})
}

View File

@ -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

View File

@ -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

View File

@ -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())