mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-13 21:13:01 +00:00
adding roles to services
This commit is contained in:
@@ -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}
|
||||||
|
|
||||||
|
@@ -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,
|
||||||
|
@@ -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()
|
||||||
|
@@ -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>" % (
|
||||||
|
@@ -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()
|
||||||
|
|
||||||
|
|
||||||
|
@@ -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()
|
||||||
|
@@ -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):
|
||||||
|
@@ -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,
|
||||||
|
@@ -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
|
||||||
|
@@ -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',
|
||||||
|
Reference in New Issue
Block a user