mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-18 15:33:02 +00:00
Add read:metrics
scope for metrics endpoint
and ensure token auth is accepted
This commit is contained in:
@@ -1419,3 +1419,4 @@ components:
|
|||||||
Read information about the proxy’s routing table, sync the Hub
|
Read information about the proxy’s routing table, sync the Hub
|
||||||
with the proxy and notify the Hub about a new proxy.
|
with the proxy and notify the Hub about a new proxy.
|
||||||
shutdown: Shutdown the hub.
|
shutdown: Shutdown the hub.
|
||||||
|
read:metrics: Read prometheus metrics.
|
||||||
|
@@ -12,6 +12,8 @@ class MetricsHandler(BaseHandler):
|
|||||||
Handler to serve Prometheus metrics
|
Handler to serve Prometheus metrics
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
_accept_token_auth = True
|
||||||
|
|
||||||
@metrics_authentication
|
@metrics_authentication
|
||||||
async def get(self):
|
async def get(self):
|
||||||
self.set_header('Content-Type', CONTENT_TYPE_LATEST)
|
self.set_header('Content-Type', CONTENT_TYPE_LATEST)
|
||||||
|
@@ -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.'
|
'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.'},
|
'shutdown': {'description': 'Shutdown the hub.'},
|
||||||
|
'read:metrics': {
|
||||||
|
'description': "Read prometheus metrics.",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@@ -1,9 +1,13 @@
|
|||||||
import json
|
import json
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
from .utils import add_user
|
|
||||||
from .utils import api_request
|
from .utils import api_request
|
||||||
|
from .utils import get_page
|
||||||
from jupyterhub import metrics
|
from jupyterhub import metrics
|
||||||
from jupyterhub import orm
|
from jupyterhub import orm
|
||||||
|
from jupyterhub import roles
|
||||||
|
|
||||||
|
|
||||||
async def test_total_users(app):
|
async def test_total_users(app):
|
||||||
@@ -32,3 +36,42 @@ async def test_total_users(app):
|
|||||||
|
|
||||||
sample = metrics.TOTAL_USERS.collect()[0].samples[0]
|
sample = metrics.TOTAL_USERS.collect()[0].samples[0]
|
||||||
assert sample.value == num_users
|
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
|
||||||
|
@@ -1110,19 +1110,6 @@ async def test_server_not_running_api_request_legacy_status(app):
|
|||||||
assert r.status_code == 503
|
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):
|
async def test_health_check_request(app):
|
||||||
r = await get_page('health', app)
|
r = await get_page('health', app)
|
||||||
assert r.status_code == 200
|
assert r.status_code == 200
|
||||||
|
@@ -320,9 +320,11 @@ def admin_only(f):
|
|||||||
@auth_decorator
|
@auth_decorator
|
||||||
def metrics_authentication(self):
|
def metrics_authentication(self):
|
||||||
"""Decorator for restricting access to metrics"""
|
"""Decorator for restricting access to metrics"""
|
||||||
user = self.current_user
|
if not self.authenticate_prometheus:
|
||||||
if user is None and self.authenticate_prometheus:
|
return
|
||||||
raise web.HTTPError(403)
|
scope = 'read:metrics'
|
||||||
|
if scope not in self.parsed_scopes:
|
||||||
|
raise web.HTTPError(403, f"Access to metrics requires scope '{scope}'")
|
||||||
|
|
||||||
|
|
||||||
# Token utilities
|
# Token utilities
|
||||||
|
Reference in New Issue
Block a user