creating roles module

This commit is contained in:
IvanaH8
2020-09-24 14:05:15 +02:00
parent ff38a9e383
commit f1ed74bae1
18 changed files with 607 additions and 78 deletions

View File

@@ -24,51 +24,6 @@ jobs:
command: |
docker run --rm -it -v $PWD/dockerfiles:/io jupyterhub/jupyterhub python3 /io/test.py
docs:
# This is the base environment that Circle will use
docker:
- image: circleci/python:3.6-stretch
steps:
# Get our data and merge with upstream
- run: sudo apt-get update
- checkout
# Update our path
- run: echo "export PATH=~/.local/bin:$PATH" >> $BASH_ENV
# Restore cached files to speed things up
- restore_cache:
keys:
- cache-pip
# Install the packages needed to build our documentation
- run:
name: Install NodeJS
command: |
# From https://github.com/nodesource/distributions/blob/master/README.md#debinstall
curl -sL https://deb.nodesource.com/setup_13.x | sudo -E bash -
sudo apt-get install -y nodejs
- run:
name: Install dependencies
command: |
python3 -m pip install --user -r dev-requirements.txt
python3 -m pip install --user -r docs/requirements.txt
sudo npm install -g configurable-http-proxy
sudo python3 -m pip install --editable .
# Cache some files for a speedup in subsequent builds
- save_cache:
key: cache-pip
paths:
- ~/.cache/pip
# Build the docs
- run:
name: Build docs to store
command: |
cd docs
make html
# Tell Circle to store the documentation output in a folder that we can access later
- store_artifacts:
path: docs/build/html/
destination: html
# Tell CircleCI to use this workflow when it builds the site
workflows:
@@ -76,4 +31,3 @@ workflows:
default:
jobs:
- build
- docs

View File

@@ -16,34 +16,34 @@ securityDefinitions:
oauth2:
type: oauth2
flow: accessCode
authorizationUrl: 'https://localhost:8000/hub/api/oauth2/authorize' # what are the absolute URIs here? is oauth2 correct here or shall we use just authorizations?
tokenUrl: 'https://localhost:8000/hub/api/oauth2/token'
authorizationUrl: '/hub/api/oauth2/authorize' # what are the absolute URIs here? is oauth2 correct here or shall we use just authorizations?
tokenUrl: '/hub/api/oauth2/token'
scopes:
all: Everything a user can do
read:all: Read-only access to everything a user can read (also whoami handler)
users: Grants access to managing users including reading users model, posting activity and starting/stoping users servers
read:users: Read-only access to the above
read:users:username: Read-only access to a single user's model
read:users!user=username: Read-only access to a single user's model
read:users:names: Read-only access to users' names
read:users:groups: Read-only access to users' groups
read:users:activity: Read-only access to users' activity
read:users:activity:groupname: Read-only access to specific group's users' activity
read:users:activity!group=groupname: Read-only access to specific group's users' activity
read:users:servers: Read-only access to users' servers
users:activity:username: Update a user's activity
users:activity!user=username: Update a user's activity
users:servers: Grants access to start/stop any server
users:servers:servername: Limits the above to a specific server
users:servers!server=servername: Limits the above to a specific server
users:tokens: Grants access to users' token (includes create/revoke a token)
read:users:tokens: Identify a user from a token
admin:users: Grants access to creating/removing users
admin:users:servers: Grants access to create/remove users' servers
groups: Add/remove users from any group
groups:groupname: Add/remove users from a specific group only
groups!group=groupname: Add/remove users from a specific group only
read:groups: Read-only access to groups
admin:groups: Grants access to create/delete groups
read:services: Read-only access to services
proxy: Grants access to proxy's routing table, syncing and notifying about a new proxy
shutdown: Grants access to shutdown the Hub
security: # global security, do we want to keep the only the apiKey (token: []), change to only oauth2 (with scope all) or have both as changed here (either can be used)?
security: # global security, do we want to keep only the apiKey (token: []), change to only oauth2 (with scope all) or have both (either can be used)?
- token: []
- oauth2:
- all
@@ -157,7 +157,7 @@ paths:
- oauth2:
- users
- read:users
- read:users:username
- read:users!user=username
parameters:
- name: name
description: username
@@ -240,7 +240,7 @@ paths:
security:
- oauth2:
- users
- users:activity:username
- users:activity!user=username
parameters:
- name: name
description: username
@@ -345,7 +345,7 @@ paths:
- oauth2:
- users
- users:servers
- users:servers:servername
- users:servers!server=servername
parameters:
- name: name
description: username
@@ -381,7 +381,7 @@ paths:
- oauth2:
- users
- users:servers
- users:servers:servername
- users:servers!server=servername
parameters:
- name: name
description: username
@@ -519,7 +519,7 @@ paths:
security:
- oauth2:
- groups
- groups:groupname
- groups!group=groupname
- read:groups
parameters:
- name: name
@@ -568,7 +568,7 @@ paths:
security:
- oauth2:
- groups
- groups:groupname
- groups!group=groupname
parameters:
- name: name
description: group name
@@ -597,7 +597,7 @@ paths:
security:
- oauth2:
- groups
- groups:groupname
- groups!group=groupname
parameters:
- name: name
description: group name
@@ -768,7 +768,7 @@ paths:
$ref: '#/definitions/User'
'404':
description: A user is not found.
# deprecated: true # minrk: lets not add a scope for this, lets remove it
deprecated: true # minrk: lets not add a scope for this, lets remove it
/oauth2/authorize:
get:
summary: 'OAuth 2.0 authorize endpoint'
@@ -886,6 +886,11 @@ definitions:
admin:
type: boolean
description: Whether the user is an admin
roles:
type: array
description: The names of roles this user has
items:
type: string
groups:
type: array
description: The names of groups where this user is a member

