diff --git a/jupyterhub/apihandlers/base.py b/jupyterhub/apihandlers/base.py index b8a72dbf..45d03852 100644 --- a/jupyterhub/apihandlers/base.py +++ b/jupyterhub/apihandlers/base.py @@ -298,10 +298,17 @@ class APIHandler(BaseHandler): 'name': service.name, 'roles': [r.name for r in service.roles], 'admin': service.admin, + 'url': getattr(service, 'url', ''), + 'prefix': service.server.base_url if getattr(service, 'server', '') else '', + 'command': getattr(service, 'command', ''), + 'pid': service.proc.pid if getattr(service, 'proc', '') else 0, + 'info': getattr(service, 'info', ''), + 'display': getattr(service, 'display', ''), } # todo: remove admin key now we have roles? + # todo: More default keys present? access_map = { - 'read:services': {'kind', 'name', 'roles', 'admin'}, + 'read:services': set(model.keys()), 'read:services:name': {'kind', 'name'}, 'read:services:roles': {'kind', 'name', 'roles'}, } diff --git a/jupyterhub/apihandlers/services.py b/jupyterhub/apihandlers/services.py index f0b87c30..7db84ccc 100644 --- a/jupyterhub/apihandlers/services.py +++ b/jupyterhub/apihandlers/services.py @@ -6,34 +6,16 @@ Currently GET-only, no actions can be taken to modify services. # Distributed under the terms of the Modified BSD License. import json -from tornado import web - -from .. import orm from ..scopes import needs_scope from .base import APIHandler -def service_model(service): - """Produce the model for a service""" - return { - 'name': service.name, - 'admin': service.admin, - 'roles': [r.name for r in service.roles], - 'url': service.url, - 'prefix': service.server.base_url if service.server else '', - 'command': service.command, - 'pid': service.proc.pid if service.proc else 0, - 'info': service.info, - 'display': service.display, - } - - class ServiceListAPIHandler(APIHandler): - @needs_scope('read:services') + @needs_scope('read:services', 'read:services:name', 'read:services:roles') def get(self): scope_filter = self.get_scope_filter('read:services') data = { - name: service_model(service) + name: self.service_model(service) for name, service in self.services.items() if scope_filter(service, kind='service') } @@ -41,10 +23,10 @@ class ServiceListAPIHandler(APIHandler): class ServiceAPIHandler(APIHandler): - @needs_scope('read:services') + @needs_scope('read:services', 'read:services:name', 'read:services:roles') def get(self, service_name): service = self.services[service_name] - self.write(json.dumps(service_model(service))) + self.write(json.dumps(self.service_model(service))) default_handlers = [ diff --git a/jupyterhub/tests/test_api.py b/jupyterhub/tests/test_api.py index 9bd66be9..336fcad1 100644 --- a/jupyterhub/tests/test_api.py +++ b/jupyterhub/tests/test_api.py @@ -1606,6 +1606,7 @@ async def test_get_services(app, mockservice_url): services = r.json() assert services == { mockservice.name: { + 'kind': 'service', 'name': mockservice.name, 'admin': True, 'roles': ['admin'], @@ -1631,6 +1632,7 @@ async def test_get_service(app, mockservice_url): service = r.json() assert service == { + 'kind': 'service', 'name': mockservice.name, 'admin': True, 'roles': ['admin'], diff --git a/jupyterhub/tests/test_scopes.py b/jupyterhub/tests/test_scopes.py index 46dabc70..7b06738b 100644 --- a/jupyterhub/tests/test_scopes.py +++ b/jupyterhub/tests/test_scopes.py @@ -763,13 +763,39 @@ async def test_resolve_token_permissions( assert token_retained_scopes == intersection_scopes +@mark.parametrize( + "scopes, model_keys", + [ + ( + {'read:services'}, + { + 'command', + 'name', + 'kind', + 'info', + 'display', + 'pid', + 'admin', + 'prefix', + 'url', + 'roles', + }, + ), + ({'read:services:roles', 'read:users:names'}, {'name', 'kind', 'roles'}), + ({'read:services:name'}, {'name', 'kind'}), + ], +) +async def test_service_model_filtering( + app, scopes, model_keys, create_user_with_scopes, create_service_with_scopes +): + user = create_user_with_scopes(*scopes, 'teddy') + service = create_service_with_scopes() + r = await api_request( + app, 'services', service.name, headers=auth_header(app.db, user.name) + ) + assert r.status_code == 200 + assert model_keys == r.json().keys() + + async def test_roles_access(app, create_user_with_scopes): pass - - -async def test_service_model_filtering(app, create_user_with_scopes): - pass - - -async def test_group_model_filtering(app, create_user_with_scopes): - pass diff --git a/jupyterhub/tests/test_services_auth.py b/jupyterhub/tests/test_services_auth.py index ac10661e..51e69c4e 100644 --- a/jupyterhub/tests/test_services_auth.py +++ b/jupyterhub/tests/test_services_auth.py @@ -298,7 +298,8 @@ async def test_hubauth_service_token(app, mockservice_url): ) r.raise_for_status() reply = r.json() - assert reply == {'kind': 'service', 'name': name, 'admin': False, 'roles': []} + service_model = {'kind': 'service', 'name': name, 'admin': False, 'roles': []} + assert service_model.items() <= reply.items() assert not r.cookies # token in ?token parameter @@ -307,7 +308,7 @@ async def test_hubauth_service_token(app, mockservice_url): ) r.raise_for_status() reply = r.json() - assert reply == {'kind': 'service', 'name': name, 'admin': False, 'roles': []} + assert service_model.items() <= reply.items() r = await async_requests.get( public_url(app, mockservice_url) + '/whoami/?token=no-such-token',