IV Users and Access

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.

  • system does not open the shop’s own routes: store:orders is not within system. 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 grantWhen
201 CreatedThe role is granted.
400 Bad RequestThe path is not an identity id, or the role is not a valid role name.
401 / 403The caller has no identity, or holds neither of the roles that allow granting.
422, code INACCESSIBLE_SCOPEThe 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 Token sees a grant or a revocation when its token is next replaced: within identity.tokens.refresh seconds, 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 holdsA route requiring store:orders:refunds
store:orders:refundsadmits
store:ordersadmits
storeadmits
store:orders:refunds:eurefuses with 403
store:catalogrefuses 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 401 at once.
  • A Token it already holds keeps working until it is next replaced, at most identity.tokens.refresh seconds, and is then answered 401.
  • 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.

RoleAllowsAlso open to
system:identity:rolesGranting any role, revoking, reading anyone’s roles.The identity itself, for reading.
system:identity:roles:delegationGranting roles the holder’s own roles cover.
system:identity:basicChanging or removing anyone’s password.The identity itself.
system:identity:tokensIssuing a token for any identity.The identity itself.
system:identity:keysListing and revoking anyone’s custom tokens.The identity itself.
system:identity:otpIssuing a sign-in code for any identity.The identity itself.
system:identity:bansBanning 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.

Search
Search the documentation