Fixed vertical filtering in user models, but does not work for OAuth yet

This commit is contained in:
0mar
2021-02-15 14:03:37 +01:00
parent de2e8ff355
commit 746be73e56
6 changed files with 49 additions and 34 deletions

View File

@@ -2,6 +2,7 @@
# Copyright (c) Jupyter Development Team. # Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License. # Distributed under the terms of the Modified BSD License.
import json import json
import re
from datetime import datetime from datetime import datetime
from http.client import responses from http.client import responses
@@ -64,15 +65,19 @@ class APIHandler(BaseHandler):
return True return True
def get_scope_filter(self, req_scope): def get_scope_filter(self, req_scope):
"""Produce a filter for `*ListAPIHandlers* so that GET method knows which models to return""" """Produce a filter for `*ListAPIHandlers* so that GET method knows which models to return
scope_translator = { If the APIHandler has unrestricted access
'read:users': 'users', """
'read:services': 'services', kind_regex = re.compile(r':?(users|services|groups):?')
'read:groups': 'groups', try:
} kind = re.search(kind_regex, req_scope).group(1)
if req_scope not in scope_translator: except AttributeError:
raise AttributeError("Internal error: inconsistent scope situation") self.log.warning(
kind = scope_translator[req_scope] "Regex error while processing scope %s, throwing 500", req_scope
)
raise web.HTTPError(
log_message="Unrecognized scope guard on method: %s" % req_scope
)
Resource = orm.get_class(kind) Resource = orm.get_class(kind)
try: try:
sub_scope = self.parsed_scopes[req_scope] sub_scope = self.parsed_scopes[req_scope]
@@ -246,7 +251,7 @@ class APIHandler(BaseHandler):
'created', 'created',
'last_activity', 'last_activity',
}, },
'read:users:name': {'kind', 'name'}, 'read:users:names': {'kind', 'name'},
'read:users:groups': {'kind', 'name', 'groups'}, 'read:users:groups': {'kind', 'name', 'groups'},
'read:users:activity': {'kind', 'name', 'last_activity'}, 'read:users:activity': {'kind', 'name', 'last_activity'},
'read:users:servers': {'kind', 'name', 'servers'}, 'read:users:servers': {'kind', 'name', 'servers'},
@@ -255,28 +260,32 @@ class APIHandler(BaseHandler):
self.log.debug( self.log.debug(
"Asking for user models with scopes [%s]" % ", ".join(self.raw_scopes) "Asking for user models with scopes [%s]" % ", ".join(self.raw_scopes)
) )
self.log.debug(
"Current requests have db loaded scopes [%s]" % self.current_user
)
allowed_keys = set() allowed_keys = set()
# if not self.parsed_scopes:
# self.parse_scopes()
for scope in access_map: for scope in access_map:
if scope in self.parsed_scopes: if scope in self.parsed_scopes:
scope_filter = self.get_scope_filter(scope) scope_filter = self.get_scope_filter(scope)
if scope_filter is None or user.name in scope_filter: if scope_filter is None or user.name in scope_filter:
allowed_keys |= access_map[scope] allowed_keys |= access_map[scope]
model = {key: model[key] for key in allowed_keys if key in model} model = {key: model[key] for key in allowed_keys if key in model}
if not model: if model:
return model # No access to this user if '' in user.spawners and 'pending' in allowed_keys:
if '' in user.spawners and 'pending' in allowed_keys: model['pending'] = user.spawners[''].pending
model['pending'] = user.spawners[''].pending if (
if not (include_servers and 'servers' in allowed_keys): include_servers and 'servers' in allowed_keys
model['servers'] = None ): # Todo: Log breaking change: now no server component in user model if no access
else: servers = model['servers'] = {}
servers = model['servers'] = {} for name, spawner in user.spawners.items():
for name, spawner in user.spawners.items(): # include 'active' servers, not just ready
# include 'active' servers, not just ready # (this includes pending events)
# (this includes pending events) if spawner.active:
if spawner.active: servers[name] = self.server_model(
servers[name] = self.server_model( spawner, include_state=include_state
spawner, include_state=include_state )
)
return model return model
def group_model(self, group): def group_model(self, group):

View File

@@ -38,7 +38,7 @@ class GroupListAPIHandler(_GroupAPIHandler):
def get(self): def get(self):
"""List groups""" """List groups"""
groups = self.db.query(orm.Group) groups = self.db.query(orm.Group)
scope_filter = self.get_scope_filter(self.db) scope_filter = self.get_scope_filter('read:groups')
if scope_filter is not None: if scope_filter is not None:
groups = groups.filter(orm.Group.name.in_(scope_filter)) groups = groups.filter(orm.Group.name.in_(scope_filter))
data = [self.group_model(g) for g in groups] data = [self.group_model(g) for g in groups]

