diff --git a/docs/source/_static/rest-api.yml b/docs/source/_static/rest-api.yml index 995a71a3..9ccc365d 100644 --- a/docs/source/_static/rest-api.yml +++ b/docs/source/_static/rest-api.yml @@ -1419,3 +1419,4 @@ components: Read information about the proxy’s routing table, sync the Hub with the proxy and notify the Hub about a new proxy. shutdown: Shutdown the hub. + read:metrics: Read prometheus metrics. diff --git a/jupyterhub/handlers/metrics.py b/jupyterhub/handlers/metrics.py index d2f0b03b..844a203f 100644 --- a/jupyterhub/handlers/metrics.py +++ b/jupyterhub/handlers/metrics.py @@ -12,6 +12,8 @@ class MetricsHandler(BaseHandler): Handler to serve Prometheus metrics """ + _accept_token_auth = True + @metrics_authentication async def get(self): self.set_header('Content-Type', CONTENT_TYPE_LATEST) diff --git a/jupyterhub/scopes.py b/jupyterhub/scopes.py index e8f8ff4f..5f441fbe 100644 --- a/jupyterhub/scopes.py +++ b/jupyterhub/scopes.py @@ -131,6 +131,9 @@ scope_definitions = { 'description': 'Read information about the proxy’s routing table, sync the Hub with the proxy and notify the Hub about a new proxy.' }, 'shutdown': {'description': 'Shutdown the hub.'}, + 'read:metrics': { + 'description': "Read prometheus metrics.", + }, } diff --git a/jupyterhub/tests/test_metrics.py b/jupyterhub/tests/test_metrics.py index 29c22122..795ca89d 100644 --- a/jupyterhub/tests/test_metrics.py +++ b/jupyterhub/tests/test_metrics.py @@ -1,9 +1,13 @@ import json +from unittest import mock + +import pytest -from .utils import add_user from .utils import api_request +from .utils import get_page from jupyterhub import metrics from jupyterhub import orm +from jupyterhub import roles async def test_total_users(app): @@ -32,3 +36,42 @@ async def test_total_users(app): sample = metrics.TOTAL_USERS.collect()[0].samples[0] assert sample.value == num_users + + +@pytest.mark.parametrize( + "authenticate_prometheus, authenticated, authorized, success", + [ + (True, True, True, True), + (True, True, False, False), + (True, False, False, False), + (False, True, True, True), + (False, False, False, True), + ], +) +async def test_metrics_auth( + app, + authenticate_prometheus, + authenticated, + authorized, + success, + create_temp_role, + user, +): + if authorized: + role = create_temp_role(["read:metrics"]) + roles.grant_role(app.db, user, role) + + headers = {} + if authenticated: + token = user.new_api_token() + headers["Authorization"] = f"token {token}" + + with mock.patch.dict( + app.tornado_settings, {"authenticate_prometheus": authenticate_prometheus} + ): + r = await get_page("metrics", app, headers=headers) + if success: + assert r.status_code == 200 + else: + assert r.status_code == 403 + assert 'read:metrics' in r.text diff --git a/jupyterhub/tests/test_pages.py b/jupyterhub/tests/test_pages.py index 019c805a..6cd5246f 100644 --- a/jupyterhub/tests/test_pages.py +++ b/jupyterhub/tests/test_pages.py @@ -1110,19 +1110,6 @@ async def test_server_not_running_api_request_legacy_status(app): assert r.status_code == 503 -async def test_metrics_no_auth(app): - r = await get_page("metrics", app) - assert r.status_code == 403 - - -async def test_metrics_auth(app): - cookies = await app.login_user('river') - metrics_url = ujoin(public_host(app), app.hub.base_url, 'metrics') - r = await get_page("metrics", app, cookies=cookies) - assert r.status_code == 200 - assert r.url == metrics_url - - async def test_health_check_request(app): r = await get_page('health', app) assert r.status_code == 200 diff --git a/jupyterhub/utils.py b/jupyterhub/utils.py index 1fc4aa89..aa3fa7f7 100644 --- a/jupyterhub/utils.py +++ b/jupyterhub/utils.py @@ -320,9 +320,11 @@ def admin_only(f): @auth_decorator def metrics_authentication(self): """Decorator for restricting access to metrics""" - user = self.current_user - if user is None and self.authenticate_prometheus: - raise web.HTTPError(403) + if not self.authenticate_prometheus: + return + scope = 'read:metrics' + if scope not in self.parsed_scopes: + raise web.HTTPError(403, f"Access to metrics requires scope '{scope}'") # Token utilities