Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

feat: Add configurable permissions for Actions automatic tokens#36173

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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Open
Excellencedev wants to merge18 commits intogo-gitea:main
base:main
Choose a base branch
Loading
fromExcellencedev:fix-24635
Open
Show file tree
Hide file tree
Changes from14 commits
Commits
Show all changes
18 commits
Select commitHold shift + click to select a range
3a10e8f
feat: Add configurable permissions for Actions automatic tokens
ExcellencedevDec 17, 2025
249794c
Merge branch 'main' into fix-24635
ExcellencedevDec 17, 2025
e20d12e
Merge branch 'main' into fix-24635
ExcellencedevDec 18, 2025
9a69f65
Adress all review comments
ExcellencedevDec 18, 2025
43e96d5
WIP
ExcellencedevDec 18, 2025
2a204e3
WIP
ExcellencedevDec 18, 2025
297ecef
Final core implementation changes
ExcellencedevDec 18, 2025
bd4420e
Merge branch 'main' into fix-24635
ExcellencedevDec 18, 2025
5317bb0
Fix lints
ExcellencedevDec 18, 2025
0682fd8
Fix test
ExcellencedevDec 18, 2025
fd1afc5
Fixing Test Failures for Token Permissions
ExcellencedevDec 18, 2025
a4aae82
Fix test
ExcellencedevDec 18, 2025
65051b1
Fix checks
ExcellencedevDec 18, 2025
a6b6e70
update tesr
ExcellencedevDec 18, 2025
b900c5c
Merge branch 'main' into fix-24635
ExcellencedevDec 19, 2025
5eb2f12
wip
ExcellencedevDec 19, 2025
92506da
Merge branch 'fix-24635' of https://github.com/Excellencedev/gitea in…
ExcellencedevDec 19, 2025
8daef63
Adress all reviewer feedback
ExcellencedevDec 19, 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
43 changes: 43 additions & 0 deletionsmodels/actions/config.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package actions

import (
"context"

repo_model"code.gitea.io/gitea/models/repo"
user_model"code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/json"
)

// GetOrgActionsConfig loads the ActionsConfig for an organization from user settings
// It returns a default config if no setting is found
funcGetOrgActionsConfig(ctx context.Context,orgIDint64) (*repo_model.ActionsConfig,error) {
val,err:=user_model.GetUserSetting(ctx,orgID,"actions.config")
iferr!=nil {
returnnil,err
}

cfg:=&repo_model.ActionsConfig{}
ifval=="" {
// Return defaults if no config exists
returncfg,nil
}

iferr:=json.Unmarshal([]byte(val),cfg);err!=nil {
returnnil,err
}

returncfg,nil
}

// SetOrgActionsConfig saves the ActionsConfig for an organization to user settings
funcSetOrgActionsConfig(ctx context.Context,orgIDint64,cfg*repo_model.ActionsConfig)error {
bs,err:=json.Marshal(cfg)
iferr!=nil {
returnerr
}

returnuser_model.SetUserSetting(ctx,orgID,"actions.config",string(bs))
}
55 changes: 45 additions & 10 deletionsmodels/perm/access/repo_permission.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -268,13 +268,31 @@ func GetActionsUserRepoPermission(ctx context.Context, repo *repo_model.Reposito
return perm, err
}

var accessMode perm_model.AccessMode
if err := repo.LoadUnits(ctx); err != nil {
return perm, err
}

actionsUnit := repo.MustGetUnit(ctx, unit.TypeActions)
actionsCfg := actionsUnit.ActionsConfig()

