mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-09-30 10:23:14 +00:00
* Lock dashboard login to GitHub org members Add an org-membership gate to the dashboard OAuth callback. After resolving the GitHub login, enforce_org_login_gate(login) checks the existing ALLOWED_GITHUB_ORGS allowlist before issuing a session. - Reuses ALLOWED_GITHUB_ORGS (no new config knob) and is_user_active_org_member (installation-token check, so no extra OAuth scope and private memberships are visible). - Fail-open when unset/blank so existing deployments keep working; fail-closed on API errors. - Gate runs before the session cookie/token is persisted. Adds unit tests and documents the behavior in INSTALLATION.md. * docs: document Organization Members permission required for org login gate
79 lines
2.6 KiB
Python
79 lines
2.6 KiB
Python
"""Tests for the dashboard GitHub-org login gate (ALLOWED_GITHUB_ORGS)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
from fastapi import HTTPException
|
|
|
|
from agent.dashboard import oauth
|
|
|
|
|
|
def _stub_membership(monkeypatch, members: dict[str, set[str]]) -> dict[str, list[tuple[str, str]]]:
|
|
"""Stub is_user_active_org_member; ``members`` maps org -> set of logins."""
|
|
seen: dict[str, list[tuple[str, str]]] = {"calls": []}
|
|
|
|
async def fake_is_user_active_org_member(username: str, org: str) -> bool:
|
|
seen["calls"].append((username, org))
|
|
return username in members.get(org, set())
|
|
|
|
monkeypatch.setattr(oauth, "is_user_active_org_member", fake_is_user_active_org_member)
|
|
return seen
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_gate_noop_when_unset(monkeypatch) -> None:
|
|
monkeypatch.delenv("ALLOWED_GITHUB_ORGS", raising=False)
|
|
seen = _stub_membership(monkeypatch, {})
|
|
|
|
await oauth.enforce_org_login_gate("anyone")
|
|
|
|
assert seen["calls"] == []
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_gate_noop_when_blank(monkeypatch) -> None:
|
|
monkeypatch.setenv("ALLOWED_GITHUB_ORGS", " , ")
|
|
seen = _stub_membership(monkeypatch, {})
|
|
|
|
await oauth.enforce_org_login_gate("anyone")
|
|
|
|
assert seen["calls"] == []
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_gate_allows_member(monkeypatch) -> None:
|
|
monkeypatch.setenv("ALLOWED_GITHUB_ORGS", "langchain-ai")
|
|
_stub_membership(monkeypatch, {"langchain-ai": {"insider"}})
|
|
|
|
await oauth.enforce_org_login_gate("insider")
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_gate_rejects_non_member(monkeypatch) -> None:
|
|
monkeypatch.setenv("ALLOWED_GITHUB_ORGS", "langchain-ai")
|
|
_stub_membership(monkeypatch, {"langchain-ai": {"insider"}})
|
|
|
|
with pytest.raises(HTTPException) as exc:
|
|
await oauth.enforce_org_login_gate("stranger")
|
|
|
|
assert exc.value.status_code == 403
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_gate_allows_member_of_any_configured_org(monkeypatch) -> None:
|
|
monkeypatch.setenv("ALLOWED_GITHUB_ORGS", "langchain-ai, anthropics")
|
|
_stub_membership(monkeypatch, {"anthropics": {"insider"}})
|
|
|
|
await oauth.enforce_org_login_gate("insider")
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_gate_rejects_when_member_of_no_configured_org(monkeypatch) -> None:
|
|
monkeypatch.setenv("ALLOWED_GITHUB_ORGS", "langchain-ai,anthropics")
|
|
seen = _stub_membership(monkeypatch, {"other-org": {"stranger"}})
|
|
|
|
with pytest.raises(HTTPException) as exc:
|
|
await oauth.enforce_org_login_gate("stranger")
|
|
|
|
assert exc.value.status_code == 403
|
|
assert {org for _, org in seen["calls"]} == {"langchain-ai", "anthropics"}
|