mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-09-24 16:30:27 +08:00
358 lines
11 KiB
Python
358 lines
11 KiB
Python
import logging
|
|
from json import JSONDecodeError
|
|
from typing import (
|
|
AsyncGenerator,
|
|
cast,
|
|
List,
|
|
Optional,
|
|
Type,
|
|
TypeVar,
|
|
)
|
|
|
|
from fastapi import (
|
|
Depends,
|
|
HTTPException,
|
|
Path,
|
|
Query,
|
|
Request,
|
|
Response,
|
|
Security,
|
|
)
|
|
from fastapi.security import (
|
|
APIKeyCookie,
|
|
APIKeyHeader,
|
|
APIKeyQuery,
|
|
)
|
|
from pydantic import BaseModel
|
|
from starlette_context import context as request_context
|
|
|
|
from galaxy.exceptions import AdminRequiredException
|
|
from galaxy.managers.session import GalaxySessionManager
|
|
from galaxy.managers.users import UserManager
|
|
from galaxy.model.base import transaction
|
|
from galaxy.security.idencoding import IdEncodingHelper
|
|
from galaxy.util import unicodify
|
|
from galaxy.web.framework.decorators import require_admin_message
|
|
from galaxy.webapps.base.webapp import create_new_session
|
|
from galaxy.webapps.galaxy.api import (
|
|
depends as framework_depends,
|
|
FrameworkRouter,
|
|
GalaxyASGIRequest,
|
|
GalaxyASGIResponse,
|
|
T,
|
|
UrlBuilder,
|
|
)
|
|
from tool_shed.context import (
|
|
SessionRequestContext,
|
|
SessionRequestContextImpl,
|
|
)
|
|
from tool_shed.structured_app import ToolShedApp
|
|
from tool_shed.webapp import app as tool_shed_app_mod
|
|
from tool_shed.webapp.model import (
|
|
GalaxySession,
|
|
User,
|
|
)
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
def get_app() -> ToolShedApp:
|
|
if tool_shed_app_mod.app is None:
|
|
raise Exception("Failed to initialize the tool shed app correctly for FastAPI")
|
|
return cast(ToolShedApp, tool_shed_app_mod.app)
|
|
|
|
|
|
async def get_app_with_request_session() -> AsyncGenerator[ToolShedApp, None]:
|
|
app = get_app()
|
|
request_id = request_context.data["X-Request-ID"]
|
|
app.model.set_request_id(request_id)
|
|
try:
|
|
yield app
|
|
finally:
|
|
app.model.unset_request_id(request_id)
|
|
|
|
|
|
DependsOnApp = cast(ToolShedApp, Depends(get_app_with_request_session))
|
|
AUTH_COOKIE_NAME = "galaxycommunitysession"
|
|
|
|
api_key_query = APIKeyQuery(name="key", auto_error=False)
|
|
api_key_header = APIKeyHeader(name="x-api-key", auto_error=False)
|
|
api_key_cookie = APIKeyCookie(name=AUTH_COOKIE_NAME, auto_error=False)
|
|
|
|
|
|
def depends(dep_type: Type[T]) -> T:
|
|
return framework_depends(dep_type, get_app=get_app)
|
|
|
|
|
|
def get_api_user(
|
|
user_manager: UserManager = depends(UserManager),
|
|
key: str = Security(api_key_query),
|
|
x_api_key: str = Security(api_key_header),
|
|
) -> Optional[User]:
|
|
api_key = key or x_api_key
|
|
if not api_key:
|
|
return None
|
|
user = user_manager.by_api_key(api_key=api_key)
|
|
return user
|
|
|
|
|
|
def get_session_manager(app: ToolShedApp = DependsOnApp) -> GalaxySessionManager:
|
|
# TODO: find out how to adapt dependency for Galaxy/Report/TS
|
|
return GalaxySessionManager(app.model)
|
|
|
|
|
|
def get_session(
|
|
session_manager=cast(GalaxySessionManager, Depends(get_session_manager)),
|
|
security: IdEncodingHelper = depends(IdEncodingHelper),
|
|
galaxysession: str = Security(api_key_cookie),
|
|
) -> Optional[GalaxySession]:
|
|
if galaxysession:
|
|
session_key = security.decode_guid(galaxysession)
|
|
if session_key:
|
|
return session_manager.get_session_from_session_key(session_key)
|
|
# TODO: What should we do if there is no session? Since this is the API, maybe nothing is the right choice?
|
|
return None
|
|
|
|
|
|
def get_user(
|
|
galaxy_session=cast(Optional[GalaxySession], Depends(get_session)),
|
|
api_user=cast(Optional[User], Depends(get_api_user)),
|
|
) -> Optional[User]:
|
|
if galaxy_session:
|
|
return galaxy_session.user
|
|
return api_user
|
|
|
|
|
|
def get_trans(
|
|
request: Request,
|
|
response: Response,
|
|
app: ToolShedApp = DependsOnApp,
|
|
user=cast(Optional[User], Depends(get_user)),
|
|
galaxy_session=cast(Optional[GalaxySession], Depends(get_session)),
|
|
) -> SessionRequestContext:
|
|
url_builder = UrlBuilder(request)
|
|
galaxy_request = GalaxyASGIRequest(request)
|
|
galaxy_response = GalaxyASGIResponse(response)
|
|
return SessionRequestContextImpl(
|
|
app,
|
|
galaxy_request,
|
|
galaxy_response,
|
|
user=user,
|
|
galaxy_session=galaxy_session,
|
|
url_builder=url_builder,
|
|
)
|
|
|
|
|
|
DependsOnTrans: SessionRequestContext = cast(SessionRequestContext, Depends(get_trans))
|
|
|
|
|
|
def get_admin_user(trans: SessionRequestContext = DependsOnTrans):
|
|
if not trans.user_is_admin:
|
|
raise AdminRequiredException(require_admin_message(trans.app.config, trans.user))
|
|
return trans.user
|
|
|
|
|
|
AdminUserRequired = Depends(get_admin_user)
|
|
|
|
|
|
class Router(FrameworkRouter):
|
|
admin_user_dependency = AdminUserRequired
|
|
|
|
|
|
B = TypeVar("B", bound=BaseModel)
|
|
|
|
|
|
# async def depend_on_either_json_or_form_data(model: Type[T]):
|
|
# return Depends(get_body)
|
|
|
|
|
|
def depend_on_either_json_or_form_data(model: Type[B]) -> B:
|
|
async def get_body(request: Request):
|
|
content_type = request.headers.get("Content-Type")
|
|
if content_type is None:
|
|
raise HTTPException(status_code=400, detail="No Content-Type provided!")
|
|
elif content_type == "application/json":
|
|
try:
|
|
return model(**await request.json())
|
|
except JSONDecodeError:
|
|
raise HTTPException(status_code=400, detail="Invalid JSON data")
|
|
elif content_type == "application/x-www-form-urlencoded" or content_type.startswith("multipart/form-data"):
|
|
try:
|
|
return model(**await request.form())
|
|
except Exception:
|
|
raise HTTPException(status_code=400, detail="Invalid Form data")
|
|
else:
|
|
raise HTTPException(status_code=400, detail="Content-Type not supported!")
|
|
|
|
return Depends(get_body)
|
|
|
|
|
|
UserIdPathParam: str = Path(..., title="User ID", description="The encoded database identifier of the user.")
|
|
|
|
RequiredRepoOwnerParam: str = Query(
|
|
title="owner",
|
|
description="Owner of the target repository.",
|
|
)
|
|
|
|
RequiredRepoNameParam: str = Query(
|
|
title="Name",
|
|
description="Name of the target repository.",
|
|
)
|
|
|
|
RequiredChangesetParam: str = Query(
|
|
title="changeset",
|
|
description="Changeset of the target repository.",
|
|
)
|
|
|
|
RepositoryIdPathParam: str = Path(
|
|
..., title="Repository ID", description="The encoded database identifier of the repository."
|
|
)
|
|
|
|
ChangesetRevisionPathParam: str = Path(
|
|
...,
|
|
title="Change Revision",
|
|
description="The changeset revision corresponding to the target revision of the target repository.",
|
|
)
|
|
|
|
UsernameIdPathParam: str = Path(..., title="Username", description="The target username.")
|
|
|
|
CommitMessageQueryParam: Optional[str] = Query(
|
|
default=None,
|
|
title="Commit Message",
|
|
description="Set commit message as a query parameter.",
|
|
)
|
|
|
|
DownloadableQueryParam: bool = Query(
|
|
default=True,
|
|
title="downloadable_only",
|
|
description="Include only downloadable repositories.",
|
|
)
|
|
|
|
CommitMessage: str = Query(
|
|
None,
|
|
title="Commit message",
|
|
description="A commit message to store with repository update.",
|
|
)
|
|
|
|
RepositoryIndexQueryParam: Optional[str] = Query(
|
|
default=None,
|
|
title="Search Query",
|
|
)
|
|
|
|
ToolsIndexQueryParam: str = Query(
|
|
default=...,
|
|
title="Search Query",
|
|
)
|
|
|
|
RepositorySearchPageQueryParam: int = Query(
|
|
default=1,
|
|
title="Page",
|
|
)
|
|
|
|
RepositorySearchPageSizeQueryParam: int = Query(
|
|
default=10,
|
|
title="Page Size",
|
|
)
|
|
|
|
RepositoryIndexDeletedQueryParam: Optional[bool] = Query(False, title="Deleted?")
|
|
|
|
RepositoryIndexOwnerQueryParam: Optional[str] = Query(None, title="Owner")
|
|
|
|
RepositoryIndexNameQueryParam: Optional[str] = Query(None, title="Name")
|
|
|
|
RepositoryIndexToolIdsQueryParam: Optional[List[str]] = Query(
|
|
None, title="Tool IDs", description="List of tool GUIDs to find the repository for"
|
|
)
|
|
|
|
|
|
OptionalRepositoryOwnerParam: Optional[str] = Query(None, title="Owner")
|
|
OptionalRepositoryNameParam: Optional[str] = Query(None, title="Name")
|
|
RequiredRepositoryChangesetRevisionParam: str = Query(..., title="Changeset Revision")
|
|
OptionalRepositoryIdParam: Optional[str] = Query(None, title="TSR ID")
|
|
OptionalHexlifyParam: Optional[bool] = Query(True, title="Hexlify response")
|
|
|
|
CategoryIdPathParam: str = Path(
|
|
..., title="Category ID", description="The encoded database identifier of the category."
|
|
)
|
|
CategoryRepositoriesInstallableQueryParam: bool = Query(False, title="Installable?")
|
|
CategoryRepositoriesSortKeyQueryParam: str = Query("name", title="Sort Key")
|
|
CategoryRepositoriesSortOrderQueryParam: str = Query("asc", title="Sort Order")
|
|
CategoryRepositoriesPageQueryParam: Optional[int] = Query(None, title="Page")
|
|
|
|
|
|
def ensure_valid_session(trans: SessionRequestContext) -> None:
|
|
"""
|
|
Ensure that a valid Galaxy session exists and is available as
|
|
trans.session (part of initialization)
|
|
"""
|
|
app = trans.app
|
|
mapping = app.model
|
|
session_manager = GalaxySessionManager(mapping)
|
|
sa_session = app.model.context
|
|
request = trans.request
|
|
# Try to load an existing session
|
|
secure_id = request.get_cookie(AUTH_COOKIE_NAME)
|
|
galaxy_session = None
|
|
prev_galaxy_session = None
|
|
user_for_new_session = None
|
|
invalidate_existing_session = False
|
|
# Track whether the session has changed so we can avoid calling flush
|
|
# in the most common case (session exists and is valid).
|
|
galaxy_session_requires_flush = False
|
|
if secure_id:
|
|
session_key: Optional[str] = app.security.decode_guid(secure_id)
|
|
if session_key:
|
|
# We do NOT catch exceptions here, if the database is down the request should fail,
|
|
# and we should not generate a new session.
|
|
galaxy_session = session_manager.get_session_from_session_key(session_key=session_key)
|
|
if not galaxy_session:
|
|
session_key = None
|
|
|
|
if galaxy_session is not None and galaxy_session.user is not None and galaxy_session.user.deleted:
|
|
invalidate_existing_session = True
|
|
log.warning(f"User '{galaxy_session.user.email}' is marked deleted, invalidating session")
|
|
# Do we need to invalidate the session for some reason?
|
|
if invalidate_existing_session:
|
|
assert galaxy_session
|
|
prev_galaxy_session = galaxy_session
|
|
prev_galaxy_session.is_valid = False
|
|
galaxy_session = None
|
|
# No relevant cookies, or couldn't find, or invalid, so create a new session
|
|
if galaxy_session is None:
|
|
galaxy_session = create_new_session(trans, prev_galaxy_session, user_for_new_session)
|
|
galaxy_session_requires_flush = True
|
|
trans.set_galaxy_session(galaxy_session)
|
|
set_auth_cookie(trans, galaxy_session)
|
|
else:
|
|
trans.set_galaxy_session(galaxy_session)
|
|
# Do we need to flush the session?
|
|
if galaxy_session_requires_flush:
|
|
sa_session.add(galaxy_session)
|
|
# FIXME: If prev_session is a proper relation this would not
|
|
# be needed.
|
|
if prev_galaxy_session:
|
|
sa_session.add(prev_galaxy_session)
|
|
with transaction(sa_session):
|
|
sa_session.commit()
|
|
|
|
|
|
def set_auth_cookie(trans: SessionRequestContext, session):
|
|
cookie_name = AUTH_COOKIE_NAME
|
|
set_cookie(trans, trans.app.security.encode_guid(session.session_key), cookie_name)
|
|
|
|
|
|
def set_cookie(trans: SessionRequestContext, value: str, key, path="/", age=90) -> None:
|
|
"""Convenience method for setting a session cookie"""
|
|
# In wsgi we were setting both a max_age and and expires, but
|
|
# all browsers support max_age now.
|
|
domain: Optional[str] = trans.app.config.cookie_domain
|
|
trans.response.set_cookie(
|
|
key,
|
|
unicodify(value),
|
|
path=path,
|
|
max_age=3600 * 24 * age, # 90 days
|
|
httponly=True,
|
|
secure=trans.request.is_secure,
|
|
domain=domain,
|
|
)
|