feat: add no-auth IAM regime as a drop-in replacement for iam-svc (#933)

Adds `no-auth-svc`, a lightweight IAM service that permits all access
unconditionally — no database, no bootstrap, no signing keys.  Deploy
it in place of `iam-svc` for development, demos, and single-user
setups where authentication overhead is unwanted.

The gateway no longer hard-codes a 401 on missing credentials.
Instead it asks the IAM regime via a new `authenticate-anonymous`
operation whether token-free access is allowed.  This keeps the
gateway regime-agnostic: `iam-svc` rejects anonymous auth (preserving
existing security), while `no-auth-svc` grants it with a configurable
default user and workspace.

Includes a tech spec (docs/tech-specs/no-auth-regime.md) and tests
that pin the safety boundary — malformed tokens never fall through
to the anonymous path, and a contract test ensures the full iam-svc
always rejects `authenticate-anonymous`.
This commit is contained in:
cybermaggedon 2026-05-18 14:10:05 +01:00 committed by GitHub
parent ab83c81d8a
commit da7d10e995
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 876 additions and 32 deletions

View file

@ -0,0 +1,44 @@
"""
Contract test: the full iam-svc MUST reject authenticate-anonymous.
This is a safety pin if someone accidentally adds anonymous access
to the production IAM handler, this test catches it.
"""
import asyncio
from unittest.mock import Mock, AsyncMock
import pytest
from trustgraph.iam.service.iam import IamService
def _make_request(**kwargs):
req = Mock()
for k, v in kwargs.items():
setattr(req, k, v)
return req
class TestIamRejectsAnonymous:
@pytest.fixture
def handler(self):
svc = object.__new__(IamService)
svc.table_store = Mock(spec=[])
svc.bootstrap_mode = "token"
svc.bootstrap_token = "tok"
svc._on_workspace_created = None
svc._on_workspace_deleted = None
svc._signing_key = None
svc._signing_key_lock = asyncio.Lock()
return svc
@pytest.mark.asyncio
async def test_authenticate_anonymous_returns_auth_failed(self, handler):
resp = await handler.handle(
_make_request(operation="authenticate-anonymous")
)
assert resp.error is not None
assert resp.error.type == "auth-failed"
assert "anonymous" in resp.error.message.lower()