aboutsummaryrefslogtreecommitdiffstats
path: root/docs
diff options
context:
space:
mode:
authortv <tv@krebsco.de>2026-11-07 22:35:09 +0100
committertv <tv@krebsco.de>2026-11-07 22:35:09 +0100
commitde12c862b3f4e69290c8e93b3b4520024ba458f6 (patch)
tree0f56381fa62948a7cb11b03fed6075fd85e7e649 /docs
parent3a9d9da74f55395ead20f3915ed04639e37b2f03 (diff)
access: the users endpoints
/admin/users: list, add (one or several), remove, disable, grant and revoke capabilities; each one change, refused if it would lock its writer out. Capabilities as the store's paths name them. Auth plan M1.
Diffstat (limited to 'docs')
-rw-r--r--docs/access.md53
-rw-r--r--docs/openapi.yaml180
-rw-r--r--docs/trust.md2
3 files changed, 219 insertions, 16 deletions
diff --git a/docs/access.md b/docs/access.md
index 519113c..db86e21 100644
--- a/docs/access.md
+++ b/docs/access.md
@@ -36,8 +36,8 @@ A swarm gets a registry from its start if its genesis node is given its first us
`--bootstrap-users`: it records them, with the swarm ID, in its first change, each with
everything (`[]` and `["_continuum"]`). Other users, of its `--htpasswd` or any node's,
may do nothing until added (the genesis node warns of those of its `--htpasswd`).
-Without it, the swarm has no registry, and is as swarms were before: users all alike
-(An existing swarm, below), until someone gives it one. `--protocol-writers` counts in
+Without it, the swarm has no registry: its users are all alike (A swarm without a
+registry, below), until someone gives it one. `--protocol-writers` counts in
both: without a registry, as who may write protocol data; with one, to set it right
should nobody be left who may manage users (Keeping the swarm manageable, below).
@@ -73,24 +73,47 @@ A user the node lets in but the registry doesn't have may do nothing (`403`).
whole swarm (its members, its remakes, its users), so it is given apart, never by
accident with the data. A user who may do both has both.
-## An existing swarm
+## Managing users
-A swarm whose document has no registry (one from before it, `_continuum/users`
-missing) goes on as before: every user a node lets in may read and write the swarm's
-data and use the admin endpoints, and only those in `--protocol-writers` write
-protocol data. It gets a registry once a protocol writer writes one, through the store:
+Through `/admin/users` ([`openapi.yaml`](openapi.yaml), Users): looking needs `read` on
+`_continuum/users`, changing `write` on it. Each change is one change to the document,
+logged and named by who made it. `POST /admin/users` adds several at once (below).
```sh
-curl -u alice --request PUT --header 'Content-Type: application/json' \
- --data '{"since": 0, "by": "user:alice", "disabled": false,
- "caps": {"1": {"path": [], "rights": ["read", "write"]},
- "2": {"path": ["_continuum"], "rights": ["read", "write"]}}}' \
- http://localhost:3000/store/_continuum/users/alice
+curl -u alice http://localhost:3000/admin/users # who, and what each may do
+curl -u alice -X POST -H 'Content-Type: application/json' \
+ --data '{"caps": [{"path": ["servers", "eu"], "rights": ["read"]}]}' \
+ http://localhost:3000/admin/users/bob # add bob, reading servers/eu
+curl -u alice -X POST -H 'Content-Type: application/json' \
+ --data '{"path": [], "rights": ["read", "write"]}' \
+ http://localhost:3000/admin/users/bob/caps # grant: answers its ID
+curl -u alice -X DELETE http://localhost:3000/admin/users/bob/caps/ID # revoke it
+curl -u alice -X PUT -H 'Content-Type: application/json' --data true \
+ http://localhost:3000/admin/users/bob/disabled # disable bob
```
-From that write on, every node serves only the users the registry has, so the first
-entry must be the writer's own, with `["_continuum"]` (a first write that isn't is
-refused, `409`); add the other users the same way.
+Disabling a user keeps who wrote what readable; removing it (`DELETE`) doesn't. The
+registry is protocol data in the store too, so a user who may write `_continuum/users`
+may also edit its entries there directly; the same checks apply.
+
+## A swarm without a registry
+
+In a swarm whose document has no registry (`_continuum/users`), started without
+`--bootstrap-users`, every user a node lets in may read and write the swarm's data and
+use the admin endpoints, and only those in `--protocol-writers` write protocol data. It
+gets a registry once a protocol writer adds its users:
+
+```sh
+curl -u alice -X POST -H 'Content-Type: application/json' --data '{
+ "alice": {"caps": [{"path": [], "rights": ["read", "write"]},
+ {"path": ["_continuum"], "rights": ["read", "write"]}]},
+ "bob": {"caps": [{"path": [], "rights": ["read", "write"]}]}
+ }' http://localhost:3000/admin/users
+```
+
+From that change on, every node serves only the users the registry has, so the caller
+must be among them, with `write` on `_continuum` (else `409`): list every node's users
+first.
## Keeping the swarm manageable
diff --git a/docs/openapi.yaml b/docs/openapi.yaml
index 49e0042..5bf5a50 100644
--- a/docs/openapi.yaml
+++ b/docs/openapi.yaml
@@ -38,6 +38,12 @@ tags:
description: |
Replacing the swarm's document with a new epoch (compaction, replacement,
import), and what happened to local writes that a remake couldn't place.
+ - name: Users
+ description: |
+ The swarm's users and what each may do (docs/access.md): reading needs `read` on
+ `_continuum/users`, changing `write` on it (in a swarm without a registry, being in
+ `protocol-writers`). A change that would keep its maker from managing users is
+ refused (`409`).
- name: Debug
description: Test hooks, only with `--debug-api`.
@@ -727,6 +733,152 @@ paths:
'401':
$ref: '#/components/responses/Unauthorized'
+ /admin/users:
+ get:
+ tags: [Users]
+ summary: List the users
+ responses:
+ '200':
+ description: Whether the swarm has a registry, and every user's entry (docs/access.md) with its `name` and whether it may manage users (`manages`).
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ registry: { type: boolean }
+ users:
+ type: array
+ items: { $ref: '#/components/schemas/User' }
+ '401':
+ $ref: '#/components/responses/Unauthorized'
+ '403':
+ description: May not read `_continuum/users`.
+ post:
+ tags: [Users]
+ summary: Add several users at once
+ description: |
+ By name, each with the capabilities `POST /admin/users/{name}` takes; none of
+ them may exist yet. How a swarm without a registry gets one: then the caller
+ must be among them, with `write` on `_continuum`.
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema:
+ type: object
+ additionalProperties:
+ type: object
+ properties:
+ caps:
+ type: array
+ items: { $ref: '#/components/schemas/CapabilityRequest' }
+ example:
+ alice: { caps: [{ path: [], rights: [read, write] }, { path: [_continuum], rights: [read, write] }] }
+ bob: { caps: [{ path: [], rights: [read, write] }] }
+ responses:
+ '201': { description: Added. }
+ '400': { description: A name that isn't one, a capability that isn't one, or nobody. }
+ '401':
+ $ref: '#/components/responses/Unauthorized'
+ '403': { description: May not manage users. }
+ '409': { description: A user exists, or the caller wouldn't manage users in a new registry. }
+
+ /admin/users/{name}:
+ parameters:
+ - { name: name, in: path, required: true, schema: { type: string } }
+ get:
+ tags: [Users]
+ summary: One user
+ responses:
+ '200':
+ description: Its entry.
+ content:
+ application/json:
+ schema: { $ref: '#/components/schemas/User' }
+ '404': { description: No such user. }
+ post:
+ tags: [Users]
+ summary: Add a user
+ requestBody:
+ required: false
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ caps:
+ type: array
+ items: { $ref: '#/components/schemas/CapabilityRequest' }
+ example: { caps: [{ path: [servers, eu], rights: [read] }] }
+ responses:
+ '201': { description: Added. }
+ '400': { description: Not a user name, or a capability that isn't one. }
+ '403': { description: May not manage users. }
+ '409': { description: The user exists. }
+ delete:
+ tags: [Users]
+ summary: Remove a user
+ description: Disabling it (`PUT …/disabled`) keeps who wrote what readable, and is usually the better choice.
+ responses:
+ '204': { description: Removed. }
+ '403': { description: May not manage users. }
+ '404': { description: No such user. }
+ '409': { description: It would keep the caller from managing users. }
+
+ /admin/users/{name}/disabled:
+ parameters:
+ - { name: name, in: path, required: true, schema: { type: string } }
+ put:
+ tags: [Users]
+ summary: Disable or enable a user
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema: { type: boolean }
+ responses:
+ '204': { description: Done. }
+ '403': { description: May not manage users. }
+ '404': { description: No such user. }
+ '409': { description: It would keep the caller from managing users. }
+
+ /admin/users/{name}/caps:
+ parameters:
+ - { name: name, in: path, required: true, schema: { type: string } }
+ post:
+ tags: [Users]
+ summary: Grant a capability
+ requestBody:
+ required: true
+ content:
+ application/json:
+ schema: { $ref: '#/components/schemas/CapabilityRequest' }
+ responses:
+ '201':
+ description: Granted, under this ID.
+ content:
+ application/json:
+ schema:
+ type: object
+ properties:
+ id: { type: string }
+ '400': { description: A capability that isn't one. }
+ '403': { description: May not manage users. }
+ '404': { description: No such user. }
+
+ /admin/users/{name}/caps/{id}:
+ parameters:
+ - { name: name, in: path, required: true, schema: { type: string } }
+ - { name: id, in: path, required: true, schema: { type: string } }
+ delete:
+ tags: [Users]
+ summary: Revoke a capability
+ responses:
+ '204': { description: Revoked. }
+ '403': { description: May not manage users. }
+ '404': { description: No such capability. }
+ '409': { description: It would keep the caller from managing users. }
+
/admin/dashboard:
get:
tags: [Admin]
@@ -1485,6 +1637,34 @@ components:
$ref: '#/components/schemas/Error'
schemas:
+ CapabilityRequest:
+ type: object
+ required: [path, rights]
+ properties:
+ path:
+ type: array
+ items: { type: string }
+ description: Segments, as the store's API takes them (`[]`, all of the swarm's data; `["_continuum"]`, protocol data).
+ rights:
+ type: array
+ items: { type: string, enum: [read, write] }
+ User:
+ type: object
+ properties:
+ name: { type: string }
+ since: { type: integer, description: Unix ms. }
+ by: { type: string, description: 'Who added it: `user:<name>`, or a node ID.' }
+ disabled: { type: boolean }
+ manages: { type: boolean, description: Whether it may manage users. }
+ caps:
+ type: object
+ additionalProperties:
+ type: object
+ properties:
+ path: { type: array, items: { type: string } }
+ rights: { type: array, items: { type: string } }
+ by: { type: string }
+ since: { type: integer }
Error:
type: object
required: [error]
diff --git a/docs/trust.md b/docs/trust.md
index bcf9bc7..fd37638 100644
--- a/docs/trust.md
+++ b/docs/trust.md
@@ -43,7 +43,7 @@ reverse proxy). It listens on `127.0.0.1:3000` by default (`--bind-addr`) and sp
plain HTTP: to expose it, put a TLS proxy in front. What each user may do is the
swarm's: its user registry, in the document, gives each capabilities by path, to read
and to write, protocol data apart ([`access.md`](access.md)); a user the registry doesn't have
-may do nothing. In a swarm without a registry (one from before it), all users may read
+may do nothing. In a swarm without a registry (started without one), all users may read
and write user data and use the admin endpoints, and only those in `--protocol-writers`
write protocol data. Epoch metadata and the swarm ID are never writable. The `/debug` endpoints exist only with
`--debug-api`.