GET /admin/tenants/{tenant} and PATCH…
GET /admin/tenants/{tenant} and PATCH /admin/tenants/{tenant}
Both require manage-tenant on the tenant named in the path — the tenant
itself, not the collection, so a tenant-local admin reaches its own and a
system admin reaches any tenant's by holding manage-tenant there too.
The GET answers the same shape GET /admin/tenants lists, with an
ETag over it; the PATCH accepts If-Match and answers 412 on a
mismatch, the same optional concurrency control PATCH /settings uses.
Two fields amend: display_name and enabled. Everything else is refused
with 400 carrying its reason, name most of all — it is already in the
issuer URL of every token this tenant has minted and in the path of every
request addressed to it, so renaming through a general amendment would
orphan both. That is a decision rather than a gap, and ADR 0039 makes it
permanent: a rename that reissued nothing would leave every relying party's
configured issuer pointing at a tenant that no longer answers. The refusal
names the way to do it instead — create a new tenant, export this one and
import it under the new name.
enabled: false is how a tenant is taken out of service without deleting
it: its own administrators stop authenticating, so the flag is not one to
set from a token issued by the tenant being disabled. On the system
tenant it is refused with 409 — every cross-tenant administrator
authenticates there, so disabling it would lock the whole deployment's
administration out with psql the only way back, the same reasoning that
guards the built-in admin client. Once a disable
has committed, every live session the tenant holds is ended, 500 to a
transaction, each batch queueing a Back-Channel Logout Token for each client
that registered a URI and writing a session.end_all row — the precondition
DELETE /admin/tenants/{tenant} below refuses without, which describes the
batches. Sending {"enabled": false} to a tenant already disabled ends any
session still live. PATCH /settings with {"enabled": false} does the same.
Re-enabling restores nothing.
On the showcase tenant POST /admin/tenants created there with display_name: "Showcase". The read, then an amendment, then the two refusals:
curl -sS -D - \
-H "Authorization: Bearer $ADMIN_TOKEN" \
http://localhost:3080/admin/tenants/showcase
HTTP/1.1 200 OK
x-request-id: 01a0ea4e-884f-7a0a-8f24-30731b9da1ec
cache-control: no-store
etag: "2d683197c5f4fe27d0a0d6521f2038605964ad36a6d949d9962050fe63c463dd"
content-type: application/json; charset=utf-8
content-length: 144
Date: Mon, 28 Sep 2026 23:16:50 GMT
Connection: keep-alive
Keep-Alive: timeout=72
{"id":"01a0ea4d-c75e-7ac7-abe7-cdb5a6b2bbad","name":"showcase","display_name":"Showcase","enabled":true,"created_at":"2026-09-28T23:16:00.990Z"}
curl -sS -X PATCH -H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" -d '{"display_name": "Showcase Holdings"}' \
http://localhost:3080/admin/tenants/showcase
curl -sS -X PATCH -H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" -d '{"name": "showcase-2"}' \
http://localhost:3080/admin/tenants/showcase
curl -sS -X PATCH -H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" -d '{"enabled": false}' \
http://localhost:3080/admin/tenants/system
{"id":"01a0ea4d-c75e-7ac7-abe7-cdb5a6b2bbad","name":"showcase","display_name":"Showcase Holdings","enabled":true,"created_at":"2026-09-28T23:16:00.990Z"}
{"type":"about:blank","title":"Bad Request","status":400,"detail":"name: name is already in the issuer URL of every token this tenant has minted, and in the path of every admin and protocol request addressed to it; renaming it needs its own operation, not a general amendment","errors":[{"path":"name","message":"name is already in the issuer URL of every token this tenant has minted, and in the path of every admin and protocol request addressed to it; renaming it needs its own operation, not a general amendment"}],"instance":"01a0ea4e-abe7-7d8b-ab67-3413d1652bfd"}
{"type":"about:blank","title":"Conflict","status":409,"detail":"system is the tenant every cross-tenant administrator authenticates against and cannot be disabled","instance":"01a0ea4e-abfd-77ec-8f27-a082041215b6"}
Replaying the ETag from the read above — one generation stale after the
amendment — is refused and changes nothing:
{"type":"about:blank","title":"Precondition Failed","status":412,"detail":"If-Match no longer matches","instance":"01a0ea4e-ac11-7806-9118-fbea0bdb05e3"}
The name refusal's text was changed after the transcript above was recorded: a rename is not offered, permanently, by ADR 0039, and the refusal now says so rather than pointing at an operation that will never exist. The tenant's and, for the same reason, a group's:
curl -sS -X PATCH -H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" -d '{"name": "ops-demo-2"}' \
http://localhost:3082/admin/tenants/ops-demo; echo
curl -sS -X PATCH -H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" -d '{"name": "finance-2"}' \
http://localhost:3082/admin/tenants/ops-demo/groups/01a0ee8b-2f54-7a5a-9da6-7ba6baacbe13; echo
{"type":"about:blank","title":"Bad Request","status":400,"detail":"name: name is already in the issuer URL of every token this tenant has minted, and in the path of every admin and protocol request addressed to it; a rename is not offered, by ADR 0039: create a new tenant, export this one and import it under the new name","errors":[{"path":"name","message":"name is already in the issuer URL of every token this tenant has minted, and in the path of every admin and protocol request addressed to it; a rename is not offered, by ADR 0039: create a new tenant, export this one and import it under the new name"}],"instance":"01a0ee9e-a4d8-72b1-9896-b14ad3d19376"}
{"type":"about:blank","title":"Bad Request","status":400,"detail":"name: name is embedded in every descendant path and in the groups claim a relying party matches on; a rename is not offered, by ADR 0039: create a new group and move its members","errors":[{"path":"name","message":"name is embedded in every descendant path and in the groups claim a relying party matches on; a rename is not offered, by ADR 0039: create a new group and move its members"}],"instance":"01a0ee9e-a4ed-7b89-9fdd-193dd93a8064"}