Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
92 commits
Select commit Hold shift + click to select a range
3a10e8f
feat: Add configurable permissions for Actions automatic tokens
Excellencedev Dec 17, 2025
249794c
Merge branch 'main' into fix-24635
Excellencedev Dec 17, 2025
e20d12e
Merge branch 'main' into fix-24635
Excellencedev Dec 18, 2025
9a69f65
Adress all review comments
Excellencedev Dec 18, 2025
43e96d5
WIP
Excellencedev Dec 18, 2025
2a204e3
WIP
Excellencedev Dec 18, 2025
297ecef
Final core implementation changes
Excellencedev Dec 18, 2025
bd4420e
Merge branch 'main' into fix-24635
Excellencedev Dec 18, 2025
5317bb0
Fix lints
Excellencedev Dec 18, 2025
0682fd8
Fix test
Excellencedev Dec 18, 2025
fd1afc5
Fixing Test Failures for Token Permissions
Excellencedev Dec 18, 2025
a4aae82
Fix test
Excellencedev Dec 18, 2025
65051b1
Fix checks
Excellencedev Dec 18, 2025
a6b6e70
update tesr
Excellencedev Dec 18, 2025
b900c5c
Merge branch 'main' into fix-24635
Excellencedev Dec 19, 2025
5eb2f12
wip
Excellencedev Dec 19, 2025
92506da
Merge branch 'fix-24635' of https://github.com/Excellencedev/gitea in…
Excellencedev Dec 19, 2025
8daef63
Adress all reviewer feedback
Excellencedev Dec 19, 2025
5fd6d0e
Merge branch 'main' into fix-24635
Excellencedev Dec 19, 2025
af229dc
Merge branch 'main' into fix-24635
Excellencedev Dec 19, 2025
79a5d07
Completely redesign UI
Excellencedev Dec 20, 2025
d97712c
Merge branch 'fix-24635' of https://github.com/Excellencedev/gitea in…
Excellencedev Dec 20, 2025
058fc07
fix conflixt
Excellencedev Dec 20, 2025
04a6658
Merge remote-tracking branch 'origin/main' into fix-24635
Excellencedev Dec 20, 2025
64c2147
Adapt to JSON format
Excellencedev Dec 20, 2025
eca961e
Minor fixes
Excellencedev Dec 20, 2025
a5163fa
minor nitpick
Excellencedev Dec 20, 2025
9c5b278
Fix all bugs I found in the code
Excellencedev Dec 20, 2025
b0811fe
Formatting issues
Excellencedev Dec 20, 2025
b2f05ff
fix test
Excellencedev Dec 20, 2025
06b3db5
Improve test coverage
Excellencedev Dec 20, 2025
b0c2a95
Format
Excellencedev Dec 20, 2025
5628ab7
lint
Excellencedev Dec 20, 2025
38f384a
fmt
Excellencedev Dec 20, 2025
6cc6fd7
lint
Excellencedev Dec 20, 2025
463c670
issue fix
Excellencedev Dec 20, 2025
d25de6f
test fix
Excellencedev Dec 20, 2025
6d94723
regression
Excellencedev Dec 20, 2025
663d9b2
empty commit
Excellencedev Dec 20, 2025
34d13de
Merge branch 'main' into fix-24635
Excellencedev Dec 20, 2025
a72803d
Merge branch 'main' into fix-24635
Excellencedev Dec 22, 2025
5da2b95
Merge branch 'main' into fix-24635
Excellencedev Dec 22, 2025
302f888
fix ui
Excellencedev Dec 22, 2025
fe5230c
more ui fixes
Excellencedev Dec 23, 2025
50f300b
Merge branch 'main' into fix-24635
Excellencedev Dec 23, 2025
9bd8b81
Review comment fixes
Excellencedev Dec 24, 2025
9ada493
Add cross repo package access test
Excellencedev Dec 24, 2025
43279bf
Use correct authentication
Excellencedev Dec 24, 2025
ebf7e2e
fix test
Excellencedev Dec 24, 2025
973de05
review comment
Excellencedev Dec 24, 2025
e52ff98
Fix updated test
Excellencedev Dec 24, 2025
7df7f72
Merge branch 'main' into fix-24635
Excellencedev Dec 26, 2025
4ccb766
Re-implement changes for feedback
Excellencedev Dec 26, 2025
c7d2080
LOGS
Excellencedev Dec 26, 2025
26e47a5
....
Excellencedev Dec 26, 2025
640004f
test fix
Excellencedev Dec 26, 2025
621c0e2
...
Excellencedev Dec 26, 2025
ae43a09
...
Excellencedev Dec 26, 2025
f9f24dd
Make all CI green again
Excellencedev Dec 26, 2025
4fb4989
Merge branch 'main' into fix-24635
Excellencedev Dec 30, 2025
efb93b5
Fixes
Excellencedev Dec 30, 2025
de0d8da
Make sure pre-receive hook can properly check Actions token permissio…
Excellencedev Dec 30, 2025
4a9a54e
...
Excellencedev Dec 30, 2025
dcfe19c
fmt
Excellencedev Dec 30, 2025
f367039
make lint fixes
Excellencedev Dec 30, 2025
1ff75aa
Implement Workflow Level Permissions
Excellencedev Dec 31, 2025
2e7bd47
chore: fix ci
Excellencedev Dec 31, 2025
f3b1457
Feedback
Excellencedev Dec 31, 2025
3af786c
Merge branch 'main' into fix-24635
Excellencedev Jan 1, 2026
cdbed1d
Merge branch 'main' into fix-24635
Excellencedev Jan 2, 2026
a944be1
improve test
Excellencedev Jan 2, 2026
91f8298
comile errors
Excellencedev Jan 2, 2026
285f366
...
Excellencedev Jan 2, 2026
be52d4a
...
Excellencedev Jan 2, 2026
3e95499
green up ci
Excellencedev Jan 3, 2026
9faf677
Merge branch 'main' into fix-24635
Excellencedev Jan 3, 2026
13660f2
cleanup
Excellencedev Jan 3, 2026
942ea3d
ui fix
Excellencedev Jan 3, 2026
766b742
Merge branch 'main' into fix-24635
Excellencedev Jan 4, 2026
494f467
Merge branch 'main' into fix-24635
Excellencedev Jan 5, 2026
b92e2eb
adding action permissions test case to `tests/integration/actions_job…
Excellencedev Jan 5, 2026
a2583d0
Merge branch 'fix-24635' of https://github.com/Excellencedev/gitea in…
Excellencedev Jan 5, 2026
f5d4ce6
compile errors
Excellencedev Jan 5, 2026
012c9e0
...
Excellencedev Jan 5, 2026
b37a967
logs
Excellencedev Jan 5, 2026
15c19f6
...
Excellencedev Jan 5, 2026
350bcab
fix bug
Excellencedev Jan 5, 2026
d262948
Merge branch 'main' into fix-24635
Excellencedev Jan 6, 2026
9610d7f
Implement all requested changes
Excellencedev Jan 6, 2026
b4a17df
lints
Excellencedev Jan 6, 2026
3fb03fb
lint
Excellencedev Jan 6, 2026
dcf43af
fix ui issues
Excellencedev Jan 6, 2026
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
41 changes: 31 additions & 10 deletions models/perm/access/repo_permission.go
Original file line number Diff line number Diff line change
Expand Up @@ -268,13 +268,18 @@ 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()
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 +293,33 @@ 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)

// 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
120 changes: 120 additions & 0 deletions models/repo/repo_unit.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,11 +168,78 @@ 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"
)

// 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"`
}

