mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-04-29 03:00:45 -04:00
Compare commits
34 Commits
github-tok
...
keycloak_n
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3731405e8c | ||
|
|
47ab99807b | ||
|
|
f70a9e3b93 | ||
|
|
d8d93d3616 | ||
|
|
dfe8d84856 | ||
|
|
0167c04271 | ||
|
|
740f17623d | ||
|
|
383eda2032 | ||
|
|
fd30bb878a | ||
|
|
ee38994c6b | ||
|
|
0b908d3fd9 | ||
|
|
30e14239c5 | ||
|
|
6b362b5984 | ||
|
|
07c95d2362 | ||
|
|
657f0b05a3 | ||
|
|
452cd25030 | ||
|
|
df95142a89 | ||
|
|
17670ed481 | ||
|
|
f7d07132c9 | ||
|
|
3391d8df95 | ||
|
|
8bf899a080 | ||
|
|
e400bfaa0a | ||
|
|
7e3a259bd4 | ||
|
|
96575998ec | ||
|
|
295b306526 | ||
|
|
58f514a47e | ||
|
|
d9355f19cb | ||
|
|
28b57b47d4 | ||
|
|
dc06a4bcdb | ||
|
|
741cf0669f | ||
|
|
1c9f0da5ea | ||
|
|
c4a4ddd170 | ||
|
|
5c2fd2d1ae | ||
|
|
929e0ec935 |
@@ -1,5 +1,6 @@
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import tempfile
|
||||
import time
|
||||
from functools import partial
|
||||
@@ -30,6 +31,7 @@ from openhands.core.config import (
|
||||
SandboxConfig,
|
||||
get_parser,
|
||||
)
|
||||
from openhands.core.config.llm_config import LLMConfig
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.core.main import create_runtime
|
||||
from openhands.events.action import CmdRunAction
|
||||
|
||||
@@ -168,17 +168,25 @@ class OpenHands {
|
||||
|
||||
/**
|
||||
* @param code Code provided by GitHub
|
||||
* @param redirectUri Code provided by GitHub
|
||||
* @returns GitHub access token
|
||||
*/
|
||||
static async getGitHubAccessToken(
|
||||
code: string,
|
||||
redirectUri: string,
|
||||
): Promise<GitHubAccessTokenResponse> {
|
||||
const { data } = await openHands.post<GitHubAccessTokenResponse>(
|
||||
const { data } = await openHands.get<GitHubAccessTokenResponse>(
|
||||
"/api/github/callback",
|
||||
{
|
||||
code,
|
||||
params: {
|
||||
code,
|
||||
redirectUri,
|
||||
},
|
||||
},
|
||||
);
|
||||
console.debug(
|
||||
`/api/github/callback response data: ${JSON.stringify(data)}`,
|
||||
);
|
||||
return data;
|
||||
}
|
||||
|
||||
|
||||
@@ -50,5 +50,13 @@ export const useSettings = () => {
|
||||
setGitHubTokenIsSet(!!query.data?.GITHUB_TOKEN_IS_SET);
|
||||
}, [query.data?.GITHUB_TOKEN_IS_SET, query.isFetched]);
|
||||
|
||||
// Return default settings if in SAAS mode and not authenticated
|
||||
if (config?.APP_MODE === "saas" && !githubTokenIsSet) {
|
||||
return {
|
||||
...query,
|
||||
data: DEFAULT_SETTINGS,
|
||||
};
|
||||
}
|
||||
|
||||
return query;
|
||||
};
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
*/
|
||||
export const generateGitHubAuthUrl = (clientId: string, requestUrl: URL) => {
|
||||
const redirectUri = `${requestUrl.origin}/oauth/github/callback`;
|
||||
const scope = "repo,user,workflow,offline_access";
|
||||
return `https://github.com/login/oauth/authorize?client_id=${clientId}&redirect_uri=${encodeURIComponent(redirectUri)}&scope=${encodeURIComponent(scope)}`;
|
||||
const baseUrl = `${requestUrl.origin}`.replace("https://", "").replace("http://", "")
|
||||
const scope = "openid email profile";
|
||||
const force_build = true
|
||||
return `https://auth.${baseUrl}/realms/allhands/protocol/openid-connect/auth?client_id=github&response_type=code&redirect_uri=${encodeURIComponent(redirectUri)}&scope=${encodeURIComponent(scope)}&state=some-state-value&nonce=222`;
|
||||
};
|
||||
|
||||
@@ -18,18 +18,15 @@ class GitHubService:
|
||||
token: SecretStr = SecretStr('')
|
||||
refresh = False
|
||||
|
||||
def __init__(self, user_id: str | None = None, token: SecretStr | None = None):
|
||||
self.user_id = user_id
|
||||
def __init__(self, user_key: str | None, token: SecretStr | None = None):
|
||||
self.user_key = user_key
|
||||
|
||||
if token:
|
||||
self.token = token
|
||||
|
||||
async def _get_github_headers(self) -> dict:
|
||||
"""
|
||||
Retrieve the GH Token from settings store to construct the headers
|
||||
"""
|
||||
|
||||
if self.user_id and not self.token:
|
||||
"""Retrieve the GH Token from settings store to construct the headers."""
|
||||
if self.user_key and not self.token:
|
||||
self.token = await self.get_latest_token()
|
||||
|
||||
return {
|
||||
|
||||
@@ -219,7 +219,7 @@ class Runtime(FileEditRuntimeMixin):
|
||||
try:
|
||||
if isinstance(event, CmdRunAction):
|
||||
if self.github_user_id and '$GITHUB_TOKEN' in event.command:
|
||||
gh_client = GithubServiceImpl(user_id=self.github_user_id)
|
||||
gh_client = GithubServiceImpl(user_key=self.github_user_id)
|
||||
token = await gh_client.get_latest_token()
|
||||
if token:
|
||||
export_cmd = CmdRunAction(
|
||||
|
||||
@@ -405,11 +405,11 @@ class DockerRuntime(ActionExecutionClient):
|
||||
"""Pause the runtime by stopping the container.
|
||||
This is different from container.stop() as it ensures environment variables are properly preserved."""
|
||||
if not self.container:
|
||||
raise RuntimeError("Container not initialized")
|
||||
|
||||
raise RuntimeError('Container not initialized')
|
||||
|
||||
# First, ensure all environment variables are properly persisted in .bashrc
|
||||
# This is already handled by add_env_vars in base.py
|
||||
|
||||
|
||||
# Stop the container
|
||||
self.container.stop()
|
||||
self.log('debug', f'Container {self.container_name} paused')
|
||||
@@ -418,12 +418,12 @@ class DockerRuntime(ActionExecutionClient):
|
||||
"""Resume the runtime by starting the container.
|
||||
This is different from container.start() as it ensures environment variables are properly restored."""
|
||||
if not self.container:
|
||||
raise RuntimeError("Container not initialized")
|
||||
|
||||
raise RuntimeError('Container not initialized')
|
||||
|
||||
# Start the container
|
||||
self.container.start()
|
||||
self.log('debug', f'Container {self.container_name} resumed')
|
||||
|
||||
|
||||
# Wait for the container to be ready
|
||||
self._wait_until_alive()
|
||||
|
||||
|
||||
@@ -13,4 +13,3 @@ function deactivate() {}
|
||||
module.exports = {
|
||||
activate,
|
||||
deactivate
|
||||
}
|
||||
@@ -20,4 +20,4 @@
|
||||
"title": "Hello World from OpenHands"
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,3 +8,7 @@ def get_github_token(request: Request) -> SecretStr | None:
|
||||
|
||||
def get_user_id(request: Request) -> str | None:
|
||||
return getattr(request.state, 'github_user_id', None)
|
||||
|
||||
|
||||
def get_keycloak_token(request: Request) -> str | None:
|
||||
return getattr(request.state, 'keycloak_token', None)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import os
|
||||
from urllib.parse import parse_qs
|
||||
|
||||
import jwt
|
||||
from keycloak import KeycloakOpenID
|
||||
from pydantic import SecretStr
|
||||
from socketio.exceptions import ConnectionRefusedError
|
||||
|
||||
@@ -52,7 +54,20 @@ async def connect(connection_id: str, environ, auth):
|
||||
else config.jwt_secret
|
||||
)
|
||||
decoded = jwt.decode(signed_token, jwt_secret, algorithms=['HS256'])
|
||||
user_id = decoded['github_user_id']
|
||||
access_token = decoded['access_token']
|
||||
|
||||
KEYCLOAK_SERVER_URL = os.getenv('KEYCLOAK_SERVER_URL', '')
|
||||
KEYCLOAK_REALM_NAME = os.getenv('KEYCLOAK_REALM_NAME', '')
|
||||
KEYCLOAK_CLIENT_ID = os.getenv('KEYCLOAK_CLIENT_ID', '')
|
||||
KEYCLOAK_CLIENT_SECRET = os.getenv('KEYCLOAK_CLIENT_SECRET', '')
|
||||
keycloak_openid = KeycloakOpenID(
|
||||
server_url=KEYCLOAK_SERVER_URL,
|
||||
realm_name=KEYCLOAK_REALM_NAME,
|
||||
client_id=KEYCLOAK_CLIENT_ID,
|
||||
client_secret_key=KEYCLOAK_CLIENT_SECRET,
|
||||
)
|
||||
user_info = await keycloak_openid.a_userinfo(access_token)
|
||||
user_id = user_info['github_id']
|
||||
|
||||
logger.info(f'User {user_id} is connecting to conversation {conversation_id}')
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from fastapi.responses import JSONResponse
|
||||
from pydantic import SecretStr
|
||||
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.integrations.github.github_service import GithubServiceImpl
|
||||
from openhands.integrations.github.github_types import (
|
||||
GhAuthenticationError,
|
||||
@@ -9,7 +9,7 @@ from openhands.integrations.github.github_types import (
|
||||
GitHubRepository,
|
||||
GitHubUser,
|
||||
)
|
||||
from openhands.server.auth import get_github_token, get_user_id
|
||||
from openhands.server.auth import get_keycloak_token, get_user_id
|
||||
|
||||
app = APIRouter(prefix='/api/github')
|
||||
|
||||
@@ -21,9 +21,13 @@ async def get_github_repositories(
|
||||
sort: str = 'pushed',
|
||||
installation_id: int | None = None,
|
||||
github_user_id: str | None = Depends(get_user_id),
|
||||
github_user_token: SecretStr | None = Depends(get_github_token),
|
||||
keycloak_token: str | None = Depends(get_keycloak_token),
|
||||
):
|
||||
client = GithubServiceImpl(user_id=github_user_id, token=github_user_token)
|
||||
client = (
|
||||
GithubServiceImpl(keycloak_token)
|
||||
if keycloak_token
|
||||
else GithubServiceImpl(github_user_id)
|
||||
)
|
||||
try:
|
||||
repos: list[GitHubRepository] = await client.get_repositories(
|
||||
page, per_page, sort, installation_id
|
||||
@@ -31,62 +35,94 @@ async def get_github_repositories(
|
||||
return repos
|
||||
|
||||
except GhAuthenticationError as e:
|
||||
logger.error("Couldn't search GitHub repositories")
|
||||
logger.error(e)
|
||||
return JSONResponse(
|
||||
content=str(e),
|
||||
status_code=401,
|
||||
)
|
||||
|
||||
except GHUnknownException as e:
|
||||
logger.error("Couldn't search GitHub repositories")
|
||||
logger.error(e)
|
||||
return JSONResponse(
|
||||
content=str(e),
|
||||
status_code=500,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error("Couldn't get GitHub repositories")
|
||||
logger.error(e)
|
||||
raise
|
||||
|
||||
|
||||
@app.get('/user')
|
||||
async def get_github_user(
|
||||
github_user_id: str | None = Depends(get_user_id),
|
||||
github_user_token: SecretStr | None = Depends(get_github_token),
|
||||
keycloak_token: str | None = Depends(get_keycloak_token),
|
||||
):
|
||||
client = GithubServiceImpl(user_id=github_user_id, token=github_user_token)
|
||||
client = (
|
||||
GithubServiceImpl(keycloak_token)
|
||||
if keycloak_token
|
||||
else GithubServiceImpl(github_user_id)
|
||||
)
|
||||
try:
|
||||
user: GitHubUser = await client.get_user()
|
||||
return user
|
||||
|
||||
except GhAuthenticationError as e:
|
||||
logger.error("Couldn't get GitHub user")
|
||||
logger.error(e)
|
||||
return JSONResponse(
|
||||
content=str(e),
|
||||
status_code=401,
|
||||
)
|
||||
|
||||
except GHUnknownException as e:
|
||||
logger.error("Couldn't get GitHub user")
|
||||
logger.error(e)
|
||||
return JSONResponse(
|
||||
content=str(e),
|
||||
status_code=500,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error("Couldn't get GitHub repositories")
|
||||
logger.error(e)
|
||||
raise
|
||||
|
||||
|
||||
@app.get('/installations')
|
||||
async def get_github_installation_ids(
|
||||
github_user_id: str | None = Depends(get_user_id),
|
||||
github_user_token: SecretStr | None = Depends(get_github_token),
|
||||
keycloak_token: str | None = Depends(get_keycloak_token),
|
||||
):
|
||||
client = GithubServiceImpl(user_id=github_user_id, token=github_user_token)
|
||||
client = (
|
||||
GithubServiceImpl(keycloak_token)
|
||||
if keycloak_token
|
||||
else GithubServiceImpl(github_user_id)
|
||||
)
|
||||
try:
|
||||
installations_ids: list[int] = await client.get_installation_ids()
|
||||
return installations_ids
|
||||
|
||||
except GhAuthenticationError as e:
|
||||
logger.error("Couldn't get GitHub installations")
|
||||
logger.error(e)
|
||||
return JSONResponse(
|
||||
content=str(e),
|
||||
status_code=401,
|
||||
)
|
||||
|
||||
except GHUnknownException as e:
|
||||
logger.error("Couldn't get GitHub installations")
|
||||
logger.error(e)
|
||||
return JSONResponse(
|
||||
content=str(e),
|
||||
status_code=500,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error("Couldn't get GitHub repositories")
|
||||
logger.error(e)
|
||||
raise
|
||||
|
||||
|
||||
@app.get('/search/repositories')
|
||||
@@ -96,9 +132,13 @@ async def search_github_repositories(
|
||||
sort: str = 'stars',
|
||||
order: str = 'desc',
|
||||
github_user_id: str | None = Depends(get_user_id),
|
||||
github_user_token: SecretStr | None = Depends(get_github_token),
|
||||
keycloak_token: str | None = Depends(get_keycloak_token),
|
||||
):
|
||||
client = GithubServiceImpl(user_id=github_user_id, token=github_user_token)
|
||||
client = (
|
||||
GithubServiceImpl(keycloak_token)
|
||||
if keycloak_token
|
||||
else GithubServiceImpl(github_user_id)
|
||||
)
|
||||
try:
|
||||
repos: list[GitHubRepository] = await client.search_repositories(
|
||||
query, per_page, sort, order
|
||||
@@ -106,13 +146,21 @@ async def search_github_repositories(
|
||||
return repos
|
||||
|
||||
except GhAuthenticationError as e:
|
||||
logger.error("Couldn't search GitHub repositories")
|
||||
logger.error(e)
|
||||
return JSONResponse(
|
||||
content=str(e),
|
||||
status_code=401,
|
||||
)
|
||||
|
||||
except GHUnknownException as e:
|
||||
logger.error("Couldn't search GitHub repositories")
|
||||
logger.error(e)
|
||||
return JSONResponse(
|
||||
content=str(e),
|
||||
status_code=500,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error("Couldn't get GitHub repositories")
|
||||
logger.error(e)
|
||||
raise
|
||||
|
||||
@@ -11,7 +11,7 @@ from openhands.events.action.message import MessageAction
|
||||
from openhands.events.stream import EventStreamSubscriber
|
||||
from openhands.integrations.github.github_service import GithubServiceImpl
|
||||
from openhands.runtime import get_runtime_cls
|
||||
from openhands.server.auth import get_github_token, get_user_id
|
||||
from openhands.server.auth import get_keycloak_token, get_user_id
|
||||
from openhands.server.session.conversation_init_data import ConversationInitData
|
||||
from openhands.server.shared import (
|
||||
ConversationStoreImpl,
|
||||
@@ -131,7 +131,8 @@ async def new_conversation(request: Request, data: InitSessionRequest):
|
||||
"""
|
||||
logger.info('Initializing new conversation')
|
||||
user_id = get_user_id(request)
|
||||
gh_client = GithubServiceImpl(user_id=user_id, token=get_github_token(request))
|
||||
access_token = get_keycloak_token(request)
|
||||
gh_client = GithubServiceImpl(user_key=access_token)
|
||||
github_token = await gh_client.get_latest_token()
|
||||
|
||||
selected_repository = data.selected_repository
|
||||
|
||||
@@ -52,7 +52,7 @@ async def store_settings(
|
||||
# We check if the token is valid by getting the user
|
||||
# If the token is invalid, this will raise an exception
|
||||
github = GithubServiceImpl(
|
||||
user_id=None, token=SecretStr(settings.github_token)
|
||||
user_key=None, token=SecretStr(settings.github_token)
|
||||
)
|
||||
await github.get_user()
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ class Settings(BaseModel):
|
||||
if context and context.get('expose_secrets', False):
|
||||
return llm_api_key.get_secret_value()
|
||||
|
||||
return pydantic_encoder(llm_api_key)
|
||||
return pydantic_encoder(llm_api_key) if llm_api_key is not None else None
|
||||
|
||||
@field_serializer('github_token')
|
||||
def github_token_serializer(
|
||||
|
||||
Reference in New Issue
Block a user