ALL-5659: fix self-hosted Slack OAuth to use configured app credentials (#14163)

Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
aivong-openhands
2026-04-28 13:02:26 -05:00
committed by GitHub
parent 56bf86ee7f
commit 420c8d0aa9
4 changed files with 75 additions and 29 deletions

View File

@@ -26,6 +26,7 @@ class SlackErrorCode(Enum):
PROVIDER_AUTH_FAILED = 'SLACK_ERR_006'
LLM_AUTH_FAILED = 'SLACK_ERR_007'
MISSING_SETTINGS = 'SLACK_ERR_008'
MISSING_SLACK_SCOPES = 'SLACK_ERR_009'
UNEXPECTED_ERROR = 'SLACK_ERR_999'
@@ -98,6 +99,11 @@ _USER_MESSAGES: dict[SlackErrorCode, str] = {
'{username} please re-login into '
f'[OpenHands Cloud]({HOST_URL}) before starting a job.'
),
SlackErrorCode.MISSING_SLACK_SCOPES: (
'⚠️ The Slack app is missing required permissions. '
f'Please ask your workspace admin to re-install the OpenHands Slack App at {HOST_URL}/slack/install '
'to authorize the updated permissions.'
),
SlackErrorCode.UNEXPECTED_ERROR: (
'Uh oh! There was an unexpected error (ref: {code}). Please try again later.'
),

View File

@@ -5,6 +5,7 @@ from uuid import UUID, uuid4
from integrations.models import Message
from integrations.resolver_context import ResolverUserContext
from integrations.resolver_org_router import resolve_org_for_repo
from integrations.slack.slack_errors import SlackError, SlackErrorCode
from integrations.slack.slack_types import (
SlackMessageView,
SlackViewInterface,
@@ -16,6 +17,7 @@ from integrations.utils import (
)
from jinja2 import Environment
from slack_sdk import WebClient
from slack_sdk.errors import SlackApiError
from storage.slack_conversation import SlackConversation
from storage.slack_conversation_store import SlackConversationStore
from storage.slack_team_store import SlackTeamStore
@@ -86,24 +88,34 @@ class SlackNewConversationView(SlackViewInterface):
messages = []
if self.thread_ts:
client = WebClient(token=self.bot_access_token)
result = client.conversations_replies(
channel=self.channel_id,
ts=self.thread_ts,
inclusive=True,
latest=self.message_ts,
limit=CONTEXT_LIMIT, # We can be smarter about getting more context/condensing it even in the future
)
try:
result = client.conversations_replies(
channel=self.channel_id,
ts=self.thread_ts,
inclusive=True,
latest=self.message_ts,
limit=CONTEXT_LIMIT, # We can be smarter about getting more context/condensing it even in the future
)
except SlackApiError as e:
if e.response.get('error') == 'missing_scope':
raise SlackError(SlackErrorCode.MISSING_SLACK_SCOPES) from e
raise
messages = result['messages']
else:
client = WebClient(token=self.bot_access_token)
result = client.conversations_history(
channel=self.channel_id,
inclusive=True,
latest=self.message_ts,
limit=CONTEXT_LIMIT,
)
try:
result = client.conversations_history(
channel=self.channel_id,
inclusive=True,
latest=self.message_ts,
limit=CONTEXT_LIMIT,
)
except SlackApiError as e:
if e.response.get('error') == 'missing_scope':
raise SlackError(SlackErrorCode.MISSING_SLACK_SCOPES) from e
raise
messages = result['messages']
messages.reverse()
@@ -280,13 +292,18 @@ class SlackUpdateExistingConversationView(SlackNewConversationView):
async def _get_instructions(self, jinja_env: Environment) -> tuple[str, str]:
client = WebClient(token=self.bot_access_token)
result = client.conversations_replies(
channel=self.channel_id,
ts=self.message_ts,
inclusive=True,
latest=self.message_ts,
limit=1, # Get exact user message, in future we can be smarter with collecting additional context
)
try:
result = client.conversations_replies(
channel=self.channel_id,
ts=self.message_ts,
inclusive=True,
latest=self.message_ts,
limit=1, # Get exact user message, in future we can be smarter with collecting additional context
)
except SlackApiError as e:
if e.response.get('error') == 'missing_scope':
raise SlackError(SlackErrorCode.MISSING_SLACK_SCOPES) from e
raise
user_message = result['messages'][0]
user_message = self._get_initial_prompt(
@@ -365,7 +382,7 @@ class SlackUpdateExistingConversationView(SlackNewConversationView):
)
# 6. Send the message to the agent server
url = f"{agent_server_url.rstrip('/')}/api/conversations/{UUID(self.conversation_id)}/events"
url = f'{agent_server_url.rstrip("/")}/api/conversations/{UUID(self.conversation_id)}/events'
headers = {'X-Session-API-Key': running_sandbox.session_api_key}
payload = send_message_request.model_dump()

View File

@@ -29,6 +29,7 @@ from server.constants import (
SLACK_WEBHOOKS_ENABLED,
)
from server.logger import logger
from slack_sdk.errors import SlackApiError
from slack_sdk.oauth import AuthorizeUrlGenerator
from slack_sdk.signature import SignatureVerifier
from slack_sdk.web.async_client import AsyncWebClient
@@ -46,7 +47,16 @@ slack_router = APIRouter(prefix='/slack')
# Build https://slack.com/oauth/v2/authorize with sufficient query parameters
authorize_url_generator = AuthorizeUrlGenerator(
client_id=SLACK_CLIENT_ID, scopes=['app_mentions:read', 'chat:write']
client_id=SLACK_CLIENT_ID,
scopes=[
'app_mentions:read',
'chat:write',
'users:read',
'channels:history',
'groups:history',
'mpim:history',
'im:history',
],
)
token_manager = TokenManager()
@@ -232,7 +242,24 @@ async def keycloak_callback(
# Retrieve the display_name from slack
client = AsyncWebClient(token=bot_access_token)
slack_user_info = await client.users_info(user=slack_user_id)
try:
slack_user_info = await client.users_info(user=slack_user_id)
except SlackApiError as e:
if e.response.get('error') == 'missing_scope':
logger.warning(
'slack_missing_scope_during_install',
extra={'slack_user_id': slack_user_id, 'team_id': team_id},
)
return _html_response(
title='Re-installation Required',
description=(
'The Slack app is missing required permissions. '
f'Please <a href="{HOST_URL}/slack/install" style="color:#ecedee;text-decoration:underline;">re-install the OpenHands Slack App</a> '
'to authorize the updated permissions.'
),
status_code=400,
)
raise
slack_display_name = slack_user_info.data['user']['profile']['display_name']
slack_user = SlackUser(
keycloak_user_id=keycloak_user_id,
@@ -366,7 +393,7 @@ async def on_options_load(request: Request, background_tasks: BackgroundTasks):
# Verify this is a block_suggestion payload
if payload.get('type') != 'block_suggestion':
logger.warning(
f"slack_on_options_load: Unexpected payload type: {payload.get('type')}"
f'slack_on_options_load: Unexpected payload type: {payload.get("type")}'
)
return JSONResponse({'options': []})

View File

@@ -12,11 +12,7 @@ export function InstallSlackAppAnchor() {
variant="primary"
className="w-55"
onClick={() =>
window.open(
"https://slack.com/oauth/v2/authorize?client_id=7477886716822.8729519890534&scope=app_mentions:read,channels:history,chat:write,groups:history,im:history,mpim:history,users:read&user_scope=",
"_blank",
"noreferrer noopener",
)
window.open("/slack/install", "_blank", "noreferrer noopener")
}
>
{t(I18nKey.SLACK$INSTALL_APP)}