mirror of
https://github.com/go-gitea/gitea.git
synced 2026-10-26 06:16:56 +09:00
Adds OpenID Connect workload identity to Gitea Actions: jobs declaring `id-token: write` can request short-lived tokens to authenticate to cloud providers and other services without stored secrets. Tokens follow GitHub's format, so clients like `@actions/core` and existing trust policies carry over. - Issuer at `/api/actions/oidc` with discovery and JWKS, signed with a dedicated RS256 key - GitHub's claims, including `workflow_ref`, `job_workflow_ref` and the immutable `repo:owner@id/repo@id:ref:...` subject - `id-token: write` or `write-all` grant it, default permissions and fork pull requests never get it, owner and repository maximum permissions can disable it - Reusable workflows record the exact ref they were loaded from, and dispatch rejects refs that only resolve through a same-named tag - Generated signing keys are written atomically, and generated ES384 and ES512 OAuth2 keys use their own curves instead of P-256 - `github.ref_type` is `branch` for pull request runs, like on GitHub Fixes https://github.com/go-gitea/gitea/issues/26383 Fixes https://github.com/go-gitea/gitea/issues/33681 Docs: https://gitea.com/gitea/docs/pulls/526 Assisted-by: ChatGPT:gpt-5.6-sol, Claude Code:claude-opus-5-5 Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com> Co-authored-by: silverwind <me@silverwind.io>
145 lines
4.1 KiB
Go
145 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.MakeWriteAllActionsTokenPermissions())
|
|
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
|
|
case "id-token":
|
|
result.IDToken = mode == perm.AccessModeWrite
|
|
// 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",
|
|
"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
|
|
}
|
|
}
|