## Overview This PR introduces granular permission controls for Gitea Actions tokens (`GITEA_TOKEN`), aligning Gitea's security model with GitHub Actions standards while maintaining compatibility with Gitea's unique repository unit system. It addresses the need for finer access control by allowing administrators and repository owners to define default token permissions, set maximum permission ceilings, and control cross-repository access within organizations. ## Key Features ### 1. Granular Token Permissions - **Standard Keyword Support**: Implements support for the `permissions:` keyword in workflow and job YAML files (e.g., `contents: read`, `issues: write`). - **Permission Modes**: - **Permissive**: Default write access for most units (backwards compatible). - **Restricted**: Default read-only access for `contents` and `packages`, with no access to other units. - ~~**Custom**: Allows defining specific default levels for each unit type (Code, Issues, PRs, Packages, etc.).~~**EDIT removed UI was confusing** - **Clamping Logic**: Workflow-defined permissions are automatically "clamped" by repository or organization-level maximum settings. Workflows cannot escalate their own permissions beyond these limits. ### 2. Organization & Repository Settings - **Settings UI**: Added new settings pages at both Organization and Repository levels to manage Actions token defaults and maximums. - **Inheritance**: Repositories can be configured to "Follow organization-level configuration," simplifying management across large organizations. - **Cross-Repository Access**: Added a policy to control whether Actions workflows can access other repositories or packages within the same organization. This can be set to "None," "All," or restricted to a "Selected" list of repositories. ### 3. Security Hardening - **Fork Pull Request Protection**: Tokens for workflows triggered by pull requests from forks are strictly enforced as read-only, regardless of repository settings. - ~~**Package Access**: Actions tokens can now only access packages explicitly linked to a repository, with cross-repo access governed by the organization's security policy.~~ **EDIT removed https://github.com/go-gitea/gitea/pull/36173#issuecomment-3873675346** - **Git Hook Integration**: Propagates Actions Task IDs to git hooks to ensure that pushes performed by Actions tokens respect the specific permissions granted at runtime. ### 4. Technical Implementation - **Permission Persistence**: Parsed permissions are calculated at job creation and stored in the `action_run_job` table. This ensures the token's authority is deterministic throughout the job's lifecycle. - **Parsing Priority**: Implemented a priority system in the YAML parser where the broad `contents` scope is applied first, allowing granular scopes like `code` or `releases` to override it for precise control. - **Re-runs**: Permissions are re-evaluated during a job re-run to incorporate any changes made to repository settings in the interim. ### How to Test 1. **Unit Tests**: Run `go test ./services/actions/...` and `go test ./models/repo/...` to verify parsing logic and permission clamping. 2. **Integration Tests**: Comprehensive tests have been added to `tests/integration/actions_job_token_test.go` covering: - Permissive vs. Restricted mode behavior. - YAML `permissions:` keyword evaluation. - Organization cross-repo access policies. - Resource access (Git, API, and Packages) under various permission configs. 3. **Manual Verification**: - Navigate to **Site/Org/Repo Settings -> Actions -> General**. - Change "Default Token Permissions" and verify that newly triggered workflows reflect these changes in their `GITEA_TOKEN` capabilities. - Attempt a cross-repo API call from an Action and verify the Org policy is enforced. ## Documentation Added a PR in gitea's docs for this : https://gitea.com/gitea/docs/pulls/318 ## UI: <img width="1366" height="619" alt="Screenshot 2026-01-24 174112" src="https://github.com/user-attachments/assets/bfa29c9a-4ea5-4346-9410-16d491ef3d44" /> <img width="1360" height="621" alt="Screenshot 2026-01-24 174048" src="https://github.com/user-attachments/assets/d5ec46c8-9a13-4874-a6a4-fb379936cef5" /> /fixes #24635 /claim #24635 --------- Signed-off-by: Excellencedev <ademiluyisuccessandexcellence@gmail.com> Signed-off-by: ChristopherHX <christopher.homberger@web.de> Signed-off-by: silverwind <me@silverwind.io> Signed-off-by: wxiaoguang <wxiaoguang@gmail.com> Co-authored-by: ChristopherHX <christopher.homberger@web.de> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: silverwind <me@silverwind.io> Co-authored-by: Zettat123 <zettat123@gmail.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
6.7 KiB
Actions Token Permission System Design
This document details the design of the Actions Token Permission system within Gitea, originally proposed in #24635.
Design Philosophy & GitHub Differences
Gitea Actions uses a strict clamping mechanism for token permissions.
While workflows can request explicit permissions that exceed the repository's default baseline
(e.g., requesting write when the default mode is Restricted),
these requests are always bounded by a hard ceiling.
The maximum allowable permissions (MaxTokenPermissions) are set at the Repository or Organization level.
Any permissions requested by a workflow are strictly clamped by this ceiling policy.
This ensures that workflows cannot bypass organizational or repository-level security restrictions.
Terminology
1. GITEA_TOKEN
- The automatic token generated for each Actions job.
- Its permissions (read/write/none) are scoped to the repository and specific features (Code, Issues, etc.).
2. Token Permission Mode
- The default access level granted to a token when no explicit
permissions:block is present in a workflow. - Permissive: Grants
writeaccess to most repository scopes by default. - Restricted: Grants
readaccess (or none) to repository scopes by default.
3. Actions Token Permissions
- A structure representing the granular permission scopes available to a token.
- Includes scopes like: Code, Releases (both grouped under
contentsin workflow syntax), Issues, PullRequests, Actions, Wiki, and Projects. - Note: The
Packagesscope is supported in workflow/jobpermissions:blocks but is currently hidden from the settings UI.
4. Cross-Repository Access
- By default, a token can access the repository where the workflow is running, as well as any public repositories (read-only) on the instance.
- Users and organizations can configure an
AllowedCrossRepoIDslist in their owner-level settings to grant the token read-only access to other private/internal repositories they own. - If the
AllowedCrossRepoIDslist is empty, there is no cross-repository access to other private repositories (default for enhanced security). - In any configuration, individual jobs can disable or limit cross-repo access
by explicitly restricting their permissions (e.g.,
permissions: none). - Note on Forks: Cross-repository access to private repositories is fundamentally denied for workflows triggered by fork pull requests (see Special Cases).
Token Lifecycle & Permission Evaluation
When a job starts, Gitea evaluates the requested permissions for the GITEA_TOKEN through a multistep clamping process:
Step 1: Determine Base Permissions From Workflow
- If the job explicitly specifies a valid
permissions:block, Gitea parses it. - If the job inherits a top-level
permissions:block, Gitea parses that. - If an invalid or unparseable
permissions:block is specified, or no explicit permissions are defined at all, Gitea falls back to using the repository's defaultTokenPermissionMode(Permissive or Restricted) to generate base permissions.
Step 2: Apply Repository Clamping
- Repositories can define
MaxTokenPermissionsin their Actions settings. - The base permissions from Step 1 are clamped against these maximum allowed permissions.
- If the repository says
Issues: readand the workflow requestsIssues: write, the final token getsIssues: read.
Step 3: Apply Organization/User Clamping (Hierarchical Override)
- The organization (or user) has an owner-level configuration (
UserActionsConfig) containingMaxTokenPermissions, and these restrictions cascade down. - The repository's clamping limits cannot exceed the owner's limits
UNLESS the repository explicitly enables
OverrideOwnerConfig. - If
OverrideOwnerConfigis false, and the owner setsMaxTokenPermissionstoreadfor all scopes, no repository under that owner can grantwriteaccess, regardless of their own settings or the workflow's request.
Parsing Priority for "contents" Scope
In GitHub Actions compatibility, the contents scope maps to multiple granular scopes in Gitea.
contents: writemaps toCode: writeandReleases: write.- When a workflow specifies both
contentsand a more granular scope (e.g.,code), the granular scope takes absolute priority.
Example YAML:
permissions:
contents: write
code: read
Result: The token gets Code: read (from granular) and Releases: write (from contents).
Special Cases & Edge Scenarios
1. Empty Permissions Mapping (permissions: {})
- Explicitly setting an empty mapping means "revoke all permissions".
- The token gets
nonefor all scopes.
2. Fork Pull Requests
- Workflows triggered by Pull Requests from forks inherently operate in
Restrictedmode for security reasons. - The base permissions for the current repository are automatically downgraded to
read(ornone), preventing untrusted code from modifying the repository. - Cross-Repo Access in Forks: For workflows triggered by fork pull requests, cross-repository access
to other private repositories is strictly denied, regardless of the
AllowedCrossRepoIDsconfiguration. Fork PRs can only read the target repository and truly public repositories.
3. Public Repositories in Cross-Repo Access
- As mentioned in Cross-Repository Access, truly public repositories can always be read by the token,
regardless of the
AllowedCrossRepoIDssetting. The allowed list only governs access to private/internal repositories owned by the same user or organization.
Packages Registry
"Packages" belong to "owner" but not "repository". Although there is a function "linking a package to a repository", in most cases it doesn't really work. When accessing a package, usually there is no information about a repository. So the "packages" permission should be designed separately from other permissions.
A possible approach is like this: let owner set packages permissions, and make the repositories follow.
-
On owner-level:
- Add a "Packages" permission section
- "Default permissions for all repositories" can be set to none/read/write
- Set different permissions for selected repositories (if needed), like the "Collaborators" permission setting
-
On repository-level:
- Now a repository can have "Packages" permission
- The repository-level "Packages" permission is clamped by the owner-level "Packages" permission
- If the owner-level "Packages" permission for this repository is read, then the repository cannot set its "Packages" permission to write
Maybe reusing the "org teams" permission system is a good choice: bind a repository's Actions token to a team.