Skip to content

Commit

Permalink
Filter users and groups a user can share an app with based on permiss…
Browse files Browse the repository at this point in the history
…ions (nebari-dev#295)

* temp

* fix users and groups filtering

* remove unused imports

* fix undo user name

* add entity filtering test

* add test for groups

* move has_scope import within function

* skip test if not jupyterhub 5

* add note for each test

* move expanded scoeps out of loop

* update assert 'scopes' in self.token_json
  • Loading branch information
aktech authored May 28, 2024
1 parent 6795002 commit d54dc50
Show file tree
Hide file tree
Showing 2 changed files with 88 additions and 7 deletions.
37 changes: 30 additions & 7 deletions jhub_apps/hub_client/hub_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ def requires_user_token(func):
"""
@wraps(func)
def wrapper(self, *args, **kwargs):
token_id = self._create_token_for_user()
response_json = self._create_token_for_user()
token_id = response_json["id"]
try:
original_method_return = func(self, *args, **kwargs)
except Exception as e:
Expand All @@ -39,6 +40,7 @@ class HubClient:
def __init__(self, username=None):
self.username = username
self.tokens = [JUPYTERHUB_API_TOKEN]
self.token_json = None
self.jhub_apps_request_id = None
self._set_request_id()

Expand Down Expand Up @@ -74,9 +76,10 @@ def _create_token_for_user(self):
# for e.g. When func_a calls func_b and both have the decorator "requires_user_token"
# The func_a on completing execution will only clear the token, which the decorator
# requires_user_token created for it, not the token created for func_a
self.token_json = rjson
self.tokens.append(rjson["token"])
logger.info(f"Created token: {rjson['id']}")
return rjson["id"]
return rjson

def _revoke_token(self, token_id):
assert self.username
Expand Down Expand Up @@ -298,17 +301,37 @@ def get_groups(self):
r.raise_for_status()
return r.json()

@requires_user_token
def get_user_scopes(self):
assert self.token_json
assert "scopes" in self.token_json
return self.token_json["scopes"]


def get_users_and_group_allowed_to_share_with(user):
"""Returns a list of users and groups"""
hclient = HubClient()
hclient = HubClient(username=user.name)
users = hclient.get_users()
user_names = [u["name"] for u in users if u["name"] != user.name]
groups = hclient.get_groups()
group_names = [group['name'] for group in groups]
# TODO: Filter users and groups based on what the user has access to share with
# parsed_scopes = parse_scopes(scopes)
user_scopes = hclient.get_user_scopes()
return {
"users": user_names,
"groups": group_names
"users": filter_entity_based_on_scopes(
scopes=user_scopes, entities=user_names
),
"groups": filter_entity_based_on_scopes(
scopes=user_scopes, entities=group_names, entity_key="group"
)
}


def filter_entity_based_on_scopes(scopes, entities, entity_key="user"):
# only available in JupyterHub>=5
from jupyterhub.scopes import has_scope, expand_scopes
allowed_entities_to_read = set()
expanded_scopes = expand_scopes(scopes)
for entity in entities:
if has_scope(f'read:{entity_key}s:name!{entity_key}={entity}', expanded_scopes):
allowed_entities_to_read.add(entity)
return list(allowed_entities_to_read)
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import pytest

from jhub_apps.hub_client.hub_client import filter_entity_based_on_scopes
from jhub_apps.hub_client.utils import is_jupyterhub_5


@pytest.mark.skipif(not is_jupyterhub_5(), reason="requires jupyterhub>=5")
@pytest.mark.parametrize("name,entity_key,scopes,entities,expected_entities", [
(
"permissions-for-some-users", "user",
[
"read:users:name!user=user_b",
"read:users:name!user=user_c",
"read:users:name!user=user_d",
"read:users:name!user=user_f",
],
["user_a", "user_c", "user_d", "user_e"],
["user_c", "user_d"]
),
(
"generic-permission-for-all-users", "user",
[
"read:users:name",
],
["user_c", "user_a", "user_e", "user_d"],
["user_c", "user_a", "user_e", "user_d"]
),
(
"no-permissions-for-users", "user",
[],
["user_a", "user_b"],
[]
),
(
"permissions-for-some-groups", "group",
[
"read:groups:name!group=group-x",
"read:groups:name!group=group-b",
"read:groups:name!group=group-c",
"read:groups:name!group=group-y",
],
["group-a", "group-b", "group-c", "group-d"],
["group-b", "group-c"]
),
(
"permissions-for-no-groups", "group",
[],
["group-a", "group-b", "group-c", "group-d"],
[]
),
])
def test_filter_users_based_on_scopes(name, entity_key, scopes, entities, expected_entities):
filtered_entities = filter_entity_based_on_scopes(
scopes=scopes,
entities=entities,
entity_key=entity_key
)
assert set(filtered_entities) == set(expected_entities)

0 comments on commit d54dc50

Please sign in to comment.