Skip to content

Commit

Permalink
fix #4106: authorize codes via oauth
Browse files Browse the repository at this point in the history
  • Loading branch information
rorour committed Mar 9, 2022
1 parent 014c280 commit 87ea297
Show file tree
Hide file tree
Showing 20 changed files with 328 additions and 158 deletions.
4 changes: 4 additions & 0 deletions etc/run-flash.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export SIREPO_FEATURE_CONFIG_PROPRIETARY_OAUTH_SIM_TYPES=flash
export SIREPO_SIM_OAUTH_FLASH_SECRET=1138b29658c27a468c0ff4ead085aa55246a0f55
export SIREPO_SIM_OAUTH_FLASH_KEY=38b331e8a62164f62374
sirepo service http
4 changes: 1 addition & 3 deletions etc/run-jupyterhub.sh
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,7 @@ elif [[ ! $SIREPO_AUTH_METHODS =~ 'email' ]]; then
export SIREPO_AUTH_METHODS=$SIREPO_AUTH_METHODS:email
fi

if [[ ${SIREPO_AUTH_GITHUB_KEY:-} || ${SIREPO_AUTH_GITHUB_SECRET:-} ]]; then
export SIREPO_AUTH_GITHUB_METHOD_VISIBLE=0
export SIREPO_AUTH_METHODS="$SIREPO_AUTH_METHODS:github"
if [[ ${SIREPO_SIM_OAUTH_JUPYTERHUBLOGIN_KEY:-} || ${SIREPO_SIM_OAUTH_JUPYTERHUBLOGIN_SECRET:-} ]]; then
export SIREPO_SIM_API_JUPYTERHUBLOGIN_RS_JUPYTER_MIGRATE=1
fi

