diff --git a/sirepo/api_auth.py b/sirepo/api_auth.py index bfe1b214ae..b448ca2f33 100644 --- a/sirepo/api_auth.py +++ b/sirepo/api_auth.py @@ -44,7 +44,7 @@ def check_api_call(func): auth.require_email_user() elif expect == a.REQUIRE_ADM: auth.require_adm() - elif expect in (a.ALLOW_VISITOR, a.MANUAL_PERMISSION_CHECK): + elif expect == a.ALLOW_VISITOR: pass elif expect == a.INTERNAL_TEST: if not pkconfig.channel_in_internal_test(): diff --git a/sirepo/api_perm.py b/sirepo/api_perm.py index e31978eaf5..5ef7eeb55a 100644 --- a/sirepo/api_perm.py +++ b/sirepo/api_perm.py @@ -22,8 +22,6 @@ class APIPerm(aenum.Flag): ALLOW_VISITOR = aenum.auto() #: a logged in email user is required but they don't have to have a role for the sim type ALLOW_SIM_TYPELESS_REQUIRE_EMAIL_USER = aenum.auto() - #: Visitor and permissions will be checked manually by API - MANUAL_PERMISSION_CHECK = aenum.auto() #: only users with role adm REQUIRE_ADM = aenum.auto() #: use basic auth authentication (only) diff --git a/sirepo/auth_role_moderation.py b/sirepo/auth_role_moderation.py index 385d17a3c9..250065b5c9 100644 --- a/sirepo/auth_role_moderation.py +++ b/sirepo/auth_role_moderation.py @@ -145,7 +145,7 @@ def raise_control_for_user(uid, role): s = sirepo.auth_db.UserRoleInvite.get_status(uid, role) if s in _ACTIVE: raise sirepo.util.SRException('moderationPending', None) - if s == 'denied': + if s == sirepo.auth_role.ModerationStatus.DENY: sirepo.util.raise_forbidden(f'uid={uid} role={role} already denied') assert s is None, \ f'Unexpected status={s} for uid={uid} and role={role}' diff --git a/sirepo/jupyterhub.py b/sirepo/jupyterhub.py index 81992b0f47..9d42a3dce3 100644 --- a/sirepo/jupyterhub.py +++ b/sirepo/jupyterhub.py @@ -4,15 +4,18 @@ :copyright: Copyright (c) 2020 RadiaSoft LLC. All Rights Reserved. :license: http://www.apache.org/licenses/LICENSE-2.0.html """ +from pykern import pkcompat from pykern.pkcollections import PKDict from pykern.pkdebug import pkdp, pkdexc import jupyterhub.auth import pykern.pkresource +import re import requests import sirepo.server import tornado.web import traitlets +_SIM_TYPE = 'jupyterhublogin' def template_dirs(): return pykern.pkresource.filename('jupyterhub_templates') @@ -32,33 +35,60 @@ def __init__(self, *args, **kwargs): sirepo.server.init() async def authenticate(self, handler, data): - d = self._check_permissions(handler) - if 'username' in d: - return d.username - elif 'uri' in d: - handler.redirect(d.uri) - raise tornado.web.Finish() # returning None means the user is forbidden (403) # https://jupyterhub.readthedocs.io/en/stable/api/auth.html#jupyterhub.auth.Authenticator.authenticate - return None + return self._check_permissions(handler).get('username') async def refresh_user(self, user, handler=None): - try: - return bool(self._check_permissions(handler).get('username')) - except Exception: - # Returning False is what the jupyterhub API expects and jupyterhub - # will handle re-authenticating the user. - # https://jupyterhub.readthedocs.io/en/stable/api/auth.html#jupyterhub.auth.Authenticator.refresh_user - return False - raise AssertionError('should not get here') + # Reading jupyterhub code the handler is never None + # We need the handler for cookies and redirects + assert handler, \ + 'handler should never be none' + # Returning True/False is what the jupyterhub API expects and jupyterhub + # will handle re-authenticating the user if needed. + # https://jupyterhub.readthedocs.io/en/stable/api/auth.html#jupyterhub.auth.Authenticator.refresh_user + return bool(self._check_permissions(handler).get('username')) def _check_permissions(self, handler): + def _cookies(response): + for k, v in response.cookies.get_dict().items(): + handler.set_cookie(k, v) + + + def _maybe_html_redirect(response): + m = re.search(r'window.location = "(.*)"', pkcompat.from_bytes(response.content)) + m and self._redirect(handler, f'{self.sirepo_uri}{m.group(1)}') + + def _maybe_srexception_redirect(response): + if 'srException' not in response: + return + from sirepo import uri + e = PKDict(response.srException) + self._redirect( + handler, + uri.local_route( + _SIM_TYPE, + route_name=e.routeName, + params=e.params, + external=True, + sirepo_uri=self.sirepo_uri, + ) + ) + r = requests.post( # POSIT: no params on checkAuthJupyterhub self.sirepo_uri + sirepo.simulation_db.SCHEMA_COMMON.route.checkAuthJupyterhub, cookies={k: handler.get_cookie(k) for k in handler.cookies.keys()}, ) + _cookies(r) + if r.status_code == requests.codes.forbidden: + return PKDict() r.raise_for_status() - for k, v in r.cookies.get_dict().items(): - handler.set_cookie(k, v) - return PKDict(r.json()) + _maybe_html_redirect(r) + res = PKDict(r.json()) + _maybe_srexception_redirect(res) + return res + + def _redirect(self, handler, uri): + handler.redirect(uri) + raise tornado.web.Finish() diff --git a/sirepo/sim_api/jupyterhublogin.py b/sirepo/sim_api/jupyterhublogin.py index efc840206f..9cde9f65c0 100644 --- a/sirepo/sim_api/jupyterhublogin.py +++ b/sirepo/sim_api/jupyterhublogin.py @@ -32,33 +32,16 @@ _JUPYTERHUB_LOGOUT_USER_NAME_ATTR = 'jupyterhub_logout_user_name' +_SIM_TYPE = 'jupyterhublogin' + class API(sirepo.api.Base): - @sirepo.api_perm.manual_permission_check + @sirepo.api_perm.require_user def api_checkAuthJupyterhub(self): - def _res_for_uri(uri): - return self.reply_ok(PKDict(uri=uri)) - - u = None - try: - sirepo.auth.require_user() - sirepo.auth.check_sim_type_role('jupyterhublogin') - u = _unchecked_jupyterhub_user_name( - have_simulation_db=False, - ) - except werkzeug.exceptions.Forbidden: - return self.reply_ok() - except sirepo.util.Redirect as e: - return _res_for_uri(sirepo.uri_router.uri_for_api( - 'root', - params=PKDict(path_info=e.sr_args.uri), - )) - except sirepo.util.SRException as e: - return _res_for_uri(sirepo.uri.local_route( - 'jupyterhublogin', - route_name=e.sr_args.routeName, - external=True, - )) + self.parse_params(type=_SIM_TYPE) + u = _unchecked_jupyterhub_user_name( + have_simulation_db=False, + ) if not u: u = create_user() return self.reply_ok(PKDict( @@ -67,18 +50,18 @@ def _res_for_uri(uri): @sirepo.api_perm.require_user def api_migrateJupyterhub(self): - sirepo.auth.check_sim_type_role('jupyterhublogin') + self.parse_params(type=_SIM_TYPE) if not cfg.rs_jupyter_migrate: sirepo.util.raise_forbidden('migrate not enabled') d = self.parse_json() if not d.doMigration: create_user() return self.reply_redirect('jupyterHub') - sirepo.oauth.raise_authorize_redirect('jupyterhublogin', github_auth=True) + sirepo.oauth.raise_authorize_redirect(_SIM_TYPE, github_auth=True) @sirepo.api_perm.require_user def api_redirectJupyterHub(self): - sirepo.auth.check_sim_type_role('jupyterhublogin') + self.parse_params(type=_SIM_TYPE) u = _unchecked_jupyterhub_user_name() if u: return self.reply_redirect('jupyterHub') @@ -139,7 +122,7 @@ def __user_name(): not _user_dir(user_name=github_handle).exists(): raise sirepo.util.SRException( 'jupyterNameConflict', - PKDict(sim_type='jupyterhublogin'), + PKDict(sim_type=_SIM_TYPE), ) return github_handle n = __handle_or_name_sanitized() diff --git a/sirepo/uri.py b/sirepo/uri.py index 130a3c1da1..06185d8743 100644 --- a/sirepo/uri.py +++ b/sirepo/uri.py @@ -17,12 +17,13 @@ #: optional parameter that consumes rest of parameters PATH_INFO_CHAR = '*' -def app_root(sim_type, external=False): +def app_root(sim_type, external=False, sirepo_uri=None): """Generate uri for application root Args: sim_type (str): application name external (bool): if True, make the uri absolute [False] + sirepo_uri (str): Base uri for sirepo (ex https://sirepo.com) Returns: str: formatted URI """ @@ -31,6 +32,7 @@ def app_root(sim_type, external=False): 'root', params=PKDict(path_info=t) if t else None, external=external, + sirepo_uri=sirepo_uri, ) @@ -44,7 +46,7 @@ def init(**imports): sirepo.util.setattr_imports(imports) -def local_route(sim_type, route_name=None, params=None, query=None, external=False): +def local_route(sim_type, route_name=None, params=None, query=None, external=False, sirepo_uri=None): """Generate uri for local route with params Args: @@ -53,9 +55,13 @@ def local_route(sim_type, route_name=None, params=None, query=None, external=Fal params (dict): paramters to pass to route query (dict): query values (joined and escaped) external (bool): if True, make the uri absolute [False] + sirepo_uri (str): Base uri for sirepo (ex https://sirepo.com) Returns: str: formatted URI """ + if sirepo_uri: + assert external, \ + f'if sirepo_uri={sirepo_uri} then external={external} must be True' t = http_request.sim_type(sim_type) s = simulation_db.get_schema(t) if not route_name: @@ -68,7 +74,7 @@ def local_route(sim_type, route_name=None, params=None, query=None, external=Fal if not params or p not in params: continue u += '/' + _to_uri(params[p]) - return app_root(t, external=external) + '#' + u + _query(query) + return app_root(t, external=external, sirepo_uri=sirepo_uri) + '#' + u + _query(query) def server_route(route_or_uri, params, query): diff --git a/sirepo/uri_router.py b/sirepo/uri_router.py index 6bd9ea3094..6590dea137 100644 --- a/sirepo/uri_router.py +++ b/sirepo/uri_router.py @@ -207,20 +207,22 @@ def _is_api_func(cls, name, obj): _api_funcs[n] = _Route(func=o, cls=c, func_name=n) -def uri_for_api(api_name, params=None, external=True): +def uri_for_api(api_name, params=None, external=True, sirepo_uri=None): """Generate uri for api method Args: api_name (str): full name of api params (PKDict): paramters to pass to uri external (bool): if True, make the uri absolute [True] + sirepo_uri (str): Base uri for sirepo (ex https://sirepo.com) Returns: str: formmatted external URI """ if params is None: params = PKDict() r = _api_to_route[api_name] - res = (flask.url_for('_dispatch_empty', _external=external) + r.base_uri).rstrip('/') + s = sirepo_uri if sirepo_uri else flask.url_for('_dispatch_empty', _external=external) + res = (s + r.base_uri).rstrip('/') for p in r.params: if p.name in params: v = params[p.name]