Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix #4106: proprietary oauth #4224

Merged
merged 11 commits into from
Jun 20, 2022
9 changes: 9 additions & 0 deletions etc/run-flash.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#!/bin/bash
set -eou pipefail

export SIREPO_FEATURE_CONFIG_PROPRIETARY_OAUTH_SIM_TYPES=flash
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_KEY, 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:+$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',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we put a test in for why we have to pin versions? Someday we'll want to upgrade and it would be helpful to know why we pinned it in the first place

'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
73 changes: 29 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 Down Expand Up @@ -135,6 +135,27 @@ def complete_registration(name=None):
cookie.set_value(_COOKIE_STATE, _STATE_LOGGED_IN)


def control_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
#QUESTION(robnagler) I think this is sufficient, that is, the tests can be reversed
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the only thing I'm concerned about. LMK what you think.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That doesn't workin in the api_checkAuthJupyterhub case. That API is allow_visitor which means that this check is always True and we return when we should really be prompting to moderate.

Thoughts ccaa8f6 ? I don't love adding another api_perm and I struggled to find a name for it...

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(role))
if r in sirepo.auth_role.for_moderated_sim_types():
auth_role_moderation.control_for_user(u, r)
sirepo.util.raise_forbidden(f'uid={u} does not have access to sim_type={t}')


def create_new_user(uid_generated_callback, module):
import sirepo.simulation_db
u = sirepo.simulation_db.user_create()
Expand Down Expand Up @@ -333,47 +354,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