Removed user model flags, scope-guarded server model with new scopes

This commit is contained in:
0mar
2021-04-01 17:26:34 +02:00
parent 036a4eb934
commit 71a5842ad2
4 changed files with 40 additions and 40 deletions

View File

@@ -79,23 +79,32 @@ class APIHandler(BaseHandler):
% req_scope, % req_scope,
) )
def has_access(orm_resource, kind): def has_access_to(orm_resource, kind):
""" """
param orm_resource: User or Service or Group param orm_resource: User or Service or Group or spawner
param kind: 'users' or 'services' or 'groups' param kind: 'user' or 'service' or 'group' or 'server'.
`kind` could probably be derived from `orm_resource`, problem is Jupyterhub.users.User
""" """
if sub_scope == scopes.Scope.ALL: if sub_scope == scopes.Scope.ALL:
return True return True
else: else:
found_resource = orm_resource.name in sub_scope[kind] found_resource = orm_resource.name in sub_scope[kind]
if not found_resource: # Try group-based access if not found_resource: # Try group-based access
if 'group' in sub_scope and kind == 'user': if kind == 'server':
# First check if we have access to user info
user_name = orm_resource.user.name
found_resource = user_name in sub_scope['user']
if not found_resource:
# Now check for specific servers:
server_format = f"{orm_resource.user / orm_resource.name}"
found_resource = server_format in sub_scope[kind]
elif 'group' in sub_scope and kind == 'user':
group_names = {group.name for group in orm_resource.groups} group_names = {group.name for group in orm_resource.groups}
user_in_group = bool(group_names & set(sub_scope['group'])) user_in_group = bool(group_names & set(sub_scope['group']))
found_resource = user_in_group found_resource = user_in_group
return found_resource return found_resource
return has_access return has_access_to
def get_current_user_cookie(self): def get_current_user_cookie(self):
"""Override get_user_cookie to check Referer header""" """Override get_user_cookie to check Referer header"""
@@ -168,17 +177,24 @@ class APIHandler(BaseHandler):
def server_model(self, spawner, include_state=False): def server_model(self, spawner, include_state=False):
"""Get the JSON model for a Spawner""" """Get the JSON model for a Spawner"""
return { server_scope = 'read:users:servers'
server_state_scope = 'admin:users:server_state'
model = {
'name': spawner.name, 'name': spawner.name,
'last_activity': isoformat(spawner.orm_spawner.last_activity), 'last_activity': isoformat(spawner.orm_spawner.last_activity),
'started': isoformat(spawner.orm_spawner.started), 'started': isoformat(spawner.orm_spawner.started),
'pending': spawner.pending, 'pending': spawner.pending,
'ready': spawner.ready, 'ready': spawner.ready,
'state': spawner.get_state() if include_state else None,
'url': url_path_join(spawner.user.url, spawner.name, '/'), 'url': url_path_join(spawner.user.url, spawner.name, '/'),
'user_options': spawner.user_options, 'user_options': spawner.user_options,
'progress_url': spawner._progress_url, 'progress_url': spawner._progress_url,
} }
# First check users, then servers
if server_state_scope in self.parsed_scopes:
scope_filter = self.get_scope_filter(server_state_scope)
if scope_filter(spawner, kind='server'):
model['state'] = spawner.get_state()
return model
def token_model(self, token): def token_model(self, token):
"""Get the JSON model for an APIToken""" """Get the JSON model for an APIToken"""
@@ -220,13 +236,7 @@ class APIHandler(BaseHandler):
model.update(extra) model.update(extra)
return model return model
def user_model( def user_model(self, user):
self,
user,
include_servers=False,
include_server_state=False,
include_auth_state=False,
):
"""Get the JSON model for a User object""" """Get the JSON model for a User object"""
if isinstance(user, orm.User): if isinstance(user, orm.User):
user = self.users[user.id] user = self.users[user.id]
@@ -258,8 +268,8 @@ class APIHandler(BaseHandler):
'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'},
'read:users:auth_state': {'kind', 'name', 'auth_state'}, 'admin:users:auth_state': {'kind', 'name', 'auth_state'},
'read:users:server_state': {'kind', 'name', 'server_state'}, 'admin:users:server_state': {'kind', 'name', 'servers', 'server_state'},
} }
self.log.debug( self.log.debug(
"Asking for user model of %s with scopes [%s]", "Asking for user model of %s with scopes [%s]",
@@ -273,21 +283,16 @@ class APIHandler(BaseHandler):
if scope_filter(user, kind='user'): if scope_filter(user, kind='user'):
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 include_auth_state:
model.pop("auth_state", None)
if model: if model:
include_server_state &= 'server_state' in allowed_keys
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 include_servers and 'servers' in allowed_keys: if 'servers' in allowed_keys:
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)
spawner, include_state=include_server_state
)
return model return model
def group_model(self, group): def group_model(self, group):
@@ -308,11 +313,8 @@ class APIHandler(BaseHandler):
scope_filter = self.get_scope_filter(req_scope) scope_filter = self.get_scope_filter(req_scope)
if scope_filter(service, kind='service'): if scope_filter(service, kind='service'):
model['roles'] = [r.name for r in service.roles] model['roles'] = [r.name for r in service.roles]
model[ model['admin'] = service.admin
'admin' # todo: Remove once we replace admin flag with role check
] = (
service.admin
) # todo: Remove once we replace admin flag with role check
return model return model
_user_model_types = {'name': str, 'admin': bool, 'groups': list, 'auth_state': dict} _user_model_types = {'name': str, 'admin': bool, 'groups': list, 'auth_state': dict}