File diff suppressed because one or more lines are too long

View File

@@ -6,8 +6,8 @@ version_info = (
1,
2,
0,
# "", # release (b1, rc1, or "" for final or dev)
"dev", # dev or nothing for beta/rc/stable releases
"b1", # release (b1, rc1, or "" for final or dev)
# "dev", # dev or nothing for beta/rc/stable releases
)
# pep 440 version: no dot before beta/rc, but before .dev

View File

@@ -190,6 +190,7 @@ class APIHandler(BaseHandler):
'kind': 'user',
'name': user.name,
'admin': user.admin,
'roles': [r.name for r in user.roles],
'groups': [g.name for g in user.groups],
'server': user.url if user.running else None,
'pending': None,

View File

@@ -13,6 +13,7 @@ from tornado import web
from tornado.iostream import StreamClosedError
from .. import orm
from .. import roles
from ..user import User
from ..utils import admin_only
from ..utils import isoformat
@@ -87,7 +88,8 @@ class UserListAPIHandler(APIHandler):
user = self.user_from_username(name)
if admin:
user.admin = True
self.db.commit()
roles.DefaultRoles.add_default_role(self.db, user)
self.db.commit()
try:
await maybe_future(self.authenticator.add_user(user))
except Exception as e:
@@ -149,7 +151,8 @@ class UserAPIHandler(APIHandler):
self._check_user_model(data)
if 'admin' in data:
user.admin = data['admin']
self.db.commit()
roles.DefaultRoles.add_default_role(self.db, user)
self.db.commit()
try:
await maybe_future(self.authenticator.add_user(user))
@@ -205,6 +208,8 @@ class UserAPIHandler(APIHandler):
if key == 'auth_state':
await user.save_auth_state(value)
else:
if key == 'admin' and value != user.admin:
roles.DefaultRoles.change_admin(self.db, user=user, admin=value)
setattr(user, key, value)
self.db.commit()
user_ = self.user_model(user)

View File

