104 lines
5.0 KiB
Python
104 lines
5.0 KiB
Python
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())
|