Skip to content

Commit

Permalink
fix #4106: proprietary oauth (#4224)
Browse files Browse the repository at this point in the history
* fix #4106: proprietary oauth

Co-authored-by: Evan Carlin <evan@carlin.com>
Co-authored-by: robnagler <github@q33.us>
  • Loading branch information
3 people authored Jun 20, 2022
1 parent 0705ec4 commit d0b2235
Show file tree
Hide file tree
Showing 26 changed files with 469 additions and 224 deletions.
10 changes: 10 additions & 0 deletions etc/run-flash.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
#!/bin/bash
set -eou pipefail

export SIREPO_FEATURE_CONFIG_PROPRIETARY_OAUTH_SIM_TYPES=flash
export SIREPO_SIM_OAUTH_FLASH_INFO_VALID_USER=G
if [[ ! ${SIREPO_SIM_OAUTH_FLASH_KEY:-} || ! ${SIREPO_SIM_OAUTH_FLASH_SECRET:-} ]]; then
echo 'You must set $SIREPO_SIM_OAUTH_FLASH_KEY and $SIREPO_SIM_OAUTH_FLASH_SECRET' 1>&2
exit 1
fi
sirepo service http
16 changes: 8 additions & 8 deletions etc/run-jupyterhub.sh
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
#!/bin/bash
#
# Start Sirepo Jupyterhub with email login
# If $SIREPO_AUTH_GITHUB_KEY and $SIREPO_AUTH_GITHUB_SECRET, then
# add github authentication to test SIREPO_SIM_API_JUPYTERHUBLOGIN_RS_JUPYTER_MIGRATE.
#
set -eou pipefail

if [[ ! -d ~/mail ]]; then
Expand Down Expand Up @@ -33,15 +38,10 @@ export SIREPO_FROM_NAME='RadiaSoft Support'
export SIREPO_SMTP_SERVER='localhost'
export SIREPO_SMTP_SEND_DIRECTLY=1

if [[ ! ${SIREPO_AUTH_METHODS:-} ]]; then
export SIREPO_AUTH_METHODS=email
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_METHODS=${SIREPO_AUTH_METHODS:+$SIREPO_AUTH_METHODS:}email
if [[ ${SIREPO_AUTH_GITHUB_KEY:-} && ${SIREPO_AUTH_GITHUB_SECRET:-} ]]; then
export SIREPO_AUTH_METHODS=$SIREPO_AUTH_METHODS:github
export SIREPO_AUTH_GITHUB_METHOD_VISIBLE=0
export SIREPO_AUTH_METHODS="$SIREPO_AUTH_METHODS:github"
export SIREPO_SIM_API_JUPYTERHUBLOGIN_RS_JUPYTER_MIGRATE=1
fi

Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
description='accelerator code gui',
install_requires=[
'Flask==2.0.3',
'SQLAlchemy',
'SQLAlchemy>=1.4',
'aenum',
'asyncssh',
'cryptography>=2.8',
Expand Down
5 changes: 2 additions & 3 deletions sirepo/api_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,5 @@ def check_api_call(func):
raise AssertionError('unhandled api_perm={}'.format(expect))


def is_sim_type_required_for_api(func):
e = getattr(func, api_perm.ATTR)
return e not in api_perm.SIM_TYPELESS_PERMS
def maybe_sim_type_required_for_api(func):
return getattr(func, api_perm.ATTR) not in api_perm.SIM_TYPELESS_PERMS
72 changes: 28 additions & 44 deletions sirepo/auth/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,21 +90,21 @@ def api_authCompleteRegistration(self):
_parse_display_name(self.parse_json().get('displayName')),
)
return self.reply_ok()


@api_perm.allow_visitor
def api_authState(self):
return self.reply_static_jinja(
'auth-state',
'js',
PKDict(auth_state=_auth_state()),
)


@api_perm.allow_visitor
def api_authLogout(self, simulation_type=None):
"""Set the current user as logged out.
Redirects to root simulation page.
"""
req = None
Expand All @@ -120,6 +120,26 @@ def api_authLogout(self, simulation_type=None):
return self.reply_redirect_for_app_root(req and req.type)


def check_sim_type_role(sim_type):
from sirepo import oauth
from sirepo import auth_role_moderation

t = sirepo.template.assert_sim_type(sim_type)
if t not in sirepo.feature_config.auth_controlled_sim_types():
return
if not uri_router.maybe_sim_type_required_for_api():
return
u = logged_in_user()
r = sirepo.auth_role.for_sim_type(t)
if auth_db.UserRole.has_role(u, r) and not auth_db.UserRole.is_expired(u, r):
return
elif r in sirepo.auth_role.for_proprietary_oauth_sim_types():
oauth.raise_authorize_redirect(sirepo.auth_role.sim_type(r))
if r in sirepo.auth_role.for_moderated_sim_types():
auth_role_moderation.raise_control_for_user(u, r)
sirepo.util.raise_forbidden(f'uid={u} does not have access to sim_type={t}')


