diff --git a/jupyterhub/scopes.py b/jupyterhub/scopes.py index 7ecacad8..a800f273 100644 --- a/jupyterhub/scopes.py +++ b/jupyterhub/scopes.py @@ -95,7 +95,7 @@ scope_definitions = { }, 'read:servers': { 'description': 'Read users’ names and their server models (excluding the server state).', - 'subscopes': [], + 'subscopes': ['read:users:name'], }, 'delete:servers': {'description': "Stop and delete users' servers."}, 'tokens': { @@ -461,7 +461,12 @@ def _expand_scope(scope): # reapply !filter if filter_: expanded_scopes = { - f"{scope_name}!{filter_}" for scope_name in expanded_scope_names + f"{scope_name}!{filter_}" + for scope_name in expanded_scope_names + # server scopes have some cross-resource subscopes + # where the !server filter doesn't make sense, + # e.g. read:servers -> read:users:name + if not (filter_.startswith("server") and scope_name.startswith("read:user")) } else: expanded_scopes = expanded_scope_names diff --git a/jupyterhub/tests/test_roles.py b/jupyterhub/tests/test_roles.py index 898442da..6dd5bdfe 100644 --- a/jupyterhub/tests/test_roles.py +++ b/jupyterhub/tests/test_roles.py @@ -203,7 +203,7 @@ def test_orm_roles_delete_cascade(db): 'read:users:activity', }, ), - (['read:servers'], {'read:servers'}), + (['read:servers'], {'read:servers', 'read:users:name'}), ( ['admin:groups'], { @@ -227,6 +227,7 @@ def test_orm_roles_delete_cascade(db): 'read:roles:groups', 'read:groups:name', 'read:servers', + 'read:users:name', }, ), ( diff --git a/jupyterhub/tests/test_scopes.py b/jupyterhub/tests/test_scopes.py index a5f0078c..4964cb5b 100644 --- a/jupyterhub/tests/test_scopes.py +++ b/jupyterhub/tests/test_scopes.py @@ -555,23 +555,28 @@ async def test_server_state_access( await api_request( app, 'users', user.name, 'servers', server_name, method='post' ) - service = create_service_with_scopes( - f"read:users:name!user={user.name}", *scopes - ) + service = create_service_with_scopes("read:users:name!user=", *scopes) api_token = service.new_api_token() headers = {'Authorization': 'token %s' % api_token} + + # can I get the user model? r = await api_request(app, 'users', user.name, headers=headers) - r.raise_for_status() - user_model = r.json() - if num_servers: - assert 'servers' in user_model - server_models = user_model['servers'] - assert len(server_models) == num_servers - for server, server_model in server_models.items(): - assert keys_in.issubset(server_model) - assert keys_out.isdisjoint(server_model) + can_read_user_model = num_servers > 1 or 'read:users' in scopes + if can_read_user_model: + r.raise_for_status() + user_model = r.json() + if num_servers > 1: + assert 'servers' in user_model + server_models = user_model['servers'] + assert len(server_models) == num_servers + for server, server_model in server_models.items(): + assert keys_in.issubset(server_model) + assert keys_out.isdisjoint(server_model) + else: + assert 'servers' not in user_model else: - assert 'servers' not in user_model + assert r.status_code == 404 + r = await api_request( app, 'users',