mirror of
https://github.com/go-gitea/gitea.git
synced 2026-07-31 09:45:33 +02:00
feat: Add configurable permissions for Actions automatic tokens
This commit is contained in:
@@ -266,13 +266,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.
|
||||
@@ -280,17 +285,33 @@ func GetActionsUserRepoPermission(ctx context.Context, repo *repo_model.Reposito
|
||||
// FIXME should owner's visibility also be considered here?
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
@@ -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)
|
||||
|
||||
@@ -6,6 +6,8 @@ package repo
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/perm"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user