mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-08 18:44:10 +00:00
adding roles to tokens
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),
|
||||||
|
@@ -14,6 +14,7 @@ from tornado.iostream import StreamClosedError
|
|||||||
|
|
||||||
from .. import orm
|
from .. import orm
|
||||||
from .. import roles
|
from .. import roles
|
||||||
|
from ..roles import update_roles
|
||||||
from ..user import User
|
from ..user import User
|
||||||
from ..utils import admin_only
|
from ..utils import admin_only
|
||||||
from ..utils import isoformat
|
from ..utils import isoformat
|
||||||
@@ -88,7 +89,7 @@ class UserListAPIHandler(APIHandler):
|
|||||||
user = self.user_from_username(name)
|
user = self.user_from_username(name)
|
||||||
if admin:
|
if admin:
|
||||||
user.admin = True
|
user.admin = True
|
||||||
roles.update_roles(self.db, user)
|
update_roles(self.db, obj=user, kind='users')
|
||||||
self.db.commit()
|
self.db.commit()
|
||||||
try:
|
try:
|
||||||
await maybe_future(self.authenticator.add_user(user))
|
await maybe_future(self.authenticator.add_user(user))
|
||||||
@@ -151,7 +152,7 @@ class UserAPIHandler(APIHandler):
|
|||||||
self._check_user_model(data)
|
self._check_user_model(data)
|
||||||
if 'admin' in data:
|
if 'admin' in data:
|
||||||
user.admin = data['admin']
|
user.admin = data['admin']
|
||||||
roles.update_roles(self.db, user)
|
update_roles(self.db, obj=user, kind='users')
|
||||||
self.db.commit()
|
self.db.commit()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -210,7 +211,7 @@ class UserAPIHandler(APIHandler):
|
|||||||
else:
|
else:
|
||||||
setattr(user, key, value)
|
setattr(user, key, value)
|
||||||
if key == 'admin':
|
if key == 'admin':
|
||||||
roles.update_roles(self.db, user=user)
|
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()
|
||||||
@@ -296,9 +297,13 @@ 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 ValueError:
|
||||||
|
raise web.HTTPError(404, "Requested roles %r not found" % 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",
|
||||||
|
@@ -318,18 +318,21 @@ class JupyterHub(Application):
|
|||||||
|
|
||||||
For instance::
|
For instance::
|
||||||
|
|
||||||
roles = [
|
load_roles = [
|
||||||
{
|
{
|
||||||
'name': 'teacher',
|
'name': 'teacher',
|
||||||
'description': 'Access users information, servers and groups without create/delete privileges',
|
'description': 'Access users information, servers and groups without create/delete privileges',
|
||||||
'scopes': ['users', 'groups'],
|
'scopes': ['users', 'groups'],
|
||||||
'users': ['cyclops', 'wolverine']
|
'users': ['cyclops', 'gandalf'],
|
||||||
}
|
'services': [],
|
||||||
]
|
'tokens': []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
All keys apart from 'name' are optional.
|
||||||
See all the available scopes in the JupyterHub REST API documentation.
|
See all the available scopes in the JupyterHub REST API documentation.
|
||||||
|
|
||||||
The default roles are in roles.py.
|
Default roles are defined in roles.py.
|
||||||
|
|
||||||
""",
|
""",
|
||||||
).tag(config=True)
|
).tag(config=True)
|
||||||
@@ -1823,6 +1826,8 @@ class JupyterHub(Application):
|
|||||||
async def init_roles(self):
|
async def init_roles(self):
|
||||||
"""Load default and predefined roles into the database"""
|
"""Load default and predefined roles into the database"""
|
||||||
db = self.db
|
db = self.db
|
||||||
|
role_bearers = ['users', 'services', 'tokens']
|
||||||
|
|
||||||
# load default roles
|
# load default roles
|
||||||
default_roles = roles.get_default_roles()
|
default_roles = roles.get_default_roles()
|
||||||
for role in default_roles:
|
for role in default_roles:
|
||||||
@@ -1831,43 +1836,31 @@ class JupyterHub(Application):
|
|||||||
# load predefined roles from config file
|
# load predefined roles from config file
|
||||||
for predef_role in self.load_roles:
|
for predef_role in self.load_roles:
|
||||||
roles.add_role(db, predef_role)
|
roles.add_role(db, predef_role)
|
||||||
role = orm.Role.find(db, predef_role['name'])
|
# add users, services and/or tokens
|
||||||
|
for bearer in role_bearers:
|
||||||
# handle users
|
if bearer in predef_role.keys():
|
||||||
if 'users' in predef_role.keys():
|
for bname in predef_role[bearer]:
|
||||||
for username in predef_role['users']:
|
if bearer == 'users':
|
||||||
username = self.authenticator.normalize_username(username)
|
bname = self.authenticator.normalize_username(bname)
|
||||||
if not (
|
if not (
|
||||||
await maybe_future(
|
await maybe_future(
|
||||||
self.authenticator.check_allowed(username, None)
|
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']
|
||||||
)
|
)
|
||||||
):
|
|
||||||
raise ValueError(
|
|
||||||
"Username %r is not in Authenticator.allowed_users"
|
|
||||||
% username
|
|
||||||
)
|
|
||||||
user = orm.User.find(db, name=username)
|
|
||||||
if user is None:
|
|
||||||
raise ValueError("%r does not exist" % username)
|
|
||||||
else:
|
|
||||||
roles.add_user(db, user=user, role=role)
|
|
||||||
|
|
||||||
# handle services
|
# make sure all users, services and tokens have at least one role (update with default)
|
||||||
if 'services' in predef_role.keys():
|
for bearer in role_bearers:
|
||||||
for servicename in predef_role['services']:
|
Class = roles.get_orm_class(bearer)
|
||||||
service = orm.Service.find(db, name=servicename)
|
for obj in db.query(Class):
|
||||||
if service is None:
|
|
||||||
raise ValueError("%r does not exist" % servicename)
|
|
||||||
else:
|
|
||||||
roles.add_user(db, user=service, role=role)
|
|
||||||
|
|
||||||
# make sure all users and services have at least one role (update with default)
|
|
||||||
Classes = [orm.User, orm.Service]
|
|
||||||
for ormClass in Classes:
|
|
||||||
for obj in db.query(ormClass):
|
|
||||||
if len(obj.roles) < 1:
|
if len(obj.roles) < 1:
|
||||||
roles.update_roles(db, obj)
|
roles.update_roles(db, obj=obj, kind=bearer)
|
||||||
|
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
async def _add_tokens(self, token_dict, kind):
|
async def _add_tokens(self, token_dict, kind):
|
||||||
|
@@ -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
|
||||||
@@ -453,6 +454,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')
|
||||||
self.db.commit()
|
self.db.commit()
|
||||||
user = self._user_from_orm(u)
|
user = self._user_from_orm(u)
|
||||||
return user
|
return user
|
||||||
@@ -722,6 +724,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
|
||||||
@@ -151,6 +154,18 @@ service_role_map = Table(
|
|||||||
Column('role_id', ForeignKey('roles.id', ondelete='CASCADE'), primary_key=True),
|
Column('role_id', ForeignKey('roles.id', ondelete='CASCADE'), primary_key=True),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 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):
|
class Role(Base):
|
||||||
"""User Roles"""
|
"""User Roles"""
|
||||||
@@ -162,6 +177,7 @@ class Role(Base):
|
|||||||
scopes = Column(JSONList)
|
scopes = Column(JSONList)
|
||||||
users = relationship('User', secondary='user_role_map', backref='roles')
|
users = relationship('User', secondary='user_role_map', backref='roles')
|
||||||
services = relationship('Service', secondary='service_role_map', backref='roles')
|
services = relationship('Service', secondary='service_role_map', backref='roles')
|
||||||
|
tokens = relationship('APIToken', secondary='api_token_role_map', backref='roles')
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "<%s %s (%s) - scopes: %s>" % (
|
return "<%s %s (%s) - scopes: %s>" % (
|
||||||
@@ -570,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,
|
||||||
@@ -598,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
|
||||||
|
|
||||||
|
@@ -1,12 +1,12 @@
|
|||||||
"""Roles utils"""
|
"""Roles utils"""
|
||||||
# Copyright (c) Jupyter Development Team.
|
# Copyright (c) Jupyter Development Team.
|
||||||
# Distributed under the terms of the Modified BSD License.
|
# Distributed under the terms of the Modified BSD License.
|
||||||
from .orm import Role
|
from . import orm
|
||||||
|
|
||||||
|
|
||||||
def get_default_roles():
|
def get_default_roles():
|
||||||
|
|
||||||
"""Returns a list of default roles dictionaries"""
|
"""Returns a list of default role dictionaries"""
|
||||||
|
|
||||||
default_roles = [
|
default_roles = [
|
||||||
{
|
{
|
||||||
@@ -43,50 +43,127 @@ def add_role(db, role_dict):
|
|||||||
|
|
||||||
"""Adds a new role to database or modifies an existing one"""
|
"""Adds a new role to database or modifies an existing one"""
|
||||||
|
|
||||||
role = Role.find(db, role_dict['name'])
|
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:
|
if role is None:
|
||||||
role = Role(
|
role = orm.Role(name=name, description=description, scopes=scopes,)
|
||||||
name=role_dict['name'],
|
|
||||||
description=role_dict['description'],
|
|
||||||
scopes=role_dict['scopes'],
|
|
||||||
)
|
|
||||||
db.add(role)
|
db.add(role)
|
||||||
else:
|
else:
|
||||||
role.description = role_dict['description']
|
if description:
|
||||||
role.scopes = role_dict['scopes']
|
role.description = description
|
||||||
role.users = []
|
if scopes:
|
||||||
role.services = []
|
role.scopes = scopes
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
|
|
||||||
def add_user(db, user, role):
|
def get_orm_class(kind):
|
||||||
if role is not None and role not in user.roles:
|
if kind == 'users':
|
||||||
user.roles.append(role)
|
Class = orm.User
|
||||||
db.commit()
|
elif kind == 'services':
|
||||||
|
Class = orm.Service
|
||||||
|
elif kind == 'tokens':
|
||||||
def remove_user(db, user, role):
|
Class = orm.APIToken
|
||||||
if role is not None and role in user.roles:
|
|
||||||
user.roles.remove(role)
|
|
||||||
db.commit()
|
|
||||||
|
|
||||||
|
|
||||||
def update_roles(db, user):
|
|
||||||
|
|
||||||
"""Updates roles if user has no role with default or when user admin status is changed"""
|
|
||||||
|
|
||||||
user_role = Role.find(db, 'user')
|
|
||||||
admin_role = Role.find(db, 'admin')
|
|
||||||
|
|
||||||
if user.admin:
|
|
||||||
if user_role in user.roles:
|
|
||||||
remove_user(db, user, user_role)
|
|
||||||
add_user(db, user, admin_role)
|
|
||||||
else:
|
else:
|
||||||
if admin_role in user.roles:
|
raise ValueError("kind must be users, services or tokens, not %r" % kind)
|
||||||
remove_user(db, user, admin_role)
|
|
||||||
# only add user role if the user has no other roles
|
return Class
|
||||||
if len(user.roles) < 1:
|
|
||||||
add_user(db, user, user_role)
|
|
||||||
db.commit()
|
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:
|
||||||
|
# FIXME - check if specified roles do not add permissions
|
||||||
|
# on top of the token owner's scopes
|
||||||
|
role = orm.Role.find(db, rolename)
|
||||||
|
if role:
|
||||||
|
role.tokens.append(obj)
|
||||||
|
else:
|
||||||
|
raise ValueError('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)
|
||||||
|
@@ -312,7 +312,7 @@ 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):
|
||||||
@@ -336,10 +336,10 @@ class MockHub(JupyterHub):
|
|||||||
user = self.db.query(orm.User).filter(orm.User.name == 'user').first()
|
user = self.db.query(orm.User).filter(orm.User.name == 'user').first()
|
||||||
if user is None:
|
if user is None:
|
||||||
user = orm.User(name='user')
|
user = orm.User(name='user')
|
||||||
user_role = orm.Role.find(self.db, 'user')
|
|
||||||
roles.add_user(self.db, user=user, role=user_role)
|
|
||||||
self.db.add(user)
|
self.db.add(user)
|
||||||
self.db.commit()
|
self.db.commit()
|
||||||
|
roles.update_roles(self.db, obj=user, kind='users')
|
||||||
|
self.db.commit()
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
super().stop()
|
super().stop()
|
||||||
|
@@ -10,8 +10,6 @@ from datetime import datetime
|
|||||||
import jupyterhub
|
import jupyterhub
|
||||||
from jupyterhub import orm
|
from jupyterhub import orm
|
||||||
|
|
||||||
# FIXME - for later versions of jupyterhub add code to test roles
|
|
||||||
|
|
||||||
|
|
||||||
def populate_db(url):
|
def populate_db(url):
|
||||||
"""Populate a jupyterhub database"""
|
"""Populate a jupyterhub database"""
|
||||||
|
@@ -70,7 +70,7 @@ async def test_referer_check(app):
|
|||||||
# add admin user
|
# 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, roles=['admin'])
|
user = add_user(app.db, name='admin', admin=True)
|
||||||
cookies = await app.login_user('admin')
|
cookies = await app.login_user('admin')
|
||||||
|
|
||||||
r = await api_request(
|
r = await api_request(
|
||||||
@@ -1159,7 +1159,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(
|
||||||
@@ -1252,7 +1252,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(
|
||||||
@@ -1269,6 +1269,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()
|
||||||
|
@@ -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
|
||||||
|
@@ -560,4 +560,3 @@ def test_expiring_oauth_code(app, user):
|
|||||||
assert orm_code in db.query(orm.OAuthCode)
|
assert orm_code in db.query(orm.OAuthCode)
|
||||||
orm.OAuthCode.purge_expired(db)
|
orm.OAuthCode.purge_expired(db)
|
||||||
assert orm_code not in db.query(orm.OAuthCode)
|
assert orm_code not in db.query(orm.OAuthCode)
|
||||||
|
|
||||||
|
@@ -1,48 +1,87 @@
|
|||||||
"""Test roles"""
|
"""Test roles"""
|
||||||
# import pytest
|
# Copyright (c) Jupyter Development Team.
|
||||||
|
# Distributed under the terms of the Modified BSD License.
|
||||||
|
import json
|
||||||
|
|
||||||
from pytest import mark
|
from pytest import mark
|
||||||
|
|
||||||
from .. import orm
|
from .. import orm
|
||||||
from .. import roles
|
from .. import roles
|
||||||
from ..utils import maybe_future
|
from ..utils import maybe_future
|
||||||
from .mocking import MockHub
|
from .mocking import MockHub
|
||||||
|
from .utils import add_user
|
||||||
|
from .utils import api_request
|
||||||
|
|
||||||
|
|
||||||
@mark.role
|
@mark.role
|
||||||
def test_orm_roles(db):
|
def test_orm_roles(db):
|
||||||
"""Test orm roles setup"""
|
"""Test orm roles setup"""
|
||||||
|
user_role = orm.Role.find(db, name='user')
|
||||||
|
if not user_role:
|
||||||
|
user_role = orm.Role(name='user')
|
||||||
|
db.add(user_role)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
|
service_role = orm.Role(name='service')
|
||||||
|
db.add(service_role)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
user = orm.User(name='falafel')
|
user = orm.User(name='falafel')
|
||||||
db.add(user)
|
db.add(user)
|
||||||
|
db.commit()
|
||||||
|
|
||||||
service = orm.Service(name='kebab')
|
service = orm.Service(name='kebab')
|
||||||
db.add(service)
|
db.add(service)
|
||||||
role = orm.Role(name='default')
|
|
||||||
db.add(role)
|
|
||||||
db.commit()
|
db.commit()
|
||||||
assert role.users == []
|
|
||||||
assert role.services == []
|
assert user_role.users == []
|
||||||
|
assert user_role.services == []
|
||||||
|
assert service_role.users == []
|
||||||
|
assert service_role.services == []
|
||||||
assert user.roles == []
|
assert user.roles == []
|
||||||
assert service.roles == []
|
assert service.roles == []
|
||||||
|
|
||||||
role.users.append(user)
|
user_role.users.append(user)
|
||||||
role.services.append(service)
|
service_role.services.append(service)
|
||||||
db.commit()
|
db.commit()
|
||||||
assert role.users == [user]
|
assert user_role.users == [user]
|
||||||
assert user.roles == [role]
|
assert user.roles == [user_role]
|
||||||
assert role.services == [service]
|
assert service_role.services == [service]
|
||||||
assert service.roles == [role]
|
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.delete(user)
|
||||||
db.commit()
|
db.commit()
|
||||||
assert role.users == []
|
assert user_role.users == []
|
||||||
db.delete(role)
|
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()
|
db.commit()
|
||||||
assert service.roles == []
|
assert service.roles == []
|
||||||
|
|
||||||
db.delete(service)
|
db.delete(service)
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|
||||||
|
|
||||||
@mark.role
|
@mark.role
|
||||||
def test_orm_role_delete_cascade(db):
|
def test_orm_roles_delete_cascade(db):
|
||||||
"""Orm roles cascade"""
|
"""Orm roles cascade"""
|
||||||
user1 = orm.User(name='user1')
|
user1 = orm.User(name='user1')
|
||||||
user2 = orm.User(name='user2')
|
user2 = orm.User(name='user2')
|
||||||
@@ -148,8 +187,6 @@ async def test_load_roles_users(tmpdir, request):
|
|||||||
hub.authenticator.admin_users = ['admin']
|
hub.authenticator.admin_users = ['admin']
|
||||||
hub.authenticator.allowed_users = ['cyclops', 'gandalf', 'bilbo', 'gargamel']
|
hub.authenticator.allowed_users = ['cyclops', 'gandalf', 'bilbo', 'gargamel']
|
||||||
await hub.init_users()
|
await hub.init_users()
|
||||||
for user in db.query(orm.User):
|
|
||||||
print(user.name)
|
|
||||||
await hub.init_roles()
|
await hub.init_roles()
|
||||||
|
|
||||||
# test if the 'user' role has been overwritten and assigned
|
# test if the 'user' role has been overwritten and assigned
|
||||||
@@ -178,6 +215,11 @@ async def test_load_roles_users(tmpdir, request):
|
|||||||
|
|
||||||
@mark.role
|
@mark.role
|
||||||
async def test_load_roles_services(tmpdir, request):
|
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 = [
|
roles_to_load = [
|
||||||
{
|
{
|
||||||
'name': 'culler',
|
'name': 'culler',
|
||||||
@@ -190,22 +232,20 @@ async def test_load_roles_services(tmpdir, request):
|
|||||||
ssl_enabled = getattr(request.module, "ssl_enabled", False)
|
ssl_enabled = getattr(request.module, "ssl_enabled", False)
|
||||||
if ssl_enabled:
|
if ssl_enabled:
|
||||||
kwargs['internal_certs_location'] = str(tmpdir)
|
kwargs['internal_certs_location'] = str(tmpdir)
|
||||||
hub = MockHub(test_clean_db=False, **kwargs)
|
hub = MockHub(**kwargs)
|
||||||
hub.init_db()
|
hub.init_db()
|
||||||
db = hub.db
|
db = hub.db
|
||||||
# add test services to db
|
# clean db of previous services and add testing ones
|
||||||
services = [
|
for service in db.query(orm.Service):
|
||||||
{'name': 'cull_idle', 'admin': False},
|
db.delete(service)
|
||||||
{'name': 'user_service', 'admin': False},
|
db.commit()
|
||||||
{'name': 'admin_service', 'admin': True},
|
for service in services:
|
||||||
]
|
orm_service = orm.Service.find(db, name=service['name'])
|
||||||
for service_specs in services:
|
if orm_service is None:
|
||||||
service = orm.Service.find(db, service_specs['name'])
|
# not found, create a new one
|
||||||
if service is None:
|
orm_service = orm.Service(name=service['name'])
|
||||||
service = orm.Service(
|
db.add(orm_service)
|
||||||
name=service_specs['name'], admin=service_specs['admin']
|
orm_service.admin = service.get('admin', False)
|
||||||
)
|
|
||||||
db.add(service)
|
|
||||||
db.commit()
|
db.commit()
|
||||||
await hub.init_roles()
|
await hub.init_roles()
|
||||||
|
|
||||||
@@ -224,3 +264,104 @@ async def test_load_roles_services(tmpdir, request):
|
|||||||
cull_idle = orm.Service.find(db, name='cull_idle')
|
cull_idle = orm.Service.find(db, name='cull_idle')
|
||||||
assert culler_role in cull_idle.roles
|
assert culler_role in cull_idle.roles
|
||||||
assert user_role not 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),
|
||||||
|
# FIXME - add requesting token with 'not allowed' role
|
||||||
|
# granting more permission than the token owner has
|
||||||
|
],
|
||||||
|
)
|
||||||
|
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']})
|
||||||
|
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
|
||||||
|
@@ -6,6 +6,7 @@ from certipy import Certipy
|
|||||||
|
|
||||||
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
|
||||||
|
|
||||||
|
|
||||||
@@ -101,6 +102,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:
|
||||||
|
Reference in New Issue
Block a user