import asyncio from pathlib import Path import httpx2 from gewu.adapters.git_repository import GitCanonicalRepository from gewu.adapters.sqlite_identity import SQLiteIdentityStore from gewu.application.reading import CanonicalNodeReader from gewu.bootstrap.app import create_app def test_admin_credentials_sessions_and_secret_free_audit(tmp_path: Path) -> None: async def exercise() -> None: store = SQLiteIdentityStore(tmp_path / "identity.sqlite3") admin = store.provision(credential_id="admin", principal_id="operator", roles=("admin",)) viewer = store.provision(credential_id="viewer", principal_id="reader", roles=("viewer",)) mixed = store.provision( credential_id="mixed", principal_id="agent", roles=("agent", "admin") ) app = create_app( identity_provider=store, credential_administration=store, node_reader=CanonicalNodeReader(GitCanonicalRepository(tmp_path / "knowledge.git")), ) async with httpx2.AsyncClient( transport=httpx2.ASGITransport(app=app), base_url="http://test" ) as http: assert (await http.get("/v1/admin/credentials")).status_code == 401 for token in (viewer, mixed): headers = {"Authorization": f"Bearer {token}"} assert (await http.get("/v1/admin/credentials", headers=headers)).status_code == 403 assert (await http.get("/v1/admin/events", headers=headers)).status_code == 403 assert ( await http.post( "/v1/admin/credentials", headers=headers, json={ "credential_id": "denied", "principal_id": "denied", "role": "admin", }, ) ).status_code == 403 assert ( await http.delete("/v1/admin/credentials/viewer", headers=headers) ).status_code == 403 session = await http.post("/v1/session", headers={"Authorization": f"Bearer {admin}"}) csrf = {"X-GEWU-CSRF": session.json()["csrf_token"]} body = { "credential_id": "new", "principal_id": "researcher", "role": "agent", "project_ids": ["pilot"], } assert (await http.post("/v1/admin/credentials", json=body)).status_code == 403 assert ( await http.post( "/v1/admin/credentials", json=body, headers={**csrf, "Origin": "https://other"} ) ).status_code == 403 assert ( await http.post("/v1/admin/credentials", headers=csrf, content=b" " * 4097) ).status_code == 413 assert ( await http.post( "/v1/admin/credentials", headers=csrf, json={**body, "project_ids": []} ) ).status_code == 422 created = await http.post("/v1/admin/credentials", headers=csrf, json=body) assert created.status_code == 201 token = created.json()["token"] principal = store.authenticate(token) assert principal and principal.project_ids == frozenset({"pilot"}) assert ( await http.post("/v1/admin/credentials", headers=csrf, json=body) ).status_code == 409 listing = await http.get("/v1/admin/credentials") assert listing.headers["cache-control"] == "private, no-store" assert token not in listing.text and "token_digest" not in listing.text assert ( await http.delete("/v1/admin/credentials/admin", headers=csrf) ).status_code == 409 assert (await http.delete("/v1/admin/credentials/new")).status_code == 403 assert (await http.delete("/v1/admin/credentials/new", headers=csrf)).status_code == 200 assert store.authenticate(token) is None assert (await http.delete("/v1/admin/credentials/new", headers=csrf)).status_code == 404 # Revocation invalidates already-issued browser sessions as well as bearer keys. viewer_session = store.create_browser_session(viewer) assert ( await http.delete("/v1/admin/credentials/viewer", headers=csrf) ).status_code == 200 assert store.authenticate_browser_session(viewer_session.session_token) is None events = await http.get("/v1/admin/events") assert [e["action"] for e in events.json()] == [ "credential.revoked", "credential.revoked", "credential.created", ] assert token not in events.text and admin not in events.text reopened = SQLiteIdentityStore(store.database) assert len(reopened.list_admin_events(limit=50)) == 3 asyncio.run(exercise())