mirror of
https://github.com/go-gitea/gitea.git
synced 2026-08-21 22:40:37 +00:00
ed4d7ea08d
Workflows using YAML anchors are rejected as invalid, because a workflow is split into one document per job and an alias whose anchor lands in another job's document no longer resolves. Aliases are now expanded once, right after the workflow is parsed and before anything reads or splits it, bounded like GitHub's parser so nested aliases cannot expand without limit. Merge keys stay unsupported, as they are upstream. Fixes https://github.com/go-gitea/gitea/issues/38983 Signed-off-by: silverwind <me@silverwind.io>
143 lines
4.1 KiB
Go
143 lines
4.1 KiB
Go
// Copyright 2026 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package actions
|
|
|
|
import (
|
|
"gitea.dev/models/perm"
|
|
repo_model "gitea.dev/models/repo"
|
|
"gitea.dev/models/unit"
|
|
"gitea.dev/modules/actions/jobparser"
|
|
"gitea.dev/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
|
|
node := rawPerms
|
|
for node.Kind == yaml.DocumentNode {
|
|
if len(node.Content) == 0 {
|
|
return nil
|
|
}
|
|
node = node.Content[0]
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|