From edbf67ad4bc8333975e0f75a7ed067f614907d84 Mon Sep 17 00:00:00 2001 From: YuviPanda Date: Wed, 17 Jan 2024 13:53:30 -0800 Subject: [PATCH 01/14] Move username_claim being callable to oauth2 autheneticator While trying to use Auth0 for authentication in one of our hubs, we discovered that the most useful username_claim (`sub`) produces usernames that look like `oauth2|cilogon|http://cilogon.org/servera/users/43431` (when using auth0 with CILogon). The last part of `sub` is generally whatever is passed on to auth0, so it's going to be different for different users. I had thought `username_claim` was a callable, but turns out that's only true for GenericOAuthenticator. I think it's pretty useful for every authenticator, so I've just moved that functionality out to the base class instead. I also added a test to verify it works. The test is in GenericOAuthenticator because it was the easiest place to put it, but it works across authenticators. This also means it is fully backwards compatible. --- oauthenticator/generic.py | 11 ----------- oauthenticator/oauth2.py | 22 +++++++++++++++------- oauthenticator/tests/test_generic.py | 24 ++++++++++++++++++++++++ 3 files changed, 39 insertions(+), 18 deletions(-) diff --git a/oauthenticator/generic.py b/oauthenticator/generic.py index 31945d6a..4c7bb81c 100644 --- a/oauthenticator/generic.py +++ b/oauthenticator/generic.py @@ -113,17 +113,6 @@ def _default_http_client(self): """, ) - def user_info_to_username(self, user_info): - """ - Overrides OAuthenticator.user_info_to_username to support the - GenericOAuthenticator unique feature of allowing username_claim to be a - callable function. - """ - if callable(self.username_claim): - return self.username_claim(user_info) - else: - return super().user_info_to_username(user_info) - def get_user_groups(self, user_info): """ Returns a set of groups the user belongs to based on claim_groups_key diff --git a/oauthenticator/oauth2.py b/oauthenticator/oauth2.py index 9ba66ecf..1aa210c3 100644 --- a/oauthenticator/oauth2.py +++ b/oauthenticator/oauth2.py @@ -18,7 +18,7 @@ from tornado.httpclient import AsyncHTTPClient, HTTPClientError, HTTPRequest from tornado.httputil import url_concat from tornado.log import app_log -from traitlets import Any, Bool, Dict, List, Unicode, default +from traitlets import Any, Bool, Dict, List, Unicode, default, Union, Callable def guess_callback_uri(protocol, host, hub_server_url): @@ -376,14 +376,17 @@ def _token_url_default(self): def _userdata_url_default(self): return os.environ.get("OAUTH2_USERDATA_URL", "") - username_claim = Unicode( - "username", + username_claim = Union( + [Unicode(os.environ.get('OAUTH2_USERNAME_KEY', 'username')), Callable()], config=True, help=""" - The key to get the JupyterHub username from in the data response to the - request made to :attr:`userdata_url`. + When `userdata_url` returns a json response, the username will be taken + from this key. - Examples include: email, username, nickname + Can be a string key name or a callable that accepts the returned + userdata json (as a dict) and returns the username. The callable is + useful e.g. for extracting the username from a nested object in the + response or doing other post processing. What keys are available will depend on the scopes requested and the authenticator used. @@ -768,7 +771,12 @@ def user_info_to_username(self, user_info): Called by the :meth:`oauthenticator.OAuthenticator.authenticate` """ - username = user_info.get(self.username_claim, None) + + + if callable(self.username_claim): + username = self.username_claim(user_info) + else: + username = user_info.get(self.username_claim, None) if not username: message = (f"No {self.username_claim} found in {user_info}",) self.log.error(message) diff --git a/oauthenticator/tests/test_generic.py b/oauthenticator/tests/test_generic.py index 42dc7781..11d62755 100644 --- a/oauthenticator/tests/test_generic.py +++ b/oauthenticator/tests/test_generic.py @@ -12,6 +12,7 @@ def user_model(username, **kwargs): """Return a user model""" return { "username": username, + "sub": "oauth2|cilogon|http://cilogon.org/servera/users/43431", "scope": "basic", "groups": ["group1"], **kwargs, @@ -186,6 +187,29 @@ async def test_generic( else: assert auth_model == None +async def test_username_claim_callable( + get_authenticator, + generic_client, +): + c = Config() + c.GenericOAuthenticator = Config() + def username_claim(user_info): + username = user_info["sub"] + if username.startswith("oauth2|cilogon"): + cilogon_sub = username.rsplit("|", 1)[-1] + cilogon_sub_parts = cilogon_sub.split("/") + username = f"oauth2|cilogon|{cilogon_sub_parts[3]}|{cilogon_sub_parts[5]}" + return username + c.GenericOAuthenticator.username_claim = username_claim + c.GenericOAuthenticator.allow_all = True + authenticator = get_authenticator(config=c) + + handled_user_model = user_model("user1") + handler = generic_client.handler_for_user(handled_user_model) + auth_model = await authenticator.get_authenticated_user(handler, None) + + assert auth_model["name"] == "oauth2|cilogon|servera|43431" + async def test_generic_data(get_authenticator, generic_client): c = Config() From 4417ec88e2b739f3ac1452cf4ce4dc83636d69e0 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 17 Jan 2024 21:58:47 +0000 Subject: [PATCH 02/14] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- oauthenticator/oauth2.py | 3 +-- oauthenticator/tests/test_generic.py | 3 +++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/oauthenticator/oauth2.py b/oauthenticator/oauth2.py index 1aa210c3..cff6b032 100644 --- a/oauthenticator/oauth2.py +++ b/oauthenticator/oauth2.py @@ -18,7 +18,7 @@ from tornado.httpclient import AsyncHTTPClient, HTTPClientError, HTTPRequest from tornado.httputil import url_concat from tornado.log import app_log -from traitlets import Any, Bool, Dict, List, Unicode, default, Union, Callable +from traitlets import Any, Bool, Callable, Dict, List, Unicode, Union, default def guess_callback_uri(protocol, host, hub_server_url): @@ -772,7 +772,6 @@ def user_info_to_username(self, user_info): Called by the :meth:`oauthenticator.OAuthenticator.authenticate` """ - if callable(self.username_claim): username = self.username_claim(user_info) else: diff --git a/oauthenticator/tests/test_generic.py b/oauthenticator/tests/test_generic.py index 11d62755..c30fe23e 100644 --- a/oauthenticator/tests/test_generic.py +++ b/oauthenticator/tests/test_generic.py @@ -187,12 +187,14 @@ async def test_generic( else: assert auth_model == None + async def test_username_claim_callable( get_authenticator, generic_client, ): c = Config() c.GenericOAuthenticator = Config() + def username_claim(user_info): username = user_info["sub"] if username.startswith("oauth2|cilogon"): @@ -200,6 +202,7 @@ def username_claim(user_info): cilogon_sub_parts = cilogon_sub.split("/") username = f"oauth2|cilogon|{cilogon_sub_parts[3]}|{cilogon_sub_parts[5]}" return username + c.GenericOAuthenticator.username_claim = username_claim c.GenericOAuthenticator.allow_all = True authenticator = get_authenticator(config=c) From 77a43d11c8435f8d270e2f3a2114baf60723ef94 Mon Sep 17 00:00:00 2001 From: YuviPanda Date: Thu, 18 Jan 2024 14:03:00 -0800 Subject: [PATCH 03/14] Remove redefenition of `username_claim` in Generic --- oauthenticator/generic.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/oauthenticator/generic.py b/oauthenticator/generic.py index 4c7bb81c..4798280d 100644 --- a/oauthenticator/generic.py +++ b/oauthenticator/generic.py @@ -60,20 +60,6 @@ def _login_service_default(self): """, ) - username_claim = Union( - [Unicode(os.environ.get('OAUTH2_USERNAME_KEY', 'username')), Callable()], - config=True, - help=""" - When `userdata_url` returns a json response, the username will be taken - from this key. - - Can be a string key name or a callable that accepts the returned - userdata json (as a dict) and returns the username. The callable is - useful e.g. for extracting the username from a nested object in the - response. - """, - ) - @default("http_client") def _default_http_client(self): return AsyncHTTPClient( From 5c1a909888c6ba1baf82fb94d7964904070ec956 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 5 Feb 2024 20:26:49 +0000 Subject: [PATCH 04/14] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/psf/black: 23.12.1 → 24.1.1](https://github.com/psf/black/compare/23.12.1...24.1.1) - [github.com/pycqa/flake8: 6.1.0 → 7.0.0](https://github.com/pycqa/flake8/compare/6.1.0...7.0.0) --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5706f2ed..d297dab9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -34,7 +34,7 @@ repos: # Autoformat: Python code - repo: https://github.com/psf/black - rev: 23.12.1 + rev: 24.1.1 hooks: - id: black @@ -64,7 +64,7 @@ repos: # Lint: Python code - repo: https://github.com/pycqa/flake8 - rev: "6.1.0" + rev: "7.0.0" hooks: - id: flake8 From 968f82eea04e1b2d607f9389850c2f4aad285055 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 5 Feb 2024 20:28:39 +0000 Subject: [PATCH 05/14] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- docs/source/how-to/example-oauthenticator.py | 1 + examples/auth_state/jupyterhub_config.py | 1 + oauthenticator/auth0.py | 1 + oauthenticator/azuread.py | 1 + oauthenticator/bitbucket.py | 1 + oauthenticator/cilogon.py | 1 + oauthenticator/generic.py | 1 + oauthenticator/github.py | 1 + oauthenticator/gitlab.py | 1 + oauthenticator/globus.py | 1 + oauthenticator/google.py | 1 + oauthenticator/mediawiki.py | 1 + oauthenticator/oauth2.py | 1 + oauthenticator/openshift.py | 1 + oauthenticator/tests/conftest.py | 1 + oauthenticator/tests/mocks.py | 1 + oauthenticator/tests/test_azuread.py | 1 + 17 files changed, 17 insertions(+) diff --git a/docs/source/how-to/example-oauthenticator.py b/docs/source/how-to/example-oauthenticator.py index 4fd44336..c139ee14 100644 --- a/docs/source/how-to/example-oauthenticator.py +++ b/docs/source/how-to/example-oauthenticator.py @@ -1,6 +1,7 @@ """ Example OAuthenticator to use with My Service """ + from jupyterhub.auth import LocalAuthenticator from oauthenticator.oauth2 import OAuthenticator, OAuthLoginHandler diff --git a/examples/auth_state/jupyterhub_config.py b/examples/auth_state/jupyterhub_config.py index dbb143d3..2852361f 100644 --- a/examples/auth_state/jupyterhub_config.py +++ b/examples/auth_state/jupyterhub_config.py @@ -5,6 +5,7 @@ 2. pass select auth_state to Spawner via environment variables 3. enable auth_state via `JUPYTERHUB_CRYPT_KEY` and `enable_auth_state = True` """ + import os import pprint import warnings diff --git a/oauthenticator/auth0.py b/oauthenticator/auth0.py index 31b2fa27..b9fa2157 100644 --- a/oauthenticator/auth0.py +++ b/oauthenticator/auth0.py @@ -1,6 +1,7 @@ """ A JupyterHub authenticator class for use with Auth0 as an identity provider. """ + import os from jupyterhub.auth import LocalAuthenticator diff --git a/oauthenticator/azuread.py b/oauthenticator/azuread.py index 48359864..e4e6682b 100644 --- a/oauthenticator/azuread.py +++ b/oauthenticator/azuread.py @@ -1,6 +1,7 @@ """ A JupyterHub authenticator class for use with Azure AD as an identity provider. """ + import os import jwt diff --git a/oauthenticator/bitbucket.py b/oauthenticator/bitbucket.py index 8221bbfa..6a836f2e 100644 --- a/oauthenticator/bitbucket.py +++ b/oauthenticator/bitbucket.py @@ -1,6 +1,7 @@ """ A JupyterHub authenticator class for use with Bitbucket as an identity provider. """ + import os from jupyterhub.auth import LocalAuthenticator diff --git a/oauthenticator/cilogon.py b/oauthenticator/cilogon.py index b11739fc..bf08f14e 100644 --- a/oauthenticator/cilogon.py +++ b/oauthenticator/cilogon.py @@ -1,6 +1,7 @@ """ A JupyterHub authenticator class for use with CILogon as an identity provider. """ + import os from fnmatch import fnmatch from urllib.parse import urlparse diff --git a/oauthenticator/generic.py b/oauthenticator/generic.py index 4dd88bb6..237c7d54 100644 --- a/oauthenticator/generic.py +++ b/oauthenticator/generic.py @@ -1,6 +1,7 @@ """ A JupyterHub authenticator class for use with any OAuth2 based identity provider. """ + import os from functools import reduce diff --git a/oauthenticator/github.py b/oauthenticator/github.py index 29535e81..fcc63b3b 100644 --- a/oauthenticator/github.py +++ b/oauthenticator/github.py @@ -1,6 +1,7 @@ """ A JupyterHub authenticator class for use with GitHub as an identity provider. """ + import json import os import warnings diff --git a/oauthenticator/gitlab.py b/oauthenticator/gitlab.py index 1d32fe34..dde71b15 100644 --- a/oauthenticator/gitlab.py +++ b/oauthenticator/gitlab.py @@ -1,6 +1,7 @@ """ A JupyterHub authenticator class for use with GitLab as an identity provider. """ + import os import warnings from urllib.parse import quote diff --git a/oauthenticator/globus.py b/oauthenticator/globus.py index 1e19a80d..09066e85 100644 --- a/oauthenticator/globus.py +++ b/oauthenticator/globus.py @@ -1,6 +1,7 @@ """ A JupyterHub authenticator class for use with Globus as an identity provider. """ + import base64 import os import pickle diff --git a/oauthenticator/google.py b/oauthenticator/google.py index 49f506d0..0fad0152 100644 --- a/oauthenticator/google.py +++ b/oauthenticator/google.py @@ -1,6 +1,7 @@ """ A JupyterHub authenticator class for use with Google as an identity provider. """ + import os from jupyterhub.auth import LocalAuthenticator diff --git a/oauthenticator/mediawiki.py b/oauthenticator/mediawiki.py index ee3c46bd..04677f83 100644 --- a/oauthenticator/mediawiki.py +++ b/oauthenticator/mediawiki.py @@ -1,6 +1,7 @@ """ A JupyterHub authenticator class for use with MediaWiki as an identity provider. """ + import json import os from asyncio import wrap_future diff --git a/oauthenticator/oauth2.py b/oauthenticator/oauth2.py index 9ba66ecf..60edeb36 100644 --- a/oauthenticator/oauth2.py +++ b/oauthenticator/oauth2.py @@ -3,6 +3,7 @@ Founded based on work by Kyle Kelley (@rgbkrk) """ + import base64 import json import os diff --git a/oauthenticator/openshift.py b/oauthenticator/openshift.py index 599f989f..6b016b0d 100644 --- a/oauthenticator/openshift.py +++ b/oauthenticator/openshift.py @@ -1,6 +1,7 @@ """ A JupyterHub authenticator class for use with OpenShift as an identity provider. """ + import concurrent.futures import json import os diff --git a/oauthenticator/tests/conftest.py b/oauthenticator/tests/conftest.py index 30fe93e0..3ffbdf8c 100644 --- a/oauthenticator/tests/conftest.py +++ b/oauthenticator/tests/conftest.py @@ -1,4 +1,5 @@ """Py.Test fixtures""" + from pytest import fixture from tornado.httpclient import AsyncHTTPClient diff --git a/oauthenticator/tests/mocks.py b/oauthenticator/tests/mocks.py index 83909eb2..f59c537a 100644 --- a/oauthenticator/tests/mocks.py +++ b/oauthenticator/tests/mocks.py @@ -1,4 +1,5 @@ """Mocking utilities for testing""" + import json import os import re diff --git a/oauthenticator/tests/test_azuread.py b/oauthenticator/tests/test_azuread.py index b3537dd5..13be6b0d 100644 --- a/oauthenticator/tests/test_azuread.py +++ b/oauthenticator/tests/test_azuread.py @@ -1,4 +1,5 @@ """test azure ad""" + import json import os import re From 64783c2b5b32df3647554dc274330a418c7d4fcb Mon Sep 17 00:00:00 2001 From: Min RK Date: Fri, 9 Feb 2024 11:24:48 +0100 Subject: [PATCH 06/14] add dedicated doc on details of allowing access --- docs/source/index.md | 1 + docs/source/topic/allowing.md | 143 ++++++++++++++++++++++++++++++++++ oauthenticator/oauth2.py | 27 ++++++- 3 files changed, 167 insertions(+), 4 deletions(-) create mode 100644 docs/source/topic/allowing.md diff --git a/docs/source/index.md b/docs/source/index.md index e3663520..dbd98d2c 100644 --- a/docs/source/index.md +++ b/docs/source/index.md @@ -47,6 +47,7 @@ Topic guides go more in-depth on a particular topic. :maxdepth: 2 :caption: Topic guides +topic/allowing topic/extending ``` diff --git a/docs/source/topic/allowing.md b/docs/source/topic/allowing.md new file mode 100644 index 00000000..2af7e3c7 --- /dev/null +++ b/docs/source/topic/allowing.md @@ -0,0 +1,143 @@ +(allowing)= + +# Allowing access to your JupyterHub + +OAuthenticator is about deferring **authentication** to an external source, +assuming your users all have accounts _somewhere_. +But many of these sources (e.g. Google, GitHub) have _lots_ of users, and you don't want _all_ of them to be able to use your hub. +This is where **authorization** comes in. + +In OAuthenticator, authorization is represented via configuration options that start with `allow` or `block`. + +There are also lots of OAuth providers, and as a result, lots of ways to tell OAuthenticator who should be allowed to access your hub. + +## Default behavior: nobody is allowed! + +Assuming you have provided no `allow` configuration, the default behavior of OAuthenticator (starting with version 16) is to not allow any users unless explicitly authorized via _some_ `allow` configuration. +If you want anyone to be able to use your hub, you must specify at least one `allow` configuration. + +```{versionchanged} 16 +Prior to OAuthenticator 16, `allow_all` was _implied_ if `allowed_users` was not specified. +Starting from 16, `allow_all` can only be enabled explicitly. +``` + +## Allowing access + +There are several `allow_` configuration options, to grant access to users according to different rules. + +When you have only one `allow` configuration, the behavior is generally unambiguous: anyone allowed by the rule can login to the Hub, while anyone not explicitly allowed cannot login. +However, once you start adding additional `allow` configuration, there is some ambiguity in how multiple rules are combined. + +```{important} +Additional allow rules **can only grant access**, meaning they only _expand_ who has access to your hub. +Adding an `allow` rule cannot prevent access granted by another `allow` rule. +To block access, use `block` configuration. +``` + +That is, if a user is granted access by _any_ `allow` configuration, they are allowed. +An allow rule cannot _exclude_ access granted by another `allow` rule. + +An example: + +```python +c.GitHubOAuthenticator.allowed_users = {"mensah", "art"} +c.GitHubOAuthenticator.allowed_organizations = {"preservation"} +``` + +means that the users `mensah` and `art` are allowed, _and_ any member of the `preservation` organization are allowed. +Any user that doesn't meet any of the allow rules will not be allowed. + +| user | allowed | reason | +| ----- | ------- | ------------------------------------------------------- | +| art | True | in `allowed_users` | +| amena | True | member of `preservation` | +| tlacy | False | not in `allowed_users` and not member of `preservation` | + +### `allow_all` + +The first and simplest way to allow access is to any user who can successfully authenticate: + +```python +c.OAuthenticator.allow_all = True +``` + +This is appropriate when you use an authentication provider (e.g. an institutional single-sign-on provider), where everyone who has an account in the provider should have access to your Hub. +It may also be appropriate for unadvertised short-lived hubs, e.g. dedicated hubs for workshops that will be shutdown after a day, where you may decide it is acceptable to allow anyone who finds your hub to login. + +If `allow_all` is enabled, no other `allow` configuration will have any effect. + +```{seealso} +Configuration documentation for {attr}`.OAuthenticator.allow_all` +``` + +### `allowed_users` + +This is top-level JupyterHub configuration, shared by all Authenticators. +This specifies a list of users that are allowed by name. +This is the simplest authorization mechanism when you have a small group of users whose usernames you know: + +```python +c.OAuthenticator.allowed_users = {"mensah", "ratthi"} +``` + +If this is your only configuration, only these users will be allowed, no others. + +Note that any additional usernames in the deprecated `admin_users` configuration will also be added to the `allowed_users` set. + +```{seealso} +Configuration documentation for {attr}`.OAuthenticator.allowed_users` +``` + +### `allow_existing_users` + +JupyterHub can allow you to add and remove users while the Hub is running via the admin page. +If you add or remove users this way, they will be added to the JupyterHub database, but their ability to login will not be affected unless they are also granted access via an `allow` rule. + +To enable managing users via the admin panel, set + +```python +c.OAuthenticator.allow_existing_users = True +``` + +```{warning} +Enabling `allow_existing_users` means that _removing_ users from any explicit allow mechanisms will no longer revoke their access. +Once the user has been added to the database, the only way to revoke their access to the hub is to remove the user from JupyterHub entirely, via the admin page. +``` + +```{seealso} +Configuration documentation for {attr}`.OAuthenticator.allow_existing_users` +``` + +### provider-specific rules + +Each OAuthenticator provider may have its own provider-specific rules to allow groups of users access, such as: + +- {attr}`.CILogonOAuthenticator.allowed_idps` +- {attr}`.GitHubOAuthenticator.allowed_organizations` +- {attr}`.GitLabOAuthenticator.allowed_gitlab_groups` +- {attr}`.GlobusOAuthenticator.allowed_globus_groups` +- {attr}`.GoogleOAuthenticator.allowed_google_groups` + +## Blocking Access + +It's possible that you want to limit who has access to your Hub to less than all of the users granted access by your `allow` configuration. +`block` configuration always has higher priority than `allow` configuration, so if a user is explicitly allowed _and_ explicitly blocked, they will not be able to login. + +The only `block` configuration is the base Authenticators `block_users`, +a set of usernames that will not be allowed to login. + +### Revoking previously-allowed access + +Any users who have logged in previously will be present in the JupyterHub database. +Removing a user's login permissions (e.g. removing them from a GitLab project when using {attr}`.GitLabOAuthenticator.project_ids`) only prevents future logins; +it does not remove the user from the JupyterHub database. +This means that: + +1. any API tokens, that the user still has access to will continue to be valid, and can continue to be used, and +2. any still-valid browser sessions will continue to be logged in. + +```{important} +To fully remove a user's access to JupyterHub, +their login permission must be revoked _and_ their User fully deleted from the Hub, +e.g. via the admin page. +``` diff --git a/oauthenticator/oauth2.py b/oauthenticator/oauth2.py index 19ce762c..18d92afa 100644 --- a/oauthenticator/oauth2.py +++ b/oauthenticator/oauth2.py @@ -269,6 +269,8 @@ class OAuthenticator(Authenticator): help=""" Allow all authenticated users to login. + Overrides all other `allow` configuration. + .. versionadded:: 16.0 """, ) @@ -280,7 +282,7 @@ class OAuthenticator(Authenticator): Allow existing users to login. An existing user is a user in JupyterHub's database of users, and it - includes all users that has previously logged in. + includes all users that have previously logged in. .. warning:: @@ -291,9 +293,9 @@ class OAuthenticator(Authenticator): .. warning:: - When this is enabled and you are to remove access for one or more - users allowed via other config options, you must make sure that they - are not part of the database of users still. This can be tricky to do + When this is enabled and you wish to remove access for one or more + users previously allowed, you must make sure that they + are not removed from the jupyterhub database. This can be tricky to do if you stop allowing a group of externally managed users for example. With this enabled, JupyterHub admin users can visit `/hub/admin` or use @@ -1086,3 +1088,20 @@ def __init__(self, **kwargs): self._deprecated_oauth_trait, names=list(self._deprecated_oauth_aliases) ) super().__init__(**kwargs) + + +# patch allowed_users help string to match our definition +# base Authenticator class help string gives the wrong impression +# when combined with other allow options +OAuthenticator.class_traits()[ + "allowed_users" +].help = """ +Set of usernames that should be allowed to login. + +If unspecified, grants no access. + +At least one `allow` configuration must be specified +if any users are to have permission to access the Hub. + +Any users in `admin_users` will be added to this set. +""" From f26b4547af71d344f954a759a4074a31fda117c5 Mon Sep 17 00:00:00 2001 From: Min RK Date: Fri, 9 Feb 2024 12:20:17 +0100 Subject: [PATCH 07/14] add example for deploying with mock-oauth2-server useful for testing --- examples/mock-provider/README.md | 21 ++++++++++++++++ examples/mock-provider/jupyterhub_config.py | 27 +++++++++++++++++++++ 2 files changed, 48 insertions(+) create mode 100644 examples/mock-provider/README.md create mode 100644 examples/mock-provider/jupyterhub_config.py diff --git a/examples/mock-provider/README.md b/examples/mock-provider/README.md new file mode 100644 index 00000000..61adee27 --- /dev/null +++ b/examples/mock-provider/README.md @@ -0,0 +1,21 @@ +# Generic OAuth with mock provider + +This example uses [mock-oauth2-server][] to launch a standalone local OAuth2 provider and configures GenericOAuthenticator to use it. + +mock-auth2-server implements OpenID Connect (OIDC), and can be used to test GenericOAuthenticator configurations for use with OIDC providers without needing to register your application with a real OAuth provider. + +[mock-oauth2-server]: https://github.com/navikt/mock-oauth2-server + +To launch the oauth provider in a container: + +``` +docker run --rm -it -p 127.0.0.1:8080:8080 ghcr.io/navikt/mock-oauth2-server:2.1.1 +``` + +Then launch JupyterHub: + +``` +jupyterhub +``` + +When you login, you will be presented with a form allowing you to specify the username, and (optionally) any additional fields that should be present in the `userinfo` response. diff --git a/examples/mock-provider/jupyterhub_config.py b/examples/mock-provider/jupyterhub_config.py new file mode 100644 index 00000000..60c9f09a --- /dev/null +++ b/examples/mock-provider/jupyterhub_config.py @@ -0,0 +1,27 @@ +c = get_config() # noqa + +c.JupyterHub.authenticator_class = "generic-oauth" + +# assumes oauth provider run with: +# docker run --rm -it -p 127.0.0.1:8080:8080 ghcr.io/navikt/mock-oauth2-server:2.1.1 + +provider = "http://127.0.0.1:8080/default" +c.GenericOAuthenticator.authorize_url = f"{provider}/authorize" +c.GenericOAuthenticator.token_url = f"{provider}/token" +c.GenericOAuthenticator.userdata_url = f"{provider}/userinfo" +c.GenericOAuthenticator.scope = ["openid", "somescope", "otherscope"] + +# these are the defaults. They can be configured at http://localhost:8080/default/debugger +c.GenericOAuthenticator.client_id = "debugger" +c.GenericOAuthenticator.client_secret = "someSecret" + +# 'sub' is the first field in the login form +c.GenericOAuthenticator.username_claim = "sub" + +c.GenericOAuthenticator.allow_all = True +c.GenericOAuthenticator.admin_users = {"admin"} + +# demo boilerplate +c.JupyterHub.default_url = "/hub/home" +c.JupyterHub.spawner_class = "simple" +c.JupyterHub.ip = "127.0.0.1" From 9445181c7a40411ef3cd4d92d1582efbe9ed8b28 Mon Sep 17 00:00:00 2001 From: Min RK Date: Fri, 9 Feb 2024 14:58:07 +0100 Subject: [PATCH 08/14] nicer short links Co-authored-by: Erik Sundell --- examples/mock-provider/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/mock-provider/README.md b/examples/mock-provider/README.md index 61adee27..8e022e19 100644 --- a/examples/mock-provider/README.md +++ b/examples/mock-provider/README.md @@ -1,6 +1,6 @@ # Generic OAuth with mock provider -This example uses [mock-oauth2-server][] to launch a standalone local OAuth2 provider and configures GenericOAuthenticator to use it. +This example uses [mock-oauth2-server] to launch a standalone local OAuth2 provider and configures GenericOAuthenticator to use it. mock-auth2-server implements OpenID Connect (OIDC), and can be used to test GenericOAuthenticator configurations for use with OIDC providers without needing to register your application with a real OAuth provider. From 0399aa59c3ca81d36242bd901754f6cee5c9d7f6 Mon Sep 17 00:00:00 2001 From: Min RK Date: Fri, 9 Feb 2024 15:01:11 +0100 Subject: [PATCH 09/14] Apply suggestions from code review Co-authored-by: Simon Li --- docs/source/topic/allowing.md | 8 ++++---- oauthenticator/oauth2.py | 6 ++---- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/docs/source/topic/allowing.md b/docs/source/topic/allowing.md index 2af7e3c7..ba655392 100644 --- a/docs/source/topic/allowing.md +++ b/docs/source/topic/allowing.md @@ -13,7 +13,7 @@ There are also lots of OAuth providers, and as a result, lots of ways to tell OA ## Default behavior: nobody is allowed! -Assuming you have provided no `allow` configuration, the default behavior of OAuthenticator (starting with version 16) is to not allow any users unless explicitly authorized via _some_ `allow` configuration. +The default behavior of OAuthenticator (starting with version 16) is to block all users unless explicitly authorized via _some_ `allow` configuration. If you want anyone to be able to use your hub, you must specify at least one `allow` configuration. ```{versionchanged} 16 @@ -91,7 +91,7 @@ Configuration documentation for {attr}`.OAuthenticator.allowed_users` ### `allow_existing_users` JupyterHub can allow you to add and remove users while the Hub is running via the admin page. -If you add or remove users this way, they will be added to the JupyterHub database, but their ability to login will not be affected unless they are also granted access via an `allow` rule. +If you add or remove users this way, they will be added to the JupyterHub database, but they will not be able to login unless they are also granted access via an `allow` rule. To enable managing users via the admin panel, set @@ -121,7 +121,7 @@ Each OAuthenticator provider may have its own provider-specific rules to allow g ## Blocking Access It's possible that you want to limit who has access to your Hub to less than all of the users granted access by your `allow` configuration. -`block` configuration always has higher priority than `allow` configuration, so if a user is explicitly allowed _and_ explicitly blocked, they will not be able to login. +`block` configuration always has higher priority than `allow` configuration, so if a user is both allowed _and_ blocked, they will not be able to login. The only `block` configuration is the base Authenticators `block_users`, a set of usernames that will not be allowed to login. @@ -133,7 +133,7 @@ Removing a user's login permissions (e.g. removing them from a GitLab project wh it does not remove the user from the JupyterHub database. This means that: -1. any API tokens, that the user still has access to will continue to be valid, and can continue to be used, and +1. any API tokens that the user still has access to will continue to be valid, and can continue to be used 2. any still-valid browser sessions will continue to be logged in. ```{important} diff --git a/oauthenticator/oauth2.py b/oauthenticator/oauth2.py index 18d92afa..ecb4ce92 100644 --- a/oauthenticator/oauth2.py +++ b/oauthenticator/oauth2.py @@ -295,7 +295,7 @@ class OAuthenticator(Authenticator): When this is enabled and you wish to remove access for one or more users previously allowed, you must make sure that they - are not removed from the jupyterhub database. This can be tricky to do + are removed from the jupyterhub database. This can be tricky to do if you stop allowing a group of externally managed users for example. With this enabled, JupyterHub admin users can visit `/hub/admin` or use @@ -1098,9 +1098,7 @@ def __init__(self, **kwargs): ].help = """ Set of usernames that should be allowed to login. -If unspecified, grants no access. - -At least one `allow` configuration must be specified +If unspecified, grants no access. You must set at least one other `allow` configuration if any users are to have permission to access the Hub. Any users in `admin_users` will be added to this set. From 609911205f28e3187d8b7250e11144b85e995bba Mon Sep 17 00:00:00 2001 From: Min RK Date: Mon, 12 Feb 2024 09:42:57 +0100 Subject: [PATCH 10/14] try to simplify and clarify allow_existing_users docstring --- oauthenticator/oauth2.py | 26 +++++++++----------------- 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/oauthenticator/oauth2.py b/oauthenticator/oauth2.py index ecb4ce92..425924c5 100644 --- a/oauthenticator/oauth2.py +++ b/oauthenticator/oauth2.py @@ -281,15 +281,19 @@ class OAuthenticator(Authenticator): help=""" Allow existing users to login. - An existing user is a user in JupyterHub's database of users, and it - includes all users that have previously logged in. + Enable this if you want to manage user access via the JupyterHub admin page (/hub/admin). + + With this enabled, all users present in the JupyterHub database are allowed to login. + This has the effect of any user who has _previously_ been allowed to login + via any means will continue to be allowed until the user is deleted via the /hub/admin page + or REST API. .. warning:: Before enabling this you should review the existing users in the JupyterHub admin panel at `/hub/admin`. You may find users existing - there because they have once been declared in config such as - `allowed_users` or once been allowed to sign in. + there because they have previously been declared in config such as + `allowed_users` or allowed to sign in. .. warning:: @@ -299,19 +303,7 @@ class OAuthenticator(Authenticator): if you stop allowing a group of externally managed users for example. With this enabled, JupyterHub admin users can visit `/hub/admin` or use - JupyterHub's REST API to add and remove users as a way to allow them - access. - - The username for existing users must match the normalized username - returned by the authenticator. When creating users, only lowercase - letters should be used unless `MWOAuthenticator` is used. - - .. note:: - - Allowing existing users is done by adding existing users on startup - and newly created users to the `allowed_users` set. Due to that, you - can't rely on this config to independently allow existing users if - you for example would reset `allowed_users` after startup. + JupyterHub's REST API to add and remove users to manage who can login. .. versionadded:: 16.0 From df77443d45b1ecf52c18ae2c944906a1f719366b Mon Sep 17 00:00:00 2001 From: Min RK Date: Mon, 12 Feb 2024 09:43:35 +0100 Subject: [PATCH 11/14] remove allowed_idps from example allow config it's not typical --- docs/source/topic/allowing.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/source/topic/allowing.md b/docs/source/topic/allowing.md index ba655392..81285032 100644 --- a/docs/source/topic/allowing.md +++ b/docs/source/topic/allowing.md @@ -112,7 +112,6 @@ Configuration documentation for {attr}`.OAuthenticator.allow_existing_users` Each OAuthenticator provider may have its own provider-specific rules to allow groups of users access, such as: -- {attr}`.CILogonOAuthenticator.allowed_idps` - {attr}`.GitHubOAuthenticator.allowed_organizations` - {attr}`.GitLabOAuthenticator.allowed_gitlab_groups` - {attr}`.GlobusOAuthenticator.allowed_globus_groups` From 2d5f9beabc00eef6620080877122e9979ea666ce Mon Sep 17 00:00:00 2001 From: Min RK Date: Mon, 12 Feb 2024 09:55:10 +0100 Subject: [PATCH 12/14] allow_all was only implied when _no_ allow config was specified --- docs/source/topic/allowing.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/topic/allowing.md b/docs/source/topic/allowing.md index 81285032..2ca4f1c7 100644 --- a/docs/source/topic/allowing.md +++ b/docs/source/topic/allowing.md @@ -17,7 +17,7 @@ The default behavior of OAuthenticator (starting with version 16) is to block al If you want anyone to be able to use your hub, you must specify at least one `allow` configuration. ```{versionchanged} 16 -Prior to OAuthenticator 16, `allow_all` was _implied_ if `allowed_users` was not specified. +Prior to OAuthenticator 16, `allow_all` was _implied_ if no other `allow` configuration was specified. Starting from 16, `allow_all` can only be enabled explicitly. ``` From 4874818cf2fa4917ec650c44f347d711cf63e107 Mon Sep 17 00:00:00 2001 From: Min RK Date: Mon, 12 Feb 2024 10:06:41 +0100 Subject: [PATCH 13/14] Apply suggestions from code review Co-authored-by: Erik Sundell --- docs/source/topic/allowing.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/topic/allowing.md b/docs/source/topic/allowing.md index 2ca4f1c7..663abf56 100644 --- a/docs/source/topic/allowing.md +++ b/docs/source/topic/allowing.md @@ -108,7 +108,7 @@ Once the user has been added to the database, the only way to revoke their acces Configuration documentation for {attr}`.OAuthenticator.allow_existing_users` ``` -### provider-specific rules +### Provider-specific rules Each OAuthenticator provider may have its own provider-specific rules to allow groups of users access, such as: @@ -137,6 +137,6 @@ This means that: ```{important} To fully remove a user's access to JupyterHub, -their login permission must be revoked _and_ their User fully deleted from the Hub, +their login permission must be revoked _and_ their user fully deleted from the Hub, e.g. via the admin page. ``` From eb30a9bf83e7c2da01ac8b9eabbbcbc2e2f2e3ec Mon Sep 17 00:00:00 2001 From: Min RK Date: Mon, 12 Feb 2024 10:10:09 +0100 Subject: [PATCH 14/14] clarify impact of admin_users withotu specifying implementation detail --- docs/source/topic/allowing.md | 2 +- oauthenticator/oauth2.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/source/topic/allowing.md b/docs/source/topic/allowing.md index 663abf56..0d88b7ce 100644 --- a/docs/source/topic/allowing.md +++ b/docs/source/topic/allowing.md @@ -82,7 +82,7 @@ c.OAuthenticator.allowed_users = {"mensah", "ratthi"} If this is your only configuration, only these users will be allowed, no others. -Note that any additional usernames in the deprecated `admin_users` configuration will also be added to the `allowed_users` set. +Note that any additional usernames in the deprecated `admin_users` configuration will also be allowed to login. ```{seealso} Configuration documentation for {attr}`.OAuthenticator.allowed_users` diff --git a/oauthenticator/oauth2.py b/oauthenticator/oauth2.py index 425924c5..8296f342 100644 --- a/oauthenticator/oauth2.py +++ b/oauthenticator/oauth2.py @@ -1093,5 +1093,5 @@ def __init__(self, **kwargs): If unspecified, grants no access. You must set at least one other `allow` configuration if any users are to have permission to access the Hub. -Any users in `admin_users` will be added to this set. +Any usernames in `admin_users` will also be allowed to login. """