open-swe/tests/test_auth_sources.py

169 lines
5.8 KiB
Python
Raw Normal View History

from __future__ import annotations
import asyncio
import pytest
from agent.utils import auth
fix: reliable, safe Slack account-connect prompt + first-login Slack dialog (#1383) * fix: deliver Slack account-link prompt as a visible threaded reply Blocked Slack users got no prompt at all. Prod logs show chat.postEphemeral returns ok, but ephemeral messages are silently dropped in Slack's assistant threads (where Open SWE runs), so the user sees nothing. Post the prompt as a normal threaded reply instead — the same channel the agent uses to reply. * fix: deliver Slack auth-failure prompt as a visible threaded reply leave_failure_comment() tried an ephemeral message first and only fell back to a thread reply on failure. Ephemeral messages succeed (ok) but are dropped in Slack's assistant threads, so the fallback never fired and the user saw no auth-failure prompt. Post the visible threaded reply directly, matching the account-link prompt fix. * fix: prompt blocked Slack users with a generic, token-free dashboard link Addresses the review findings that posting the per-user account-link token / auth URL in a visible thread lets any channel member bind their GitHub account to the triggering user's Slack identity. Drop the per-user signed link entirely. Both the account-link prompt (_post_account_link_prompt) and the runtime auth-failure prompt (leave_failure_comment) now post a plain dashboard settings link (build_settings_url) as a visible threaded reply. The user signs in with GitHub from their own session and connects Slack via verified OIDC on the settings page — no secret in the thread, nothing to hijack, and no DM machinery. * feat: nudge first-time users to connect Slack from the dashboard home Show a Connect Slack banner on the agents landing page whenever Slack OAuth is enabled and the user hasn't linked Slack yet. A first-time user (no Slack mapping) sees it immediately after signing in; it disappears once connected. * feat: prompt first-time users to connect Slack via a dialog Replace the inline Connect Slack card on the agents home with a modal dialog (Base UI). It opens automatically once the mapping query resolves to "not connected" and closes itself once Slack is linked; "Maybe later" dismisses it for the session. No new dependency — uses the design system's Base UI. * copy: frame Slack connect as resolving the user's GitHub account Drop 'act/reply on your behalf' wording across the connect-Slack dialog, the Slack thread prompts (blocked + auth-failure), and the settings description. Connecting Slack lets Open SWE resolve the user's GitHub account when they tag it in Slack.
2026-06-02 20:55:07 -07:00
def test_leave_failure_comment_posts_generic_token_free_slack_notice(
monkeypatch: pytest.MonkeyPatch,
) -> None:
fix: reliable, safe Slack account-connect prompt + first-login Slack dialog (#1383) * fix: deliver Slack account-link prompt as a visible threaded reply Blocked Slack users got no prompt at all. Prod logs show chat.postEphemeral returns ok, but ephemeral messages are silently dropped in Slack's assistant threads (where Open SWE runs), so the user sees nothing. Post the prompt as a normal threaded reply instead — the same channel the agent uses to reply. * fix: deliver Slack auth-failure prompt as a visible threaded reply leave_failure_comment() tried an ephemeral message first and only fell back to a thread reply on failure. Ephemeral messages succeed (ok) but are dropped in Slack's assistant threads, so the fallback never fired and the user saw no auth-failure prompt. Post the visible threaded reply directly, matching the account-link prompt fix. * fix: prompt blocked Slack users with a generic, token-free dashboard link Addresses the review findings that posting the per-user account-link token / auth URL in a visible thread lets any channel member bind their GitHub account to the triggering user's Slack identity. Drop the per-user signed link entirely. Both the account-link prompt (_post_account_link_prompt) and the runtime auth-failure prompt (leave_failure_comment) now post a plain dashboard settings link (build_settings_url) as a visible threaded reply. The user signs in with GitHub from their own session and connects Slack via verified OIDC on the settings page — no secret in the thread, nothing to hijack, and no DM machinery. * feat: nudge first-time users to connect Slack from the dashboard home Show a Connect Slack banner on the agents landing page whenever Slack OAuth is enabled and the user hasn't linked Slack yet. A first-time user (no Slack mapping) sees it immediately after signing in; it disappears once connected. * feat: prompt first-time users to connect Slack via a dialog Replace the inline Connect Slack card on the agents home with a modal dialog (Base UI). It opens automatically once the mapping query resolves to "not connected" and closes itself once Slack is linked; "Maybe later" dismisses it for the session. No new dependency — uses the design system's Base UI. * copy: frame Slack connect as resolving the user's GitHub account Drop 'act/reply on your behalf' wording across the connect-Slack dialog, the Slack thread prompts (blocked + auth-failure), and the settings description. Connecting Slack lets Open SWE resolve the user's GitHub account when they tag it in Slack.
2026-06-02 20:55:07 -07:00
"""Slack auth failures post a generic notice, never the (possibly sensitive) message."""
monkeypatch.setenv("DASHBOARD_BASE_URL", "https://app.example.com")
thread_called: dict[str, str] = {}
async def fake_post_slack_thread_reply(channel_id: str, thread_ts: str, message: str) -> bool:
thread_called["channel_id"] = channel_id
thread_called["thread_ts"] = thread_ts
thread_called["message"] = message
return True
monkeypatch.setattr(auth, "post_slack_thread_reply", fake_post_slack_thread_reply)
monkeypatch.setattr(
auth,
"get_config",
lambda: {
"configurable": {
"slack_thread": {
"channel_id": "C123",
"thread_ts": "1.2",
"triggering_user_id": "U123",
}
}
},
)
fix: reliable, safe Slack account-connect prompt + first-login Slack dialog (#1383) * fix: deliver Slack account-link prompt as a visible threaded reply Blocked Slack users got no prompt at all. Prod logs show chat.postEphemeral returns ok, but ephemeral messages are silently dropped in Slack's assistant threads (where Open SWE runs), so the user sees nothing. Post the prompt as a normal threaded reply instead — the same channel the agent uses to reply. * fix: deliver Slack auth-failure prompt as a visible threaded reply leave_failure_comment() tried an ephemeral message first and only fell back to a thread reply on failure. Ephemeral messages succeed (ok) but are dropped in Slack's assistant threads, so the fallback never fired and the user saw no auth-failure prompt. Post the visible threaded reply directly, matching the account-link prompt fix. * fix: prompt blocked Slack users with a generic, token-free dashboard link Addresses the review findings that posting the per-user account-link token / auth URL in a visible thread lets any channel member bind their GitHub account to the triggering user's Slack identity. Drop the per-user signed link entirely. Both the account-link prompt (_post_account_link_prompt) and the runtime auth-failure prompt (leave_failure_comment) now post a plain dashboard settings link (build_settings_url) as a visible threaded reply. The user signs in with GitHub from their own session and connects Slack via verified OIDC on the settings page — no secret in the thread, nothing to hijack, and no DM machinery. * feat: nudge first-time users to connect Slack from the dashboard home Show a Connect Slack banner on the agents landing page whenever Slack OAuth is enabled and the user hasn't linked Slack yet. A first-time user (no Slack mapping) sees it immediately after signing in; it disappears once connected. * feat: prompt first-time users to connect Slack via a dialog Replace the inline Connect Slack card on the agents home with a modal dialog (Base UI). It opens automatically once the mapping query resolves to "not connected" and closes itself once Slack is linked; "Maybe later" dismisses it for the session. No new dependency — uses the design system's Base UI. * copy: frame Slack connect as resolving the user's GitHub account Drop 'act/reply on your behalf' wording across the connect-Slack dialog, the Slack thread prompts (blocked + auth-failure), and the settings description. Connecting Slack lets Open SWE resolve the user's GitHub account when they tag it in Slack.
2026-06-02 20:55:07 -07:00
# Pass a message that embeds a per-user auth URL; it must NOT be echoed publicly.
asyncio.run(auth.leave_failure_comment("slack", "Click https://auth.example/secret-token"))
fix: reliable, safe Slack account-connect prompt + first-login Slack dialog (#1383) * fix: deliver Slack account-link prompt as a visible threaded reply Blocked Slack users got no prompt at all. Prod logs show chat.postEphemeral returns ok, but ephemeral messages are silently dropped in Slack's assistant threads (where Open SWE runs), so the user sees nothing. Post the prompt as a normal threaded reply instead — the same channel the agent uses to reply. * fix: deliver Slack auth-failure prompt as a visible threaded reply leave_failure_comment() tried an ephemeral message first and only fell back to a thread reply on failure. Ephemeral messages succeed (ok) but are dropped in Slack's assistant threads, so the fallback never fired and the user saw no auth-failure prompt. Post the visible threaded reply directly, matching the account-link prompt fix. * fix: prompt blocked Slack users with a generic, token-free dashboard link Addresses the review findings that posting the per-user account-link token / auth URL in a visible thread lets any channel member bind their GitHub account to the triggering user's Slack identity. Drop the per-user signed link entirely. Both the account-link prompt (_post_account_link_prompt) and the runtime auth-failure prompt (leave_failure_comment) now post a plain dashboard settings link (build_settings_url) as a visible threaded reply. The user signs in with GitHub from their own session and connects Slack via verified OIDC on the settings page — no secret in the thread, nothing to hijack, and no DM machinery. * feat: nudge first-time users to connect Slack from the dashboard home Show a Connect Slack banner on the agents landing page whenever Slack OAuth is enabled and the user hasn't linked Slack yet. A first-time user (no Slack mapping) sees it immediately after signing in; it disappears once connected. * feat: prompt first-time users to connect Slack via a dialog Replace the inline Connect Slack card on the agents home with a modal dialog (Base UI). It opens automatically once the mapping query resolves to "not connected" and closes itself once Slack is linked; "Maybe later" dismisses it for the session. No new dependency — uses the design system's Base UI. * copy: frame Slack connect as resolving the user's GitHub account Drop 'act/reply on your behalf' wording across the connect-Slack dialog, the Slack thread prompts (blocked + auth-failure), and the settings description. Connecting Slack lets Open SWE resolve the user's GitHub account when they tag it in Slack.
2026-06-02 20:55:07 -07:00
assert thread_called["channel_id"] == "C123"
assert thread_called["thread_ts"] == "1.2"
assert "secret-token" not in thread_called["message"]
assert "https://app.example.com/my-settings" in thread_called["message"]
feat: open Slack-triggered PRs as the triggering user (#1375) * feat: open Slack-triggered PRs as the triggering user Route the Slack per-user GitHub token through the dashboard OAuth store (the backend the self-service link prompt populates) and block runs that lack a valid user token, prompting the user to (re-)link. Per-user OAuth now wins over bot-token-only mode for mapped Slack/dashboard users. Flip commit/PR authorship across all sources: the triggering user is the commit author (via repo-local git identity using their resolvable GitHub noreply email) and open-swe[bot] is the Co-authored-by collaborator. * fix: address PR review — shell-escape commit identity, fix token cache impersonation - Shell-escape the triggering user's name/email with shlex.quote before embedding them in the repo-setup `git config` command, so a name like O'Connor (or a crafted one) can't break or inject into the command. - Stop consulting the shared thread-metadata token cache in _resolve_dashboard_user_token. Slack thread ids are shared across the conversation, so a cached token from a prior triggering user could be returned for the current github_login. Always resolve by login from the dashboard OAuth store instead. * feat: dashboard self-service user mapping + UI cleanup - Add session-scoped GET/PUT /dashboard/api/my-mapping so users can set their own work email / Slack member ID (keyed by their GitHub login, source=self). - Slack account-link prompt now redirects to Profile Settings after auth. - Rename "My Settings" -> "Profile Settings" and "Cloud Agents" -> "Open SWE Agent"; remove the Integrations tab/section (folded out, low value for now) and redirect /integrations to Profile Settings. - Add a "User mapping" section to Profile Settings (work email used by Slack and Linear, optional Slack member ID). - Make dashboard auth cookies scheme-aware: Secure;SameSite=None over HTTPS, non-Secure;SameSite=Lax over http://localhost so local login works. * feat: self-service Slack account linking via Sign in with Slack (OIDC) Replace the spoofable manual work-email/Slack-ID form with a verified "Sign in with Slack" flow so a logged-in GitHub user can only ever link their own Slack identity. - New agent/dashboard/slack_oauth.py: OIDC authorize URL, code exchange, userInfo identity parse, optional workspace gate, configured check. - routes.py: session-gated GET /slack/login and /slack/callback that upsert the mapping from Slack-verified user_id + email (source=slack_oauth). Remove the spoofable PUT /my-mapping; expose slack_oauth_enabled on /me. - UI: drop the editable inputs; add a Connect Slack button + status to the User mapping section. Admin-managed mappings are unaffected and still resolve at trigger time.
2026-06-02 15:04:20 -07:00
def _slack_config(github_login: str | None = "mason-gh") -> dict:
configurable: dict = {
"source": "slack",
"user_email": "mason@example.com",
"thread_id": "t1",
}
if github_login is not None:
configurable["github_login"] = github_login
return {"configurable": configurable}
def _stub_dashboard_store(
monkeypatch: pytest.MonkeyPatch,
*,
token: str | None,
expires_at: str | None = "2099-01-01T00:00:00Z",
cached: tuple[str | None, str | None, str | None] = (None, None, None),
) -> None:
from agent.dashboard import profiles
async def fake_get_from_thread(thread_id: str):
return cached
async def fake_get_valid(login: str):
return token
async def fake_get_value(namespace, key):
return {"token_expires_at": expires_at}
async def fake_persist(thread_id: str, tok: str, expires_at: str | None = None):
return "enc"
monkeypatch.setattr(auth, "get_github_token_from_thread", fake_get_from_thread)
monkeypatch.setattr(auth, "persist_encrypted_github_token", fake_persist)
monkeypatch.setattr(profiles, "get_valid_access_token", fake_get_valid)
monkeypatch.setattr(profiles, "_get_value", fake_get_value)
def test_resolve_github_token_slack_uses_dashboard_store(
monkeypatch: pytest.MonkeyPatch,
) -> None:
_stub_dashboard_store(monkeypatch, token="user-tok")
monkeypatch.setattr(auth, "is_bot_token_only_mode", lambda: False)
token, encrypted, expires_at = asyncio.run(auth.resolve_github_token(_slack_config(), "t1"))
assert token == "user-tok"
assert encrypted == "enc"
assert expires_at == "2099-01-01T00:00:00Z"
def test_resolve_github_token_slack_ignores_stale_thread_cache(
monkeypatch: pytest.MonkeyPatch,
) -> None:
# Slack thread ids are shared, so a prior user's cached token must NOT be
# returned. Resolution always goes by github_login via the dashboard store.
_stub_dashboard_store(
monkeypatch,
token="bob-token",
cached=("alice-token", "alice-enc", "2099-01-01T00:00:00Z"),
)
monkeypatch.setattr(auth, "is_bot_token_only_mode", lambda: False)
token, _, _ = asyncio.run(auth.resolve_github_token(_slack_config(), "t1"))
assert token == "bob-token"
def test_resolve_github_token_slack_no_token_raises(
monkeypatch: pytest.MonkeyPatch,
) -> None:
_stub_dashboard_store(monkeypatch, token=None)
monkeypatch.setattr(auth, "is_bot_token_only_mode", lambda: False)
with pytest.raises(auth.GitHubUserAuthRequired):
asyncio.run(auth.resolve_github_token(_slack_config(), "t1"))
def test_resolve_github_token_per_user_wins_over_bot_only_mode(
monkeypatch: pytest.MonkeyPatch,
) -> None:
_stub_dashboard_store(monkeypatch, token="user-tok")
monkeypatch.setattr(auth, "is_bot_token_only_mode", lambda: True)
async def fail_bot(thread_id: str):
raise AssertionError("bot token must not be used when a user token exists")
monkeypatch.setattr(auth, "_resolve_bot_installation_token", fail_bot)
token, _, _ = asyncio.run(auth.resolve_github_token(_slack_config(), "t1"))
assert token == "user-tok"
def test_resolve_github_token_slack_no_token_falls_back_to_bot_in_bot_only_mode(
monkeypatch: pytest.MonkeyPatch,
) -> None:
_stub_dashboard_store(monkeypatch, token=None)
monkeypatch.setattr(auth, "is_bot_token_only_mode", lambda: True)
async def fake_bot(thread_id: str):
return ("bot-tok", "bot-enc", None)
monkeypatch.setattr(auth, "_resolve_bot_installation_token", fake_bot)
token, encrypted, expires_at = asyncio.run(auth.resolve_github_token(_slack_config(), "t1"))
assert (token, encrypted, expires_at) == ("bot-tok", "bot-enc", None)
@pytest.mark.parametrize("source", ["github", "linear"])
def test_resolve_github_token_bot_only_mode_non_slack_uses_bot(
monkeypatch: pytest.MonkeyPatch, source: str
) -> None:
monkeypatch.setattr(auth, "is_bot_token_only_mode", lambda: True)
async def fake_bot(thread_id: str):
return ("bot-tok", "bot-enc", None)
monkeypatch.setattr(auth, "_resolve_bot_installation_token", fake_bot)
config = {"configurable": {"source": source, "github_login": "octo", "thread_id": "t1"}}
token, _, _ = asyncio.run(auth.resolve_github_token(config, "t1"))
assert token == "bot-tok"