if task.RepoID != repo.ID {
taskRepo, exist, err := db.GetByID[repo_model.Repository](ctx, task.RepoID)
if err != nil || !exist {
return perm, err
}
actionsCfg := repo.MustGetUnit(ctx, unit.TypeActions).ActionsConfig()

// Check Organization Cross-Repo Access Policy
if repo.OwnerID == taskRepo.OwnerID && repo.Owner.IsOrganization() {
orgCfg, err := actions_model.GetOrgActionsConfig(ctx, repo.OwnerID)
if err != nil {
return perm, err
}
if !orgCfg.AllowCrossRepoAccess {
// Deny access if cross-repo is disabled in Org
return perm, nil
}
}

if !actionsCfg.IsCollaborativeOwner(taskRepo.OwnerID) || !taskRepo.IsPrivate {
// The task repo can access the current repo only if the task repo is private and
// the owner of the task repo is a collaborative owner of the current repo.
Expand All@@ -288,17 +306,34 @@ func GetActionsUserRepoPermission(ctx context.Context, repo *repo_model.Reposito
perm.AccessMode = min(perm.AccessMode, perm_model.AccessModeRead)
return perm, nil
}
accessMode = perm_model.AccessModeRead
} else if task.IsForkPullRequest {
accessMode = perm_model.AccessModeRead
} else {
accessMode = perm_model.AccessModeWrite
// Cross-repo access is always read-only
perm.SetUnitsWithDefaultAccessMode(repo.Units, perm_model.AccessModeRead)
return perm, nil
}

if err := repo.LoadUnits(ctx); err != nil {
return perm, err
// Get effective token permissions from repository settings
effectivePerms := actionsCfg.GetEffectiveTokenPermissions(task.IsForkPullRequest)
effectivePerms = actionsCfg.ClampPermissions(effectivePerms)

// 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.TypeIssues] = effectivePerms.Issues
perm.unitsMode[unit.TypePullRequests] = effectivePerms.PullRequests
perm.unitsMode[unit.TypePackages] = effectivePerms.Packages
perm.unitsMode[unit.TypeActions] = effectivePerms.Actions
perm.unitsMode[unit.TypeWiki] = effectivePerms.Wiki

// Set base access mode to the maximum of all unit permissions
maxMode := perm_model.AccessModeNone
for _, mode := range perm.unitsMode {
if mode > maxMode {
maxMode = mode
}
}
perm.SetUnitsWithDefaultAccessMode(repo.Units, accessMode)
perm.AccessMode = maxMode

return perm, nil
}

Expand Down
164 changes: 164 additions & 0 deletionsmodels/repo/repo_unit.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -168,11 +168,122 @@ func (cfg *PullRequestsConfig) GetDefaultMergeStyle() MergeStyle {
return MergeStyleMerge
}

// ActionsTokenPermissionMode defines the default permission mode for Actions tokens
type ActionsTokenPermissionMode string

const (
// ActionsTokenPermissionModePermissive - write access by default (current behavior, backwards compatible)
ActionsTokenPermissionModePermissive ActionsTokenPermissionMode = "permissive"
// ActionsTokenPermissionModeRestricted - read access by default
ActionsTokenPermissionModeRestricted ActionsTokenPermissionMode = "restricted"
// ActionsTokenPermissionModeCustom - user-defined permissions
ActionsTokenPermissionModeCustom ActionsTokenPermissionMode = "custom"
)

// ActionsTokenPermissions defines the permissions for different repository units
type ActionsTokenPermissions struct {
// Contents (repository code) - read/write/none
Contents perm.AccessMode `json:"contents"`
// Issues - read/write/none
Issues perm.AccessMode `json:"issues"`
// PullRequests - read/write/none
PullRequests perm.AccessMode `json:"pull_requests"`
// Packages - read/write/none
Packages perm.AccessMode `json:"packages"`
// Actions - read/write/none
Actions perm.AccessMode `json:"actions"`
// Wiki - read/write/none
Wiki perm.AccessMode `json:"wiki"`
}

// HasRead checks if the permission has read access for the given scope
func (p ActionsTokenPermissions) HasRead(scope string) bool {
var mode perm.AccessMode
switch scope {
case "actions":
mode = p.Actions
case "contents":
mode = p.Contents
case "issues":
mode = p.Issues
case "packages":
mode = p.Packages
case "pull_requests":
mode = p.PullRequests
case "wiki":
mode = p.Wiki
}
return mode >= perm.AccessModeRead
}

// HasWrite checks if the permission has write access for the given scope
func (p ActionsTokenPermissions) HasWrite(scope string) bool {
var mode perm.AccessMode
switch scope {
case "actions":
mode = p.Actions
case "contents":
mode = p.Contents
case "issues":
mode = p.Issues
case "packages":
mode = p.Packages
case "pull_requests":
mode = p.PullRequests
case "wiki":
mode = p.Wiki
}
return mode >= perm.AccessModeWrite
}

// DefaultActionsTokenPermissions returns the default permissions for permissive mode
func DefaultActionsTokenPermissions(mode ActionsTokenPermissionMode) ActionsTokenPermissions {
if mode == ActionsTokenPermissionModeRestricted {
return ActionsTokenPermissions{
Contents: perm.AccessModeRead,
Issues: perm.AccessModeRead,
PullRequests: perm.AccessModeRead,
Packages: perm.AccessModeRead,
Actions: perm.AccessModeRead,
Wiki: perm.AccessModeRead,
}
}
// Permissive mode (default)
return ActionsTokenPermissions{
Contents: perm.AccessModeWrite,
Issues: perm.AccessModeWrite,
PullRequests: perm.AccessModeWrite,
Packages: perm.AccessModeRead, // Packages read by default for security
Actions: perm.AccessModeWrite,
Wiki: perm.AccessModeWrite,
}
}

// ForkPullRequestPermissions returns the restricted permissions for fork pull requests
func ForkPullRequestPermissions() ActionsTokenPermissions {
return ActionsTokenPermissions{
Contents: perm.AccessModeRead,
Issues: perm.AccessModeRead,
PullRequests: perm.AccessModeRead,
Packages: perm.AccessModeRead,
Actions: perm.AccessModeRead,
Wiki: perm.AccessModeRead,
}
}

type ActionsConfig struct {
DisabledWorkflows []string
// CollaborativeOwnerIDs is a list of owner IDs used to share actions from private repos.
// Only workflows from the private repos whose owners are in CollaborativeOwnerIDs can access the current repo's actions.
CollaborativeOwnerIDs []int64
// TokenPermissionMode defines the default permission mode (permissive or restricted)
TokenPermissionMode ActionsTokenPermissionMode `json:"token_permission_mode,omitempty"`
// DefaultTokenPermissions defines the default permissions for workflow tokens
DefaultTokenPermissions *ActionsTokenPermissions `json:"default_token_permissions,omitempty"`
// MaxTokenPermissions defines the maximum permissions (cannot be exceeded by workflow permissions keyword)
MaxTokenPermissions *ActionsTokenPermissions `json:"max_token_permissions,omitempty"`
// AllowCrossRepoAccess indicates if actions in this repo/org can access other repos in the same org
AllowCrossRepoAccess bool `json:"allow_cross_repo_access,omitempty"`
}

func (cfg *ActionsConfig) EnableWorkflow(file string) {
Expand DownExpand Up@@ -209,6 +320,59 @@ func (cfg *ActionsConfig) IsCollaborativeOwner(ownerID int64) bool {
return slices.Contains(cfg.CollaborativeOwnerIDs, ownerID)
}

// GetTokenPermissionMode returns the token permission mode (defaults to permissive for backwards compatibility)
func (cfg *ActionsConfig) GetTokenPermissionMode() ActionsTokenPermissionMode {
if cfg.TokenPermissionMode == "" {
return ActionsTokenPermissionModePermissive
}
return cfg.TokenPermissionMode
}

// GetEffectiveTokenPermissions returns the effective token permissions based on settings and context
func (cfg *ActionsConfig) GetEffectiveTokenPermissions(isForkPullRequest bool) ActionsTokenPermissions {
// Fork pull requests always get restricted read-only access for security
if isForkPullRequest {
return ForkPullRequestPermissions()
}

// Use custom default permissions if set
if cfg.DefaultTokenPermissions != nil {
return *cfg.DefaultTokenPermissions
}

// Otherwise use mode-based defaults
return DefaultActionsTokenPermissions(cfg.GetTokenPermissionMode())
}

// GetMaxTokenPermissions returns the maximum allowed permissions
func (cfg *ActionsConfig) GetMaxTokenPermissions() ActionsTokenPermissions {
if cfg.MaxTokenPermissions != nil {
return *cfg.MaxTokenPermissions
}
// Default max is write for everything except packages
return ActionsTokenPermissions{
Contents: perm.AccessModeWrite,
Issues: perm.AccessModeWrite,
PullRequests: perm.AccessModeWrite,
Packages: perm.AccessModeWrite,
Actions: perm.AccessModeWrite,
Wiki: perm.AccessModeWrite,
}
}

// ClampPermissions ensures that the given permissions don't exceed the maximum
func (cfg *ActionsConfig) ClampPermissions(perms ActionsTokenPermissions) ActionsTokenPermissions {
maxPerms := cfg.GetMaxTokenPermissions()
return ActionsTokenPermissions{
Contents: min(perms.Contents, maxPerms.Contents),
Issues: min(perms.Issues, maxPerms.Issues),
PullRequests: min(perms.PullRequests, maxPerms.PullRequests),
Packages: min(perms.Packages, maxPerms.Packages),
Actions: min(perms.Actions, maxPerms.Actions),
Wiki: min(perms.Wiki, maxPerms.Wiki),
}
}

// FromDB fills up a ActionsConfig from serialized format.
func (cfg *ActionsConfig) FromDB(bs []byte) error {
return json.UnmarshalHandleDoubleEncode(bs, &cfg)
Expand Down
75 changes: 75 additions & 0 deletionsmodels/repo/repo_unit_test.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,8 @@ package repo
import (
"testing"

"code.gitea.io/gitea/models/perm"

"github.com/stretchr/testify/assert"
)

Expand All@@ -28,3 +30,76 @@ func TestActionsConfig(t *testing.T) {
cfg.DisableWorkflow("test3.yaml")
assert.Equal(t,"test1.yaml,test2.yaml,test3.yaml",cfg.ToString())
}

funcTestActionsConfigTokenPermissions(t*testing.T) {
t.Run("Default Permission Mode",func(t*testing.T) {
cfg:=&ActionsConfig{}
assert.Equal(t,ActionsTokenPermissionModePermissive,cfg.GetTokenPermissionMode())
})

t.Run("Explicit Permission Mode",func(t*testing.T) {
cfg:=&ActionsConfig{
TokenPermissionMode:ActionsTokenPermissionModeRestricted,
}
assert.Equal(t,ActionsTokenPermissionModeRestricted,cfg.GetTokenPermissionMode())
})

t.Run("Effective Permissions - Permissive Mode",func(t*testing.T) {
cfg:=&ActionsConfig{
TokenPermissionMode:ActionsTokenPermissionModePermissive,
}
perms:=cfg.GetEffectiveTokenPermissions(false)
assert.Equal(t,perm.AccessModeWrite,perms.Contents)
assert.Equal(t,perm.AccessModeWrite,perms.Issues)
assert.Equal(t,perm.AccessModeRead,perms.Packages)// Packages read by default for security
})

t.Run("Effective Permissions - Restricted Mode",func(t*testing.T) {
cfg:=&ActionsConfig{
TokenPermissionMode:ActionsTokenPermissionModeRestricted,
}
perms:=cfg.GetEffectiveTokenPermissions(false)
assert.Equal(t,perm.AccessModeRead,perms.Contents)
assert.Equal(t,perm.AccessModeRead,perms.Issues)
assert.Equal(t,perm.AccessModeRead,perms.Packages)
})

t.Run("Fork Pull Request Always Read-Only",func(t*testing.T) {
cfg:=&ActionsConfig{
TokenPermissionMode:ActionsTokenPermissionModePermissive,
}
// 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.Issues)
assert.Equal(t,perm.AccessModeRead,perms.Packages)
})

t.Run("Clamp Permissions",func(t*testing.T) {
cfg:=&ActionsConfig{
MaxTokenPermissions:&ActionsTokenPermissions{
Contents:perm.AccessModeRead,
Issues:perm.AccessModeWrite,
PullRequests:perm.AccessModeRead,
Packages:perm.AccessModeRead,
Actions:perm.AccessModeNone,
Wiki:perm.AccessModeWrite,
},
}
input:=ActionsTokenPermissions{
Contents: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
Actions:perm.AccessModeRead,// Should be clamped to None
Wiki:perm.AccessModeRead,// Should stay Read
}
clamped:=cfg.ClampPermissions(input)
assert.Equal(t,perm.AccessModeRead,clamped.Contents)
assert.Equal(t,perm.AccessModeWrite,clamped.Issues)
assert.Equal(t,perm.AccessModeRead,clamped.PullRequests)
assert.Equal(t,perm.AccessModeRead,clamped.Packages)
assert.Equal(t,perm.AccessModeNone,clamped.Actions)
assert.Equal(t,perm.AccessModeRead,clamped.Wiki)
})
}
Loading
Loading

[8]ページ先頭

©2009-2025 Movatter.jp