// 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"`
}

func (cfg *ActionsConfig) EnableWorkflow(file string) {
Expand Down Expand Up @@ -209,6 +276,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 deletions models/repo/repo_unit_test.go
Original file line number Diff line number Diff 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())
}

func TestActionsConfigTokenPermissions(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)
})
}
19 changes: 19 additions & 0 deletions options/locale/locale_en-US.ini
Original file line number Diff line number Diff line change
Expand Up @@ -3937,6 +3937,25 @@ general.collaborative_owner_not_exist = The collaborative owner does not exist.
general.remove_collaborative_owner = Remove Collaborative Owner
general.remove_collaborative_owner_desc = Removing a collaborative owner will prevent the repositories of the owner from accessing the actions in this repository. Continue?

general.token_permissions = Workflow Permissions
general.token_permissions.description = Configure the default permissions granted to the GITHUB_TOKEN when running workflows in this repository.
general.token_permissions.mode = Permission Mode
general.token_permissions.permissive = Read and write permissions
general.token_permissions.permissive.description = Workflows have read and write permissions in the repository for all scopes.
general.token_permissions.restricted = Read repository contents and packages permissions
general.token_permissions.restricted.description = Workflows have read permissions in the repository for the contents and packages scopes only.
general.token_permissions.fork_pr_note = Note: For workflows triggered by a pull request from a forked repository, the default GITHUB_TOKEN is always read-only.
general.token_permissions.contents = Contents
general.token_permissions.issues = Issues
general.token_permissions.pull_requests = Pull Requests
general.token_permissions.packages = Packages
general.token_permissions.actions_scope = Actions
general.token_permissions.wiki = Wiki
general.token_permissions.access_read = Read
general.token_permissions.access_write = Write
general.token_permissions.access_none = No access
general.token_permissions.update_success = Token permissions updated successfully.