View File

@@ -104,9 +104,7 @@ class UserListAPIHandler(APIHandler):
data = [] data = []
for u in query: for u in query:
if post_filter is None or post_filter(u): if post_filter is None or post_filter(u):
user_model = self.user_model( user_model = self.user_model(u)
u, include_servers=True, include_server_state=True
)
if user_model: if user_model:
data.append(user_model) data.append(user_model)
self.write(json.dumps(data)) self.write(json.dumps(data))
@@ -188,16 +186,15 @@ def admin_or_self(method):
class UserAPIHandler(APIHandler): class UserAPIHandler(APIHandler):
@needs_scope( @needs_scope(
'read:users' 'read:users',
) # Todo: Add the same list of scopes as at UserListAPIHandler 'read:users:name',
'reda:users:servers',
'read:users:groups',
'read:users:activity',
)
async def get(self, user_name): async def get(self, user_name):
user = self.find_user(user_name) user = self.find_user(user_name)
model = self.user_model( model = self.user_model(user)
user,
include_servers=True,
include_server_state=True,
include_auth_state=True,
)
# auth state will only be shown if the requester is an admin # auth state will only be shown if the requester is an admin
# this means users can't see their own auth state unless they # this means users can't see their own auth state unless they
# are admins, Hub admins often are also marked as admins so they # are admins, Hub admins often are also marked as admins so they

View File

@@ -90,7 +90,7 @@ def get_scope_hierarchy():
'read:users:servers', 'read:users:servers',
], ],
'users:tokens': ['read:users:tokens'], 'users:tokens': ['read:users:tokens'],
'admin:users': ['read:users:auth_state', 'read:users:server_state'], 'admin:users': ['admin:users:auth_state', 'admin:users:server_state'],
'admin:users:servers': None, 'admin:users:servers': None,
'groups': ['read:groups'], 'groups': ['read:groups'],
'admin:groups': None, 'admin:groups': None,

View File

@@ -159,6 +159,7 @@ def fill_user(model):
model.setdefault('created', TIMESTAMP) model.setdefault('created', TIMESTAMP)
model.setdefault('last_activity', TIMESTAMP) model.setdefault('last_activity', TIMESTAMP)
model.setdefault('servers', {}) model.setdefault('servers', {})
model.setdefault('auth_state', '')
return model return model