mirror of
https://github.com/OpenHands/OpenHands.git
synced 2025-12-26 05:48:36 +08:00
fix(backend): github token not working for v1 conversations (#11814)
This commit is contained in:
parent
c58e2157ea
commit
b532a5e7fe
@ -73,12 +73,12 @@ from openhands.sdk.conversation.secret_source import LookupSecret, StaticSecret
|
||||
from openhands.sdk.llm import LLM
|
||||
from openhands.sdk.security.confirmation_policy import AlwaysConfirm
|
||||
from openhands.sdk.workspace.remote.async_remote_workspace import AsyncRemoteWorkspace
|
||||
from openhands.server.types import AppMode
|
||||
from openhands.tools.preset.default import get_default_agent
|
||||
from openhands.tools.preset.planning import get_planning_agent
|
||||
|
||||
_conversation_info_type_adapter = TypeAdapter(list[ConversationInfo | None])
|
||||
_logger = logging.getLogger(__name__)
|
||||
GIT_TOKEN = 'GIT_TOKEN'
|
||||
|
||||
|
||||
@dataclass
|
||||
@ -97,6 +97,8 @@ class LiveStatusAppConversationService(AppConversationServiceBase):
|
||||
httpx_client: httpx.AsyncClient
|
||||
web_url: str | None
|
||||
access_token_hard_timeout: timedelta | None
|
||||
app_mode: str | None = None
|
||||
keycloak_auth_cookie: str | None = None
|
||||
|
||||
async def search_app_conversations(
|
||||
self,
|
||||
@ -529,6 +531,7 @@ class LiveStatusAppConversationService(AppConversationServiceBase):
|
||||
# Set up a secret for the git token
|
||||
secrets = await self.user_context.get_secrets()
|
||||
if git_provider:
|
||||
secret_name = f'{git_provider.name}_TOKEN'
|
||||
if self.web_url:
|
||||
# If there is a web url, then we create an access token to access it.
|
||||
# For security reasons, we are explicit here - only this user, and
|
||||
@ -540,9 +543,15 @@ class LiveStatusAppConversationService(AppConversationServiceBase):
|
||||
},
|
||||
expires_in=self.access_token_hard_timeout,
|
||||
)
|
||||
secrets[GIT_TOKEN] = LookupSecret(
|
||||
headers = {'X-Access-Token': access_token}
|
||||
|
||||
# Include keycloak_auth cookie in headers if app_mode is SaaS
|
||||
if self.app_mode == 'saas' and self.keycloak_auth_cookie:
|
||||
headers['Cookie'] = f'keycloak_auth={self.keycloak_auth_cookie}'
|
||||
|
||||
secrets[secret_name] = LookupSecret(
|
||||
url=self.web_url + '/api/v1/webhooks/secrets',
|
||||
headers={'X-Access-Token': access_token},
|
||||
headers=headers,
|
||||
)
|
||||
else:
|
||||
# If there is no URL specified where the sandbox can access the app server
|
||||
@ -550,7 +559,7 @@ class LiveStatusAppConversationService(AppConversationServiceBase):
|
||||
# on the type, this may eventually expire.
|
||||
static_token = await self.user_context.get_latest_token(git_provider)
|
||||
if static_token:
|
||||
secrets[GIT_TOKEN] = StaticSecret(value=static_token)
|
||||
secrets[secret_name] = StaticSecret(value=static_token)
|
||||
|
||||
workspace = LocalWorkspace(working_dir=working_dir)
|
||||
|
||||
@ -841,6 +850,21 @@ class LiveStatusAppConversationServiceInjector(AppConversationServiceInjector):
|
||||
if isinstance(sandbox_service, DockerSandboxService):
|
||||
web_url = f'http://host.docker.internal:{sandbox_service.host_port}'
|
||||
|
||||
# Get app_mode and keycloak_auth cookie for SaaS mode
|
||||
app_mode = None
|
||||
keycloak_auth_cookie = None
|
||||
try:
|
||||
from openhands.server.shared import server_config
|
||||
|
||||
app_mode = (
|
||||
server_config.app_mode.value if server_config.app_mode else None
|
||||
)
|
||||
if request and server_config.app_mode == AppMode.SAAS:
|
||||
keycloak_auth_cookie = request.cookies.get('keycloak_auth')
|
||||
except (ImportError, AttributeError):
|
||||
# If server_config is not available (e.g., in tests), continue without it
|
||||
pass
|
||||
|
||||
yield LiveStatusAppConversationService(
|
||||
init_git_in_empty_workspace=self.init_git_in_empty_workspace,
|
||||
user_context=user_context,
|
||||
@ -855,4 +879,6 @@ class LiveStatusAppConversationServiceInjector(AppConversationServiceInjector):
|
||||
httpx_client=httpx_client,
|
||||
web_url=web_url,
|
||||
access_token_hard_timeout=access_token_hard_timeout,
|
||||
app_mode=app_mode,
|
||||
keycloak_auth_cookie=keycloak_auth_cookie,
|
||||
)
|
||||
|
||||
@ -6,9 +6,10 @@ import logging
|
||||
import pkgutil
|
||||
from uuid import UUID
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from fastapi import APIRouter, Depends, HTTPException, Response, status
|
||||
from fastapi.security import APIKeyHeader
|
||||
from jwt import InvalidTokenError
|
||||
from pydantic import SecretStr
|
||||
|
||||
from openhands import tools # type: ignore[attr-defined]
|
||||
from openhands.agent_server.models import ConversationInfo, Success
|
||||
@ -33,6 +34,7 @@ from openhands.app_server.sandbox.sandbox_models import SandboxInfo
|
||||
from openhands.app_server.sandbox.sandbox_service import SandboxService
|
||||
from openhands.app_server.services.injector import InjectorState
|
||||
from openhands.app_server.services.jwt_service import JwtService
|
||||
from openhands.app_server.user.auth_user_context import AuthUserContext
|
||||
from openhands.app_server.user.specifiy_user_context import (
|
||||
USER_CONTEXT_ATTR,
|
||||
SpecifyUserContext,
|
||||
@ -41,6 +43,10 @@ from openhands.app_server.user.specifiy_user_context import (
|
||||
from openhands.app_server.user.user_context import UserContext
|
||||
from openhands.integrations.provider import ProviderType
|
||||
from openhands.sdk import Event
|
||||
from openhands.server.user_auth.default_user_auth import DefaultUserAuth
|
||||
from openhands.server.user_auth.user_auth import (
|
||||
get_for_user as get_user_auth_for_user,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix='/webhooks', tags=['Webhooks'])
|
||||
sandbox_service_dependency = depends_sandbox_service()
|
||||
@ -154,23 +160,34 @@ async def on_event(
|
||||
async def get_secret(
|
||||
access_token: str = Depends(APIKeyHeader(name='X-Access-Token', auto_error=False)),
|
||||
jwt_service: JwtService = jwt_dependency,
|
||||
) -> str:
|
||||
) -> Response:
|
||||
"""Given an access token, retrieve a user secret. The access token
|
||||
is limited by user and provider type, and may include a timeout, limiting
|
||||
the damage in the event that a token is ever leaked"""
|
||||
try:
|
||||
payload = jwt_service.verify_jws_token(access_token)
|
||||
user_id = payload['user_id']
|
||||
provider_type = ProviderType[payload['provider_type']]
|
||||
user_injector = config.user
|
||||
assert user_injector is not None
|
||||
user_context = await user_injector.get_for_user(user_id)
|
||||
secret = None
|
||||
if user_context:
|
||||
secret = await user_context.get_latest_token(provider_type)
|
||||
provider_type = ProviderType(payload['provider_type'])
|
||||
|
||||
# Get UserAuth for the user_id
|
||||
if user_id:
|
||||
user_auth = await get_user_auth_for_user(user_id)
|
||||
else:
|
||||
# OSS mode - use default user auth
|
||||
user_auth = DefaultUserAuth()
|
||||
|
||||
# Create UserContext directly
|
||||
user_context = AuthUserContext(user_auth=user_auth)
|
||||
|
||||
secret = await user_context.get_latest_token(provider_type)
|
||||
if secret is None:
|
||||
raise HTTPException(404, 'No such provider')
|
||||
return secret
|
||||
if isinstance(secret, SecretStr):
|
||||
secret_value = secret.get_secret_value()
|
||||
else:
|
||||
secret_value = secret
|
||||
|
||||
return Response(content=secret_value, media_type='text/plain')
|
||||
except InvalidTokenError:
|
||||
raise HTTPException(status.HTTP_401_UNAUTHORIZED)
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user