8c84d76bd5
Adds docs/raw-api.md (two-layer policy, sensitive denylist, env vars, write-mode warning), links it from index and api-reference, documents RAW_API_ENABLED / RAW_API_ALLOW_SENSITIVE in .env.example, and adds commented virtual-tool-name deny examples to policy.yaml. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
120 lines
5.1 KiB
Markdown
120 lines
5.1 KiB
Markdown
# Raw API Dispatch (`gitea_request`)
|
|
|
|
`gitea_request` is a generic escape hatch that can call **any** Gitea REST
|
|
endpoint by method and path. It exists for the long tail of the Gitea API that
|
|
the curated, typed tools do not cover (merging PRs, reviews, writing files,
|
|
webhooks, branch/tag protections, collaborators, Actions/CI, packages,
|
|
notifications, and so on).
|
|
|
|
> Prefer the dedicated tools whenever one exists. Use `gitea_request` only for
|
|
> endpoints they do not cover. It is subject to policy, write-mode, and the
|
|
> sensitive-path denylist described below.
|
|
|
|
## Arguments
|
|
|
|
| Field | Type | Notes |
|
|
|-------|------|-------|
|
|
| `method` | enum | `GET`, `HEAD`, `POST`, `PUT`, `PATCH`, `DELETE` (case-insensitive). Any other method is rejected before any network call. |
|
|
| `path` | string | Gitea REST path. The `/api/v1` prefix is optional. A full URL may be supplied — the host and query string are stripped. |
|
|
| `query` | object | Optional query-string parameters. |
|
|
| `body` | object | Optional JSON request body. **Never logged.** |
|
|
|
|
The response is returned in a stable envelope:
|
|
|
|
```json
|
|
{
|
|
"method": "GET",
|
|
"path": "/api/v1/repos/acme/app/pulls/1",
|
|
"write": false,
|
|
"repository": "acme/app",
|
|
"data": { "...": "..." }
|
|
}
|
|
```
|
|
|
|
List responses add `count` and `omitted`; oversized objects are returned as a
|
|
truncated JSON string with `"truncated": true`. All responses are bounded by
|
|
`MAX_TOOL_RESPONSE_ITEMS` / `MAX_TOOL_RESPONSE_CHARS`.
|
|
|
|
## Two-layer authorization
|
|
|
|
A single tool surface would normally collapse the granularity of `policy.yaml`.
|
|
To preserve it, every call is authorized twice:
|
|
|
|
1. **Central gate (`server.py`).** The registered `gitea_request` tool name is
|
|
allowed/denied like any other tool. In service-PAT mode the central gate also
|
|
parses the target repository from the path and verifies that the signed-in
|
|
user has permission on that repository before the service PAT is used.
|
|
2. **Handler gate (`raw_tools.py`).** The handler derives a coarse **virtual
|
|
tool name** of the form `gitea_request:<METHOD>:<top-path-segment>` (for
|
|
example `gitea_request:GET:repos` or `gitea_request:DELETE:repos`) and runs
|
|
it back through the policy engine with the parsed repository, target path, and
|
|
a `is_write` flag (`true` for any method other than GET/HEAD). This reuses the
|
|
existing write-mode + write-whitelist enforcement and lets `policy.yaml` allow
|
|
or deny raw dispatch per method and per top-level path segment.
|
|
|
|
Because the policy engine matches tool names by **exact set membership** (only
|
|
`paths` use globbing), the virtual name is deliberately coarse and stable.
|
|
|
|
### Example: lock raw dispatch to reads
|
|
|
|
```yaml
|
|
tools:
|
|
deny:
|
|
- gitea_request:POST:repos
|
|
- gitea_request:PUT:repos
|
|
- gitea_request:PATCH:repos
|
|
- gitea_request:DELETE:repos
|
|
```
|
|
|
|
## Sensitive-path denylist
|
|
|
|
Independently of `policy.yaml`, the handler blocks endpoints that touch an
|
|
admin or credential surface **for every method, including GET** (a GET on these
|
|
already leaks credentials or privileged configuration):
|
|
|
|
- `/admin`
|
|
- `*tokens*`
|
|
- `*secrets*`
|
|
- `*hooks*`
|
|
- `*keys*` (and `*gpg_keys*`)
|
|
- `applications/oauth2`
|
|
- `actions/runners/registration-token`
|
|
|
|
This denylist lives in the handler and **cannot be re-opened from
|
|
`policy.yaml`.** It is overridden only by setting `RAW_API_ALLOW_SENSITIVE=true`.
|
|
|
|
## Configuration
|
|
|
|
| Variable | Default | Notes |
|
|
|----------|---------|-------|
|
|
| `RAW_API_ENABLED` | `true` | Killswitch. When `false`, `gitea_request` refuses every dispatch with a `403`. |
|
|
| `RAW_API_ALLOW_SENSITIVE` | `false` | When `true`, the admin/credential denylist is bypassed. Leave `false` unless you fully understand the exposure. |
|
|
|
|
## Security warning
|
|
|
|
> With `WRITE_MODE=true`, the **write whitelist is the only brake** on
|
|
> `POST`/`PUT`/`PATCH`/`DELETE` across the *entire* Gitea API surface reachable
|
|
> by `gitea_request`. Any write method against a whitelisted repository will be
|
|
> attempted. Keep the whitelist tight, prefer denying the write virtual tool
|
|
> names in `policy.yaml`, and keep `RAW_API_ALLOW_SENSITIVE=false`.
|
|
|
|
## Behavioral notes and edge cases
|
|
|
|
- **Full URL supplied instead of a path:** only the path is used; the host and
|
|
query string are discarded (`query` carries query parameters).
|
|
- **Path traversal (`..`):** rejected during argument validation (`400`).
|
|
- **Unknown / non-HTTP method:** rejected during argument validation, before any
|
|
network call.
|
|
- **Cross-repo endpoints** such as `/repos/search` and `/repos/issues/search`
|
|
are intentionally *not* treated as repository-scoped, so `repository` is
|
|
`null` for them.
|
|
- **Non-repository writes** such as `POST /user/repos` or `POST /orgs` are denied
|
|
with *"write operation requires a repository target"*. This is the secure
|
|
default — the per-user permission model is repository-scoped, so there is no
|
|
repository against which to verify the write. This behavior is intentional and
|
|
is not worked around.
|
|
- **Service-PAT mode:** non-repository endpoints (for example `GET /user/orgs`)
|
|
are denied by the central gate because per-user permission can only be verified
|
|
against a repository target. Use the dedicated tools for those, or run in
|
|
OAuth-only mode.
|