[projects]
deleted.display_name = Deleted Project
type-1.display_name = Individual Project
Expand Down
40 changes: 39 additions & 1 deletion routers/web/repo/setting/actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,17 @@ func ActionsGeneralSettings(ctx *context.Context) {
return
}

actionsCfg := actionsUnit.ActionsConfig()

// Token permission settings
ctx.Data["TokenPermissionMode"] = actionsCfg.GetTokenPermissionMode()
ctx.Data["TokenPermissionModePermissive"] = repo_model.ActionsTokenPermissionModePermissive
ctx.Data["TokenPermissionModeRestricted"] = repo_model.ActionsTokenPermissionModeRestricted
ctx.Data["EffectiveTokenPermissions"] = actionsCfg.GetEffectiveTokenPermissions(false)
ctx.Data["MaxTokenPermissions"] = actionsCfg.GetMaxTokenPermissions()

if ctx.Repo.Repository.IsPrivate {
collaborativeOwnerIDs := actionsUnit.ActionsConfig().CollaborativeOwnerIDs
collaborativeOwnerIDs := actionsCfg.CollaborativeOwnerIDs
collaborativeOwners, err := user_model.GetUsersByIDs(ctx, collaborativeOwnerIDs)
if err != nil {
ctx.ServerError("GetUsersByIDs", err)
Expand Down Expand Up @@ -119,3 +128,32 @@ func DeleteCollaborativeOwner(ctx *context.Context) {

ctx.JSONOK()
}

// UpdateTokenPermissions updates the token permission settings for the repository
func UpdateTokenPermissions(ctx *context.Context) {
redirectURL := ctx.Repo.RepoLink + "/settings/actions/general"

actionsUnit, err := ctx.Repo.Repository.GetUnit(ctx, unit_model.TypeActions)
if err != nil {
ctx.ServerError("GetUnit", err)
return
}

actionsCfg := actionsUnit.ActionsConfig()

// Update permission mode
permissionMode := ctx.FormString("token_permission_mode")
if permissionMode == string(repo_model.ActionsTokenPermissionModeRestricted) {
actionsCfg.TokenPermissionMode = repo_model.ActionsTokenPermissionModeRestricted
} else {
actionsCfg.TokenPermissionMode = repo_model.ActionsTokenPermissionModePermissive
}

if err := repo_model.UpdateRepoUnit(ctx, actionsUnit); err != nil {
ctx.ServerError("UpdateRepoUnit", err)
return
}

ctx.Flash.Success(ctx.Tr("repo.settings.update_settings_success"))
ctx.Redirect(redirectURL)
}
1 change: 1 addition & 0 deletions routers/web/web.go
Original file line number Diff line number Diff line change
Expand Up @@ -1167,6 +1167,7 @@ func registerWebRoutes(m *web.Router) {
m.Post("/add", repo_setting.AddCollaborativeOwner)
m.Post("/delete", repo_setting.DeleteCollaborativeOwner)
})
m.Post("/token_permissions", repo_setting.UpdateTokenPermissions)
})
}, actions.MustEnableActions)
// the follow handler must be under "settings", otherwise this incomplete repo can't be accessed
Expand Down
Loading