e2x-hub-rbac
Role-Based Access Control (RBAC) for JupyterHub within the e2x ecosystem, supporting hierarchical scopes (Hub, Course, Term) based on JupyterHub groups.
Overview
e2x-hub-rbac provides role-based access control for JupyterHub with two main capabilities:
- Permission Checking: Maps JupyterHub group memberships to predefined roles and evaluates permissions.
- Membership Management: Provides an API to add, remove, and list users in roles with automatic permission checking.
The core idea:
- A user's JupyterHub groups are parsed into role assignments at a specific scope (Hub, Course, or Term).
- Consuming packages define permissions and a role → permissions mapping.
- The
PermissionChecker(or therequire_permissiondecorator) evaluates whether a user's roles grant a requested permission within the given scope. - The
MembershipAPImanages user-role assignments via a backend (e.g., JupyterHub's group API).
Predefined Roles
Roles are fixed and ship with this package.
| Role | Scope | Group name format |
|---|---|---|
HUB_ADMIN |
Hub | hub.hub_admin |
COURSE_CREATOR |
Hub | hub.course_creator |
COURSE_OWNER |
Course | course.{course_id}.course_owner |
INSTRUCTOR |
Term | term.{course_id}.{term_id}.instructor |
TEACHING_ASSISTANT |
Term | term.{course_id}.{term_id}.teaching_assistant |
OBSERVER |
Term | term.{course_id}.{term_id}.observer |
STUDENT |
Term | term.{course_id}.{term_id}.student |
Group Names
JupyterHub group memberships are automatically parsed into role assignments based on a structured naming convention. Each group name encodes the role scope and identifiers.
Format
Group names follow these patterns:
- Hub-level roles:
hub.<role_name> - Course-level roles:
course.<course_id>.<role_name> - Term-level roles:
term.<course_id>.<term_id>.<role_name>
Examples
hub.hub_admin # Hub admin (global access)
hub.course_creator # Can create courses (global)
course.math101.course_owner # Owner of course math101
term.math101.2024ws.instructor # Instructor for math101 in 2024ws
term.math101.2024ws.teaching_assistant # TA for math101 in 2024ws
term.cs101.2024ss.student # Student in cs101 for 2024ss
term.physics201.2025ws.observer # Observer in physics201 for 2025ws
Parsing Rules
- Group names are case-sensitive and use dot (
.) as the separator. - Only group names matching the expected formats are parsed; others are silently ignored.
- The role name must exactly match one of the predefined roles at the correct scope.
- Course IDs and term IDs can contain any characters except dots.
Invalid Examples
These group names will be ignored during parsing:
admin # Missing scope prefix
hub.invalid_role # Unknown role name
course.math101 # Missing role name
term.math101.instructor # Missing term_id
hub.math101.student # Wrong scope for student role
Permission Resolution
- Hub roles apply globally to any resource.
- Course roles apply to their course and all terms within it.
- Term roles apply only to their specific course + term combination.
📦 Installation
From PyPI
pip install e2x-hub-rbac
From Source
git clone https://github.com/Digiklausur/e2x-hub-rbac.git
cd e2x-hub-rbac
pip install -e .
Architecture
This package provides two main components:
- Permission System: Check if users have specific permissions based on their role assignments
- Membership API: Manage user memberships in courses and terms (add/remove users from roles)
Usage
1. Define your permissions
PermissionProtocol is a structural protocol — implement it with class-level code and required_scope attributes.
from e2x_hub_rbac.auth import Scope, PermissionProtocol, Role, RolePermissions
class Permission(PermissionProtocol):
code = "view_profile"
required_scope = Scope.TERM
ROLE_PERMISSIONS: RolePermissions = {
Role.HUB_ADMIN: frozenset({Permission}),
Role.COURSE_CREATOR: frozenset(),
Role.COURSE_OWNER: frozenset({Permission}),
Role.INSTRUCTOR: frozenset({Permission}),
Role.TEACHING_ASSISTANT: frozenset({Permission}),
Role.OBSERVER: frozenset({Permission}),
Role.STUDENT: frozenset({Permission}),
}
Note:
ROLE_PERMISSIONSmust include an entry for everyRolevalue, because the checker looks up each of the user's assigned roles in this mapping.
2. Check permissions directly
from e2x_hub_rbac.auth import UserLike, PermissionChecker
from dataclasses import dataclass
@dataclass
class User:
"""Example user representation."""
username: str
groups: list[str]
alice = User(username="alice", groups=["term.math101.2024ws.student"])
checker = PermissionChecker(alice, ROLE_PERMISSIONS)
checker.has_permission(Permission, course_id="math101", term_id="2024ws") # True
checker.has_permission(Permission, course_id="cs101", term_id="2024ws") # False
3. Use the decorator with BaseAPI
Extend BaseAPI and annotate methods with @require_permission. The decorator resolves user, course_id, and term_id from the method arguments by name.
from e2x_hub_rbac.api import BaseAPI
from e2x_hub_rbac.auth import UserLike, require_permission
class MyAPI(BaseAPI):
def __init__(self):
super().__init__(role_permissions=ROLE_PERMISSIONS)
@require_permission(Permission)
def get_profile(self, user, course_id, term_id):
return {"profile": "data"}
api.get_profile(alice, "math101", "2024ws") # succeeds
api.get_profile(alice, "cs101", "2024ws") # raises APIPermissionError (403)
APIPermissionError is RFC 9457-compliant and carries status_code = 403.
Managing Memberships
The MembershipAPI provides methods to add, remove, and list users in various roles. It requires a backend implementation of the GroupBackend protocol.
Backend Setup
The package includes a HubAPI backend for JupyterHub:
from e2x_hub_rbac.backend import HubAPI
from e2x_hub_rbac.api import MembershipAPI
# Initialize the JupyterHub backend
hub_backend = HubAPI(
api_token="your-jupyterhub-api-token",
api_url="https://your-hub.example.com/hub/api"
)
# Create the membership API
membership_api = MembershipAPI(
group_backend=hub_backend,
add_users_to_hub=True # Automatically create users if they don't exist
)
Hub-Level Operations
Manage hub administrators and course creators:
from e2x_hub_rbac.auth import UserLike
# Admin user who can manage memberships
admin = User(username="admin", groups=["hub.hub_admin"])
# Add/remove hub admins
await membership_api.add_hub_admins(admin, ["user1", "user2"])
await membership_api.remove_hub_admins(admin, ["user1"])
admins = await membership_api.list_hub_admins(admin)
# Add/remove course creators
await membership_api.add_course_creators(admin, ["instructor1"])
await membership_api.remove_course_creators(admin, ["instructor1"])
creators = await membership_api.list_course_creators(admin)
Course-Level Operations
Manage course owners:
# Add/remove course owners
await membership_api.add_course_owners(admin, "math101", ["prof_smith"])
await membership_api.remove_course_owners(admin, "math101", ["prof_smith"])
owners = await membership_api.list_course_owners(admin, "math101")
Term-Level Operations
Manage instructors, teaching assistants, observers, and students:
course_id = "math101"
term_id = "2024ws"
# Instructors
await membership_api.add_instructors(admin, course_id, term_id, ["instructor1"])
await membership_api.remove_instructors(admin, course_id, term_id, ["instructor1"])
instructors = await membership_api.list_instructors(admin, course_id, term_id)
# Teaching Assistants
await membership_api.add_teaching_assistants(admin, course_id, term_id, ["ta1", "ta2"])
await membership_api.remove_teaching_assistants(admin, course_id, term_id, ["ta1"])
tas = await membership_api.list_teaching_assistants(admin, course_id, term_id)
# Observers
await membership_api.add_observers(admin, course_id, term_id, ["observer1"])
await membership_api.remove_observers(admin, course_id, term_id, ["observer1"])
observers = await membership_api.list_observers(admin, course_id, term_id)
# Students
await membership_api.add_students(admin, course_id, term_id, ["alice", "bob"])
await membership_api.remove_students(admin, course_id, term_id, ["alice"])
students = await membership_api.list_students(admin, course_id, term_id)
Permission-Based Access Control
All membership operations are protected by permissions. Different roles can perform different operations:
| Operation | Required Permission | Who Can Do It |
|---|---|---|
| Manage hub admins | Hub-scoped | Hub admins only |
| Manage course creators | Hub-scoped | Hub admins only |
| Manage course owners | Course-scoped | Hub admins, course owners |
| Manage instructors | Term-scoped | Hub admins, course owners, instructors |
| Manage TAs | Term-scoped | Hub admins, course owners, instructors |
| Manage observers | Term-scoped | Hub admins, course owners, instructors |
| Manage students | Term-scoped | Hub admins, course owners, instructors, TAs |
| List members | Term-scoped | Hub admins, course owners, instructors, TAs, observers |
Example of permission checking:
# Course owner can manage their course
course_owner = User(username="prof", groups=["course.math101.course_owner"])
await membership_api.add_students(course_owner, "math101", "2024ws", ["student1"]) # ✓ Succeeds
# But cannot manage a different course
await membership_api.add_students(course_owner, "cs101", "2024ws", ["student1"]) # ✗ Raises APIPermissionError
# Teaching assistant can add students
ta = User(username="ta", groups=["term.math101.2024ws.teaching_assistant"])
await membership_api.add_students(ta, "math101", "2024ws", ["student2"]) # ✓ Succeeds
# But cannot remove instructors
await membership_api.remove_instructors(ta, "math101", "2024ws", ["instructor1"]) # ✗ Raises APIPermissionError
Custom Backend Implementation
You can implement your own backend by implementing the GroupBackend protocol:
from e2x_hub_rbac.backend.protocol import GroupBackend
class CustomBackend(GroupBackend):
async def ensure_group_exists(self, group_name: str, create_if_missing: bool) -> None:
# Your implementation
...
async def ensure_users_exist(self, usernames: list[str], create_if_missing: bool) -> None:
# Your implementation
...
async def add_users_to_group(self, group_name: str, usernames: list[str]) -> None:
# Your implementation
...
async def remove_users_from_group(self, group_name: str, usernames: list[str]) -> None:
# Your implementation
...
async def get_group_members(self, group_name: str) -> list[str]:
# Your implementation
...
🛠️ Development
Setup
git clone https://github.com/Digiklausur/e2x-hub-rbac.git
cd e2x-hub-rbac
pip install -e ".[dev]"
pre-commit install
Running Tests
pip install -e ".[test]"
pytest
The test suite includes:
- Permission checker tests
- Decorator tests
- MembershipAPI tests (requires
pytest-asyncio) - RBAC tests
📜 License
This project is licensed under the MIT License - see the LICENSE file for details.
🤝 Contributing
Contributions are welcome! Please open an issue or submit a pull request.
📧 Contact
For questions or support, please contact Tim Metzler.
🔗 Links
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file e2x_hub_rbac-0.1.0.tar.gz.
File metadata
- Download URL: e2x_hub_rbac-0.1.0.tar.gz
- Upload date:
- Size: 22.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
33dda4d5e1c0bd441b1a367268b89cbcf3ce94882c52dab418f54b7b84760a15
|
|
| MD5 |
15aad3a757076deb4b9b6c0e9fd39583
|
|
| BLAKE2b-256 |
cc63fa13bd80e030289f30dce979b2391238d24fc23ba7164fa31bf0eea4cd61
|
Provenance
The following attestation bundles were made for e2x_hub_rbac-0.1.0.tar.gz:
Publisher:
python-publish.yml on DigiKlausur/e2x-hub-rbac
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
e2x_hub_rbac-0.1.0.tar.gz -
Subject digest:
33dda4d5e1c0bd441b1a367268b89cbcf3ce94882c52dab418f54b7b84760a15 - Sigstore transparency entry: 2300578647
- Sigstore integration time:
-
Permalink:
DigiKlausur/e2x-hub-rbac@73e4740a62b53e50cc5b7f91fdc314b80e5d52c2 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/DigiKlausur
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
python-publish.yml@73e4740a62b53e50cc5b7f91fdc314b80e5d52c2 -
Trigger Event:
release
-
Statement type:
File details
Details for the file e2x_hub_rbac-0.1.0-py3-none-any.whl.
File metadata
- Download URL: e2x_hub_rbac-0.1.0-py3-none-any.whl
- Upload date:
- Size: 18.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8508f6fdf0757b9735003613cdd2fd90f5b015b1c7bf3ff7a42c9fde0936688d
|
|
| MD5 |
c183006c0822091549f8354dcec23de2
|
|
| BLAKE2b-256 |
fed75b6a93beb4e2d3a6f64f3af8de2b2b8d0caf7f519e477543b85c5f7f8616
|
Provenance
The following attestation bundles were made for e2x_hub_rbac-0.1.0-py3-none-any.whl:
Publisher:
python-publish.yml on DigiKlausur/e2x-hub-rbac
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
e2x_hub_rbac-0.1.0-py3-none-any.whl -
Subject digest:
8508f6fdf0757b9735003613cdd2fd90f5b015b1c7bf3ff7a42c9fde0936688d - Sigstore transparency entry: 2300578651
- Sigstore integration time:
-
Permalink:
DigiKlausur/e2x-hub-rbac@73e4740a62b53e50cc5b7f91fdc314b80e5d52c2 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/DigiKlausur
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
python-publish.yml@73e4740a62b53e50cc5b7f91fdc314b80e5d52c2 -
Trigger Event:
release
-
Statement type: