Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 76 additions & 6 deletions docs/integrate/management-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ scope.
Use this flow when no person is present:

1. Create a role containing only the required Modgud permissions. For the
currently exposed Position reads, grant `position:read`.
Position reads, grant `position:read`. Terminal provisioning needs both
`position:write` and `oauth-client:write`.
2. Create or choose a group, attach the role, and add the Service Account as a
member.
3. On that Service Account, issue a credential and allow the
Expand Down Expand Up @@ -70,6 +71,72 @@ The token's `sub` is the Service Account id. A token from an unlinked or
differently linked client is rejected even if its claims were otherwise
well-formed.

## Provision a terminal in one call

`POST /api/admin/oauth/clients` is the generic client-provisioning operation.
When the request carries the staffing grant, Modgud atomically creates the
terminal-managed OAuth client and terminal slot, and either links an existing
Position or creates one inline. Nothing is committed if any part fails.

The consumer MUST choose a stable `clientId`; Modgud does not generate one on
this path. For an existing Position:

```http
POST /api/admin/oauth/clients
Authorization: Bearer <management-access-token>
Content-Type: application/json

{
"clientId": "alerthub-gate-3",
"displayName": "AlertHub terminal: Gate 3",
"clientType": "public",
"allowedGrantTypes": [
"urn:cocoar:params:oauth:grant-type:staffing"
],
"linkedPositionPrincipalId": "<position-guid-or-short-guid>",
"terminalDisplayName": "Gate terminal left",
"terminalLocation": "Gate 3",
"terminalBinding": "dpop",
"webAuthnRpId": "terminal.example.com",
"scopes": ["alerthub-terminal"],
"appIds": ["<app-guid-or-short-guid>"]
}
```

To create the Position in the same transaction, omit
`linkedPositionPrincipalId` and send `newPosition` instead:

```json
{
"accountName": "gate-3",
"purpose": "Gatehouse response position",
"terminalPolicy": {
"enabled": true
}
}
```

The first successful request returns `201 Created` with `client`,
`createdTerminalId`, and—only for inline creation—`createdPosition`.
`client-secret` binding also returns `clientSecret` once. The returned terminal
ShortGuid is accepted directly by the terminal routes; consumers do not need to
convert it to a canonical GUID.

The caller-selected `clientId` is the retry key:

- the same normalized request returns `200 OK`, the same terminal id, and
`wasAlreadyProvisioned: true`;
- a different request under the same `clientId` returns `409 Conflict`;
- a replay never repeats a one-time `clientSecret`. If its original response
was lost, rotate that secret deliberately. DPoP provisioning has no secret to
recover.

Terminal provisioning evaluates both `oauth-client:write` and
`position:write`. Generic client creation needs `oauth-client:write`; linking
or inline-creating a Service Account additionally needs
`service-account:write`. This prevents a client administrator from minting a
credential for a more privileged machine identity.

## Delegated-person setup

Use this flow when the consumer should act with the permissions of a signed-in
Expand Down Expand Up @@ -116,11 +183,14 @@ scheme does not turn every cookie-only admin route into a remote API.
|---|---|---|---|
| `GET` | `/api/position` | `position:read` | `PositionTerminals` enabled |
| `GET` | `/api/position/{id}` | `position:read` | `PositionTerminals` enabled |

Position creation, mutation, deletion, grants, terminal enrollment, and all
other admin resources remain cookie-only until their contracts are deliberately
added and tested. The [Admin endpoint reference](/reference/admin-api) is the
source of truth for the exposed surface.
| `POST` | `/api/admin/oauth/clients` | `oauth-client:write` | `position:write` for terminal provisioning; `service-account:write` for an SA link |

Direct Position creation, mutation, deletion, grants, terminal enrollment, and
all other admin resources remain cookie-only until their contracts are
deliberately added and tested. The atomic OAuth-client create above is the
supported remote terminal-provisioning path. The
[Admin endpoint reference](/reference/admin-api) is the source of truth for the
exposed surface.

## Security rules for consumers

Expand Down
10 changes: 8 additions & 2 deletions docs/integrate/position-terminals.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,8 @@ assigned to several compatible positions before enrollment.
| Parameter | Source | Notes |
|---|---|---|
| Modgud base URL | deployment | |
| `client_id` | slot response | generated as `terminal.{8 chars}` |
| `terminal_id` | slot response | used by lock, registration, and step-up routes |
| `client_id` | consumer provisioning request | stable, caller-selected identifier; never generated by the Management API path |
| `terminal_id` | provisioning response | ShortGuid accepted directly by lock, registration, and step-up routes |
| `client_secret` | creation response | only for `client-secret`; shown once |
| device P-256 key | terminal | only for `dpop`; ideally non-exportable |
| RP-ID | slot response | WebAuthn RP for personal passkeys and position-token credentials |
Expand All @@ -83,6 +83,12 @@ Changing a binding, losing a key/secret, or adding a position after enrollment
means a fresh slot and Device Flow. Removing an assignment is immediate and
ends a running session for that position.

Backends should use the
[Management API terminal-provisioning contract](./management-api#provision-a-terminal-in-one-call):
it creates or links the Position, terminal slot, and managed OAuth client
atomically. The interactive admin quick-add path may still generate a
convenience client id, but that is not the consumer provisioning contract.

Apps, business scopes, and the OAuth-client display name remain editable under
**Admin → OAuth Clients**. The terminal lifecycle, grants, binding, RP-ID, and
reference-token profile remain terminal-owned and locked. Changing Apps or
Expand Down
20 changes: 11 additions & 9 deletions docs/reference/admin-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,16 +186,18 @@ rejects mutating them directly).
## OAuth clients

Deletes are gated by `oauth-client:write` — there is no
`oauth-client:delete` tier.
`oauth-client:delete` tier. Client creation is also a **Management API** route.
Terminal intent additionally requires `position:write`; linking or creating a
Service Account additionally requires `service-account:write`.

| Method | Path | Permission |
|---|---|---|
| `GET` | `/api/admin/oauth/clients` | `oauth-client:read` |
| `GET` | `/api/admin/oauth/clients/{id}` | `oauth-client:read` |
| `POST` | `/api/admin/oauth/clients` | `oauth-client:write` |
| `PUT` | `/api/admin/oauth/clients/{id}` | `oauth-client:write` |
| `DELETE` | `/api/admin/oauth/clients/{id}` | `oauth-client:write` |
| `POST` | `/api/admin/oauth/clients/{id}/regenerate-secret` | `oauth-client:write` |
| Method | Path | Permission | Authentication |
|---|---|---|---|
| `GET` | `/api/admin/oauth/clients` | `oauth-client:read` | Cookie only |
| `GET` | `/api/admin/oauth/clients/{id}` | `oauth-client:read` | Cookie only |
| `POST` | `/api/admin/oauth/clients` | `oauth-client:write` plus conditional resource permission | Cookie or Management API bearer |
| `PUT` | `/api/admin/oauth/clients/{id}` | `oauth-client:write` | Cookie only |
| `DELETE` | `/api/admin/oauth/clients/{id}` | `oauth-client:write` | Cookie only |
| `POST` | `/api/admin/oauth/clients/{id}/regenerate-secret` | `oauth-client:write` | Cookie only |

## OAuth scopes

Expand Down
Loading
Loading