rename white/blacklist allowed/blocked

- group_whitelist -> allowed_groups

still todo: handle deprecated signatures in check_whitelist methods while preserving subclass overrides
This commit is contained in:
Min RK
2020-06-15 14:40:20 +02:00
parent b5defabf49
commit cc8e780653
9 changed files with 219 additions and 139 deletions

View File

@@ -201,7 +201,7 @@ class OAuthAuthorizeHandler(OAuthHandler, BaseHandler):
def needs_oauth_confirm(self, user, oauth_client): def needs_oauth_confirm(self, user, oauth_client):
"""Return whether the given oauth client needs to prompt for access for the given user """Return whether the given oauth client needs to prompt for access for the given user
Checks whitelist for oauth clients Checks list for oauth clients that don't need confirmation
(i.e. the user's own server) (i.e. the user's own server)
@@ -214,9 +214,8 @@ class OAuthAuthorizeHandler(OAuthHandler, BaseHandler):
if ( if (
# it's the user's own server # it's the user's own server
oauth_client.identifier in own_oauth_client_ids oauth_client.identifier in own_oauth_client_ids
# or it's in the global whitelist # or it's in the global no-confirm list
or oauth_client.identifier or oauth_client.identifier in self.settings.get('oauth_no_confirm', set())
in self.settings.get('oauth_no_confirm_whitelist', set())
): ):
return False return False
# default: require confirmation # default: require confirmation
@@ -229,7 +228,7 @@ class OAuthAuthorizeHandler(OAuthHandler, BaseHandler):
Render oauth confirmation page: Render oauth confirmation page:
"Server at ... would like permission to ...". "Server at ... would like permission to ...".
Users accessing their own server or a service whitelist Users accessing their own server or a blessed service
will skip confirmation. will skip confirmation.
""" """

View File

