mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
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:
@@ -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.'
|
||||
),
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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': []})
|
||||
|
||||
|
||||
@@ -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)}
|
||||
|
||||
Reference in New Issue
Block a user