Give Staff Wider Access
Staff of the shop see the orders of every customer; support may read them, managers may approve them; and a customer who abuses the shop is shut out. Staff are identities with roles. A route names the role it requires, and somebody who is allowed to grants that role to a person.
§Require a role on a route
Customers already reach their own orders through auth:id. Add a second way in:
# components/store.orders/manifest.toa.yaml
exposition:
/:customer:
auth:id: customer
auth:role: store:orders
io:output: [id, items, total, status]
GET: enumerate
POST: create
/:id:
GET: observe The customer is admitted by auth:id, and so is any identity that holds store:orders. Nothing
registers a role: it exists once it is granted, and is required once a route names it.
§The first administrator
Granting a role takes a role, so one identity has it by declaration. Name its username in the
Context, with the authority it belongs to. toa create wrote the authority, local; the
principal is added:
# context.toa.yaml
exposition:
authorities:
local: localhost:8000
configuration:
identity.basic:
principal:
authority: local
username: root $ npm run restart and create it, once per environment:
POST /identity/ HTTP/1.1
host: api.store.example
authorization: Basic cm9vdDpzZWNyZXQjMTIzNA== 201 Created
authorization: Token eyJhbGciOiJkaXIi... The identity created with that username under that authority is given the role system, and
the token of this reply already has it. system allows granting roles, banning, and issuing
tokens for anyone. Grant narrower roles to the people who need them and keep the principal’s
password out of daily use.
systemdoes not open the shop’s own routes:store:ordersis not withinsystem. Grant the principal what it needs, or declare an administrative role on the whole tree.- An account of Google or another provider can be the principal instead; see Sign In with Google or Apple.
- Created with
POST /identity/basic/, which returns no token, the principal has the role by the time it first signs in.
§Grant a role
Carol works in support. She has an identity like anyone else
(POST /identity/ gives her one without an
account in the shop). The principal grants her the role:
POST /identity/roles/9c1f0a6d5b7e4f3a8d2c1b0a9e8f7d6c/ HTTP/1.1
host: api.store.example
authorization: Token <the principal's token>
content-type: application/yaml
accept: application/yaml
role: store:orders 201 Created
id: 9c4702490ff84f2a9e1b1da2ab64bdd4
grantor: 72cf9b0ab0ac4ab2b8036e4e940ddcae The path names the identity that receives the role; grantor is the identity that granted it.
Carol now reads Bob’s orders:
GET /store/orders/2428c31ecb6e4a51a24ef52f0c4181b9/ HTTP/1.1
host: api.store.example
authorization: Token <Carol's token> 200 OK where a customer other than Bob still gets 403 Forbidden.
| Reply to a grant | When |
|---|---|
201 Created | The role is granted. |
400 Bad Request | The path is not an identity id, or the role is not a valid role name. |
401 / 403 | The caller has no identity, or holds neither of the roles that allow granting. |
422, code INACCESSIBLE_SCOPE | The caller may pass on roles, but not this one. |
§When a change takes effect
A token a client already holds does not have the new role yet.
- A client that uses a
Tokensees a grant or a revocation when its token is next replaced: withinidentity.tokens.refreshseconds, 10 minutes by default. The client does nothing for it. - A request authenticated in any other way (a password, a provider’s token) acts with the current roles. To see a role at once, the client signs in again.
- A custom token never changes. A grant does not reach it: issue a new one. Revoking any role of an identity revokes all its custom tokens; issue them again and they carry the roles that are left.
§Test it
The scenario signs the principal in with its password, which must be known where the tests run.
The tests call localhost:8000, the host of the authority local, so root is the principal
there once it is created:
$ curl -i -X POST http://localhost:8000/identity/ -u 'root:secret#1234' The steps are the ones toa create wrote, which Write a Test explains;
the customer signs up at the sign-up route.
# features/staff.feature
Feature: Staff
Scenario: Support reads the orders of any customer
# the principal, created once with `POST /identity/`
When the request is sent:
"""
GET /identity/ HTTP/1.1
authorization: Basic cm9vdDpzZWNyZXQjMTIzNA==
"""
Then the response is received:
"""
200 OK
authorization: Token ${{ root.token }}
"""
# a customer
When the request is sent:
"""
POST /accounts/ HTTP/1.1
content-type: application/yaml
accept: application/yaml
name: Bob
"""
Then the response is received:
"""
201 Created
id: ${{ bob.id }}
"""
# support
When the request is sent:
"""
POST /identity/ HTTP/1.1
authorization: Basic #{{ basic | set carol.basic }}
accept: application/yaml
"""
Then the response is received:
"""
201 Created
authorization: Token ${{ carol.stale }}
id: ${{ carol.id }}
"""
When the request is sent:
"""
GET /store/orders/${{ bob.id }}/ HTTP/1.1
authorization: Token ${{ carol.stale }}
"""
Then the response is received:
"""
403 Forbidden
"""
When the request is sent:
"""
POST /identity/roles/${{ carol.id }}/ HTTP/1.1
authorization: Token ${{ root.token }}
content-type: application/yaml
role: store:orders
"""
Then the response is received:
"""
201 Created
"""
# signing in again gives a token with the role
When the request is sent:
"""
GET /identity/ HTTP/1.1
authorization: Basic ${{ carol.basic }}
accept: application/yaml
"""
Then the response is received:
"""
200 OK
authorization: Token ${{ carol.token }}
roles:
- store:orders
"""
When the request is sent:
"""
GET /store/orders/${{ bob.id }}/ HTTP/1.1
authorization: Token ${{ carol.token }}
"""
Then the response is received:
"""
200 OK
""" $ npm run restart
$ npm run features §Name roles from general to specific
A role is a string of alphanumeric segments separated by colons, at most 32 characters per segment:
store
store:orders
store:orders:refunds A role covers itself and everything under it. A route that requires store:orders:refunds admits a holder of that role or of a more general one:
| The identity holds | A route requiring store:orders:refunds |
|---|---|
store:orders:refunds | admits |
store:orders | admits |
store | admits |
store:orders:refunds:eu | refuses with 403 |
store:catalog | refuses with 403 |
So one grant near the root gives a whole area: a clerk gets store:orders:refunds, a manager store:orders, the owner store.
The root system is reserved for the gateway’s own resources; its roles are listed below.
§Roles per resource
A required role can include a route variable:
# components/store.catalog/manifest.toa.yaml
exposition:
/:vendor:
auth:role: store:vendors:{vendor}:manager
PUT: transit Grant store:vendors:acme:manager to the manager of one vendor, store:vendors:acme to its
owner, and store:vendors to staff who manage all of them. Values of a variable used this way
must be valid role segments.
§Read the roles
An identity’s roles come with the identity, and a client uses them to decide what to show:
GET /identity/ HTTP/1.1
host: api.store.example
authorization: Token eyJhbGciOiJkaXIi...
accept: application/yaml id: 9c1f0a6d5b7e4f3a8d2c1b0a9e8f7d6c
roles:
- store:orders The roles of any identity are read at:
GET /identity/roles/9c1f0a6d5b7e4f3a8d2c1b0a9e8f7d6c/ HTTP/1.1
host: api.store.example
authorization: Token eyJhbGciOiJkaXIi...
accept: application/yaml - store:orders This is open to the identity itself and to holders of system:identity:roles.
§Let staff pass their roles on
Granting any role to anyone (system:identity:roles) is too much for most people who need to
grant something. An identity with the role system:identity:roles:delegation may grant roles
that its own roles cover, and no others.
A moderator who holds store:moderation and system:identity:roles:delegation can give an
assistant store:moderation or store:moderation:photos. Asked to grant store:finance, the
gateway answers:
422 Unprocessable Entity
code: INACCESSIBLE_SCOPE The right to pass roles on cannot itself be passed on: roles within system:identity:roles are
never granted this way.
§Revoke a role
DELETE /identity/roles/9c1f0a6d5b7e4f3a8d2c1b0a9e8f7d6c/store:orders/ HTTP/1.1
host: api.store.example
authorization: Token <a token of an identity with system:identity:roles> 204 No Content The last segment is the role. Revoking requires system:identity:roles; the right to pass roles
on does not include taking them back.
§Ban a user
A banned identity cannot sign in with anything it has.
PUT /identity/bans/e8e4f9c2a68d419b861403d71fabc915/ HTTP/1.1
host: api.store.example
authorization: Token <a token of an identity with system:identity:bans>
content-type: application/yaml
banned: true
comment: Chargeback fraud 204 No Content comment is optional. The identity that issued the ban is recorded with it.
- Requests with the identity’s password, provider token, emailed code or passkey are answered
401at once. - A
Tokenit already holds keeps working until it is next replaced, at mostidentity.tokens.refreshseconds, and is then answered401. - Its custom tokens are revoked, and stop working
within
identity.tokens.cache.ttl. - A sign-up cannot create credentials for a banned id.
Lifting the ban is the same request with banned: false:
PUT /identity/bans/e8e4f9c2a68d419b861403d71fabc915/ HTTP/1.1
host: api.store.example
authorization: Token <a token of an identity with system:identity:bans>
content-type: application/yaml
banned: false The user signs in again and receives a new token. Script tokens are not restored; the user issues new ones.
A ban concerns the identity, not the shop’s account or orders with the same id. If a banned customer’s pending orders must be cancelled, that is an operation of the shop, called by whoever issues the ban.
§System roles
The gateway’s resources require these roles. Some are also open to the identity they concern.
| Role | Allows | Also open to |
|---|---|---|
system:identity:roles | Granting any role, revoking, reading anyone’s roles. | The identity itself, for reading. |
system:identity:roles:delegation | Granting roles the holder’s own roles cover. | |
system:identity:basic | Changing or removing anyone’s password. | The identity itself. |
system:identity:tokens | Issuing a token for any identity. | The identity itself. |
system:identity:keys | Listing and revoking anyone’s custom tokens. | The identity itself. |
system:identity:otp | Issuing a sign-in code for any identity. | The identity itself. |
system:identity:bans | Banning and unbanning. |
Routes that require a system role are marked system in resource descriptions, so that tools
which list an application’s resources can leave them out.