@@ -73,6 +73,7 @@ from .services.service import Service
from . import crypto
from . import dbutil, orm
from . import roles
from .user import UserDict
from .oauth.provider import make_provider
from ._data import DATA_FILES_PATH
@@ -311,6 +312,28 @@ class JupyterHub(Application):
""",
).tag(config=True)
load_roles = List(
Dict(),
help="""List of predefined role dictionaries to load at startup.
For instance::
roles = [
{
'name': 'teacher',
'description': 'Access users information, servers and groups without create/delete privileges',
'scopes': ['users', 'groups'],
'users': ['cyclops', 'wolverine']
}
]
See all the available scopes in the JupyterHub REST API documentation.
The default roles are in roles.py.
""",
).tag(config=True)
config_file = Unicode('jupyterhub_config.py', help="The config file to load").tag(
config=True
)
@@ -1692,6 +1715,7 @@ class JupyterHub(Application):
for name in admin_users:
# ensure anyone specified as admin in config is admin in db
# and gets admin role
user = orm.User.find(db, name)
if user is None:
user = orm.User(name=name, admin=True)
@@ -1699,7 +1723,6 @@ class JupyterHub(Application):
db.add(user)
else:
user.admin = True
# the admin_users config variable will never be used after this point.
# only the database values will be referenced.
@@ -1798,6 +1821,37 @@ class JupyterHub(Application):
group.users.append(user)
db.commit()
async def init_roles(self):
"""Load default and predefined roles into the database"""
db = self.db
# load default roles
roles.DefaultRoles.load_to_database(db)
# load predefined roles from config file
for predef_role in self.load_roles:
role = roles.add_predef_role(db, predef_role)
# 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)
# make sure every existing user has a default user or admin role
for user in db.query(orm.User):
roles.DefaultRoles.add_default_role(db, user)
db.commit()
async def _add_tokens(self, token_dict, kind):
"""Add tokens for users or services to the database"""
if kind == 'user':
@@ -2376,6 +2430,7 @@ 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()
self.init_tornado_settings()

View File

@@ -139,7 +139,7 @@ def upgrade_if_needed(db_url, backup=True, log=None):
def shell(args=None):
"""Start an IPython shell hooked up to the jupyerhub database"""
"""Start an IPython shell hooked up to the jupyterhub database"""
from .app import JupyterHub
hub = JupyterHub()

View File

@@ -90,6 +90,26 @@ class JSONDict(TypeDecorator):
return value
class JSONList(JSONDict):
"""Represents an immutable structure as a json-encoded string (to be used for list type columns).
Usage::
JSONList(JSONDict)
"""
def process_bind_param(self, value, dialect):
if isinstance(value, list) and value is not None:
value = json.dumps(value)
return value
def process_result_value(self, value, dialect):
if value is not None:
value = json.loads(value)
return value
Base = declarative_base()
Base.log = app_log
@@ -113,6 +133,41 @@ class Server(Base):
return "<Server(%s:%s)>" % (self.ip, self.port)
# user:role many:many mapping table
user_role_map = Table(
'user_role_map',
Base.metadata,
Column('user_id', ForeignKey('users.id', ondelete='CASCADE'), primary_key=True),
Column('role_id', ForeignKey('roles.id', ondelete='CASCADE'), primary_key=True),
)
class Role(Base):
"""User Roles"""
__tablename__ = 'roles'
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(Unicode(255), unique=True)
description = Column(Unicode(1023))
scopes = Column(JSONList)
users = relationship('User', secondary='user_role_map', backref='roles')
def __repr__(self):
return "<%s %s (%s) - scopes: %s>" % (
self.__class__.__name__,
self.name,
self.description,
self.scopes,
)
@classmethod
def find(cls, db, name):
"""Find a role by name.
Returns None if not found.
"""
return db.query(cls).filter(cls.name == name).first()
# user:group many:many mapping table
user_group_map = Table(
'user_group_map',

127
jupyterhub/roles.py Normal file
View File

@@ -0,0 +1,127 @@
"""Roles utils"""
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
from .orm import Role
# define default roles
class DefaultRoles:
user = Role(name='user', description='Everything the user can do', scopes=['all'])
admin = Role(
name='admin',
description='Admin privileges (currently can do everything)',
scopes=[
'all',
'users',
'users:tokens',
'admin:users',
'admin:users:servers',
'groups',
'admin:groups',
'read:services',
'proxy',
'shutdown',
],
)
server = Role(
name='server',
description='Post activity only',
scopes=['users:activity!user=username'],
)
roles = (user, admin, server)
def __init__(cls, roles=roles):
cls.roles = roles
@classmethod
def get_user_role(cls, db):
return Role.find(db, name=cls.user.name)
@classmethod
def get_admin_role(cls, db):
return Role.find(db, name=cls.admin.name)
@classmethod
def get_server_role(cls, db):
return Role.find(db, name=cls.server.name)
@classmethod
def load_to_database(cls, db):
for role in cls.roles:
db_role = Role.find(db, name=role.name)
if db_role is None:
new_role = Role(
name=role.name, description=role.description, scopes=role.scopes,
)
db.add(new_role)
db.commit()
@classmethod
def add_default_role(cls, db, user):
role = None
if user.admin and cls.admin not in user.roles:
role = cls.get_admin_role(db)
if not user.admin and cls.user not in user.roles:
role = cls.get_user_role(db)
if role is not None:
add_user(db, user, role)
db.commit()
@classmethod
def change_admin(cls, db, user, admin):
user_role = cls.get_user_role(db)
admin_role = cls.get_admin_role(db)
if admin:
if user_role in user.roles:
remove_user(db, user, user_role)
add_user(db, user, admin_role)
else:
if admin_role in user.roles:
remove_user(db, user, admin_role)
add_user(db, user, user_role)
db.commit()
def add_user(db, user, role):
if role is not None and role not in user.roles:
user.roles.append(role)
db.commit()
def remove_user(db, user, role):
if role is not None and role in user.roles:
user.roles.remove(role)
db.commit()
def add_predef_role(db, predef_role):
"""
Returns either the role to write into db or updated role if already in db
"""
role = Role.find(db, predef_role['name'])
# if a new role, add to db, if existing, rewrite its attributes apart from the name
if role is None:
role = Role(
name=predef_role['name'],
description=predef_role['description'],
scopes=predef_role['scopes'],
)
db.add(role)
db.commit()
else:
# check if it's not one of the default roles
if not any(d.name == predef_role['name'] for d in DefaultRoles.roles):
# if description and scopes specified, rewrite the old ones
if 'description' in predef_role.keys():
role.description = predef_role['description']
if 'scopes' in predef_role.keys():
role.scopes = predef_role['scopes']
# FIXME - for now deletes old users and writes new ones
role.users = []
else:
raise ValueError(
"The role %r is a default role that cannot be overwritten, use a different role name"
% predef_role['name']
)
return role

View File

@@ -44,6 +44,7 @@ from traitlets import default
from traitlets import Dict
from .. import orm
from .. import roles
from ..app import JupyterHub
from ..auth import PAMAuthenticator
from ..objects import Server
@@ -318,6 +319,8 @@ class MockHub(JupyterHub):
self.db.delete(user)
for group in self.db.query(orm.Group):
self.db.delete(group)
for role in self.db.query(orm.Role):
self.db.delete(role)
self.db.commit()
@gen.coroutine
@@ -333,6 +336,7 @@ class MockHub(JupyterHub):
user = self.db.query(orm.User).filter(orm.User.name == 'user').first()
if user is None:
user = orm.User(name='user')
roles.DefaultRoles.add_default_role(self.db, user=user)
self.db.add(user)
self.db.commit()
@@ -403,7 +407,7 @@ class StubSingleUserSpawner(MockSpawner):
- authenticated, so we are testing auth
- always available (i.e. in base ServerApp and NotebookApp
"""
return "/api/spec.yaml"
return "/api/status"
_thread = None

