Files
new-api/services/actions/token_permission_design.md
T
2026-05-30 22:47:36 +08:00

124 lines
6.7 KiB
Markdown

# Actions Token Permission System Design
This document details the design of the Actions Token Permission system within Gitea, originally proposed in [#24635](https://github.com/go-gitea/gitea/issues/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](#2-fork-pull-requests)).
## 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**:
```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.