mirror of
https://github.com/andrewyng/openworker.git
synced 2026-09-01 15:36:08 +00:00
265 lines
9.8 KiB
Python
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"] == ""
|