132 lines
3.9 KiB
Python
132 lines
3.9 KiB
Python
"""Gateway authorization tests for shell-backed goal quality gates."""
|
|
|
|
from types import SimpleNamespace
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
|
|
import pytest
|
|
|
|
from gateway.config import GatewayConfig, Platform, PlatformConfig
|
|
from gateway.session import SessionSource
|
|
|
|
|
|
def _source(user_id: str = "user-1", *, chat_type: str = "dm") -> SessionSource:
|
|
return SessionSource(
|
|
platform=Platform.DISCORD,
|
|
user_id=user_id,
|
|
chat_id=f"{chat_type}-1",
|
|
chat_type=chat_type,
|
|
)
|
|
|
|
|
|
def _runner(*, admins=(), group_admins=()):
|
|
from gateway.run import GatewayRunner
|
|
|
|
runner = object.__new__(GatewayRunner)
|
|
runner.config = GatewayConfig(
|
|
platforms={
|
|
Platform.DISCORD: PlatformConfig(
|
|
enabled=True,
|
|
extra={
|
|
"allow_admin_from": list(admins),
|
|
"group_allow_admin_from": list(group_admins),
|
|
},
|
|
)
|
|
}
|
|
)
|
|
manager = MagicMock()
|
|
manager.add_gate.return_value = SimpleNamespace(
|
|
command="touch /tmp/host-in-the-shell",
|
|
max_retries=3,
|
|
timeout_seconds=300,
|
|
)
|
|
runner._get_goal_manager_for_event = AsyncMock(
|
|
return_value=(manager, SimpleNamespace(session_id="session-1"))
|
|
)
|
|
return runner, manager
|
|
|
|
|
|
def _event(user_id: str = "user-1", *, chat_type: str = "dm"):
|
|
event = MagicMock()
|
|
event.source = _source(user_id, chat_type=chat_type)
|
|
event.get_command_args.return_value = (
|
|
"gate add touch /tmp/host-in-the-shell"
|
|
)
|
|
return event
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize(
|
|
("admins", "user_id"),
|
|
[
|
|
((), "user-1"),
|
|
(("admin-1",), "user-1"),
|
|
],
|
|
)
|
|
async def test_gateway_gate_add_requires_explicit_admin(admins, user_id):
|
|
"""Allowed chat users must not turn /goal into an unrestricted host shell."""
|
|
from gateway.run import GatewayRunner
|
|
|
|
runner, manager = _runner(admins=admins)
|
|
|
|
result = await GatewayRunner._handle_goal_command(runner, _event(user_id))
|
|
|
|
assert "explicitly configured gateway admin" in result
|
|
manager.add_gate.assert_not_called()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_gateway_explicit_admin_can_add_goal_gate():
|
|
"""The fix preserves the documented quality-gate capability for operators."""
|
|
from gateway.run import GatewayRunner
|
|
|
|
runner, manager = _runner(admins=("admin-1",))
|
|
|
|
result = await GatewayRunner._handle_goal_command(runner, _event("admin-1"))
|
|
|
|
assert "Gate added" in result
|
|
manager.add_gate.assert_called_once_with(
|
|
"touch /tmp/host-in-the-shell"
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_gateway_group_gate_add_uses_group_admin_scope():
|
|
"""DM admin status must not silently grant host-shell access in groups."""
|
|
from gateway.run import GatewayRunner
|
|
|
|
runner, manager = _runner(
|
|
admins=("dm-admin",),
|
|
group_admins=("group-admin",),
|
|
)
|
|
|
|
denied = await GatewayRunner._handle_goal_command(
|
|
runner,
|
|
_event("dm-admin", chat_type="group"),
|
|
)
|
|
allowed = await GatewayRunner._handle_goal_command(
|
|
runner,
|
|
_event("group-admin", chat_type="group"),
|
|
)
|
|
|
|
assert "explicitly configured gateway admin" in denied
|
|
assert "Gate added" in allowed
|
|
manager.add_gate.assert_called_once_with(
|
|
"touch /tmp/host-in-the-shell"
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_gateway_non_admin_can_still_list_goal_gates():
|
|
"""Non-admins retain read-only visibility into the active goal's gates."""
|
|
from gateway.run import GatewayRunner
|
|
|
|
runner, manager = _runner(admins=("admin-1",))
|
|
manager.render_gates.return_value = "- 1. $ scripts/run_tests.sh"
|
|
event = _event("user-1")
|
|
event.get_command_args.return_value = "gate list"
|
|
|
|
result = await GatewayRunner._handle_goal_command(runner, event)
|
|
|
|
assert result == "- 1. $ scripts/run_tests.sh"
|
|
manager.render_gates.assert_called_once_with()
|