diff --git a/lib/galaxy/config/__init__.py b/lib/galaxy/config/__init__.py index 8fdef452fdc..eee53c0850e 100644 --- a/lib/galaxy/config/__init__.py +++ b/lib/galaxy/config/__init__.py @@ -422,7 +422,7 @@ class CommonConfigurationMixin: def is_admin_user(self, user): """Determine if the provided user is listed in `admin_users`.""" - return user is not None and user.email in self.admin_users_list + return user and (user.email in self.admin_users_list or user.bootstrap_admin_user) @property def sentry_dsn_public(self): diff --git a/lib/galaxy/managers/context.py b/lib/galaxy/managers/context.py index 6c9725ace0e..d927316571e 100644 --- a/lib/galaxy/managers/context.py +++ b/lib/galaxy/managers/context.py @@ -103,13 +103,13 @@ class ProvidesUserContext: """ For transaction-like objects to provide Galaxy convience layer for reasoning about users. - Mixed in class must provide `user`, `api_inherit_admin`, and `app` + Mixed in class must provide `user` and `app` properties. """ @property def anonymous(self): - return self.user is None and not self.api_inherit_admin + return self.user is None def get_current_user_roles(self): user = self.user @@ -121,7 +121,7 @@ class ProvidesUserContext: @property def user_is_admin(self): - return self.api_inherit_admin or self.app.config.is_admin_user(self.user) + return self.app.config.is_admin_user(self.user) @property def user_can_do_run_as(self): @@ -130,7 +130,7 @@ class ProvidesUserContext: return False user_in_run_as_users = self.user and self.user.email in run_as_users # Can do if explicitly in list or master_api_key supplied. - can_do_run_as = user_in_run_as_users or self.api_inherit_admin + can_do_run_as = user_in_run_as_users or self.user.bootstrap_admin_user return can_do_run_as @property diff --git a/lib/galaxy/managers/users.py b/lib/galaxy/managers/users.py index 0a51c99f1f7..0236a353679 100644 --- a/lib/galaxy/managers/users.py +++ b/lib/galaxy/managers/users.py @@ -1,6 +1,7 @@ """ Manager and Serializer for Users. """ +import hashlib import logging import random import socket @@ -14,7 +15,8 @@ from sqlalchemy.orm.exc import NoResultFound from galaxy import ( exceptions, model, - util + schema, + util, ) from galaxy.managers import ( api_keys, @@ -239,6 +241,8 @@ class UserManager(base.ModelManager, deletable.PurgableManagerMixin): try: provided_key = sa_session.query(self.app.model.APIKeys).filter(self.app.model.APIKeys.table.c.key == api_key).one() except NoResultFound: + if self.check_master_api_key(api_key=api_key): + return schema.BoostrapAdminUser() raise exceptions.AuthenticationFailed('Provided API key is not valid.') if provided_key.user.deleted: raise exceptions.AuthenticationFailed('User account is deactivated, please contact an administrator.') @@ -247,6 +251,15 @@ class UserManager(base.ModelManager, deletable.PurgableManagerMixin): raise exceptions.AuthenticationFailed('Provided API key has expired.') return provided_key.user + def check_master_api_key(self, api_key): + master_api_key = getattr(self.app.config, 'master_api_key', None) + if not master_api_key: + return False + # Hash keys to make them the same size, so we can do safe comparison. + master_hash = hashlib.sha256(util.smart_str(master_api_key)).hexdigest() + provided_hash = hashlib.sha256(util.smart_str(api_key)).hexdigest() + return util.safe_str_cmp(master_hash, provided_hash) + # ---- admin def is_admin(self, user, trans=None): """Return True if this user is an admin (or session is authenticated as admin). diff --git a/lib/galaxy/model/__init__.py b/lib/galaxy/model/__init__.py index 862f6ae4b46..edb4ed85961 100644 --- a/lib/galaxy/model/__init__.py +++ b/lib/galaxy/model/__init__.py @@ -356,6 +356,7 @@ class JobLike: class User(Dictifiable, RepresentById): use_pbkdf2 = True + bootstrap_admin_user = False """ Data for a Galaxy user or admin and relations to their histories, credentials, and roles. diff --git a/lib/galaxy/schema/__init__.py b/lib/galaxy/schema/__init__.py index e69de29bb2d..3d4e3131f80 100644 --- a/lib/galaxy/schema/__init__.py +++ b/lib/galaxy/schema/__init__.py @@ -0,0 +1,12 @@ +import typing + +from pydantic import BaseModel + + +class BoostrapAdminUser(BaseModel): + id = 0 + email: typing.Optional[str] = None + bootstrap_admin_user = True + + def all_roles(*args) -> list: + return [] diff --git a/lib/galaxy/web/framework/decorators.py b/lib/galaxy/web/framework/decorators.py index d13a4b7a0e5..24db7570752 100644 --- a/lib/galaxy/web/framework/decorators.py +++ b/lib/galaxy/web/framework/decorators.py @@ -161,7 +161,6 @@ def legacy_expose_api(func, to_json=True, user_required=True): return "Malformed user id ( %s ) specified, unable to decode." % str(kwargs['payload']['run_as']) try: user = trans.sa_session.query(trans.app.model.User).get(decoded_user_id) - trans.api_inherit_admin = trans.user_is_admin trans.set_user(user) except Exception: trans.response.status = 400 @@ -286,7 +285,6 @@ def expose_api(func, to_json=True, user_required=True, user_or_session_required= return __api_error_response(trans, err_code=error_code, err_msg=error_message, status_code=400) try: user = trans.sa_session.query(trans.app.model.User).get(decoded_user_id) - trans.api_inherit_admin = trans.user_is_admin trans.set_user(user) except Exception: error_code = error_codes.USER_INVALID_RUN_AS diff --git a/lib/galaxy/webapps/base/webapp.py b/lib/galaxy/webapps/base/webapp.py index d96b8a26b26..5f8b6ba04b5 100644 --- a/lib/galaxy/webapps/base/webapp.py +++ b/lib/galaxy/webapps/base/webapp.py @@ -1,7 +1,6 @@ """ """ import datetime -import hashlib import inspect import logging import os @@ -33,8 +32,6 @@ from galaxy.managers.users import UserManager from galaxy.util import ( asbool, safe_makedirs, - safe_str_cmp, - smart_str, unicodify ) from galaxy.util.sanitize_html import sanitize_html @@ -204,8 +201,6 @@ class GalaxyWebTransaction(base.DefaultWebTransaction, # that the current history should not be used for parameter values # and such). self.workflow_building_mode = False - # Flag indicating whether this is an API call and the API key user is an administrator - self.api_inherit_admin = False self.__user = None self.galaxy_session = None self.error_message = None @@ -362,9 +357,10 @@ class GalaxyWebTransaction(base.DefaultWebTransaction, def set_user(self, user): """Set the current user.""" if self.galaxy_session: - self.galaxy_session.user = user - self.sa_session.add(self.galaxy_session) - self.sa_session.flush() + if user.bootstrap_admin_user: + self.galaxy_session.user = user + self.sa_session.add(self.galaxy_session) + self.sa_session.flush() self.__user = user user = property(get_user, set_user) @@ -407,12 +403,7 @@ class GalaxyWebTransaction(base.DefaultWebTransaction, api_key = self.request.params.get('key', None) or self.request.headers.get('x-api-key', None) secure_id = self.get_cookie(name=session_cookie) api_key_supplied = self.environ.get('is_api_request', False) and api_key - if api_key_supplied and self._check_master_api_key(api_key): - self.api_inherit_admin = True - log.info("Session authenticated using Galaxy master api key") - self.user = None - self.galaxy_session = None - elif api_key_supplied: + if api_key_supplied: # Sessionless API transaction, we just need to associate a user. try: user = self.user_manager.by_api_key(api_key) @@ -434,15 +425,6 @@ class GalaxyWebTransaction(base.DefaultWebTransaction, self.user = None self.galaxy_session = None - def _check_master_api_key(self, api_key): - master_api_key = getattr(self.app.config, 'master_api_key', None) - if not master_api_key: - return False - # Hash keys to make them the same size, so we can do safe comparison. - master_hash = hashlib.sha256(smart_str(master_api_key)).hexdigest() - provided_hash = hashlib.sha256(smart_str(api_key)).hexdigest() - return safe_str_cmp(master_hash, provided_hash) - def _ensure_valid_session(self, session_cookie, create=True): """ Ensure that a valid Galaxy session exists and is available as diff --git a/lib/galaxy/webapps/galaxy/api/users.py b/lib/galaxy/webapps/galaxy/api/users.py index 8bc5028510d..42246cc6256 100644 --- a/lib/galaxy/webapps/galaxy/api/users.py +++ b/lib/galaxy/webapps/galaxy/api/users.py @@ -222,7 +222,7 @@ class UserAPIController(BaseAPIController, UsesTagsMixin, CreatesApiKeysMixin, B # only allow updating other users if they're admin editing_someone_else = current_user != user_to_update - is_admin = trans.api_inherit_admin or self.user_manager.is_admin(current_user) + is_admin = self.user_manager.is_admin(current_user) if editing_someone_else and not is_admin: raise exceptions.InsufficientPermissionsException('You are not allowed to update that user', id=id) diff --git a/lib/galaxy/work/context.py b/lib/galaxy/work/context.py index cc5d88c6b95..64a4333ab0b 100644 --- a/lib/galaxy/work/context.py +++ b/lib/galaxy/work/context.py @@ -24,7 +24,6 @@ class WorkRequestContext(ProvidesAppContext, ProvidesUserContext, ProvidesHistor self.__user = user self.__user_current_roles = None self.__history = history - self.api_inherit_admin = False self.workflow_building_mode = workflow_building_mode def get_history(self, create=False): diff --git a/lib/tool_shed/webapp/model/__init__.py b/lib/tool_shed/webapp/model/__init__.py index d3ab82470bb..cbfad8f11be 100644 --- a/lib/tool_shed/webapp/model/__init__.py +++ b/lib/tool_shed/webapp/model/__init__.py @@ -33,6 +33,7 @@ class APIKeys: class User(Dictifiable): dict_collection_visible_keys = ['id', 'username'] dict_element_visible_keys = ['id', 'username'] + bootstrap_admin_user = False def __init__(self, email=None, password=None): self.email = email diff --git a/test/unit/unittest_utils/galaxy_mock.py b/test/unit/unittest_utils/galaxy_mock.py index b0513375184..cbcfe489a16 100644 --- a/test/unit/unittest_utils/galaxy_mock.py +++ b/test/unit/unittest_utils/galaxy_mock.py @@ -211,6 +211,7 @@ class MockTrans: self.error_message = None self.anonymous = False self.debug = True + self.user_is_admin = False self.galaxy_session = None self.__user = user