diff --git a/jupyterhub/app.py b/jupyterhub/app.py index 324656f4..bc04b31f 100644 --- a/jupyterhub/app.py +++ b/jupyterhub/app.py @@ -1185,6 +1185,10 @@ class JupyterHub(Application): False, help="""Shuts down all user servers on logout""" ).tag(config=True) + server_tokens = Bool( + True, help="""Display JupyterHub version information on admin page""" + ).tag(config=True) + @default('statsd') def _statsd(self): if self.statsd_host: @@ -2133,6 +2137,7 @@ class JupyterHub(Application): internal_ssl_ca=self.internal_ssl_ca, trusted_alt_names=self.trusted_alt_names, shutdown_on_logout=self.shutdown_on_logout, + server_tokens=self.server_tokens, eventlog=self.eventlog, ) # allow configured settings to have priority diff --git a/jupyterhub/handlers/pages.py b/jupyterhub/handlers/pages.py index aa2f37cc..8b0afa3d 100644 --- a/jupyterhub/handlers/pages.py +++ b/jupyterhub/handlers/pages.py @@ -14,6 +14,7 @@ from tornado import gen from tornado import web from tornado.httputil import url_concat +from .. import __version__ from .. import orm from ..metrics import SERVER_POLL_DURATION_SECONDS from ..metrics import ServerPollStatus @@ -422,6 +423,8 @@ class AdminHandler(BaseHandler): sort={s: o for s, o in zip(sorts, orders)}, allow_named_servers=self.allow_named_servers, named_server_limit_per_user=self.named_server_limit_per_user, + server_tokens=self.settings.get('server_tokens', True), + server_version='{} {}'.format(__version__, self.version_hash), ) self.finish(html) diff --git a/jupyterhub/tests/test_pages.py b/jupyterhub/tests/test_pages.py index 77b5cf1e..3b8f242a 100644 --- a/jupyterhub/tests/test_pages.py +++ b/jupyterhub/tests/test_pages.py @@ -110,6 +110,21 @@ async def test_admin(app): assert r.url.endswith('/admin') +async def test_admin_version(app): + cookies = await app.login_user('admin') + r = await get_page('admin', app, cookies=cookies, allow_redirects=False) + r.raise_for_status() + assert "version_footer" in r.text + + +async def test_admin_version_disabled(app): + cookies = await app.login_user('admin') + with mock.patch.dict(app.tornado_settings, {'server_tokens': False}): + r = await get_page('admin', app, cookies=cookies, allow_redirects=False) + r.raise_for_status() + assert "version_footer" not in r.text + + @pytest.mark.parametrize('sort', ['running', 'last_activity', 'admin', 'name']) async def test_admin_sort(app, sort): cookies = await app.login_user('admin') diff --git a/share/jupyterhub/static/less/admin.less b/share/jupyterhub/static/less/admin.less index 70a262d9..6189e4e9 100644 --- a/share/jupyterhub/static/less/admin.less +++ b/share/jupyterhub/static/less/admin.less @@ -1,3 +1,9 @@ i.sort-icon { margin-left: 4px; } + +.version_footer { + position: fixed; + bottom: 0; + width: 100%; +} diff --git a/share/jupyterhub/templates/admin.html b/share/jupyterhub/templates/admin.html index 12edbdcd..f5ddec4a 100644 --- a/share/jupyterhub/templates/admin.html +++ b/share/jupyterhub/templates/admin.html @@ -103,6 +103,13 @@ +{%- if server_tokens -%} +
+{%- endif -%} {% call modal('Delete User', btn_class='btn-danger delete-button') %} Are you sure you want to delete user USER?