def complete_registration(name=None):
"""Update the database with the user's display_name and sets state to logged-in.
Guests will have no name.
Expand Down Expand Up @@ -333,47 +353,11 @@ def require_auth_basic():
login(m, uid=uid)


def require_sim_type(sim_type):
def _assert_login():
try:
return logged_in_user()
except util.SRException as e:
if (
getattr(e, 'sr_args', PKDict()).get('routeName') == LOGIN_ROUTE_NAME
and not uri_router.is_sim_type_required_for_api()
):
return None
raise

def _moderate(uid, role):
s = sirepo.auth_db.UserRoleInvite.get_status(uid, role)
if s in ('clarify', 'pending'):
raise sirepo.util.SRException('moderationPending', None)
if s == 'denied':
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}'
require_email_user()
raise sirepo.util.SRException('moderationRequest', None)

if sim_type not in sirepo.feature_config.auth_controlled_sim_types():
return
u = _assert_login()
if u is None:
return
r = sirepo.auth_role.for_sim_type(sim_type)
if auth_db.UserRole.has_role(u, r):
return
if r not in sirepo.auth_role.for_moderated_sim_types():
sirepo.util.raise_forbidden(f'uid={u} does not have access to sim_type={sim_type}')
_moderate(u, r)


def require_email_user():
uid = require_user()
u = user_name(uid)
i = require_user()
u = user_name(i)
if not pyisemail.is_email(u):
util.raise_forbidden(f'uid={uid} username={u} is not an email')
util.raise_forbidden(f'uid={i} username={u} is not an email')


def require_user():
Expand Down
80 changes: 20 additions & 60 deletions sirepo/auth/github.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,10 @@
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 uri_router
from sirepo import util
import authlib.integrations.requests_client
import authlib.oauth2.rfc6749.errors
import flask
import sirepo.api
import sirepo.events
import sirepo.oauth
import sqlalchemy


Expand All @@ -33,34 +27,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'


class API(sirepo.api.Base):
@api_perm.allow_cookieless_set_user
def api_authGithubAuthorized(self):
"""Handle a callback from a successful OAUTH request.
Tracks oauth users in a database.
"""
# clear temporary cookie values first
s = cookie.unchecked_remove(_COOKIE_NONCE)
t = cookie.unchecked_remove(_COOKIE_SIM_TYPE)
oc = _client()
try:
oc.fetch_token(
authorization_response=flask.request.url,
state=s,
)
except authlib.oauth2.rfc6749.errors.MismatchingStateException:
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('https://api.github.com/user').json()
sirepo.events.emit('github_authorized', PKDict(user_name=d['login']))
with util.THREAD_LOCK:
Expand All @@ -71,29 +45,23 @@ def api_authGithubAuthorized(self):
else:
u = AuthGithubUser(oauth_id=d['id'], user_name=d['login'])
u.save()
auth.login(this_module, model=u, sim_type=t, sapi=self, 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(self, simulation_type):
"""Redirects to Github"""
req = self.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')
u, _ = _client().create_authorization_url(
'https://github.com/login/oauth/authorize',
redirect_uri=cfg.callback_uri,
state=s,
sirepo.oauth.raise_authorize_redirect(
self.parse_params(type=simulation_type).type,
github_auth=True,
)
return self.reply_redirect(u)



@api_perm.allow_cookieless_set_user
def api_oauthAuthorized(self, oauth_type):
"""Deprecated use `api_authGithubAuthorized`"""
Expand All @@ -107,19 +75,6 @@ def avatar_uri(model, size):
)


def _client(token=None):
"""Makes it easier to mock, see github_srunit.py"""
# OAuth2Session doesn't inherit from OAuth2Mixin for some reason.
# So, supplying api_base_url has no effect.
return authlib.integrations.requests_client.OAuth2Session(
cfg.key,
cfg.secret,
scope='user:email',
token=token,
token_endpoint='https://github.com/login/oauth/access_token',
)


def _init():
def _init_model(base):
"""Creates User class bound to dynamic `db` variable"""
Expand All @@ -135,15 +90,20 @@ class AuthGithubUser(base):

global cfg, AUTH_METHOD_VISIBLE
cfg = pkconfig.init(
authorize_url=('https://github.com/login/oauth/authorize', str, 'url to redirect to for authorization'),
callback_uri=(None, str, 'Github callback URI (defaults to api_authGithubAuthorized)'),
key=pkconfig.Required(str, 'Github key'),
method_visible=(
True,
bool,
'github auth method is visible to users when it is an enabled method',
),
scope=('user:email', str, 'scope of data to request about user'),
secret=pkconfig.Required(str, 'Github secret'),
token_endpoint=('https://github.com/login/oauth/access_token', str, 'url for obtaining access token')
)
cfg.callback_api = 'authGithubAuthorized'

AUTH_METHOD_VISIBLE = cfg.method_visible
auth_db.init_model(_init_model)

Expand Down
Loading

0 comments on commit d0b2235

Please sign in to comment.