fixed default roles for mocked services

This commit is contained in:
IvanaH8
2020-12-16 11:17:43 +01:00
parent 4cc2f0a363
commit 5e8864f29d
7 changed files with 175 additions and 16 deletions

View File

@@ -222,6 +222,7 @@ class APIHandler(BaseHandler):
'kind': 'group',
'name': group.name,
'users': [u.name for u in group.users],
'roles': [r.name for r in group.roles],
}
def service_model(self, service):
@@ -233,9 +234,15 @@ class APIHandler(BaseHandler):
'roles': [r.name for r in service.roles],
}
_user_model_types = {'name': str, 'admin': bool, 'groups': list, 'auth_state': dict}
_user_model_types = {
'name': str,
'admin': bool,
'groups': list,
'roles': list,
'auth_state': dict,
}
_group_model_types = {'name': str, 'users': list}
_group_model_types = {'name': str, 'users': list, 'roles': list}
def _check_model(self, model, model_types, name):
"""Check a model provided by a REST API request

View File

@@ -326,7 +326,8 @@ class JupyterHub(Application):
'scopes': ['users', 'groups'],
'users': ['cyclops', 'gandalf'],
'services': [],
'tokens': []
'tokens': [],
'groups': []
}
]
@@ -1827,7 +1828,7 @@ class JupyterHub(Application):
async def init_roles(self):
"""Load default and predefined roles into the database"""
db = self.db
role_bearers = ['users', 'services', 'tokens']
role_bearers = ['groups', 'users', 'services', 'tokens']
# load default roles
default_roles = roles.get_default_roles()
@@ -1857,11 +1858,18 @@ class JupyterHub(Application):
)
# make sure all users, services and tokens have at least one role (update with default)
# groups can be without a role but all group members should have the same role(s) as the group
for bearer in role_bearers:
Class = roles.get_orm_class(bearer)
for obj in db.query(Class):
if len(obj.roles) < 1:
roles.update_roles(db, obj=obj, kind=bearer)
if bearer == 'groups':
for group in db.query(Class):
for user in group.users:
rolenames = roles.get_rolenames(group.roles)
roles.update_roles(db, obj=user, kind='users', roles=rolenames)
else:
for obj in db.query(Class):
if len(obj.roles) < 1:
roles.update_roles(db, obj=obj, kind=bearer)
db.commit()
async def _add_tokens(self, token_dict, kind):

View File

@@ -166,6 +166,14 @@ api_token_role_map = Table(
Column('role_id', ForeignKey('roles.id', ondelete='CASCADE'), primary_key=True),
)
# group:role many:many mapping table
group_role_map = Table(
'group_role_map',
Base.metadata,
Column('group_id', ForeignKey('groups.id', ondelete='CASCADE'), primary_key=True),
Column('role_id', ForeignKey('roles.id', ondelete='CASCADE'), primary_key=True),
)
class Role(Base):
"""User Roles"""
@@ -178,6 +186,7 @@ class Role(Base):
users = relationship('User', secondary='user_role_map', backref='roles')
services = relationship('Service', secondary='service_role_map', backref='roles')
tokens = relationship('APIToken', secondary='api_token_role_map', backref='roles')
groups = relationship('Group', secondary='group_role_map', backref='roles')
def __repr__(self):
return "<%s %s (%s) - scopes: %s>" % (

View File

@@ -147,12 +147,26 @@ def get_orm_class(kind):
Class = orm.Service
elif kind == 'tokens':
Class = orm.APIToken
elif kind == 'groups':
Class = orm.Group
else:
raise ValueError("kind must be users, services or tokens, not %r" % kind)
raise ValueError(
"kind must be users, services, tokens or groups, not %r" % kind
)
return Class
def get_rolenames(role_list):
"""Return a list of rolenames from a list of roles"""
rolenames = []
for role in role_list:
rolenames.append(role.name)
return rolenames
def existing_only(func):
"""Decorator for checking if objects and roles exist"""
@@ -176,7 +190,7 @@ def existing_only(func):
@existing_only
def add_obj(db, objname, kind, rolename):
"""Adds a role for users, services or tokens"""
"""Adds a role for users, services, tokens or groups"""
if rolename not in objname.roles:
objname.roles.append(rolename)
@@ -250,12 +264,28 @@ def update_roles(db, obj, kind, roles=None):
else:
add_obj(db, objname=obj.name, kind=kind, rolename=rolename)
else:
# groups can be without a role
if Class == orm.Group:
pass
# tokens can have only 'user' role as default
# assign the default only for user tokens
if Class == orm.APIToken:
elif Class == orm.APIToken:
if len(obj.roles) < 1 and obj.user is not None:
user_role.tokens.append(obj)
db.commit()
# users and services can have 'user' or 'admin' roles as default
else:
switch_default_role(db, obj, kind, obj.admin)
def mock_roles(app, name, kind):
"""Loads and assigns default roles for mocked objects"""
Class = get_orm_class(kind)
obj = Class.find(app.db, name=name)
default_roles = get_default_roles()
for role in default_roles:
add_role(app.db, role)
update_roles(db=app.db, obj=obj, kind=kind)

View File

@@ -44,6 +44,7 @@ import jupyterhub.services.service
from . import mocking
from .. import crypto
from .. import orm
from ..roles import mock_roles
from ..utils import random_port
from .mocking import MockHub
from .test_services import mockservice_cmd
@@ -245,6 +246,7 @@ def _mockservice(request, app, url=False):
):
app.services = [spec]
app.init_services()
mock_roles(app, name, 'services')
assert name in app._service_map
service = app._service_map[name]

View File

@@ -1446,7 +1446,7 @@ async def test_groups_list(app):
r = await api_request(app, 'groups')
r.raise_for_status()
reply = r.json()
assert reply == [{'kind': 'group', 'name': 'alphaflight', 'users': []}]
assert reply == [{'kind': 'group', 'name': 'alphaflight', 'users': [], 'roles': []}]
@mark.group
@@ -1481,7 +1481,12 @@ async def test_group_get(app):
r = await api_request(app, 'groups/alphaflight')
r.raise_for_status()
reply = r.json()
assert reply == {'kind': 'group', 'name': 'alphaflight', 'users': ['sasquatch']}
assert reply == {
'kind': 'group',
'name': 'alphaflight',
'users': ['sasquatch'],
'roles': [],
}
@mark.group
@@ -1594,7 +1599,7 @@ async def test_get_services(app, mockservice_url):
mockservice.name: {
'name': mockservice.name,
'admin': True,
'roles': [],
'roles': ['admin'],
'command': mockservice.command,
'pid': mockservice.proc.pid,
'prefix': mockservice.server.base_url,
@@ -1620,7 +1625,7 @@ async def test_get_service(app, mockservice_url):
assert service == {
'name': mockservice.name,
'admin': True,
'roles': [],
'roles': ['admin'],
'command': mockservice.command,
'pid': mockservice.proc.pid,
'prefix': mockservice.server.base_url,

View File

@@ -26,6 +26,18 @@ def test_orm_roles(db):
db.add(service_role)
db.commit()
group_role = orm.Role(name='group', scopes=['read:users'])
db.add(group_role)
db.commit()
group_role = orm.Role(name='group', scopes=['read:users'])
db.add(group_role)
db.commit()
group_role = orm.Role(name='group', scopes=['read:users'])
db.add(group_role)
db.commit()
user = orm.User(name='falafel')
db.add(user)
db.commit()
@@ -34,20 +46,30 @@ def test_orm_roles(db):
db.add(service)
db.commit()
group = orm.Group(name='fast-food')
db.add(group)
db.commit()
assert user_role.users == []
assert user_role.services == []
assert user_role.groups == []
assert service_role.users == []
assert service_role.services == []
assert service_role.groups == []
assert user.roles == []
assert service.roles == []
assert group.roles == []
user_role.users.append(user)
service_role.services.append(service)
group_role.groups.append(group)
db.commit()
assert user_role.users == [user]
assert user.roles == [user_role]
assert service_role.services == [service]
assert service.roles == [service_role]
assert group_role.groups == [group]
assert group.roles == [group_role]
# check token creation without specifying its role
# assigns it the default 'user' role
@@ -67,16 +89,22 @@ def test_orm_roles(db):
db.commit()
assert user_role.users == []
assert user_token not in user_role.tokens
# check deleting the service token removes it from 'service' role
# check deleting the service token removes it from service_role
db.delete(service_token)
db.commit()
assert service_token not in service_role.tokens
# check deleting the 'service' role removes it from service roles
# check deleting the service_role removes it from service.roles
db.delete(service_role)
db.commit()
assert service.roles == []
# check deleting the group removes it from group_roles
db.delete(group)
db.commit()
assert group_role.groups == []
# clean up db
db.delete(service)
db.delete(group_role)
db.commit()
@@ -310,6 +338,76 @@ async def test_load_roles_services(tmpdir, request):
db.commit()
@mark.role
async def test_load_roles_groups(tmpdir, request):
"""Test loading predefined roles for groups in app.py"""
groups_to_load = {
'group1': ['gandalf'],
'group2': ['bilbo', 'gargamel'],
'group3': ['cyclops'],
}
roles_to_load = [
{
'name': 'assistant',
'description': 'Access users information only',
'scopes': ['read:users'],
'groups': ['group2'],
},
{
'name': 'head',
'description': 'Whole user access',
'scopes': ['users', 'admin:users'],
'groups': ['group3'],
},
]
kwargs = {'load_groups': groups_to_load, 'load_roles': roles_to_load}
ssl_enabled = getattr(request.module, "ssl_enabled", False)
if ssl_enabled:
kwargs['internal_certs_location'] = str(tmpdir)
hub = MockHub(**kwargs)
hub.init_db()
db = hub.db
hub.authenticator.allowed_users = ['cyclops', 'gandalf', 'bilbo', 'gargamel']
await hub.init_users()
await hub.init_groups()
await hub.init_roles()
assist_role = orm.Role.find(db, name='assistant')
head_role = orm.Role.find(db, name='head')
user_role = orm.Role.find(db, name='user')
group1 = orm.Group.find(db, name='group1')
group2 = orm.Group.find(db, name='group2')
group3 = orm.Group.find(db, name='group3')
gandalf = orm.User.find(db, name='gandalf')
bilbo = orm.User.find(db, name='bilbo')
gargamel = orm.User.find(db, name='gargamel')
cyclops = orm.User.find(db, name='cyclops')
# test group roles
assert group1.roles == []
assert group2 in assist_role.groups
assert group3 in head_role.groups
# test group members' roles
assert assist_role in bilbo.roles
assert assist_role in gargamel.roles
assert head_role in cyclops.roles
# check the default user_role assignment
# FIXME - should users with group roles still have default?
assert user_role in gandalf.roles
assert user_role not in bilbo.roles
assert user_role not in gargamel.roles
assert user_role not in cyclops.roles
# FIXME
# @mark.role
# async def test_group_roles_delete_cascade(tmpdir, request):
@mark.role
async def test_load_roles_tokens(tmpdir, request):
services = [