mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 06:06:10 +00:00
Add a self-service measurements:write API token scope
Since v1.30.17 there has been no self-service path to a token that can write a measurement. That release made the Bearer scope default fail-closed, which closed a real privilege-escalation hole, and removed the generic mint at POST /api/tokens along with it. The generic mint had been the only way to get a token that could reach /api/measurements, so a local sensor pushed through Home Assistant or Node-RED lost its door while the OAuth integrations kept theirs. Add a narrow scope accepted by exactly two routes, both writes, both on the holder's own record. The scope constant is a leaf module beside the surface that consumes it, matching FHIR_READ_SCOPE and the MCP scopes. requireRecordAuth grows an options object rather than a sibling function, because the sharing surface guard keys every leg on that one identifier and a second name would mean two copies of the delegable declaration. AuthContext carries the admitting scope as a single nullable string rather than the token's permission array, so nothing downstream can re-derive an authorisation decision from it. A scoped credential is refused a record selector before any grant is read. Placing the refusal above the sharing check rather than beside it means no future widening of what a grant permits can reach one of these tokens. Rows are attributed MANUAL. An entry that explicitly names APPLE_HEALTH is refused 422 rather than relabelled: that source is half the (userId, type, source, externalId) dedup key the iOS client writes into, it participates in the cross-source merge, and it gates the HealthKit sync fields. For the same reason a scoped write no longer advances User.lastSyncedAt, which the phone reads to size its undelivered window. Also closes a narrower gap found while building this. The idempotency wrapper resolved a replayed delegated response against a live grant without considering which credential was presenting it, so a cached response about a shared record could be returned to a token the handler itself would refuse. No write was reachable that way; the response body was. Guards: T3 gains the mint site, T4's vocabulary gains the scope and learns to read the requireRecordAuth form (without which this change would open a way to widen a route that the closed vocabulary could not see), and a new T5 freezes the set of delegable routes naming a scope. The sharing surface guard's AST matcher reads only the first two arguments, so it needed prose but no code.
This commit is contained in:
1 parent
4308114c5b
commit
cba7a0fce5
26 files changed
+1704
-34
No files matched your search
+16
-2
@@ -56,8 +56,22 @@ before treating a warning as a regression.
|
||||
`Labs`, `Illness`, `Documents`, `Records`, `Admin`, `Meta`,
|
||||
`Retired`).
|
||||
- **`securitySchemes`**:
|
||||
- `bearerAuth` — long-lived `hlk_*` API tokens issued via `POST /api/tokens`.
|
||||
Required on `/api/ingest/medication`.
|
||||
- `bearerAuth` — `hlk_*` API tokens. A scope grants what it names and
|
||||
nothing adjacent, so where a token comes from decides what it reaches:
|
||||
- a completed password / passkey / refresh exchange mints the
|
||||
cookie-equivalent `["*"]` token the native client carries;
|
||||
- a medication's own API-endpoint toggle mints `medication:ingest` plus
|
||||
`medication:<id>:ingest`, the pair `/api/ingest/medication` gates on;
|
||||
- `POST /api/mcp/tokens` mints `health:read` (optionally plus
|
||||
`health:write`), audience-bound to `/mcp` and refused on REST writes;
|
||||
- `POST /api/tokens/measurements` mints `measurements:write`, accepted by
|
||||
`POST /api/measurements` and `POST /api/measurements/batch` only, on the
|
||||
holder's own record only.
|
||||
|
||||
`POST /api/tokens` is **not** among them — that generic mint was removed in
|
||||
v1.30.17. The path still lists (`GET`) and revokes (`DELETE /api/tokens/{id}`)
|
||||
every token above, whichever surface issued it.
|
||||
|
||||
- `sessionCookie` — HttpOnly `healthlog_session` cookie set by the
|
||||
password and passkey login flows. Used by the web app and any iOS
|
||||
client running inside a web view.
|
||||
|
||||
Reference in new issue
Block a user