mirror of
https://github.com/trustgraph-ai/trustgraph.git
synced 2026-04-30 02:46:23 +02:00
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:
parent
6302eb8c97
commit
9fc1d4527b
15 changed files with 555 additions and 147 deletions
|
|
@ -83,17 +83,16 @@ The four arguments separate concerns:
|
|||
identifier. See *The Resource model* below.
|
||||
- **`parameters`** — operation-specific data that the regime may
|
||||
need to consider beyond the resource identifier. Used when a
|
||||
decision depends on attributes the request supplies — e.g. an
|
||||
admin scoped to one workspace creating a user *with workspace
|
||||
association W*: the resource is the system-level user registry,
|
||||
and W is a parameter the regime checks against the admin's
|
||||
scope.
|
||||
decision depends on attributes the request supplies — e.g.
|
||||
creating a user *with workspace association W*: the resource is
|
||||
the system-level user registry, and W is a parameter the regime
|
||||
checks against the caller's permissions for `users:write`.
|
||||
|
||||
Different regimes use the four arguments differently — the OSS
|
||||
regime checks role bundles against the capability and the role's
|
||||
workspace scope against parameters; an SSO regime might consult an
|
||||
upstream IdP's group memberships; an ABAC regime evaluates a
|
||||
policy with all four as inputs. The contract is unchanged.
|
||||
Different regimes use the four arguments differently — one regime
|
||||
might evaluate role bundles whose grants carry workspace scope;
|
||||
another might consult upstream IdP group memberships; an ABAC
|
||||
regime evaluates a policy with all four as inputs. The contract
|
||||
is unchanged.
|
||||
|
||||
### `authorise_many`
|
||||
|
||||
|
|
@ -129,14 +128,49 @@ most of them) but the operation set the gateway can forward is:
|
|||
`revoke-api-key`, `change-password`, `reset-password`
|
||||
- Workspace management: `create-workspace`, `list-workspaces`,
|
||||
`get-workspace`, `update-workspace`, `disable-workspace`
|
||||
- Session management: `login`
|
||||
- Session management: `login`, `whoami`
|
||||
- Key management: `get-signing-key-public`, `rotate-signing-key`
|
||||
- Bootstrap: `bootstrap`
|
||||
- Bootstrap: `bootstrap`, `bootstrap-status`
|
||||
|
||||
`whoami` is the self-read counterpart to `get-user`: any
|
||||
authenticated caller can read their own identity record without
|
||||
holding a user-management capability. It is the gating-free probe
|
||||
a UI uses to render affordances appropriate to the caller's role.
|
||||
|
||||
`bootstrap-status` is a side-effect-free probe of whether an
|
||||
unconsumed `bootstrap` call would currently succeed. It exists so
|
||||
a first-run UI can decide whether to render setup without invoking
|
||||
the consuming `bootstrap` op. Public — no authentication.
|
||||
|
||||
A regime that does not support one of these (e.g. an SSO regime
|
||||
where users are managed in the IdP) returns a defined "not
|
||||
supported" error; the gateway surfaces it as a 501.
|
||||
|
||||
### Actor injection
|
||||
|
||||
For any management operation forwarded by the gateway after
|
||||
authentication, the gateway injects the authenticated caller's
|
||||
`handle` as an `actor` field on the request. Regimes use `actor`
|
||||
to identify *who is making the request* — distinct from the
|
||||
operation's target (which lives in `user_id` / `key_id` /
|
||||
`workspace_record` / etc.) — for purposes such as:
|
||||
|
||||
- Self-service operations (`whoami`, `change-password`) that
|
||||
resolve "the caller" without taking a target argument.
|
||||
- Audit logging, where the actor is recorded against the change.
|
||||
- Decisions that depend on the resolved resource state. The
|
||||
gateway authorises against the parameters on the request, but it
|
||||
cannot know the resolved resource's actual properties (e.g. the
|
||||
workspace association of a target user) before the regime loads
|
||||
it. When that matters, the regime can re-decide using the
|
||||
actor's permissions and the resolved record — closing a class
|
||||
of cases the gateway-side check can't see.
|
||||
|
||||
Caller-supplied `actor` values on the request body are overwritten
|
||||
by the gateway — the gateway is the only authority for actor
|
||||
identity, and a regime that consults `actor` can rely on it being
|
||||
authentic.
|
||||
|
||||
## The `Identity` surface
|
||||
|
||||
`Identity` is *mostly* opaque. The gateway holds the value as a
|
||||
|
|
@ -327,13 +361,16 @@ contract via:
|
|||
- Credentials are API keys (opaque) or JWTs (Ed25519, locally
|
||||
validated by the gateway against the regime's published public
|
||||
key).
|
||||
- `authorise` reduces to a role-and-workspace-scope check against
|
||||
the role table defined in [`capabilities.md`](capabilities.md).
|
||||
- `authorise` reduces to a lookup against the role bundles in
|
||||
[`capabilities.md`](capabilities.md), with each grant's workspace
|
||||
scope checked against the operation's workspace component.
|
||||
- Identity, user, and workspace records live in Cassandra.
|
||||
|
||||
The OSS regime is deliberately simple — three roles, single
|
||||
home-workspace per user (a regime data-model decision, not a
|
||||
contract assertion), no policy language.
|
||||
The OSS regime is deliberately simple — three roles, a single
|
||||
workspace association per user (a regime data-model decision, not
|
||||
a contract assertion), no policy language. Other regimes can
|
||||
grant the same user different permissions in different workspaces
|
||||
without changing anything outside the regime.
|
||||
|
||||
### Future regimes
|
||||
|
||||
|
|
|
|||
|
|
@ -72,10 +72,16 @@ class IamRequest:
|
|||
# login).
|
||||
workspace: str = ""
|
||||
|
||||
# Acting user id, for audit. Set by the gateway to the
|
||||
# authenticated caller's id on user-initiated operations.
|
||||
# Empty for internal-origin (bootstrap, reconcilers) and for
|
||||
# resolve-api-key / login (no actor yet).
|
||||
# Acting user id. Set by the gateway to the authenticated
|
||||
# caller's identity handle for every authenticated request
|
||||
# (overwrites any caller-supplied value — the gateway is the
|
||||
# only authority for actor identity, so handlers can rely on it
|
||||
# being authentic). Used for audit logging, self-service ops
|
||||
# like ``whoami`` that resolve "the caller", and future actor-
|
||||
# scoped policy checks. Empty for unauthenticated ops
|
||||
# (``login``, ``bootstrap``, ``bootstrap-status``,
|
||||
# ``get-signing-key-public``, ``resolve-api-key``). See the
|
||||
# actor-injection rule in the IAM contract spec.
|
||||
actor: str = ""
|
||||
|
||||
# --- identity selectors ---
|
||||
|
|
@ -135,6 +141,11 @@ class IamResponse:
|
|||
bootstrap_admin_user_id: str = ""
|
||||
bootstrap_admin_api_key: str = ""
|
||||
|
||||
# bootstrap-status: true iff an unconsumed ``bootstrap`` call
|
||||
# would currently succeed. Always emitted by the response
|
||||
# translator (the false case is meaningful for first-run UIs).
|
||||
bootstrap_available: bool = False
|
||||
|
||||
# Present on any failed operation.
|
||||
error: Error | None = None
|
||||
```
|
||||
|
|
@ -201,25 +212,29 @@ class ApiKeyRecord:
|
|||
| Operation | Request fields | Response fields | Notes |
|
||||
|---|---|---|---|
|
||||
| `login` | `username`, `password`, `workspace` (optional) | `jwt`, `jwt_expires` | If `workspace` omitted, IAM resolves to the user's assigned workspace. |
|
||||
| `whoami` | `actor` (gateway-injected) | `user` | Returns the calling user's own record. AUTHENTICATED-only; no `users:read` capability required. |
|
||||
| `resolve-api-key` | `api_key` (plaintext) | `resolved_user_id`, `resolved_workspace`, `resolved_roles` | Gateway-internal. Service returns `auth-failed` for unknown / expired / revoked keys. |
|
||||
| `change-password` | `user_id`, `password` (current), `new_password` | — | Self-service. IAM validates `password` against stored hash. |
|
||||
| `reset-password` | `user_id` | `temporary_password` | Admin-initiated. IAM generates a random password, sets `must_change_password=true` on the user, returns the plaintext once. |
|
||||
| `create-user` | `workspace`, `user` | `user` | Admin-only. `user.password` is hashed and stored; `user.roles` must be subset of known roles. |
|
||||
| `list-users` | `workspace` | `users` | |
|
||||
| `get-user` | `workspace`, `user_id` | `user` | |
|
||||
| `update-user` | `workspace`, `user_id`, `user` | `user` | `password` field on `user` is rejected; use `change-password` / `reset-password`. |
|
||||
| `disable-user` | `workspace`, `user_id` | — | Soft-delete; sets `enabled=false`. Revokes all the user's API keys. |
|
||||
| `reset-password` | `user_id`, `workspace` (optional integrity check) | `temporary_password` | Admin-initiated. IAM generates a random password, sets `must_change_password=true` on the user, returns the plaintext once. |
|
||||
| `create-user` | `workspace`, `user` | `user` | `user.password` is hashed and stored; `user.roles` must be subset of known roles. `workspace` is the new user's home-workspace binding (a required *parameter*, not an address). |
|
||||
| `list-users` | `workspace` (optional filter) | `users` | If `workspace` omitted, returns the deployment-wide list. |
|
||||
| `get-user` | `user_id`, `workspace` (optional integrity check) | `user` | |
|
||||
| `update-user` | `user_id`, `user`, `workspace` (optional integrity check) | `user` | `password` field on `user` is rejected; use `change-password` / `reset-password`. Username is immutable. |
|
||||
| `disable-user` | `user_id`, `workspace` (optional integrity check) | — | Soft-delete; sets `enabled=false`. Revokes all the user's API keys. |
|
||||
| `enable-user` | `user_id`, `workspace` (optional integrity check) | — | Re-enables a previously disabled user; does not restore API keys. |
|
||||
| `delete-user` | `user_id`, `workspace` (optional integrity check) | — | Hard-delete; removes user record, username lookup, and all the user's API keys. |
|
||||
| `create-workspace` | `workspace_record` | `workspace` | System-level. |
|
||||
| `list-workspaces` | — | `workspaces` | System-level. |
|
||||
| `get-workspace` | `workspace_record` (id only) | `workspace` | System-level. |
|
||||
| `update-workspace` | `workspace_record` | `workspace` | System-level. |
|
||||
| `disable-workspace` | `workspace_record` (id only) | — | System-level. Sets `enabled=false`; revokes all workspace API keys; disables all users in the workspace. |
|
||||
| `create-api-key` | `workspace`, `key` | `api_key_plaintext`, `api_key` | Plaintext returned **once**; only hash stored. `key.name` required. |
|
||||
| `list-api-keys` | `workspace`, `user_id` | `api_keys` | |
|
||||
| `revoke-api-key` | `workspace`, `key_id` | — | Deletes the key record. |
|
||||
| `create-api-key` | `key`, `workspace` (optional integrity check) | `api_key_plaintext`, `api_key` | Plaintext returned **once**; only hash stored. `key.name` required. |
|
||||
| `list-api-keys` | `user_id`, `workspace` (optional integrity check) | `api_keys` | |
|
||||
| `revoke-api-key` | `key_id`, `workspace` (optional integrity check) | — | Deletes the key record. |
|
||||
| `get-signing-key-public` | — | `signing_key_public` | Gateway fetches this at startup. |
|
||||
| `rotate-signing-key` | — | — | System-level. Introduces a new signing key; old key continues to validate JWTs for a grace period (implementation-defined, minimum 1h). |
|
||||
| `bootstrap` | — | `bootstrap_admin_user_id`, `bootstrap_admin_api_key` | If IAM tables are empty, creates the initial `default` workspace, an `admin` user, an initial API key, and an initial signing key; returns them once. No-op on subsequent calls (returns empty fields). |
|
||||
| `bootstrap` | — | `bootstrap_admin_user_id`, `bootstrap_admin_api_key` | If IAM tables are empty and the service is in `bootstrap` mode, creates the initial `default` workspace, an `admin` user, an initial API key, and an initial signing key; returns them once. Otherwise returns a masked auth failure. |
|
||||
| `bootstrap-status` | — | `bootstrap_available` | Side-effect-free probe; `true` iff iam-svc is in `bootstrap` mode and tables are empty. Intended for first-run UX. |
|
||||
|
||||
## Error taxonomy
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue