Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
1c1ee5f
FEAT: add Spawner options for to Admin logins
flongford Apr 14, 2022
be85ef4
TST: update selenium tests with admin login and logout routines
flongford Apr 14, 2022
799cbe2
TST: fix to AdminDriverTest logout
flongford Apr 14, 2022
302caac
TST: include tests for Spawner Options form
flongford Apr 14, 2022
ae5d4fa
FIX: explictly include default methods on BaseSpawner
flongford Apr 14, 2022
37901be
Merge branch 'master' into enh/admin-spawner-options
flongford Apr 14, 2022
fcb5f34
FIX: typos in HTML string for options form
flongford Apr 20, 2022
6e3cde0
FIX: further typos in HTML string for options form
flongford Apr 20, 2022
0005010
TST: fix CSS selector reference in options form test
flongford Apr 20, 2022
7cadf50
DOC: update user documentation on admin UI
flongford Apr 20, 2022
6d6a99b
CLN: simplify BaseSpawner options_from_form and cmd logic
flongford Apr 20, 2022
1517a85
FIX: typo, BaseSpawner.cmd property should be a list
flongford Apr 20, 2022
4549398
CLN: small clean up and flake8 fix
flongford Apr 20, 2022
1ad0b7e
TST: fixes to BaseSpawner.cmd unit tests
flongford Apr 20, 2022
a318ae8
DOC: add documentation for manually shutting down sessions when admin…
flongford Apr 20, 2022
1ca6c84
DEV: override LogoutHandler to close admin session when user logs out…
flongford Apr 20, 2022
4b62823
DEV: expose SimphonyRemoteAuthMixin in remoteappmanager.jupyterhub.au…
flongford Apr 20, 2022
d210afb
DOC: update documentation on admin auto-sign outs
flongford Apr 20, 2022
c59a8e5
ENH: robustify custom logout handler by explictly waiting for stop_si…
flongford Apr 20, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 16 additions & 5 deletions doc/source/administration.rst
Original file line number Diff line number Diff line change
@@ -1,11 +1,22 @@
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

**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 performed by default
upon logging out. Typically it must be manually carried out by either navigating to
``https://<simphony-remote>/hub/admin`` or ``https://<simphony-remote>/hub/home`` whilst logged
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
Expand Down
4 changes: 2 additions & 2 deletions doc/source/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
1 change: 1 addition & 0 deletions remoteappmanager/jupyterhub/auth/__init__.py
Original file line number Diff line number Diff line change
@@ -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
4 changes: 3 additions & 1 deletion remoteappmanager/jupyterhub/auth/basic_authenticator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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):
"""
Expand Down Expand Up @@ -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
Expand Down
41 changes: 41 additions & 0 deletions remoteappmanager/jupyterhub/auth/simphony_remote_auth_mixin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
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:
# 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")
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:
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)
]
4 changes: 3 additions & 1 deletion remoteappmanager/jupyterhub/auth/world_authenticator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
40 changes: 35 additions & 5 deletions remoteappmanager/jupyterhub/spawners.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@
import escapism
import string

from traitlets import Any, Unicode
from traitlets import Any, Unicode, default
from tornado import gen

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
Expand All @@ -25,10 +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 (["remoteappadmin"]
if self.user.admin is True
else ["remoteappmanager"])
return self.user_options.get('cmd', self._default_cmd())

def __init__(self, **kwargs):
super().__init__(**kwargs)
Expand All @@ -38,6 +38,36 @@ 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
"""
if self.user.admin:
return """
<div>
<label for="session">Choose RemoteAppManager Session:</label>
<select id="session_form" name="session" size="2">
<option value="admin" selected>Admin</option>
<option value="user">User</option>
</select>
</div>
"""
return ""

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
"""
cmd = self._default_cmd()
if "session" in form_data:
selected = form_data.pop("session")[0]
cmd = [ADMIN_CMD] if selected == "admin" else [USER_CMD]
return {'cmd': cmd}

def get_args(self):
args = super().get_args()

Expand Down
25 changes: 21 additions & 4 deletions remoteappmanager/jupyterhub/tests/test_spawners.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -94,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, "")
Expand Down Expand Up @@ -161,7 +160,25 @@ 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, [USER_CMD])

def test_parse_options_from_form(self):
self.assertEqual(
self.spawner.options_from_form({}),
{"cmd": [ADMIN_CMD]}
)
self.assertEqual(
self.spawner.options_from_form({"session": ["user"]}),
{"cmd": [USER_CMD]}
)
self.assertEqual(
self.spawner.options_from_form({"session": ["admin"]}),
{"cmd": [ADMIN_CMD]}
)


class TestVirtualUserSpawner(TestSystemUserSpawner):
Expand Down
14 changes: 13 additions & 1 deletion selenium_tests/AdminDriverTest.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,21 @@


class AdminDriverTest(RemoteAppDriverTest):

def admin_login(self):
""" 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")

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 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
Expand Down
4 changes: 2 additions & 2 deletions selenium_tests/RemoteAppDriverTest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down
32 changes: 32 additions & 0 deletions selenium_tests/test_spawner_options_form.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
from selenium_tests.RemoteAppDriverTest import RemoteAppDriverTest
from selenium.webdriver.common.by import By


class TestSpawnerOptionsForm(RemoteAppDriverTest):

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, "#session_form > option: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.logout()
RemoteAppDriverTest.tearDown(self)