Files
openworker/tests/test_team_allowlist.py

265 lines
9.8 KiB
Python

"""Per-workspace allow-list (managed Slack relay, M3.5).
Slack user ids are workspace-scoped, so each connected workspace (`slack:team:*`)
carries its OWN allow-list; a relay event is authorized against ITS team's list alone.
Team-less sources (manual Socket Mode) keep the flat `slack:default` list — these tests
guard both paths so neither regresses the other.
"""
from __future__ import annotations
from fastapi.testclient import TestClient
from coworker.connectors import (
ConnectorSettings,
Gateway,
MessageEvent,
SessionSource,
TeamAuth,
load_settings,
)
from coworker.connectors.config import is_authorized
from coworker.providers import ModelCapabilities, ProviderClient
from coworker.secrets import SecretStore
from coworker.server import create_app
from coworker.server.manager import SessionManager
class ScriptedProvider(ProviderClient):
def complete(self, *, model, messages, tools=None, **settings):
raise AssertionError("no turns expected")
def capabilities(self, model):
return ModelCapabilities()
def _relay_manager(tmp_path, *, teams=("T1",)) -> SessionManager:
m = SessionManager(data_dir=tmp_path / "data", provider=ScriptedProvider())
for t in teams:
m.secrets.put(
f"slack:team:{t}",
{"type": "oauth", "managed": True, "bot_token": f"xoxb-{t}", "team_id": t},
)
m.secrets.put(
"slack:default",
{"type": "oauth", "managed": True, "mode": "relay", "enabled": True},
)
return m
# -- is_authorized ---------------------------------------------------------------
def test_is_authorized_team_scoped():
s = ConnectorSettings(
platform="slack",
allowed_users={"U_FLAT"},
teams={"T1": TeamAuth(allowed_users={"U_OK"}), "T2": TeamAuth()},
)
# authorized only via the event's OWN team's list
assert is_authorized(s, SessionSource("slack", "C1", user_id="U_OK", team_id="T1"))
assert not is_authorized(
s, SessionSource("slack", "C1", user_id="U_OK", team_id="T2")
)
# the flat list never authorizes a team-scoped event
assert not is_authorized(
s, SessionSource("slack", "C1", user_id="U_FLAT", team_id="T1")
)
# unknown team = no install we know of → deny
assert not is_authorized(
s, SessionSource("slack", "C1", user_id="U_OK", team_id="T_UNKNOWN")
)
# per-team allow_all opens only that team
s.teams["T2"].allow_all = True
assert is_authorized(s, SessionSource("slack", "C1", user_id="U_X", team_id="T2"))
assert not is_authorized(
s, SessionSource("slack", "C1", user_id="U_X", team_id="T1")
)
def test_is_authorized_flat_path_unchanged():
# Manual Socket Mode sources carry no team_id → the flat list, exactly as before,
# even when team lists exist alongside.
s = ConnectorSettings(
platform="slack",
allowed_users={"U_FLAT"},
teams={"T1": TeamAuth(allowed_users={"U_OK"}, allow_all=True)},
)
assert is_authorized(s, SessionSource("slack", "C1", user_id="U_FLAT"))
assert not is_authorized(s, SessionSource("slack", "C1", user_id="U_OK"))
s2 = ConnectorSettings(platform="slack", allow_all=True)
assert is_authorized(s2, SessionSource("slack", "C1", user_id="anyone"))
# -- load_settings ---------------------------------------------------------------
def test_load_settings_populates_teams(tmp_path):
secrets = SecretStore(tmp_path / "secrets.json")
secrets.put("slack:default", {"type": "oauth", "mode": "relay", "enabled": True})
secrets.put(
"slack:team:T1",
{"bot_token": "xoxb-1", "allowed_users": ["U_A", "U_B"]},
)
secrets.put("slack:team:T2", {"bot_token": "xoxb-2", "allow_all": True})
settings = load_settings(secrets)
slack = settings["slack"]
assert slack.enabled is True
assert slack.teams["T1"].allowed_users == {"U_A", "U_B"}
assert slack.teams["T1"].allow_all is False
assert slack.teams["T2"].allow_all is True
# -- manager write path ----------------------------------------------------------
def test_set_allowed_with_team_writes_team_profile(tmp_path):
m = _relay_manager(tmp_path, teams=("T1", "T2"))
m.gateway = Gateway(
secrets=m.secrets, settings={"slack": load_settings(m.secrets)["slack"]}
)
out = m.allow_user("slack", "U_NEW", team_id="T1")
assert out["ok"] is True and out["team_id"] == "T1"
assert m.secrets.get("slack:team:T1")["allowed_users"] == ["U_NEW"]
# the sibling team and the flat list are untouched
assert not m.secrets.get("slack:team:T2").get("allowed_users")
assert not m.secrets.get("slack:default").get("allowed_users")
# live gateway reflects it without a restart
assert m.gateway.settings["slack"].teams["T1"].allowed_users == {"U_NEW"}
m.disallow_user("slack", "U_NEW", team_id="T1")
assert m.secrets.get("slack:team:T1")["allowed_users"] == []
assert m.gateway.settings["slack"].teams["T1"].allowed_users == set()
# unknown workspace → error, nothing written
assert m.allow_user("slack", "U_X", team_id="T_NOPE")["ok"] is False
def test_set_allowed_without_team_keeps_flat_behavior(tmp_path):
m = _relay_manager(tmp_path)
assert m.allow_user("slack", "U_FLAT")["allowed_users"] == ["U_FLAT"]
assert m.secrets.get("slack:default")["allowed_users"] == ["U_FLAT"]
assert not m.secrets.get("slack:team:T1").get("allowed_users")
# -- park + resolve --------------------------------------------------------------
async def test_park_carries_team_and_resolve_allows_into_team(tmp_path):
m = _relay_manager(tmp_path, teams=("T1",))
delivered: list[MessageEvent] = []
async def _capture(event: MessageEvent) -> None:
delivered.append(event)
m._dispatch_inbound = _capture
event = MessageEvent(
text="hello from T1",
source=SessionSource(
"slack", "C9", user_id="U_STRANGER", user_name="Zed", team_id="T1"
),
)
await m._park_unauthorized(event)
items = m.parked.list("slack")
assert items[0]["team_id"] == "T1"
out = await m.resolve_unauthorized("slack", items[0]["id"], "allow_deliver")
assert out["ok"] is True
# the allow landed on the WORKSPACE list, not the flat one
assert m.secrets.get("slack:team:T1")["allowed_users"] == ["U_STRANGER"]
assert not m.secrets.get("slack:default").get("allowed_users")
# the replayed event keeps its workspace, so per-team auth re-checks correctly
assert len(delivered) == 1
assert delivered[0].source.team_id == "T1"
assert delivered[0].text == "hello from T1"
async def test_resolve_teamless_parked_uses_flat_list(tmp_path):
# Manual-mode parked items (no team_id) keep resolving into slack:default.
m = _relay_manager(tmp_path)
async def _noop(event) -> None:
pass
m._dispatch_inbound = _noop
await m._park_unauthorized(
MessageEvent(
text="hi",
source=SessionSource("slack", "D1", user_id="U_M", chat_type="dm"),
)
)
item = m.parked.list("slack")[0]
assert item["team_id"] is None
assert (await m.resolve_unauthorized("slack", item["id"], "allow"))["ok"] is True
assert m.secrets.get("slack:default")["allowed_users"] == ["U_M"]
assert not m.secrets.get("slack:team:T1").get("allowed_users")
# -- REST + connector_list surface ------------------------------------------------
def test_rest_allow_with_team_and_workspaces_field(tmp_path):
m = _relay_manager(tmp_path, teams=("T1", "T2"))
client = TestClient(create_app(m))
r = client.post(
"/v1/connectors/slack/allow", json={"user_id": "U_W", "team_id": "T1"}
)
assert r.json()["ok"] is True
slack = next(
c
for c in client.get("/v1/connectors").json()["connectors"]
if c["name"] == "slack"
)
assert slack["connected"] is True and slack["mode"] == "relay"
ws = {w["team_id"]: w for w in slack["workspaces"]}
assert ws["T1"]["allowed_users"] == ["U_W"]
assert ws["T2"]["allowed_users"] == []
r = client.post(
"/v1/connectors/slack/disallow", json={"user_id": "U_W", "team_id": "T1"}
)
assert r.json()["allowed_users"] == []
# -- installer pre-add on managed install (UX-027) --------------------------------
def test_managed_install_preadds_the_installer(tmp_path):
from coworker.connectors.setup import managed_connect_slack_install
s = SecretStore(tmp_path / "secrets.json")
managed_connect_slack_install(
s, {"team_id": "T1", "access_token": "xoxb-t1", "slack_user_id": "U_ME"}
)
assert s.get("slack:team:T1")["allowed_users"] == ["U_ME"]
src = SessionSource("slack", "T1/C1", user_id="U_ME", team_id="T1")
assert is_authorized(load_settings(s)["slack"], src) is True
def test_reinstall_preserves_the_existing_allow_list(tmp_path):
from coworker.connectors.setup import managed_connect_slack_install
s = SecretStore(tmp_path / "secrets.json")
s.put(
"slack:team:T1",
{
"bot_token": "xoxb-old",
"allowed_users": ["U_ANNA", "U_ME"],
"sender_name": "Rohit",
},
)
managed_connect_slack_install(
s, {"team_id": "T1", "access_token": "xoxb-new", "slack_user_id": "U_ME"}
)
profile = s.get("slack:team:T1")
assert profile["allowed_users"] == ["U_ANNA", "U_ME"]
assert profile["bot_token"] == "xoxb-new"
assert profile["sender_name"] == "Rohit"
def test_workspace_listing_carries_installer_identity(tmp_path):
from coworker.connectors.setup import (
_slack_workspaces,
managed_connect_slack_install,
)
s = SecretStore(tmp_path / "secrets.json")
managed_connect_slack_install(
s, {"team_id": "T1", "access_token": "xoxb-t1", "slack_user_id": "U_ME"}
)
(w,) = _slack_workspaces(s)
assert w["installer_user_id"] == "U_ME"
assert w["approval_owner_ids"] == ["U_ME"]
assert w["installer_name"] == ""