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

@@ -7,6 +7,7 @@ import re
import sys
import warnings
from concurrent.futures import ThreadPoolExecutor
from functools import partial
from shutil import which
from subprocess import PIPE
from subprocess import Popen
@@ -100,41 +101,74 @@ class Authenticator(LoggingConfigurable):
"""
).tag(config=True)
whitelist = Set(
whitelist = Set(help="Deprecated, use `Authenticator.allowed`", config=True,)
allowed = Set(
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
additional whitelist that further restricts users, beyond whatever restrictions the
additional list that further restricts users, beyond whatever restrictions the
authenticator has in place.
If empty, does not perform any additional restriction.
.. versionchanged:: 1.2
`Authenticator.whitelist` renamed to `allowed`
"""
).tag(config=True)
blacklist = Set(
blocked = Set(
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
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.
If empty, does not perform any additional restriction.
.. versionadded: 0.9
.. versionchanged:: 1.2
`Authenticator.blacklist` renamed to `blocked`
"""
).tag(config=True)
@observe('whitelist')
def _check_whitelist(self, change):
_deprecated_aliases = {
"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]
if short_names:
sorted_names = sorted(short_names)
single = ''.join(sorted_names)
string_set_typo = "set('%s')" % single
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],
single,
string_set_typo,
@@ -260,6 +294,8 @@ class Authenticator(LoggingConfigurable):
def __init__(self, **kwargs):
super().__init__(**kwargs)
# TODO: properly handle deprecated signature *and* name
# with correct subclass override priority!
for method_name in (
'check_whitelist',
'check_blacklist',
@@ -326,39 +362,45 @@ class Authenticator(LoggingConfigurable):
username = self.username_map.get(username, username)
return username
def check_whitelist(self, username, authentication=None):
"""Check if a username is allowed to authenticate based on whitelist configuration
def check_allowed(self, username, authentication=None):
"""Check if a username is allowed to authenticate based on configuration
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
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):
"""Check if a username is blocked to authenticate based on blacklist configuration
.. versionchanged:: 1.2
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.
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
.. versionchanged:: 1.0
Signature updated to accept authentication data as second argument
.. versionchanged:: 1.2
Renamed check_blacklist to check_blocked
"""
if not self.blacklist:
# No blacklist means any name is allowed
if not self.blocked:
# No block list means any name is allowed
return True
return username not in self.blacklist
return username not in self.blocked
async def get_authenticated_user(self, handler, data):
"""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,
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.
Subclasses should not override this method.
@@ -375,7 +417,7 @@ class Authenticator(LoggingConfigurable):
The various stages can be overridden separately:
- `authenticate` turns formdata into a username
- `normalize_username` normalizes the username
- `check_whitelist` checks against the user whitelist
- `check_allowed` checks against the user allowed
.. versionchanged:: 0.8
return dict instead of username
@@ -389,7 +431,7 @@ class Authenticator(LoggingConfigurable):
else:
authenticated = {'name': authenticated}
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)
# normalize the username
@@ -400,20 +442,16 @@ class Authenticator(LoggingConfigurable):
self.log.warning("Disallowing invalid username %r.", username)
return
blacklist_pass = await maybe_future(
self.check_blacklist(username, authenticated)
)
whitelist_pass = await maybe_future(
self.check_whitelist(username, authenticated)
)
blocked_pass = await maybe_future(self.check_blocked(username, authenticated))
allowed_pass = await maybe_future(self.check_allowed(username, authenticated))
if blacklist_pass:
if blocked_pass:
pass
else:
self.log.warning("User %r in blacklist. Stop authentication", username)
self.log.warning("User %r blocked. Stop authentication", username)
return
if whitelist_pass:
if allowed_pass:
if authenticated['admin'] is None:
authenticated['admin'] = await maybe_future(
self.is_admin(handler, authenticated)
@@ -423,7 +461,7 @@ class Authenticator(LoggingConfigurable):
return authenticated
else:
self.log.warning("User %r not in whitelist.", username)
self.log.warning("User %r not allowed.", username)
return
async def refresh_user(self, user, handler=None):
@@ -479,7 +517,7 @@ class Authenticator(LoggingConfigurable):
It must return the username on successful 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
Allow `authenticate` to return a dict containing auth_state.
@@ -520,10 +558,10 @@ class Authenticator(LoggingConfigurable):
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,
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
for all users.
@@ -533,19 +571,19 @@ class Authenticator(LoggingConfigurable):
"""
if not self.validate_username(user.name):
raise ValueError("Invalid username: %s" % user.name)
if self.whitelist:
self.whitelist.add(user.name)
if self.allowed:
self.allowed.add(user.name)
def delete_user(self, user):
"""Hook called when a user is deleted
Removes the user from the whitelist.
Subclasses should call super to ensure the whitelist is updated.
Removes the user from the allowed set.
Subclasses should call super to ensure the allowed set is updated.
Args:
user (User): The User wrapper object
"""
self.whitelist.discard(user.name)
self.allowed.discard(user.name)
auto_login = Bool(
False,
@@ -610,6 +648,38 @@ class Authenticator(LoggingConfigurable):
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):
"""Base class for Authenticators that work with local Linux/UNIX users
@@ -669,37 +739,37 @@ class LocalAuthenticator(Authenticator):
"""
).tag(config=True)
group_whitelist = Set(
help="""
Whitelist all users from this UNIX group.
group_whitelist = Set(help="""DEPRECATED: use allowed_groups""",).tag(config=True)
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)
@observe('group_whitelist')
def _group_whitelist_changed(self, change):
"""
Log a warning if both group_whitelist and user whitelist are set.
"""
if self.whitelist:
@observe('allowed_groups')
def _allowed_groups_changed(self, change):
"""Log a warning if mutually exclusive user and group allowed sets are specified."""
if self.allowed:
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):
if self.group_whitelist:
return self.check_group_whitelist(username, authentication)
def check_allowed(self, username, authentication=None):
if self.allowed_groups:
return self.check_allowed_groups(username, authentication)
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
for grnam in self.group_whitelist:
for grnam in self.allowed_groups:
try:
group = self._getgrnam(grnam)
except KeyError:
@@ -843,7 +913,7 @@ class PAMAuthenticator(LocalAuthenticator):
Authoritative list of user groups that determine admin access.
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)
@@ -986,6 +1056,16 @@ class PAMAuthenticator(LocalAuthenticator):
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):
"""Dummy Authenticator for testing