View File

@@ -10,6 +10,9 @@ 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
def populate_db(url):
"""Populate a jupyterhub database"""

View File

@@ -17,6 +17,7 @@ from tornado import gen
import jupyterhub
from .. import orm
from .. import roles
from ..utils import url_path_join as ujoin
from ..utils import utcnow
from .mocking import public_host
@@ -66,9 +67,10 @@ async def test_auth_api(app):
async def test_referer_check(app):
url = ujoin(public_host(app), app.hub.base_url)
host = urlparse(url).netloc
# add admin user
user = find_user(app.db, 'admin')
if user is None:
user = add_user(app.db, name='admin', admin=True)
user = add_user(app.db, name='admin', admin=True, roles=['admin'])
cookies = await app.login_user('admin')
r = await api_request(
@@ -152,6 +154,7 @@ def fill_user(model):
"""
model.setdefault('server', None)
model.setdefault('kind', 'user')
model.setdefault('roles', [])
model.setdefault('groups', [])
model.setdefault('admin', False)
model.setdefault('server', None)
@@ -166,6 +169,7 @@ TIMESTAMP = normalize_timestamp(datetime.now().isoformat() + 'Z')
@mark.user
@mark.role
async def test_get_users(app):
db = app.db
r = await api_request(app, 'users')
@@ -174,8 +178,10 @@ async def test_get_users(app):
users = sorted(r.json(), key=lambda d: d['name'])
users = [normalize_user(u) for u in users]
assert users == [
fill_user({'name': 'admin', 'admin': True}),
fill_user({'name': 'user', 'admin': False, 'last_activity': None}),
fill_user({'name': 'admin', 'admin': True, 'roles': ['admin']}),
fill_user(
{'name': 'user', 'admin': False, 'roles': ['user'], 'last_activity': None}
),
]
r = await api_request(app, 'users', headers=auth_header(db, 'user'))
@@ -216,6 +222,7 @@ async def test_get_self(app):
@mark.user
@mark.role
async def test_add_user(app):
db = app.db
name = 'newuser'
@@ -225,16 +232,20 @@ async def test_add_user(app):
assert user is not None
assert user.name == name
assert not user.admin
# assert newuser has default 'user' role
assert roles.DefaultRoles.get_user_role(db=db) in user.roles
assert roles.DefaultRoles.get_admin_role(db=db) not in user.roles
@mark.user
@mark.role
async def test_get_user(app):
name = 'user'
r = await api_request(app, 'users', name)
assert r.status_code == 200
user = normalize_user(r.json())
assert user == fill_user({'name': name, 'auth_state': None})
assert user == fill_user({'name': name, 'roles': ['user'], 'auth_state': None})
@mark.user
@@ -262,6 +273,7 @@ async def test_add_multi_user_invalid(app):
@mark.user
@mark.role
async def test_add_multi_user(app):
db = app.db
names = ['a', 'b']
@@ -278,6 +290,9 @@ async def test_add_multi_user(app):
assert user is not None
assert user.name == name
assert not user.admin
# assert default 'user' role added
assert roles.DefaultRoles.get_user_role(db=db) in user.roles
assert roles.DefaultRoles.get_admin_role(db=db) not in user.roles
# try to create the same users again
r = await api_request(
@@ -298,6 +313,7 @@ async def test_add_multi_user(app):
@mark.user
@mark.role
async def test_add_multi_user_admin(app):
db = app.db
names = ['c', 'd']
@@ -317,6 +333,8 @@ async def test_add_multi_user_admin(app):
assert user is not None
assert user.name == name
assert user.admin
assert roles.DefaultRoles.get_user_role(db=db) not in user.roles
assert roles.DefaultRoles.get_admin_role(db=db) in user.roles
@mark.user
@@ -342,6 +360,7 @@ async def test_add_user_duplicate(app):
@mark.user
@mark.role
async def test_add_admin(app):
db = app.db
name = 'newadmin'
@@ -350,9 +369,13 @@ async def test_add_admin(app):
)
assert r.status_code == 201
user = find_user(db, name)
user_role = orm.Role.find(db, 'user')
assert user is not None
assert user.name == name
assert user.admin
# assert newadmin has default 'admin' role
assert roles.DefaultRoles.get_user_role(db=db) not in user.roles
assert roles.DefaultRoles.get_admin_role(db=db) in user.roles
@mark.user
@@ -364,6 +387,7 @@ async def test_delete_user(app):
@mark.user
@mark.role
async def test_make_admin(app):
db = app.db
name = 'admin2'
@@ -373,15 +397,20 @@ async def test_make_admin(app):
assert user is not None
assert user.name == name
assert not user.admin
assert roles.DefaultRoles.get_user_role(db=db) in user.roles
assert roles.DefaultRoles.get_admin_role(db=db) not in user.roles
r = await api_request(
app, 'users', name, method='patch', data=json.dumps({'admin': True})
)
assert r.status_code == 200
user = find_user(db, name)
assert user is not None
assert user.name == name
assert user.admin
assert roles.DefaultRoles.get_user_role(db=db) not in user.roles
assert roles.DefaultRoles.get_admin_role(db=db) in user.roles
@mark.user

View File

@@ -245,10 +245,12 @@ def test_groups(db):
db.commit()
assert group.users == []
assert user.groups == []
group.users.append(user)
db.commit()
assert group.users == [user]
assert user.groups == [group]
db.delete(user)
db.commit()
assert group.users == []
@@ -460,7 +462,7 @@ def test_group_delete_cascade(db):
assert group2 in user2.groups
# now start deleting
# 1. remove group via user.groups
# 1. remove group via user.group
user1.groups.remove(group2)
db.commit()
assert user1 not in group2.users
@@ -480,6 +482,7 @@ def test_group_delete_cascade(db):
# 4. delete user object
db.delete(user1)
db.delete(user2)
db.commit()
assert user1 not in group1.users
@@ -557,3 +560,4 @@ def test_expiring_oauth_code(app, user):
assert orm_code in db.query(orm.OAuthCode)
orm.OAuthCode.purge_expired(db)
assert orm_code not in db.query(orm.OAuthCode)

View File

@@ -0,0 +1,135 @@
"""Test roles"""
# import pytest
from pytest import mark
from .. import orm
from ..roles import DefaultRoles
from .mocking import MockHub
@mark.role
def test_roles(db):
"""Test orm roles setup"""
user = orm.User(name='falafel')
db.add(user)
role = orm.Role(name='default')
db.add(role)
db.commit()
assert role.users == []
assert user.roles == []
role.users.append(user)
db.commit()
assert role.users == [user]
assert user.roles == [role]
db.delete(user)
db.commit()
assert role.users == []
db.delete(role)
@mark.role
def test_role_delete_cascade(db):
"""Orm roles cascade"""
user1 = orm.User(name='user1')
user2 = orm.User(name='user2')
role1 = orm.Role(name='role1')
role2 = orm.Role(name='role2')
db.add(user1)
db.add(user2)
db.add(role1)
db.add(role2)
db.commit()
# add user to role via user.roles
user1.roles.append(role1)
db.commit()
assert user1 in role1.users
assert role1 in user1.roles
# add user to role via roles.users
role1.users.append(user2)
db.commit()
assert user2 in role1.users
assert role1 in user2.roles
# fill role2 and check role1 again
role2.users.append(user1)
role2.users.append(user2)
db.commit()
assert user1 in role1.users
assert user2 in role1.users
assert user1 in role2.users
assert user2 in role2.users
assert role1 in user1.roles
assert role1 in user2.roles
assert role2 in user1.roles
assert role2 in user2.roles
# now start deleting
# 1. remove role via user.roles
user1.roles.remove(role2)
db.commit()
assert user1 not in role2.users
assert role2 not in user1.roles
# 2. remove user via role.users
role1.users.remove(user2)
db.commit()
assert user2 not in role1.users
assert role1 not in user2.roles
# 3. delete role object
db.delete(role2)
db.commit()
assert role2 not in user1.roles
assert role2 not in user2.roles
# 4. delete user object
db.delete(user1)
db.delete(user2)
db.commit()
assert user1 not in role1.users
@mark.role
async def test_load_roles(tmpdir, request):
"""Test loading default and predefined roles in app.py"""
to_load = [
{
'name': 'teacher',
'description': 'Access users information, servers and groups without create/delete privileges',
'scopes': ['users', 'groups'],
'users': ['cyclops', 'gandalf'],
}
]
kwargs = {'load_roles': to_load}
ssl_enabled = getattr(request.module, "ssl_enabled", False)
if ssl_enabled:
kwargs['internal_certs_location'] = str(tmpdir)
# keep the users and groups from test_load_groups
hub = MockHub(test_clean_db=False, **kwargs)
hub.init_db()
await hub.init_users()
await hub.init_roles()
db = hub.db
# test default roles loaded to database
assert DefaultRoles.get_user_role(db) is not None
assert DefaultRoles.get_admin_role(db) is not None
assert DefaultRoles.get_server_role(db) is not None
# test if every existing user has a correct default role
for user in db.query(orm.User):
assert len(user.roles) == len(set(user.roles))
if user.admin:
assert DefaultRoles.get_admin_role(db) in user.roles
assert DefaultRoles.get_user_role(db) not in user.roles
else:
assert DefaultRoles.get_user_role(db) in user.roles
assert DefaultRoles.get_admin_role(db) 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
gandalf_user = orm.User.find(db, name='gandalf')
assert teacher_role in gandalf_user.roles
cyclops_user = orm.User.find(db, name='cyclops')
assert teacher_role in cyclops_user.roles

View File

@@ -9,6 +9,7 @@ from traitlets import List
from traitlets import TraitError
from traitlets import TraitType
from traitlets import Type
from traitlets import Undefined
from traitlets import Unicode
@@ -27,11 +28,15 @@ class Command(List):
but allows it to be specified as a single string.
"""
def __init__(self, default_value=None, **kwargs):
def __init__(self, default_value=Undefined, **kwargs):
kwargs.setdefault('minlen', 1)
if isinstance(default_value, str):
default_value = [default_value]
super().__init__(Unicode(), default_value, **kwargs)
if default_value is not Undefined and (
not (default_value is None and not kwargs.get("allow_none", False))
):
kwargs["default_value"] = default_value
super().__init__(Unicode(), **kwargs)
def validate(self, obj, value):
if isinstance(value, str):

View File

@@ -173,7 +173,8 @@ async def exponential_backoff(
# this prevents overloading any single tornado loop iteration with
# too many things
dt = min(max_wait, remaining, random.uniform(0, start_wait * scale))
scale *= scale_factor
if dt < max_wait:
scale *= scale_factor
await gen.sleep(dt)
raise TimeoutError(fail_message)

View File

@@ -13,3 +13,4 @@ markers =
services: mark as a services test
user: mark as a test for a user
slow: mark a test as slow
role: mark as a test for roles