diff --git a/jupyterhub/auth.py b/jupyterhub/auth.py index 048f1d82..f95ece37 100644 --- a/jupyterhub/auth.py +++ b/jupyterhub/auth.py @@ -235,6 +235,7 @@ class Authenticator(LoggingConfigurable): 'name': authenticated, } authenticated.setdefault('auth_state', None) + authenticated.setdefault('admin', None) # normalize the username authenticated['name'] = username = self.normalize_username(authenticated['name']) @@ -269,10 +270,10 @@ class Authenticator(LoggingConfigurable): Returns: user (str or dict or None): The username of the authenticated user, or None if Authentication failed. - If the Authenticator has state associated with the user, - it can return a dict with the keys 'name' and 'auth_state', - where 'name' is the username and 'auth_state' is a dictionary - of auth state that will be persisted. + The Authenticator may return a dict instead, which MUST have a + key 'name' holding the username, and may have two optional keys + set - 'auth_state', a dictionary of of auth state that will be + persisted; and 'admin', the admin setting value for the user. """ def pre_spawn_start(self, user, spawner): diff --git a/jupyterhub/handlers/base.py b/jupyterhub/handlers/base.py index d603da6a..2dc51318 100644 --- a/jupyterhub/handlers/base.py +++ b/jupyterhub/handlers/base.py @@ -336,7 +336,11 @@ class BaseHandler(RequestHandler): if authenticated: username = authenticated['name'] auth_state = authenticated.get('auth_state') + admin = authenticated.get('admin') user = self.user_from_username(username) + # Only set `admin` if the authenticator returned an explicit value. + if admin is not None: + user.admin = admin # always set auth_state and commit, # because there could be key-rotation or clearing of previous values # going on. diff --git a/jupyterhub/tests/mocking.py b/jupyterhub/tests/mocking.py index 703ea015..63f283b9 100644 --- a/jupyterhub/tests/mocking.py +++ b/jupyterhub/tests/mocking.py @@ -138,6 +138,8 @@ class FormSpawner(MockSpawner): class MockPAMAuthenticator(PAMAuthenticator): auth_state = None + # If true, return admin users marked as admin. + return_admin = False @default('admin_users') def _admin_users_default(self): return {'admin'} @@ -161,6 +163,11 @@ class MockPAMAuthenticator(PAMAuthenticator): 'name': username, 'auth_state': self.auth_state, } + elif self.return_admin: + return { + 'name': username, + 'admin': username in self.admin_users, + } else: return username diff --git a/jupyterhub/tests/test_auth.py b/jupyterhub/tests/test_auth.py index 93aa8550..3ad172ad 100644 --- a/jupyterhub/tests/test_auth.py +++ b/jupyterhub/tests/test_auth.py @@ -201,6 +201,49 @@ def test_auth_state(app, auth_state_enabled): assert auth_state == app.authenticator.auth_state +@pytest.fixture +def use_auth_admin(app): + before_admin = app.authenticator.return_admin + app.authenticator.return_admin = True + try: + yield + finally: + app.authenticator.return_admin = before_admin + + +@pytest.mark.gen_test +def test_auth_admin_non_admin(app, use_auth_admin): + """admin should be passed through for non-admin users""" + name = 'kiwi' + user = add_user(app.db, app, name=name, admin=False) + assert user.admin is False + cookies = yield app.login_user(name) + assert user.admin is False + + +@pytest.mark.gen_test +def test_auth_admin_is_admin(app, use_auth_admin): + """admin should be passed through for admin users""" + # Admin user defined in MockPAMAuthenticator. + name = 'admin' + user = add_user(app.db, app, name=name, admin=False) + assert user.admin is False + cookies = yield app.login_user(name) + assert user.admin is True + + +@pytest.mark.gen_test +def test_auth_admin_retained_if_unset(app): + """admin should be unchanged if authenticator doesn't return admin value""" + name = 'kiwi' + # Add user as admin. + user = add_user(app.db, app, name=name, admin=True) + assert user.admin is True + # User should remain unchanged. + cookies = yield app.login_user(name) + assert user.admin is True + + @pytest.fixture def auth_state_unavailable(auth_state_enabled): """auth_state enabled at the Authenticator level,