mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-14 21:43:01 +00:00
Fixed vertical filtering in user models, but does not work for OAuth yet
This commit is contained in:
@@ -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):
|
||||||
|
@@ -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]
|
||||||
|
@@ -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)
|
||||||
|
@@ -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()
|
||||||
|
@@ -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'
|
||||||
|
|
||||||
|
@@ -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):
|
||||||
|
Reference in New Issue
Block a user