Files
gitea/services/actions/token_permission_design.md
Excellencedev 45809c8f54 feat: Add configurable permissions for Actions automatic tokens (#36173)
## 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>
2026-03-21 15:39:47 -07:00

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 write access to most repository scopes by default.
  • Restricted: Grants read access (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 contents in workflow syntax), Issues, PullRequests, Actions, Wiki, and Projects.
  • Note: The Packages scope is supported in workflow/job permissions: 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 AllowedCrossRepoIDs list in their owner-level settings to grant the token read-only access to other private/internal repositories they own.
  • If the AllowedCrossRepoIDs list 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 default TokenPermissionMode (Permissive or Restricted) to generate base permissions.

Step 2: Apply Repository Clamping

  • Repositories can define MaxTokenPermissions in their Actions settings.
  • The base permissions from Step 1 are clamped against these maximum allowed permissions.
  • If the repository says Issues: read and the workflow requests Issues: write, the final token gets Issues: read.

Step 3: Apply Organization/User Clamping (Hierarchical Override)

  • The organization (or user) has an owner-level configuration (UserActionsConfig) containing MaxTokenPermissions, and these restrictions cascade down.
  • The repository's clamping limits cannot exceed the owner's limits UNLESS the repository explicitly enables OverrideOwnerConfig.
  • If OverrideOwnerConfig is false, and the owner sets MaxTokenPermissions to read for all scopes, no repository under that owner can grant write access, 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: write maps to Code: write and Releases: write.
  • When a workflow specifies both contents and 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 none for all scopes.

2. Fork Pull Requests

  • Workflows triggered by Pull Requests from forks inherently operate in Restricted mode for security reasons.
  • The base permissions for the current repository are automatically downgraded to read (or none), 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 AllowedCrossRepoIDs configuration. 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 AllowedCrossRepoIDs setting. 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.