From 1c1ee5f175b1f7194e3e5ca7d04af14d97fa9409 Mon Sep 17 00:00:00 2001 From: flongford Date: Thu, 14 Apr 2022 20:06:30 +0100 Subject: [PATCH 01/18] FEAT: add Spawner options for to Admin logins --- remoteappmanager/jupyterhub/spawners.py | 41 +++++++++++++++++-- .../jupyterhub/tests/test_spawners.py | 18 ++++++++ 2 files changed, 55 insertions(+), 4 deletions(-) diff --git a/remoteappmanager/jupyterhub/spawners.py b/remoteappmanager/jupyterhub/spawners.py index 682b5b83..312bac14 100644 --- a/remoteappmanager/jupyterhub/spawners.py +++ b/remoteappmanager/jupyterhub/spawners.py @@ -8,6 +8,9 @@ from jupyterhub.spawner import LocalProcessSpawner from jupyterhub import orm +ADMIN_CMD = "remoteappadmin" +USER_CMD = "remoteappmanager" + class BaseSpawner(LocalProcessSpawner): """Base class that provides common infrastructure to @@ -25,10 +28,11 @@ class BaseSpawner(LocalProcessSpawner): def cmd(self): """Overrides the base class traitlet so that we take full control of the spawned command according to user admin status""" - - return (["remoteappadmin"] - if self.user.admin is True - else ["remoteappmanager"]) + if not self.user.admin: + return [USER_CMD] + return ([ADMIN_CMD] + if self.user_options['session'] == "admin" + else [USER_CMD]) def __init__(self, **kwargs): super().__init__(**kwargs) @@ -38,6 +42,35 @@ def __init__(self, **kwargs): # contain only one. self.proxy = self.db.query(orm.Proxy).first() + def _options_form_default(self): + """ Gives admins the option of spawning either RemoteAppManager + admin or user sessions + """ + if self.user.admin: + return """ +
+ Choose RemoteAppManager Session: + +
+ """ + return "" + + def _default_session(self): + return "admin" if self.user.admin else "user" + + def _default_user_options(self): + return {"session": self._default_session()} + + def options_from_form(self, form_data): + """ Attempt to extract session selection from HTML form and + return default session if not available + """ + session = form_data.get("session", [self._default_session()])[0] + return {'session': session} + def get_args(self): args = super().get_args() diff --git a/remoteappmanager/jupyterhub/tests/test_spawners.py b/remoteappmanager/jupyterhub/tests/test_spawners.py index bc185128..d23437fe 100644 --- a/remoteappmanager/jupyterhub/tests/test_spawners.py +++ b/remoteappmanager/jupyterhub/tests/test_spawners.py @@ -163,6 +163,24 @@ def setUp(self): def test_cmd(self): self.assertEqual(self.spawner.cmd, ['remoteappadmin']) + def test_cmd_user_session_override(self): + self.spawner.user_options = {"session": "user"} + self.assertEqual(self.spawner.cmd, ['remoteappmanager']) + + def test_parse_options_from_form(self): + self.assertEqual( + self.spawner.options_from_form({}), + {"session": "admin"} + ) + self.assertEqual( + self.spawner.options_from_form({"session": ["user"]}), + {"session": "user"} + ) + self.assertEqual( + self.spawner.options_from_form({"session": ["admin"]}), + {"session": "admin"} + ) + class TestVirtualUserSpawner(TestSystemUserSpawner): def setUp(self): From be85ef4052a55b3483cc2961892e49b6fdd0520e Mon Sep 17 00:00:00 2001 From: flongford Date: Thu, 14 Apr 2022 20:17:41 +0100 Subject: [PATCH 02/18] TST: update selenium tests with admin login and logout routines --- selenium_tests/AdminDriverTest.py | 27 +++++++++++++++++++++++++-- selenium_tests/RemoteAppDriverTest.py | 4 ++-- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/selenium_tests/AdminDriverTest.py b/selenium_tests/AdminDriverTest.py index 96456473..ab82b846 100644 --- a/selenium_tests/AdminDriverTest.py +++ b/selenium_tests/AdminDriverTest.py @@ -4,9 +4,32 @@ class AdminDriverTest(RemoteAppDriverTest): + + def admin_login(self): + """ Login as am admin user. We assume that if you use this routine, + you are currently on the login page. + """ + self.login("admin") + + self.click_first_element_located(By.ID, "start") + self.click_first_element_located(By.CSS_SELECTOR, "input.btn") + self.click_first_element_located(By.ID, "start") + + def admin_logout(self): + """ Admin Logout. Ensures user session is stopped before performing logout. + This action should be generally used whenever the Spawner options form is + presented during logins so that it is correctly displayed for the next test + runner. + """ + self.driver.get(self.base_url + "/home") + + self.click_first_element_located(By.ID, "stop") + self.click_first_element_located(By.ID, "logout") + self.wait_until_text_inside_element_located(By.CSS_SELECTOR, "div.auth-form-header", "Sign in") + def setUp(self): RemoteAppDriverTest.setUp(self) - self.login("admin") + self.admin_login() def wait_until_visibility_of_row(self, row): """ Wait until a specific row is visible @@ -80,5 +103,5 @@ def click_row_action_button(self, row, action_name): ) def tearDown(self): - self.logout() + self.admin_login() RemoteAppDriverTest.tearDown(self) diff --git a/selenium_tests/RemoteAppDriverTest.py b/selenium_tests/RemoteAppDriverTest.py index 7a06cc66..34e5073c 100644 --- a/selenium_tests/RemoteAppDriverTest.py +++ b/selenium_tests/RemoteAppDriverTest.py @@ -247,15 +247,15 @@ def click_modal_footer_button(self, name): def login(self, username="test"): """ Login as a given user name. We assume that if you use this routine, you are currently on the login page. + Parameters ---------- username: String - The name of the user, it can be "admin" if you want to login as admin. + The name of the user, it is not expected to be an admin. Example: -------- login("JohnDoe") - login("admin") """ self.driver.get(self.base_url + "/hub/login") From 799cbe264b78dd56cf0648058a0fe9ea053099e4 Mon Sep 17 00:00:00 2001 From: flongford Date: Thu, 14 Apr 2022 21:38:17 +0100 Subject: [PATCH 03/18] TST: fix to AdminDriverTest logout --- selenium_tests/AdminDriverTest.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/selenium_tests/AdminDriverTest.py b/selenium_tests/AdminDriverTest.py index ab82b846..61526a2a 100644 --- a/selenium_tests/AdminDriverTest.py +++ b/selenium_tests/AdminDriverTest.py @@ -6,8 +6,9 @@ class AdminDriverTest(RemoteAppDriverTest): def admin_login(self): - """ Login as am admin user. We assume that if you use this routine, - you are currently on the login page. + """ Login as an admin user. Handles both entering admin credentials + and selecting appropriate Spawner options. We assume that if you + use this routine, you are currently on the login page. """ self.login("admin") @@ -103,5 +104,5 @@ def click_row_action_button(self, row, action_name): ) def tearDown(self): - self.admin_login() + self.admin_logout() RemoteAppDriverTest.tearDown(self) From 302caacc82adf2d4e25152e5400c2b8b653ea1a3 Mon Sep 17 00:00:00 2001 From: flongford Date: Thu, 14 Apr 2022 21:43:09 +0100 Subject: [PATCH 04/18] TST: include tests for Spawner Options form --- selenium_tests/test_spawner_options_form.py | 44 +++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 selenium_tests/test_spawner_options_form.py diff --git a/selenium_tests/test_spawner_options_form.py b/selenium_tests/test_spawner_options_form.py new file mode 100644 index 00000000..22f8280d --- /dev/null +++ b/selenium_tests/test_spawner_options_form.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +from selenium_tests.RemoteAppDriverTest import RemoteAppDriverTest +from selenium.webdriver.common.by import By + + +class TestSpawnerOptionsForm(RemoteAppDriverTest): + + def admin_logout(self): + """ Admin Logout. Ensures user session is stopped before performing logout. + This action should be generally used whenever the Spawner options form is + presented during logins so that it is correctly displayed for the next test + runner. + """ + self.driver.get(self.base_url + "/home") + + self.click_first_element_located(By.ID, "stop") + self.click_first_element_located(By.ID, "logout") + self.wait_until_text_inside_element_located(By.CSS_SELECTOR, "div.auth-form-header", "Sign in") + + def test_admin_login_default_session(self): + self.login("admin") + + self.click_first_element_located(By.ID, "start") + self.click_first_element_located(By.CSS_SELECTOR, "input.btn") + self.click_first_element_located(By.ID, "start") + + self.wait_until_text_inside_element_located( + By.CSS_SELECTOR, ".header", "ADMIN") + + def test_admin_login_user_session(self): + self.login("admin") + + self.click_first_element_located(By.ID, "start") + self.click_first_element_located( + By.CSS_SELECTOR, "# spawner_form > options:nth-child(2)") + self.click_first_element_located(By.CSS_SELECTOR, "input.btn") + self.click_first_element_located(By.ID, "start") + + self.wait_until_text_inside_element_located( + By.CSS_SELECTOR, ".header", "APPLICATIONS") + + def tearDown(self): + self.admin_logout() + RemoteAppDriverTest.tearDown(self) From ae5d4fa45947de5a34a2a79670b20f9491c0c170 Mon Sep 17 00:00:00 2001 From: flongford Date: Thu, 14 Apr 2022 21:46:16 +0100 Subject: [PATCH 05/18] FIX: explictly include default methods on BaseSpawner --- remoteappmanager/jupyterhub/spawners.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/remoteappmanager/jupyterhub/spawners.py b/remoteappmanager/jupyterhub/spawners.py index 312bac14..321e4b5d 100644 --- a/remoteappmanager/jupyterhub/spawners.py +++ b/remoteappmanager/jupyterhub/spawners.py @@ -2,7 +2,7 @@ import escapism import string -from traitlets import Any, Unicode +from traitlets import Any, Unicode, default from tornado import gen from jupyterhub.spawner import LocalProcessSpawner @@ -42,6 +42,7 @@ def __init__(self, **kwargs): # contain only one. self.proxy = self.db.query(orm.Proxy).first() + @default("options_form") def _options_form_default(self): """ Gives admins the option of spawning either RemoteAppManager admin or user sessions @@ -61,6 +62,7 @@ def _options_form_default(self): def _default_session(self): return "admin" if self.user.admin else "user" + @default("user_options") def _default_user_options(self): return {"session": self._default_session()} From fcb5f341db484e6193967752e59bb24eeabb4e13 Mon Sep 17 00:00:00 2001 From: flongford Date: Wed, 20 Apr 2022 11:46:45 +0100 Subject: [PATCH 06/18] FIX: typos in HTML string for options form --- remoteappmanager/jupyterhub/spawners.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/remoteappmanager/jupyterhub/spawners.py b/remoteappmanager/jupyterhub/spawners.py index 321e4b5d..1438399f 100644 --- a/remoteappmanager/jupyterhub/spawners.py +++ b/remoteappmanager/jupyterhub/spawners.py @@ -51,11 +51,11 @@ def _options_form_default(self): return """
Choose RemoteAppManager Session: - -
+ +
""" return "" From 6e3cde0210dcce9b3d5d28df26f756d8c121e027 Mon Sep 17 00:00:00 2001 From: flongford Date: Wed, 20 Apr 2022 12:06:09 +0100 Subject: [PATCH 07/18] FIX: further typos in HTML string for options form --- remoteappmanager/jupyterhub/spawners.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/remoteappmanager/jupyterhub/spawners.py b/remoteappmanager/jupyterhub/spawners.py index 1438399f..9a60a50d 100644 --- a/remoteappmanager/jupyterhub/spawners.py +++ b/remoteappmanager/jupyterhub/spawners.py @@ -50,8 +50,8 @@ def _options_form_default(self): if self.user.admin: return """
- Choose RemoteAppManager Session: - From 0005010a89200bf14e6109754f2667e83e79e8f9 Mon Sep 17 00:00:00 2001 From: flongford Date: Wed, 20 Apr 2022 12:16:06 +0100 Subject: [PATCH 08/18] TST: fix CSS selector reference in options form test --- selenium_tests/test_spawner_options_form.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/selenium_tests/test_spawner_options_form.py b/selenium_tests/test_spawner_options_form.py index 22f8280d..2510562e 100644 --- a/selenium_tests/test_spawner_options_form.py +++ b/selenium_tests/test_spawner_options_form.py @@ -32,7 +32,7 @@ def test_admin_login_user_session(self): self.click_first_element_located(By.ID, "start") self.click_first_element_located( - By.CSS_SELECTOR, "# spawner_form > options:nth-child(2)") + By.CSS_SELECTOR, "#session_form > option:nth-child(2)") self.click_first_element_located(By.CSS_SELECTOR, "input.btn") self.click_first_element_located(By.ID, "start") From 7cadf50795f82fc3ece5cc56e889315338391242 Mon Sep 17 00:00:00 2001 From: flongford Date: Wed, 20 Apr 2022 12:16:27 +0100 Subject: [PATCH 09/18] DOC: update user documentation on admin UI --- doc/source/administration.rst | 11 ++++++----- doc/source/configuration.rst | 4 ++-- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/doc/source/administration.rst b/doc/source/administration.rst index ecc8dc61..d1b6e227 100644 --- a/doc/source/administration.rst +++ b/doc/source/administration.rst @@ -1,11 +1,12 @@ Administration ============== -As specified in the deployment section, the authenticator will grant administrative -rights to users in the specified set. -Once logged in, an administrative user will be served by a different application, -where it can add or remove users, applications, and authorize users to run specific -applications. It is also possible to stop currently running containers. +As specified in the configuration section, the authenticator will grant additional +administrative rights to users in the specified set. + +Once logged in, an administrative user will have the option to spawn an "Admin" session, +providing them with a different application, where they can add or remove users, +applications, and authorize users to run specific applications. It is also possible to stop currently running containers It is important to note that the administrative interface works only with accounting backends supporting addition and removal. More specifically, it diff --git a/doc/source/configuration.rst b/doc/source/configuration.rst index 59801c90..0e8ad8b6 100644 --- a/doc/source/configuration.rst +++ b/doc/source/configuration.rst @@ -37,8 +37,8 @@ Administration capabilities are decided by jupyterhub, not remoteappmanager. c.Authenticator.admin_users = {"admin"} Note that the entry must be a python set. Users in this set will, once logged -in, reach an administrative interface, instead of the docker application -management. +in, be able to launch an administrative interface in addition to the standard +docker application management. .. _config_remoteappmanager: From 6d6a99b64628a6e62899290ff443262ff74cf81e Mon Sep 17 00:00:00 2001 From: flongford Date: Wed, 20 Apr 2022 12:26:52 +0100 Subject: [PATCH 10/18] CLN: simplify BaseSpawner options_from_form and cmd logic --- remoteappmanager/jupyterhub/spawners.py | 20 +++++++------------ .../jupyterhub/tests/test_spawners.py | 11 +++++----- 2 files changed, 12 insertions(+), 19 deletions(-) diff --git a/remoteappmanager/jupyterhub/spawners.py b/remoteappmanager/jupyterhub/spawners.py index 9a60a50d..b834e884 100644 --- a/remoteappmanager/jupyterhub/spawners.py +++ b/remoteappmanager/jupyterhub/spawners.py @@ -28,11 +28,7 @@ class BaseSpawner(LocalProcessSpawner): def cmd(self): """Overrides the base class traitlet so that we take full control of the spawned command according to user admin status""" - if not self.user.admin: - return [USER_CMD] - return ([ADMIN_CMD] - if self.user_options['session'] == "admin" - else [USER_CMD]) + return self.user_options.get('cmd', self._default_cmd()) def __init__(self, **kwargs): super().__init__(**kwargs) @@ -59,19 +55,17 @@ def _options_form_default(self): """ return "" - def _default_session(self): - return "admin" if self.user.admin else "user" - - @default("user_options") - def _default_user_options(self): - return {"session": self._default_session()} + def _default_cmd(self): + return ADMIN_CMD if self.user.admin else USER_CMD def options_from_form(self, form_data): """ Attempt to extract session selection from HTML form and return default session if not available """ - session = form_data.get("session", [self._default_session()])[0] - return {'session': session} + cmd = self._default_cmd() + if "session" in form_data: + cmd = ADMIN_CMD if form_data.pop("session")[0] == "admin" else USER_CMD + return {'cmd': cmd} def get_args(self): args = super().get_args() diff --git a/remoteappmanager/jupyterhub/tests/test_spawners.py b/remoteappmanager/jupyterhub/tests/test_spawners.py index d23437fe..1e467118 100644 --- a/remoteappmanager/jupyterhub/tests/test_spawners.py +++ b/remoteappmanager/jupyterhub/tests/test_spawners.py @@ -10,8 +10,7 @@ from jupyterhub import orm from remoteappmanager.jupyterhub.spawners import ( - SystemUserSpawner, - VirtualUserSpawner) + ADMIN_CMD, USER_CMD, SystemUserSpawner, VirtualUserSpawner) from remoteappmanager.tests import fixtures from remoteappmanager.tests.temp_mixin import TempMixin @@ -164,21 +163,21 @@ def test_cmd(self): self.assertEqual(self.spawner.cmd, ['remoteappadmin']) def test_cmd_user_session_override(self): - self.spawner.user_options = {"session": "user"} + self.spawner.user_options = {"cmd": USER_CMD} self.assertEqual(self.spawner.cmd, ['remoteappmanager']) def test_parse_options_from_form(self): self.assertEqual( self.spawner.options_from_form({}), - {"session": "admin"} + {"cmd": ADMIN_CMD} ) self.assertEqual( self.spawner.options_from_form({"session": ["user"]}), - {"session": "user"} + {"cmd": USER_CMD} ) self.assertEqual( self.spawner.options_from_form({"session": ["admin"]}), - {"session": "admin"} + {"cmd": ADMIN_CMD} ) From 1517a853c8d81d8df1b7b1ac0d1599619053d29b Mon Sep 17 00:00:00 2001 From: flongford Date: Wed, 20 Apr 2022 12:30:10 +0100 Subject: [PATCH 11/18] FIX: typo, BaseSpawner.cmd property should be a list --- remoteappmanager/jupyterhub/spawners.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/remoteappmanager/jupyterhub/spawners.py b/remoteappmanager/jupyterhub/spawners.py index b834e884..d1bb229f 100644 --- a/remoteappmanager/jupyterhub/spawners.py +++ b/remoteappmanager/jupyterhub/spawners.py @@ -28,7 +28,7 @@ class BaseSpawner(LocalProcessSpawner): def cmd(self): """Overrides the base class traitlet so that we take full control of the spawned command according to user admin status""" - return self.user_options.get('cmd', self._default_cmd()) + return [self.user_options.get('cmd', self._default_cmd())] def __init__(self, **kwargs): super().__init__(**kwargs) From 4549398b83e74c7ac0608fbb548a4e7f80452a40 Mon Sep 17 00:00:00 2001 From: flongford Date: Wed, 20 Apr 2022 12:37:26 +0100 Subject: [PATCH 12/18] CLN: small clean up and flake8 fix --- remoteappmanager/jupyterhub/spawners.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/remoteappmanager/jupyterhub/spawners.py b/remoteappmanager/jupyterhub/spawners.py index d1bb229f..6bd2a533 100644 --- a/remoteappmanager/jupyterhub/spawners.py +++ b/remoteappmanager/jupyterhub/spawners.py @@ -28,7 +28,7 @@ class BaseSpawner(LocalProcessSpawner): def cmd(self): """Overrides the base class traitlet so that we take full control of the spawned command according to user admin status""" - return [self.user_options.get('cmd', self._default_cmd())] + return self.user_options.get('cmd', self._default_cmd()) def __init__(self, **kwargs): super().__init__(**kwargs) @@ -56,7 +56,7 @@ def _options_form_default(self): return "" def _default_cmd(self): - return ADMIN_CMD if self.user.admin else USER_CMD + return [ADMIN_CMD] if self.user.admin else [USER_CMD] def options_from_form(self, form_data): """ Attempt to extract session selection from HTML form and @@ -64,7 +64,8 @@ def options_from_form(self, form_data): """ cmd = self._default_cmd() if "session" in form_data: - cmd = ADMIN_CMD if form_data.pop("session")[0] == "admin" else USER_CMD + selected = form_data.pop("session")[0] + cmd = [ADMIN_CMD] if selected == "admin" else [USER_CMD] return {'cmd': cmd} def get_args(self): From 1ad0b7eb0db5305ca5ba4951fd005416ddcb0f1d Mon Sep 17 00:00:00 2001 From: flongford Date: Wed, 20 Apr 2022 12:47:34 +0100 Subject: [PATCH 13/18] TST: fixes to BaseSpawner.cmd unit tests --- remoteappmanager/jupyterhub/tests/test_spawners.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/remoteappmanager/jupyterhub/tests/test_spawners.py b/remoteappmanager/jupyterhub/tests/test_spawners.py index 1e467118..735f6dcd 100644 --- a/remoteappmanager/jupyterhub/tests/test_spawners.py +++ b/remoteappmanager/jupyterhub/tests/test_spawners.py @@ -93,7 +93,7 @@ def test_args_without_config_file_path(self): self.assertIn("--base-urlpath=\"/\"", args) def test_cmd(self): - self.assertEqual(self.spawner.cmd, ['remoteappmanager']) + self.assertEqual(self.spawner.cmd, [USER_CMD]) def test_default_config_file_path(self): self.assertEqual(self.spawner.config_file_path, "") @@ -160,24 +160,24 @@ def setUp(self): self.spawner.user.admin = True def test_cmd(self): - self.assertEqual(self.spawner.cmd, ['remoteappadmin']) + self.assertEqual(self.spawner.cmd, [ADMIN_CMD]) def test_cmd_user_session_override(self): - self.spawner.user_options = {"cmd": USER_CMD} - self.assertEqual(self.spawner.cmd, ['remoteappmanager']) + self.spawner.user_options = {"cmd": [USER_CMD]} + self.assertEqual(self.spawner.cmd, [USER_CMD]) def test_parse_options_from_form(self): self.assertEqual( self.spawner.options_from_form({}), - {"cmd": ADMIN_CMD} + {"cmd": [ADMIN_CMD]} ) self.assertEqual( self.spawner.options_from_form({"session": ["user"]}), - {"cmd": USER_CMD} + {"cmd": [USER_CMD]} ) self.assertEqual( self.spawner.options_from_form({"session": ["admin"]}), - {"cmd": ADMIN_CMD} + {"cmd": [ADMIN_CMD]} ) From a318ae8e48f7819c40506c07de72c1e3c02e5d20 Mon Sep 17 00:00:00 2001 From: flongford Date: Wed, 20 Apr 2022 12:55:50 +0100 Subject: [PATCH 14/18] DOC: add documentation for manually shutting down sessions when admin to re-create the options form --- doc/source/administration.rst | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/doc/source/administration.rst b/doc/source/administration.rst index d1b6e227..bf35cbfa 100644 --- a/doc/source/administration.rst +++ b/doc/source/administration.rst @@ -6,7 +6,14 @@ administrative rights to users in the specified set. Once logged in, an administrative user will have the option to spawn an "Admin" session, providing them with a different application, where they can add or remove users, -applications, and authorize users to run specific applications. It is also possible to stop currently running containers +applications, and authorize users to run specific applications. It is also possible to stop +currently running containers + + **NOTE**: the existing "Admin" or "User" session must be shut down before the options form + will be shown again. This is a JupyterHub-level operation and is not automatically performed + upon logging out. Therefore it must be manually carried out by either navigating to + ``https:///hub/admin`` or ``https:///hub/home`` whilst logged + in and selecting the appropriate the "Stop My Sever" option. It is important to note that the administrative interface works only with accounting backends supporting addition and removal. More specifically, it From 1ca6c84ee9ae04effc0dadf3f49135439af4bb19 Mon Sep 17 00:00:00 2001 From: flongford Date: Wed, 20 Apr 2022 15:58:11 +0100 Subject: [PATCH 15/18] DEV: override LogoutHandler to close admin session when user logs out, rather than manually navigating to JupyterHub home page --- .../jupyterhub/auth/basic_authenticator.py | 4 ++- .../auth/github_whitelist_authenticator.py | 5 ++- .../auth/simphony_remote_auth_mixin.py | 35 +++++++++++++++++++ .../jupyterhub/auth/world_authenticator.py | 4 ++- selenium_tests/AdminDriverTest.py | 14 +------- selenium_tests/test_spawner_options_form.py | 14 +------- 6 files changed, 47 insertions(+), 29 deletions(-) create mode 100644 remoteappmanager/jupyterhub/auth/simphony_remote_auth_mixin.py diff --git a/remoteappmanager/jupyterhub/auth/basic_authenticator.py b/remoteappmanager/jupyterhub/auth/basic_authenticator.py index fa135d25..df449b8c 100644 --- a/remoteappmanager/jupyterhub/auth/basic_authenticator.py +++ b/remoteappmanager/jupyterhub/auth/basic_authenticator.py @@ -3,8 +3,10 @@ from jupyterhub.auth import Authenticator from traitlets import Dict +from .simphony_remote_auth_mixin import SimphonyRemoteAuthMixin -class BasicAuthenticator(Authenticator): + +class BasicAuthenticator(SimphonyRemoteAuthMixin, Authenticator): """ Simple authenticator based on a fixed set of users""" #: Dictionary of regular username: password keys allowed diff --git a/remoteappmanager/jupyterhub/auth/github_whitelist_authenticator.py b/remoteappmanager/jupyterhub/auth/github_whitelist_authenticator.py index 334bcd38..157bffc9 100644 --- a/remoteappmanager/jupyterhub/auth/github_whitelist_authenticator.py +++ b/remoteappmanager/jupyterhub/auth/github_whitelist_authenticator.py @@ -3,6 +3,8 @@ from traitlets.config import LoggingConfigurable from traitlets import Unicode, Float, Set +from .simphony_remote_auth_mixin import SimphonyRemoteAuthMixin + class FileWhitelistMixin(LoggingConfigurable): """ @@ -59,7 +61,8 @@ def whitelist(self, value): pass -class GitHubWhitelistAuthenticator(FileWhitelistMixin, GitHubOAuthenticator): +class GitHubWhitelistAuthenticator( + SimphonyRemoteAuthMixin, FileWhitelistMixin, GitHubOAuthenticator): """A github authenticator that also verifies that the user belongs to a specified whitelisted user as provided by an external file (so that we don't have to restart diff --git a/remoteappmanager/jupyterhub/auth/simphony_remote_auth_mixin.py b/remoteappmanager/jupyterhub/auth/simphony_remote_auth_mixin.py new file mode 100644 index 00000000..47b544b5 --- /dev/null +++ b/remoteappmanager/jupyterhub/auth/simphony_remote_auth_mixin.py @@ -0,0 +1,35 @@ +from jupyterhub.handlers import LogoutHandler as _LogoutHandler +from jupyterhub.handlers import LoginHandler + + +class LogoutHandler(_LogoutHandler): + def get(self): + user = self.get_current_user() + if user: + # Ensures admin sessions are shut down when user + # logs out so that the spawner options form is + # shown upon subsequent logins + # TODO: replace for configuring shutdown_on_logout option + # once running on jupyterhub>=1.0.0 + if user.admin and user.spawner is not None: + self.log.info(f"Shutting down {user.name}'s server") + self.stop_single_user(user) + self.log.info("User logged out: %s", user.name) + self.clear_login_cookie() + for name in user.other_user_cookies: + self.clear_login_cookie(name) + user.other_user_cookies = set([]) + self.statsd.incr('logout') + self.redirect(self.settings['login_url'], permanent=False) + + +class SimphonyRemoteAuthMixin: + + def get_handlers(self, app): + """Includes standard LoginHandler and modified LogoutHandler that + closes admin sessions upon signing out + """ + return [ + ('/login', LoginHandler), + ('/logout', LogoutHandler) + ] diff --git a/remoteappmanager/jupyterhub/auth/world_authenticator.py b/remoteappmanager/jupyterhub/auth/world_authenticator.py index 127f5b8d..0d30c54d 100644 --- a/remoteappmanager/jupyterhub/auth/world_authenticator.py +++ b/remoteappmanager/jupyterhub/auth/world_authenticator.py @@ -5,8 +5,10 @@ from jupyterhub.auth import Authenticator +from .simphony_remote_auth_mixin import SimphonyRemoteAuthMixin -class WorldAuthenticator(Authenticator): + +class WorldAuthenticator(SimphonyRemoteAuthMixin, Authenticator): """ This authenticator authenticates everyone """ @gen.coroutine diff --git a/selenium_tests/AdminDriverTest.py b/selenium_tests/AdminDriverTest.py index 61526a2a..4efc372f 100644 --- a/selenium_tests/AdminDriverTest.py +++ b/selenium_tests/AdminDriverTest.py @@ -16,18 +16,6 @@ def admin_login(self): self.click_first_element_located(By.CSS_SELECTOR, "input.btn") self.click_first_element_located(By.ID, "start") - def admin_logout(self): - """ Admin Logout. Ensures user session is stopped before performing logout. - This action should be generally used whenever the Spawner options form is - presented during logins so that it is correctly displayed for the next test - runner. - """ - self.driver.get(self.base_url + "/home") - - self.click_first_element_located(By.ID, "stop") - self.click_first_element_located(By.ID, "logout") - self.wait_until_text_inside_element_located(By.CSS_SELECTOR, "div.auth-form-header", "Sign in") - def setUp(self): RemoteAppDriverTest.setUp(self) self.admin_login() @@ -104,5 +92,5 @@ def click_row_action_button(self, row, action_name): ) def tearDown(self): - self.admin_logout() + self.logout() RemoteAppDriverTest.tearDown(self) diff --git a/selenium_tests/test_spawner_options_form.py b/selenium_tests/test_spawner_options_form.py index 2510562e..f2a3d472 100644 --- a/selenium_tests/test_spawner_options_form.py +++ b/selenium_tests/test_spawner_options_form.py @@ -5,18 +5,6 @@ class TestSpawnerOptionsForm(RemoteAppDriverTest): - def admin_logout(self): - """ Admin Logout. Ensures user session is stopped before performing logout. - This action should be generally used whenever the Spawner options form is - presented during logins so that it is correctly displayed for the next test - runner. - """ - self.driver.get(self.base_url + "/home") - - self.click_first_element_located(By.ID, "stop") - self.click_first_element_located(By.ID, "logout") - self.wait_until_text_inside_element_located(By.CSS_SELECTOR, "div.auth-form-header", "Sign in") - def test_admin_login_default_session(self): self.login("admin") @@ -40,5 +28,5 @@ def test_admin_login_user_session(self): By.CSS_SELECTOR, ".header", "APPLICATIONS") def tearDown(self): - self.admin_logout() + self.logout() RemoteAppDriverTest.tearDown(self) From 4b62823b56e4cdbe119a4b14153d45616ea82de8 Mon Sep 17 00:00:00 2001 From: flongford Date: Wed, 20 Apr 2022 16:43:04 +0100 Subject: [PATCH 16/18] DEV: expose SimphonyRemoteAuthMixin in remoteappmanager.jupyterhub.auth module --- remoteappmanager/jupyterhub/auth/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/remoteappmanager/jupyterhub/auth/__init__.py b/remoteappmanager/jupyterhub/auth/__init__.py index 83f8ac23..98219036 100644 --- a/remoteappmanager/jupyterhub/auth/__init__.py +++ b/remoteappmanager/jupyterhub/auth/__init__.py @@ -1,3 +1,4 @@ from .world_authenticator import WorldAuthenticator # noqa from .basic_authenticator import BasicAuthenticator # noqa from .github_whitelist_authenticator import GitHubWhitelistAuthenticator # noqa +from .simphony_remote_auth_mixin import SimphonyRemoteAuthMixin # noqa From d210afbfa714145539be740525a717534977a3fc Mon Sep 17 00:00:00 2001 From: flongford Date: Wed, 20 Apr 2022 16:43:34 +0100 Subject: [PATCH 17/18] DOC: update documentation on admin auto-sign outs --- doc/source/administration.rst | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/doc/source/administration.rst b/doc/source/administration.rst index bf35cbfa..f038c2e9 100644 --- a/doc/source/administration.rst +++ b/doc/source/administration.rst @@ -10,10 +10,13 @@ applications, and authorize users to run specific applications. It is also possi currently running containers **NOTE**: the existing "Admin" or "User" session must be shut down before the options form - will be shown again. This is a JupyterHub-level operation and is not automatically performed - upon logging out. Therefore it must be manually carried out by either navigating to + will be shown again. This is a JupyterHub-level operation and is not performed by default + upon logging out. Typically it must be manually carried out by either navigating to ``https:///hub/admin`` or ``https:///hub/home`` whilst logged - in and selecting the appropriate the "Stop My Sever" option. + in and selecting the appropriate the "Stop My Sever" option. For convenience we provide a custom + logout handler that automatically shuts down sessions upon an administrator sign out. This can be + used with any ``jupyterhub.auth.Authenticator`` subclass via inheriting the + ``remoteappmanager.jupyterhub.auth.SimphonyRemoteAuthMixin`` mixin. It is important to note that the administrative interface works only with accounting backends supporting addition and removal. More specifically, it From c59a8e5f2869c8954cdde1f63d595c6d9b682f71 Mon Sep 17 00:00:00 2001 From: flongford Date: Wed, 20 Apr 2022 17:41:47 +0100 Subject: [PATCH 18/18] ENH: robustify custom logout handler by explictly waiting for stop_single_user call to complete --- .../jupyterhub/auth/simphony_remote_auth_mixin.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/remoteappmanager/jupyterhub/auth/simphony_remote_auth_mixin.py b/remoteappmanager/jupyterhub/auth/simphony_remote_auth_mixin.py index 47b544b5..1abb32eb 100644 --- a/remoteappmanager/jupyterhub/auth/simphony_remote_auth_mixin.py +++ b/remoteappmanager/jupyterhub/auth/simphony_remote_auth_mixin.py @@ -1,8 +1,14 @@ +from tornado import gen + from jupyterhub.handlers import LogoutHandler as _LogoutHandler from jupyterhub.handlers import LoginHandler class LogoutHandler(_LogoutHandler): + """ Custom logout handler that also closes servers of admin + users, so that spawner options form will be shown during every login + """ + @gen.coroutine def get(self): user = self.get_current_user() if user: @@ -13,7 +19,7 @@ def get(self): # once running on jupyterhub>=1.0.0 if user.admin and user.spawner is not None: self.log.info(f"Shutting down {user.name}'s server") - self.stop_single_user(user) + yield gen.maybe_future(self.stop_single_user(user)) self.log.info("User logged out: %s", user.name) self.clear_login_cookie() for name in user.other_user_cookies: