deer-flow/backend/tests/test_extension_route_principal.py
Xuehao Xu 4fe64ac916
feat(extensions): add request-scoped run evidence access (#5727)
* feat(extensions): add request-scoped run evidence reader

* fix(extensions): reject padded evidence reader identities
2026-09-23 15:25:45 +08:00

140 lines
5.7 KiB
Python

"""Contributed routers need the caller's identity without importing app.*.
Every extension route is session-authenticated (contributed routers cannot
enter host-reserved or auth-exempt prefixes), but "logged in" and "admin" are
different questions and an extension must be able to ask the second one.
"""
from types import SimpleNamespace
import pytest
from deerflow_extension_api import (
EXTENSION_PRINCIPAL_RESOLVER_KEY,
RUN_EVIDENCE_READER_RESOLVER_KEY,
ExtensionPrincipal,
require_admin,
resolve_principal,
)
ADMIN = ExtensionPrincipal(user_id="u1", is_admin=True, is_internal=False)
PLAIN = ExtensionPrincipal(user_id="u2", is_admin=False, is_internal=False)
def _request(principal):
state = SimpleNamespace(**{EXTENSION_PRINCIPAL_RESOLVER_KEY: (lambda request: principal)})
return SimpleNamespace(app=SimpleNamespace(state=state))
def test_resolve_returns_the_hosts_principal():
assert resolve_principal(_request(ADMIN)) == ADMIN
def test_resolve_returns_none_when_the_host_installed_no_resolver():
request = SimpleNamespace(app=SimpleNamespace(state=SimpleNamespace()))
assert resolve_principal(request) is None
def test_resolve_returns_none_rather_than_raising_when_the_resolver_fails():
def boom(request):
raise RuntimeError("nope")
state = SimpleNamespace(**{EXTENSION_PRINCIPAL_RESOLVER_KEY: boom})
request = SimpleNamespace(app=SimpleNamespace(state=state))
assert resolve_principal(request) is None
def test_require_admin_accepts_an_admin():
assert require_admin(_request(ADMIN)) == ADMIN
def test_require_admin_rejects_a_plain_user():
with pytest.raises(PermissionError):
require_admin(_request(PLAIN))
def test_require_admin_fails_closed_with_no_resolver():
"""An unanswerable authorization question must not resolve to 'allowed'."""
request = SimpleNamespace(app=SimpleNamespace(state=SimpleNamespace()))
with pytest.raises(PermissionError):
require_admin(request)
@pytest.fixture
def _stub_app_config(monkeypatch):
"""Keep ``create_app()`` independent of a real ``config.yaml``.
The repo-root ``config.yaml`` is gitignored; a checkout that configures
plugins there would otherwise make this test load them for real and leak
a populated extension registry into the process-global singleton other
tests read through (see ``tests/test_extension_app_loading.py`` for the
same pattern).
"""
import app.gateway.app as app_module
from deerflow.config.app_config import AppConfig
from deerflow.config.sandbox_config import SandboxConfig
from deerflow.extensions import reset_loaded_extensions, reset_runtime_diagnostics
config = AppConfig(sandbox=SandboxConfig(use="test"))
monkeypatch.setattr(app_module, "get_app_config", lambda: config)
reset_loaded_extensions()
reset_runtime_diagnostics()
yield
reset_runtime_diagnostics()
reset_loaded_extensions()
def test_host_installs_a_resolver_on_app_state(_stub_app_config):
from app.gateway.app import create_app
app = create_app()
assert callable(getattr(app.state, EXTENSION_PRINCIPAL_RESOLVER_KEY, None))
assert callable(getattr(app.state, RUN_EVIDENCE_READER_RESOLVER_KEY, None))
def test_the_installed_resolver_projects_system_role_into_roles(_stub_app_config):
"""The host's only role concept is the single system_role column; the
projection must actually populate ``roles`` from it rather than reading a
"roles" attribute the user model never had (which would always resolve to
an empty tuple, silently breaking the documented contract)."""
from app.gateway.app import create_app
app = create_app()
resolver = getattr(app.state, EXTENSION_PRINCIPAL_RESOLVER_KEY)
admin_request = SimpleNamespace(state=SimpleNamespace(user=SimpleNamespace(id="u1", system_role="admin"), auth_source=None))
assert resolver(admin_request).roles == ("admin",)
plain_request = SimpleNamespace(state=SimpleNamespace(user=SimpleNamespace(id="u2", system_role="user"), auth_source=None))
assert resolver(plain_request).roles == ("user",)
no_role_request = SimpleNamespace(state=SimpleNamespace(user=SimpleNamespace(id="u3", system_role=None), auth_source=None))
assert resolver(no_role_request).roles == ()
def test_the_installed_resolver_suppresses_admin_for_pat_callers(_stub_app_config):
"""P1 regression (#5041 review): an admin-owned PAT must not regain admin
capability through the extension principal projection. Every admin signal
— ``is_admin`` and the ``admin`` role — is suppressed for PAT callers,
matching the documented guarantee that PAT credentials never carry admin
capability."""
from app.gateway.app import create_app
from app.gateway.auth_disabled import AUTH_SOURCE_PAT, AUTH_SOURCE_SESSION
app = create_app()
resolver = getattr(app.state, EXTENSION_PRINCIPAL_RESOLVER_KEY)
pat_request = SimpleNamespace(state=SimpleNamespace(user=SimpleNamespace(id="u1", system_role="admin"), auth_source=AUTH_SOURCE_PAT))
pat_principal = resolver(pat_request)
assert pat_principal.is_admin is False
assert "admin" not in pat_principal.roles
# Control: the same admin over a session cookie still projects admin.
session_request = SimpleNamespace(state=SimpleNamespace(user=SimpleNamespace(id="u1", system_role="admin"), auth_source=AUTH_SOURCE_SESSION))
session_principal = resolver(session_request)
assert session_principal.is_admin is True
assert session_principal.roles == ("admin",)
# A non-admin PAT keeps its plain role: only admin signals are suppressed.
plain_pat_request = SimpleNamespace(state=SimpleNamespace(user=SimpleNamespace(id="u2", system_role="user"), auth_source=AUTH_SOURCE_PAT))
assert resolver(plain_pat_request).roles == ("user",)