mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-10 03:23:04 +00:00
creating roles module
This commit is contained in:
@@ -24,51 +24,6 @@ jobs:
|
|||||||
command: |
|
command: |
|
||||||
docker run --rm -it -v $PWD/dockerfiles:/io jupyterhub/jupyterhub python3 /io/test.py
|
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
|
# Tell CircleCI to use this workflow when it builds the site
|
||||||
workflows:
|
workflows:
|
||||||
@@ -76,4 +31,3 @@ workflows:
|
|||||||
default:
|
default:
|
||||||
jobs:
|
jobs:
|
||||||
- build
|
- build
|
||||||
- docs
|
|
||||||
|
@@ -16,34 +16,34 @@ securityDefinitions:
|
|||||||
oauth2:
|
oauth2:
|
||||||
type: oauth2
|
type: oauth2
|
||||||
flow: accessCode
|
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?
|
authorizationUrl: '/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'
|
tokenUrl: '/hub/api/oauth2/token'
|
||||||
scopes:
|
scopes:
|
||||||
all: Everything a user can do
|
all: Everything a user can do
|
||||||
read:all: Read-only access to everything a user can read (also whoami handler)
|
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
|
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: 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:names: Read-only access to users' names
|
||||||
read:users:groups: Read-only access to users' groups
|
read:users:groups: Read-only access to users' groups
|
||||||
read:users:activity: Read-only access to users' activity
|
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
|
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: 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)
|
users:tokens: Grants access to users' token (includes create/revoke a token)
|
||||||
read:users:tokens: Identify a user from a token
|
read:users:tokens: Identify a user from a token
|
||||||
admin:users: Grants access to creating/removing users
|
admin:users: Grants access to creating/removing users
|
||||||
admin:users:servers: Grants access to create/remove users' servers
|
admin:users:servers: Grants access to create/remove users' servers
|
||||||
groups: Add/remove users from any group
|
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
|
read:groups: Read-only access to groups
|
||||||
admin:groups: Grants access to create/delete groups
|
admin:groups: Grants access to create/delete groups
|
||||||
read:services: Read-only access to services
|
read:services: Read-only access to services
|
||||||
proxy: Grants access to proxy's routing table, syncing and notifying about a new proxy
|
proxy: Grants access to proxy's routing table, syncing and notifying about a new proxy
|
||||||
shutdown: Grants access to shutdown the Hub
|
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: []
|
- token: []
|
||||||
- oauth2:
|
- oauth2:
|
||||||
- all
|
- all
|
||||||
@@ -157,7 +157,7 @@ paths:
|
|||||||
- oauth2:
|
- oauth2:
|
||||||
- users
|
- users
|
||||||
- read:users
|
- read:users
|
||||||
- read:users:username
|
- read:users!user=username
|
||||||
parameters:
|
parameters:
|
||||||
- name: name
|
- name: name
|
||||||
description: username
|
description: username
|
||||||
@@ -240,7 +240,7 @@ paths:
|
|||||||
security:
|
security:
|
||||||
- oauth2:
|
- oauth2:
|
||||||
- users
|
- users
|
||||||
- users:activity:username
|
- users:activity!user=username
|
||||||
parameters:
|
parameters:
|
||||||
- name: name
|
- name: name
|
||||||
description: username
|
description: username
|
||||||
@@ -345,7 +345,7 @@ paths:
|
|||||||
- oauth2:
|
- oauth2:
|
||||||
- users
|
- users
|
||||||
- users:servers
|
- users:servers
|
||||||
- users:servers:servername
|
- users:servers!server=servername
|
||||||
parameters:
|
parameters:
|
||||||
- name: name
|
- name: name
|
||||||
description: username
|
description: username
|
||||||
@@ -381,7 +381,7 @@ paths:
|
|||||||
- oauth2:
|
- oauth2:
|
||||||
- users
|
- users
|
||||||
- users:servers
|
- users:servers
|
||||||
- users:servers:servername
|
- users:servers!server=servername
|
||||||
parameters:
|
parameters:
|
||||||
- name: name
|
- name: name
|
||||||
description: username
|
description: username
|
||||||
@@ -519,7 +519,7 @@ paths:
|
|||||||
security:
|
security:
|
||||||
- oauth2:
|
- oauth2:
|
||||||
- groups
|
- groups
|
||||||
- groups:groupname
|
- groups!group=groupname
|
||||||
- read:groups
|
- read:groups
|
||||||
parameters:
|
parameters:
|
||||||
- name: name
|
- name: name
|
||||||
@@ -568,7 +568,7 @@ paths:
|
|||||||
security:
|
security:
|
||||||
- oauth2:
|
- oauth2:
|
||||||
- groups
|
- groups
|
||||||
- groups:groupname
|
- groups!group=groupname
|
||||||
parameters:
|
parameters:
|
||||||
- name: name
|
- name: name
|
||||||
description: group name
|
description: group name
|
||||||
@@ -597,7 +597,7 @@ paths:
|
|||||||
security:
|
security:
|
||||||
- oauth2:
|
- oauth2:
|
||||||
- groups
|
- groups
|
||||||
- groups:groupname
|
- groups!group=groupname
|
||||||
parameters:
|
parameters:
|
||||||
- name: name
|
- name: name
|
||||||
description: group name
|
description: group name
|
||||||
@@ -768,7 +768,7 @@ paths:
|
|||||||
$ref: '#/definitions/User'
|
$ref: '#/definitions/User'
|
||||||
'404':
|
'404':
|
||||||
description: A user is not found.
|
description: A user is not found.
|
||||||
# deprecated: true # minrk: let’s not add a scope for this, let’s remove it
|
deprecated: true # minrk: let’s not add a scope for this, let’s remove it
|
||||||
/oauth2/authorize:
|
/oauth2/authorize:
|
||||||
get:
|
get:
|
||||||
summary: 'OAuth 2.0 authorize endpoint'
|
summary: 'OAuth 2.0 authorize endpoint'
|
||||||
@@ -886,6 +886,11 @@ definitions:
|
|||||||
admin:
|
admin:
|
||||||
type: boolean
|
type: boolean
|
||||||
description: Whether the user is an admin
|
description: Whether the user is an admin
|
||||||
|
roles:
|
||||||
|
type: array
|
||||||
|
description: The names of roles this user has
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
groups:
|
groups:
|
||||||
type: array
|
type: array
|
||||||
description: The names of groups where this user is a member
|
description: The names of groups where this user is a member
|
||||||
|
File diff suppressed because one or more lines are too long
@@ -6,8 +6,8 @@ version_info = (
|
|||||||
1,
|
1,
|
||||||
2,
|
2,
|
||||||
0,
|
0,
|
||||||
# "", # release (b1, rc1, or "" for final or dev)
|
"b1", # release (b1, rc1, or "" for final or dev)
|
||||||
"dev", # dev or nothing for beta/rc/stable releases
|
# "dev", # dev or nothing for beta/rc/stable releases
|
||||||
)
|
)
|
||||||
|
|
||||||
# pep 440 version: no dot before beta/rc, but before .dev
|
# pep 440 version: no dot before beta/rc, but before .dev
|
||||||
|
@@ -190,6 +190,7 @@ class APIHandler(BaseHandler):
|
|||||||
'kind': 'user',
|
'kind': 'user',
|
||||||
'name': user.name,
|
'name': user.name,
|
||||||
'admin': user.admin,
|
'admin': user.admin,
|
||||||
|
'roles': [r.name for r in user.roles],
|
||||||
'groups': [g.name for g in user.groups],
|
'groups': [g.name for g in user.groups],
|
||||||
'server': user.url if user.running else None,
|
'server': user.url if user.running else None,
|
||||||
'pending': None,
|
'pending': None,
|
||||||
|
@@ -13,6 +13,7 @@ from tornado import web
|
|||||||
from tornado.iostream import StreamClosedError
|
from tornado.iostream import StreamClosedError
|
||||||
|
|
||||||
from .. import orm
|
from .. import orm
|
||||||
|
from .. import roles
|
||||||
from ..user import User
|
from ..user import User
|
||||||
from ..utils import admin_only
|
from ..utils import admin_only
|
||||||
from ..utils import isoformat
|
from ..utils import isoformat
|
||||||
@@ -87,6 +88,7 @@ class UserListAPIHandler(APIHandler):
|
|||||||
user = self.user_from_username(name)
|
user = self.user_from_username(name)
|
||||||
if admin:
|
if admin:
|
||||||
user.admin = True
|
user.admin = True
|
||||||
|
roles.DefaultRoles.add_default_role(self.db, user)
|
||||||
self.db.commit()
|
self.db.commit()
|
||||||
try:
|
try:
|
||||||
await maybe_future(self.authenticator.add_user(user))
|
await maybe_future(self.authenticator.add_user(user))
|
||||||
@@ -149,6 +151,7 @@ class UserAPIHandler(APIHandler):
|
|||||||
self._check_user_model(data)
|
self._check_user_model(data)
|
||||||
if 'admin' in data:
|
if 'admin' in data:
|
||||||
user.admin = data['admin']
|
user.admin = data['admin']
|
||||||
|
roles.DefaultRoles.add_default_role(self.db, user)
|
||||||
self.db.commit()
|
self.db.commit()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -205,6 +208,8 @@ class UserAPIHandler(APIHandler):
|
|||||||
if key == 'auth_state':
|
if key == 'auth_state':
|
||||||
await user.save_auth_state(value)
|
await user.save_auth_state(value)
|
||||||
else:
|
else:
|
||||||
|
if key == 'admin' and value != user.admin:
|
||||||
|
roles.DefaultRoles.change_admin(self.db, user=user, admin=value)
|
||||||
setattr(user, key, value)
|
setattr(user, key, value)
|
||||||
self.db.commit()
|
self.db.commit()
|
||||||
user_ = self.user_model(user)
|
user_ = self.user_model(user)
|
||||||
|
@@ -73,6 +73,7 @@ from .services.service import Service
|
|||||||
|
|
||||||
from . import crypto
|
from . import crypto
|
||||||
from . import dbutil, orm
|
from . import dbutil, orm
|
||||||
|
from . import roles
|
||||||
from .user import UserDict
|
from .user import UserDict
|
||||||
from .oauth.provider import make_provider
|
from .oauth.provider import make_provider
|
||||||
from ._data import DATA_FILES_PATH
|
from ._data import DATA_FILES_PATH
|
||||||
@@ -311,6 +312,28 @@ class JupyterHub(Application):
|
|||||||
""",
|
""",
|
||||||
).tag(config=True)
|
).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_file = Unicode('jupyterhub_config.py', help="The config file to load").tag(
|
||||||
config=True
|
config=True
|
||||||
)
|
)
|
||||||
@@ -1692,6 +1715,7 @@ class JupyterHub(Application):
|
|||||||
|
|
||||||
for name in admin_users:
|
for name in admin_users:
|
||||||
# ensure anyone specified as admin in config is admin in db
|
# ensure anyone specified as admin in config is admin in db
|
||||||
|
# and gets admin role
|
||||||
user = orm.User.find(db, name)
|
user = orm.User.find(db, name)
|
||||||
if user is None:
|
if user is None:
|
||||||
user = orm.User(name=name, admin=True)
|
user = orm.User(name=name, admin=True)
|
||||||
@@ -1699,7 +1723,6 @@ class JupyterHub(Application):
|
|||||||
db.add(user)
|
db.add(user)
|
||||||
else:
|
else:
|
||||||
user.admin = True
|
user.admin = True
|
||||||
|
|
||||||
# the admin_users config variable will never be used after this point.
|
# the admin_users config variable will never be used after this point.
|
||||||
# only the database values will be referenced.
|
# only the database values will be referenced.
|
||||||
|
|
||||||
@@ -1798,6 +1821,37 @@ class JupyterHub(Application):
|
|||||||
group.users.append(user)
|
group.users.append(user)
|
||||||
db.commit()
|
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):
|
async def _add_tokens(self, token_dict, kind):
|
||||||
"""Add tokens for users or services to the database"""
|
"""Add tokens for users or services to the database"""
|
||||||
if kind == 'user':
|
if kind == 'user':
|
||||||
@@ -2376,6 +2430,7 @@ 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()
|
||||||
self.init_tornado_settings()
|
self.init_tornado_settings()
|
||||||
|
@@ -139,7 +139,7 @@ def upgrade_if_needed(db_url, backup=True, log=None):
|
|||||||
|
|
||||||
|
|
||||||
def shell(args=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
|
from .app import JupyterHub
|
||||||
|
|
||||||
hub = JupyterHub()
|
hub = JupyterHub()
|
||||||
|
@@ -90,6 +90,26 @@ class JSONDict(TypeDecorator):
|
|||||||
return value
|
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 = declarative_base()
|
||||||
Base.log = app_log
|
Base.log = app_log
|
||||||
|
|
||||||
@@ -113,6 +133,41 @@ class Server(Base):
|
|||||||
return "<Server(%s:%s)>" % (self.ip, self.port)
|
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 many:many mapping table
|
||||||
user_group_map = Table(
|
user_group_map = Table(
|
||||||
'user_group_map',
|
'user_group_map',
|
||||||
|
127
jupyterhub/roles.py
Normal file
127
jupyterhub/roles.py
Normal 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
|
@@ -44,6 +44,7 @@ from traitlets import default
|
|||||||
from traitlets import Dict
|
from traitlets import Dict
|
||||||
|
|
||||||
from .. import orm
|
from .. import orm
|
||||||
|
from .. import roles
|
||||||
from ..app import JupyterHub
|
from ..app import JupyterHub
|
||||||
from ..auth import PAMAuthenticator
|
from ..auth import PAMAuthenticator
|
||||||
from ..objects import Server
|
from ..objects import Server
|
||||||
@@ -318,6 +319,8 @@ class MockHub(JupyterHub):
|
|||||||
self.db.delete(user)
|
self.db.delete(user)
|
||||||
for group in self.db.query(orm.Group):
|
for group in self.db.query(orm.Group):
|
||||||
self.db.delete(group)
|
self.db.delete(group)
|
||||||
|
for role in self.db.query(orm.Role):
|
||||||
|
self.db.delete(role)
|
||||||
self.db.commit()
|
self.db.commit()
|
||||||
|
|
||||||
@gen.coroutine
|
@gen.coroutine
|
||||||
@@ -333,6 +336,7 @@ class MockHub(JupyterHub):
|
|||||||
user = self.db.query(orm.User).filter(orm.User.name == 'user').first()
|
user = self.db.query(orm.User).filter(orm.User.name == 'user').first()
|
||||||
if user is None:
|
if user is None:
|
||||||
user = orm.User(name='user')
|
user = orm.User(name='user')
|
||||||
|
roles.DefaultRoles.add_default_role(self.db, user=user)
|
||||||
self.db.add(user)
|
self.db.add(user)
|
||||||
self.db.commit()
|
self.db.commit()
|
||||||
|
|
||||||
@@ -403,7 +407,7 @@ class StubSingleUserSpawner(MockSpawner):
|
|||||||
- authenticated, so we are testing auth
|
- authenticated, so we are testing auth
|
||||||
- always available (i.e. in base ServerApp and NotebookApp
|
- always available (i.e. in base ServerApp and NotebookApp
|
||||||
"""
|
"""
|
||||||
return "/api/spec.yaml"
|
return "/api/status"
|
||||||
|
|
||||||
_thread = None
|
_thread = None
|
||||||
|
|
||||||
|
@@ -10,6 +10,9 @@ 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
|
||||||
|
# from jupyterhub.orm import Role
|
||||||
|
|
||||||
|
|
||||||
def populate_db(url):
|
def populate_db(url):
|
||||||
"""Populate a jupyterhub database"""
|
"""Populate a jupyterhub database"""
|
||||||
|
@@ -17,6 +17,7 @@ from tornado import gen
|
|||||||
|
|
||||||
import jupyterhub
|
import jupyterhub
|
||||||
from .. import orm
|
from .. import orm
|
||||||
|
from .. import roles
|
||||||
from ..utils import url_path_join as ujoin
|
from ..utils import url_path_join as ujoin
|
||||||
from ..utils import utcnow
|
from ..utils import utcnow
|
||||||
from .mocking import public_host
|
from .mocking import public_host
|
||||||
@@ -66,9 +67,10 @@ async def test_auth_api(app):
|
|||||||
async def test_referer_check(app):
|
async def test_referer_check(app):
|
||||||
url = ujoin(public_host(app), app.hub.base_url)
|
url = ujoin(public_host(app), app.hub.base_url)
|
||||||
host = urlparse(url).netloc
|
host = urlparse(url).netloc
|
||||||
|
# add admin user
|
||||||
user = find_user(app.db, 'admin')
|
user = find_user(app.db, 'admin')
|
||||||
if user is None:
|
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')
|
cookies = await app.login_user('admin')
|
||||||
|
|
||||||
r = await api_request(
|
r = await api_request(
|
||||||
@@ -152,6 +154,7 @@ def fill_user(model):
|
|||||||
"""
|
"""
|
||||||
model.setdefault('server', None)
|
model.setdefault('server', None)
|
||||||
model.setdefault('kind', 'user')
|
model.setdefault('kind', 'user')
|
||||||
|
model.setdefault('roles', [])
|
||||||
model.setdefault('groups', [])
|
model.setdefault('groups', [])
|
||||||
model.setdefault('admin', False)
|
model.setdefault('admin', False)
|
||||||
model.setdefault('server', None)
|
model.setdefault('server', None)
|
||||||
@@ -166,6 +169,7 @@ TIMESTAMP = normalize_timestamp(datetime.now().isoformat() + 'Z')
|
|||||||
|
|
||||||
|
|
||||||
@mark.user
|
@mark.user
|
||||||
|
@mark.role
|
||||||
async def test_get_users(app):
|
async def test_get_users(app):
|
||||||
db = app.db
|
db = app.db
|
||||||
r = await api_request(app, 'users')
|
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 = sorted(r.json(), key=lambda d: d['name'])
|
||||||
users = [normalize_user(u) for u in users]
|
users = [normalize_user(u) for u in users]
|
||||||
assert users == [
|
assert users == [
|
||||||
fill_user({'name': 'admin', 'admin': True}),
|
fill_user({'name': 'admin', 'admin': True, 'roles': ['admin']}),
|
||||||
fill_user({'name': 'user', 'admin': False, 'last_activity': None}),
|
fill_user(
|
||||||
|
{'name': 'user', 'admin': False, 'roles': ['user'], 'last_activity': None}
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
r = await api_request(app, 'users', headers=auth_header(db, 'user'))
|
r = await api_request(app, 'users', headers=auth_header(db, 'user'))
|
||||||
@@ -216,6 +222,7 @@ async def test_get_self(app):
|
|||||||
|
|
||||||
|
|
||||||
@mark.user
|
@mark.user
|
||||||
|
@mark.role
|
||||||
async def test_add_user(app):
|
async def test_add_user(app):
|
||||||
db = app.db
|
db = app.db
|
||||||
name = 'newuser'
|
name = 'newuser'
|
||||||
@@ -225,16 +232,20 @@ async def test_add_user(app):
|
|||||||
assert user is not None
|
assert user is not None
|
||||||
assert user.name == name
|
assert user.name == name
|
||||||
assert not user.admin
|
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.user
|
||||||
|
@mark.role
|
||||||
async def test_get_user(app):
|
async def test_get_user(app):
|
||||||
name = 'user'
|
name = 'user'
|
||||||
r = await api_request(app, 'users', name)
|
r = await api_request(app, 'users', name)
|
||||||
assert r.status_code == 200
|
assert r.status_code == 200
|
||||||
|
|
||||||
user = normalize_user(r.json())
|
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
|
@mark.user
|
||||||
@@ -262,6 +273,7 @@ async def test_add_multi_user_invalid(app):
|
|||||||
|
|
||||||
|
|
||||||
@mark.user
|
@mark.user
|
||||||
|
@mark.role
|
||||||
async def test_add_multi_user(app):
|
async def test_add_multi_user(app):
|
||||||
db = app.db
|
db = app.db
|
||||||
names = ['a', 'b']
|
names = ['a', 'b']
|
||||||
@@ -278,6 +290,9 @@ async def test_add_multi_user(app):
|
|||||||
assert user is not None
|
assert user is not None
|
||||||
assert user.name == name
|
assert user.name == name
|
||||||
assert not user.admin
|
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
|
# try to create the same users again
|
||||||
r = await api_request(
|
r = await api_request(
|
||||||
@@ -298,6 +313,7 @@ async def test_add_multi_user(app):
|
|||||||
|
|
||||||
|
|
||||||
@mark.user
|
@mark.user
|
||||||
|
@mark.role
|
||||||
async def test_add_multi_user_admin(app):
|
async def test_add_multi_user_admin(app):
|
||||||
db = app.db
|
db = app.db
|
||||||
names = ['c', 'd']
|
names = ['c', 'd']
|
||||||
@@ -317,6 +333,8 @@ async def test_add_multi_user_admin(app):
|
|||||||
assert user is not None
|
assert user is not None
|
||||||
assert user.name == name
|
assert user.name == name
|
||||||
assert user.admin
|
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
|
@mark.user
|
||||||
@@ -342,6 +360,7 @@ async def test_add_user_duplicate(app):
|
|||||||
|
|
||||||
|
|
||||||
@mark.user
|
@mark.user
|
||||||
|
@mark.role
|
||||||
async def test_add_admin(app):
|
async def test_add_admin(app):
|
||||||
db = app.db
|
db = app.db
|
||||||
name = 'newadmin'
|
name = 'newadmin'
|
||||||
@@ -350,9 +369,13 @@ async def test_add_admin(app):
|
|||||||
)
|
)
|
||||||
assert r.status_code == 201
|
assert r.status_code == 201
|
||||||
user = find_user(db, name)
|
user = find_user(db, name)
|
||||||
|
user_role = orm.Role.find(db, 'user')
|
||||||
assert user is not None
|
assert user is not None
|
||||||
assert user.name == name
|
assert user.name == name
|
||||||
assert user.admin
|
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
|
@mark.user
|
||||||
@@ -364,6 +387,7 @@ async def test_delete_user(app):
|
|||||||
|
|
||||||
|
|
||||||
@mark.user
|
@mark.user
|
||||||
|
@mark.role
|
||||||
async def test_make_admin(app):
|
async def test_make_admin(app):
|
||||||
db = app.db
|
db = app.db
|
||||||
name = 'admin2'
|
name = 'admin2'
|
||||||
@@ -373,15 +397,20 @@ async def test_make_admin(app):
|
|||||||
assert user is not None
|
assert user is not None
|
||||||
assert user.name == name
|
assert user.name == name
|
||||||
assert not user.admin
|
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(
|
r = await api_request(
|
||||||
app, 'users', name, method='patch', data=json.dumps({'admin': True})
|
app, 'users', name, method='patch', data=json.dumps({'admin': True})
|
||||||
)
|
)
|
||||||
|
|
||||||
assert r.status_code == 200
|
assert r.status_code == 200
|
||||||
user = find_user(db, name)
|
user = find_user(db, name)
|
||||||
assert user is not None
|
assert user is not None
|
||||||
assert user.name == name
|
assert user.name == name
|
||||||
assert user.admin
|
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
|
@mark.user
|
||||||
|
@@ -245,10 +245,12 @@ def test_groups(db):
|
|||||||
db.commit()
|
db.commit()
|
||||||
assert group.users == []
|
assert group.users == []
|
||||||
assert user.groups == []
|
assert user.groups == []
|
||||||
|
|
||||||
group.users.append(user)
|
group.users.append(user)
|
||||||
db.commit()
|
db.commit()
|
||||||
assert group.users == [user]
|
assert group.users == [user]
|
||||||
assert user.groups == [group]
|
assert user.groups == [group]
|
||||||
|
|
||||||
db.delete(user)
|
db.delete(user)
|
||||||
db.commit()
|
db.commit()
|
||||||
assert group.users == []
|
assert group.users == []
|
||||||
@@ -460,7 +462,7 @@ def test_group_delete_cascade(db):
|
|||||||
assert group2 in user2.groups
|
assert group2 in user2.groups
|
||||||
|
|
||||||
# now start deleting
|
# now start deleting
|
||||||
# 1. remove group via user.groups
|
# 1. remove group via user.group
|
||||||
user1.groups.remove(group2)
|
user1.groups.remove(group2)
|
||||||
db.commit()
|
db.commit()
|
||||||
assert user1 not in group2.users
|
assert user1 not in group2.users
|
||||||
@@ -480,6 +482,7 @@ def test_group_delete_cascade(db):
|
|||||||
|
|
||||||
# 4. delete user object
|
# 4. delete user object
|
||||||
db.delete(user1)
|
db.delete(user1)
|
||||||
|
db.delete(user2)
|
||||||
db.commit()
|
db.commit()
|
||||||
assert user1 not in group1.users
|
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)
|
assert orm_code in db.query(orm.OAuthCode)
|
||||||
orm.OAuthCode.purge_expired(db)
|
orm.OAuthCode.purge_expired(db)
|
||||||
assert orm_code not in db.query(orm.OAuthCode)
|
assert orm_code not in db.query(orm.OAuthCode)
|
||||||
|
|
||||||
|
135
jupyterhub/tests/test_roles.py
Normal file
135
jupyterhub/tests/test_roles.py
Normal 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
|
@@ -9,6 +9,7 @@ from traitlets import List
|
|||||||
from traitlets import TraitError
|
from traitlets import TraitError
|
||||||
from traitlets import TraitType
|
from traitlets import TraitType
|
||||||
from traitlets import Type
|
from traitlets import Type
|
||||||
|
from traitlets import Undefined
|
||||||
from traitlets import Unicode
|
from traitlets import Unicode
|
||||||
|
|
||||||
|
|
||||||
@@ -27,11 +28,15 @@ class Command(List):
|
|||||||
but allows it to be specified as a single string.
|
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)
|
kwargs.setdefault('minlen', 1)
|
||||||
if isinstance(default_value, str):
|
if isinstance(default_value, str):
|
||||||
default_value = [default_value]
|
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):
|
def validate(self, obj, value):
|
||||||
if isinstance(value, str):
|
if isinstance(value, str):
|
||||||
|
@@ -173,6 +173,7 @@ async def exponential_backoff(
|
|||||||
# this prevents overloading any single tornado loop iteration with
|
# this prevents overloading any single tornado loop iteration with
|
||||||
# too many things
|
# too many things
|
||||||
dt = min(max_wait, remaining, random.uniform(0, start_wait * scale))
|
dt = min(max_wait, remaining, random.uniform(0, start_wait * scale))
|
||||||
|
if dt < max_wait:
|
||||||
scale *= scale_factor
|
scale *= scale_factor
|
||||||
await gen.sleep(dt)
|
await gen.sleep(dt)
|
||||||
raise TimeoutError(fail_message)
|
raise TimeoutError(fail_message)
|
||||||
|
@@ -13,3 +13,4 @@ markers =
|
|||||||
services: mark as a services test
|
services: mark as a services test
|
||||||
user: mark as a test for a user
|
user: mark as a test for a user
|
||||||
slow: mark a test as slow
|
slow: mark a test as slow
|
||||||
|
role: mark as a test for roles
|
||||||
|
Reference in New Issue
Block a user