@@ -1689,22 +1689,22 @@ class JupyterHub(Application):
# the admin_users config variable will never be used after this point. # the admin_users config variable will never be used after this point.
# only the database values will be referenced. # only the database values will be referenced.
whitelist = [ allowed = [
self.authenticator.normalize_username(name) self.authenticator.normalize_username(name)
for name in self.authenticator.whitelist for name in self.authenticator.allowed
] ]
self.authenticator.whitelist = set(whitelist) # force normalization self.authenticator.allowed = set(allowed) # force normalization
for username in whitelist: for username in allowed:
if not self.authenticator.validate_username(username): if not self.authenticator.validate_username(username):
raise ValueError("username %r is not valid" % username) raise ValueError("username %r is not valid" % username)
if not whitelist: if not allowed:
self.log.info( self.log.info(
"Not using whitelist. Any authenticated user will be allowed." "Not using allowed user list. Any authenticated user will be allowed."
) )
# add whitelisted users to the db # add allowed users to the db
for name in whitelist: for name in allowed:
user = orm.User.find(db, name) user = orm.User.find(db, name)
if user is None: if user is None:
user = orm.User(name=name) user = orm.User(name=name)
@@ -1714,9 +1714,9 @@ class JupyterHub(Application):
db.commit() db.commit()
# Notify authenticator of all users. # Notify authenticator of all users.
# This ensures Auth whitelist is up-to-date with the database. # This ensures Authenticator.allowed is up-to-date with the database.
# This lets whitelist be used to set up initial list, # This lets .allowed be used to set up initial list,
# but changes to the whitelist can occur in the database, # but changes to the allowed list can occur in the database,
# and persist across sessions. # and persist across sessions.
total_users = 0 total_users = 0
for user in db.query(orm.User): for user in db.query(orm.User):
@@ -1753,9 +1753,9 @@ class JupyterHub(Application):
user.created = user.last_activity or datetime.utcnow() user.created = user.last_activity or datetime.utcnow()
db.commit() db.commit()
# The whitelist set and the users in the db are now the same. # The allowed set and the users in the db are now the same.
# From this point on, any user changes should be done simultaneously # From this point on, any user changes should be done simultaneously
# to the whitelist set and user db, unless the whitelist is empty (all users allowed). # to the allowed set and user db, unless the allowed set is empty (all users allowed).
TOTAL_USERS.set(total_users) TOTAL_USERS.set(total_users)
@@ -1770,11 +1770,11 @@ class JupyterHub(Application):
for username in usernames: for username in usernames:
username = self.authenticator.normalize_username(username) username = self.authenticator.normalize_username(username)
if not ( if not (
await maybe_future( await maybe_future(self.authenticator.check_allowed(username, None))
self.authenticator.check_whitelist(username, None)
)
): ):
raise ValueError("Username %r is not in whitelist" % username) raise ValueError(
"Username %r is not in Authenticator.allowed" % username
)
user = orm.User.find(db, name=username) user = orm.User.find(db, name=username)
if user is None: if user is None:
if not self.authenticator.validate_username(username): if not self.authenticator.validate_username(username):
@@ -1798,11 +1798,13 @@ class JupyterHub(Application):
if kind == 'user': if kind == 'user':
name = self.authenticator.normalize_username(name) name = self.authenticator.normalize_username(name)
if not ( if not (
await maybe_future(self.authenticator.check_whitelist(name, None)) await maybe_future(self.authenticator.check_allowed(name, None))
): ):
raise ValueError("Token name %r is not in whitelist" % name) raise ValueError(
"Token user name %r is not in Authenticator.allowed" % name
)
if not self.authenticator.validate_username(name): if not self.authenticator.validate_username(name):
raise ValueError("Token name %r is not valid" % name) raise ValueError("Token user name %r is not valid" % name)
if kind == 'service': if kind == 'service':
if not any(service["name"] == name for service in self.services): if not any(service["name"] == name for service in self.services):
self.log.warning( self.log.warning(
@@ -2183,14 +2185,14 @@ class JupyterHub(Application):
else: else:
version_hash = datetime.now().strftime("%Y%m%d%H%M%S") version_hash = datetime.now().strftime("%Y%m%d%H%M%S")
oauth_no_confirm_whitelist = set() oauth_no_confirm_list = set()
for service in self._service_map.values(): for service in self._service_map.values():
if service.oauth_no_confirm: if service.oauth_no_confirm:
self.log.warning( self.log.warning(
"Allowing service %s to complete OAuth without confirmation on an authorization web page", "Allowing service %s to complete OAuth without confirmation on an authorization web page",
service.name, service.name,
) )
oauth_no_confirm_whitelist.add(service.oauth_client_id) oauth_no_confirm_list.add(service.oauth_client_id)
settings = dict( settings = dict(
log_function=log_request, log_function=log_request,
@@ -2226,7 +2228,7 @@ class JupyterHub(Application):
default_server_name=self._default_server_name, default_server_name=self._default_server_name,
named_server_limit_per_user=self.named_server_limit_per_user, named_server_limit_per_user=self.named_server_limit_per_user,
oauth_provider=self.oauth_provider, oauth_provider=self.oauth_provider,
oauth_no_confirm_whitelist=oauth_no_confirm_whitelist, oauth_no_confirm_list=oauth_no_confirm_list,
concurrent_spawn_limit=self.concurrent_spawn_limit, concurrent_spawn_limit=self.concurrent_spawn_limit,
spawn_throttle_retry_range=self.spawn_throttle_retry_range, spawn_throttle_retry_range=self.spawn_throttle_retry_range,
active_server_limit=self.active_server_limit, active_server_limit=self.active_server_limit,

View File

@@ -7,6 +7,7 @@ import re
import sys import sys
import warnings import warnings
from concurrent.futures import ThreadPoolExecutor from concurrent.futures import ThreadPoolExecutor
from functools import partial
from shutil import which from shutil import which
from subprocess import PIPE from subprocess import PIPE
from subprocess import Popen from subprocess import Popen
@@ -100,41 +101,74 @@ class Authenticator(LoggingConfigurable):
""" """
).tag(config=True) ).tag(config=True)
whitelist = Set( whitelist = Set(help="Deprecated, use `Authenticator.allowed`", config=True,)
allowed = Set(
help=""" help="""
Whitelist of usernames that are allowed to log in. Set of usernames that are allowed to log in.
Use this with supported authenticators to restrict which users can log in. This is an Use this with supported authenticators to restrict which users can log in. This is an
additional whitelist that further restricts users, beyond whatever restrictions the additional list that further restricts users, beyond whatever restrictions the
authenticator has in place. authenticator has in place.
If empty, does not perform any additional restriction. If empty, does not perform any additional restriction.
.. versionchanged:: 1.2
`Authenticator.whitelist` renamed to `allowed`
""" """
).tag(config=True) ).tag(config=True)
blacklist = Set( blocked = Set(
help=""" help="""
Blacklist of usernames that are not allowed to log in. Set of usernames that are not allowed to log in.
Use this with supported authenticators to restrict which users can not log in. This is an Use this with supported authenticators to restrict which users can not log in. This is an
additional blacklist that further restricts users, beyond whatever restrictions the additional block list that further restricts users, beyond whatever restrictions the
authenticator has in place. authenticator has in place.
If empty, does not perform any additional restriction. If empty, does not perform any additional restriction.
.. versionadded: 0.9 .. versionadded: 0.9
.. versionchanged:: 1.2
`Authenticator.blacklist` renamed to `blocked`
""" """
).tag(config=True) ).tag(config=True)
@observe('whitelist') _deprecated_aliases = {
def _check_whitelist(self, change): "whitelist": ("allowed", "1.2"),
"blacklist": ("blocked", "1.2"),
}
@observe(*list(_deprecated_aliases))
def _deprecated_trait(self, change):
"""observer for deprecated traits"""
old_attr = change.name
new_attr, version = self._deprecated_aliases.get(old_attr)
new_value = getattr(self, new_attr)
if new_value != change.new:
# only warn if different
# protects backward-compatible config from warnings
# if they set the same value under both names
self.log.warning(
"{cls}.{old} is deprecated in JupyterHub {version}, use {cls}.{new} instead".format(
cls=self.__class__.__name__,
old=old_attr,
new=new_attr,
version=version,
)
)
setattr(self, new_attr, change.new)
@observe('allowed')
def _check_allowed(self, change):
short_names = [name for name in change['new'] if len(name) <= 1] short_names = [name for name in change['new'] if len(name) <= 1]
if short_names: if short_names:
sorted_names = sorted(short_names) sorted_names = sorted(short_names)
single = ''.join(sorted_names) single = ''.join(sorted_names)
string_set_typo = "set('%s')" % single string_set_typo = "set('%s')" % single
self.log.warning( self.log.warning(
"whitelist contains single-character names: %s; did you mean set([%r]) instead of %s?", "Allowed list contains single-character names: %s; did you mean set([%r]) instead of %s?",
sorted_names[:8], sorted_names[:8],
single, single,
string_set_typo, string_set_typo,
@@ -260,6 +294,8 @@ class Authenticator(LoggingConfigurable):
def __init__(self, **kwargs): def __init__(self, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
# TODO: properly handle deprecated signature *and* name
# with correct subclass override priority!
for method_name in ( for method_name in (
'check_whitelist', 'check_whitelist',
'check_blacklist', 'check_blacklist',
@@ -326,39 +362,45 @@ class Authenticator(LoggingConfigurable):
username = self.username_map.get(username, username) username = self.username_map.get(username, username)
return username return username
def check_whitelist(self, username, authentication=None): def check_allowed(self, username, authentication=None):
"""Check if a username is allowed to authenticate based on whitelist configuration """Check if a username is allowed to authenticate based on configuration
Return True if username is allowed, False otherwise. Return True if username is allowed, False otherwise.
No whitelist means any username is allowed. No allowed set means any username is allowed.
Names are normalized *before* being checked against the whitelist. Names are normalized *before* being checked against the allowed set.
.. versionchanged:: 1.0 .. versionchanged:: 1.0
Signature updated to accept authentication data and any future changes Signature updated to accept authentication data and any future changes
"""
if not self.whitelist:
# No whitelist means any name is allowed
return True
return username in self.whitelist
def check_blacklist(self, username, authentication=None): .. versionchanged:: 1.2
"""Check if a username is blocked to authenticate based on blacklist configuration Renamed check_whitelist to check_allowed
"""
if not self.allowed:
# No allowed set means any name is allowed
return True
return username in self.allowed
def check_blocked(self, username, authentication=None):
"""Check if a username is blocked to authenticate based on Authenticator.blocked configuration
Return True if username is allowed, False otherwise. Return True if username is allowed, False otherwise.
No blacklist means any username is allowed. No block list means any username is allowed.
Names are normalized *before* being checked against the blacklist. Names are normalized *before* being checked against the block list.
.. versionadded: 0.9 .. versionadded: 0.9
.. versionchanged:: 1.0 .. versionchanged:: 1.0
Signature updated to accept authentication data as second argument Signature updated to accept authentication data as second argument
.. versionchanged:: 1.2
Renamed check_blacklist to check_blocked
""" """
if not self.blacklist: if not self.blocked:
# No blacklist means any name is allowed # No block list means any name is allowed
return True return True
return username not in self.blacklist return username not in self.blocked
async def get_authenticated_user(self, handler, data): async def get_authenticated_user(self, handler, data):
"""Authenticate the user who is attempting to log in """Authenticate the user who is attempting to log in
@@ -367,7 +409,7 @@ class Authenticator(LoggingConfigurable):
This calls `authenticate`, which should be overridden in subclasses, This calls `authenticate`, which should be overridden in subclasses,
normalizes the username if any normalization should be done, normalizes the username if any normalization should be done,
and then validates the name in the whitelist. and then validates the name in the allowed set.
This is the outer API for authenticating a user. This is the outer API for authenticating a user.
Subclasses should not override this method. Subclasses should not override this method.
@@ -375,7 +417,7 @@ class Authenticator(LoggingConfigurable):
The various stages can be overridden separately: The various stages can be overridden separately:
- `authenticate` turns formdata into a username - `authenticate` turns formdata into a username
- `normalize_username` normalizes the username - `normalize_username` normalizes the username
- `check_whitelist` checks against the user whitelist - `check_allowed` checks against the user allowed
.. versionchanged:: 0.8 .. versionchanged:: 0.8
return dict instead of username return dict instead of username
@@ -389,7 +431,7 @@ class Authenticator(LoggingConfigurable):
else: else:
authenticated = {'name': authenticated} authenticated = {'name': authenticated}
authenticated.setdefault('auth_state', None) authenticated.setdefault('auth_state', None)
# Leave the default as None, but reevaluate later post-whitelist # Leave the default as None, but reevaluate later post-allowed-check
authenticated.setdefault('admin', None) authenticated.setdefault('admin', None)
# normalize the username # normalize the username
@@ -400,20 +442,16 @@ class Authenticator(LoggingConfigurable):
self.log.warning("Disallowing invalid username %r.", username) self.log.warning("Disallowing invalid username %r.", username)
return return
blacklist_pass = await maybe_future( blocked_pass = await maybe_future(self.check_blocked(username, authenticated))
self.check_blacklist(username, authenticated) allowed_pass = await maybe_future(self.check_allowed(username, authenticated))
)
whitelist_pass = await maybe_future(
self.check_whitelist(username, authenticated)
)
if blacklist_pass: if blocked_pass:
pass pass
else: else:
self.log.warning("User %r in blacklist. Stop authentication", username) self.log.warning("User %r blocked. Stop authentication", username)
return return
if whitelist_pass: if allowed_pass:
if authenticated['admin'] is None: if authenticated['admin'] is None:
authenticated['admin'] = await maybe_future( authenticated['admin'] = await maybe_future(
self.is_admin(handler, authenticated) self.is_admin(handler, authenticated)
@@ -423,7 +461,7 @@ class Authenticator(LoggingConfigurable):
return authenticated return authenticated
else: else:
self.log.warning("User %r not in whitelist.", username) self.log.warning("User %r not allowed.", username)
return return
async def refresh_user(self, user, handler=None): async def refresh_user(self, user, handler=None):
@@ -479,7 +517,7 @@ class Authenticator(LoggingConfigurable):
It must return the username on successful authentication, It must return the username on successful authentication,
and return None on failed authentication. and return None on failed authentication.
Checking the whitelist is handled separately by the caller. Checking allowed/blocked is handled separately by the caller.
.. versionchanged:: 0.8 .. versionchanged:: 0.8
Allow `authenticate` to return a dict containing auth_state. Allow `authenticate` to return a dict containing auth_state.
@@ -520,10 +558,10 @@ class Authenticator(LoggingConfigurable):
This method may be a coroutine. This method may be a coroutine.
By default, this just adds the user to the whitelist. By default, this just adds the user to the allowed set.
Subclasses may do more extensive things, such as adding actual unix users, Subclasses may do more extensive things, such as adding actual unix users,
but they should call super to ensure the whitelist is updated. but they should call super to ensure the allowed set is updated.
Note that this should be idempotent, since it is called whenever the hub restarts Note that this should be idempotent, since it is called whenever the hub restarts
for all users. for all users.
@@ -533,19 +571,19 @@ class Authenticator(LoggingConfigurable):
""" """
if not self.validate_username(user.name): if not self.validate_username(user.name):
raise ValueError("Invalid username: %s" % user.name) raise ValueError("Invalid username: %s" % user.name)
if self.whitelist: if self.allowed:
self.whitelist.add(user.name) self.allowed.add(user.name)
def delete_user(self, user): def delete_user(self, user):
"""Hook called when a user is deleted """Hook called when a user is deleted
Removes the user from the whitelist. Removes the user from the allowed set.
Subclasses should call super to ensure the whitelist is updated. Subclasses should call super to ensure the allowed set is updated.
Args: Args:
user (User): The User wrapper object user (User): The User wrapper object
""" """
self.whitelist.discard(user.name) self.allowed.discard(user.name)
auto_login = Bool( auto_login = Bool(
False, False,
@@ -610,6 +648,38 @@ class Authenticator(LoggingConfigurable):
return [('/login', LoginHandler)] return [('/login', LoginHandler)]
def _deprecated_method(old_name, new_name, version, self, *args, **kwargs):
"""Method wrapper for a deprecated method name"""
warnings.warn(
(
"{cls}.{old_name} is deprecated in JupyterHub {version}."
" Please use {cls}.{new_name} instead."
).format(
cls=self.__class__.__name__,
old_name=old_name,
new_name=new_name,
version=version,
),
DeprecationWarning,
stacklevel=2,
)
old_method = getattr(self, new_name)
return old_method(*args, **kwargs)
# deprecate white/blacklist method names
for _old_name, _new_name, _version in [
("check_whitelist", "check_allowed", "1.2"),
("check_blacklist", "check_blocked", "1.2"),
]:
setattr(
Authenticator,
_old_name,
partial(_deprecated_method, _old_name, _new_name, _version),
)
class LocalAuthenticator(Authenticator): class LocalAuthenticator(Authenticator):
"""Base class for Authenticators that work with local Linux/UNIX users """Base class for Authenticators that work with local Linux/UNIX users
@@ -669,37 +739,37 @@ class LocalAuthenticator(Authenticator):
""" """
).tag(config=True) ).tag(config=True)
group_whitelist = Set( group_whitelist = Set(help="""DEPRECATED: use allowed_groups""",).tag(config=True)
help="""
Whitelist all users from this UNIX group.
This makes the username whitelist ineffective. allowed_groups = Set(
help="""
Allow login from all users in these UNIX groups.
If set, allowed username set is ignored.
""" """
).tag(config=True) ).tag(config=True)
@observe('group_whitelist') @observe('allowed_groups')
def _group_whitelist_changed(self, change): def _allowed_groups_changed(self, change):
""" """Log a warning if mutually exclusive user and group allowed sets are specified."""
Log a warning if both group_whitelist and user whitelist are set. if self.allowed:
"""
if self.whitelist:
self.log.warning( self.log.warning(
"Ignoring username whitelist because group whitelist supplied!" "Ignoring Authenticator.allowed set because Authenticator.allowed_groups supplied!"
) )
def check_whitelist(self, username, authentication=None): def check_allowed(self, username, authentication=None):
if self.group_whitelist: if self.allowed_groups:
return self.check_group_whitelist(username, authentication) return self.check_allowed_groups(username, authentication)
else: else:
return super().check_whitelist(username, authentication) return super().check_allowed(username, authentication)
def check_group_whitelist(self, username, authentication=None): def check_allowed_groups(self, username, authentication=None):
""" """
If group_whitelist is configured, check if authenticating user is part of group. If allowed_groups is configured, check if authenticating user is part of group.
""" """
if not self.group_whitelist: if not self.allowed_groups:
return False return False
for grnam in self.group_whitelist: for grnam in self.allowed_groups:
try: try:
group = self._getgrnam(grnam) group = self._getgrnam(grnam)
except KeyError: except KeyError:
@@ -843,7 +913,7 @@ class PAMAuthenticator(LocalAuthenticator):
Authoritative list of user groups that determine admin access. Authoritative list of user groups that determine admin access.
Users not in these groups can still be granted admin status through admin_users. Users not in these groups can still be granted admin status through admin_users.
White/blacklisting rules still apply. allowed/blocked rules still apply.
""" """
).tag(config=True) ).tag(config=True)
@@ -986,6 +1056,16 @@ class PAMAuthenticator(LocalAuthenticator):
return super().normalize_username(username) return super().normalize_username(username)
for _old_name, _new_name, _version in [
("check_group_whitelist", "check_group_allowed", "1.2"),
]:
setattr(
LocalAuthenticator,
_old_name,
partial(_deprecated_method, _old_name, _new_name, _version),
)
class DummyAuthenticator(Authenticator): class DummyAuthenticator(Authenticator):
"""Dummy Authenticator for testing """Dummy Authenticator for testing

View File

@@ -860,15 +860,15 @@ class HubAuthenticated(object):
if kind == 'service': if kind == 'service':
# it's a service, check hub_services # it's a service, check hub_services
if self.hub_services and name in self.hub_services: if self.hub_services and name in self.hub_services:
app_log.debug("Allowing whitelisted Hub service %s", name) app_log.debug("Allowing Hub service %s", name)
return model return model
else: else:
app_log.warning("Not allowing Hub service %s", name) app_log.warning("Not allowing Hub service %s", name)
raise UserNotAllowed(model) raise UserNotAllowed(model)
if self.hub_users and name in self.hub_users: if self.hub_users and name in self.hub_users:
# user in whitelist # user in allowed list
app_log.debug("Allowing whitelisted Hub user %s", name) app_log.debug("Allowing Hub user %s", name)
return model return model
elif self.hub_groups and set(model['groups']).intersection(self.hub_groups): elif self.hub_groups and set(model['groups']).intersection(self.hub_groups):
allowed_groups = set(model['groups']).intersection(self.hub_groups) allowed_groups = set(model['groups']).intersection(self.hub_groups)
@@ -877,7 +877,7 @@ class HubAuthenticated(object):
name, name,
','.join(sorted(allowed_groups)), ','.join(sorted(allowed_groups)),
) )
# group in whitelist # group in allowed list
return model return model
else: else:
app_log.warning("Not allowing Hub user %s", name) app_log.warning("Not allowing Hub user %s", name)

View File

@@ -435,9 +435,9 @@ class Spawner(LoggingConfigurable):
'LC_ALL', 'LC_ALL',
], ],
help=""" help="""
Whitelist of environment variables for the single-user server to inherit from the JupyterHub process. List of environment variables for the single-user server to inherit from the JupyterHub process.
This whitelist is used to ensure that sensitive information in the JupyterHub process's environment This list is used to ensure that sensitive information in the JupyterHub process's environment
(such as `CONFIGPROXY_AUTH_TOKEN`) is not passed to the single-user server's process. (such as `CONFIGPROXY_AUTH_TOKEN`) is not passed to the single-user server's process.
""", """,
).tag(config=True) ).tag(config=True)
@@ -456,7 +456,7 @@ class Spawner(LoggingConfigurable):
Environment variables that end up in the single-user server's process come from 3 sources: Environment variables that end up in the single-user server's process come from 3 sources:
- This `environment` configurable - This `environment` configurable
- The JupyterHub process' environment variables that are whitelisted in `env_keep` - The JupyterHub process' environment variables that are listed in `env_keep`
- Variables to establish contact between the single-user notebook and the hub (such as JUPYTERHUB_API_TOKEN) - Variables to establish contact between the single-user notebook and the hub (such as JUPYTERHUB_API_TOKEN)
The `environment` configurable should be set by JupyterHub administrators to add The `environment` configurable should be set by JupyterHub administrators to add

