0
0
mirror of https://github.com/go-gitea/gitea.git synced 2026-03-27 15:39:17 +01:00
gitea/services/actions/permission_parser.go
TheFox0x7 cfd9008891
add valid github scopes (#36977)
test for github supported scopes with test to ensure all of them work
and don't panic

fixes: https://github.com/go-gitea/gitea/issues/36967
2026-03-24 19:16:23 +00:00

147 lines
4.3 KiB
Go

// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package actions
import (
"code.gitea.io/gitea/models/perm"
repo_model "code.gitea.io/gitea/models/repo"
"code.gitea.io/gitea/models/unit"
"code.gitea.io/gitea/modules/actions/jobparser"
"code.gitea.io/gitea/modules/setting"
"go.yaml.in/yaml/v4"
)
// ExtractJobPermissionsFromWorkflow extracts permissions from an already parsed workflow/job.
// It returns nil if neither workflow nor job explicitly specifies permissions.
func ExtractJobPermissionsFromWorkflow(flow *jobparser.SingleWorkflow, job *jobparser.Job) *repo_model.ActionsTokenPermissions {
if flow == nil || job == nil {
return nil
}
jobPerms := parseRawPermissionsExplicit(&job.RawPermissions)
if jobPerms != nil {
return jobPerms
}
workflowPerms := parseRawPermissionsExplicit(&flow.RawPermissions)
if workflowPerms != nil {
return workflowPerms
}
return nil
}
// parseRawPermissionsExplicit parses a YAML permissions node and returns only explicit scopes.
// It returns nil if the node does not explicitly specify permissions.
func parseRawPermissionsExplicit(rawPerms *yaml.Node) *repo_model.ActionsTokenPermissions {
if rawPerms == nil || (rawPerms.Kind == yaml.ScalarNode && rawPerms.Value == "") {
return nil
}
// Unwrap DocumentNode and resolve AliasNode
node := rawPerms
for node.Kind == yaml.DocumentNode || node.Kind == yaml.AliasNode {
if node.Kind == yaml.DocumentNode {
if len(node.Content) == 0 {
return nil
}
node = node.Content[0]
} else {
node = node.Alias
}
}
if node.Kind == yaml.ScalarNode && node.Value == "" {
return nil
}
// Handle scalar values: "read-all" or "write-all"
if node.Kind == yaml.ScalarNode {
switch node.Value {
case "read-all":
return new(repo_model.MakeActionsTokenPermissions(perm.AccessModeRead))
case "write-all":
return new(repo_model.MakeActionsTokenPermissions(perm.AccessModeWrite))
default:
// Explicit but unrecognized scalar: return all-none permissions.
return new(repo_model.MakeActionsTokenPermissions(perm.AccessModeNone))
}
}
// Handle mapping: individual permission scopes
if node.Kind == yaml.MappingNode {
result := repo_model.MakeActionsTokenPermissions(perm.AccessModeNone)
// Collect all scopes into a map first to handle priority
scopes := make(map[string]perm.AccessMode)
for i := 0; i < len(node.Content); i += 2 {
if i+1 >= len(node.Content) {
break
}
keyNode := node.Content[i]
valueNode := node.Content[i+1]
if keyNode.Kind != yaml.ScalarNode || valueNode.Kind != yaml.ScalarNode {
continue
}
scopes[keyNode.Value] = parseAccessMode(valueNode.Value)
}
// 1. Apply 'contents' first (lower priority)
if mode, ok := scopes["contents"]; ok {
result.UnitAccessModes[unit.TypeCode] = mode
result.UnitAccessModes[unit.TypeReleases] = mode
}
// 2. Apply all other scopes (overwrites contents if specified)
for scope, mode := range scopes {
switch scope {
case "contents":
// already handled
case "code":
result.UnitAccessModes[unit.TypeCode] = mode
case "issues":
result.UnitAccessModes[unit.TypeIssues] = mode
case "pull-requests":
result.UnitAccessModes[unit.TypePullRequests] = mode
case "packages":
result.UnitAccessModes[unit.TypePackages] = mode
case "actions":
result.UnitAccessModes[unit.TypeActions] = mode
case "wiki":
result.UnitAccessModes[unit.TypeWiki] = mode
case "releases":
result.UnitAccessModes[unit.TypeReleases] = mode
case "projects":
result.UnitAccessModes[unit.TypeProjects] = mode
// Scopes github supports but gitea does not, see url for details
// https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax
case "artifact-metadata", "attestations", "checks", "deployments",
"id-token", "models", "discussions", "pages", "security-events", "statuses":
// not supported
default:
setting.PanicInDevOrTesting("Unrecognized permission scope: %s", scope)
}
}
return &result
}
return nil
}
// parseAccessMode converts a string access level to perm.AccessMode
func parseAccessMode(s string) perm.AccessMode {
switch s {
case "write":
return perm.AccessModeWrite
case "read":
return perm.AccessModeRead
default:
return perm.AccessModeNone
}
}