diff --git a/docs/source/_static/rest-api.yml b/docs/source/_static/rest-api.yml index a4a5390e..2609558c 100644 --- a/docs/source/_static/rest-api.yml +++ b/docs/source/_static/rest-api.yml @@ -1391,6 +1391,9 @@ components: inherit: Everything that the token-owning entity can access _(metascope for tokens)_ + admin-ui: + Access the admin page. Permission to take actions via the admin + page granted separately. admin:users: Read, write, create and delete users and their authentication state, not including their servers or tokens. diff --git a/jupyterhub/handlers/pages.py b/jupyterhub/handlers/pages.py index b599cf76..b7aad49c 100644 --- a/jupyterhub/handlers/pages.py +++ b/jupyterhub/handlers/pages.py @@ -454,15 +454,14 @@ class AdminHandler(BaseHandler): @web.authenticated # stacked decorators: all scopes must be present # note: keep in sync with admin link condition in page.html - @needs_scope('admin:users') - @needs_scope('admin:servers') + @needs_scope('admin-ui') async def get(self): auth_state = await self.current_user.get_auth_state() html = await self.render_template( 'admin.html', current_user=self.current_user, auth_state=auth_state, - admin_access=self.settings.get('admin_access', False), + admin_access=True, allow_named_servers=self.allow_named_servers, named_server_limit_per_user=self.named_server_limit_per_user, server_version=f'{__version__} {self.version_hash}', diff --git a/jupyterhub/roles.py b/jupyterhub/roles.py index afa77a25..f4a11aac 100644 --- a/jupyterhub/roles.py +++ b/jupyterhub/roles.py @@ -31,6 +31,7 @@ def get_default_roles(): 'name': 'admin', 'description': 'Elevated privileges (can do anything)', 'scopes': [ + 'admin-ui', 'admin:users', 'admin:servers', 'tokens', diff --git a/jupyterhub/scopes.py b/jupyterhub/scopes.py index a3719cf3..99907fc4 100644 --- a/jupyterhub/scopes.py +++ b/jupyterhub/scopes.py @@ -42,6 +42,10 @@ scope_definitions = { 'description': 'Anything you have access to', 'doc_description': 'Everything that the token-owning entity can access _(metascope for tokens)_', }, + 'admin-ui': { + 'description': 'Access the admin page.', + 'doc_description': 'Access the admin page. Permission to take actions via the admin page granted separately.', + }, 'admin:users': { 'description': 'Read, write, create and delete users and their authentication state, not including their servers or tokens.', 'subscopes': ['admin:auth_state', 'users', 'read:roles:users', 'delete:users'], diff --git a/jupyterhub/tests/test_pages.py b/jupyterhub/tests/test_pages.py index 5d3911bb..7e421524 100644 --- a/jupyterhub/tests/test_pages.py +++ b/jupyterhub/tests/test_pages.py @@ -1105,17 +1105,27 @@ async def test_bad_oauth_get(app, params): [ (["users"], False), (["admin:users"], False), - (["users", "admin:users", "admin:servers"], True), + (["users", "admin:users", "admin:servers"], False), + (["admin-ui"], True), ], ) async def test_admin_page_access(app, scopes, has_access, create_user_with_scopes): user = create_user_with_scopes(*scopes) cookies = await app.login_user(user.name) - r = await get_page("/admin", app, cookies=cookies) + home_resp = await get_page("/home", app, cookies=cookies) + admin_resp = await get_page("/admin", app, cookies=cookies) + assert home_resp.status_code == 200 + soup = BeautifulSoup(home_resp.text, "html.parser") + nav = soup.find("div", id="thenavbar") + links = [a["href"] for a in nav.find_all("a")] + + admin_url = app.base_url + "hub/admin" if has_access: - assert r.status_code == 200 + assert admin_resp.status_code == 200 + assert admin_url in links else: - assert r.status_code == 403 + assert admin_resp.status_code == 403 + assert admin_url not in links async def test_oauth_page_scope_appearance( diff --git a/share/jupyterhub/templates/page.html b/share/jupyterhub/templates/page.html index 4bf8e7a3..45376653 100644 --- a/share/jupyterhub/templates/page.html +++ b/share/jupyterhub/templates/page.html @@ -122,7 +122,7 @@ {% block nav_bar_left_items %}
  • Home
  • Token
  • - {% if 'admin:users' in parsed_scopes and 'admin:servers' in parsed_scopes %} + {% if 'admin-ui' in parsed_scopes %}
  • Admin
  • {% endif %} {% if services %}