A multi-user server for Jupyter notebooks that provides authentication, spawning, and proxying for multiple users simultaneously
—
JupyterHub implements a comprehensive Role-Based Access Control (RBAC) system with fine-grained permissions through scopes. The system supports roles that can be assigned to users, groups, services, and API tokens, providing flexible authorization for all JupyterHub operations.
Core functions for managing roles in the RBAC system.
def get_default_roles() -> Dict[str, Dict[str, Any]]:
"""
Get the default role definitions for JupyterHub.
Returns:
Dictionary of default roles with their scopes and descriptions:
- 'user': Basic user permissions
- 'admin': Full system administrator permissions
- 'server': Permissions for single-user servers
- 'token': Permissions for API tokens
"""
# Default roles structure
DEFAULT_ROLES = {
'user': {
'description': 'Default permissions for users',
'scopes': [
'self',
'servers!user',
'access:servers!user'
]
},
'admin': {
'description': 'Unrestricted administrative privileges',
'scopes': ['admin:*']
},
'server': {
'description': 'Permissions for single-user servers',
'scopes': [
'access:servers!user',
'read:users:activity!user',
'read:servers!user'
]
},
'token': {
'description': 'Default permissions for API tokens',
'scopes': ['identify']
}
}Complete scope system defining all available permissions in JupyterHub.
# Dictionary of all available scopes with descriptions
scope_definitions: Dict[str, str] = {
# Administrative scopes
'admin:*': 'Full administrative privileges (all permissions)',
'admin:users': 'Full user administration',
'admin:groups': 'Full group administration',
'admin:servers': 'Full server administration',
'admin:auth': 'Authentication administration',
# User management scopes
'users': 'Full user management (read/write)',
'users:activity': 'Read user activity information',
'read:users': 'Read user information',
'read:users:name': 'Read usernames',
'read:users:groups': 'Read user group membership',
'read:users:activity': 'Read user activity timestamps',
# Group management scopes
'groups': 'Full group management (read/write)',
'read:groups': 'Read group information',
'read:groups:name': 'Read group names',
# Server management scopes
'servers': 'Full server management (read/write/delete)',
'read:servers': 'Read server information',
'access:servers': 'Access servers (connect to running servers)',
'delete:servers': 'Stop/delete servers',
# Service scopes
'services': 'Full service management',
'read:services': 'Read service information',
# Role and token scopes
'roles': 'Full role management',
'read:roles': 'Read role information',
'tokens': 'Full token management',
'read:tokens': 'Read token information',
# Proxy scopes
'proxy': 'Full proxy management',
'read:proxy': 'Read proxy routing information',
# Hub scopes
'read:hub': 'Read hub information',
'shutdown': 'Shutdown the hub',
# Special scopes
'self': 'User\'s own resources (expands to user-specific scopes)',
'identify': 'Identify the token owner',
'inherit': 'Inherit permissions from user/service'
}
# Scope expansion functions
def expand_scopes(*scopes) -> Set[str]:
"""
Expand scope patterns into concrete scopes.
Args:
*scopes: Variable number of scope strings
Returns:
Set of expanded concrete scopes
"""
def check_scopes(required_scopes, available_scopes) -> bool:
"""
Check if available scopes satisfy required scopes.
Args:
required_scopes: Scopes needed for operation
available_scopes: Scopes granted to user/token
Returns:
True if access should be granted
"""
def describe_scope(scope: str) -> str:
"""
Get human-readable description of a scope.
Args:
scope: Scope string
Returns:
Description string
"""Functions for assigning roles and managing permission inheritance.
class Role(Base):
"""Role database model with permission management"""
name: str
description: str
scopes: List[str]
def has_scope(self, scope: str) -> bool:
"""
Check if role has specific scope.
Args:
scope: Scope to check
Returns:
True if role grants this scope
"""
def grant_scope(self, scope: str) -> None:
"""
Grant a scope to this role.
Args:
scope: Scope to grant
"""
def revoke_scope(self, scope: str) -> None:
"""
Revoke a scope from this role.
Args:
scope: Scope to revoke
"""
# Permission inheritance functions
def get_user_scopes(user: User) -> Set[str]:
"""
Get all scopes for a user (direct + inherited from groups/roles).
Args:
user: User object
Returns:
Set of all scopes available to user
"""
def get_token_scopes(token: APIToken) -> Set[str]:
"""
Get all scopes for an API token.
Args:
token: API token object
Returns:
Set of scopes granted to token
"""from jupyterhub.orm import User, Group, Role
from jupyterhub.scopes import get_default_roles
# Create custom role
instructor_role = Role(
name='instructor',
description='Course instructor permissions',
scopes=[
'read:users',
'read:users:activity',
'servers!group=students',
'access:servers!group=students'
]
)
db.add(instructor_role)
# Assign role to user
instructor = db.query(User).filter(User.name == 'prof_smith').first()
instructor.roles.append(instructor_role)
db.commit()# Create student group with role
students_group = Group(name='students')
student_role = Role(
name='student',
description='Student permissions',
scopes=[
'self', # Own resources
'read:groups:name!group=students' # Read group member names
]
)
students_group.roles.append(student_role)
# Add users to group
alice = db.query(User).filter(User.name == 'alice').first()
students_group.users.append(alice)
db.commit()# Create service with specific permissions
monitoring_service = Service(
name='monitoring',
url='http://localhost:8002'
)
monitoring_role = Role(
name='monitoring-service',
description='Monitoring service permissions',
scopes=[
'read:users:activity',
'read:servers',
'read:hub'
]
)
monitoring_service.roles.append(monitoring_role)
# Create API token for service
token = monitoring_service.new_api_token(
note='Monitoring service token',
roles=['monitoring-service']
)
db.commit()# Scoped permissions using filters
custom_scopes = [
'servers!user=alice', # Alice's servers only
'read:users!group=students', # Users in students group
'access:servers!server=shared-*', # Servers with shared- prefix
'admin:users!user!=admin' # All users except admin
]
# Create role with scoped permissions
ta_role = Role(
name='teaching-assistant',
description='TA permissions for specific course',
scopes=[
'read:users!group=cs101-students',
'servers!group=cs101-students',
'read:users:activity!group=cs101-students'
]
)# Create limited-scope API token
user = db.query(User).filter(User.name == 'researcher').first()
limited_token = user.new_api_token(
note='Data analysis token',
scopes=[
'read:servers!user', # Only own servers
'access:servers!user', # Access own servers
'read:users:name' # Read usernames for sharing
],
expires_in=3600 # 1 hour expiration
)# Create hierarchy of roles
roles_hierarchy = {
'student': [
'self',
'read:groups:name'
],
'ta': [
'inherit:student', # Inherit student permissions
'read:users!group=assigned-students',
'servers!group=assigned-students'
],
'instructor': [
'inherit:ta', # Inherit TA permissions
'admin:users!group=course-students',
'admin:servers!group=course-students'
],
'admin': [
'admin:*' # Full admin access
]
}
# Apply hierarchical roles
for role_name, scopes in roles_hierarchy.items():
role = Role(name=role_name, scopes=scopes)
db.add(role)class ConditionalRole(Role):
"""Role with time-based or context-based conditions"""
conditions: Dict[str, Any] # Additional conditions
def check_conditions(self, context: Dict[str, Any]) -> bool:
"""
Check if role conditions are met.
Args:
context: Current request context
Returns:
True if conditions are satisfied
"""
# Time-based conditions
if 'time_range' in self.conditions:
current_time = datetime.now().time()
start, end = self.conditions['time_range']
if not (start <= current_time <= end):
return False
# IP-based conditions
if 'allowed_ips' in self.conditions:
client_ip = context.get('client_ip')
if client_ip not in self.conditions['allowed_ips']:
return False
return True
# Usage with conditions
lab_access_role = ConditionalRole(
name='lab-access',
scopes=['access:servers!server=lab-*'],
conditions={
'time_range': (time(9, 0), time(17, 0)), # 9 AM - 5 PM
'allowed_ips': ['192.168.1.0/24'] # Lab network only
}
)async def assign_dynamic_roles(user: User, authenticator_data: Dict[str, Any]):
"""
Dynamically assign roles based on authentication data.
Args:
user: User object
authenticator_data: Data from authenticator (LDAP groups, OAuth claims, etc.)
"""
# Clear existing dynamic roles
user.roles = [role for role in user.roles if not role.name.startswith('dynamic-')]
# Assign roles based on LDAP groups
ldap_groups = authenticator_data.get('groups', [])
role_mapping = {
'faculty': 'instructor',
'graduate_students': 'ta',
'undergraduate_students': 'student'
}
for ldap_group, role_name in role_mapping.items():
if ldap_group in ldap_groups:
role = db.query(Role).filter(Role.name == role_name).first()
if role:
user.roles.append(role)
db.commit()from jupyterhub.scopes import needs_scope
class CustomAPIHandler(APIHandler):
"""API handler with scope-based permissions"""
@needs_scope('read:users')
def get(self):
"""Endpoint requiring read:users scope"""
users = self.db.query(User).all()
self.write({'users': [user.name for user in users]})
@needs_scope('admin:users')
def post(self):
"""Endpoint requiring admin:users scope"""
user_data = self.get_json_body()
user = User(**user_data)
self.db.add(user)
self.db.commit()from jupyterhub.scopes import check_scopes
def check_user_permission(user: User, required_scope: str, resource: str = None) -> bool:
"""
Check if user has permission for specific operation.
Args:
user: User object
required_scope: Scope needed for operation
resource: Optional resource identifier
Returns:
True if user has permission
"""
user_scopes = get_user_scopes(user)
# Add resource context if provided
if resource:
required_scope = f"{required_scope}!{resource}"
return check_scopes([required_scope], user_scopes)
# Usage
if check_user_permission(user, 'servers', 'user=alice'):
# User can manage Alice's servers
pass# jupyterhub_config.py
# Define custom roles
c.JupyterHub.custom_roles = [
{
'name': 'instructor',
'description': 'Course instructor permissions',
'scopes': [
'read:users!group=students',
'servers!group=students',
'admin:users!group=students'
]
},
{
'name': 'grader',
'description': 'Grading assistant permissions',
'scopes': [
'read:users!group=students',
'access:servers!group=students'
]
}
]
# Load balancing with role-based spawning
c.JupyterHub.load_roles = [
{
'name': 'instructor',
'users': ['prof_smith', 'prof_jones'],
'scopes': ['inherit:instructor']
},
{
'name': 'grader',
'groups': ['teaching-assistants'],
'scopes': ['inherit:grader']
}
]Install with Tessl CLI
npx tessl i tessl/pypi-jupyterhub