Expand Down
82 changes: 49 additions & 33 deletions sirepo/auth/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@
def api_authCompleteRegistration():
# Needs to be explicit, because we would need a special permission
# for just this API.
if not _is_logged_in():
if not is_logged_in():
raise util.SRException(LOGIN_ROUTE_NAME, None)
complete_registration(
_parse_display_name(http_request.parse_json().get('displayName')),
Expand Down Expand Up @@ -111,29 +111,45 @@ def api_authLogout(simulation_type=None):
req = http_request.parse_params(type=simulation_type)
except AssertionError:
pass
if _is_logged_in():
if is_logged_in():
events.emit('auth_logout', PKDict(uid=_get_user()))
cookie.set_value(_COOKIE_STATE, _STATE_LOGGED_OUT)
_set_log_user()
return http_reply.gen_redirect_for_app_root(req and req.type)


def check_user_has_role(uid, role, raise_forbidden=True):
if auth_db.UserRole.has_role(uid, role):
def _exception_for_moderation_prompt():
s = sirepo.auth_db.UserRoleInvite.get_status(uid, role)
if s == 'pending':
return sirepo.util.SRException('moderationPending', None)
if s == 'denied':
sirepo.util.raise_forbidden('uid={} role={} not found'.format(uid, role))
assert s is None, \
f'Unexpected status={s} for uid={uid} and role={role}'
return sirepo.util.SRException('moderationInfo', None)

def _check_oauth_permission(role):
import sirepo.oauth
return util.Redirect(
sirepo.oauth.create_authorize_redirect(
sirepo.auth_role.sim_type(role),
url_only=True,
)
)

def _role_for_sim_type(sim_types):
return [sirepo.auth_role.for_sim_type(s) for s in sim_types]

if auth_db.UserRole.has_role(uid, role) and not auth_db.UserRole.is_expired(uid, role):
return True
if not raise_forbidden:
return False
if role not in [sirepo.auth_role.for_sim_type(s) for s in sirepo.feature_config.cfg().proprietary_with_moderation_prompt_sim_types]:
sirepo.util.raise_forbidden('uid={} role={} not found'.format(uid, role))
s = sirepo.auth_db.UserRoleInvite.get_status(uid, role)
if s == 'pending':
raise sirepo.util.SRException('moderationPending', None)
if s == 'denied':
sirepo.util.raise_forbidden('uid={} role={} not found'.format(uid, role))
assert s is None, \
f'Unexpected status={s} for uid={uid} and role={role}'
raise sirepo.util.SRException('moderationInfo', None)

if role in _role_for_sim_type(sirepo.feature_config.cfg().proprietary_with_moderation_prompt_sim_types):
raise _exception_for_moderation_prompt()
elif role in _role_for_sim_type(sirepo.feature_config.cfg().proprietary_oauth_sim_types):
raise _check_oauth_permission(role)
sirepo.util.raise_forbidden('uid={} role={} not found'.format(uid, role))

def check_user_is_adm():
assert check_user_has_role(logged_in_user(), sirepo.auth_role.ROLE_ADM)
Expand Down Expand Up @@ -184,6 +200,18 @@ def init_apis(*args, **kwargs):
f'payment plans from SCHEMA_COMMON={s} not equal to _ALL_PAYMENT_PLANS={_ALL_PAYMENT_PLANS}'


def is_logged_in(state=None):
"""Logged in is either needing to complete registration or done
Args:
state (str): logged in state [None: from cookie]
Returns:
bool: is in one of the logged in states
"""
s = state or cookie.unchecked_get_value(_COOKIE_STATE)
return s in (_STATE_COMPLETE_REGISTRATION, _STATE_LOGGED_IN)


def is_premium_user():
return check_user_has_role(
logged_in_user(),
Expand All @@ -201,7 +229,7 @@ def logged_in_user(check_path=True):
str: uid of authenticated user
"""
u = _get_user()
if not _is_logged_in():
if not is_logged_in():
raise util.SRException(
'login',
None,
Expand Down Expand Up @@ -237,7 +265,7 @@ def login(module, uid=None, model=None, sim_type=None, display_name=None, is_moc
# if previously cookied as a guest, move the non-example simulations into uid below
m = cookie.unchecked_get_value(_COOKIE_METHOD)
if m == METHOD_GUEST and module.AUTH_METHOD != METHOD_GUEST:
guest_uid = _get_user() if _is_logged_in() else None
guest_uid = _get_user() if is_logged_in() else None
if uid:
_login_user(module, uid)
if module.AUTH_METHOD in cfg.deprecated_methods:
Expand All @@ -254,7 +282,7 @@ def login(module, uid=None, model=None, sim_type=None, display_name=None, is_moc
# Not allowed to go to guest from other methods, because there's
# no authentication for guest.
# Or, this is just a new user, and we'll create one.
uid = _get_user() if _is_logged_in() else None
uid = _get_user() if is_logged_in() else None
m = cookie.unchecked_get_value(_COOKIE_METHOD)
if uid and module.AUTH_METHOD not in (m, METHOD_GUEST):
# switch this method to this uid (even for methods)
Expand Down Expand Up @@ -355,7 +383,7 @@ def require_auth_basic():
def require_sim_type(sim_type):
if sim_type not in sirepo.feature_config.auth_controlled_sim_types():
return
if not _is_logged_in():
if not is_logged_in():
# If a user is not logged in, we allow any sim_type, because
# the GUI has to be able to get access to certain APIs before
# logging in.
Expand Down Expand Up @@ -486,13 +514,13 @@ def user_dir_not_found(user_dir, uid):
)


def user_if_logged_in(method):
def user_if_logged_in(method=None):
"""Verify user is logged in and method matches
Args:
method (str): method must be logged in as
"""
if not _is_logged_in():
if not is_logged_in():
return None
m = cookie.unchecked_get_value(_COOKIE_METHOD)
if m != method:
Expand Down Expand Up @@ -599,7 +627,7 @@ def _auth_state():
displayName=None,
guestIsOnlyMethod=not non_guest_methods,
isGuestUser=False,
isLoggedIn=_is_logged_in(s),
isLoggedIn=is_logged_in(s),
isLoginExpired=False,
jobRunModeMap=sirepo.simulation_db.JOB_RUN_MODE_MAP,
method=cookie.unchecked_get_value(_COOKIE_METHOD),
Expand Down Expand Up @@ -690,18 +718,6 @@ def user_dir_not_found(user_dir, *args, **kwargs):
cfg.methods = set((METHOD_GUEST,))


def _is_logged_in(state=None):
"""Logged in is either needing to complete registration or done
Args:
state (str): logged in state [None: from cookie]
Returns:
bool: is in one of the logged in states
"""
s = state or cookie.unchecked_get_value(_COOKIE_STATE)
return s in (_STATE_COMPLETE_REGISTRATION, _STATE_LOGGED_IN)


def _login_user(module, uid):
"""Set up the cookie for logged in state
Expand Down
104 changes: 26 additions & 78 deletions sirepo/auth/github.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,17 @@
:copyright: Copyright (c) 2016-2019 RadiaSoft LLC. All Rights Reserved.
:license: http://www.apache.org/licenses/LICENSE-2.0.html
"""
from __future__ import absolute_import, division, print_function
from pykern import pkconfig
from pykern import pkinspect
from pykern.pkcollections import PKDict
from pykern.pkdebug import pkdc, pkdexc, pkdlog, pkdp
from sirepo import api_perm
from sirepo import auth
from sirepo import auth_db
from sirepo import cookie
from sirepo import feature_config
from sirepo import http_reply
from sirepo import http_request
from sirepo import uri_router
from sirepo import util
import authlib.integrations.base_client
import authlib.integrations.requests_client
import flask
import sirepo.events
import sirepo.oauth
import sqlalchemy


Expand All @@ -35,28 +28,14 @@
#: Well known alias for auth
UserModel = None

#: module handle
this_module = pkinspect.this_module()

#: cookie keys for github (prefix is "srag")
_COOKIE_NONCE = 'sragn'
_COOKIE_SIM_TYPE = 'srags'


@api_perm.allow_cookieless_set_user
def api_authGithubAuthorized():
"""Handle a callback from a successful OAUTH request.
Tracks oauth users in a database.
"""
# clear temporary cookie values first
oc = _client(cookie.unchecked_remove(_COOKIE_NONCE))
t = cookie.unchecked_remove(_COOKIE_SIM_TYPE)
if not oc.authorize_access_token():
auth.login_fail_redirect(t, this_module, 'oauth-state', reload_js=True)
raise AssertionError('auth.login_fail_redirect returned unexpectedly')
oc, t = sirepo.oauth.check_authorized_callback(github_auth=True)
d = oc.get('user').json()
sirepo.events.emit('github_authorized', PKDict(user_name=d['login']))
with util.THREAD_LOCK:
u = AuthGithubUser.search_by(oauth_id=d['id'])
if u:
Expand All @@ -65,22 +44,26 @@ def api_authGithubAuthorized():
else:
u = AuthGithubUser(oauth_id=d['id'], user_name=d['login'])
u.save()
auth.login(this_module, model=u, sim_type=t, want_redirect=True)
auth.login(
pkinspect.this_module(),
model=u,
sim_type=t,
want_redirect=True,
)
raise AssertionError('auth.login returned unexpectedly')


@api_perm.require_cookie_sentinel
def api_authGithubLogin(simulation_type):
"""Redirects to Github"""
req = http_request.parse_params(type=simulation_type)
s = util.random_base62()
cookie.set_value(_COOKIE_NONCE, s)
cookie.set_value(_COOKIE_SIM_TYPE, req.type)
if not cfg.callback_uri:
# must be executed in an app and request context so can't
# initialize earlier.
cfg.callback_uri = uri_router.uri_for_api('authGithubAuthorized')
return _client(s).authorize_redirect(redirect_uri=cfg.callback_uri, state=s)
import sirepo.oauth
return sirepo.oauth.create_authorize_redirect(
http_request.parse_params(
type=simulation_type,
require_sim_type=False,
).type,
github_auth=True,
)


@api_perm.allow_cookieless_set_user
Expand All @@ -96,51 +79,6 @@ def avatar_uri(model, size):
)


class _Client(authlib.integrations.base_client.RemoteApp):

def __init__(self, state):
super().__init__(
framework=PKDict(oauth2_client_cls=authlib.integrations.requests_client.OAuth2Session),
name='github',
access_token_params=None,
access_token_url='https://github.com/login/oauth/access_token',
api_base_url='https://api.github.com/',
authorize_params=None,
authorize_url='https://github.com/login/oauth/authorize',
client_id=cfg.key,
client_kwargs={'scope': 'user:email'},
client_secret=cfg.secret,
)
self.__state = state

def authorize_access_token(self):
assert flask.request.method == 'GET'
a = self.__state
assert a
b = flask.request.args.get('state')
if a != b:
pkdlog('mismatch oauth state: expected {} != got {}', a, b)
return None
t = self.fetch_access_token(code=flask.request.args['code'], state=b)
self.token = t
return t

def authorize_redirect(self, redirect_uri=None, **kwargs):
return http_reply.gen_redirect(
self.create_authorization_url(redirect_uri, **kwargs)['url'],
)

def request(self, method, url, token=None, **kwargs):
if token is None and not kwargs.get('withhold_token'):
token = self.token
return super().request(method, url, token=token, **kwargs)


def _client(state):
"""Makes it easier to mock, see github_srunit.py"""
return _Client(state)


def _init():
def _init_model(base):
"""Creates User class bound to dynamic `db` variable"""
Expand All @@ -165,6 +103,16 @@ class AuthGithubUser(base):
),
secret=pkconfig.Required(str, 'Github secret'),
)
cfg.callback_api = 'authGithubAuthorized'
cfg.oauth_client = PKDict(
name='github',
access_token_params=None,
access_token_url='https://github.com/login/oauth/access_token',
api_base_url='https://api.github.com/',
authorize_params=None,
authorize_url='https://github.com/login/oauth/authorize',
client_kwargs={'scope': 'user:email'},
)
AUTH_METHOD_VISIBLE = cfg.method_visible
auth_db.init_model(_init_model)

Expand Down
2 changes: 1 addition & 1 deletion sirepo/auth/guest.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
#: module handle
this_module = pkinspect.this_module()

#: time to recheck login against db (prefix is "sraz", because github is "srag")
#: time to recheck login against db (prefix is "sraz", because github was "srag")
_COOKIE_EXPIRY_TIMESTAMP = 'srazt'

_ONE_DAY = datetime.timedelta(days=1)
Expand Down
Loading

0 comments on commit 87ea297

Please sign in to comment.