mirror of
https://github.com/trustgraph-ai/trustgraph.git
synced 2026-07-15 00:02:11 +02:00
The gateway no longer holds any policy state — capability sets, role
definitions, workspace scope rules. Per the IAM contract it asks the
regime "may this identity perform this capability on this resource?"
per request. That moves the OSS role-based regime entirely into
iam-svc, which can be replaced (SSO, ABAC, ReBAC) without changing
the gateway, the wire protocol, or backend services.
Contract:
- authenticate(credential) -> Identity (handle, workspace,
principal_id, source). No roles, claims, or policy state surface
to the gateway.
- authorise(identity, capability, resource, parameters) -> (allow,
ttl). Cached per-decision (regime TTL clamped above; fail-closed
on regime errors).
- authorise_many available as a fan-out variant.
Operation registry drives every authorisation decision:
- /api/v1/iam -> IamEndpoint, looks up bare op name (create-user,
list-workspaces, ...).
- /api/v1/{kind} -> RegistryRoutedVariableEndpoint, <kind>:<op>
(config:get, flow:list-blueprints, librarian:add-document, ...).
- /api/v1/flow/{flow}/service/{kind} -> flow-service:<kind>.
- /api/v1/flow/{flow}/{import,export}/{kind} ->
flow-{import,export}:<kind>.
- WS Mux per-frame -> flow-service:<kind>; closes a gap where
authenticated users could hit any service kind.
85 operations registered across the surface.
JWT carries identity only — sub + workspace. The roles claim is gone;
the gateway never reads policy state from a credential.
The three coarse *_KIND_CAPABILITY maps are removed. The registry is
the only source of truth for the capability + resource shape of an
operation. Tests migrated to the new Identity shape and to
authorise()-mocked auth doubles.
Specs updated: docs/tech-specs/iam-contract.md (Identity surface,
caching, registry-naming conventions), iam.md (JWT shape, gateway
flow, role section reframed as OSS-regime detail), iam-protocol.md
(positioned as one implementation of the contract).
169 lines
4.6 KiB
Python
169 lines
4.6 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 = ""
|
|
|
|
# ---- 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')
|
|
|
|
############################################################################
|