mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-14 05:23:01 +00:00
adding roles to services
This commit is contained in:
@@ -222,7 +222,12 @@ class APIHandler(BaseHandler):
|
||||
|
||||
def service_model(self, service):
|
||||
"""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}
|
||||
|
||||
|
@@ -18,6 +18,7 @@ def service_model(service):
|
||||
return {
|
||||
'name': service.name,
|
||||
'admin': service.admin,
|
||||
'roles': [r.name for r in service.roles],
|
||||
'url': service.url,
|
||||
'prefix': service.server.base_url if service.server else '',
|
||||
'command': service.command,
|
||||
|
@@ -1834,26 +1834,39 @@ class JupyterHub(Application):
|
||||
role = orm.Role.find(db, predef_role['name'])
|
||||
|
||||
# handle users
|
||||
for username in predef_role['users']:
|
||||
username = self.authenticator.normalize_username(username)
|
||||
if not (
|
||||
await maybe_future(self.authenticator.check_allowed(username, None))
|
||||
):
|
||||
raise ValueError(
|
||||
"Username %r is not in Authenticator.allowed_users" % username
|
||||
)
|
||||
user = orm.User.find(db, name=username)
|
||||
if user is None:
|
||||
if not self.authenticator.validate_username(username):
|
||||
raise ValueError("Role username %r is not valid" % username)
|
||||
user = orm.User(name=username)
|
||||
db.add(user)
|
||||
roles.add_user(db, user=user, role=role)
|
||||
if 'users' in predef_role.keys():
|
||||
for username in predef_role['users']:
|
||||
username = self.authenticator.normalize_username(username)
|
||||
if not (
|
||||
await maybe_future(
|
||||
self.authenticator.check_allowed(username, None)
|
||||
)
|
||||
):
|
||||
raise ValueError(
|
||||
"Username %r is not in Authenticator.allowed_users"
|
||||
% username
|
||||
)
|
||||
user = orm.User.find(db, name=username)
|
||||
if user is None:
|
||||
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)
|
||||
for user in db.query(orm.User):
|
||||
if len(user.roles) < 1:
|
||||
roles.update_roles(db, user)
|
||||
# handle services
|
||||
if 'services' in predef_role.keys():
|
||||
for servicename in predef_role['services']:
|
||||
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()
|
||||
|
||||
@@ -1968,6 +1981,7 @@ class JupyterHub(Application):
|
||||
base_url=self.base_url,
|
||||
db=self.db,
|
||||
orm=orm_service,
|
||||
roles=orm_service.roles,
|
||||
domain=domain,
|
||||
host=host,
|
||||
hub=self.hub,
|
||||
@@ -2435,9 +2449,9 @@ class JupyterHub(Application):
|
||||
self.init_oauth()
|
||||
await self.init_users()
|
||||
await self.init_groups()
|
||||
await self.init_roles()
|
||||
self.init_services()
|
||||
await self.init_api_tokens()
|
||||
await self.init_roles()
|
||||
self.init_tornado_settings()
|
||||
self.init_handlers()
|
||||
self.init_tornado_application()
|
||||
|
@@ -141,6 +141,16 @@ user_role_map = Table(
|
||||
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):
|
||||
"""User Roles"""
|
||||
@@ -151,6 +161,7 @@ class Role(Base):
|
||||
description = Column(Unicode(1023))
|
||||
scopes = Column(JSONList)
|
||||
users = relationship('User', secondary='user_role_map', backref='roles')
|
||||
services = relationship('Service', secondary='service_role_map', backref='roles')
|
||||
|
||||
def __repr__(self):
|
||||
return "<%s %s (%s) - scopes: %s>" % (
|
||||
|
@@ -55,6 +55,8 @@ def add_role(db, role_dict):
|
||||
else:
|
||||
role.description = role_dict['description']
|
||||
role.scopes = role_dict['scopes']
|
||||
role.users = []
|
||||
role.services = []
|
||||
db.commit()
|
||||
|
||||
|
||||
|
@@ -267,6 +267,7 @@ class Service(LoggingConfigurable):
|
||||
base_url = Unicode()
|
||||
db = Any()
|
||||
orm = Any()
|
||||
roles = Any()
|
||||
cookie_options = Dict()
|
||||
|
||||
oauth_provider = Any()
|
||||
|
@@ -10,8 +10,7 @@ from datetime import datetime
|
||||
import jupyterhub
|
||||
from jupyterhub import orm
|
||||
|
||||
# FIXME - for later versions of jupyterhub add code to test Roles
|
||||
# from jupyterhub.orm import Role
|
||||
# FIXME - for later versions of jupyterhub add code to test roles
|
||||
|
||||
|
||||
def populate_db(url):
|
||||
|
@@ -1536,6 +1536,7 @@ async def test_get_services(app, mockservice_url):
|
||||
mockservice.name: {
|
||||
'name': mockservice.name,
|
||||
'admin': True,
|
||||
'roles': [],
|
||||
'command': mockservice.command,
|
||||
'pid': mockservice.proc.pid,
|
||||
'prefix': mockservice.server.base_url,
|
||||
@@ -1561,6 +1562,7 @@ async def test_get_service(app, mockservice_url):
|
||||
assert service == {
|
||||
'name': mockservice.name,
|
||||
'admin': True,
|
||||
'roles': [],
|
||||
'command': mockservice.command,
|
||||
'pid': mockservice.proc.pid,
|
||||
'prefix': mockservice.server.base_url,
|
||||
|
@@ -4,33 +4,45 @@ from pytest import mark
|
||||
|
||||
from .. import orm
|
||||
from .. import roles
|
||||
from ..utils import maybe_future
|
||||
from .mocking import MockHub
|
||||
|
||||
|
||||
@mark.role
|
||||
def test_roles(db):
|
||||
def test_orm_roles(db):
|
||||
"""Test orm roles setup"""
|
||||
user = orm.User(name='falafel')
|
||||
db.add(user)
|
||||
service = orm.Service(name='kebab')
|
||||
db.add(service)
|
||||
role = orm.Role(name='default')
|
||||
db.add(role)
|
||||
db.commit()
|
||||
assert role.users == []
|
||||
assert role.services == []
|
||||
assert user.roles == []
|
||||
assert service.roles == []
|
||||
|
||||
role.users.append(user)
|
||||
role.services.append(service)
|
||||
db.commit()
|
||||
assert role.users == [user]
|
||||
assert user.roles == [role]
|
||||
assert role.services == [service]
|
||||
assert service.roles == [role]
|
||||
|
||||
db.delete(user)
|
||||
db.commit()
|
||||
assert role.users == []
|
||||
db.delete(role)
|
||||
db.commit()
|
||||
assert service.roles == []
|
||||
db.delete(service)
|
||||
db.commit()
|
||||
|
||||
|
||||
@mark.role
|
||||
def test_role_delete_cascade(db):
|
||||
def test_orm_role_delete_cascade(db):
|
||||
"""Orm roles cascade"""
|
||||
user1 = orm.User(name='user1')
|
||||
user2 = orm.User(name='user2')
|
||||
@@ -93,8 +105,25 @@ def test_role_delete_cascade(db):
|
||||
|
||||
|
||||
@mark.role
|
||||
async def test_load_roles(tmpdir, request):
|
||||
"""Test loading default and predefined roles in app.py"""
|
||||
async def test_load_default_roles(tmpdir, request):
|
||||
"""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 = [
|
||||
{
|
||||
'name': 'teacher',
|
||||
@@ -106,7 +135,7 @@ async def test_load_roles(tmpdir, request):
|
||||
'name': 'user',
|
||||
'description': 'Only read access',
|
||||
'scopes': ['read:all'],
|
||||
'users': ['test_user'],
|
||||
'users': ['bilbo'],
|
||||
},
|
||||
]
|
||||
kwargs = {'load_roles': roles_to_load}
|
||||
@@ -116,20 +145,28 @@ async def test_load_roles(tmpdir, request):
|
||||
hub = MockHub(**kwargs)
|
||||
hub.init_db()
|
||||
db = hub.db
|
||||
hub.authenticator.admin_users = ['admin']
|
||||
hub.authenticator.allowed_users = ['cyclops', 'gandalf', 'bilbo', 'gargamel']
|
||||
await hub.init_users()
|
||||
for user in db.query(orm.User):
|
||||
print(user.name)
|
||||
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')
|
||||
admin_role = orm.Role.find(db, 'admin')
|
||||
assert user_role is not None
|
||||
assert user_role.scopes == ['read:all']
|
||||
# test other 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
|
||||
# test if every existing user has a role (and no duplicates)
|
||||
|
||||
# test if every user has a role (and no duplicates)
|
||||
# and admins have admin role
|
||||
for user in db.query(orm.User):
|
||||
assert len(user.roles) > 0
|
||||
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
|
||||
teacher_role = orm.Role.find(db, name='teacher')
|
||||
assert teacher_role is not None
|
||||
@@ -137,3 +174,53 @@ async def test_load_roles(tmpdir, request):
|
||||
assert teacher_role in gandalf_user.roles
|
||||
cyclops_user = orm.User.find(db, name='cyclops')
|
||||
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
|
||||
|
@@ -295,7 +295,7 @@ async def test_hubauth_service_token(app, mockservice_url):
|
||||
)
|
||||
r.raise_for_status()
|
||||
reply = r.json()
|
||||
assert reply == {'kind': 'service', 'name': name, 'admin': False}
|
||||
assert reply == {'kind': 'service', 'name': name, 'admin': False, 'roles': []}
|
||||
assert not r.cookies
|
||||
|
||||
# token in ?token parameter
|
||||
@@ -304,7 +304,7 @@ async def test_hubauth_service_token(app, mockservice_url):
|
||||
)
|
||||
r.raise_for_status()
|
||||
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(
|
||||
public_url(app, mockservice_url) + '/whoami/?token=no-such-token',
|
||||
|
Reference in New Issue
Block a user