0
0
mirror of https://github.com/go-gitea/gitea.git synced 2026-02-12 02:27:08 +01:00
This commit is contained in:
Excellencedev 2025-12-31 19:14:10 +01:00
parent 2e7bd47be6
commit f3b14570f2
11 changed files with 233 additions and 34 deletions

View File

@ -399,6 +399,7 @@ func prepareMigrationTasks() []*migration {
newMigration(323, "Add support for actions concurrency", v1_26.AddActionsConcurrency),
newMigration(324, "Fix closed milestone completeness for milestones with no issues", v1_26.FixClosedMilestoneCompleteness),
newMigration(325, "Add TokenPermissions column to ActionRunJob", v1_26.AddTokenPermissionsToActionRunJob),
}
return preparedMigrations
}

View File

@ -0,0 +1,15 @@
// Copyright 2025 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package v1_26
import (
"xorm.io/xorm"
)
func AddTokenPermissionsToActionRunJob(x *xorm.Engine) error {
type ActionRunJob struct {
TokenPermissions string `xorm:"TEXT"`
}
return x.Sync(new(ActionRunJob))
}

View File

@ -357,7 +357,7 @@ func GetActionsUserRepoPermission(ctx context.Context, repo *repo_model.Reposito
// Set up per-unit access modes based on configured permissions
perm.units = repo.Units
perm.unitsMode = make(map[unit.Type]perm_model.AccessMode)
perm.unitsMode[unit.TypeCode] = effectivePerms.Contents
perm.unitsMode[unit.TypeCode] = effectivePerms.Code
perm.unitsMode[unit.TypeIssues] = effectivePerms.Issues
perm.unitsMode[unit.TypePullRequests] = effectivePerms.PullRequests
perm.unitsMode[unit.TypePackages] = effectivePerms.Packages

View File

@ -182,8 +182,8 @@ const (
// ActionsTokenPermissions defines the permissions for different repository units
type ActionsTokenPermissions struct {
// Contents (repository code) - read/write/none
Contents perm.AccessMode `json:"contents"`
// Code (repository code) - read/write/none
Code perm.AccessMode `json:"contents"`
// Issues - read/write/none
Issues perm.AccessMode `json:"issues"`
// PullRequests - read/write/none
@ -203,7 +203,7 @@ func (p ActionsTokenPermissions) HasAccess(scope string, required perm.AccessMod
case "actions":
mode = p.Actions
case "contents":
mode = p.Contents
mode = p.Code
case "issues":
mode = p.Issues
case "packages":
@ -230,7 +230,7 @@ func (p ActionsTokenPermissions) HasWrite(scope string) bool {
func DefaultActionsTokenPermissions(mode ActionsTokenPermissionMode) ActionsTokenPermissions {
if mode == ActionsTokenPermissionModeRestricted {
return ActionsTokenPermissions{
Contents: perm.AccessModeRead,
Code: perm.AccessModeRead,
Issues: perm.AccessModeRead,
PullRequests: perm.AccessModeRead,
Packages: perm.AccessModeRead,
@ -240,7 +240,7 @@ func DefaultActionsTokenPermissions(mode ActionsTokenPermissionMode) ActionsToke
}
// Permissive mode (default)
return ActionsTokenPermissions{
Contents: perm.AccessModeWrite,
Code: perm.AccessModeWrite,
Issues: perm.AccessModeWrite,
PullRequests: perm.AccessModeWrite,
Packages: perm.AccessModeRead, // Packages read by default for security
@ -252,7 +252,7 @@ func DefaultActionsTokenPermissions(mode ActionsTokenPermissionMode) ActionsToke
// ForkPullRequestPermissions returns the restricted permissions for fork pull requests
func ForkPullRequestPermissions() ActionsTokenPermissions {
return ActionsTokenPermissions{
Contents: perm.AccessModeRead,
Code: perm.AccessModeRead,
Issues: perm.AccessModeRead,
PullRequests: perm.AccessModeRead,
Packages: perm.AccessModeRead,
@ -364,7 +364,7 @@ func (cfg *ActionsConfig) GetMaxTokenPermissions() ActionsTokenPermissions {
}
// Default max is write for everything except packages
return ActionsTokenPermissions{
Contents: perm.AccessModeWrite,
Code: perm.AccessModeWrite,
Issues: perm.AccessModeWrite,
PullRequests: perm.AccessModeWrite,
Packages: perm.AccessModeWrite,
@ -377,7 +377,7 @@ func (cfg *ActionsConfig) GetMaxTokenPermissions() ActionsTokenPermissions {
func (cfg *ActionsConfig) ClampPermissions(perms ActionsTokenPermissions) ActionsTokenPermissions {
maxPerms := cfg.GetMaxTokenPermissions()
return ActionsTokenPermissions{
Contents: min(perms.Contents, maxPerms.Contents),
Code: min(perms.Code, maxPerms.Code),
Issues: min(perms.Issues, maxPerms.Issues),
PullRequests: min(perms.PullRequests, maxPerms.PullRequests),
Packages: min(perms.Packages, maxPerms.Packages),

View File

@ -49,7 +49,7 @@ func TestActionsConfigTokenPermissions(t *testing.T) {
TokenPermissionMode: ActionsTokenPermissionModePermissive,
}
perms := cfg.GetEffectiveTokenPermissions(false)
assert.Equal(t, perm.AccessModeWrite, perms.Contents)
assert.Equal(t, perm.AccessModeWrite, perms.Code)
assert.Equal(t, perm.AccessModeWrite, perms.Issues)
assert.Equal(t, perm.AccessModeRead, perms.Packages) // Packages read by default for security
})
@ -59,7 +59,7 @@ func TestActionsConfigTokenPermissions(t *testing.T) {
TokenPermissionMode: ActionsTokenPermissionModeRestricted,
}
perms := cfg.GetEffectiveTokenPermissions(false)
assert.Equal(t, perm.AccessModeRead, perms.Contents)
assert.Equal(t, perm.AccessModeRead, perms.Code)
assert.Equal(t, perm.AccessModeRead, perms.Issues)
assert.Equal(t, perm.AccessModeRead, perms.Packages)
})
@ -70,7 +70,7 @@ func TestActionsConfigTokenPermissions(t *testing.T) {
}
// Even with permissive mode, fork PRs get read-only
perms := cfg.GetEffectiveTokenPermissions(true)
assert.Equal(t, perm.AccessModeRead, perms.Contents)
assert.Equal(t, perm.AccessModeRead, perms.Code)
assert.Equal(t, perm.AccessModeRead, perms.Issues)
assert.Equal(t, perm.AccessModeRead, perms.Packages)
})
@ -78,7 +78,7 @@ func TestActionsConfigTokenPermissions(t *testing.T) {
t.Run("Clamp Permissions", func(t *testing.T) {
cfg := &ActionsConfig{
MaxTokenPermissions: &ActionsTokenPermissions{
Contents: perm.AccessModeRead,
Code: perm.AccessModeRead,
Issues: perm.AccessModeWrite,
PullRequests: perm.AccessModeRead,
Packages: perm.AccessModeRead,
@ -87,7 +87,7 @@ func TestActionsConfigTokenPermissions(t *testing.T) {
},
}
input := ActionsTokenPermissions{
Contents: perm.AccessModeWrite, // Should be clamped to Read
Code: perm.AccessModeWrite, // Should be clamped to Read
Issues: perm.AccessModeWrite, // Should stay Write
PullRequests: perm.AccessModeWrite, // Should be clamped to Read
Packages: perm.AccessModeWrite, // Should be clamped to Read
@ -95,7 +95,7 @@ func TestActionsConfigTokenPermissions(t *testing.T) {
Wiki: perm.AccessModeRead, // Should stay Read
}
clamped := cfg.ClampPermissions(input)
assert.Equal(t, perm.AccessModeRead, clamped.Contents)
assert.Equal(t, perm.AccessModeRead, clamped.Code)
assert.Equal(t, perm.AccessModeWrite, clamped.Issues)
assert.Equal(t, perm.AccessModeRead, clamped.PullRequests)
assert.Equal(t, perm.AccessModeRead, clamped.Packages)

View File

@ -76,8 +76,7 @@ func ActionsGeneralPost(ctx *context.Context) {
}
actionsCfg.MaxTokenPermissions = &repo_model.ActionsTokenPermissions{
Actions: parseMaxPerm("actions"),
Contents: parseMaxPerm("contents"),
Code: parseMaxPerm("contents"),
Issues: parseMaxPerm("issues"),
Packages: parseMaxPerm("packages"),
PullRequests: parseMaxPerm("pull_requests"),

View File

@ -34,6 +34,7 @@ import (
context_module "code.gitea.io/gitea/services/context"
notify_service "code.gitea.io/gitea/services/notify"
"github.com/nektos/act/pkg/jobparser"
"github.com/nektos/act/pkg/model"
"gopkg.in/yaml.v3"
"xorm.io/builder"
@ -536,8 +537,37 @@ func rerunJob(ctx *context_module.Context, job *actions_model.ActionRunJob, shou
}
}
// Recalculate permissions on rerun to respect current repo settings
if len(job.WorkflowPayload) > 0 {
singleWorkflow, err := jobparser.Parse(job.WorkflowPayload)
if err != nil {
log.Warn("rerunJob: failed to parse workflow payload for job %d: %v", job.ID, err)
} else {
for _, flow := range singleWorkflow {
wfJobID, wfJob := flow.Job()
if wfJobID == job.JobID {
if job.Run.Repo == nil {
if err := job.Run.LoadRepo(ctx); err != nil {
return err
}
}
cfgUnit := job.Run.Repo.MustGetUnit(ctx, unit.TypeActions)
cfg := cfgUnit.ActionsConfig()
defaultPerms := cfg.GetEffectiveTokenPermissions(job.Run.IsForkPullRequest)
workflowPerms := actions_service.ParseWorkflowPermissions(flow, defaultPerms)
jobPerms := actions_service.ParseJobPermissions(wfJob, workflowPerms)
finalPerms := cfg.ClampPermissions(jobPerms)
job.TokenPermissions = repo_model.MarshalTokenPermissions(finalPerms)
break
}
}
}
}
if err := db.WithTx(ctx, func(ctx context.Context) error {
updateCols := []string{"task_id", "status", "started", "stopped", "concurrency_group", "concurrency_cancel", "is_concurrency_evaluated"}
updateCols := []string{"task_id", "status", "started", "stopped", "concurrency_group", "concurrency_cancel", "is_concurrency_evaluated", "token_permissions"}
_, err := actions_model.UpdateRunJob(ctx, job, builder.Eq{"status": status}, updateCols...)
return err
}); err != nil {

View File

@ -177,8 +177,7 @@ func UpdateTokenPermissions(ctx *context.Context) {
}
actionsCfg.MaxTokenPermissions = &repo_model.ActionsTokenPermissions{
Actions: parseMaxPerm("actions"),
Contents: parseMaxPerm("contents"),
Code: parseMaxPerm("contents"),
Issues: parseMaxPerm("issues"),
Packages: parseMaxPerm("packages"),
PullRequests: parseMaxPerm("pull_requests"),

View File

@ -64,7 +64,7 @@ func parseRawPermissions(rawPerms *yaml.Node, defaultPerms repo_model.ActionsTok
switch node.Value {
case "read-all":
return repo_model.ActionsTokenPermissions{
Contents: perm.AccessModeRead,
Code: perm.AccessModeRead,
Issues: perm.AccessModeRead,
PullRequests: perm.AccessModeRead,
Packages: perm.AccessModeRead,
@ -73,7 +73,7 @@ func parseRawPermissions(rawPerms *yaml.Node, defaultPerms repo_model.ActionsTok
}
case "write-all":
return repo_model.ActionsTokenPermissions{
Contents: perm.AccessModeWrite,
Code: perm.AccessModeWrite,
Issues: perm.AccessModeWrite,
PullRequests: perm.AccessModeWrite,
Packages: perm.AccessModeWrite,
@ -106,7 +106,7 @@ func parseRawPermissions(rawPerms *yaml.Node, defaultPerms repo_model.ActionsTok
// Map GitHub Actions scopes to Gitea units
switch scope {
case "contents":
result.Contents = accessMode
result.Code = accessMode
case "issues":
result.Issues = accessMode
case "pull-requests":

View File

@ -21,7 +21,7 @@ func TestParseRawPermissions_ReadAll(t *testing.T) {
defaultPerms := repo_model.DefaultActionsTokenPermissions(repo_model.ActionsTokenPermissionModePermissive)
result := parseRawPermissions(&rawPerms, defaultPerms)
assert.Equal(t, perm.AccessModeRead, result.Contents)
assert.Equal(t, perm.AccessModeRead, result.Code)
assert.Equal(t, perm.AccessModeRead, result.Issues)
assert.Equal(t, perm.AccessModeRead, result.PullRequests)
assert.Equal(t, perm.AccessModeRead, result.Packages)
@ -37,7 +37,7 @@ func TestParseRawPermissions_WriteAll(t *testing.T) {
defaultPerms := repo_model.DefaultActionsTokenPermissions(repo_model.ActionsTokenPermissionModeRestricted)
result := parseRawPermissions(&rawPerms, defaultPerms)
assert.Equal(t, perm.AccessModeWrite, result.Contents)
assert.Equal(t, perm.AccessModeWrite, result.Code)
assert.Equal(t, perm.AccessModeWrite, result.Issues)
assert.Equal(t, perm.AccessModeWrite, result.PullRequests)
assert.Equal(t, perm.AccessModeWrite, result.Packages)
@ -59,7 +59,7 @@ wiki: write
assert.NoError(t, err)
defaultPerms := repo_model.ActionsTokenPermissions{
Contents: perm.AccessModeNone,
Code: perm.AccessModeNone,
Issues: perm.AccessModeNone,
PullRequests: perm.AccessModeNone,
Packages: perm.AccessModeNone,
@ -68,7 +68,7 @@ wiki: write
}
result := parseRawPermissions(&rawPerms, defaultPerms)
assert.Equal(t, perm.AccessModeWrite, result.Contents)
assert.Equal(t, perm.AccessModeWrite, result.Code)
assert.Equal(t, perm.AccessModeRead, result.Issues)
assert.Equal(t, perm.AccessModeNone, result.PullRequests)
assert.Equal(t, perm.AccessModeWrite, result.Packages)
@ -90,7 +90,7 @@ issues: write
result := parseRawPermissions(&rawPerms, defaultPerms)
// Overridden scopes
assert.Equal(t, perm.AccessModeRead, result.Contents)
assert.Equal(t, perm.AccessModeRead, result.Code)
assert.Equal(t, perm.AccessModeWrite, result.Issues)
// Non-overridden scopes keep defaults
assert.Equal(t, perm.AccessModeWrite, result.PullRequests)
@ -107,7 +107,7 @@ func TestParseRawPermissions_EmptyNode(t *testing.T) {
result := parseRawPermissions(&rawPerms, defaultPerms)
// Should return defaults
assert.Equal(t, defaultPerms.Contents, result.Contents)
assert.Equal(t, defaultPerms.Code, result.Code)
assert.Equal(t, defaultPerms.Issues, result.Issues)
}
@ -116,7 +116,7 @@ func TestParseRawPermissions_NilNode(t *testing.T) {
result := parseRawPermissions(nil, defaultPerms)
// Should return defaults
assert.Equal(t, defaultPerms.Contents, result.Contents)
assert.Equal(t, defaultPerms.Code, result.Code)
assert.Equal(t, defaultPerms.Issues, result.Issues)
}
@ -142,7 +142,7 @@ func TestParseAccessMode(t *testing.T) {
func TestMarshalUnmarshalTokenPermissions(t *testing.T) {
original := repo_model.ActionsTokenPermissions{
Contents: perm.AccessModeWrite,
Code: perm.AccessModeWrite,
Issues: perm.AccessModeRead,
PullRequests: perm.AccessModeNone,
Packages: perm.AccessModeWrite,
@ -164,6 +164,6 @@ func TestUnmarshalTokenPermissions_EmptyString(t *testing.T) {
result, err := repo_model.UnmarshalTokenPermissions("")
assert.NoError(t, err)
// Should return zero-value struct
assert.Equal(t, perm.AccessModeNone, result.Contents)
assert.Equal(t, perm.AccessModeNone, result.Code)
assert.Equal(t, perm.AccessModeNone, result.Issues)
}

View File

@ -269,7 +269,7 @@ func TestActionsTokenPermissionsClamping(t *testing.T) {
Config: &repo_model.ActionsConfig{
TokenPermissionMode: repo_model.ActionsTokenPermissionModePermissive,
MaxTokenPermissions: &repo_model.ActionsTokenPermissions{
Contents: perm.AccessModeRead, // Max is Read - will clamp default Write to Read
Code: perm.AccessModeRead, // Max is Read - will clamp default Write to Read
},
},
})
@ -535,7 +535,7 @@ func TestActionsWorkflowPermissionsKeyword(t *testing.T) {
// Create an Actions run job with TokenPermissions set (simulating a workflow with permissions: read-all)
// This is what the permission parser does when parsing the workflow YAML
readOnlyPerms := repo_model.ActionsTokenPermissions{
Contents: perm.AccessModeRead,
Code: perm.AccessModeRead,
Issues: perm.AccessModeRead,
PullRequests: perm.AccessModeRead,
Packages: perm.AccessModeRead,
@ -607,6 +607,161 @@ func TestActionsWorkflowPermissionsKeyword(t *testing.T) {
},
ContentBase64: base64.StdEncoding.EncodeToString([]byte("Should Not Be Created")),
}))
// Subtest: Verify that job-level overriding works
// Create another job with `permissions: contents: write` to override `read-all`
// Logic: Workflow read-all -> Code: Read. Job contents:write -> Code: Write.
// Repo is Permissive (Max: Write). So Result: Write.
overridePerms := repo_model.ActionsTokenPermissions{
Code: perm.AccessModeWrite,
Issues: perm.AccessModeRead,
PullRequests: perm.AccessModeRead,
Packages: perm.AccessModeRead,
Actions: perm.AccessModeRead,
Wiki: perm.AccessModeRead,
}
overridePermsJSON := repo_model.MarshalTokenPermissions(overridePerms)
jobOverride := &actions_model.ActionRunJob{
RunID: run.ID,
RepoID: repository.ID,
OwnerID: repository.Owner.ID,
CommitSHA: "abc123",
Name: "test-job-override",
JobID: "test-job-override",
Status: actions_model.StatusRunning,
TokenPermissions: overridePermsJSON,
}
require.NoError(t, db.Insert(t.Context(), jobOverride))
taskOverride := &actions_model.ActionTask{
JobID: jobOverride.ID,
RepoID: repository.ID,
Status: actions_model.StatusRunning,
IsForkPullRequest: false,
}
require.NoError(t, taskOverride.GenerateToken())
require.NoError(t, db.Insert(t.Context(), taskOverride))
jobOverride.TaskID = taskOverride.ID
_, err = db.GetEngine(t.Context()).ID(jobOverride.ID).Cols("task_id").Update(jobOverride)
require.NoError(t, err)
testCtxOverride := APITestContext{
Session: session,
Token: taskOverride.Token,
Username: "user2",
Reponame: "repo-workflow-perms-kw",
}
testCtxOverride.ExpectedCode = http.StatusCreated
t.Run("GITEA_TOKEN Create File (Write ALLOWED by job override)", doAPICreateFile(testCtxOverride, "should-succeed-override.txt", &structs.CreateFileOptions{
FileOptions: structs.FileOptions{
BranchName: "master",
Message: "this should succeed due to job permissions override",
},
ContentBase64: base64.StdEncoding.EncodeToString([]byte("Should Be Created")),
}))
}))
})
}
func TestActionsRerunPermissions(t *testing.T) {
onGiteaRun(t, func(t *testing.T, u *url.URL) {
session := loginUser(t, "user2")
httpContext := NewAPITestContext(t, "user2", "repo-rerun-perms", auth_model.AccessTokenScopeWriteUser, auth_model.AccessTokenScopeWriteRepository)
t.Run("Rerun Permissions", doAPICreateRepository(httpContext, false, func(t *testing.T, repository structs.Repository) {
// 1. Enable Actions with PERMISSIVE mode
err := db.Insert(t.Context(), &repo_model.RepoUnit{
RepoID: repository.ID,
Type: unit_model.TypeActions,
Config: &repo_model.ActionsConfig{
TokenPermissionMode: repo_model.ActionsTokenPermissionModePermissive,
},
})
require.NoError(t, err)
// 2. Create Run and Job with implicit permissions (no parsed perms stored yet)
// or with parsed perms that allow write (Permissive default)
workflowPayload := `
name: Test Rerun
on: workflow_dispatch
jobs:
test-rerun:
runs-on: ubuntu-latest
steps:
- run: echo hello
`
run := &actions_model.ActionRun{
RepoID: repository.ID,
OwnerID: repository.Owner.ID,
Title: "Test Rerun",
Status: actions_model.StatusSuccess, // Run finished
Ref: "refs/heads/master",
CommitSHA: "abc123",
WorkflowID: "test-rerun.yaml",
TriggerUserID: repository.Owner.ID,
}
require.NoError(t, db.Insert(t.Context(), run))
// Initial permissions: Permissive (Write)
initialPerms := repo_model.ActionsTokenPermissions{
Code: perm.AccessModeWrite,
}
job := &actions_model.ActionRunJob{
RunID: run.ID,
RepoID: repository.ID,
OwnerID: repository.Owner.ID,
CommitSHA: "abc123",
Name: "test-rerun",
JobID: "test-rerun",
Status: actions_model.StatusSuccess, // Job finished
WorkflowPayload: []byte(workflowPayload),
TokenPermissions: repo_model.MarshalTokenPermissions(initialPerms),
}
require.NoError(t, db.Insert(t.Context(), job))
// 3. Change Repo Settings to RESTRICTED
// We need to update the RepoUnit config
unitConfig := &repo_model.ActionsConfig{
TokenPermissionMode: repo_model.ActionsTokenPermissionModeRestricted,
}
// Update the specific unit
// Need to find the unit first
repo, err := repo_model.GetRepositoryByID(t.Context(), repository.ID)
require.NoError(t, err)
unit, err := repo.GetUnit(t.Context(), unit_model.TypeActions)
require.NoError(t, err)
unit.Config = unitConfig
require.NoError(t, repo_model.UpdateRepoUnit(t.Context(), unit))
// 4. Trigger Rerun via Web Handler
// POST /:username/:reponame/actions/runs/:index/rerun
// We need to know operation run index. Since it's the first run, it should be 1?
// ActionRun.Index is auto-increment but not set in my insert.
// Ideally we use CreateRun which handles index.
// Let's manually set index 1.
run.Index = 1
_, err = db.GetEngine(t.Context()).ID(run.ID).Cols("index").Update(run)
require.NoError(t, err)
req := NewRequest(t, "POST", fmt.Sprintf("/%s/%s/actions/runs/%d/rerun", "user2", "repo-rerun-perms", run.Index))
session.MakeRequest(t, req, http.StatusOK)
// 5. Verify TokenPermissions in DB are now Restricted (Read-only)
// Reload job
jobReload := new(actions_model.ActionRunJob)
has, err := db.GetEngine(t.Context()).ID(job.ID).Get(jobReload)
require.NoError(t, err)
assert.True(t, has)
// Check permissions
perms, err := repo_model.UnmarshalTokenPermissions(jobReload.TokenPermissions)
require.NoError(t, err)
// Should be restricted (Read)
assert.Equal(t, perm.AccessModeRead, perms.Code, "Permissions should be restricted to Read after rerun in restricted mode")
}))
})
}