Skip to content

Commit

Permalink
use "normal" permission checks and have sirepo.jupyterhub parse the r…
Browse files Browse the repository at this point in the history
…esponse
  • Loading branch information
e-carlin committed Jun 14, 2022
1 parent 7d2f7e0 commit 890681e
Show file tree
Hide file tree
Showing 7 changed files with 74 additions and 55 deletions.
2 changes: 1 addition & 1 deletion sirepo/api_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down
2 changes: 0 additions & 2 deletions sirepo/api_perm.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion sirepo/auth_role_moderation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}'
Expand Down
66 changes: 48 additions & 18 deletions sirepo/jupyterhub.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -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()
39 changes: 11 additions & 28 deletions sirepo/sim_api/jupyterhublogin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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')
Expand Down Expand Up @@ -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()
Expand Down
12 changes: 9 additions & 3 deletions sirepo/uri.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
"""
Expand All @@ -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,
)


Expand All @@ -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:
Expand All @@ -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:
Expand All @@ -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):
Expand Down
6 changes: 4 additions & 2 deletions sirepo/uri_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down

0 comments on commit 890681e

Please sign in to comment.