POST …/subjects, refused for another principal
POST /console/api/admin/tenants/{tenant}/subjects, refused for another principal
On the same tenant. Every tab of a browser shares the console cookie, so another
tab's sign-in can replace the session under a page that still shows the old
administrator. The console therefore names, on each admin request, the subject
its tab believes is signed in, in X-Odudu-Console-Subject. The session this
run's jar carries is grace's:
curl -sS -D - -c jar -b jar http://localhost:3185/console/api/session
HTTP/1.1 200 OK
x-request-id: 01a13ff3-8277-7d7b-a675-a2fce66260f9
cache-control: no-store
content-type: application/json; charset=utf-8
content-length: 97
Date: Thu, 15 Oct 2026 14:24:45 GMT
Connection: keep-alive
Keep-Alive: timeout=72
{"tenant":"console-paths","subject_id":"01a13ff2-eff9-746c-b8cb-e56af2072daa","username":"grace"}
The three writes below ran one after another, between
2026-10-15T14:24:45.738Z and 2026-10-15T14:24:45.846Z as
new Date().toISOString() printed them before and after, with the same
cookie and body. The first names hopper, another subject of the tenant, and is
refused 409 with the gateway's own problem type. The session is live, so
the cookie is left alone:
curl -sS -D - -c jar -b jar -X POST \
-H 'Origin: http://localhost:3185' -H 'X-Odudu-Console: 1' \
-H 'X-Odudu-Console-Subject: 01a13ff2-f33a-79ce-a952-0aeec04a80d4' \
-H 'Content-Type: application/json' \
-d '{"username":"lovelace","email":"lovelace@example.com"}' \
http://localhost:3185/console/api/admin/tenants/console-paths/subjects
HTTP/1.1 409 Conflict
x-request-id: 01a13ff3-82b7-7c08-9976-da2cd1906ab8
content-type: application/problem+json; charset=utf-8
cache-control: no-store
content-length: 130
Date: Thu, 15 Oct 2026 14:24:45 GMT
Connection: keep-alive
Keep-Alive: timeout=72
{"status":409,"type":"about:blank#console-principal-changed","title":"Conflict","instance":"01a13ff3-82b7-7c08-9976-da2cd1906ab8"}
The second names no subject. A write that does not say who it acts for is refused the same way; a read that names nobody is still forwarded:
curl -sS -D - -c jar -b jar -X POST \
-H 'Origin: http://localhost:3185' -H 'X-Odudu-Console: 1' \
-H 'Content-Type: application/json' \
-d '{"username":"lovelace","email":"lovelace@example.com"}' \
http://localhost:3185/console/api/admin/tenants/console-paths/subjects
HTTP/1.1 409 Conflict
x-request-id: 01a13ff3-82c7-70e7-ae4e-61259517bd0c
content-type: application/problem+json; charset=utf-8
cache-control: no-store
content-length: 130
Date: Thu, 15 Oct 2026 14:24:45 GMT
Connection: keep-alive
Keep-Alive: timeout=72
{"status":409,"type":"about:blank#console-principal-changed","title":"Conflict","instance":"01a13ff3-82c7-70e7-ae4e-61259517bd0c"}
The third names grace, and is forwarded. The refusals above were the
gateway's, not the admin API refusing the body:
curl -sS -D - -c jar -b jar -X POST \
-H 'Origin: http://localhost:3185' -H 'X-Odudu-Console: 1' \
-H 'X-Odudu-Console-Subject: 01a13ff2-eff9-746c-b8cb-e56af2072daa' \
-H 'Content-Type: application/json' \
-d '{"username":"lovelace","email":"lovelace@example.com"}' \
http://localhost:3185/console/api/admin/tenants/console-paths/subjects
HTTP/1.1 201 Created
x-request-id: 01a13ff3-82d7-7673-a308-c737b160a07e
content-type: application/json; charset=utf-8
etag: "a2ce5bf02107d3ac576007a7107ff7fd5fccf91e7d1e1c7c51376c48046f85f9"
cache-control: no-store
content-length: 167
Date: Thu, 15 Oct 2026 14:24:45 GMT
Connection: keep-alive
Keep-Alive: timeout=72
{"id":"01a13ff3-82ec-7627-b426-616ba224159c","type":"user","username":"lovelace","email":"lovelace@example.com","enabled":true,"created_at":"2026-10-15T14:24:45.803Z"}
The audit trail for that second, read through the proxy, holds one row, the third write's. The two refused writes reached nothing:
curl -sS -D - -b jar -H 'X-Odudu-Console-Subject: 01a13ff2-eff9-746c-b8cb-e56af2072daa' \
'http://localhost:3185/console/api/admin/tenants/console-paths/audit?from=2026-10-15T14:24:45Z&to=2026-10-15T14:24:46Z'
HTTP/1.1 200 OK
x-request-id: 01a13ff3-90bc-7645-93c2-b50e860c88fb
content-type: application/json; charset=utf-8
cache-control: no-store
content-length: 552
Date: Thu, 15 Oct 2026 14:24:49 GMT
Connection: keep-alive
Keep-Alive: timeout=72
{"items":[{"id":"01a13ff3-82f0-7a4e-aec6-f4859251f88e","occurred_at":"2026-10-15T14:24:45.803Z","event_type":"admin_mutation","action":"subject.create","outcome":"allowed","actor_tenant_id":"01a13ff2-eddb-7d89-8555-c32d1cd8acf9","actor_subject_id":"01a13ff2-eff9-746c-b8cb-e56af2072daa","actor_client_id":"01a13ff2-ee18-7fab-b610-0fb99df8e511","actor_name":"grace","actor_origin":"tenant","resource_type":"subject","resource_id":"01a13ff3-82ec-7627-b426-616ba224159c","request_id":"01a13ff3-82d7-7673-a308-c737b160a07e","ip":"172.20.0.1","detail":{}}]}