mirror of
https://github.com/trustgraph-ai/trustgraph.git
synced 2026-04-29 18:36:22 +02:00
refactor(iam): pluggable IAM regime via authenticate/authorise contract (#853)
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).
This commit is contained in:
parent
9f2d9adcb1
commit
5e28d3cce0
24 changed files with 2359 additions and 587 deletions
|
|
@ -25,11 +25,11 @@ from trustgraph.gateway.auth import Identity
|
|||
TEST_CAP = "graph:write"
|
||||
|
||||
|
||||
def _valid_identity(roles=("admin",)):
|
||||
def _valid_identity():
|
||||
return Identity(
|
||||
user_id="test-user",
|
||||
handle="test-user",
|
||||
workspace="default",
|
||||
roles=list(roles),
|
||||
principal_id="test-user",
|
||||
source="api-key",
|
||||
)
|
||||
|
||||
|
|
@ -37,11 +37,12 @@ def _valid_identity(roles=("admin",)):
|
|||
@pytest.fixture
|
||||
def mock_auth():
|
||||
"""Mock IAM-backed authenticator. Successful by default —
|
||||
``authenticate`` returns a valid admin identity. Tests that
|
||||
need the auth failure path override the ``authenticate``
|
||||
attribute locally."""
|
||||
``authenticate`` returns a valid identity and ``authorise``
|
||||
allows everything. Tests that need the failure paths override
|
||||
the relevant attribute locally."""
|
||||
auth = MagicMock()
|
||||
auth.authenticate = AsyncMock(return_value=_valid_identity())
|
||||
auth.authorise = AsyncMock(return_value=None)
|
||||
return auth
|
||||
|
||||
|
||||
|
|
@ -135,6 +136,7 @@ async def test_handle_normal_flow():
|
|||
"""Valid bearer → handshake accepted, dispatcher created."""
|
||||
mock_auth = MagicMock()
|
||||
mock_auth.authenticate = AsyncMock(return_value=_valid_identity())
|
||||
mock_auth.authorise = AsyncMock(return_value=None)
|
||||
|
||||
dispatcher_created = False
|
||||
async def mock_dispatcher_factory(ws, running, match_info):
|
||||
|
|
@ -192,6 +194,7 @@ async def test_handle_exception_group_cleanup():
|
|||
"""Test exception group triggers dispatcher cleanup."""
|
||||
mock_auth = MagicMock()
|
||||
mock_auth.authenticate = AsyncMock(return_value=_valid_identity())
|
||||
mock_auth.authorise = AsyncMock(return_value=None)
|
||||
|
||||
mock_dispatcher = AsyncMock()
|
||||
mock_dispatcher.destroy = AsyncMock()
|
||||
|
|
@ -262,6 +265,7 @@ async def test_handle_dispatcher_cleanup_timeout():
|
|||
"""Test dispatcher cleanup with timeout."""
|
||||
mock_auth = MagicMock()
|
||||
mock_auth.authenticate = AsyncMock(return_value=_valid_identity())
|
||||
mock_auth.authorise = AsyncMock(return_value=None)
|
||||
|
||||
# Mock dispatcher that takes long to destroy
|
||||
mock_dispatcher = AsyncMock()
|
||||
|
|
@ -388,6 +392,7 @@ async def test_handle_websocket_already_closed():
|
|||
"""Test handling when websocket is already closed."""
|
||||
mock_auth = MagicMock()
|
||||
mock_auth.authenticate = AsyncMock(return_value=_valid_identity())
|
||||
mock_auth.authorise = AsyncMock(return_value=None)
|
||||
|
||||
mock_dispatcher = AsyncMock()
|
||||
mock_dispatcher.destroy = AsyncMock()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue