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 #3654: jupyterhub moderation requests #4211

Merged
merged 1 commit into from
May 30, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 1 addition & 30 deletions etc/run-auth-email.sh
Original file line number Diff line number Diff line change
@@ -1,34 +1,5 @@
#!/bin/bash
: how to setup postfix with sasl for smtpd auth <<'EOF'
# as user vagrant
install -m 600 /dev/stdin ~/.procmailrc <<'END'
UMASK=077
:0
mail/.
END
install -m 600 -d ~/mail
sudo su -
dnf install -y postfix cyrus-sasl cyrus-sasl-lib cyrus-sasl-plain telnet procmail
systemctl start postfix
systemctl enable postfix
echo vagrant | saslpasswd2 -f /etc/sasldb2 -c -p vagrant
chgrp mail /etc/sasldb2
cat > /etc/sasl2/smtpd-sasldb.conf <<'END'
auxprop_plugin: sasldb
log_level: 4
mech_list: plain
pwcheck_method: auxprop
END
postconf smtpd_sasl_path=smtpd-sasldb smtpd_sasl_auth_enable=yes 'mailbox_command=/usr/bin/procmail -a "$EXTENSION"'
systemctl restart postfix
exit
# to test procmail delivery
echo hello | sendmail vagrant
# to test sasl
(sleep 1; echo EHLO localhost; sleep 1; echo AUTH PLAIN AHZhZ3JhbnQAdmFncmFudA==; sleep 1; echo QUIT) | telnet localhost 25
# then use
export SIREPO_SMTP_SERVER='localhost'
EOF
# see run-jupyterhub.sh for setting up local mail delivery
export SIREPO_FROM_EMAIL='support@radiasoft.net'
export SIREPO_FROM_NAME='RadiaSoft Support'
export SIREPO_SMTP_PASSWORD='vagrant'
Expand Down
32 changes: 31 additions & 1 deletion etc/run-jupyterhub.sh
Original file line number Diff line number Diff line change
@@ -1,7 +1,37 @@
#!/bin/bash
set -eou pipefail

export SIREPO_FEATURE_CONFIG_DEFAULT_PROPRIETARY_SIM_TYPES=jupyterhublogin
if [[ ! -d ~/mail ]]; then
install -m 700 -d ~/mail
install -m 600 /dev/stdin ~/.procmailrc <<'END'
UMASK=077
:0
mail/.
END
sudo su - <<'END'
dnf install -y postfix procmail
postconf -e \
'mydestination=$myhostname, localhost.$mydomain, localhost, localhost.localdomain' \
mailbox_command=/usr/bin/procmail
systemctl enable postfix
systemctl restart postfix
END
echo 'Testing mail delivery'
echo hello | sendmail vagrant@localhost.localdomain
sleep 4
if ! grep -s hello ~/mail/1; then
echo mail delivery test failed
exit 1
fi
rm ~/mail/1
fi

export SIREPO_FEATURE_CONFIG_MODERATED_SIM_TYPES=jupyterhublogin
export SIREPO_AUTH_ROLE_MODERATION_MODERATOR_EMAIL='vagrant@localhost.localdomain'
export SIREPO_FROM_EMAIL='$USER+support@localhost.localdomain'
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
Expand Down
16 changes: 15 additions & 1 deletion sirepo/api_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,20 @@ def assert_api_def(func):
def check_api_call(func):
expect = getattr(func, api_perm.ATTR)
a = api_perm.APIPerm
if expect in (a.REQUIRE_COOKIE_SENTINEL, a.REQUIRE_USER):
if expect in (
a.ALLOW_SIM_TYPELESS_REQUIRE_EMAIL_USER,
a.REQUIRE_COOKIE_SENTINEL,
a.REQUIRE_USER,
a.REQUIRE_ADM,
):
if not cookie.has_sentinel():
raise sirepo.util.SRException('missingCookies', None)
if expect == a.REQUIRE_USER:
auth.require_user()
elif expect == a.ALLOW_SIM_TYPELESS_REQUIRE_EMAIL_USER:
auth.require_email_user()
elif expect == a.REQUIRE_ADM:
auth.require_adm()
elif expect == a.ALLOW_VISITOR:
pass
elif expect == a.INTERNAL_TEST:
Expand All @@ -48,3 +57,8 @@ def check_api_call(func):
auth.require_auth_basic()
else:
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
14 changes: 13 additions & 1 deletion sirepo/api_perm.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,17 @@
#: decorator sets this attribute with an APIPerm
ATTR = 'api_perm'


class APIPerm(aenum.Flag):
#: A user is required but there might not be a cookie yet
ALLOW_COOKIELESS_REQUIRE_USER = aenum.auto()
#: cookie.set_user can be called even if a cookie wasn't received
ALLOW_COOKIELESS_SET_USER = aenum.auto()
#: anybody can view this page, even without cookies
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()
#: only users with role adm
REQUIRE_ADM = aenum.auto()
#: use basic auth authentication (only)
REQUIRE_AUTH_BASIC = aenum.auto()
#: a cookie has to have been returned, which might contain a user
Expand All @@ -31,6 +34,15 @@ class APIPerm(aenum.Flag):
INTERNAL_TEST = aenum.auto()


