mirror of
https://github.com/trustgraph-ai/trustgraph.git
synced 2026-06-23 13:48:06 +02:00
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.
173 lines
4.7 KiB
Python
173 lines
4.7 KiB
Python
|
|
from dataclasses import dataclass, field
|
|
|
|
from ..core.topic import queue
|
|
from ..core.primitives import Error
|
|
|
|
############################################################################
|
|
|
|
# IAM service — see docs/tech-specs/iam-protocol.md for the full protocol.
|
|
#
|
|
# Transport: request/response pub/sub, correlated by the `id` message
|
|
# property. Caller is the API gateway only; the IAM service trusts
|
|
# the bus per the enforcement-boundary policy (no per-request auth
|
|
# against the caller).
|
|
|
|
|
|
@dataclass
|
|
class UserInput:
|
|
username: str = ""
|
|
name: str = ""
|
|
email: str = ""
|
|
# Only populated on create-user; never on update-user.
|
|
password: str = ""
|
|
roles: list[str] = field(default_factory=list)
|
|
enabled: bool = True
|
|
must_change_password: bool = False
|
|
|
|
|
|
@dataclass
|
|
class UserRecord:
|
|
id: str = ""
|
|
workspace: str = ""
|
|
username: str = ""
|
|
name: str = ""
|
|
email: str = ""
|
|
roles: list[str] = field(default_factory=list)
|
|
enabled: bool = True
|
|
must_change_password: bool = False
|
|
created: str = ""
|
|
|
|
|
|
@dataclass
|
|
class WorkspaceInput:
|
|
id: str = ""
|
|
name: str = ""
|
|
enabled: bool = True
|
|
|
|
|
|
@dataclass
|
|
class WorkspaceRecord:
|
|
id: str = ""
|
|
name: str = ""
|
|
enabled: bool = True
|
|
created: str = ""
|
|
|
|
|
|
@dataclass
|
|
class ApiKeyInput:
|
|
user_id: str = ""
|
|
name: str = ""
|
|
expires: str = ""
|
|
|
|
|
|
@dataclass
|
|
class ApiKeyRecord:
|
|
id: str = ""
|
|
user_id: str = ""
|
|
name: str = ""
|
|
# First 4 chars of the plaintext token, for operator identification
|
|
# in list-api-keys. Never enough to reconstruct the key.
|
|
prefix: str = ""
|
|
expires: str = ""
|
|
created: str = ""
|
|
last_used: str = ""
|
|
|
|
|
|
@dataclass
|
|
class IamRequest:
|
|
operation: str = ""
|
|
|
|
# Workspace scope. Required on workspace-scoped operations;
|
|
# omitted for system-level ops (workspace CRUD, signing-key
|
|
# ops, bootstrap, resolve-api-key, login).
|
|
workspace: str = ""
|
|
|
|
# Acting user id for audit. Empty for internal-origin and for
|
|
# operations that resolve an identity (login, resolve-api-key).
|
|
actor: str = ""
|
|
|
|
user_id: str = ""
|
|
username: str = ""
|
|
key_id: str = ""
|
|
api_key: str = ""
|
|
|
|
password: str = ""
|
|
new_password: str = ""
|
|
|
|
user: UserInput | None = None
|
|
workspace_record: WorkspaceInput | None = None
|
|
key: ApiKeyInput | None = None
|
|
|
|
# ---- authorise / authorise-many inputs ----
|
|
# Capability string from the vocabulary in capabilities.md.
|
|
capability: str = ""
|
|
# Resource identifier as JSON. See the IAM contract spec for
|
|
# the resource-component vocabulary. An empty dict denotes a
|
|
# system-level resource.
|
|
resource_json: str = ""
|
|
# Operation parameters as JSON. Decision-relevant fields the
|
|
# operation supplied that are not part of the resource address
|
|
# (e.g. workspace association on create-user).
|
|
parameters_json: str = ""
|
|
# For authorise-many: a JSON-serialised list of
|
|
# {"capability": str, "resource": dict, "parameters": dict}.
|
|
authorise_checks: str = ""
|
|
|
|
|
|
@dataclass
|
|
class IamResponse:
|
|
user: UserRecord | None = None
|
|
users: list[UserRecord] = field(default_factory=list)
|
|
|
|
workspace: WorkspaceRecord | None = None
|
|
workspaces: list[WorkspaceRecord] = field(default_factory=list)
|
|
|
|
# create-api-key returns the plaintext once; never populated
|
|
# on any other operation.
|
|
api_key_plaintext: str = ""
|
|
api_key: ApiKeyRecord | None = None
|
|
api_keys: list[ApiKeyRecord] = field(default_factory=list)
|
|
|
|
# login, rotate-signing-key
|
|
jwt: str = ""
|
|
jwt_expires: str = ""
|
|
|
|
# get-signing-key-public
|
|
signing_key_public: str = ""
|
|
|
|
# resolve-api-key
|
|
resolved_user_id: str = ""
|
|
resolved_workspace: str = ""
|
|
resolved_roles: list[str] = field(default_factory=list)
|
|
|
|
# reset-password
|
|
temporary_password: str = ""
|
|
|
|
# bootstrap
|
|
bootstrap_admin_user_id: str = ""
|
|
bootstrap_admin_api_key: str = ""
|
|
|
|
# bootstrap-status — true iff iam-svc is in 'bootstrap' mode with
|
|
# empty tables, i.e. an unconsumed bootstrap call would succeed.
|
|
bootstrap_available: bool = False
|
|
|
|
# ---- authorise / authorise-many outputs ----
|
|
# authorise: the regime's allow / deny verdict.
|
|
decision_allow: bool = False
|
|
# Cache TTL the regime suggests, in seconds. Gateway respects
|
|
# this for both allow and deny decisions; bounded above by
|
|
# gateway-side policy (typically <= 60s).
|
|
decision_ttl_seconds: int = 0
|
|
# authorise-many: a JSON-serialised list of {"allow": bool,
|
|
# "ttl": int} in the same order as the request's
|
|
# authorise_checks.
|
|
decisions_json: str = ""
|
|
|
|
error: Error | None = None
|
|
|
|
|
|
iam_request_queue = queue('iam', cls='request')
|
|
iam_response_queue = queue('iam', cls='response')
|
|
|
|
############################################################################
|