153 lines
6.3 KiB
Python
153 lines
6.3 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.application.registry import LocalProjectRegistry
|
||
|
|
from gewu.bootstrap.app import create_app
|
||
|
|
from gewu.domain.contracts import AutoAcceptPolicy, ProjectRecord, ProjectStatus
|
||
|
|
|
||
|
|
|
||
|
|
def test_project_registry_lifecycle_is_admin_only_and_durable(tmp_path: Path) -> None:
|
||
|
|
async def exercise() -> None:
|
||
|
|
identities = SQLiteIdentityStore(tmp_path / "identity.sqlite3")
|
||
|
|
admin_token = identities.provision(
|
||
|
|
credential_id="project-admin",
|
||
|
|
principal_id="owner-operator",
|
||
|
|
roles=("admin",),
|
||
|
|
)
|
||
|
|
viewer_token = identities.provision(
|
||
|
|
credential_id="project-viewer",
|
||
|
|
principal_id="research-reader",
|
||
|
|
roles=("viewer",),
|
||
|
|
)
|
||
|
|
mixed_agent_admin_token = identities.provision(
|
||
|
|
credential_id="mixed-agent-admin",
|
||
|
|
principal_id="agent-with-admin-role",
|
||
|
|
roles=("admin", "agent"),
|
||
|
|
)
|
||
|
|
repository = GitCanonicalRepository(tmp_path / "knowledge.git")
|
||
|
|
registry = LocalProjectRegistry(
|
||
|
|
tmp_path / "control" / "registry.json",
|
||
|
|
repository=repository,
|
||
|
|
index_directory=tmp_path / "publishing",
|
||
|
|
authority_lock_path=tmp_path / "workflow" / "locks" / "canonical-writer.lock",
|
||
|
|
)
|
||
|
|
app = create_app(
|
||
|
|
identity_provider=identities,
|
||
|
|
node_reader=CanonicalNodeReader(repository),
|
||
|
|
project_registry=registry,
|
||
|
|
)
|
||
|
|
project = ProjectRecord(
|
||
|
|
project_id="new-research",
|
||
|
|
title="New research",
|
||
|
|
status=ProjectStatus.ACTIVE,
|
||
|
|
description="An administrator-owned research lens.",
|
||
|
|
owners=("owner-operator",),
|
||
|
|
north_star="Investigate a new research area.",
|
||
|
|
key_questions=("What is known?",),
|
||
|
|
autonomy_policy_id="new-policy",
|
||
|
|
)
|
||
|
|
policy = AutoAcceptPolicy(
|
||
|
|
policy_id="new-policy",
|
||
|
|
policy_version=1,
|
||
|
|
project_id="new-research",
|
||
|
|
require_supported_assessment=True,
|
||
|
|
)
|
||
|
|
body = {
|
||
|
|
"project": project.model_dump(mode="json"),
|
||
|
|
"policy": policy.model_dump(mode="json"),
|
||
|
|
}
|
||
|
|
async with httpx2.AsyncClient(
|
||
|
|
transport=httpx2.ASGITransport(app=app), base_url="http://test"
|
||
|
|
) as http:
|
||
|
|
assert (await http.get("/v1/admin/projects")).status_code == 401
|
||
|
|
denied = await http.post(
|
||
|
|
"/v1/admin/projects",
|
||
|
|
json=body,
|
||
|
|
headers={"Authorization": f"Bearer {viewer_token}"},
|
||
|
|
)
|
||
|
|
assert denied.status_code == 403
|
||
|
|
mixed_agent_denied = await http.post(
|
||
|
|
"/v1/admin/projects",
|
||
|
|
json=body,
|
||
|
|
headers={"Authorization": f"Bearer {mixed_agent_admin_token}"},
|
||
|
|
)
|
||
|
|
assert mixed_agent_denied.status_code == 403
|
||
|
|
oversized = await http.post(
|
||
|
|
"/v1/admin/projects",
|
||
|
|
content=b" " * (64 * 1024 + 1),
|
||
|
|
headers={
|
||
|
|
"Authorization": f"Bearer {admin_token}",
|
||
|
|
"Content-Type": "application/json",
|
||
|
|
},
|
||
|
|
)
|
||
|
|
assert oversized.status_code == 413
|
||
|
|
|
||
|
|
headers = {"Authorization": f"Bearer {admin_token}"}
|
||
|
|
diagnostics = await http.get("/v1/diagnostics", headers=headers)
|
||
|
|
assert diagnostics.status_code == 200
|
||
|
|
assert {
|
||
|
|
"admin_list_projects",
|
||
|
|
"project_create",
|
||
|
|
"project_archive",
|
||
|
|
}.issubset(diagnostics.json()["authorized_operations"])
|
||
|
|
created = await http.post("/v1/admin/projects", json=body, headers=headers)
|
||
|
|
assert created.status_code == 201, created.text
|
||
|
|
assert created.json()["project_id"] == "new-research"
|
||
|
|
listing = await http.get("/v1/admin/projects", headers=headers)
|
||
|
|
assert [item["project_id"] for item in listing.json()] == ["new-research"]
|
||
|
|
duplicate = await http.post("/v1/admin/projects", json=body, headers=headers)
|
||
|
|
assert duplicate.status_code == 409
|
||
|
|
archived = await http.post("/v1/admin/projects/new-research/archive", headers=headers)
|
||
|
|
assert archived.status_code == 200
|
||
|
|
assert archived.json()["status"] == "archived"
|
||
|
|
assert registry.records()[0].status == ProjectStatus.ARCHIVED
|
||
|
|
|
||
|
|
session = identities.create_browser_session(admin_token)
|
||
|
|
browser_project = project.model_copy(
|
||
|
|
update={
|
||
|
|
"project_id": "browser-created",
|
||
|
|
"title": "Browser created",
|
||
|
|
"autonomy_policy_id": "browser-policy",
|
||
|
|
}
|
||
|
|
)
|
||
|
|
browser_policy = policy.model_copy(
|
||
|
|
update={"policy_id": "browser-policy", "project_id": "browser-created"}
|
||
|
|
)
|
||
|
|
browser_body = {
|
||
|
|
"project": browser_project.model_dump(mode="json"),
|
||
|
|
"policy": browser_policy.model_dump(mode="json"),
|
||
|
|
}
|
||
|
|
cookie = {"Cookie": f"gewu_session={session.session_token}"}
|
||
|
|
without_csrf = await http.post(
|
||
|
|
"/v1/admin/projects",
|
||
|
|
json=browser_body,
|
||
|
|
headers={**cookie, "Origin": "http://test"},
|
||
|
|
)
|
||
|
|
assert without_csrf.status_code == 403
|
||
|
|
wrong_origin = await http.post(
|
||
|
|
"/v1/admin/projects",
|
||
|
|
json=browser_body,
|
||
|
|
headers={
|
||
|
|
**cookie,
|
||
|
|
"Origin": "https://attacker.example",
|
||
|
|
"X-GEWU-CSRF": session.csrf_token,
|
||
|
|
},
|
||
|
|
)
|
||
|
|
assert wrong_origin.status_code == 403
|
||
|
|
browser_created = await http.post(
|
||
|
|
"/v1/admin/projects",
|
||
|
|
json=browser_body,
|
||
|
|
headers={
|
||
|
|
**cookie,
|
||
|
|
"Origin": "http://test",
|
||
|
|
"X-GEWU-CSRF": session.csrf_token,
|
||
|
|
},
|
||
|
|
)
|
||
|
|
assert browser_created.status_code == 201, browser_created.text
|
||
|
|
|
||
|
|
asyncio.run(exercise())
|