mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-10 19:43:01 +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
|
||||
if isinstance(token, orm.APIToken):
|
||||
kind = 'api_token'
|
||||
roles = [r.name for r in token.roles]
|
||||
extra = {'note': token.note}
|
||||
expires_at = token.expires_at
|
||||
elif isinstance(token, orm.OAuthAccessToken):
|
||||
kind = 'oauth'
|
||||
# oauth tokens do not bear roles
|
||||
roles = []
|
||||
extra = {'oauth_client': token.client.description or token.client.client_id}
|
||||
if token.expires_at:
|
||||
expires_at = datetime.fromtimestamp(token.expires_at)
|
||||
@@ -174,6 +177,7 @@ class APIHandler(BaseHandler):
|
||||
owner_key: owner,
|
||||
'id': token.api_id,
|
||||
'kind': kind,
|
||||
'roles': [role for role in roles],
|
||||
'created': isoformat(token.created),
|
||||
'last_activity': isoformat(token.last_activity),
|
||||
'expires_at': isoformat(expires_at),
|
||||
@@ -190,6 +194,7 @@ class APIHandler(BaseHandler):
|
||||
'kind': 'user',
|
||||
'name': user.name,
|
||||
'admin': user.admin,
|
||||
'roles': [r.name for r in user.roles],
|
||||
'groups': [g.name for g in user.groups],
|
||||
'server': user.url if user.running else None,
|
||||
'pending': None,
|
||||
@@ -221,7 +226,12 @@ class APIHandler(BaseHandler):
|
||||
|
||||
def service_model(self, service):
|
||||
"""Get the JSON model for a Service object"""
|
||||
return {'kind': 'service', 'name': service.name, 'admin': service.admin}
|
||||
return {
|
||||
'kind': 'service',
|
||||
'name': service.name,
|
||||
'admin': service.admin,
|
||||
'roles': [r.name for r in service.roles],
|
||||
}
|
||||
|
||||
_user_model_types = {'name': str, 'admin': bool, 'groups': list, 'auth_state': dict}
|
||||
|
||||
|
@@ -19,6 +19,7 @@ def service_model(service):
|
||||
return {
|
||||
'name': service.name,
|
||||
'admin': service.admin,
|
||||
'roles': [r.name for r in service.roles],
|
||||
'url': service.url,
|
||||
'prefix': service.server.base_url if service.server else '',
|
||||
'command': service.command,
|
||||
|
@@ -14,6 +14,8 @@ from tornado import web
|
||||
from tornado.iostream import StreamClosedError
|
||||
|
||||
from .. import orm
|
||||
from .. import roles
|
||||
from ..roles import update_roles
|
||||
from ..user import User
|
||||
from ..utils import isoformat
|
||||
from ..utils import iterate_until
|
||||
@@ -140,7 +142,8 @@ class UserListAPIHandler(APIHandler):
|
||||
user = self.user_from_username(name)
|
||||
if admin:
|
||||
user.admin = True
|
||||
self.db.commit()
|
||||
update_roles(self.db, obj=user, kind='users')
|
||||
self.db.commit()
|
||||
try:
|
||||
await maybe_future(self.authenticator.add_user(user))
|
||||
except Exception as e:
|
||||
@@ -202,7 +205,8 @@ class UserAPIHandler(APIHandler):
|
||||
self._check_user_model(data)
|
||||
if 'admin' in data:
|
||||
user.admin = data['admin']
|
||||
self.db.commit()
|
||||
update_roles(self.db, obj=user, kind='users')
|
||||
self.db.commit()
|
||||
|
||||
try:
|
||||
await maybe_future(self.authenticator.add_user(user))
|
||||
@@ -261,6 +265,8 @@ class UserAPIHandler(APIHandler):
|
||||
await user.save_auth_state(value)
|
||||
else:
|
||||
setattr(user, key, value)
|
||||
if key == 'admin':
|
||||
update_roles(self.db, obj=user, kind='users')
|
||||
self.db.commit()
|
||||
user_ = self.user_model(user)
|
||||
user_['auth_state'] = await user.get_auth_state()
|
||||
@@ -345,9 +351,19 @@ class UserTokenListAPIHandler(APIHandler):
|
||||
if requester is not user:
|
||||
note += " by %s %s" % (kind, requester.name)
|
||||
|
||||
api_token = user.new_api_token(
|
||||
note=note, expires_in=body.get('expires_in', None)
|
||||
)
|
||||
token_roles = body.get('roles')
|
||||
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:
|
||||
self.log.info(
|
||||
"%s %s requested API token for %s",
|
||||
|
@@ -73,6 +73,7 @@ from .services.service import Service
|
||||
|
||||
from . import crypto
|
||||
from . import dbutil, orm
|
||||
from . import roles
|
||||
from .user import UserDict
|
||||
from .oauth.provider import make_provider
|
||||
from ._data import DATA_FILES_PATH
|
||||
@@ -312,6 +313,31 @@ class JupyterHub(Application):
|
||||
""",
|
||||
).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=True
|
||||
)
|
||||
@@ -1700,7 +1726,6 @@ class JupyterHub(Application):
|
||||
db.add(user)
|
||||
else:
|
||||
user.admin = True
|
||||
|
||||
# the admin_users config variable will never be used after this point.
|
||||
# only the database values will be referenced.
|
||||
|
||||
@@ -1799,6 +1824,46 @@ class JupyterHub(Application):
|
||||
group.users.append(user)
|
||||
db.commit()
|
||||
|
||||
async def init_roles(self):
|
||||
"""Load default and predefined roles into the database"""
|
||||
db = self.db
|
||||
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):
|
||||
"""Add tokens for users or services to the database"""
|
||||
if kind == 'user':
|
||||
@@ -1910,6 +1975,7 @@ class JupyterHub(Application):
|
||||
base_url=self.base_url,
|
||||
db=self.db,
|
||||
orm=orm_service,
|
||||
roles=orm_service.roles,
|
||||
domain=domain,
|
||||
host=host,
|
||||
hub=self.hub,
|
||||
@@ -2388,6 +2454,7 @@ class JupyterHub(Application):
|
||||
await self.init_groups()
|
||||
self.init_services()
|
||||
await self.init_api_tokens()
|
||||
await self.init_roles()
|
||||
self.init_tornado_settings()
|
||||
self.init_handlers()
|
||||
self.init_tornado_application()
|
||||
|
@@ -139,7 +139,7 @@ def upgrade_if_needed(db_url, backup=True, log=None):
|
||||
|
||||
|
||||
def shell(args=None):
|
||||
"""Start an IPython shell hooked up to the jupyerhub database"""
|
||||
"""Start an IPython shell hooked up to the jupyterhub database"""
|
||||
from .app import JupyterHub
|
||||
|
||||
hub = JupyterHub()
|
||||
|
@@ -30,6 +30,7 @@ from tornado.web import RequestHandler
|
||||
|
||||
from .. import __version__
|
||||
from .. import orm
|
||||
from .. import roles
|
||||
from ..metrics import PROXY_ADD_DURATION_SECONDS
|
||||
from ..metrics import PROXY_DELETE_DURATION_SECONDS
|
||||
from ..metrics import ProxyDeleteStatus
|
||||
@@ -457,6 +458,7 @@ class BaseHandler(RequestHandler):
|
||||
# not found, create and register user
|
||||
u = orm.User(name=username)
|
||||
self.db.add(u)
|
||||
roles.update_roles(self.db, obj=u, kind='users')
|
||||
TOTAL_USERS.inc()
|
||||
self.db.commit()
|
||||
user = self._user_from_orm(u)
|
||||
@@ -736,6 +738,7 @@ class BaseHandler(RequestHandler):
|
||||
# Only set `admin` if the authenticator returned an explicit value.
|
||||
if admin is not None and admin != user.admin:
|
||||
user.admin = admin
|
||||
roles.update_roles(self.db, obj=user, kind='users')
|
||||
self.db.commit()
|
||||
# always set auth_state and commit,
|
||||
# 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 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 hash_token
|
||||
from .utils import new_token
|
||||
@@ -90,6 +93,26 @@ class JSONDict(TypeDecorator):
|
||||
return value
|
||||
|
||||
|
||||
class JSONList(JSONDict):
|
||||
"""Represents an immutable structure as a json-encoded string (to be used for list type columns).
|
||||
|
||||
Usage::
|
||||
|
||||
JSONList(JSONDict)
|
||||
|
||||
"""
|
||||
|
||||
def process_bind_param(self, value, dialect):
|
||||
if isinstance(value, list) and value is not None:
|
||||
value = json.dumps(value)
|
||||
return value
|
||||
|
||||
def process_result_value(self, value, dialect):
|
||||
if value is not None:
|
||||
value = json.loads(value)
|
||||
return value
|
||||
|
||||
|
||||
Base = declarative_base()
|
||||
Base.log = app_log
|
||||
|
||||
@@ -113,6 +136,65 @@ class Server(Base):
|
||||
return "<Server(%s:%s)>" % (self.ip, self.port)
|
||||
|
||||
|
||||
# user:role many:many mapping table
|
||||
user_role_map = Table(
|
||||
'user_role_map',
|
||||
Base.metadata,
|
||||
Column('user_id', ForeignKey('users.id', ondelete='CASCADE'), primary_key=True),
|
||||
Column('role_id', ForeignKey('roles.id', ondelete='CASCADE'), primary_key=True),
|
||||
)
|
||||
|
||||
# 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_map = Table(
|
||||
'user_group_map',
|
||||
@@ -504,6 +586,7 @@ class APIToken(Hashed, Base):
|
||||
token=None,
|
||||
user=None,
|
||||
service=None,
|
||||
roles=None,
|
||||
note='',
|
||||
generated=True,
|
||||
expires_in=None,
|
||||
@@ -532,6 +615,14 @@ class APIToken(Hashed, Base):
|
||||
if expires_in is not None:
|
||||
orm_token.expires_at = cls.now() + timedelta(seconds=expires_in)
|
||||
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()
|
||||
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()
|
||||
db = Any()
|
||||
orm = Any()
|
||||
roles = Any()
|
||||
cookie_options = Dict()
|
||||
|
||||
oauth_provider = Any()
|
||||
|
@@ -43,6 +43,7 @@ from traitlets import Dict
|
||||
|
||||
from .. import metrics
|
||||
from .. import orm
|
||||
from .. import roles
|
||||
from ..app import JupyterHub
|
||||
from ..auth import PAMAuthenticator
|
||||
from ..objects import Server
|
||||
@@ -314,13 +315,15 @@ class MockHub(JupyterHub):
|
||||
test_clean_db = Bool(True)
|
||||
|
||||
def init_db(self):
|
||||
"""Ensure we start with a clean user list"""
|
||||
"""Ensure we start with a clean user & role list"""
|
||||
super().init_db()
|
||||
if self.test_clean_db:
|
||||
for user in self.db.query(orm.User):
|
||||
self.db.delete(user)
|
||||
for group in self.db.query(orm.Group):
|
||||
self.db.delete(group)
|
||||
for role in self.db.query(orm.Role):
|
||||
self.db.delete(role)
|
||||
self.db.commit()
|
||||
|
||||
async def initialize(self, argv=None):
|
||||
@@ -338,6 +341,8 @@ class MockHub(JupyterHub):
|
||||
self.db.add(user)
|
||||
self.db.commit()
|
||||
metrics.TOTAL_USERS.inc()
|
||||
roles.update_roles(self.db, obj=user, kind='users')
|
||||
self.db.commit()
|
||||
|
||||
def stop(self):
|
||||
super().stop()
|
||||
|
@@ -14,6 +14,7 @@ from pytest import mark
|
||||
|
||||
import jupyterhub
|
||||
from .. import orm
|
||||
from .. import roles
|
||||
from ..objects import Server
|
||||
from ..utils import url_path_join as ujoin
|
||||
from ..utils import utcnow
|
||||
@@ -64,6 +65,7 @@ async def test_auth_api(app):
|
||||
async def test_referer_check(app):
|
||||
url = ujoin(public_host(app), app.hub.base_url)
|
||||
host = urlparse(url).netloc
|
||||
# add admin user
|
||||
user = find_user(app.db, 'admin')
|
||||
if user is None:
|
||||
user = add_user(app.db, name='admin', admin=True)
|
||||
@@ -150,6 +152,7 @@ def fill_user(model):
|
||||
"""
|
||||
model.setdefault('server', None)
|
||||
model.setdefault('kind', 'user')
|
||||
model.setdefault('roles', [])
|
||||
model.setdefault('groups', [])
|
||||
model.setdefault('admin', False)
|
||||
model.setdefault('server', None)
|
||||
@@ -164,6 +167,7 @@ TIMESTAMP = normalize_timestamp(datetime.now().isoformat() + 'Z')
|
||||
|
||||
|
||||
@mark.user
|
||||
@mark.role
|
||||
async def test_get_users(app):
|
||||
db = app.db
|
||||
r = await api_request(app, 'users', 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 = [normalize_user(u) for u in users]
|
||||
assert users == [
|
||||
fill_user({'name': 'admin', 'admin': True}),
|
||||
fill_user({'name': 'user', 'admin': False, 'last_activity': None}),
|
||||
fill_user({'name': 'admin', 'admin': True, 'roles': ['admin']}),
|
||||
fill_user(
|
||||
{'name': 'user', 'admin': False, 'roles': ['user'], 'last_activity': None}
|
||||
),
|
||||
]
|
||||
with mock_role(app, '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.role
|
||||
async def test_add_user(app):
|
||||
db = app.db
|
||||
name = 'newuser'
|
||||
@@ -297,9 +304,13 @@ async def test_add_user(app):
|
||||
assert user is not None
|
||||
assert user.name == name
|
||||
assert not user.admin
|
||||
# assert newuser has default 'user' role
|
||||
assert orm.Role.find(db, 'user') in user.roles
|
||||
assert orm.Role.find(db, 'admin') not in user.roles
|
||||
|
||||
|
||||
@mark.user
|
||||
@mark.role
|
||||
async def test_get_user(app):
|
||||
name = 'user'
|
||||
_ = 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
|
||||
|
||||
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
|
||||
@@ -340,6 +351,7 @@ async def test_add_multi_user_invalid(app):
|
||||
|
||||
|
||||
@mark.user
|
||||
@mark.role
|
||||
async def test_add_multi_user(app):
|
||||
db = app.db
|
||||
names = ['a', 'b']
|
||||
@@ -356,6 +368,9 @@ async def test_add_multi_user(app):
|
||||
assert user is not None
|
||||
assert user.name == name
|
||||
assert not user.admin
|
||||
# assert default 'user' role added
|
||||
assert 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
|
||||
r = await api_request(
|
||||
@@ -376,6 +391,7 @@ async def test_add_multi_user(app):
|
||||
|
||||
|
||||
@mark.user
|
||||
@mark.role
|
||||
async def test_add_multi_user_admin(app):
|
||||
db = app.db
|
||||
names = ['c', 'd']
|
||||
@@ -395,6 +411,8 @@ async def test_add_multi_user_admin(app):
|
||||
assert user is not None
|
||||
assert user.name == name
|
||||
assert user.admin
|
||||
assert orm.Role.find(db, 'user') not in user.roles
|
||||
assert orm.Role.find(db, 'admin') in user.roles
|
||||
|
||||
|
||||
@mark.user
|
||||
@@ -420,6 +438,7 @@ async def test_add_user_duplicate(app):
|
||||
|
||||
|
||||
@mark.user
|
||||
@mark.role
|
||||
async def test_add_admin(app):
|
||||
db = app.db
|
||||
name = 'newadmin'
|
||||
@@ -431,6 +450,9 @@ async def test_add_admin(app):
|
||||
assert user is not None
|
||||
assert user.name == name
|
||||
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
|
||||
@@ -442,6 +464,7 @@ async def test_delete_user(app):
|
||||
|
||||
|
||||
@mark.user
|
||||
@mark.role
|
||||
async def test_make_admin(app):
|
||||
db = app.db
|
||||
name = 'admin2'
|
||||
@@ -451,15 +474,20 @@ async def test_make_admin(app):
|
||||
assert user is not None
|
||||
assert user.name == name
|
||||
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(
|
||||
app, 'users', name, method='patch', data=json.dumps({'admin': True})
|
||||
)
|
||||
|
||||
assert r.status_code == 200
|
||||
user = find_user(db, name)
|
||||
assert user is not None
|
||||
assert user.name == name
|
||||
assert user.admin
|
||||
assert orm.Role.find(db, 'user') not in user.roles
|
||||
assert orm.Role.find(db, 'admin') in user.roles
|
||||
|
||||
|
||||
@mark.user
|
||||
@@ -1184,7 +1212,7 @@ async def test_token_as_user_deprecated(app, as_user, for_user, status):
|
||||
# ensure both users exist
|
||||
u = add_user(app.db, app, name=as_user)
|
||||
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}
|
||||
headers = {'Authorization': 'token %s' % u.new_api_token()}
|
||||
r = await api_request(
|
||||
@@ -1277,7 +1305,7 @@ async def test_token_for_user(app, as_user, for_user, status):
|
||||
# ensure both users exist
|
||||
u = add_user(app.db, app, name=as_user)
|
||||
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}
|
||||
headers = {'Authorization': 'token %s' % u.new_api_token()}
|
||||
r = await api_request(
|
||||
@@ -1294,6 +1322,7 @@ async def test_token_for_user(app, as_user, for_user, status):
|
||||
if status != 200:
|
||||
return
|
||||
assert 'token' in reply
|
||||
|
||||
token_id = reply['id']
|
||||
r = await api_request(app, 'users', for_user, 'tokens', token_id, headers=headers)
|
||||
r.raise_for_status()
|
||||
@@ -1565,6 +1594,7 @@ async def test_get_services(app, mockservice_url):
|
||||
mockservice.name: {
|
||||
'name': mockservice.name,
|
||||
'admin': True,
|
||||
'roles': [],
|
||||
'command': mockservice.command,
|
||||
'pid': mockservice.proc.pid,
|
||||
'prefix': mockservice.server.base_url,
|
||||
@@ -1590,6 +1620,7 @@ async def test_get_service(app, mockservice_url):
|
||||
assert service == {
|
||||
'name': mockservice.name,
|
||||
'admin': True,
|
||||
'roles': [],
|
||||
'command': mockservice.command,
|
||||
'pid': mockservice.proc.pid,
|
||||
'prefix': mockservice.server.base_url,
|
||||
|
@@ -56,6 +56,7 @@ async def test_default_server(app, named_servers):
|
||||
assert user_model == fill_user(
|
||||
{
|
||||
'name': username,
|
||||
'roles': ['user'],
|
||||
'auth_state': None,
|
||||
'server': user.url,
|
||||
'servers': {
|
||||
@@ -86,7 +87,7 @@ async def test_default_server(app, named_servers):
|
||||
|
||||
user_model = normalize_user(r.json())
|
||||
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(
|
||||
{
|
||||
'name': username,
|
||||
'roles': ['user'],
|
||||
'auth_state': None,
|
||||
'servers': {
|
||||
servername: {
|
||||
@@ -159,7 +161,7 @@ async def test_delete_named_server(app, named_servers):
|
||||
|
||||
user_model = normalize_user(r.json())
|
||||
assert user_model == fill_user(
|
||||
{'name': username, 'auth_state': None, 'servers': {}}
|
||||
{'name': username, 'roles': ['user'], 'auth_state': None, 'servers': {}}
|
||||
)
|
||||
# wrapper Spawner is gone
|
||||
assert servername not in user.spawners
|
||||
|
@@ -244,10 +244,12 @@ def test_groups(db):
|
||||
db.commit()
|
||||
assert group.users == []
|
||||
assert user.groups == []
|
||||
|
||||
group.users.append(user)
|
||||
db.commit()
|
||||
assert group.users == [user]
|
||||
assert user.groups == [group]
|
||||
|
||||
db.delete(user)
|
||||
db.commit()
|
||||
assert group.users == []
|
||||
@@ -459,7 +461,7 @@ def test_group_delete_cascade(db):
|
||||
assert group2 in user2.groups
|
||||
|
||||
# now start deleting
|
||||
# 1. remove group via user.groups
|
||||
# 1. remove group via user.group
|
||||
user1.groups.remove(group2)
|
||||
db.commit()
|
||||
assert user1 not in group2.users
|
||||
@@ -479,6 +481,7 @@ def test_group_delete_cascade(db):
|
||||
|
||||
# 4. delete user object
|
||||
db.delete(user1)
|
||||
db.delete(user2)
|
||||
db.commit()
|
||||
assert user1 not in group1.users
|
||||
|
||||
|
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()
|
||||
reply = r.json()
|
||||
assert reply == {'kind': 'service', 'name': name, 'admin': False}
|
||||
assert reply == {'kind': 'service', 'name': name, 'admin': False, 'roles': []}
|
||||
assert not r.cookies
|
||||
|
||||
# token in ?token parameter
|
||||
@@ -305,7 +305,7 @@ async def test_hubauth_service_token(app, mockservice_url):
|
||||
)
|
||||
r.raise_for_status()
|
||||
reply = r.json()
|
||||
assert reply == {'kind': 'service', 'name': name, 'admin': False}
|
||||
assert reply == {'kind': 'service', 'name': name, 'admin': False, 'roles': []}
|
||||
|
||||
r = await async_requests.get(
|
||||
public_url(app, mockservice_url) + '/whoami/?token=no-such-token',
|
||||
|
@@ -9,6 +9,7 @@ from certipy import Certipy
|
||||
from jupyterhub import metrics
|
||||
from jupyterhub import orm
|
||||
from jupyterhub.objects import Server
|
||||
from jupyterhub.roles import update_roles
|
||||
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():
|
||||
setattr(orm_user, attr, value)
|
||||
db.commit()
|
||||
requested_roles = kwargs.get('roles')
|
||||
update_roles(db, obj=orm_user, kind='users', roles=requested_roles)
|
||||
if app:
|
||||
return app.users[orm_user.id]
|
||||
else:
|
||||
|
@@ -406,9 +406,15 @@ def needs_scope(scope):
|
||||
if check_scope(self, scope, parsed_scopes, **s_kwargs):
|
||||
return func(self, *args, **kwargs)
|
||||
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(
|
||||
"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(
|
||||
|
@@ -13,3 +13,4 @@ markers =
|
||||
services: mark as a services test
|
||||
user: mark as a test for a user
|
||||
slow: mark a test as slow
|
||||
role: mark as a test for roles
|
||||
|
Reference in New Issue
Block a user