mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-11 03:52:59 +00:00
Merge remote-tracking branch 'upstream/rbac' into merge_api_with_orm
This commit is contained in:
@@ -150,10 +150,13 @@ class APIHandler(BaseHandler):
|
|||||||
expires_at = None
|
expires_at = None
|
||||||
if isinstance(token, orm.APIToken):
|
if isinstance(token, orm.APIToken):
|
||||||
kind = 'api_token'
|
kind = 'api_token'
|
||||||
|
roles = [r.name for r in token.roles]
|
||||||
extra = {'note': token.note}
|
extra = {'note': token.note}
|
||||||
expires_at = token.expires_at
|
expires_at = token.expires_at
|
||||||
elif isinstance(token, orm.OAuthAccessToken):
|
elif isinstance(token, orm.OAuthAccessToken):
|
||||||
kind = 'oauth'
|
kind = 'oauth'
|
||||||
|
# oauth tokens do not bear roles
|
||||||
|
roles = []
|
||||||
extra = {'oauth_client': token.client.description or token.client.client_id}
|
extra = {'oauth_client': token.client.description or token.client.client_id}
|
||||||
if token.expires_at:
|
if token.expires_at:
|
||||||
expires_at = datetime.fromtimestamp(token.expires_at)
|
expires_at = datetime.fromtimestamp(token.expires_at)
|
||||||
@@ -174,6 +177,7 @@ class APIHandler(BaseHandler):
|
|||||||
owner_key: owner,
|
owner_key: owner,
|
||||||
'id': token.api_id,
|
'id': token.api_id,
|
||||||
'kind': kind,
|
'kind': kind,
|
||||||
|
'roles': [role for role in roles],
|
||||||
'created': isoformat(token.created),
|
'created': isoformat(token.created),
|
||||||
'last_activity': isoformat(token.last_activity),
|
'last_activity': isoformat(token.last_activity),
|
||||||
'expires_at': isoformat(expires_at),
|
'expires_at': isoformat(expires_at),
|
||||||
@@ -190,6 +194,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,
|
||||||
@@ -221,7 +226,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}
|
||||||
|
|
||||||
|
@@ -19,6 +19,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,
|
||||||
|
@@ -14,6 +14,8 @@ from tornado import web
|
|||||||
from tornado.iostream import StreamClosedError
|
from tornado.iostream import StreamClosedError
|
||||||
|
|
||||||
from .. import orm
|
from .. import orm
|
||||||
|
from .. import roles
|
||||||
|
from ..roles import update_roles
|
||||||
from ..user import User
|
from ..user import User
|
||||||
from ..utils import isoformat
|
from ..utils import isoformat
|
||||||
from ..utils import iterate_until
|
from ..utils import iterate_until
|
||||||
@@ -140,7 +142,8 @@ 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
|
||||||
self.db.commit()
|
update_roles(self.db, obj=user, kind='users')
|
||||||
|
self.db.commit()
|
||||||
try:
|
try:
|
||||||
await maybe_future(self.authenticator.add_user(user))
|
await maybe_future(self.authenticator.add_user(user))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -202,7 +205,8 @@ 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']
|
||||||
self.db.commit()
|
update_roles(self.db, obj=user, kind='users')
|
||||||
|
self.db.commit()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
await maybe_future(self.authenticator.add_user(user))
|
await maybe_future(self.authenticator.add_user(user))
|
||||||
@@ -261,6 +265,8 @@ class UserAPIHandler(APIHandler):
|
|||||||
await user.save_auth_state(value)
|
await user.save_auth_state(value)
|
||||||
else:
|
else:
|
||||||
setattr(user, key, value)
|
setattr(user, key, value)
|
||||||
|
if key == 'admin':
|
||||||
|
update_roles(self.db, obj=user, kind='users')
|
||||||
self.db.commit()
|
self.db.commit()
|
||||||
user_ = self.user_model(user)
|
user_ = self.user_model(user)
|
||||||
user_['auth_state'] = await user.get_auth_state()
|
user_['auth_state'] = await user.get_auth_state()
|
||||||
@@ -345,9 +351,19 @@ class UserTokenListAPIHandler(APIHandler):
|
|||||||
if requester is not user:
|
if requester is not user:
|
||||||
note += " by %s %s" % (kind, requester.name)
|
note += " by %s %s" % (kind, requester.name)
|
||||||
|
|
||||||
api_token = user.new_api_token(
|
token_roles = body.get('roles')
|
||||||
note=note, expires_in=body.get('expires_in', None)
|
try:
|
||||||
)
|
api_token = user.new_api_token(
|
||||||
|
note=note, expires_in=body.get('expires_in', None), roles=token_roles
|
||||||
|
)
|
||||||
|
except NameError:
|
||||||
|
raise web.HTTPError(404, "Requested roles %r not found" % token_roles)
|
||||||
|
except ValueError:
|
||||||
|
raise web.HTTPError(
|
||||||
|
403,
|
||||||
|
"Requested token roles %r have higher permissions than the token owner"
|
||||||
|
% token_roles,
|
||||||
|
)
|
||||||
if requester is not user:
|
if requester is not user:
|
||||||
self.log.info(
|
self.log.info(
|
||||||
"%s %s requested API token for %s",
|
"%s %s requested API token for %s",
|
||||||
|
@@ -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
|
||||||
@@ -312,6 +313,31 @@ 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::
|
||||||
|
|
||||||
|
load_roles = [
|
||||||
|
{
|
||||||
|
'name': 'teacher',
|
||||||
|
'description': 'Access users information, servers and groups without create/delete privileges',
|
||||||
|
'scopes': ['users', 'groups'],
|
||||||
|
'users': ['cyclops', 'gandalf'],
|
||||||
|
'services': [],
|
||||||
|
'tokens': []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
All keys apart from 'name' are optional.
|
||||||
|
See all the available scopes in the JupyterHub REST API documentation.
|
||||||
|
|
||||||
|
Default roles are defined 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
|
||||||
)
|
)
|
||||||
@@ -1700,7 +1726,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.
|
||||||
|
|
||||||
@@ -1799,6 +1824,46 @@ 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
|
||||||
|
role_bearers = ['users', 'services', 'tokens']
|
||||||
|
|
||||||
|
# load default roles
|
||||||
|
default_roles = roles.get_default_roles()
|
||||||
|
for role in default_roles:
|
||||||
|
roles.add_role(db, role)
|
||||||
|
|
||||||
|
# load predefined roles from config file
|
||||||
|
for predef_role in self.load_roles:
|
||||||
|
roles.add_role(db, predef_role)
|
||||||
|
# add users, services and/or tokens
|
||||||
|
for bearer in role_bearers:
|
||||||
|
if bearer in predef_role.keys():
|
||||||
|
for bname in predef_role[bearer]:
|
||||||
|
if bearer == 'users':
|
||||||
|
bname = self.authenticator.normalize_username(bname)
|
||||||
|
if not (
|
||||||
|
await maybe_future(
|
||||||
|
self.authenticator.check_allowed(bname, None)
|
||||||
|
)
|
||||||
|
):
|
||||||
|
raise ValueError(
|
||||||
|
"Username %r is not in Authenticator.allowed_users"
|
||||||
|
% bname
|
||||||
|
)
|
||||||
|
roles.add_obj(
|
||||||
|
db, objname=bname, kind=bearer, rolename=predef_role['name']
|
||||||
|
)
|
||||||
|
|
||||||
|
# make sure all users, services and tokens have at least one role (update with default)
|
||||||
|
for bearer in role_bearers:
|
||||||
|
Class = roles.get_orm_class(bearer)
|
||||||
|
for obj in db.query(Class):
|
||||||
|
if len(obj.roles) < 1:
|
||||||
|
roles.update_roles(db, obj=obj, kind=bearer)
|
||||||
|
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':
|
||||||
@@ -1910,6 +1975,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,
|
||||||
@@ -2388,6 +2454,7 @@ class JupyterHub(Application):
|
|||||||
await self.init_groups()
|
await self.init_groups()
|
||||||
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()
|
||||||
|
@@ -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()
|
||||||
|
@@ -30,6 +30,7 @@ from tornado.web import RequestHandler
|
|||||||
|
|
||||||
from .. import __version__
|
from .. import __version__
|
||||||
from .. import orm
|
from .. import orm
|
||||||
|
from .. import roles
|
||||||
from ..metrics import PROXY_ADD_DURATION_SECONDS
|
from ..metrics import PROXY_ADD_DURATION_SECONDS
|
||||||
from ..metrics import PROXY_DELETE_DURATION_SECONDS
|
from ..metrics import PROXY_DELETE_DURATION_SECONDS
|
||||||
from ..metrics import ProxyDeleteStatus
|
from ..metrics import ProxyDeleteStatus
|
||||||
@@ -457,6 +458,7 @@ class BaseHandler(RequestHandler):
|
|||||||
# not found, create and register user
|
# not found, create and register user
|
||||||
u = orm.User(name=username)
|
u = orm.User(name=username)
|
||||||
self.db.add(u)
|
self.db.add(u)
|
||||||
|
roles.update_roles(self.db, obj=u, kind='users')
|
||||||
TOTAL_USERS.inc()
|
TOTAL_USERS.inc()
|
||||||
self.db.commit()
|
self.db.commit()
|
||||||
user = self._user_from_orm(u)
|
user = self._user_from_orm(u)
|
||||||
@@ -736,6 +738,7 @@ class BaseHandler(RequestHandler):
|
|||||||
# Only set `admin` if the authenticator returned an explicit value.
|
# Only set `admin` if the authenticator returned an explicit value.
|
||||||
if admin is not None and admin != user.admin:
|
if admin is not None and admin != user.admin:
|
||||||
user.admin = admin
|
user.admin = admin
|
||||||
|
roles.update_roles(self.db, obj=user, kind='users')
|
||||||
self.db.commit()
|
self.db.commit()
|
||||||
# always set auth_state and commit,
|
# always set auth_state and commit,
|
||||||
# because there could be key-rotation or clearing of previous values
|
# because there could be key-rotation or clearing of previous values
|
||||||
|
@@ -39,6 +39,9 @@ from sqlalchemy.types import Text
|
|||||||
from sqlalchemy.types import TypeDecorator
|
from sqlalchemy.types import TypeDecorator
|
||||||
from tornado.log import app_log
|
from tornado.log import app_log
|
||||||
|
|
||||||
|
from .roles import add_role
|
||||||
|
from .roles import get_default_roles
|
||||||
|
from .roles import update_roles
|
||||||
from .utils import compare_token
|
from .utils import compare_token
|
||||||
from .utils import hash_token
|
from .utils import hash_token
|
||||||
from .utils import new_token
|
from .utils import new_token
|
||||||
@@ -90,6 +93,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 +136,65 @@ 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),
|
||||||
|
)
|
||||||
|
|
||||||
|
# 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),
|
||||||
|
)
|
||||||
|
|
||||||
|
# token:role many:many mapping table
|
||||||
|
api_token_role_map = Table(
|
||||||
|
'api_token_role_map',
|
||||||
|
Base.metadata,
|
||||||
|
Column(
|
||||||
|
'api_token_id',
|
||||||
|
ForeignKey('api_tokens.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')
|
||||||
|
services = relationship('Service', secondary='service_role_map', backref='roles')
|
||||||
|
tokens = relationship('APIToken', secondary='api_token_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',
|
||||||
@@ -504,6 +586,7 @@ class APIToken(Hashed, Base):
|
|||||||
token=None,
|
token=None,
|
||||||
user=None,
|
user=None,
|
||||||
service=None,
|
service=None,
|
||||||
|
roles=None,
|
||||||
note='',
|
note='',
|
||||||
generated=True,
|
generated=True,
|
||||||
expires_in=None,
|
expires_in=None,
|
||||||
@@ -532,6 +615,14 @@ class APIToken(Hashed, Base):
|
|||||||
if expires_in is not None:
|
if expires_in is not None:
|
||||||
orm_token.expires_at = cls.now() + timedelta(seconds=expires_in)
|
orm_token.expires_at = cls.now() + timedelta(seconds=expires_in)
|
||||||
db.add(orm_token)
|
db.add(orm_token)
|
||||||
|
# load default roles if they haven't been initiated
|
||||||
|
# correct to have this here? otherwise some tests fail
|
||||||
|
user_role = Role.find(db, 'user')
|
||||||
|
if not user_role:
|
||||||
|
default_roles = get_default_roles()
|
||||||
|
for role in default_roles:
|
||||||
|
add_role(db, role)
|
||||||
|
update_roles(db, obj=orm_token, kind='tokens', roles=roles)
|
||||||
db.commit()
|
db.commit()
|
||||||
return token
|
return token
|
||||||
|
|
||||||
|
261
jupyterhub/roles.py
Normal file
261
jupyterhub/roles.py
Normal file
@@ -0,0 +1,261 @@
|
|||||||
|
"""Roles utils"""
|
||||||
|
# Copyright (c) Jupyter Development Team.
|
||||||
|
# Distributed under the terms of the Modified BSD License.
|
||||||
|
from itertools import chain
|
||||||
|
|
||||||
|
from . import orm
|
||||||
|
|
||||||
|
|
||||||
|
def get_default_roles():
|
||||||
|
|
||||||
|
"""Returns a list of default role dictionaries"""
|
||||||
|
|
||||||
|
default_roles = [
|
||||||
|
{
|
||||||
|
'name': 'user',
|
||||||
|
'description': 'Everything the user can do',
|
||||||
|
'scopes': ['all'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'admin',
|
||||||
|
'description': 'Admin privileges (currently can do everything)',
|
||||||
|
'scopes': [
|
||||||
|
'all',
|
||||||
|
'users',
|
||||||
|
'users:tokens',
|
||||||
|
'admin:users',
|
||||||
|
'admin:users:servers',
|
||||||
|
'groups',
|
||||||
|
'admin:groups',
|
||||||
|
'read:services',
|
||||||
|
'read:hub',
|
||||||
|
'proxy',
|
||||||
|
'shutdown',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'server',
|
||||||
|
'description': 'Post activity only',
|
||||||
|
'scopes': ['users:activity!user=username'],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
return default_roles
|
||||||
|
|
||||||
|
|
||||||
|
def get_scopes():
|
||||||
|
|
||||||
|
"""
|
||||||
|
Returns a dictionary of scopes:
|
||||||
|
scopes.keys() = scopes of highest level and scopes that have their own subscopes
|
||||||
|
scopes.values() = a list of first level subscopes or None
|
||||||
|
"""
|
||||||
|
|
||||||
|
scopes = {
|
||||||
|
'all': ['read:all'],
|
||||||
|
'users': ['read:users', 'users:activity', 'users:servers'],
|
||||||
|
'read:users': [
|
||||||
|
'read:users:name',
|
||||||
|
'read:users:groups',
|
||||||
|
'read:users:activity',
|
||||||
|
'read:users:servers',
|
||||||
|
],
|
||||||
|
'users:tokens': ['read:users:tokens'],
|
||||||
|
'admin:users': None,
|
||||||
|
'admin:users:servers': None,
|
||||||
|
'groups': ['read:groups'],
|
||||||
|
'admin:groups': None,
|
||||||
|
'read:services': None,
|
||||||
|
'read:hub': None,
|
||||||
|
'proxy': None,
|
||||||
|
'shutdown': None,
|
||||||
|
}
|
||||||
|
|
||||||
|
return scopes
|
||||||
|
|
||||||
|
|
||||||
|
def expand_scope(scopename):
|
||||||
|
|
||||||
|
"""Returns a set of all subscopes"""
|
||||||
|
|
||||||
|
scopes = get_scopes()
|
||||||
|
subscopes = [scopename]
|
||||||
|
|
||||||
|
def expand_subscopes(index):
|
||||||
|
|
||||||
|
more_subscopes = list(
|
||||||
|
filter(lambda scope: scope in scopes.keys(), subscopes[index:])
|
||||||
|
)
|
||||||
|
for scope in more_subscopes:
|
||||||
|
subscopes.extend(scopes[scope])
|
||||||
|
|
||||||
|
if scopename in scopes.keys() and scopes[scopename] is not None:
|
||||||
|
subscopes.extend(scopes[scopename])
|
||||||
|
# record the index from where it should check for "subscopes of sub-subscopes"
|
||||||
|
index_for_sssc = len(subscopes)
|
||||||
|
# check for "subscopes of subscopes"
|
||||||
|
expand_subscopes(index=1)
|
||||||
|
# check for "subscopes of sub-subscopes"
|
||||||
|
expand_subscopes(index=index_for_sssc)
|
||||||
|
|
||||||
|
expanded_scope = set(subscopes)
|
||||||
|
|
||||||
|
return expanded_scope
|
||||||
|
|
||||||
|
|
||||||
|
def get_subscopes(*args):
|
||||||
|
|
||||||
|
"""Returns a set of all available subscopes for a specified role or list of roles"""
|
||||||
|
|
||||||
|
scope_list = []
|
||||||
|
|
||||||
|
for role in args:
|
||||||
|
scope_list.extend(role.scopes)
|
||||||
|
|
||||||
|
scopes = set(chain.from_iterable(list(map(expand_scope, scope_list))))
|
||||||
|
|
||||||
|
return scopes
|
||||||
|
|
||||||
|
|
||||||
|
def add_role(db, role_dict):
|
||||||
|
|
||||||
|
"""Adds a new role to database or modifies an existing one"""
|
||||||
|
|
||||||
|
if 'name' not in role_dict.keys():
|
||||||
|
raise ValueError('Role must have a name')
|
||||||
|
else:
|
||||||
|
name = role_dict['name']
|
||||||
|
role = orm.Role.find(db, name)
|
||||||
|
|
||||||
|
description = role_dict.get('description')
|
||||||
|
scopes = role_dict.get('scopes')
|
||||||
|
|
||||||
|
if role is None:
|
||||||
|
role = orm.Role(name=name, description=description, scopes=scopes)
|
||||||
|
db.add(role)
|
||||||
|
else:
|
||||||
|
if description:
|
||||||
|
role.description = description
|
||||||
|
if scopes:
|
||||||
|
role.scopes = scopes
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def get_orm_class(kind):
|
||||||
|
if kind == 'users':
|
||||||
|
Class = orm.User
|
||||||
|
elif kind == 'services':
|
||||||
|
Class = orm.Service
|
||||||
|
elif kind == 'tokens':
|
||||||
|
Class = orm.APIToken
|
||||||
|
else:
|
||||||
|
raise ValueError("kind must be users, services or tokens, not %r" % kind)
|
||||||
|
|
||||||
|
return Class
|
||||||
|
|
||||||
|
|
||||||
|
def existing_only(func):
|
||||||
|
|
||||||
|
"""Decorator for checking if objects and roles exist"""
|
||||||
|
|
||||||
|
def check_existence(db, objname, kind, rolename):
|
||||||
|
|
||||||
|
Class = get_orm_class(kind)
|
||||||
|
obj = Class.find(db, objname)
|
||||||
|
role = orm.Role.find(db, rolename)
|
||||||
|
|
||||||
|
if obj is None:
|
||||||
|
raise ValueError("%r of kind %r does not exist" % (objname, kind))
|
||||||
|
elif role is None:
|
||||||
|
raise ValueError("Role %r does not exist" % rolename)
|
||||||
|
else:
|
||||||
|
func(db, obj, kind, role)
|
||||||
|
|
||||||
|
return check_existence
|
||||||
|
|
||||||
|
|
||||||
|
@existing_only
|
||||||
|
def add_obj(db, objname, kind, rolename):
|
||||||
|
|
||||||
|
"""Adds a role for users, services or tokens"""
|
||||||
|
|
||||||
|
if rolename not in objname.roles:
|
||||||
|
objname.roles.append(rolename)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
|
||||||
|
@existing_only
|
||||||
|
def remove_obj(db, objname, kind, rolename):
|
||||||
|
|
||||||
|
"""Removes a role for users, services or tokens"""
|
||||||
|
|
||||||
|
if rolename in objname.roles:
|
||||||
|
objname.roles.remove(rolename)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def switch_default_role(db, obj, kind, admin):
|
||||||
|
|
||||||
|
"""Switch between default user and admin roles for users/services"""
|
||||||
|
|
||||||
|
user_role = orm.Role.find(db, 'user')
|
||||||
|
admin_role = orm.Role.find(db, 'admin')
|
||||||
|
|
||||||
|
def add_and_remove(db, obj, kind, current_role, new_role):
|
||||||
|
|
||||||
|
if current_role in obj.roles:
|
||||||
|
remove_obj(db, objname=obj.name, kind=kind, rolename=current_role.name)
|
||||||
|
# only add new default role if the user has no other roles
|
||||||
|
if len(obj.roles) < 1:
|
||||||
|
add_obj(db, objname=obj.name, kind=kind, rolename=new_role.name)
|
||||||
|
|
||||||
|
if admin:
|
||||||
|
add_and_remove(db, obj, kind, user_role, admin_role)
|
||||||
|
else:
|
||||||
|
add_and_remove(db, obj, kind, admin_role, user_role)
|
||||||
|
|
||||||
|
|
||||||
|
def update_roles(db, obj, kind, roles=None):
|
||||||
|
|
||||||
|
"""Updates object's roles if specified,
|
||||||
|
assigns default if no roles specified"""
|
||||||
|
|
||||||
|
Class = get_orm_class(kind)
|
||||||
|
user_role = orm.Role.find(db, 'user')
|
||||||
|
|
||||||
|
if roles:
|
||||||
|
for rolename in roles:
|
||||||
|
if Class == orm.APIToken:
|
||||||
|
|
||||||
|
role = orm.Role.find(db, rolename)
|
||||||
|
if role:
|
||||||
|
# compare the requested role permissions with the owner's permissions (scopes)
|
||||||
|
token_scopes = get_subscopes(role)
|
||||||
|
# find the owner and their roles
|
||||||
|
owner = None
|
||||||
|
if obj.user_id:
|
||||||
|
owner = db.query(orm.User).get(obj.user_id)
|
||||||
|
elif obj.service_id:
|
||||||
|
owner = db.query(orm.Service).get(obj.service_id)
|
||||||
|
if owner:
|
||||||
|
owner_scopes = get_subscopes(*owner.roles)
|
||||||
|
if token_scopes.issubset(owner_scopes):
|
||||||
|
role.tokens.append(obj)
|
||||||
|
else:
|
||||||
|
raise ValueError(
|
||||||
|
'Requested token role %r has higher permissions than the token owner'
|
||||||
|
% rolename
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise NameError('Role %r does not exist' % rolename)
|
||||||
|
else:
|
||||||
|
add_obj(db, objname=obj.name, kind=kind, rolename=rolename)
|
||||||
|
else:
|
||||||
|
# tokens can have only 'user' role as default
|
||||||
|
# assign the default only for user tokens
|
||||||
|
if Class == orm.APIToken:
|
||||||
|
if len(obj.roles) < 1 and obj.user is not None:
|
||||||
|
user_role.tokens.append(obj)
|
||||||
|
db.commit()
|
||||||
|
# users and services can have 'user' or 'admin' roles as default
|
||||||
|
else:
|
||||||
|
switch_default_role(db, obj, kind, obj.admin)
|
@@ -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()
|
||||||
|
@@ -43,6 +43,7 @@ from traitlets import Dict
|
|||||||
|
|
||||||
from .. import metrics
|
from .. import metrics
|
||||||
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
|
||||||
@@ -314,13 +315,15 @@ class MockHub(JupyterHub):
|
|||||||
test_clean_db = Bool(True)
|
test_clean_db = Bool(True)
|
||||||
|
|
||||||
def init_db(self):
|
def init_db(self):
|
||||||
"""Ensure we start with a clean user list"""
|
"""Ensure we start with a clean user & role list"""
|
||||||
super().init_db()
|
super().init_db()
|
||||||
if self.test_clean_db:
|
if self.test_clean_db:
|
||||||
for user in self.db.query(orm.User):
|
for user in self.db.query(orm.User):
|
||||||
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()
|
||||||
|
|
||||||
async def initialize(self, argv=None):
|
async def initialize(self, argv=None):
|
||||||
@@ -338,6 +341,8 @@ class MockHub(JupyterHub):
|
|||||||
self.db.add(user)
|
self.db.add(user)
|
||||||
self.db.commit()
|
self.db.commit()
|
||||||
metrics.TOTAL_USERS.inc()
|
metrics.TOTAL_USERS.inc()
|
||||||
|
roles.update_roles(self.db, obj=user, kind='users')
|
||||||
|
self.db.commit()
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
super().stop()
|
super().stop()
|
||||||
|
@@ -14,6 +14,7 @@ from pytest import mark
|
|||||||
|
|
||||||
import jupyterhub
|
import jupyterhub
|
||||||
from .. import orm
|
from .. import orm
|
||||||
|
from .. import roles
|
||||||
from ..objects import Server
|
from ..objects import Server
|
||||||
from ..utils import url_path_join as ujoin
|
from ..utils import url_path_join as ujoin
|
||||||
from ..utils import utcnow
|
from ..utils import utcnow
|
||||||
@@ -64,6 +65,7 @@ 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)
|
||||||
@@ -150,6 +152,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)
|
||||||
@@ -164,6 +167,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', headers=auth_header(db, 'admin'))
|
r = await api_request(app, 'users', headers=auth_header(db, 'admin'))
|
||||||
@@ -172,8 +176,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}
|
||||||
|
),
|
||||||
]
|
]
|
||||||
with mock_role(app, 'user'):
|
with mock_role(app, 'user'):
|
||||||
r = await api_request(app, 'users', headers=auth_header(db, 'user'))
|
r = await api_request(app, 'users', headers=auth_header(db, 'user'))
|
||||||
@@ -288,6 +294,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'
|
||||||
@@ -297,9 +304,13 @@ 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 orm.Role.find(db, 'user') in user.roles
|
||||||
|
assert orm.Role.find(db, 'admin') 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'
|
||||||
_ = await api_request(app, 'users', name, headers=auth_header(app.db, name))
|
_ = await api_request(app, 'users', name, headers=auth_header(app.db, name))
|
||||||
@@ -312,7 +323,7 @@ async def test_get_user(app):
|
|||||||
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
|
||||||
@@ -340,6 +351,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']
|
||||||
@@ -356,6 +368,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 orm.Role.find(db, 'user') in user.roles
|
||||||
|
assert orm.Role.find(db, 'admin') 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(
|
||||||
@@ -376,6 +391,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']
|
||||||
@@ -395,6 +411,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 orm.Role.find(db, 'user') not in user.roles
|
||||||
|
assert orm.Role.find(db, 'admin') in user.roles
|
||||||
|
|
||||||
|
|
||||||
@mark.user
|
@mark.user
|
||||||
@@ -420,6 +438,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'
|
||||||
@@ -431,6 +450,9 @@ async def test_add_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 newadmin has default 'admin' role
|
||||||
|
assert orm.Role.find(db, 'user') not in user.roles
|
||||||
|
assert orm.Role.find(db, 'admin') in user.roles
|
||||||
|
|
||||||
|
|
||||||
@mark.user
|
@mark.user
|
||||||
@@ -442,6 +464,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'
|
||||||
@@ -451,15 +474,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 orm.Role.find(db, 'user') in user.roles
|
||||||
|
assert orm.Role.find(db, 'admin') 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 orm.Role.find(db, 'user') not in user.roles
|
||||||
|
assert orm.Role.find(db, 'admin') in user.roles
|
||||||
|
|
||||||
|
|
||||||
@mark.user
|
@mark.user
|
||||||
@@ -1184,7 +1212,7 @@ async def test_token_as_user_deprecated(app, as_user, for_user, status):
|
|||||||
# ensure both users exist
|
# ensure both users exist
|
||||||
u = add_user(app.db, app, name=as_user)
|
u = add_user(app.db, app, name=as_user)
|
||||||
if for_user != 'missing':
|
if for_user != 'missing':
|
||||||
add_user(app.db, app, name=for_user)
|
for_user_obj = add_user(app.db, app, name=for_user)
|
||||||
data = {'username': for_user}
|
data = {'username': for_user}
|
||||||
headers = {'Authorization': 'token %s' % u.new_api_token()}
|
headers = {'Authorization': 'token %s' % u.new_api_token()}
|
||||||
r = await api_request(
|
r = await api_request(
|
||||||
@@ -1277,7 +1305,7 @@ async def test_token_for_user(app, as_user, for_user, status):
|
|||||||
# ensure both users exist
|
# ensure both users exist
|
||||||
u = add_user(app.db, app, name=as_user)
|
u = add_user(app.db, app, name=as_user)
|
||||||
if for_user != 'missing':
|
if for_user != 'missing':
|
||||||
add_user(app.db, app, name=for_user)
|
for_user_obj = add_user(app.db, app, name=for_user)
|
||||||
data = {'username': for_user}
|
data = {'username': for_user}
|
||||||
headers = {'Authorization': 'token %s' % u.new_api_token()}
|
headers = {'Authorization': 'token %s' % u.new_api_token()}
|
||||||
r = await api_request(
|
r = await api_request(
|
||||||
@@ -1294,6 +1322,7 @@ async def test_token_for_user(app, as_user, for_user, status):
|
|||||||
if status != 200:
|
if status != 200:
|
||||||
return
|
return
|
||||||
assert 'token' in reply
|
assert 'token' in reply
|
||||||
|
|
||||||
token_id = reply['id']
|
token_id = reply['id']
|
||||||
r = await api_request(app, 'users', for_user, 'tokens', token_id, headers=headers)
|
r = await api_request(app, 'users', for_user, 'tokens', token_id, headers=headers)
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
@@ -1565,6 +1594,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,
|
||||||
@@ -1590,6 +1620,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,
|
||||||
|
@@ -56,6 +56,7 @@ async def test_default_server(app, named_servers):
|
|||||||
assert user_model == fill_user(
|
assert user_model == fill_user(
|
||||||
{
|
{
|
||||||
'name': username,
|
'name': username,
|
||||||
|
'roles': ['user'],
|
||||||
'auth_state': None,
|
'auth_state': None,
|
||||||
'server': user.url,
|
'server': user.url,
|
||||||
'servers': {
|
'servers': {
|
||||||
@@ -86,7 +87,7 @@ async def test_default_server(app, named_servers):
|
|||||||
|
|
||||||
user_model = normalize_user(r.json())
|
user_model = normalize_user(r.json())
|
||||||
assert user_model == fill_user(
|
assert user_model == fill_user(
|
||||||
{'name': username, 'servers': {}, 'auth_state': None}
|
{'name': username, 'roles': ['user'], 'servers': {}, 'auth_state': None}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -117,6 +118,7 @@ async def test_create_named_server(app, named_servers):
|
|||||||
assert user_model == fill_user(
|
assert user_model == fill_user(
|
||||||
{
|
{
|
||||||
'name': username,
|
'name': username,
|
||||||
|
'roles': ['user'],
|
||||||
'auth_state': None,
|
'auth_state': None,
|
||||||
'servers': {
|
'servers': {
|
||||||
servername: {
|
servername: {
|
||||||
@@ -159,7 +161,7 @@ async def test_delete_named_server(app, named_servers):
|
|||||||
|
|
||||||
user_model = normalize_user(r.json())
|
user_model = normalize_user(r.json())
|
||||||
assert user_model == fill_user(
|
assert user_model == fill_user(
|
||||||
{'name': username, 'auth_state': None, 'servers': {}}
|
{'name': username, 'roles': ['user'], 'auth_state': None, 'servers': {}}
|
||||||
)
|
)
|
||||||
# wrapper Spawner is gone
|
# wrapper Spawner is gone
|
||||||
assert servername not in user.spawners
|
assert servername not in user.spawners
|
||||||
|
@@ -244,10 +244,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 == []
|
||||||
@@ -459,7 +461,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
|
||||||
@@ -479,6 +481,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
|
||||||
|
|
||||||
|
406
jupyterhub/tests/test_roles.py
Normal file
406
jupyterhub/tests/test_roles.py
Normal file
@@ -0,0 +1,406 @@
|
|||||||
|
"""Test roles"""
|
||||||
|
# Copyright (c) Jupyter Development Team.
|
||||||
|
# Distributed under the terms of the Modified BSD License.
|
||||||
|
import json
|
||||||
|
|
||||||
|
from pytest import mark
|
||||||
|
|
||||||
|
from .. import orm
|
||||||
|
from .. import roles
|
||||||
|
from ..utils import maybe_future
|
||||||
|
from .mocking import MockHub
|
||||||
|
from .utils import add_user
|
||||||
|
from .utils import api_request
|
||||||
|
|
||||||
|
|
||||||
|
@mark.role
|
||||||
|
def test_orm_roles(db):
|
||||||
|
"""Test orm roles setup"""
|
||||||
|
user_role = orm.Role.find(db, name='user')
|
||||||
|
if not user_role:
|
||||||
|
user_role = orm.Role(name='user', scopes=['all', 'read:all'])
|
||||||
|
db.add(user_role)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
service_role = orm.Role(name='service', scopes=['users:servers'])
|
||||||
|
db.add(service_role)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
user = orm.User(name='falafel')
|
||||||
|
db.add(user)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
service = orm.Service(name='kebab')
|
||||||
|
db.add(service)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
assert user_role.users == []
|
||||||
|
assert user_role.services == []
|
||||||
|
assert service_role.users == []
|
||||||
|
assert service_role.services == []
|
||||||
|
assert user.roles == []
|
||||||
|
assert service.roles == []
|
||||||
|
|
||||||
|
user_role.users.append(user)
|
||||||
|
service_role.services.append(service)
|
||||||
|
db.commit()
|
||||||
|
assert user_role.users == [user]
|
||||||
|
assert user.roles == [user_role]
|
||||||
|
assert service_role.services == [service]
|
||||||
|
assert service.roles == [service_role]
|
||||||
|
|
||||||
|
# check token creation without specifying its role
|
||||||
|
# assigns it the default 'user' role
|
||||||
|
token = user.new_api_token()
|
||||||
|
user_token = orm.APIToken.find(db, token=token)
|
||||||
|
assert user_token in user_role.tokens
|
||||||
|
assert user_role in user_token.roles
|
||||||
|
|
||||||
|
# check creating token with a specific role
|
||||||
|
token = service.new_api_token(roles=['service'])
|
||||||
|
service_token = orm.APIToken.find(db, token=token)
|
||||||
|
assert service_token in service_role.tokens
|
||||||
|
assert service_role in service_token.roles
|
||||||
|
|
||||||
|
# check deleting user removes the user and the token from roles
|
||||||
|
db.delete(user)
|
||||||
|
db.commit()
|
||||||
|
assert user_role.users == []
|
||||||
|
assert user_token not in user_role.tokens
|
||||||
|
# check deleting the service token removes it from 'service' role
|
||||||
|
db.delete(service_token)
|
||||||
|
db.commit()
|
||||||
|
assert service_token not in service_role.tokens
|
||||||
|
# check deleting the 'service' role removes it from service roles
|
||||||
|
db.delete(service_role)
|
||||||
|
db.commit()
|
||||||
|
assert service.roles == []
|
||||||
|
|
||||||
|
db.delete(service)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
|
||||||
|
@mark.role
|
||||||
|
def test_orm_roles_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
|
||||||
|
@mark.parametrize(
|
||||||
|
"scopes, subscopes",
|
||||||
|
[
|
||||||
|
(
|
||||||
|
['users'],
|
||||||
|
{
|
||||||
|
'users',
|
||||||
|
'read:users',
|
||||||
|
'users:activity',
|
||||||
|
'users:servers',
|
||||||
|
'read:users:name',
|
||||||
|
'read:users:groups',
|
||||||
|
'read:users:activity',
|
||||||
|
'read:users:servers',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
['read:users'],
|
||||||
|
{
|
||||||
|
'read:users',
|
||||||
|
'read:users:name',
|
||||||
|
'read:users:groups',
|
||||||
|
'read:users:activity',
|
||||||
|
'read:users:servers',
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(['read:users:servers'], {'read:users:servers'}),
|
||||||
|
(['admin:groups'], {'admin:groups'}),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_get_subscopes(db, scopes, subscopes):
|
||||||
|
"""Test role scopes expansion into their subscopes"""
|
||||||
|
roles.add_role(db, {'name': 'testing_scopes', 'scopes': scopes})
|
||||||
|
role = orm.Role.find(db, name='testing_scopes')
|
||||||
|
response = roles.get_subscopes(role)
|
||||||
|
assert response == subscopes
|
||||||
|
db.delete(role)
|
||||||
|
|
||||||
|
|
||||||
|
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',
|
||||||
|
'description': 'Access users information, servers and groups without create/delete privileges',
|
||||||
|
'scopes': ['users', 'groups'],
|
||||||
|
'users': ['cyclops', 'gandalf'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'name': 'user',
|
||||||
|
'description': 'Only read access',
|
||||||
|
'scopes': ['read:all'],
|
||||||
|
'users': ['bilbo'],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
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(**kwargs)
|
||||||
|
hub.init_db()
|
||||||
|
db = hub.db
|
||||||
|
hub.authenticator.admin_users = ['admin']
|
||||||
|
hub.authenticator.allowed_users = ['cyclops', 'gandalf', 'bilbo', 'gargamel']
|
||||||
|
await hub.init_users()
|
||||||
|
await hub.init_roles()
|
||||||
|
|
||||||
|
# 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 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
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
@mark.role
|
||||||
|
async def test_load_roles_services(tmpdir, request):
|
||||||
|
services = [
|
||||||
|
{'name': 'cull_idle', 'api_token': 'some-token'},
|
||||||
|
{'name': 'user_service', 'api_token': 'some-other-token'},
|
||||||
|
{'name': 'admin_service', 'api_token': 'secret-token', 'admin': True},
|
||||||
|
]
|
||||||
|
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(**kwargs)
|
||||||
|
hub.init_db()
|
||||||
|
db = hub.db
|
||||||
|
# clean db of previous services and add testing ones
|
||||||
|
for service in db.query(orm.Service):
|
||||||
|
db.delete(service)
|
||||||
|
db.commit()
|
||||||
|
for service in services:
|
||||||
|
orm_service = orm.Service.find(db, name=service['name'])
|
||||||
|
if orm_service is None:
|
||||||
|
# not found, create a new one
|
||||||
|
orm_service = orm.Service(name=service['name'])
|
||||||
|
db.add(orm_service)
|
||||||
|
orm_service.admin = service.get('admin', False)
|
||||||
|
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
|
||||||
|
|
||||||
|
# delete the test services
|
||||||
|
for service in db.query(orm.Service):
|
||||||
|
db.delete(service)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
|
||||||
|
@mark.role
|
||||||
|
async def test_load_roles_tokens(tmpdir, request):
|
||||||
|
services = [
|
||||||
|
{'name': 'cull_idle', 'admin': True, 'api_token': 'another-secret-token'}
|
||||||
|
]
|
||||||
|
user_tokens = {
|
||||||
|
'secret-token': 'cyclops',
|
||||||
|
'super-secret-token': 'admin',
|
||||||
|
}
|
||||||
|
service_tokens = {
|
||||||
|
'another-secret-token': 'cull_idle',
|
||||||
|
}
|
||||||
|
roles_to_load = [
|
||||||
|
{
|
||||||
|
'name': 'culler',
|
||||||
|
'description': 'Cull idle servers',
|
||||||
|
'scopes': ['users:servers', 'admin:servers'],
|
||||||
|
'tokens': ['another-secret-token'],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
kwargs = {
|
||||||
|
'load_roles': roles_to_load,
|
||||||
|
'services': services,
|
||||||
|
'api_tokens': user_tokens,
|
||||||
|
'service_tokens': service_tokens,
|
||||||
|
}
|
||||||
|
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
|
||||||
|
hub.authenticator.admin_users = ['admin']
|
||||||
|
hub.authenticator.allowed_users = ['cyclops', 'gandalf']
|
||||||
|
await hub.init_users()
|
||||||
|
await hub.init_api_tokens()
|
||||||
|
await hub.init_roles()
|
||||||
|
|
||||||
|
# test if another-secret-token has culler role
|
||||||
|
service = orm.Service.find(db, 'cull_idle')
|
||||||
|
culler_role = orm.Role.find(db, 'culler')
|
||||||
|
token = orm.APIToken.find(db, 'another-secret-token')
|
||||||
|
assert len(token.roles) == 1
|
||||||
|
assert culler_role in token.roles
|
||||||
|
|
||||||
|
# test if all other tokens have default 'user' role
|
||||||
|
user_role = orm.Role.find(db, 'user')
|
||||||
|
sec_token = orm.APIToken.find(db, 'secret-token')
|
||||||
|
assert user_role in sec_token.roles
|
||||||
|
s_sec_token = orm.APIToken.find(db, 'super-secret-token')
|
||||||
|
assert user_role in s_sec_token.roles
|
||||||
|
|
||||||
|
|
||||||
|
@mark.role
|
||||||
|
@mark.parametrize(
|
||||||
|
"headers, role_list, status",
|
||||||
|
[
|
||||||
|
({}, None, 200),
|
||||||
|
({}, ['reader'], 200),
|
||||||
|
({}, ['non-existing'], 404),
|
||||||
|
({}, ['user_creator'], 403),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_get_new_token_via_api(app, headers, role_list, status):
|
||||||
|
user = add_user(app.db, app, name='user')
|
||||||
|
roles.add_role(app.db, {'name': 'reader', 'scopes': ['read:all']})
|
||||||
|
roles.add_role(app.db, {'name': 'user_creator', 'scopes': ['admin:users']})
|
||||||
|
if role_list:
|
||||||
|
body = json.dumps({'roles': role_list})
|
||||||
|
else:
|
||||||
|
body = ''
|
||||||
|
# request a new token
|
||||||
|
r = await api_request(
|
||||||
|
app, 'users/user/tokens', method='post', headers=headers, data=body
|
||||||
|
)
|
||||||
|
assert r.status_code == status
|
||||||
|
if status != 200:
|
||||||
|
return
|
||||||
|
# check the new-token reply for roles
|
||||||
|
reply = r.json()
|
||||||
|
assert 'token' in reply
|
||||||
|
assert reply['user'] == 'user'
|
||||||
|
if not role_list:
|
||||||
|
assert reply['roles'] == ['user']
|
||||||
|
else:
|
||||||
|
assert reply['roles'] == ['reader']
|
||||||
|
token_id = reply['id']
|
||||||
|
|
||||||
|
# delete the token
|
||||||
|
r = await api_request(app, 'users/user/tokens', token_id, method='delete')
|
||||||
|
assert r.status_code == 204
|
||||||
|
# verify deletion
|
||||||
|
r = await api_request(app, 'users/user/tokens', token_id)
|
||||||
|
assert r.status_code == 404
|
@@ -296,7 +296,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
|
||||||
@@ -305,7 +305,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',
|
||||||
|
@@ -9,6 +9,7 @@ from certipy import Certipy
|
|||||||
from jupyterhub import metrics
|
from jupyterhub import metrics
|
||||||
from jupyterhub import orm
|
from jupyterhub import orm
|
||||||
from jupyterhub.objects import Server
|
from jupyterhub.objects import Server
|
||||||
|
from jupyterhub.roles import update_roles
|
||||||
from jupyterhub.utils import url_path_join as ujoin
|
from jupyterhub.utils import url_path_join as ujoin
|
||||||
|
|
||||||
|
|
||||||
@@ -111,6 +112,8 @@ def add_user(db, app=None, **kwargs):
|
|||||||
for attr, value in kwargs.items():
|
for attr, value in kwargs.items():
|
||||||
setattr(orm_user, attr, value)
|
setattr(orm_user, attr, value)
|
||||||
db.commit()
|
db.commit()
|
||||||
|
requested_roles = kwargs.get('roles')
|
||||||
|
update_roles(db, obj=orm_user, kind='users', roles=requested_roles)
|
||||||
if app:
|
if app:
|
||||||
return app.users[orm_user.id]
|
return app.users[orm_user.id]
|
||||||
else:
|
else:
|
||||||
|
@@ -406,9 +406,15 @@ def needs_scope(scope):
|
|||||||
if check_scope(self, scope, parsed_scopes, **s_kwargs):
|
if check_scope(self, scope, parsed_scopes, **s_kwargs):
|
||||||
return func(self, *args, **kwargs)
|
return func(self, *args, **kwargs)
|
||||||
else:
|
else:
|
||||||
|
# catching attr error occurring for older_requirements test
|
||||||
|
# could be done more ellegantly?
|
||||||
|
try:
|
||||||
|
request_path = self.request.path
|
||||||
|
except AttributeError:
|
||||||
|
request_path = 'the requested API'
|
||||||
app_log.warning(
|
app_log.warning(
|
||||||
"Not authorizing access to {}. Requires scope {}, not derived from scopes {}".format(
|
"Not authorizing access to {}. Requires scope {}, not derived from scopes {}".format(
|
||||||
self.request.path, scope, ", ".join(self.scopes)
|
request_path, scope, ", ".join(self.scopes)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
raise web.HTTPError(
|
raise web.HTTPError(
|
||||||
|
@@ -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