#: A user can access APIs decorated with these permissions even if they don't have the role
SIM_TYPELESS_PERMS = {
APIPerm.ALLOW_COOKIELESS_SET_USER,
APIPerm.ALLOW_SIM_TYPELESS_REQUIRE_EMAIL_USER,
APIPerm.ALLOW_VISITOR,
APIPerm.REQUIRE_COOKIE_SENTINEL,
}


def _init():
def _new(e):
def _decorator(func):
Expand Down
103 changes: 69 additions & 34 deletions sirepo/auth/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import contextlib
import datetime
import importlib
import pyisemail
import sirepo.auth_role
import sirepo.feature_config
import sirepo.request
Expand Down Expand Up @@ -119,22 +120,14 @@ def api_authLogout(self, simulation_type=None):
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):
return True
if raise_forbidden:
sirepo.util.raise_forbidden('uid={} role={} not found'.format(uid, role))
return False


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.
"""
u = _get_user()
with util.THREAD_LOCK:
r = user_registration(u)
if cookie.unchecked_get_value(_COOKIE_METHOD) is METHOD_GUEST:
if cookie.unchecked_get_value(_COOKIE_METHOD) == METHOD_GUEST:
assert name is None, \
'Cookie method is {} and name is {}. Expected name to be None'.format(METHOD_GUEST, name)
r.display_name = name
Expand Down Expand Up @@ -173,10 +166,9 @@ def init_apis(*args, **kwargs):


def is_premium_user():
return check_user_has_role(
return auth_db.UserRole.has_role(
logged_in_user(),
sirepo.auth_role.ROLE_PAYMENT_PLAN_PREMIUM,
raise_forbidden=False,
)


Expand All @@ -191,7 +183,7 @@ def logged_in_user(check_path=True):
u = _get_user()
if not _is_logged_in():
raise util.SRException(
'login',
LOGIN_ROUTE_NAME,
None,
'user not logged in uid={}',
u,
Expand Down Expand Up @@ -325,6 +317,12 @@ def process_request(unit_test=None):
yield


def require_adm():
u = require_user()
if not auth_db.UserRole.has_role(u, sirepo.auth_role.ROLE_ADM):
sirepo.util.raise_forbidden(f'uid={u} role=ROLE_ADM not found')


def require_auth_basic():
m = _METHOD_MODULES['basic']
_validate_method(m)
Expand All @@ -341,35 +339,70 @@ def require_auth_basic():


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
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.
u = _assert_login()
if u is None:
return
check_user_has_role(
logged_in_user(),
sirepo.auth_role.for_sim_type(sim_type),
)
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)
if not pyisemail.is_email(u):
util.raise_forbidden(f'uid={uid} username={u} is not an email')


def require_user():
"""Asserts whether user is logged in

Returns:
str: user id
"""
e = None
m = cookie.unchecked_get_value(_COOKIE_METHOD)
p = None
r = 'login'
r = LOGIN_ROUTE_NAME
s = cookie.unchecked_get_value(_COOKIE_STATE)
u = _get_user()
if s is None:
pass
elif s == _STATE_LOGGED_IN:
if m in cfg.methods:

f = getattr(_METHOD_MODULES[m], 'validate_login', None)
if f:
pkdc('validate_login method={}', m)
f()
return
return u
if m in cfg.deprecated_methods:
e = 'deprecated'
else:
Expand All @@ -388,7 +421,7 @@ def require_user():
if m == METHOD_GUEST:
pkdc('guest completeRegistration={}', u)
complete_registration()
return
return u
r = 'completeRegistration'
e = 'uid={} needs to complete registration'.format(u)
else:
Expand All @@ -406,7 +439,6 @@ def reset_state():
_set_log_user()



@contextlib.contextmanager
def set_user_outside_of_http_request(uid):
"""A user set explicitly outside of flask request cycle
Expand Down Expand Up @@ -467,13 +499,17 @@ def user_dir_not_found(user_dir, uid):
u.delete()
reset_state()
raise util.Redirect(
sirepo.uri.ROOT,
uri_router.uri_for_api('root', external=False),
'simulation_db dir={} not found, deleted uid={}',
user_dir,
uid,
)


def user_display_name(uid):
return auth_db.UserRegistration.search_by(uid=uid).display_name


def user_if_logged_in(method):
"""Verify user is logged in and method matches

Expand All @@ -488,18 +524,17 @@ def user_if_logged_in(method):
return _get_user()


def user_name():
def user_name(uid=None):
if not uid:
uid = logged_in_user()
m = cookie.unchecked_get_value(_COOKIE_METHOD)
u = getattr(
_METHOD_MODULES[m],
'UserModel',
)
u = getattr(_METHOD_MODULES[m], 'UserModel', None)
if u:
with util.THREAD_LOCK:
return u.search_by(uid=logged_in_user()).user_name
raise AssertionError(
f'user_name not found for uid={logged_in_user()} with method={m}',
)
return u.search_by(uid=uid).user_name
elif m == METHOD_GUEST:
return 'guest-' + uid
raise AssertionError(f'user_name not found for uid={uid} with method={m}')


def user_registration(uid, display_name=None):
Expand Down
Loading