iam: self-service ops, optional workspace filters, Mux service routing (#855)

Three threads, all reinforcing the contract's system-level vs.
workspace-association distinction.

WS Mux service routing
- tg-show-flows (and any workspace-level service over the WS) was
  failing with "unknown service" because the post-refactor Mux
  unconditionally looked up flow-service:<kind>.  Now branches on
  the envelope's flow field: with flow → flow-service:<kind>;
  without flow → <kind>:<op> from the inner body; with bare op
  lookup for service=iam.  Resource and parameters come from the
  matched op's own extractors — same path the HTTP endpoints take.

Optional workspace on system-level user/key ops
- list-users returns the deployment-wide list when no workspace is
  supplied, filters when one is.  get-user, update-user,
  disable-user, enable-user, delete-user, reset-password,
  create-api-key, list-api-keys, revoke-api-key all treat workspace
  as an optional integrity check rather than a required argument.
- create-user keeps workspace required — there it's the new user's
  home-workspace binding, a parameter rather than an address.
- API keys reclassified as SYSTEM-level resources.  By the same
  reasoning that makes users system-level, an API key is a
  credential record on a deployment-wide registry; the workspace it
  authenticates to is a property, not a containment.

Self-service surface
- whoami: returns the caller's own user record.  AUTHENTICATED-only;
  no users:read capability required.  Foundation for UI affordances
  that depend on the caller's permissions.
- bootstrap-status: POST /api/v1/auth/bootstrap-status, PUBLIC,
  side-effect-free.  Returns {bootstrap_available: bool} so a
  first-run UI can decide whether to render setup without consuming
  the bootstrap op.
- Gateway now injects actor=identity.handle on every authenticated
  forward to iam-svc (IamEndpoint and WS Mux iam path), overwriting
  any caller-supplied value.  Underpins whoami, audit logging, and
  future regime-side decisions that need actor identity.
- tg-whoami and tg-update-user CLIs.

Spec polish
- iam-contract.md: actor-injection rule documented; whoami /
  bootstrap-status added to operations list; permission-scope
  framing tightened (workspace scope is a property of the grant,
  not the user or role).
- iam.md: self-service section; gateway flow gains the actor-
  injection step; role section reframed so iam-svc constraints
  don't leak into contract-level prose.
- iam-protocol.md: ops table updated for whoami, bootstrap-status,
  optional-workspace pattern; bootstrap_available added to the
  IamResponse listing.
This commit is contained in:
cybermaggedon 2026-04-28 22:13:12 +01:00 committed by GitHub
parent 6302eb8c97
commit 9fc1d4527b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 555 additions and 147 deletions

View file

@ -268,6 +268,26 @@ The gateway forwards this to the IAM service, which validates
credentials and returns a signed JWT. The gateway returns the JWT to
the caller.
#### Self-service: `whoami` and `bootstrap-status`
Two side-effect-free probes that exist to support UI affordances
without giving the caller broad read access:
- `POST /api/v1/iam` with `{"operation": "whoami"}` — authenticated
only. Returns the caller's own user record (id, username, name,
email, workspace, roles, enabled, must_change_password,
created). No `users:read` capability is required, because every
authenticated caller can read themselves. The gateway populates
`actor` on the request from the authenticated identity, so the
regime resolves "the caller" without taking a target argument.
- `POST /api/v1/auth/bootstrap-status` — public, side-effect-free.
Returns `{"bootstrap_available": true|false}`. `true` iff
iam-svc is in `bootstrap` mode and its tables are empty (i.e. an
unconsumed `bootstrap` call would currently succeed). Exists so
a first-run UI can decide whether to render the setup flow
without invoking the consuming `bootstrap` op.
#### IAM service delegation
The gateway stays thin. Its authentication logic is:
@ -387,9 +407,10 @@ workspace; every `authorise` call sees a concrete value.
Whether the resolved workspace is permitted to be operated on by
this caller is an **IAM decision**, not a gateway one. The gateway
calls `authorise(identity, capability, {workspace: ..., ...})` and
relays the answer. In the OSS regime, the answer comes from the
caller's role × workspace-scope — see [`capabilities.md`](capabilities.md).
In other regimes it could come from group mappings, policies,
relays the answer. In the OSS regime, the regime checks whether
the caller's permission grants for `<capability>` include this
workspace — see [`capabilities.md`](capabilities.md). In other
regimes the decision could come from group mappings, policies,
relationship tuples, or anything else the regime models.
### Request anatomy
@ -500,8 +521,19 @@ The OSS regime ships three roles:
| `writer` | All reader capabilities, plus `graph:write`, `documents:write`, `rows:write`, `knowledge:write`, `collections:write`. |
| `admin` | All writer capabilities, plus `config:write`, `flows:write`, `users:read`, `users:write`, `users:admin`, `keys:admin`, `workspaces:admin`, `iam:admin`, `metrics:read`. |
Workspace scope: `reader` and `writer` are active only in the
caller's bound workspace; `admin` is active across all workspaces.
Workspace scope is a property of the *grant*, not of the user or
role. In the OSS regime each capability granted by `reader` /
`writer` is scoped to the workspace the user record is associated
with; capabilities granted by `admin` are scoped to `*` (every
workspace). A user is a system-level object — they don't "live
in" a workspace, they hold permissions whose scope happens to
reference one.
The OSS regime is deliberately limited to one workspace association
per user; future regimes are free to grant the same user different
permissions in different workspaces, or use a non-workspace scope
entirely. This is regime-internal — neither the contract nor the
gateway carries an assumption either way.
The gateway gates each endpoint by *capability*, not by role.
Capabilities are declared per operation in the gateway's operation
@ -647,6 +679,9 @@ For HTTP requests:
error, fail closed (401 / 503 per deployment).
8. Cache the decision per the contract's caching rules (clamped
above by a deployment-set ceiling).
9. For requests forwarded to iam-svc, set `actor` on the body
from `identity.handle`, overwriting any caller-supplied value.
See [`iam-contract.md`](iam-contract.md#actor-injection).
For WebSocket connections: