mirror of
https://github.com/jupyterhub/jupyterhub.git
synced 2025-10-17 23:13:00 +00:00
tokens have scopes
instead of roles, which allow tokens to change permissions over time This is mostly a low-level change, with little outward-facing effects. - on upgrade, evaluate all token role assignments to their current scopes, and store those scopes on the tokens - assigning roles to tokens still works, but scopes are evaluated and validated immediately, rather than lazily stored as roles - no longer need to check for role permission changes on startup, because token permissions aren't affected - move a few scope utilities from roles to scopes - oauth allows specifying scopes, not just roles. But these are still at the level specified in roles, not fully-resolved scopes. - more granular APIs for working with scopes and roles
This commit is contained in:
@@ -3,7 +3,6 @@
|
||||
# Distributed under the terms of the Modified BSD License.
|
||||
import enum
|
||||
import json
|
||||
import warnings
|
||||
from base64 import decodebytes
|
||||
from base64 import encodebytes
|
||||
from datetime import datetime
|
||||
@@ -40,10 +39,7 @@ from sqlalchemy.types import Text
|
||||
from sqlalchemy.types import TypeDecorator
|
||||
from tornado.log import app_log
|
||||
|
||||
from .roles import assign_default_roles
|
||||
from .roles import create_role
|
||||
from .roles import get_default_roles
|
||||
from .roles import update_roles
|
||||
from .roles import roles_to_scopes
|
||||
from .utils import compare_token
|
||||
from .utils import hash_token
|
||||
from .utils import new_token
|
||||
@@ -110,7 +106,9 @@ class JSONList(JSONDict):
|
||||
return value
|
||||
|
||||
def process_result_value(self, value, dialect):
|
||||
if value is not None:
|
||||
if value is None:
|
||||
return []
|
||||
else:
|
||||
value = json.loads(value)
|
||||
return value
|
||||
|
||||
@@ -157,9 +155,7 @@ for has_role in (
|
||||
'user',
|
||||
'group',
|
||||
'service',
|
||||
'api_token',
|
||||
'oauth_client',
|
||||
'oauth_code',
|
||||
):
|
||||
role_map = Table(
|
||||
f'{has_role}_role_map',
|
||||
@@ -185,10 +181,9 @@ class Role(Base):
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
name = Column(Unicode(255), unique=True)
|
||||
description = Column(Unicode(1023))
|
||||
scopes = Column(JSONList)
|
||||
scopes = Column(JSONList, default=[])
|
||||
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')
|
||||
groups = relationship('Group', secondary='group_role_map', backref='roles')
|
||||
|
||||
def __repr__(self):
|
||||
@@ -597,6 +592,10 @@ class APIToken(Hashed, Base):
|
||||
def api_id(self):
|
||||
return 'a%i' % self.id
|
||||
|
||||
@property
|
||||
def owner(self):
|
||||
return self.user or self.service
|
||||
|
||||
# added in 2.0
|
||||
client_id = Column(
|
||||
Unicode(255),
|
||||
@@ -624,6 +623,7 @@ class APIToken(Hashed, Base):
|
||||
expires_at = Column(DateTime, default=None, nullable=True)
|
||||
last_activity = Column(DateTime)
|
||||
note = Column(Unicode(1023))
|
||||
scopes = Column(JSONList, default=[])
|
||||
|
||||
def __repr__(self):
|
||||
if self.user is not None:
|
||||
@@ -676,9 +676,11 @@ class APIToken(Hashed, Base):
|
||||
def new(
|
||||
cls,
|
||||
token=None,
|
||||
*,
|
||||
user=None,
|
||||
service=None,
|
||||
roles=None,
|
||||
scopes=None,
|
||||
note='',
|
||||
generated=True,
|
||||
session_id=None,
|
||||
@@ -697,6 +699,42 @@ class APIToken(Hashed, Base):
|
||||
generated = True
|
||||
else:
|
||||
cls.check_token(db, token)
|
||||
|
||||
if scopes is not None and roles is not None:
|
||||
raise ValueError(
|
||||
"Can only assign one of scopes or roles when creating tokens."
|
||||
)
|
||||
|
||||
elif scopes is None and roles is None:
|
||||
# this is the default branch
|
||||
# use the default 'token' role to specify default permissions for API tokens
|
||||
default_token_role = Role.find(db, 'token')
|
||||
if not default_token_role:
|
||||
scopes = ["inherit"]
|
||||
else:
|
||||
scopes = roles_to_scopes([default_token_role])
|
||||
elif roles is not None:
|
||||
# evaluate roles to scopes immediately
|
||||
# TODO: should this be deprecated, or not?
|
||||
# warnings.warn(
|
||||
# "Setting roles on tokens is deprecated in JupyterHub 2.2. Use scopes.",
|
||||
# DeprecationWarning,
|
||||
# stacklevel=3,
|
||||
# )
|
||||
orm_roles = []
|
||||
for rolename in roles:
|
||||
role = Role.find(db, name=rolename)
|
||||
if role is None:
|
||||
raise ValueError(f"No such role: {rolename}")
|
||||
orm_roles.append(role)
|
||||
scopes = roles_to_scopes(orm_roles)
|
||||
|
||||
# avoid circular import
|
||||
from .scopes import _check_scopes_exist, _check_token_scopes
|
||||
|
||||
_check_scopes_exist(scopes, who_for="token")
|
||||
_check_token_scopes(scopes, owner=user or service)
|
||||
|
||||
# two stages to ensure orm_token.generated has been set
|
||||
# before token setter is called
|
||||
orm_token = cls(
|
||||
@@ -704,6 +742,7 @@ class APIToken(Hashed, Base):
|
||||
note=note or '',
|
||||
client_id=client_id,
|
||||
session_id=session_id,
|
||||
scopes=list(scopes),
|
||||
)
|
||||
orm_token.token = token
|
||||
if user:
|
||||
@@ -716,21 +755,17 @@ class APIToken(Hashed, Base):
|
||||
orm_token.expires_at = cls.now() + timedelta(seconds=expires_in)
|
||||
|
||||
db.add(orm_token)
|
||||
if not Role.find(db, 'token'):
|
||||
raise RuntimeError("Default token role has not been created")
|
||||
try:
|
||||
if roles is not None:
|
||||
update_roles(db, entity=orm_token, roles=roles)
|
||||
else:
|
||||
assign_default_roles(db, entity=orm_token)
|
||||
except Exception:
|
||||
db.delete(orm_token)
|
||||
db.commit()
|
||||
raise
|
||||
|
||||
db.commit()
|
||||
return token
|
||||
|
||||
def update_scopes(self, new_scopes):
|
||||
"""Set new scopes, checking that they are allowed"""
|
||||
from .scopes import _check_scopes_exist, _check_token_scopes
|
||||
|
||||
_check_scopes_exist(new_scopes, who_for="token")
|
||||
_check_token_scopes(new_scopes, owner=self.owner)
|
||||
self.scopes = new_scopes
|
||||
|
||||
|
||||
class OAuthCode(Expiring, Base):
|
||||
__tablename__ = 'oauth_codes'
|
||||
@@ -746,7 +781,7 @@ class OAuthCode(Expiring, Base):
|
||||
# state = Column(Unicode(1023))
|
||||
user_id = Column(Integer, ForeignKey('users.id', ondelete='CASCADE'))
|
||||
|
||||
roles = relationship('Role', secondary='oauth_code_role_map')
|
||||
scopes = Column(JSONList, default=[])
|
||||
|
||||
@staticmethod
|
||||
def now():
|
||||
|
Reference in New Issue
Block a user