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.llm import LLM
|
||||||
from openhands.sdk.security.confirmation_policy import AlwaysConfirm
|
from openhands.sdk.security.confirmation_policy import AlwaysConfirm
|
||||||
from openhands.sdk.workspace.remote.async_remote_workspace import AsyncRemoteWorkspace
|
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.default import get_default_agent
|
||||||
from openhands.tools.preset.planning import get_planning_agent
|
from openhands.tools.preset.planning import get_planning_agent
|
||||||
|
|
||||||
_conversation_info_type_adapter = TypeAdapter(list[ConversationInfo | None])
|
_conversation_info_type_adapter = TypeAdapter(list[ConversationInfo | None])
|
||||||
_logger = logging.getLogger(__name__)
|
_logger = logging.getLogger(__name__)
|
||||||
GIT_TOKEN = 'GIT_TOKEN'
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@ -97,6 +97,8 @@ class LiveStatusAppConversationService(AppConversationServiceBase):
|
|||||||
httpx_client: httpx.AsyncClient
|
httpx_client: httpx.AsyncClient
|
||||||
web_url: str | None
|
web_url: str | None
|
||||||
access_token_hard_timeout: timedelta | None
|
access_token_hard_timeout: timedelta | None
|
||||||
|
app_mode: str | None = None
|
||||||
|
keycloak_auth_cookie: str | None = None
|
||||||
|
|
||||||
async def search_app_conversations(
|
async def search_app_conversations(
|
||||||
self,
|
self,
|
||||||
@ -529,6 +531,7 @@ class LiveStatusAppConversationService(AppConversationServiceBase):
|
|||||||
# Set up a secret for the git token
|
# Set up a secret for the git token
|
||||||
secrets = await self.user_context.get_secrets()
|
secrets = await self.user_context.get_secrets()
|
||||||
if git_provider:
|
if git_provider:
|
||||||
|
secret_name = f'{git_provider.name}_TOKEN'
|
||||||
if self.web_url:
|
if self.web_url:
|
||||||
# If there is a web url, then we create an access token to access it.
|
# 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
|
# 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,
|
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',
|
url=self.web_url + '/api/v1/webhooks/secrets',
|
||||||
headers={'X-Access-Token': access_token},
|
headers=headers,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
# If there is no URL specified where the sandbox can access the app server
|
# 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.
|
# on the type, this may eventually expire.
|
||||||
static_token = await self.user_context.get_latest_token(git_provider)
|
static_token = await self.user_context.get_latest_token(git_provider)
|
||||||
if static_token:
|
if static_token:
|
||||||
secrets[GIT_TOKEN] = StaticSecret(value=static_token)
|
secrets[secret_name] = StaticSecret(value=static_token)
|
||||||
|
|
||||||
workspace = LocalWorkspace(working_dir=working_dir)
|
workspace = LocalWorkspace(working_dir=working_dir)
|
||||||
|
|
||||||
@ -841,6 +850,21 @@ class LiveStatusAppConversationServiceInjector(AppConversationServiceInjector):
|
|||||||
if isinstance(sandbox_service, DockerSandboxService):
|
if isinstance(sandbox_service, DockerSandboxService):
|
||||||
web_url = f'http://host.docker.internal:{sandbox_service.host_port}'
|
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(
|
yield LiveStatusAppConversationService(
|
||||||
init_git_in_empty_workspace=self.init_git_in_empty_workspace,
|
init_git_in_empty_workspace=self.init_git_in_empty_workspace,
|
||||||
user_context=user_context,
|
user_context=user_context,
|
||||||
@ -855,4 +879,6 @@ class LiveStatusAppConversationServiceInjector(AppConversationServiceInjector):
|
|||||||
httpx_client=httpx_client,
|
httpx_client=httpx_client,
|
||||||
web_url=web_url,
|
web_url=web_url,
|
||||||
access_token_hard_timeout=access_token_hard_timeout,
|
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
|
import pkgutil
|
||||||
from uuid import UUID
|
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 fastapi.security import APIKeyHeader
|
||||||
from jwt import InvalidTokenError
|
from jwt import InvalidTokenError
|
||||||
|
from pydantic import SecretStr
|
||||||
|
|
||||||
from openhands import tools # type: ignore[attr-defined]
|
from openhands import tools # type: ignore[attr-defined]
|
||||||
from openhands.agent_server.models import ConversationInfo, Success
|
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.sandbox.sandbox_service import SandboxService
|
||||||
from openhands.app_server.services.injector import InjectorState
|
from openhands.app_server.services.injector import InjectorState
|
||||||
from openhands.app_server.services.jwt_service import JwtService
|
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 (
|
from openhands.app_server.user.specifiy_user_context import (
|
||||||
USER_CONTEXT_ATTR,
|
USER_CONTEXT_ATTR,
|
||||||
SpecifyUserContext,
|
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.app_server.user.user_context import UserContext
|
||||||
from openhands.integrations.provider import ProviderType
|
from openhands.integrations.provider import ProviderType
|
||||||
from openhands.sdk import Event
|
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'])
|
router = APIRouter(prefix='/webhooks', tags=['Webhooks'])
|
||||||
sandbox_service_dependency = depends_sandbox_service()
|
sandbox_service_dependency = depends_sandbox_service()
|
||||||
@ -154,23 +160,34 @@ async def on_event(
|
|||||||
async def get_secret(
|
async def get_secret(
|
||||||
access_token: str = Depends(APIKeyHeader(name='X-Access-Token', auto_error=False)),
|
access_token: str = Depends(APIKeyHeader(name='X-Access-Token', auto_error=False)),
|
||||||
jwt_service: JwtService = jwt_dependency,
|
jwt_service: JwtService = jwt_dependency,
|
||||||
) -> str:
|
) -> Response:
|
||||||
"""Given an access token, retrieve a user secret. The access token
|
"""Given an access token, retrieve a user secret. The access token
|
||||||
is limited by user and provider type, and may include a timeout, limiting
|
is limited by user and provider type, and may include a timeout, limiting
|
||||||
the damage in the event that a token is ever leaked"""
|
the damage in the event that a token is ever leaked"""
|
||||||
try:
|
try:
|
||||||
payload = jwt_service.verify_jws_token(access_token)
|
payload = jwt_service.verify_jws_token(access_token)
|
||||||
user_id = payload['user_id']
|
user_id = payload['user_id']
|
||||||
provider_type = ProviderType[payload['provider_type']]
|
provider_type = ProviderType(payload['provider_type'])
|
||||||
user_injector = config.user
|
|
||||||
assert user_injector is not None
|
# Get UserAuth for the user_id
|
||||||
user_context = await user_injector.get_for_user(user_id)
|
if user_id:
|
||||||
secret = None
|
user_auth = await get_user_auth_for_user(user_id)
|
||||||
if user_context:
|
else:
|
||||||
secret = await user_context.get_latest_token(provider_type)
|
# 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:
|
if secret is None:
|
||||||
raise HTTPException(404, 'No such provider')
|
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:
|
except InvalidTokenError:
|
||||||
raise HTTPException(status.HTTP_401_UNAUTHORIZED)
|
raise HTTPException(status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user