adding roles to services

This commit is contained in:
IvanaH8
2020-10-28 11:16:03 +01:00
parent 4142dc1bc0
commit 087c763d41
10 changed files with 158 additions and 36 deletions

View File

@@ -222,7 +222,12 @@ class APIHandler(BaseHandler):
def service_model(self, service): def service_model(self, service):
"""Get the JSON model for a Service object""" """Get the JSON model for a Service object"""
return {'kind': 'service', 'name': service.name, 'admin': service.admin} return {
'kind': 'service',
'name': service.name,
'admin': service.admin,
'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, 'auth_state': dict}

View File

@@ -18,6 +18,7 @@ def service_model(service):
return { return {
'name': service.name, 'name': service.name,
'admin': service.admin, 'admin': service.admin,
'roles': [r.name for r in service.roles],
'url': service.url, 'url': service.url,
'prefix': service.server.base_url if service.server else '', 'prefix': service.server.base_url if service.server else '',
'command': service.command, 'command': service.command,

View File

@@ -1834,26 +1834,39 @@ class JupyterHub(Application):
role = orm.Role.find(db, predef_role['name']) role = orm.Role.find(db, predef_role['name'])
# handle users # handle users
for username in predef_role['users']: if 'users' in predef_role.keys():
username = self.authenticator.normalize_username(username) for username in predef_role['users']:
if not ( username = self.authenticator.normalize_username(username)
await maybe_future(self.authenticator.check_allowed(username, None)) if not (
): await maybe_future(
raise ValueError( self.authenticator.check_allowed(username, None)
"Username %r is not in Authenticator.allowed_users" % username )
) ):
user = orm.User.find(db, name=username) raise ValueError(
if user is None: "Username %r is not in Authenticator.allowed_users"
if not self.authenticator.validate_username(username): % username
raise ValueError("Role username %r is not valid" % username) )
user = orm.User(name=username) user = orm.User.find(db, name=username)
db.add(user) if user is None:
roles.add_user(db, user=user, role=role) raise ValueError("%r does not exist" % username)
else:
roles.add_user(db, user=user, role=role)
# make sure all users have at least one role (update with default) # handle services
for user in db.query(orm.User): if 'services' in predef_role.keys():
if len(user.roles) < 1: for servicename in predef_role['services']:
roles.update_roles(db, user) service = orm.Service.find(db, name=servicename)
if service is None:
raise ValueError("%r does not exist" % servicename)
else:
roles.add_user(db, user=service, role=role)
# make sure all users and services have at least one role (update with default)
Classes = [orm.User, orm.Service]
for ormClass in Classes:
for obj in db.query(ormClass):
if len(obj.roles) < 1:
roles.update_roles(db, obj)
db.commit() db.commit()
@@ -1968,6 +1981,7 @@ class JupyterHub(Application):
base_url=self.base_url, base_url=self.base_url,
db=self.db, db=self.db,
orm=orm_service, orm=orm_service,
roles=orm_service.roles,
domain=domain, domain=domain,
host=host, host=host,
hub=self.hub, hub=self.hub,
@@ -2435,9 +2449,9 @@ class JupyterHub(Application):
self.init_oauth() self.init_oauth()
await self.init_users() await self.init_users()
await self.init_groups() await self.init_groups()
await self.init_roles()
self.init_services() self.init_services()
await self.init_api_tokens() await self.init_api_tokens()
await self.init_roles()
self.init_tornado_settings() self.init_tornado_settings()
self.init_handlers() self.init_handlers()
self.init_tornado_application() self.init_tornado_application()

View File

@@ -141,6 +141,16 @@ user_role_map = Table(
Column('role_id', ForeignKey('roles.id', ondelete='CASCADE'), primary_key=True), Column('role_id', ForeignKey('roles.id', ondelete='CASCADE'), primary_key=True),
) )
# service:role many:many mapping table
service_role_map = Table(
'service_role_map',
Base.metadata,
Column(
'service_id', ForeignKey('services.id', ondelete='CASCADE'), primary_key=True
),
Column('role_id', ForeignKey('roles.id', ondelete='CASCADE'), primary_key=True),
)
class Role(Base): class Role(Base):
"""User Roles""" """User Roles"""
@@ -151,6 +161,7 @@ class Role(Base):
description = Column(Unicode(1023)) description = Column(Unicode(1023))
scopes = Column(JSONList) scopes = Column(JSONList)
users = relationship('User', secondary='user_role_map', backref='roles') users = relationship('User', secondary='user_role_map', backref='roles')
services = relationship('Service', secondary='service_role_map', backref='roles')
def __repr__(self): def __repr__(self):
return "<%s %s (%s) - scopes: %s>" % ( return "<%s %s (%s) - scopes: %s>" % (

View File

@@ -55,6 +55,8 @@ def add_role(db, role_dict):
else: else:
role.description = role_dict['description'] role.description = role_dict['description']
role.scopes = role_dict['scopes'] role.scopes = role_dict['scopes']
role.users = []
role.services = []
db.commit() db.commit()

View File

@@ -267,6 +267,7 @@ class Service(LoggingConfigurable):
base_url = Unicode() base_url = Unicode()
db = Any() db = Any()
orm = Any() orm = Any()
roles = Any()
cookie_options = Dict() cookie_options = Dict()
oauth_provider = Any() oauth_provider = Any()

View File

@@ -10,8 +10,7 @@ from datetime import datetime
import jupyterhub import jupyterhub
from jupyterhub import orm from jupyterhub import orm
# FIXME - for later versions of jupyterhub add code to test Roles # FIXME - for later versions of jupyterhub add code to test roles
# from jupyterhub.orm import Role
def populate_db(url): def populate_db(url):

View File

@@ -1536,6 +1536,7 @@ async def test_get_services(app, mockservice_url):
mockservice.name: { mockservice.name: {
'name': mockservice.name, 'name': mockservice.name,
'admin': True, 'admin': True,
'roles': [],
'command': mockservice.command, 'command': mockservice.command,
'pid': mockservice.proc.pid, 'pid': mockservice.proc.pid,
'prefix': mockservice.server.base_url, 'prefix': mockservice.server.base_url,
@@ -1561,6 +1562,7 @@ async def test_get_service(app, mockservice_url):
assert service == { assert service == {
'name': mockservice.name, 'name': mockservice.name,
'admin': True, 'admin': True,
'roles': [],
'command': mockservice.command, 'command': mockservice.command,
'pid': mockservice.proc.pid, 'pid': mockservice.proc.pid,
'prefix': mockservice.server.base_url, 'prefix': mockservice.server.base_url,

View File

@@ -4,33 +4,45 @@ from pytest import mark
from .. import orm from .. import orm
from .. import roles from .. import roles
from ..utils import maybe_future
from .mocking import MockHub from .mocking import MockHub
@mark.role @mark.role
def test_roles(db): def test_orm_roles(db):
"""Test orm roles setup""" """Test orm roles setup"""
user = orm.User(name='falafel') user = orm.User(name='falafel')
db.add(user) db.add(user)
service = orm.Service(name='kebab')
db.add(service)
role = orm.Role(name='default') role = orm.Role(name='default')
db.add(role) db.add(role)
db.commit() db.commit()
assert role.users == [] assert role.users == []
assert role.services == []
assert user.roles == [] assert user.roles == []
assert service.roles == []
role.users.append(user) role.users.append(user)
role.services.append(service)
db.commit() db.commit()
assert role.users == [user] assert role.users == [user]
assert user.roles == [role] assert user.roles == [role]
assert role.services == [service]
assert service.roles == [role]
db.delete(user) db.delete(user)
db.commit() db.commit()
assert role.users == [] assert role.users == []
db.delete(role) db.delete(role)
db.commit()
assert service.roles == []
db.delete(service)
db.commit()
@mark.role @mark.role
def test_role_delete_cascade(db): def test_orm_role_delete_cascade(db):
"""Orm roles cascade""" """Orm roles cascade"""
user1 = orm.User(name='user1') user1 = orm.User(name='user1')
user2 = orm.User(name='user2') user2 = orm.User(name='user2')
@@ -93,8 +105,25 @@ def test_role_delete_cascade(db):
@mark.role @mark.role
async def test_load_roles(tmpdir, request): async def test_load_default_roles(tmpdir, request):
"""Test loading default and predefined roles in app.py""" """Test loading default roles in app.py"""
kwargs = {}
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
await hub.init_roles()
# test default roles loaded to database
assert orm.Role.find(db, 'user') is not None
assert orm.Role.find(db, 'admin') is not None
assert orm.Role.find(db, 'server') is not None
@mark.role
async def test_load_roles_users(tmpdir, request):
"""Test loading predefined roles for users in app.py"""
roles_to_load = [ roles_to_load = [
{ {
'name': 'teacher', 'name': 'teacher',
@@ -106,7 +135,7 @@ async def test_load_roles(tmpdir, request):
'name': 'user', 'name': 'user',
'description': 'Only read access', 'description': 'Only read access',
'scopes': ['read:all'], 'scopes': ['read:all'],
'users': ['test_user'], 'users': ['bilbo'],
}, },
] ]
kwargs = {'load_roles': roles_to_load} kwargs = {'load_roles': roles_to_load}
@@ -116,20 +145,28 @@ async def test_load_roles(tmpdir, request):
hub = MockHub(**kwargs) hub = MockHub(**kwargs)
hub.init_db() hub.init_db()
db = hub.db db = hub.db
hub.authenticator.admin_users = ['admin']
hub.authenticator.allowed_users = ['cyclops', 'gandalf', 'bilbo', 'gargamel']
await hub.init_users() await hub.init_users()
for user in db.query(orm.User):
print(user.name)
await hub.init_roles() await hub.init_roles()
# test if the 'user' role has been overwritten
# test if the 'user' role has been overwritten and assigned
user_role = orm.Role.find(db, 'user') user_role = orm.Role.find(db, 'user')
admin_role = orm.Role.find(db, 'admin')
assert user_role is not None assert user_role is not None
assert user_role.scopes == ['read:all'] assert user_role.scopes == ['read:all']
# test other default roles loaded to database
assert orm.Role.find(db, 'user') is not None # test if every user has a role (and no duplicates)
assert orm.Role.find(db, 'admin') is not None # and admins have admin role
assert orm.Role.find(db, 'server') is not None
# test if every existing user has a role (and no duplicates)
for user in db.query(orm.User): for user in db.query(orm.User):
assert len(user.roles) > 0 assert len(user.roles) > 0
assert len(user.roles) == len(set(user.roles)) assert len(user.roles) == len(set(user.roles))
if user.admin:
assert admin_role in user.roles
assert user_role not in user.roles
# test if predefined roles loaded and assigned # test if predefined roles loaded and assigned
teacher_role = orm.Role.find(db, name='teacher') teacher_role = orm.Role.find(db, name='teacher')
assert teacher_role is not None assert teacher_role is not None
@@ -137,3 +174,53 @@ async def test_load_roles(tmpdir, request):
assert teacher_role in gandalf_user.roles assert teacher_role in gandalf_user.roles
cyclops_user = orm.User.find(db, name='cyclops') cyclops_user = orm.User.find(db, name='cyclops')
assert teacher_role in cyclops_user.roles assert teacher_role in cyclops_user.roles
@mark.role
async def test_load_roles_services(tmpdir, request):
roles_to_load = [
{
'name': 'culler',
'description': 'Cull idle servers',
'scopes': ['users:servers', 'admin:servers'],
'services': ['cull_idle'],
},
]
kwargs = {'load_roles': roles_to_load}
ssl_enabled = getattr(request.module, "ssl_enabled", False)
if ssl_enabled:
kwargs['internal_certs_location'] = str(tmpdir)
hub = MockHub(test_clean_db=False, **kwargs)
hub.init_db()
db = hub.db
# add test services to db
services = [
{'name': 'cull_idle', 'admin': False},
{'name': 'user_service', 'admin': False},
{'name': 'admin_service', 'admin': True},
]
for service_specs in services:
service = orm.Service.find(db, service_specs['name'])
if service is None:
service = orm.Service(
name=service_specs['name'], admin=service_specs['admin']
)
db.add(service)
db.commit()
await hub.init_roles()
# test if every service has a role (and no duplicates)
admin_role = orm.Role.find(db, name='admin')
user_role = orm.Role.find(db, name='user')
for service in db.query(orm.Service):
assert len(service.roles) > 0
assert len(service.roles) == len(set(service.roles))
if service.admin:
assert admin_role in service.roles
assert user_role not in service.roles
# test if predefined roles loaded and assigned
culler_role = orm.Role.find(db, name='culler')
cull_idle = orm.Service.find(db, name='cull_idle')
assert culler_role in cull_idle.roles
assert user_role not in cull_idle.roles

View File

@@ -295,7 +295,7 @@ async def test_hubauth_service_token(app, mockservice_url):
) )
r.raise_for_status() r.raise_for_status()
reply = r.json() reply = r.json()
assert reply == {'kind': 'service', 'name': name, 'admin': False} assert reply == {'kind': 'service', 'name': name, 'admin': False, 'roles': []}
assert not r.cookies assert not r.cookies
# token in ?token parameter # token in ?token parameter
@@ -304,7 +304,7 @@ async def test_hubauth_service_token(app, mockservice_url):
) )
r.raise_for_status() r.raise_for_status()
reply = r.json() reply = r.json()
assert reply == {'kind': 'service', 'name': name, 'admin': False} assert reply == {'kind': 'service', 'name': name, 'admin': False, 'roles': []}
r = await async_requests.get( r = await async_requests.get(
public_url(app, mockservice_url) + '/whoami/?token=no-such-token', public_url(app, mockservice_url) + '/whoami/?token=no-such-token',