set allow_all=False by default

This commit is contained in:
Min RK
2024-03-22 15:46:03 +01:00
parent e1e34a14a2
commit c3c69027fa
6 changed files with 333 additions and 140 deletions

View File

@@ -41,6 +41,11 @@ When testing, it may be helpful to use the
password unless a global password has been set. Once set, any username will
still be accepted but the correct password will need to be provided.
:::{versionadded} 5.0
The DummyAuthenticator's default `allow_all` is True,
unlike most other Authenticators.
:::
## Additional Authenticators
Additional authenticators can be found on GitHub
@@ -81,6 +86,7 @@ Writing an Authenticator that looks up passwords in a dictionary
requires only overriding this one method:
```python
from secrets import compare_digest
from traitlets import Dict
from jupyterhub.auth import Authenticator
@@ -91,8 +97,14 @@ class DictionaryAuthenticator(Authenticator):
)
async def authenticate(self, handler, data):
if self.passwords.get(data['username']) == data['password']:
return data['username']
username = data["username"]
password = data["password"]
check_password = self.passwords.get(username, "")
# always call compare_digest, for timing attacks
if compare_digest(check_password, password) and username in self.passwords:
return username
else:
return None
```
#### Normalize usernames
@@ -190,10 +202,10 @@ via `jupyterhub --generate-config`.
When dealing with logging in, there are generally two _separate_ steps:
authentication
: identifying who is logged in, and
: identifying who is trying to log in, and
authorization
: deciding whether an authenticated user is logged in
: deciding whether an authenticated user is allowed to access your JupyterHub
{meth}`Authenticator.authenticate` is responsible for authenticating users.
It is perfectly fine in the simplest cases for `Authenticator.authenticate` to be responsible for authentication _and_ authorization,
@@ -203,71 +215,137 @@ However, Authenticators also have have two methods {meth}`~.Authenticator.check_
If `check_blocked_users()` returns False, authorization stops and the user is not allowed.
If `check_allowed()` returns True, authorization proceeds.
If `Authenticator.allow_all` is True OR `check_allowed()` returns True, authorization proceeds.
:::{versionadded} 5.0
{attr}`Authenticator.allow_all` and {attr}`Authenticator.allow_existing_users` are new in JupyterHub 5.0.
{attr}`.Authenticator.allow_all` and {attr}`.Authenticator.allow_existing_users` are new in JupyterHub 5.0.
By default, `allow_all` is True when `allowed_users` is empty,
and `allow_existing_users` is True when `allowed_users` is not empty.
This is to ensure backward-compatibility, but subclasses are free to pick more restrictive defaults.
By default, `allow_all` is False,
which is a change from pre-5.0, where `allow_all` was implicitly True if `allowed_users` was empty.
:::
### Overriding `check_allowed`
:::{versionchanged} 5.0
`check_allowed()` is **not called** is `allow_all` is True.
:::
:::{versionchanged} 5.0
Starting with 5.0, `check_allowed()` should **NOT** return True if no allow config
is specified (`allow_all` should be used instead).
:::
The base implementation of {meth}`~.Authenticator.check_allowed` checks:
- if `allow_all` is True, return True
- if username is in the `allowed_users` set, return True
- else return False
If a custom Authenticator defines additional sources of `allow` configuration,
such as membership in a group or other information,
it should override `check_allowed` to account for this.
`allow_` configuration should generally be _additive_,
i.e. if permission is granted by _any_ allow configuration,
a user should be authorized.
:::{note}
For backward-compatibility, it is the responsibility of `Authenticator.check_allowed()` to check `.allow_all`.
This is to avoid the backward-compatible default values from granting permissions unexpectedly.
:::
If an Authenticator defines additional `allow` configuration, it must at least:
1. override `check_allowed`, and
2. override the default for `allow_all`
The default for `allow_all` in a custom authenticator should be one of `False` or a dynamic default matching something like `if not any allow configuration specified`.
False is recommended for authenticators which source much larger pools of users than are _typically_ allowed to access a Hub (e.g. generic OAuth providers like Google, GitHub, etc.).
For example, here is how `PAMAuthenticator` extends the base class to add `allowed_groups`:
:::{versionchanged} 5.0
Prior to 5.0, the check was
```python
from traitlets import default
@default("allow_all")
def _allow_all_default(self):
if self.allowed_users or self.allowed_groups:
# if any allow config is specified, default to False
return False
return True
def check_allowed(self, username, authentication=None):
if self.allow_all:
return True
if self.check_allowed_groups(username, authentication):
return True
return super().check_allowed(username, authentication)
if (not allowed_users) or username in allowed_users:
```
Important points to note:
but the implicit `not allowed_users` has been replaced by explicit `allow_all`, which is checked _before_ calling `check_allowed`.
`check_allowed` **is not called** if `allow_all` is True.
- overriding the default for `allow_all` is required to avoid `allow_all` being True when `allowed_groups` is specified, but `allowed_users` is not.
- `allow_all` must be checked inside `check_allowed`
- `allowed_groups` strictly expands who is authorized,
it does not apply restrictions `allowed_users`.
This is recommended for all `allow_` configuration added by Authenticators.
If your Authenticator subclass similarly returns True when no allow config is defined,
this is fully backward compatible for your users, but means `allow_all = False` has no real effect.
You can make your Authenticator forward-compatible with JupyterHub 5 by defining `allow_all` as a boolean config trait on your class:
```python
class MyAuthenticator(Authenticator):
# backport allow_all from JupyterHub 5
allow_all = Bool(False, config=True)
def check_allowed(self, username, authentication):
if self.allow_all:
# replaces previous "if no auth config"
return True
...
```
:::
If an Authenticator defines additional sources of `allow` configuration,
such as membership in a group or other information,
it should override `check_allowed` to account for this.
:::{note}
`allow_` configuration should generally be _additive_,
i.e. if access is granted by _any_ allow configuration,
a user should be authorized.
JupyterHub recommends that Authenticators applying _restrictive_ configuration should use names like `block_` or `require_`,
and check this during `check_blocked_users` or `authenticate`, not `check_allowed`.
:::
In general, an Authenticator's skeleton should look like:
```python
class MyAuthenticator(Authenticator):
# backport allow_all for compatibility with JupyterHub < 5
allow_all = Bool(False, config=True)
require_something = List(config=True)
allowed_something = Set()
def authenticate(self, data, handler):
...
if success:
return {"username": username, "auth_state": {...}}
else:
return None
def check_blocked_users(self, username, authentication=None):
"""Apply _restrictive_ configuration"""
if self.require_something and not has_something(username, self.request_):
return False
# repeat for each restriction
if restriction_defined and restriction_not_met:
return False
return super().check_blocked_users(self, username, authentication)
def check_allowed(self, username, authentication=None):
"""Apply _permissive_ configuration
Only called if check_blocked_users returns True
AND allow_all is False
"""
if self.allow_all:
# check here to backport allow_all behavior
# from JupyterHub 5
# this branch will never be taken with jupyterhub >=5
return True
if self.allowed_something and user_has_something(username):
return True
# repeat for each allow
if allow_config and allow_met:
return True
# should always have this at the end
if self.allowed_users and username in self.allowed_users:
return True
# do not call super!
# super().check_allowed is not safe with JupyterHub < 5.0,
# as it will return True if allowed_users is empty
return False
```
Key points:
- `allow_all` is backported from JupyterHub 5, for consistent behavior in all versions of JupyterHub (optional)
- restrictive configuration is checked in `check_blocked_users`
- if any restriction is not met, `check_blocked_users` returns False
- permissive configuration is checked in `check_allowed`
- if any `allow` condition is met, `check_allowed` returns True
So the logical expression for a user being authorized should look like:
> if ALL restrictions are met AND ANY admissions are met: user is authorized
#### Custom error messages

View File

@@ -6,30 +6,57 @@ The default Authenticator uses [PAM][] (Pluggable Authentication Module) to auth
their usernames and passwords. With the default Authenticator, any user
with an account and password on the system will be allowed to login.
## Create a set of allowed users (`allowed_users`)
## Deciding who is allowed
In the base Authenticator, there are 3 configuration options for granting users access to your Hub:
1. `allow_all` grants any user who can successfully authenticate access to the Hub
2. `allowed_users` defines a set of users who can access the Hub
3. `allow_existing_users` enables managing users via the JupyterHub API or admin page
These options should apply to all Authenticators.
Your chosen Authenticator may add additional configuration options to admit users, such as team membership, course enrollment, etc.
:::{important}
You should always specify at least one allow configuration if you want people to be able to access your Hub!
In most cases, this looks like:
```python
c.Authenticator.allow_all = True
# or
c.Authenticator.allowed_users = {"name", ...}
```
:::
:::{versionchanged} 5.0
If no allow config is specified, then by default **nobody will have access to your Hub**.
Prior to 5.0, the opposite was true; effectively `allow_all = True` if no other allow config was specified.
:::
You can restrict which users are allowed to login with a set,
`Authenticator.allowed_users`:
```python
c.Authenticator.allowed_users = {'mal', 'zoe', 'inara', 'kaylee'}
c.Authenticator.allow_all = False
# c.Authenticator.allow_all = False
c.Authenticator.allow_existing_users = False
```
Users in the `allowed_users` set are added to the Hub database when the Hub is started.
```{warning}
If `allowed_users` is not specified, then by default **all authenticated users will be allowed into your hub**,
i.e. `allow_all` defaults to True if neither `allowed_users` nor `allow_all` are set.
```
:::{versionchanged} 5.0
{attr}`.Authenticator.allow_all` and {attr}`.Authenticator.allow_existing_users` are new in JupyterHub 5.0
to enable explicit configuration of previously implicit behavior.
:::{versionadded} 5.0
{attr}`Authenticator.allow_all` and {attr}`Authenticator.allow_existing_users` are new in JupyterHub 5.0.
Prior to 5.0, `allow_all` was implicitly True if `allowed_users` was empty.
Starting with 5.0, to allow all authenticated users by default,
`allow_all` must be explicitly set to True.
By default, `allow_all` is True when `allowed_users` is empty,
and `allow_existing_users` is True when `allowed_users` is not empty.
This is to ensure backward-compatibility.
By default, `allow_existing_users` is True when `allowed_users` is not empty,
to ensure backward-compatibility.
To make the `allowed_users` set _restrictive_,
set `allow_existing_users = False`.
:::
## One Time Passwords ( request_otp )
@@ -102,6 +129,11 @@ By default, only the deprecated `admin` role has global `access` permissions.
## Add or remove users from the Hub
:::{versionadded} 5.0
`c.Authenticator.allow_existing_users` is added in 5.0 and enabled by default.
Prior to 5.0, this behavior was not optional.
:::
Users can be added to and removed from the Hub via the admin
panel or the REST API.

View File

@@ -2077,6 +2077,9 @@ class JupyterHub(Application):
"auth_state is enabled, but encryption is not available: %s" % e
)
# give the authenticator a chance to check its own config
self.authenticator.check_allow_config()
if self.admin_users and not self.authenticator.admin_users:
self.log.warning(
"\nJupyterHub.admin_users is deprecated since version 0.7.2."
@@ -2104,9 +2107,9 @@ class JupyterHub(Application):
new_users.append(user)
else:
user.admin = True
# the admin_users config variable will never be used after this point.
# only the database values will be referenced.
allowed_users = [
self.authenticator.normalize_username(name)
for name in self.authenticator.allowed_users
@@ -2116,10 +2119,10 @@ class JupyterHub(Application):
if not self.authenticator.validate_username(username):
raise ValueError("username %r is not valid" % username)
if not allowed_users:
self.log.info(
"Not using allowed_users. Any authenticated user will be allowed."
)
if self.authenticator.allowed_users and self.authenticator.admin_users:
# make sure admin users are in the allowed_users set, if defined,
# otherwise they won't be able to login
self.authenticator.allowed_users |= self.authenticator.admin_users
# add allowed users to the db
for name in allowed_users:

View File

@@ -121,6 +121,54 @@ class Authenticator(LoggingConfigurable):
"""
).tag(config=True)
any_allow_config = Bool(
False,
help="""Is there any allow config?
Used to show a warning if it looks like nobody can access the HUb,
which can happen when upgrading to JupyterHub 5,
now that `allow_all` defaults to False.
Deployments can set this explicitly to True to suppress
the "No allow config found" warning.
Will be True if any config tagged with `.tag(allow_config=True)`
or starts with `allow` is truthy.
.. versionadded:: 5.0
""",
).tag(config=True)
@default("any_allow_config")
def _default_any_allowed(self):
for trait_name, trait in self.traits(config=True).items():
if trait.metadata.get("allow_config", False) or trait_name.startswith(
"allow"
):
# this is only used for a helpful warning, so not the biggest deal if it's imperfect
if getattr(self, trait_name):
return True
return False
def check_allow_config(self):
"""Log a warning if no allow config can be found.
Could get a false positive if _only_ unrecognized allow config is used.
Authenticators can apply `.tag(allow_config=True)` to label this config
to make sure it is found.
Subclasses can override to perform additonal checks and warn about likely
authenticator configuration problems.
.. versionadded:: 5.0
"""
if not self.any_allow_config:
self.log.warning(
"No allow config found, it's possible that nobody can login to your Hub!\n"
"You can set `c.Authenticator.allow_all = True` to allow any user who can login to access the Hub,\n"
"or e.g. `allowed_users` to a set of users who should have access."
)
whitelist = Set(
help="Deprecated, use `Authenticator.allowed_users`",
config=True,
@@ -145,50 +193,35 @@ class Authenticator(LoggingConfigurable):
).tag(config=True)
allow_all = Bool(
# dynamic default computed from allowed_users
False,
config=True,
help="""
Any users who can successfully authenticate are allowed to access JupyterHub.
For backward-compatibility, this is True by default if `allowed_users` is unspecified,
False if `allowed_users` is specified.
Allow every user who can successfully authenticate to access JupyterHub.
False by default, which means for most Authenticators,
_some_ allow-related configuration is required to allow any users to log in.
Authenticator subclasses may override the default with e.g.::
@default("allow_all")
def _default_allow_all(self):
return False
# or
@default("allow_all")
def _default_allow_all(self):
if self.allowed_users or self.allowed_groups:
# if _any_ auth config (depends on the Authenticator)
if self.allowed_users or self.allowed_groups or self.allow_existing_users:
return False
else:
return True
False is a good idea when the group of users who can authenticate is typically larger
than the group users who should have access (e.g. OAuth providers).
Authenticator subclasses that define additional sources of `allow` config
beyond `allowed_users` should specify a default value for allow_all,
either always False or `if not any allow_config`.
This is checked inside `check_allowed`, so subclasses that override `check_allowed`
must explicitly check `allow_all` for it to have any effect.
This is for safety, to ensure that no Authenticator subclass gets unexpected behavior from `allow_all`.
.. versionadded:: 5.0
.. versionchanged:: 5.0
Prior to 5.0, `allow_all` wasn't defined on its own,
and was instead implicitly True when no allow config was provided,
i.e. `allowed_users` unspecified or empty on the base Authenticator class.
To preserve pre-5.0 behavior,
set `allow_all = True` if you have no other allow configuration.
""",
)
@default("allow_all")
def _default_allow_all(self):
if self.allowed_users:
return False
else:
return True
).tag(allow_config=True)
allow_existing_users = Bool(
# dynamic default computed from allowed_users
@@ -223,7 +256,7 @@ class Authenticator(LoggingConfigurable):
.. versionadded:: 5.0
""",
)
).tag(allow_config=True)
@default("allow_existing_users")
def _allow_existing_users_default(self):
@@ -616,8 +649,9 @@ class Authenticator(LoggingConfigurable):
The various stages can be overridden separately:
- `authenticate` turns formdata into a username
- `normalize_username` normalizes the username
- `check_allowed` checks against the allowed usernames
- `check_blocked_users` check against the blocked usernames
- `allow_all` is checked
- `check_allowed` checks against the allowed usernames
- `is_admin` check if a user is an admin
.. versionchanged:: 0.8
@@ -651,7 +685,11 @@ class Authenticator(LoggingConfigurable):
self.log.warning("User %r blocked. Stop authentication", username)
return
allowed_pass = await maybe_future(self.check_allowed(username, authenticated))
allowed_pass = self.allow_all
if not allowed_pass:
allowed_pass = await maybe_future(
self.check_allowed(username, authenticated)
)
if allowed_pass:
if authenticated['admin'] is None:
@@ -776,14 +814,14 @@ class Authenticator(LoggingConfigurable):
By default, this adds the user to the allowed_users set if
allow_existing_users is true.
Subclasses may do more extensive things, such as adding actual unix users,
Subclasses may do more extensive things, such as creating actual system users,
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.
.. versionchanged:: 5.0
Now adds users to the allowed_users set if allow_all is False,
Now adds users to the allowed_users set if allow_all is False and allow_existing_users is True,
instead of if allowed_users is not empty.
Args:
@@ -791,10 +829,7 @@ class Authenticator(LoggingConfigurable):
"""
if not self.validate_username(user.name):
raise ValueError("Invalid username: %s" % user.name)
# this is unnecessary if allow_all is True,
# but skipping this when allow_all is False breaks backward-compatibility
# for Authenticator subclasses that may not yet understand allow_all
if self.allow_existing_users:
if self.allow_existing_users and not self.allow_all:
self.allowed_users.add(user.name)
def delete_user(self, user):
@@ -1005,18 +1040,9 @@ class LocalAuthenticator(Authenticator):
`allowed_groups` may be specified together with allowed_users,
to grant access by group OR name.
"""
).tag(config=True)
@default("allow_all")
def _allow_all_default(self):
if self.allowed_users or self.allowed_groups:
# if any allow config is specified, default to False
return False
return True
).tag(config=True, allow_config=True)
def check_allowed(self, username, authentication=None):
if self.allow_all:
return True
if self.check_allowed_groups(username, authentication):
return True
return super().check_allowed(username, authentication)
@@ -1349,8 +1375,20 @@ class DummyAuthenticator(Authenticator):
if it logs in with that password.
.. versionadded:: 1.0
.. versionadded:: 5.0
`allow_all` defaults to True,
preserving default behavior.
"""
@default("allow_all")
def _allow_all_default(self):
if self.allowed_users:
return False
else:
# allow all by default
return True
password = Unicode(
config=True,
help="""
@@ -1360,6 +1398,12 @@ class DummyAuthenticator(Authenticator):
""",
)
def check_allow_config(self):
super().check_allow_config()
self.log.warning(
f"Using testing authenticator {self.__class__.__name__}! This is not meant for production!"
)
async def authenticate(self, handler, data):
"""Checks against a global password if it's been set. If not, allow any user/pass combo"""
if self.password:

View File

@@ -243,6 +243,7 @@ class MockHub(JupyterHub):
cert_location = kwargs['internal_certs_location']
kwargs['external_certs'] = ssl_setup(cert_location, 'hub-ca')
super().__init__(*args, **kwargs)
self.config.Authenticator.allow_all = True
@default('subdomain_host')
def _subdomain_host_default(self):

View File

@@ -19,7 +19,7 @@ from .utils import add_user, async_requests, get_page, public_url
async def test_pam_auth():
authenticator = MockPAMAuthenticator()
authenticator = MockPAMAuthenticator(allow_all=True)
authorized = await authenticator.get_authenticated_user(
None, {'username': 'match', 'password': 'match'}
)
@@ -38,7 +38,7 @@ async def test_pam_auth():
async def test_pam_auth_account_check_disabled():
authenticator = MockPAMAuthenticator(check_account=False)
authenticator = MockPAMAuthenticator(allow_all=True, check_account=False)
authorized = await authenticator.get_authenticated_user(
None, {'username': 'allowedmatch', 'password': 'allowedmatch'}
)
@@ -83,7 +83,9 @@ async def test_pam_auth_admin_groups():
return user_group_map[name]
authenticator = MockPAMAuthenticator(
admin_groups={'jh_admins', 'wheel'}, admin_users={'override_admin'}
admin_groups={'jh_admins', 'wheel'},
admin_users={'override_admin'},
allow_all=True,
)
# Check admin_group applies as expected
@@ -142,7 +144,10 @@ async def test_pam_auth_admin_groups():
async def test_pam_auth_allowed():
authenticator = MockPAMAuthenticator(allowed_users={'wash', 'kaylee'})
authenticator = MockPAMAuthenticator(
allowed_users={'wash', 'kaylee'}, allow_all=False
)
authorized = await authenticator.get_authenticated_user(
None, {'username': 'kaylee', 'password': 'kaylee'}
)
@@ -163,7 +168,7 @@ async def test_pam_auth_allowed_groups():
def getgrnam(name):
return MockStructGroup('grp', ['kaylee'])
authenticator = MockPAMAuthenticator(allowed_groups={'group'})
authenticator = MockPAMAuthenticator(allowed_groups={'group'}, allow_all=False)
with mock.patch.object(authenticator, '_getgrnam', getgrnam):
authorized = await authenticator.get_authenticated_user(
@@ -180,14 +185,14 @@ async def test_pam_auth_allowed_groups():
async def test_pam_auth_blocked():
# Null case compared to next case
authenticator = MockPAMAuthenticator()
authenticator = MockPAMAuthenticator(allow_all=True)
authorized = await authenticator.get_authenticated_user(
None, {'username': 'wash', 'password': 'wash'}
)
assert authorized['name'] == 'wash'
# Blacklist basics
authenticator = MockPAMAuthenticator(blocked_users={'wash'})
# Blocklist basics
authenticator = MockPAMAuthenticator(blocked_users={'wash'}, allow_all=True)
authorized = await authenticator.get_authenticated_user(
None, {'username': 'wash', 'password': 'wash'}
)
@@ -195,7 +200,9 @@ async def test_pam_auth_blocked():
# User in both allowed and blocked: default deny. Make error someday?
authenticator = MockPAMAuthenticator(
blocked_users={'wash'}, allowed_users={'wash', 'kaylee'}
blocked_users={'wash'},
allowed_users={'wash', 'kaylee'},
allow_all=True,
)
authorized = await authenticator.get_authenticated_user(
None, {'username': 'wash', 'password': 'wash'}
@@ -204,7 +211,8 @@ async def test_pam_auth_blocked():
# User not in blocked set can log in
authenticator = MockPAMAuthenticator(
blocked_users={'wash'}, allowed_users={'wash', 'kaylee'}
blocked_users={'wash'},
allowed_users={'wash', 'kaylee'},
)
authorized = await authenticator.get_authenticated_user(
None, {'username': 'kaylee', 'password': 'kaylee'}
@@ -222,7 +230,8 @@ async def test_pam_auth_blocked():
# User in neither list
authenticator = MockPAMAuthenticator(
blocked_users={'mal'}, allowed_users={'wash', 'kaylee'}
blocked_users={'mal'},
allowed_users={'wash', 'kaylee'},
)
authorized = await authenticator.get_authenticated_user(
None, {'username': 'simon', 'password': 'simon'}
@@ -258,7 +267,9 @@ async def test_deprecated_signatures():
async def test_pam_auth_no_such_group():
authenticator = MockPAMAuthenticator(allowed_groups={'nosuchcrazygroup'})
authenticator = MockPAMAuthenticator(
allowed_groups={'nosuchcrazygroup'},
)
authorized = await authenticator.get_authenticated_user(
None, {'username': 'kaylee', 'password': 'kaylee'}
)
@@ -406,7 +417,7 @@ async def test_auth_state_disabled(app, auth_state_unavailable):
async def test_normalize_names():
a = MockPAMAuthenticator()
a = MockPAMAuthenticator(allow_all=True)
authorized = await a.get_authenticated_user(
None, {'username': 'ZOE', 'password': 'ZOE'}
)
@@ -429,7 +440,7 @@ async def test_normalize_names():
async def test_username_map():
a = MockPAMAuthenticator(username_map={'wash': 'alpha'})
a = MockPAMAuthenticator(username_map={'wash': 'alpha'}, allow_all=True)
authorized = await a.get_authenticated_user(
None, {'username': 'WASH', 'password': 'WASH'}
)
@@ -459,7 +470,7 @@ async def test_post_auth_hook():
authentication['testkey'] = 'testvalue'
return authentication
a = MockPAMAuthenticator(post_auth_hook=test_auth_hook)
a = MockPAMAuthenticator(allow_all=True, post_auth_hook=test_auth_hook)
authorized = await a.get_authenticated_user(
None, {'username': 'test_user', 'password': 'test_user'}
@@ -567,6 +578,7 @@ async def test_auth_managed_groups(
parent=app,
authenticated_groups=authenticated_groups,
refresh_groups=refresh_groups,
allow_all=True,
)
user.groups.append(group)
@@ -600,15 +612,18 @@ async def test_auth_managed_groups(
"allowed_users, allow_all, allow_existing_users",
[
('specified', False, True),
('', True, False),
('', False, False),
],
)
def test_allow_all_defaults(app, user, allowed_users, allow_all, allow_existing_users):
async def test_allow_defaults(
app, user, allowed_users, allow_all, allow_existing_users
):
if allowed_users:
allowed_users = set(allowed_users.split(','))
else:
allowed_users = set()
authenticator = auth.Authenticator(allowed_users=allowed_users)
authenticator.authenticate = lambda handler, data: data["username"]
assert authenticator.allow_all == allow_all
assert authenticator.allow_existing_users == allow_existing_users
@@ -620,8 +635,21 @@ def test_allow_all_defaults(app, user, allowed_users, allow_all, allow_existing_
else:
authenticator.allowed_users == set()
assert authenticator.check_allowed("specified")
assert authenticator.check_allowed(user.name)
specified_allowed = await authenticator.get_authenticated_user(
None, {"username": "specified"}
)
if "specified" in allowed_users:
assert specified_allowed is not None
else:
assert specified_allowed is None
existing_allowed = await authenticator.get_authenticated_user(
None, {"username": user.name}
)
if allow_existing_users:
assert existing_allowed is not None
else:
assert existing_allowed is None
@pytest.mark.parametrize("allow_all", [None, True, False])
@@ -693,6 +721,9 @@ class AllowAllIgnoringAuthenticator(auth.Authenticator):
allowed_letters = Tuple(config=True, help="Initial letters to allow")
def authenticate(self, handler, data):
return {"name": data["username"]}
def check_allowed(self, username, auth=None):
if not self.allowed_users and not self.allowed_letters:
# this subclass doesn't know about the JupyterHub 5 allow_all config
@@ -707,7 +738,7 @@ class AllowAllIgnoringAuthenticator(auth.Authenticator):
# allow_all is not recognized by Authenticator subclass
# make sure it doesn't make anything more permissive, at least
@pytest.mark.parametrize("allow_all", [True, False, None])
@pytest.mark.parametrize("allow_all", [True, False])
@pytest.mark.parametrize(
"allowed_users, allowed_letters, allow_existing_users, allowed, not_allowed",
[
@@ -720,7 +751,7 @@ class AllowAllIgnoringAuthenticator(auth.Authenticator):
("specified", "a,b", True, "specified,alice,bebe,existing", "other"),
],
)
def test_authenticator_without_allow_all(
async def test_authenticator_without_allow_all(
app,
allowed_users,
allowed_letters,
@@ -753,10 +784,14 @@ def test_authenticator_without_allow_all(
expected_allowed = sorted(allowed)
expected_not_allowed = sorted(not_allowed)
to_check = list(chain(expected_allowed, expected_not_allowed))
if allow_all:
expected_allowed = to_check
expected_not_allowed = []
are_allowed = []
are_not_allowed = []
for username in to_check:
if authenticator.check_allowed(username):
if await authenticator.get_authenticated_user(None, {"username": username}):
are_allowed.append(username)
else:
are_not_allowed.append(username)