View File

@@ -427,16 +427,17 @@ class BaseHandler(RequestHandler):
if user and isinstance(user, User): if user and isinstance(user, User):
user = await self.refresh_auth(user) user = await self.refresh_auth(user)
self._jupyterhub_user = user self._jupyterhub_user = user
self._parse_scopes()
except Exception: except Exception:
# don't let errors here raise more than once # don't let errors here raise more than once
self._jupyterhub_user = None self._jupyterhub_user = None
self.log.exception("Error getting current user") self.log.exception("Error getting current user")
self._parse_scopes()
return self._jupyterhub_user return self._jupyterhub_user
def _parse_scopes(self): def _parse_scopes(self):
if self._jupyterhub_user is not None or self.get_current_user_oauth_token(): if self._jupyterhub_user is not None or self.get_current_user_oauth_token():
self.raw_scopes = roles.get_subscopes(*self._jupyterhub_user.roles) if self.current_user: # Todo: Deal with oauth tokens
self.raw_scopes = roles.get_subscopes(*self.current_user.roles)
if 'all' in self.raw_scopes: if 'all' in self.raw_scopes:
self.raw_scopes |= scopes.get_user_scopes(self.current_user.name) self.raw_scopes |= scopes.get_user_scopes(self.current_user.name)
self.parsed_scopes = scopes.parse_scopes(self.raw_scopes) self.parsed_scopes = scopes.parse_scopes(self.raw_scopes)

View File

@@ -143,6 +143,7 @@ def needs_scope(*scopes):
def scope_decorator(func): def scope_decorator(func):
@functools.wraps(func) @functools.wraps(func)
def _auth_func(self, *args, **kwargs): def _auth_func(self, *args, **kwargs):
self.parse_scopes() # Todo: Check most practical locations for parsing scopes
sig = inspect.signature(func) sig = inspect.signature(func)
bound_sig = sig.bind(self, *args, **kwargs) bound_sig = sig.bind(self, *args, **kwargs)
bound_sig.apply_defaults() bound_sig.apply_defaults()

View File

@@ -260,7 +260,9 @@ async def test_get_self(app):
db = app.db db = app.db
# basic get self # basic get self
r = await api_request(app, 'user') r = await api_request(
app, 'user', headers=auth_header(db, 'user')
) # Todo: check after dealing with oauth
r.raise_for_status() r.raise_for_status()
assert r.json()['kind'] == 'user' assert r.json()['kind'] == 'user'

View File

@@ -54,7 +54,7 @@ def test_scope_check_present():
def test_scope_check_not_present(): def test_scope_check_not_present():
handler = get_handler_with_scopes(['read:users!user=maeby']) handler = get_handler_with_scopes(['read:users!user=maeby'])
assert not _check_scope(handler, 'read:users') assert _check_scope(handler, 'read:users')
with pytest.raises(web.HTTPError): with pytest.raises(web.HTTPError):
_check_scope(handler, 'read:users', user='gob') _check_scope(handler, 'read:users', user='gob')
with pytest.raises(web.HTTPError): with pytest.raises(web.HTTPError):
@@ -103,7 +103,8 @@ class MockAPIHandler:
return True return True
@needs_scope('users') @needs_scope('users')
def other_thing(self, other_name): def other_thing(self, non_filter_argument):
# Rely on inner vertical filtering
return True return True
@needs_scope('users') @needs_scope('users')
@@ -161,8 +162,8 @@ class MockAPIHandler:
), ),
(['users'], 'other_thing', ('gob',), True), (['users'], 'other_thing', ('gob',), True),
(['read:users'], 'other_thing', ('gob',), False), (['read:users'], 'other_thing', ('gob',), False),
(['users!user=gob'], 'other_thing', ('gob',), False), (['users!user=gob'], 'other_thing', ('gob',), True),
(['users!user=gob'], 'other_thing', ('maeby',), False), (['users!user=gob'], 'other_thing', ('maeby',), True),
], ],
) )
def test_scope_method_access(scopes, method, arguments, is_allowed): def test_scope_method_access(scopes, method, arguments, is_allowed):
@@ -403,7 +404,8 @@ async def test_vertical_filter(app):
r = await api_request(app, 'users', headers=auth_header(app.db, user_name)) r = await api_request(app, 'users', headers=auth_header(app.db, user_name))
assert r.status_code == 200 assert r.status_code == 200
assert set(r.json().keys()) == {'names'} allowed_keys = {'name', 'kind'}
assert set([key for user in r.json() for key in user.keys()]) == allowed_keys
async def test_stacked_vertical_filter(app): async def test_stacked_vertical_filter(app):