mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-19 16:03:00 +00:00
Merge pull request #3090 from minrk/words-matter
This commit is contained in:
@@ -7,6 +7,8 @@ command line for details.
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## 1.1
|
## 1.1
|
||||||
|
|
||||||
### [1.1.0] 2020-01-17
|
### [1.1.0] 2020-01-17
|
||||||
@@ -116,7 +118,7 @@ Thanks to everyone who has contributed to this release!
|
|||||||
- Log JupyterHub version on startup [#2752](https://github.com/jupyterhub/jupyterhub/pull/2752) ([@consideRatio](https://github.com/consideRatio))
|
- Log JupyterHub version on startup [#2752](https://github.com/jupyterhub/jupyterhub/pull/2752) ([@consideRatio](https://github.com/consideRatio))
|
||||||
- Reduce verbosity for "Failing suspected API request to not-running server" (new) [#2751](https://github.com/jupyterhub/jupyterhub/pull/2751) ([@rkdarst](https://github.com/rkdarst))
|
- Reduce verbosity for "Failing suspected API request to not-running server" (new) [#2751](https://github.com/jupyterhub/jupyterhub/pull/2751) ([@rkdarst](https://github.com/rkdarst))
|
||||||
- Add missing package for json schema doc build [#2744](https://github.com/jupyterhub/jupyterhub/pull/2744) ([@willingc](https://github.com/willingc))
|
- Add missing package for json schema doc build [#2744](https://github.com/jupyterhub/jupyterhub/pull/2744) ([@willingc](https://github.com/willingc))
|
||||||
- blacklist urllib3 versions with encoding bug [#2743](https://github.com/jupyterhub/jupyterhub/pull/2743) ([@minrk](https://github.com/minrk))
|
- block urllib3 versions with encoding bug [#2743](https://github.com/jupyterhub/jupyterhub/pull/2743) ([@minrk](https://github.com/minrk))
|
||||||
- Remove tornado deprecated/unnecessary AsyncIOMainLoop().install() call [#2740](https://github.com/jupyterhub/jupyterhub/pull/2740) ([@kinow](https://github.com/kinow))
|
- Remove tornado deprecated/unnecessary AsyncIOMainLoop().install() call [#2740](https://github.com/jupyterhub/jupyterhub/pull/2740) ([@kinow](https://github.com/kinow))
|
||||||
- Fix deprecated call [#2739](https://github.com/jupyterhub/jupyterhub/pull/2739) ([@kinow](https://github.com/kinow))
|
- Fix deprecated call [#2739](https://github.com/jupyterhub/jupyterhub/pull/2739) ([@kinow](https://github.com/kinow))
|
||||||
- Remove duplicate hub and authenticator traitlets from Spawner [#2736](https://github.com/jupyterhub/jupyterhub/pull/2736) ([@eslavich](https://github.com/eslavich))
|
- Remove duplicate hub and authenticator traitlets from Spawner [#2736](https://github.com/jupyterhub/jupyterhub/pull/2736) ([@eslavich](https://github.com/eslavich))
|
||||||
@@ -231,8 +233,8 @@ whether it was through discussion, testing, documentation, or development.
|
|||||||
This hook may transform the return value of `Authenticator.authenticate()`
|
This hook may transform the return value of `Authenticator.authenticate()`
|
||||||
and return a new authentication dictionary,
|
and return a new authentication dictionary,
|
||||||
e.g. specifying admin privileges, group membership,
|
e.g. specifying admin privileges, group membership,
|
||||||
or custom white/blacklisting logic.
|
or custom allowed/blocked logic.
|
||||||
This hook is called *after* existing normalization and whitelist checking.
|
This hook is called *after* existing normalization and allowed-username checking.
|
||||||
- `Spawner.options_from_form` may now be async
|
- `Spawner.options_from_form` may now be async
|
||||||
- Added `JupyterHub.shutdown_on_logout` option to trigger shutdown of a user's
|
- Added `JupyterHub.shutdown_on_logout` option to trigger shutdown of a user's
|
||||||
servers when they log out.
|
servers when they log out.
|
||||||
@@ -418,7 +420,7 @@ and tornado < 5.0.
|
|||||||
launching an IPython session connected to your JupyterHub database.
|
launching an IPython session connected to your JupyterHub database.
|
||||||
- Include `User.auth_state` in user model on single-user REST endpoints for admins only.
|
- Include `User.auth_state` in user model on single-user REST endpoints for admins only.
|
||||||
- Include `Server.state` in server model on REST endpoints for admins only.
|
- Include `Server.state` in server model on REST endpoints for admins only.
|
||||||
- Add `Authenticator.blacklist` for blacklisting users instead of whitelisting.
|
- Add `Authenticator.blacklist` for blocking users instead of allowing.
|
||||||
- Pass `c.JupyterHub.tornado_settings['cookie_options']` down to Spawners
|
- Pass `c.JupyterHub.tornado_settings['cookie_options']` down to Spawners
|
||||||
so that cookie options (e.g. `expires_days`) can be set globally for the whole application.
|
so that cookie options (e.g. `expires_days`) can be set globally for the whole application.
|
||||||
- SIGINFO (`ctrl-t`) handler showing the current status of all running threads,
|
- SIGINFO (`ctrl-t`) handler showing the current status of all running threads,
|
||||||
|
@@ -4,23 +4,23 @@ The default Authenticator uses [PAM][] to authenticate system users with
|
|||||||
their username and password. With the default Authenticator, any user
|
their username and password. With the default Authenticator, any user
|
||||||
with an account and password on the system will be allowed to login.
|
with an account and password on the system will be allowed to login.
|
||||||
|
|
||||||
## Create a whitelist of users
|
## Create a set of allowed users
|
||||||
|
|
||||||
You can restrict which users are allowed to login with a whitelist,
|
You can restrict which users are allowed to login with a set,
|
||||||
`Authenticator.whitelist`:
|
`Authenticator.allowed_users`:
|
||||||
|
|
||||||
|
|
||||||
```python
|
```python
|
||||||
c.Authenticator.whitelist = {'mal', 'zoe', 'inara', 'kaylee'}
|
c.Authenticator.allowed_users = {'mal', 'zoe', 'inara', 'kaylee'}
|
||||||
```
|
```
|
||||||
|
|
||||||
Users in the whitelist are added to the Hub database when the Hub is
|
Users in the `allowed_users` set are added to the Hub database when the Hub is
|
||||||
started.
|
started.
|
||||||
|
|
||||||
## Configure admins (`admin_users`)
|
## Configure admins (`admin_users`)
|
||||||
|
|
||||||
Admin users of JupyterHub, `admin_users`, can add and remove users from
|
Admin users of JupyterHub, `admin_users`, can add and remove users from
|
||||||
the user `whitelist`. `admin_users` can take actions on other users'
|
the user `allowed_users` set. `admin_users` can take actions on other users'
|
||||||
behalf, such as stopping and restarting their servers.
|
behalf, such as stopping and restarting their servers.
|
||||||
|
|
||||||
A set of initial admin users, `admin_users` can configured be as follows:
|
A set of initial admin users, `admin_users` can configured be as follows:
|
||||||
@@ -28,7 +28,7 @@ A set of initial admin users, `admin_users` can configured be as follows:
|
|||||||
```python
|
```python
|
||||||
c.Authenticator.admin_users = {'mal', 'zoe'}
|
c.Authenticator.admin_users = {'mal', 'zoe'}
|
||||||
```
|
```
|
||||||
Users in the admin list are automatically added to the user `whitelist`,
|
Users in the admin set are automatically added to the user `allowed_users` set,
|
||||||
if they are not already present.
|
if they are not already present.
|
||||||
|
|
||||||
Each authenticator may have different ways of determining whether a user is an
|
Each authenticator may have different ways of determining whether a user is an
|
||||||
@@ -53,12 +53,12 @@ sure your users know if admin_access is enabled.**
|
|||||||
|
|
||||||
Users can be added to and removed from the Hub via either the admin
|
Users can be added to and removed from the Hub via either the admin
|
||||||
panel or the REST API. When a user is **added**, the user will be
|
panel or the REST API. When a user is **added**, the user will be
|
||||||
automatically added to the whitelist and database. Restarting the Hub
|
automatically added to the allowed users set and database. Restarting the Hub
|
||||||
will not require manually updating the whitelist in your config file,
|
will not require manually updating the allowed users set in your config file,
|
||||||
as the users will be loaded from the database.
|
as the users will be loaded from the database.
|
||||||
|
|
||||||
After starting the Hub once, it is not sufficient to **remove** a user
|
After starting the Hub once, it is not sufficient to **remove** a user
|
||||||
from the whitelist in your config file. You must also remove the user
|
from the allowed users set in your config file. You must also remove the user
|
||||||
from the Hub's database, either by deleting the user from JupyterHub's
|
from the Hub's database, either by deleting the user from JupyterHub's
|
||||||
admin page, or you can clear the `jupyterhub.sqlite` database and start
|
admin page, or you can clear the `jupyterhub.sqlite` database and start
|
||||||
fresh.
|
fresh.
|
||||||
|
@@ -52,7 +52,7 @@ c.GitHubOAuthenticator.oauth_callback_url = os.environ['OAUTH_CALLBACK_URL']
|
|||||||
c.LocalAuthenticator.create_system_users = True
|
c.LocalAuthenticator.create_system_users = True
|
||||||
|
|
||||||
# specify users and admin
|
# specify users and admin
|
||||||
c.Authenticator.whitelist = {'rgbkrk', 'minrk', 'jhamrick'}
|
c.Authenticator.allowed_users = {'rgbkrk', 'minrk', 'jhamrick'}
|
||||||
c.Authenticator.admin_users = {'jhamrick', 'rgbkrk'}
|
c.Authenticator.admin_users = {'jhamrick', 'rgbkrk'}
|
||||||
|
|
||||||
# uses the default spawner
|
# uses the default spawner
|
||||||
|
@@ -57,7 +57,7 @@ To do this we add to `/etc/sudoers` (use `visudo` for safe editing of sudoers):
|
|||||||
For example:
|
For example:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# comma-separated whitelist of users that can spawn single-user servers
|
# comma-separated list of users that can spawn single-user servers
|
||||||
# this should include all of your Hub users
|
# this should include all of your Hub users
|
||||||
Runas_Alias JUPYTER_USERS = rhea, zoe, wash
|
Runas_Alias JUPYTER_USERS = rhea, zoe, wash
|
||||||
|
|
||||||
|
@@ -313,7 +313,7 @@ class MyHandler(HubAuthenticated, web.RequestHandler):
|
|||||||
The HubAuth will automatically load the desired configuration from the Service
|
The HubAuth will automatically load the desired configuration from the Service
|
||||||
environment variables.
|
environment variables.
|
||||||
|
|
||||||
If you want to limit user access, you can whitelist users through either the
|
If you want to limit user access, you can specify allowed users through either the
|
||||||
`.hub_users` attribute or `.hub_groups`. These are sets that check against the
|
`.hub_users` attribute or `.hub_groups`. These are sets that check against the
|
||||||
username and user group list, respectively. If a user matches neither the user
|
username and user group list, respectively. If a user matches neither the user
|
||||||
list nor the group list, they will not be allowed access. If both are left
|
list nor the group list, they will not be allowed access. If both are left
|
||||||
|
@@ -7,8 +7,8 @@ problem and how to resolve it.
|
|||||||
[*Behavior*](#behavior)
|
[*Behavior*](#behavior)
|
||||||
- JupyterHub proxy fails to start
|
- JupyterHub proxy fails to start
|
||||||
- sudospawner fails to run
|
- sudospawner fails to run
|
||||||
- What is the default behavior when none of the lists (admin, whitelist,
|
- What is the default behavior when none of the lists (admin, allowed,
|
||||||
group whitelist) are set?
|
allowed groups) are set?
|
||||||
- JupyterHub Docker container not accessible at localhost
|
- JupyterHub Docker container not accessible at localhost
|
||||||
|
|
||||||
[*Errors*](#errors)
|
[*Errors*](#errors)
|
||||||
@@ -55,12 +55,12 @@ or add:
|
|||||||
|
|
||||||
to the config file, `jupyterhub_config.py`.
|
to the config file, `jupyterhub_config.py`.
|
||||||
|
|
||||||
### What is the default behavior when none of the lists (admin, whitelist, group whitelist) are set?
|
### What is the default behavior when none of the lists (admin, allowed, allowed groups) are set?
|
||||||
|
|
||||||
When nothing is given for these lists, there will be no admins, and all users
|
When nothing is given for these lists, there will be no admins, and all users
|
||||||
who can authenticate on the system (i.e. all the unix users on the server with
|
who can authenticate on the system (i.e. all the unix users on the server with
|
||||||
a password) will be allowed to start a server. The whitelist lets you limit
|
a password) will be allowed to start a server. The allowed username set lets you limit
|
||||||
this to a particular set of users, and the admin_users lets you specify who
|
this to a particular set of users, and admin_users lets you specify who
|
||||||
among them may use the admin interface (not necessary, unless you need to do
|
among them may use the admin interface (not necessary, unless you need to do
|
||||||
things like inspect other users' servers, or modify the user list at runtime).
|
things like inspect other users' servers, or modify the user list at runtime).
|
||||||
|
|
||||||
@@ -332,8 +332,7 @@ notebook servers to default to JupyterLab:
|
|||||||
### How do I set up JupyterHub for a workshop (when users are not known ahead of time)?
|
### How do I set up JupyterHub for a workshop (when users are not known ahead of time)?
|
||||||
|
|
||||||
1. Set up JupyterHub using OAuthenticator for GitHub authentication
|
1. Set up JupyterHub using OAuthenticator for GitHub authentication
|
||||||
2. Configure whitelist to be an empty list in` jupyterhub_config.py`
|
2. Configure admin list to have workshop leaders be listed with administrator privileges.
|
||||||
3. Configure admin list to have workshop leaders be listed with administrator privileges.
|
|
||||||
|
|
||||||
Users will need a GitHub account to login and be authenticated by the Hub.
|
Users will need a GitHub account to login and be authenticated by the Hub.
|
||||||
|
|
||||||
|
@@ -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.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@@ -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_users = [
|
||||||
self.authenticator.normalize_username(name)
|
self.authenticator.normalize_username(name)
|
||||||
for name in self.authenticator.whitelist
|
for name in self.authenticator.allowed_users
|
||||||
]
|
]
|
||||||
self.authenticator.whitelist = set(whitelist) # force normalization
|
self.authenticator.allowed_users = set(allowed_users) # force normalization
|
||||||
for username in whitelist:
|
for username in allowed_users:
|
||||||
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_users:
|
||||||
self.log.info(
|
self.log.info(
|
||||||
"Not using whitelist. Any authenticated user will be allowed."
|
"Not using allowed_users. 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_users:
|
||||||
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_users is up-to-date with the database.
|
||||||
# This lets whitelist be used to set up initial list,
|
# This lets .allowed_users be used to set up initial list,
|
||||||
# but changes to the whitelist can occur in the database,
|
# but changes to the allowed_users set 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_users 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_users 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_users" % 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,14 @@ 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_users"
|
||||||
|
% 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 +2186,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 +2229,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,
|
||||||
|
@@ -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_users`", config=True,)
|
||||||
|
|
||||||
|
allowed_users = 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_users`
|
||||||
"""
|
"""
|
||||||
).tag(config=True)
|
).tag(config=True)
|
||||||
|
|
||||||
blacklist = Set(
|
blocked_users = 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_users`
|
||||||
"""
|
"""
|
||||||
).tag(config=True)
|
).tag(config=True)
|
||||||
|
|
||||||
@observe('whitelist')
|
_deprecated_aliases = {
|
||||||
def _check_whitelist(self, change):
|
"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]
|
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 set 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,
|
||||||
@@ -261,39 +295,74 @@ class Authenticator(LoggingConfigurable):
|
|||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
for method_name in (
|
self._init_deprecated_methods()
|
||||||
'check_whitelist',
|
|
||||||
'check_blacklist',
|
def _init_deprecated_methods(self):
|
||||||
'check_group_whitelist',
|
# 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)
|
old_method = getattr(self, old_name, None)
|
||||||
if original_method is None:
|
if old_method is None:
|
||||||
# no such method (check_group_whitelist is optional)
|
# no such method (check_group_whitelist is optional)
|
||||||
continue
|
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
|
# adapt to pre-1.0 signature for compatibility
|
||||||
warnings.warn(
|
warnings.warn(
|
||||||
"""
|
"""
|
||||||
{0}.{1} does not support the authentication argument,
|
{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:
|
It should have the signature:
|
||||||
|
|
||||||
def {1}(self, username, authentication=None):
|
def {2}(self, username, authentication=None):
|
||||||
...
|
...
|
||||||
|
|
||||||
Adapting for compatibility.
|
Adapting for compatibility.
|
||||||
""".format(
|
""".format(
|
||||||
self.__class__.__name__, method_name
|
self.__class__.__name__, old_name, new_name
|
||||||
),
|
),
|
||||||
DeprecationWarning,
|
DeprecationWarning,
|
||||||
)
|
)
|
||||||
|
|
||||||
def wrapped_method(username, authentication=None, **kwargs):
|
def wrapped_method(
|
||||||
|
original_method, username, authentication=None, **kwargs
|
||||||
|
):
|
||||||
return original_method(username, **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):
|
async def run_post_auth_hook(self, handler, authentication):
|
||||||
"""
|
"""
|
||||||
@@ -327,39 +396,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_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
|
.. 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_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.
|
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_users
|
||||||
"""
|
"""
|
||||||
if not self.blacklist:
|
if not self.blocked_users:
|
||||||
# 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_users
|
||||||
|
|
||||||
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
|
||||||
@@ -368,7 +443,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.
|
||||||
@@ -376,7 +451,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 allowed usernames
|
||||||
|
|
||||||
.. versionchanged:: 0.8
|
.. versionchanged:: 0.8
|
||||||
return dict instead of username
|
return dict instead of username
|
||||||
@@ -390,7 +465,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
|
||||||
@@ -401,20 +476,18 @@ 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_blacklist(username, authenticated)
|
self.check_blocked_users(username, authenticated)
|
||||||
)
|
|
||||||
whitelist_pass = await maybe_future(
|
|
||||||
self.check_whitelist(username, authenticated)
|
|
||||||
)
|
)
|
||||||
|
allowed_pass = await maybe_future(self.check_allowed(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)
|
||||||
@@ -424,7 +497,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):
|
||||||
@@ -480,7 +553,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_users/blocked_users 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.
|
||||||
@@ -521,10 +594,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_users 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_users 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.
|
||||||
@@ -534,19 +607,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_users:
|
||||||
self.whitelist.add(user.name)
|
self.allowed_users.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_users set.
|
||||||
Subclasses should call super to ensure the whitelist is updated.
|
Subclasses should call super to ensure the allowed_users set is updated.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
user (User): The User wrapper object
|
user (User): The User wrapper object
|
||||||
"""
|
"""
|
||||||
self.whitelist.discard(user.name)
|
self.allowed_users.discard(user.name)
|
||||||
|
|
||||||
auto_login = Bool(
|
auto_login = Bool(
|
||||||
False,
|
False,
|
||||||
@@ -611,6 +684,41 @@ class Authenticator(LoggingConfigurable):
|
|||||||
return [('/login', LoginHandler)]
|
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):
|
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
|
||||||
|
|
||||||
@@ -670,37 +778,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_users:
|
||||||
"""
|
|
||||||
if self.whitelist:
|
|
||||||
self.log.warning(
|
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):
|
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:
|
||||||
@@ -844,7 +952,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)
|
||||||
|
|
||||||
@@ -987,6 +1095,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,
|
||||||
|
_deprecated_method(_old_name, _new_name, _version),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class DummyAuthenticator(Authenticator):
|
class DummyAuthenticator(Authenticator):
|
||||||
"""Dummy Authenticator for testing
|
"""Dummy Authenticator for testing
|
||||||
|
|
||||||
|
@@ -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)
|
||||||
|
@@ -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
|
||||||
|
@@ -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_users' in cfg_text
|
||||||
|
|
||||||
|
|
||||||
async def test_init_tokens(request):
|
async def test_init_tokens(request):
|
||||||
|
@@ -1,11 +1,14 @@
|
|||||||
"""Tests for PAM authentication"""
|
"""Tests for PAM authentication"""
|
||||||
# Copyright (c) Jupyter Development Team.
|
# Copyright (c) Jupyter Development Team.
|
||||||
# Distributed under the terms of the Modified BSD License.
|
# Distributed under the terms of the Modified BSD License.
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import warnings
|
||||||
from unittest import mock
|
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 +140,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_users={'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 +158,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 +177,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,33 +186,33 @@ 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_users={'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(
|
||||||
blacklist={'wash'}, whitelist={'wash', 'kaylee'}
|
blocked_users={'wash'}, allowed_users={'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(
|
||||||
blacklist={'wash'}, whitelist={'wash', 'kaylee'}
|
blocked_users={'wash'}, allowed_users={'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(
|
||||||
blacklist={'mal'}, whitelist={'wash', 'kaylee'}
|
blocked_users={'mal'}, allowed_users={'wash', 'kaylee'}
|
||||||
)
|
)
|
||||||
authorized = await authenticator.get_authenticated_user(
|
authorized = await authenticator.get_authenticated_user(
|
||||||
None, {'username': 'wash', 'password': 'wash'}
|
None, {'username': 'wash', 'password': 'wash'}
|
||||||
@@ -218,15 +221,16 @@ async def test_pam_auth_blacklist():
|
|||||||
|
|
||||||
# User in neither list
|
# User in neither list
|
||||||
authenticator = MockPAMAuthenticator(
|
authenticator = MockPAMAuthenticator(
|
||||||
blacklist={'mal'}, whitelist={'wash', 'kaylee'}
|
blocked_users={'mal'}, allowed_users={'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(
|
||||||
authenticator = MockPAMAuthenticator(blacklist=set(), whitelist={'wash', 'kaylee'})
|
blocked_users=set(), allowed_users={'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 +257,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 +266,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_users={'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 +274,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_users={'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 +300,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_users={'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 +321,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_users={'mal'})
|
||||||
|
|
||||||
assert 'zoe' not in a.whitelist
|
assert 'zoe' not in a.allowed_users
|
||||||
await a.add_user(user)
|
await a.add_user(user)
|
||||||
assert 'zoe' in a.whitelist
|
assert 'zoe' in a.allowed_users
|
||||||
a.delete_user(user)
|
a.delete_user(user)
|
||||||
assert 'zoe' not in a.whitelist
|
assert 'zoe' not in a.allowed_users
|
||||||
|
|
||||||
|
|
||||||
def test_urls():
|
def test_urls():
|
||||||
@@ -461,3 +465,55 @@ async def test_post_auth_hook():
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert authorized['testkey'] == 'testvalue'
|
assert authorized['testkey'] == 'testvalue'
|
||||||
|
|
||||||
|
|
||||||
|
class MyAuthenticator(auth.Authenticator):
|
||||||
|
def check_whitelist(self, username, authentication=None):
|
||||||
|
return username == "subclass-allowed"
|
||||||
|
|
||||||
|
|
||||||
|
def test_deprecated_config(caplog):
|
||||||
|
cfg = Config()
|
||||||
|
cfg.Authenticator.whitelist = {'user'}
|
||||||
|
log = logging.getLogger("testlog")
|
||||||
|
authenticator = auth.Authenticator(config=cfg, log=log)
|
||||||
|
assert caplog.record_tuples == [
|
||||||
|
(
|
||||||
|
log.name,
|
||||||
|
logging.WARNING,
|
||||||
|
'Authenticator.whitelist is deprecated in JupyterHub 1.2, use '
|
||||||
|
'Authenticator.allowed_users instead',
|
||||||
|
)
|
||||||
|
]
|
||||||
|
assert authenticator.allowed_users == {'user'}
|
||||||
|
|
||||||
|
|
||||||
|
def test_deprecated_methods():
|
||||||
|
cfg = Config()
|
||||||
|
cfg.Authenticator.whitelist = {'user'}
|
||||||
|
authenticator = auth.Authenticator(config=cfg)
|
||||||
|
|
||||||
|
assert authenticator.check_allowed("user")
|
||||||
|
with pytest.deprecated_call():
|
||||||
|
assert authenticator.check_whitelist("user")
|
||||||
|
assert not authenticator.check_allowed("otheruser")
|
||||||
|
with pytest.deprecated_call():
|
||||||
|
assert not authenticator.check_whitelist("otheruser")
|
||||||
|
|
||||||
|
|
||||||
|
def test_deprecated_config_subclass():
|
||||||
|
cfg = Config()
|
||||||
|
cfg.MyAuthenticator.whitelist = {'user'}
|
||||||
|
with pytest.deprecated_call():
|
||||||
|
authenticator = MyAuthenticator(config=cfg)
|
||||||
|
assert authenticator.allowed_users == {'user'}
|
||||||
|
|
||||||
|
|
||||||
|
def test_deprecated_methods_subclass():
|
||||||
|
with pytest.deprecated_call():
|
||||||
|
authenticator = MyAuthenticator()
|
||||||
|
|
||||||
|
assert authenticator.check_allowed("subclass-allowed")
|
||||||
|
assert authenticator.check_whitelist("subclass-allowed")
|
||||||
|
assert not authenticator.check_allowed("otheruser")
|
||||||
|
assert not authenticator.check_whitelist("otheruser")
|
||||||
|
@@ -744,7 +744,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):
|
||||||
|
@@ -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,
|
||||||
|
Reference in New Issue
Block a user