"""System-registry MCP tools must enforce the same RBAC as their HTTP twins, and the self-scope tool must stay self-scope. Two distinct claims, because they are different authorisation stories: * ``list_my_systems`` needs NO role — the scope IS the authorisation, and an employee must be able to answer "what am I on the hook for" (the question a leaver needs answered before their last day). * the three admin reads answer "who owns THIS", which is somebody else's accountability, so they require the same ``can_view_system_registry`` roles the HTTP routes do. Any employee can self-mint a PAT, so a tool that only checks active-employee status is an open door. """ from __future__ import annotations import uuid import pytest from app.core.exceptions import ForbiddenError from app.domains.system_registry import constants as const from app.domains.system_registry import service as svc from app.identity.person_models import IdentityPerson from app.mcp import context as _mcp_ctx from app.mcp import tools as _tools pytestmark = pytest.mark.asyncio _ADMIN_CALLS = [ (_tools.list_registered_systems, {}), (_tools.get_registered_system, {"system": "ticard"}), (_tools.find_system_owner, {"host": "clinic.pingcap.com"}), ] @pytest.fixture() def _patch_factory(db_engine, monkeypatch): from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker from app.core import database as _db monkeypatch.setattr( _db, "async_session_factory", async_sessionmaker(db_engine, class_=AsyncSession, expire_on_commit=False), ) def _as_user(user_id): return _mcp_ctx.mcp_user_id.set(user_id) async def _person(db_session) -> IdentityPerson: person = IdentityPerson( id=uuid.uuid4(), work_email=f"m-{uuid.uuid4().hex[:8]}@t.com", display_name="Other", employment_status="active", source_system="successfactors", hris_person_id=f"sf-{uuid.uuid4().hex[:10]}", ) db_session.add(person) await db_session.flush() await db_session.commit() return person async def test_employee_is_refused_on_every_admin_read( employee_user, _patch_factory, ): user, _ = employee_user token = _as_user(user.id) try: for fn, kwargs in _ADMIN_CALLS: with pytest.raises(ForbiddenError): await fn(**kwargs) finally: _mcp_ctx.mcp_user_id.reset(token) async def test_employee_can_read_their_own_systems( employee_user, db_session, _patch_factory, ): user, _ = employee_user stranger = await _person(db_session) mine = await svc.create_system( db_session, name="Mine", slug=f"mine-{uuid.uuid4().hex[:6]}", owner_person_id=user.id, kind=const.KIND_INTERNAL_TOOL, ) theirs = await svc.create_system( db_session, name="Theirs", slug=f"th-{uuid.uuid4().hex[:6]}", owner_person_id=stranger.id, ) token = _as_user(user.id) try: result = await _tools.list_my_systems() finally: _mcp_ctx.mcp_user_id.reset(token) ids = {row["id"] for row in result["items"]} assert str(mine.id) in ids assert str(theirs.id) not in ids assert result["owned"] == 1 async def test_admin_is_admitted_and_resolves_a_slug( admin_user, db_session, _patch_factory, ): system = await svc.create_system( db_session, name="Clinic", slug=f"clinic-{uuid.uuid4().hex[:6]}", hostnames=["clinic-mcp.pingcap.com"], ) user, _ = admin_user token = _as_user(user.id) try: listed = await _tools.list_registered_systems() assert any(r["id"] == str(system.id) for r in listed["items"]) assert "coverage_pct" in listed["summary"] detail = await _tools.get_registered_system(system=system.slug) assert detail["id"] == str(system.id) # The ownership history rides along: "who was responsible in May" # is the question an incident asks. assert any(e["event_type"] == "created" for e in detail["events"]) found = await _tools.find_system_owner( host="https://clinic-mcp.pingcap.com/health", ) assert [c["id"] for c in found["candidates"]] == [str(system.id)] finally: _mcp_ctx.mcp_user_id.reset(token) async def test_auditor_reads_the_registry(auditor_user, _patch_factory): """The security lane triages findings against these systems, so it reads the register even though it cannot maintain it.""" user, _ = auditor_user token = _as_user(user.id) try: result = await _tools.list_registered_systems(ownerless=True) assert "items" in result finally: _mcp_ctx.mcp_user_id.reset(token)