View File

@@ -93,7 +93,7 @@ def test_generate_config():
os.remove(cfg_file) os.remove(cfg_file)
assert cfg_file in out assert cfg_file in out
assert 'Spawner.cmd' in cfg_text assert 'Spawner.cmd' in cfg_text
assert 'Authenticator.whitelist' in cfg_text assert 'Authenticator.allowed' in cfg_text
async def test_init_tokens(request): async def test_init_tokens(request):

View File

@@ -6,6 +6,7 @@ from unittest import mock
import pytest import pytest
from requests import HTTPError from requests import HTTPError
from traitlets.config import Config
from .mocking import MockPAMAuthenticator from .mocking import MockPAMAuthenticator
from .mocking import MockStructGroup from .mocking import MockStructGroup
@@ -137,8 +138,8 @@ async def test_pam_auth_admin_groups():
assert authorized['admin'] is False assert authorized['admin'] is False
async def test_pam_auth_whitelist(): async def test_pam_auth_allowed():
authenticator = MockPAMAuthenticator(whitelist={'wash', 'kaylee'}) authenticator = MockPAMAuthenticator(allowed={'wash', 'kaylee'})
authorized = await authenticator.get_authenticated_user( authorized = await authenticator.get_authenticated_user(
None, {'username': 'kaylee', 'password': 'kaylee'} None, {'username': 'kaylee', 'password': 'kaylee'}
) )
@@ -155,11 +156,11 @@ async def test_pam_auth_whitelist():
assert authorized is None assert authorized is None
async def test_pam_auth_group_whitelist(): async def test_pam_auth_allowed_groups():
def getgrnam(name): def getgrnam(name):
return MockStructGroup('grp', ['kaylee']) return MockStructGroup('grp', ['kaylee'])
authenticator = MockPAMAuthenticator(group_whitelist={'group'}) authenticator = MockPAMAuthenticator(allowed_groups={'group'})
with mock.patch.object(authenticator, '_getgrnam', getgrnam): with mock.patch.object(authenticator, '_getgrnam', getgrnam):
authorized = await authenticator.get_authenticated_user( authorized = await authenticator.get_authenticated_user(
@@ -174,7 +175,7 @@ async def test_pam_auth_group_whitelist():
assert authorized is None assert authorized is None
async def test_pam_auth_blacklist(): async def test_pam_auth_blocked():
# Null case compared to next case # Null case compared to next case
authenticator = MockPAMAuthenticator() authenticator = MockPAMAuthenticator()
authorized = await authenticator.get_authenticated_user( authorized = await authenticator.get_authenticated_user(
@@ -183,50 +184,41 @@ async def test_pam_auth_blacklist():
assert authorized['name'] == 'wash' assert authorized['name'] == 'wash'
# Blacklist basics # Blacklist basics
authenticator = MockPAMAuthenticator(blacklist={'wash'}) authenticator = MockPAMAuthenticator(blocked={'wash'})
authorized = await authenticator.get_authenticated_user( authorized = await authenticator.get_authenticated_user(
None, {'username': 'wash', 'password': 'wash'} None, {'username': 'wash', 'password': 'wash'}
) )
assert authorized is None assert authorized is None
# User in both white and blacklists: default deny. Make error someday? # User in both allowed and blocked: default deny. Make error someday?
authenticator = MockPAMAuthenticator( authenticator = MockPAMAuthenticator(blocked={'wash'}, allowed={'wash', 'kaylee'})
blacklist={'wash'}, whitelist={'wash', 'kaylee'}
)
authorized = await authenticator.get_authenticated_user( authorized = await authenticator.get_authenticated_user(
None, {'username': 'wash', 'password': 'wash'} None, {'username': 'wash', 'password': 'wash'}
) )
assert authorized is None assert authorized is None
# User not in blacklist can log in # User not in blocked set can log in
authenticator = MockPAMAuthenticator( authenticator = MockPAMAuthenticator(blocked={'wash'}, allowed={'wash', 'kaylee'})
blacklist={'wash'}, whitelist={'wash', 'kaylee'}
)
authorized = await authenticator.get_authenticated_user( authorized = await authenticator.get_authenticated_user(
None, {'username': 'kaylee', 'password': 'kaylee'} None, {'username': 'kaylee', 'password': 'kaylee'}
) )
assert authorized['name'] == 'kaylee' assert authorized['name'] == 'kaylee'
# User in whitelist, blacklist irrelevent # User in allowed, blocked irrelevent
authenticator = MockPAMAuthenticator( authenticator = MockPAMAuthenticator(blocked={'mal'}, allowed={'wash', 'kaylee'})
blacklist={'mal'}, whitelist={'wash', 'kaylee'}
)
authorized = await authenticator.get_authenticated_user( authorized = await authenticator.get_authenticated_user(
None, {'username': 'wash', 'password': 'wash'} None, {'username': 'wash', 'password': 'wash'}
) )
assert authorized['name'] == 'wash' assert authorized['name'] == 'wash'
# User in neither list # User in neither list
authenticator = MockPAMAuthenticator( authenticator = MockPAMAuthenticator(blocked={'mal'}, allowed={'wash', 'kaylee'})
blacklist={'mal'}, whitelist={'wash', 'kaylee'}
)
authorized = await authenticator.get_authenticated_user( authorized = await authenticator.get_authenticated_user(
None, {'username': 'simon', 'password': 'simon'} None, {'username': 'simon', 'password': 'simon'}
) )
assert authorized is None assert authorized is None
# blacklist == {} authenticator = MockPAMAuthenticator(blocked=set(), allowed={'wash', 'kaylee'})
authenticator = MockPAMAuthenticator(blacklist=set(), whitelist={'wash', 'kaylee'})
authorized = await authenticator.get_authenticated_user( authorized = await authenticator.get_authenticated_user(
None, {'username': 'kaylee', 'password': 'kaylee'} None, {'username': 'kaylee', 'password': 'kaylee'}
) )
@@ -253,7 +245,7 @@ async def test_deprecated_signatures():
async def test_pam_auth_no_such_group(): async def test_pam_auth_no_such_group():
authenticator = MockPAMAuthenticator(group_whitelist={'nosuchcrazygroup'}) authenticator = MockPAMAuthenticator(allowed_groups={'nosuchcrazygroup'})
authorized = await authenticator.get_authenticated_user( authorized = await authenticator.get_authenticated_user(
None, {'username': 'kaylee', 'password': 'kaylee'} None, {'username': 'kaylee', 'password': 'kaylee'}
) )
@@ -262,7 +254,7 @@ async def test_pam_auth_no_such_group():
async def test_wont_add_system_user(): async def test_wont_add_system_user():
user = orm.User(name='lioness4321') user = orm.User(name='lioness4321')
authenticator = auth.PAMAuthenticator(whitelist={'mal'}) authenticator = auth.PAMAuthenticator(allowed={'mal'})
authenticator.create_system_users = False authenticator.create_system_users = False
with pytest.raises(KeyError): with pytest.raises(KeyError):
await authenticator.add_user(user) await authenticator.add_user(user)
@@ -270,7 +262,7 @@ async def test_wont_add_system_user():
async def test_cant_add_system_user(): async def test_cant_add_system_user():
user = orm.User(name='lioness4321') user = orm.User(name='lioness4321')
authenticator = auth.PAMAuthenticator(whitelist={'mal'}) authenticator = auth.PAMAuthenticator(allowed={'mal'})
authenticator.add_user_cmd = ['jupyterhub-fake-command'] authenticator.add_user_cmd = ['jupyterhub-fake-command']
authenticator.create_system_users = True authenticator.create_system_users = True
@@ -296,7 +288,7 @@ async def test_cant_add_system_user():
async def test_add_system_user(): async def test_add_system_user():
user = orm.User(name='lioness4321') user = orm.User(name='lioness4321')
authenticator = auth.PAMAuthenticator(whitelist={'mal'}) authenticator = auth.PAMAuthenticator(allowed={'mal'})
authenticator.create_system_users = True authenticator.create_system_users = True
authenticator.add_user_cmd = ['echo', '/home/USERNAME'] authenticator.add_user_cmd = ['echo', '/home/USERNAME']
@@ -317,13 +309,13 @@ async def test_add_system_user():
async def test_delete_user(): async def test_delete_user():
user = orm.User(name='zoe') user = orm.User(name='zoe')
a = MockPAMAuthenticator(whitelist={'mal'}) a = MockPAMAuthenticator(allowed={'mal'})
assert 'zoe' not in a.whitelist assert 'zoe' not in a.allowed
await a.add_user(user) await a.add_user(user)
assert 'zoe' in a.whitelist assert 'zoe' in a.allowed
a.delete_user(user) a.delete_user(user)
assert 'zoe' not in a.whitelist assert 'zoe' not in a.allowed
def test_urls(): def test_urls():
@@ -461,3 +453,10 @@ async def test_post_auth_hook():
) )
assert authorized['testkey'] == 'testvalue' assert authorized['testkey'] == 'testvalue'
async def test_deprecations():
cfg = Config()
cfg.Authenticator.whitelist = {'user'}
authenticator = auth.Authenticator(config=cfg)
assert authenticator.allowed == {'user'}

View File

@@ -686,7 +686,7 @@ async def test_shutdown_on_logout(app, shutdown_on_logout):
assert spawner.ready == (not shutdown_on_logout) assert spawner.ready == (not shutdown_on_logout)
async def test_login_no_whitelist_adds_user(app): async def test_login_no_allowed_adds_user(app):
auth = app.authenticator auth = app.authenticator
mock_add_user = mock.Mock() mock_add_user = mock.Mock()
with mock.patch.object(auth, 'add_user', mock_add_user): with mock.patch.object(auth, 'add_user', mock_add_user):

View File

@@ -185,7 +185,7 @@ def test_hub_authenticated(request):
m.get(good_url, text=json.dumps(mock_model)) m.get(good_url, text=json.dumps(mock_model))
# no whitelist # no specific allowed user
r = requests.get( r = requests.get(
'http://127.0.0.1:%i' % port, 'http://127.0.0.1:%i' % port,
cookies={'jubal': 'early'}, cookies={'jubal': 'early'},
@@ -194,7 +194,7 @@ def test_hub_authenticated(request):
r.raise_for_status() r.raise_for_status()
assert r.status_code == 200 assert r.status_code == 200
# pass whitelist # pass allowed user
TestHandler.hub_users = {'jubalearly'} TestHandler.hub_users = {'jubalearly'}
r = requests.get( r = requests.get(
'http://127.0.0.1:%i' % port, 'http://127.0.0.1:%i' % port,
@@ -204,7 +204,7 @@ def test_hub_authenticated(request):
r.raise_for_status() r.raise_for_status()
assert r.status_code == 200 assert r.status_code == 200
# no pass whitelist # no pass allowed ser
TestHandler.hub_users = {'kaylee'} TestHandler.hub_users = {'kaylee'}
r = requests.get( r = requests.get(
'http://127.0.0.1:%i' % port, 'http://127.0.0.1:%i' % port,
@@ -213,7 +213,7 @@ def test_hub_authenticated(request):
) )
assert r.status_code == 403 assert r.status_code == 403
# pass group whitelist # pass allowed group
TestHandler.hub_groups = {'lions'} TestHandler.hub_groups = {'lions'}
r = requests.get( r = requests.get(
'http://127.0.0.1:%i' % port, 'http://127.0.0.1:%i' % port,
@@ -223,7 +223,7 @@ def test_hub_authenticated(request):
r.raise_for_status() r.raise_for_status()
assert r.status_code == 200 assert r.status_code == 200
# no pass group whitelist # no pass allowed group
TestHandler.hub_groups = {'tigers'} TestHandler.hub_groups = {'tigers'}
r = requests.get( r = requests.get(
'http://127.0.0.1:%i' % port, 'http://127.0.0.1:%i' % port,