Skip to main content

GET /scopes/:id/mappers and PUT /scopes/:id/mappers

Both require manage-tenant. GET returns available — every mapper name the process's ClaimMapperRegistry carries, the same registry ID token and /userinfo issuance assemble claims from — and bound, the names this tenant bound to this scope, empty when the scope has no binding rows and falls back to whichever mappers declare it, which declared names. PUT replaces the whole binding set and answers the same shape; binding a name the registry does not carry answers 400, listing the known names.

If-Match is mandatory here, not optional. This route replaces an authorization-bearing list whole, so a stale write reinstates exactly what another administrator has just removed; the header comes from the matching GET, which answers an ETag over the same list. Absent, the request is refused with 428 Precondition Required and nothing is changed; stale, with 412. The list is read under the same lock the replacement runs under, so two callers sent at once are serialised — the second sees what the first wrote rather than matching the same pre-write state.

A scope with no bindings is unaffected by another scope's: binding sub to one scope narrows only that scope's own claims, never email's or any other scope's in the same tenant.

declared names the mappers that declare the scope, the ones issuance runs for it while bound is empty. It comes from code, so a write never changes it and the ETag does not cover it, as it does not cover available.

In scope-reach-demo. The scope email (01a12d29-70d3-7b41-8b8f-2b3bcf07e79d) is one every tenant starts with, and the email mapper declares it; billing (01a12d29-72a6-77f6-9e8b-c2fcbb928cd2) is declared by none. The read, then the write under its tag, then the read back:

curl -sS -D - \
-H "Authorization: Bearer $ADMIN_TOKEN" \
http://localhost:3082/admin/tenants/scope-reach-demo/scopes/01a12d29-70d3-7b41-8b8f-2b3bcf07e79d/mappers

curl -sS -D - -X PUT \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-H 'If-Match: "59c1141bb2da82132d257ffa8b417c0a7c4e9985057f06388c4d62534ecb2183"' \
-d '{"mapper_names": ["sub"]}' \
http://localhost:3082/admin/tenants/scope-reach-demo/scopes/01a12d29-70d3-7b41-8b8f-2b3bcf07e79d/mappers

curl -sS -D - \
-H "Authorization: Bearer $ADMIN_TOKEN" \
http://localhost:3082/admin/tenants/scope-reach-demo/scopes/01a12d29-70d3-7b41-8b8f-2b3bcf07e79d/mappers

The empty bound on the first read is the fallback case, not an error: email runs for this scope until something is bound. Binding sub replaces it, and declared still names email:

HTTP/1.1 200 OK
x-request-id: 01a12d29-9b02-721b-93cd-cb20e533372b
cache-control: no-store
etag: "59c1141bb2da82132d257ffa8b417c0a7c4e9985057f06388c4d62534ecb2183"
content-type: application/json; charset=utf-8
content-length: 106

{"available":["sub","profile","email","roles","groups","address","phone"],"bound":[],"declared":["email"]}

HTTP/1.1 200 OK
x-request-id: 01a12d29-9b36-777e-8b19-641e3f840103
cache-control: no-store
etag: "e853268082f466e074aba6be62bd16b45ac4a0037e40335ae4eaf476c9defcb4"
content-type: application/json; charset=utf-8
content-length: 111

{"available":["sub","profile","email","roles","groups","address","phone"],"bound":["sub"],"declared":["email"]}

HTTP/1.1 200 OK
x-request-id: 01a12d29-9b55-74f9-a386-3516a179a75a
cache-control: no-store
etag: "e853268082f466e074aba6be62bd16b45ac4a0037e40335ae4eaf476c9defcb4"
content-type: application/json; charset=utf-8
content-length: 111

{"available":["sub","profile","email","roles","groups","address","phone"],"bound":["sub"],"declared":["email"]}

On billing, which no mapper declares, declared is empty and so is the fallback; the same write without the header is refused:

curl -sS \
-H "Authorization: Bearer $ADMIN_TOKEN" \
http://localhost:3082/admin/tenants/scope-reach-demo/scopes/01a12d29-72a6-77f6-9e8b-c2fcbb928cd2/mappers

curl -sS -X PUT \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"mapper_names": ["sub"]}' \
http://localhost:3082/admin/tenants/scope-reach-demo/scopes/01a12d29-72a6-77f6-9e8b-c2fcbb928cd2/mappers
{"available":["sub","profile","email","roles","groups","address","phone"],"bound":[],"declared":[]}

{"type":"about:blank","title":"Precondition Required","status":428,"detail":"If-Match is required to replace a scope’s claim mapper bindings","instance":"01a12d29-b389-725f-abd1-4064b5d48e63"}

A name the registry does not carry, refused with the names it does, on the billing scope in its demo, under the ETag its GET …/mappers answered:

curl -sS -X PUT \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-H 'If-Match: "59c1141bb2da82132d257ffa8b417c0a7c4e9985057f06388c4d62534ecb2183"' \
-d '{"mapper_names": ["nonesuch"]}' \
http://localhost:3080/admin/tenants/demo/scopes/01a0ea5d-c42a-723c-8bb2-b45950186b42/mappers
{"type":"about:blank","title":"Bad Request","status":400,"detail":"unknown mapper name(s): nonesuch; known: sub, profile, email, roles, groups, address, phone","errors":[{"path":"mapper_names","message":"names no registered mapper nonesuch"}],"instance":"01a0ea5e-0ac9-7763-81a3-db353b147f0c"}