From 61e704d991b247ede2f91b2a88241e76131ba4dd Mon Sep 17 00:00:00 2001 From: Bartender <10+bartender@noreply.hiddenden.cafe> Date: Fri, 26 Jun 2026 11:13:36 +0000 Subject: [PATCH] docs(wiki): add Raw API page (from dev docs/raw-api.md) --- Raw-API.md | 119 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 Raw-API.md diff --git a/Raw-API.md b/Raw-API.md new file mode 100644 index 0000000..951c5cf --- /dev/null +++ b/Raw-API.md @@ -0,0 +1,119 @@ +# 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::` (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.