ci: raise the coverage gate to 90 percent; docs: reconcile OAuth docs with the implementation
This commit is contained in:
1 parent
0f5f5cdd95
commit
d7cac91175
7 files changed
+66
-21
No files matched your search
@@ -102,10 +102,12 @@ compared, so length does not leak). A match runs the call as `GITEA_TOKEN`, with
|
||||
resolved once from `GET /user`. stdio is always this mode.
|
||||
|
||||
Per-user OAuth mode: the server is an OAuth 2.1 authorization server for MCP clients (dynamic
|
||||
client registration, PKCE) and a confidential OAuth client of Gitea. `/oauth/callback` receives
|
||||
Gitea's code, exchanges it, and issues the client opaque access and refresh tokens that map to
|
||||
the user's Gitea tokens. A call with such a token runs with the user's own Gitea access token,
|
||||
so Gitea enforces that user's permissions. The mapping lives in `DATA_DIR/oauth-tokens.json`
|
||||
client registration, PKCE) and a confidential OAuth client of Gitea. `/authorize` parks the
|
||||
request and shows the consent page `/oauth/consent` (client name, redirect URI, approval with
|
||||
CSRF and origin checks) before the browser goes to Gitea. `/oauth/callback` receives Gitea's
|
||||
code in the same browser, exchanges it, and issues the client opaque access and refresh tokens
|
||||
that map to the user's Gitea tokens. A call with such a token runs with the user's own Gitea access token,
|
||||
so Gitea enforces that user's permissions. The mapping lives in `DATA_DIR/oauth-store.json`
|
||||
(mode 0600). Gitea tokens in it are encrypted with Fernet (key derived from
|
||||
`TOKEN_ENCRYPTION_KEY`); our own tokens are stored as SHA-256 hashes. Upstream refresh is
|
||||
automatic.
|
||||
|
||||
+18
-4
@@ -56,22 +56,36 @@ when OAuth is on.
|
||||
|
||||
| Variable | Default | Rules |
|
||||
|---|---|---|
|
||||
| `OAUTH_ENABLED` | `false` | When true, the four settings below are required, or the server refuses to start (exit code 2, with the list of what is missing). |
|
||||
| `OAUTH_ENABLED` | `false` | When true, `GITEA_OAUTH_CLIENT_ID`, `GITEA_OAUTH_CLIENT_SECRET`, `TOKEN_ENCRYPTION_KEY` and `PUBLIC_BASE_URL` are required, or the server refuses to start (exit code 2, with the list of what is missing). |
|
||||
| `GITEA_OAUTH_CLIENT_ID` | unset | Client id of the Gitea OAuth application (created by `setup`). |
|
||||
| `GITEA_OAUTH_CLIENT_SECRET` | unset | Its client secret. |
|
||||
| `TOKEN_ENCRYPTION_KEY` | unset | At least 32 characters. Encrypts stored user tokens. Losing it invalidates every stored login. |
|
||||
| `TOKEN_ENCRYPTION_KEY` | unset | At least 32 characters. Encrypts the token store. The key cannot be rotated in place: a store written with another key stops startup (exit code 2) instead of being wiped, until you restore the original key or move `DATA_DIR/oauth-store.json` away, which signs every user out. |
|
||||
| `OAUTH_SCOPES` | all `write:<category>` scopes | Granular Gitea scopes only (`read:` or `write:` plus activitypub, admin, issue, misc, notification, organization, package, repository, user). Anything else is rejected, because Gitea grants full access for unknown scope strings. Must not be empty. |
|
||||
|
||||
| `OAUTH_REDIRECT_ALLOWLIST` | empty | Comma list of redirect URIs MCP clients may register: exact URIs (`https://client.example/cb`) or prefixes ending in `/` that allow a whole host (`https://claude.ai/`). A bare origin without a path is refused. Empty accepts any https URI (and http on `localhost`, `127.0.0.1`, `[::1]`). With a list, https URIs must match it; loopback URIs stay allowed for native clients. Checked at registration and again at `/authorize`. |
|
||||
|
||||
`PUBLIC_BASE_URL` is also required for OAuth. The Gitea OAuth application's redirect URI is
|
||||
`<PUBLIC_BASE_URL>/oauth/callback`.
|
||||
|
||||
OAuth-only deployments are supported: with `OAUTH_ENABLED=true` and neither `MCP_BEARER_TOKEN`
|
||||
nor `GITEA_TOKEN`, every user brings their own Gitea token and admin-token mode is simply not
|
||||
available (nothing falls back to it). `GITEA_TOKEN` stays required whenever admin-token mode is
|
||||
reachable: with a static bearer, without any HTTP authentication, and on stdio.
|
||||
|
||||
Sign-in flow and limits: `/authorize` sends the browser to the consent page `/oauth/consent`,
|
||||
which names the client and the redirect URI and needs an explicit approval (CSRF token, cookie
|
||||
and an `Origin` that matches `PUBLIC_BASE_URL`) before the browser goes to Gitea; the callback
|
||||
`/oauth/callback` completes only in that same browser. Issued `agm_` access tokens live 1 hour
|
||||
and refresh tokens 30 days (rotated on use, reuse revokes the grant); authorization codes live 5
|
||||
minutes and pending sign-ins 10 minutes. Bounds: 500 registered clients (10 redirect URIs and 8
|
||||
KiB of metadata each), 1024 pending sign-ins and codes, 50 sessions per Gitea login, and per
|
||||
client address 20 pending sign-ins and 20 registrations per hour. The consent form body is
|
||||
limited to 16 KiB and must be `application/x-www-form-urlencoded`.
|
||||
|
||||
## State
|
||||
|
||||
| Variable | Default | Rules |
|
||||
|---|---|---|
|
||||
| `DATA_DIR` | `~/.local/state/aegis-gitea-mcp` (`/var/lib/aegis-mcp` in the image) | `~` is expanded. Holds the token store (`oauth-tokens.json`, mode 0600) and the audit log. |
|
||||
| `DATA_DIR` | `~/.local/state/aegis-gitea-mcp` (`/var/lib/aegis-mcp` in the image) | `~` is expanded. Holds the encrypted token store (`oauth-store.json`, mode 0600) and the audit log. A new directory is created with mode 0700; an existing one must belong to the running user and is tightened to 0700, otherwise OAuth startup fails. |
|
||||
| `AUDIT_LOG_PATH` | `DATA_DIR/audit.log` | Path of the hash-chained audit log. See [audit-logging.md](audit-logging.md). The server opens it for append at startup and exits with code 2 and a message on stderr when it cannot. |
|
||||
|
||||
## Tool exposure
|
||||
|
||||
+16
-5
@@ -9,8 +9,12 @@ hands the power of its Gitea token to every client that can authenticate.
|
||||
- Admin-token only: set `GITEA_TOKEN` and `MCP_BEARER_TOKEN`. Every client with the bearer token
|
||||
acts as the token owner.
|
||||
- Per-user OAuth: additionally set `OAUTH_ENABLED=true` with the OAuth settings. Users sign in
|
||||
with their own Gitea account and act under their own permissions. The static bearer keeps
|
||||
working next to it as the admin identity; leave `MCP_BEARER_TOKEN` unset to turn that off.
|
||||
with their own Gitea account (after a consent step on `/oauth/consent`) and act under their
|
||||
own permissions. The static bearer keeps working next to it as the admin identity; leave
|
||||
`MCP_BEARER_TOKEN` unset to turn that off.
|
||||
- OAuth only: `OAUTH_ENABLED=true` with neither `MCP_BEARER_TOKEN` nor `GITEA_TOKEN`. The server
|
||||
starts and serves signed-in users only; admin-token mode does not exist in this deployment, so
|
||||
no request can ever fall back to an admin identity. Recommended for public deployments.
|
||||
|
||||
## Docker
|
||||
|
||||
@@ -50,7 +54,7 @@ Points that matter:
|
||||
- The port is published on `127.0.0.1` only. The reverse proxy on the same host is the public
|
||||
entry point.
|
||||
- `DATA_DIR` must be a persistent volume writable by the container user. It holds the audit log
|
||||
and the encrypted OAuth token store (`oauth-tokens.json`, mode 0600). Losing the volume loses
|
||||
and the encrypted OAuth token store (`oauth-store.json`, mode 0600). Losing the volume loses
|
||||
all OAuth logins and the audit history. Back it up, and protect the backup like the secrets it
|
||||
protects.
|
||||
- Keep `.env` out of version control and out of the image (mode 0600; `setup` writes it that
|
||||
@@ -104,6 +108,12 @@ Gitea has no instance-wide OAuth application API, so the application belongs to
|
||||
that created it. Re-running setup reuses it; if the stored client secret is missing, Gitea has to
|
||||
regenerate it, and setup asks before doing so.
|
||||
|
||||
Before the browser is sent to Gitea, the consent page `/oauth/consent` shows the client name and
|
||||
the redirect URI, because Gitea remembers consent for this one application and any client can
|
||||
register itself. For a public deployment also set `OAUTH_REDIRECT_ALLOWLIST` to the hosted
|
||||
clients you use, for example `https://claude.ai/,https://chat.mistral.ai/` (see
|
||||
[configuration.md](configuration.md)).
|
||||
|
||||
`OAUTH_SCOPES` is the scope list sent to Gitea. Keep it to granular scopes. Narrow it (for
|
||||
example to `read:repository,read:issue`) to limit what signed-in users' tokens can do through this
|
||||
server regardless of their Gitea rights.
|
||||
@@ -111,8 +121,9 @@ server regardless of their Gitea rights.
|
||||
## Upgrades
|
||||
|
||||
- Application: pull the new image (or package) and restart. The data volume is kept.
|
||||
- `TOKEN_ENCRYPTION_KEY` must stay the same, or stored OAuth logins can no longer be decrypted
|
||||
and users have to sign in again.
|
||||
- `TOKEN_ENCRYPTION_KEY` must stay the same. A store written with another key stops the server
|
||||
at startup (exit code 2, the file is kept) instead of being wiped: restore the old key, or move
|
||||
`DATA_DIR/oauth-store.json` away to start over, which signs every OAuth user out.
|
||||
- Upgrading Gitea: tool names and schemas come from the pinned swagger, so a Gitea upgrade does
|
||||
not change them. To pick up a newer API, re-pin the swagger and regenerate the catalog:
|
||||
|
||||
|
||||
+7
-1
@@ -27,7 +27,13 @@ authentication), but it does not decide what is safe. This checklist is how you
|
||||
header).
|
||||
- `TOKEN_ENCRYPTION_KEY` protects the stored per-user tokens. Keep it separate from the data
|
||||
volume backups. `GITEA_OAUTH_CLIENT_SECRET` likewise.
|
||||
- Use `setup`, which writes the env file with mode 0600 and never prints secrets.
|
||||
- Use `setup`, which writes the env file with mode 0600 and never prints secrets. The server
|
||||
loads `./.env` only while it is a regular file you own with mode 0600; keep it elsewhere with
|
||||
`--env-file` or `AEGIS_ENV_FILE`.
|
||||
- For a public OAuth deployment set `OAUTH_REDIRECT_ALLOWLIST` (for example
|
||||
`https://claude.ai/,https://chat.mistral.ai/`), so only the hosted clients you use can
|
||||
register, and leave `MCP_BEARER_TOKEN` and `GITEA_TOKEN` unset when nobody needs the admin
|
||||
identity.
|
||||
- Do not paste tokens into chat with the assistant, and do not commit `.env`.
|
||||
|
||||
## Network and transport
|
||||
|
||||
@@ -13,6 +13,8 @@ Gitea's sanitized message, and the audit log has one event per call (see
|
||||
| Exit code 2, "env file ... does not exist" | The file named by `--env-file` or `AEGIS_ENV_FILE` is missing or not a regular file. |
|
||||
| Exit code 2, refuses a non-loopback bind | `MCP_HOST` is not loopback and there is no `MCP_BEARER_TOKEN` or complete OAuth setup. Set one of them, or bind `127.0.0.1` behind a reverse proxy. |
|
||||
| Exit code 2, lists OAuth settings | `OAUTH_ENABLED=true` needs `GITEA_OAUTH_CLIENT_ID`, `GITEA_OAUTH_CLIENT_SECRET`, `TOKEN_ENCRYPTION_KEY` and `PUBLIC_BASE_URL`. |
|
||||
| Exit code 2, "cannot decrypt .../oauth-store.json" | `TOKEN_ENCRYPTION_KEY` differs from the key the store was written with (or the file is damaged). The server does not wipe it. Restore the original key, or move `DATA_DIR/oauth-store.json` away to start over (signs every OAuth user out). |
|
||||
| Exit code 2, "owned by another user" or "writable by group or others" (OAuth) | `DATA_DIR` must belong to the user the server runs as and not be group or world writable. Fix the owner or `chmod 700`. |
|
||||
| Validation error naming a setting | See the rules in [configuration.md](configuration.md): URLs must be http(s) without credentials, `PUBLIC_BASE_URL` must be https, secrets need 32 characters, `OAUTH_SCOPES` accepts only granular scopes. |
|
||||
| Startup error listing tool names | `DISABLED_TOOLS` contains names that are not registered. Check [tools.md](tools.md). |
|
||||
|
||||
@@ -69,8 +71,13 @@ A tool the client cannot see is probably filtered by `TOOLSETS`, `READ_ONLY` or
|
||||
- Gitea shows a redirect URI error: the OAuth application's redirect URI must be exactly
|
||||
`<PUBLIC_BASE_URL>/oauth/callback`. Re-run `setup --public-base-url ...` or fix it in Gitea.
|
||||
- Signed in, but admin tools return 403: the user is not a site administrator. That is intended.
|
||||
- Users are asked to sign in again after a restart: `TOKEN_ENCRYPTION_KEY` or the `DATA_DIR`
|
||||
volume changed. The key must stay stable and the volume persistent.
|
||||
- Users are asked to sign in again after a restart: the `DATA_DIR` volume changed or was not
|
||||
persistent (a changed `TOKEN_ENCRYPTION_KEY` stops startup instead, see above). Keep the key
|
||||
stable and the volume persistent.
|
||||
- The sign-in page says the request is invalid or expired: the consent page lives 10 minutes
|
||||
and a form is single use. Start the sign-in again from the client. Registration or sign-in
|
||||
refused with "too many": one client address may hold 20 pending sign-ins and register 20
|
||||
clients per hour.
|
||||
- After fixing OAuth settings the client still fails: some clients cache tokens. Disconnect the
|
||||
server in the client and connect again, which starts a fresh flow. Users can also revoke the
|
||||
application under Settings, Applications, Authorized OAuth2 Applications in Gitea.
|
||||
Reference in new issue
Block a user