docs: identity-binding rules + promotion-window hardening checklist
Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
co-authored by
Claude Fable 5
parent
c674db4746
commit
74d206cb4a
@@ -32,18 +32,47 @@ git CLI only — same portability heritage as the estate's `gitea_sync.py` mesh.
|
||||
|
||||
* `GET /health`
|
||||
* `POST /v1/link {zitadel_access_token, device_name}` → validates the token
|
||||
against `https://id.shre.ai/oidc/v1/userinfo`, derives a login from
|
||||
`preferred_username` (email local-part fallback), ensures the Gitea user
|
||||
exists, mints a token scoped `write:repository,write:user`, returns
|
||||
`{gitea_base, login, token, token_name}`.
|
||||
against `https://id.shre.ai/oidc/v1/userinfo`, applies the **identity
|
||||
binding rules** (below), mints a token scoped
|
||||
`write:repository,write:user`, returns
|
||||
`{gitea_base, login, token, token_name}`. POST bodies are capped at
|
||||
**64 KB** (413 beyond; missing `Content-Length` → 411, invalid → 400).
|
||||
* `POST /v1/repos {token, name, private}` → creates the user repo with the
|
||||
USER token; clone/html URLs are rebased onto `public_gitea_base` because the
|
||||
container `ROOT_URL` (https://granthi-beta.shre.ai) does not resolve for
|
||||
tailnet-only clients.
|
||||
|
||||
Deployment: `/opt/granthi-link/{granthi_link.py,config.json}` + systemd unit
|
||||
`granthi-link.service`; binds `127.0.0.1:3042` **and** `100.111.127.127:3042`
|
||||
(tailnet). **Not publicly exposed** — see promotion window.
|
||||
Deployment: `/opt/granthi-link/{granthi_link.py,config.json,state.json}` +
|
||||
systemd unit `granthi-link.service`; binds `127.0.0.1:3042` **and**
|
||||
`100.111.127.127:3042` (tailnet). **Not publicly exposed** — see promotion
|
||||
window. The service **refuses to start** (exit 2) unless `config.json` is
|
||||
mode 0600/0400 and owned by the user it runs as — the config carries the
|
||||
forge admin password, so permissive perms fail closed, not open.
|
||||
|
||||
#### Identity binding (`state.json`)
|
||||
|
||||
`/v1/link` originally bound purely by `preferred_username` / email
|
||||
local-part — any Zitadel identity whose derived login collided with an
|
||||
existing account got a token for that account (account takeover). The
|
||||
service now persists a map of Zitadel `sub` → Gitea login in
|
||||
`/opt/granthi-link/state.json` (0600, atomic tmp+rename writes) and applies:
|
||||
|
||||
1. **Mapped sub** → always the mapped login, regardless of what the current
|
||||
userinfo claims. If the mapped login was deleted from the forge it is
|
||||
re-created only when the service created it originally; adopted accounts
|
||||
are refused (409).
|
||||
2. **Unmapped sub, login free** → create the user, record the mapping
|
||||
(`created_by_service: true`). A concurrent-create 409 from Gitea is
|
||||
handled idempotently: the user is re-fetched and accepted only if its
|
||||
primary email is exactly the one this request would have set.
|
||||
3. **Unmapped sub, login taken** → bind ONLY when the Gitea user's primary
|
||||
email equals the Zitadel userinfo `email` **and** `email_verified` is
|
||||
true (recorded with `created_by_service: false`); anything else →
|
||||
`409 login exists and is not linked to this identity`.
|
||||
|
||||
A Gitea token is **never minted before the binding rule passes**, and a
|
||||
corrupt/unreadable `state.json` fails closed (500) instead of falling back
|
||||
to an empty map.
|
||||
|
||||
Empirically verified mechanics on Gitea **1.27.1** (beta forge):
|
||||
|
||||
@@ -62,7 +91,9 @@ Empirically verified mechanics on Gitea **1.27.1** (beta forge):
|
||||
regardless of this choice.
|
||||
* `test_mode` (config flag, **never in production**): allows `/v1/link` to
|
||||
accept `test_userinfo` in the body instead of a Zitadel round-trip, so E2E
|
||||
can exercise the ensure-user + mint path headlessly.
|
||||
can exercise the ensure-user + mint path headlessly. It is honored **only
|
||||
when the service environment also sets `GRANTHI_LINK_ALLOW_TEST_MODE=1`**;
|
||||
a config flag without the env gate is logged as an ERROR and ignored.
|
||||
|
||||
### `client/granthi_sync_client.py` (+ `bin/granthi-sync`) — client daemon
|
||||
|
||||
@@ -88,11 +119,15 @@ Empirically verified mechanics on Gitea **1.27.1** (beta forge):
|
||||
|
||||
## Tests
|
||||
|
||||
* `python3 -m unittest discover -s tests` — 24 tests: autocommit/ff/diverged
|
||||
* `python3 -m unittest discover -s tests` — 53 tests: autocommit/ff/diverged
|
||||
logic against real temp git repos (including "diverged never touches the
|
||||
remote"), config 0600 handling, mocked device-flow polling, credential
|
||||
helper protocol, and the full `/v1/link` + `/v1/repos` service flows
|
||||
against an in-process stub playing Zitadel + Gitea.
|
||||
remote"), config 0600 handling (including umask-proof creation and a
|
||||
no-chmod guard), credential-helper quoting/injection, mocked device-flow
|
||||
polling, the full `/v1/link` + `/v1/repos` service flows against an
|
||||
in-process stub playing Zitadel + Gitea, all identity-binding rules
|
||||
(collision 409, verified-email adoption, deleted-login re-create/refuse,
|
||||
concurrent-create race, corrupt-state fail-closed), the test_mode env
|
||||
gate, config-permission refusal, and the 64 KB body cap.
|
||||
* Live E2E against the beta forge is recorded in the delivery notes
|
||||
(link → add → watch ff/push → forced divergence → DIVERGED skip verified
|
||||
via API, remote sha untouched).
|
||||
@@ -120,5 +155,17 @@ Every linked folder becomes a private repo under their account.
|
||||
promotion is an operator action, not an agent action).
|
||||
4. Add rate limiting / abuse controls before public exposure (one token mint
|
||||
per link call today).
|
||||
5. **Hardening checklist (must all hold before exposing):**
|
||||
- [ ] `config.json` is 0600 (or 0400) and owned by the service user —
|
||||
the service refuses to start otherwise; verify with
|
||||
`systemctl status granthi-link` after any config edit.
|
||||
- [ ] `test_mode` is absent from the production config **and**
|
||||
`GRANTHI_LINK_ALLOW_TEST_MODE` is not set in the unit environment.
|
||||
- [ ] `/opt/granthi-link/state.json` exists, is 0600, and is included in
|
||||
VPS backups — losing it orphans sub→login bindings (existing users
|
||||
would need verified-email re-adoption).
|
||||
- [ ] `GET /health` returns 200 on both binds after restart.
|
||||
- [ ] Spot-check the identity map: a repeat link for a known sub returns
|
||||
the same login; a colliding username with a different sub gets 409.
|
||||
|
||||
<!-- review-service live check 20260819T131430Z -->
|
||||
|
||||
Reference in New Issue
Block a user