Merge pull request #3090 from minrk/words-matter

This commit is contained in:
Min RK
2020-07-03 12:27:08 +02:00
committed by GitHub
15 changed files with 345 additions and 168 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_users`", config=True,)
allowed_users = 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_users`
"""
).tag(config=True)
blacklist = Set(
blocked_users = 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_users`
"""
).tag(config=True)
@observe('whitelist')
def _check_whitelist(self, change):
_deprecated_aliases = {
"whitelist": ("allowed_users", "1.2"),
"blacklist": ("blocked_users", "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_users')
def _check_allowed_users(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 set contains single-character names: %s; did you mean set([%r]) instead of %s?",
sorted_names[:8],
single,
string_set_typo,
@@ -261,39 +295,74 @@ class Authenticator(LoggingConfigurable):
def __init__(self, **kwargs):
super().__init__(**kwargs)
for method_name in (
'check_whitelist',
'check_blacklist',
'check_group_whitelist',
self._init_deprecated_methods()
def _init_deprecated_methods(self):
# handles deprecated signature *and* name
# with correct subclass override priority!
for old_name, new_name in (
('check_whitelist', 'check_allowed'),
('check_blacklist', 'check_blocked_users'),
('check_group_whitelist', 'check_allowed_groups'),
):
original_method = getattr(self, method_name, None)
if original_method is None:
old_method = getattr(self, old_name, None)
if old_method is None:
# no such method (check_group_whitelist is optional)
continue
signature = inspect.signature(original_method)
if 'authentication' not in signature.parameters:
# allow old name to have higher priority
# if and only if it's defined in a later subclass
# than the new name
for cls in self.__class__.mro():
has_old_name = old_name in cls.__dict__
has_new_name = new_name in cls.__dict__
if has_new_name:
break
if has_old_name and not has_new_name:
warnings.warn(
"{0}.{1} should be renamed to {0}.{2} for JupyterHub >= 1.2".format(
cls.__name__, old_name, new_name
),
DeprecationWarning,
)
# use old name instead of new
# if old name is overridden in subclass
def _new_calls_old(old_name, *args, **kwargs):
return getattr(self, old_name)(*args, **kwargs)
setattr(self, new_name, partial(_new_calls_old, old_name))
break
# deprecate pre-1.0 method signatures
signature = inspect.signature(old_method)
if 'authentication' not in signature.parameters and not any(
param.kind == inspect.Parameter.VAR_KEYWORD
for param in signature.parameters.values()
):
# adapt to pre-1.0 signature for compatibility
warnings.warn(
"""
{0}.{1} does not support the authentication argument,
added in JupyterHub 1.0.
added in JupyterHub 1.0. and is renamed to {2} in JupyterHub 1.2.
It should have the signature:
def {1}(self, username, authentication=None):
def {2}(self, username, authentication=None):
...
Adapting for compatibility.
""".format(
self.__class__.__name__, method_name
self.__class__.__name__, old_name, new_name
),
DeprecationWarning,
)
def wrapped_method(username, authentication=None, **kwargs):
def wrapped_method(
original_method, username, authentication=None, **kwargs
):
return original_method(username, **kwargs)
setattr(self, method_name, wrapped_method)
setattr(self, old_name, partial(wrapped_method, old_method))
async def run_post_auth_hook(self, handler, authentication):
"""
@@ -327,39 +396,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_users 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_users:
# No allowed set means any name is allowed
return True
return username in self.allowed_users
def check_blocked_users(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_users
"""
if not self.blacklist:
# No blacklist means any name is allowed
if not self.blocked_users:
# No block list means any name is allowed
return True
return username not in self.blacklist
return username not in self.blocked_users
async def get_authenticated_user(self, handler, data):
"""Authenticate the user who is attempting to log in
@@ -368,7 +443,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.
@@ -376,7 +451,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 allowed usernames
.. versionchanged:: 0.8
return dict instead of username
@@ -390,7 +465,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
@@ -401,20 +476,18 @@ 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_users(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)
@@ -424,7 +497,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):
@@ -480,7 +553,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_users/blocked_users is handled separately by the caller.
.. versionchanged:: 0.8
Allow `authenticate` to return a dict containing auth_state.
@@ -521,10 +594,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_users 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_users set is updated.
Note that this should be idempotent, since it is called whenever the hub restarts
for all users.
@@ -534,19 +607,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_users:
self.allowed_users.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_users set.
Subclasses should call super to ensure the allowed_users set is updated.
Args:
user (User): The User wrapper object
"""
self.whitelist.discard(user.name)
self.allowed_users.discard(user.name)
auto_login = Bool(
False,
@@ -611,6 +684,41 @@ class Authenticator(LoggingConfigurable):
return [('/login', LoginHandler)]
def _deprecated_method(old_name, new_name, version):
"""Create a deprecated method wrapper for a deprecated method name"""
def deprecated(self, *args, **kwargs):
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)
return deprecated
import types
# deprecate white/blacklist method names
for _old_name, _new_name, _version in [
("check_whitelist", "check_allowed", "1.2"),
("check_blacklist", "check_blocked_users", "1.2"),
]:
setattr(
Authenticator, _old_name, _deprecated_method(_old_name, _new_name, _version),
)
class LocalAuthenticator(Authenticator):
"""Base class for Authenticators that work with local Linux/UNIX users
@@ -670,37 +778,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_users:
self.log.warning(
"Ignoring username whitelist because group whitelist supplied!"
"Ignoring Authenticator.allowed_users 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:
@@ -844,7 +952,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)
@@ -987,6 +1095,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,
_deprecated_method(_old_name, _new_name, _version),
)
class DummyAuthenticator(Authenticator):
"""Dummy Authenticator for testing