e2x-hub-rbac
e2x-hub-rbac provides the management and authorization layer for LMS-like applications running on JupyterHub within the e2x ecosystem.
It connects JupyterHub's users and groups with the concepts an LMS needs to manage courses, terms, and participants. Rather than implementing a complete LMS itself, the package provides the common infrastructure for managing who can do what, and where.
A typical setup looks roughly like this:
┌─────────────────────────┐
│ JupyterHub │
│ │
│ Users + Groups │
└────────────┬────────────┘
│
│
┌────────────▼────────────┐
│ e2x-hub-rbac │
│ │
│ Roles & Permissions │
│ Scope Resolution │
│ Membership Management │
└────────────┬────────────┘
│
┌────────────────┼────────────────┐
│ │ │
┌────────▼───────┐ ┌──────▼──────┐ ┌──────▼──────┐
│ LMS / Course │ │ Assessment │ │ Other e2x │
│ Management │ │ Services │ │ Services │
└────────────────┘ └─────────────┘ └─────────────┘
The idea is to use JupyterHub as the user and group management infrastructure, while e2x-hub-rbac adds the LMS-specific concepts needed by applications built on top of it.
For example, an LMS-like application may need to answer questions such as:
- Is this user an LMS administrator?
- Can this user create a course?
- Is this user the owner of course
math101? - Is this user an instructor for
math101during the2024wsterm? - Can this teaching assistant add students to that term?
- Can this observer view the members of a term?
e2x-hub-rbac provides a common way to represent and enforce these relationships.
What does it provide?
The package has two closely related responsibilities:
- Authorization — Translate JupyterHub group memberships into roles and determine whether a role grants a requested permission.
- Membership management — Provide an API for managing those roles by adding and removing users from the corresponding JupyterHub groups.
The package deliberately does not try to be a complete LMS. Course data, terms, assessments, content, and other domain-specific functionality remain the responsibility of consuming applications.
Instead, e2x-hub-rbac provides the shared management layer that applications can build upon.
The RBAC Model
The authorization model is based on three hierarchical scopes:
- LMS — Global permissions across the LMS.
- Course — Permissions for a specific course and all of its terms.
- Term — Permissions for a specific course and term.
Users receive roles through JupyterHub group memberships. For example:
lms.lms-admin
lms.course-creator
lms.course.math101.course-owner
lms.course.math101.term.2024ws.instructor
lms.course.math101.term.2024ws.student
These groups are interpreted as role assignments:
LMS
└── LMS_ADMIN
LMS
└── COURSE_CREATOR
Course: math101
└── COURSE_OWNER
Course: math101
└── Term: 2024ws
└── INSTRUCTOR
Course: math101
└── Term: 2024ws
└── STUDENT
Consuming applications then define which permissions each role provides.
For example, an application might define:
class ViewProfile(PermissionProtocol):
code = "view_profile"
required_scope = Scope.TERM
and decide that INSTRUCTOR, TEACHING_ASSISTANT, and STUDENT can exercise that permission within their respective terms.
This separation is intentional:
e2x-hub-rbacdefines the roles and authorization machinery.- The consuming application defines its permissions and what each role is allowed to do.
- JupyterHub stores the users and group memberships.
Why use it?
An LMS-like application running on JupyterHub typically needs both JupyterHub's infrastructure and LMS-specific authorization concepts.
Without a common layer, every application would have to implement its own:
- Role definitions
- Course and term scoping
- JupyterHub group-name parsing
- Permission checks
- Membership management
- Authorization decorators
- JupyterHub API integration
e2x-hub-rbac centralizes these concerns so that multiple e2x services can use the same roles, group conventions, and authorization model.
In short:
JupyterHub provides the users and groups;
e2x-hub-rbacturns those groups into an LMS-oriented management and authorization model.
The remainder of this document describes the roles, group naming convention, permission resolution, and APIs provided by the package.
Predefined Roles
Roles are fixed and ship with this package.
| Role | Scope | Group name format |
|---|---|---|
LMS_ADMIN |
LMS | lms.lms-admin |
COURSE_CREATOR |
LMS | lms.course-creator |
COURSE_OWNER |
Course | lms.course.{course_id}.course-owner |
INSTRUCTOR |
Term | lms.course.{course_id}.term.{term_id}.instructor |
TEACHING_ASSISTANT |
Term | lms.course.{course_id}.term.{term_id}.teaching_assistant |
OBSERVER |
Term | lms.course.{course_id}.term.{term_id}.observer |
STUDENT |
Term | lms.course.{course_id}.term.{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:
- LMS-level roles:
lms.<role_name> - Course-level roles:
lms.course.<course_id>.<role_name> - Term-level roles:
lms.course.<course_id>.term.<term_id>.<role_name>
Examples
lms.lms-admin # LMS admin (global access)
lms.course-creator # Can create courses (global)
lms.course.math101.course-owner # Owner of course math101
lms.course.math101.term.2024ws.instructor # Instructor for math101 in 2024ws
lms.course.math101..2024ws.teaching_assistant # TA for math101 in 2024ws
lms.course.cs101.term.2024ss.student # Student in cs101 for 2024ss
lms.course.physics201.term.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
lms.invalid_role # Unknown role name
lms.course.math101 # Missing role name
lms.course.math101.math101.term.instructor # Missing term_id
lms.course.math101.student # Wrong scope for student role
Permission Resolution
- Lms 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.LMS_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=["lms.course.math101.term.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
)
LMS-Level Operations
Manage lms administrators and course creators:
from e2x_hub_rbac.auth import UserLike
# Admin user who can manage memberships
admin = User(username="admin", groups=["lms.lms-admin"])
# Add/remove lms admins
await membership_api.add_lms_admins(admin, ["user1", "user2"])
await membership_api.remove_lms_admins(admin, ["user1"])
admins = await membership_api.list_lms_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 LMS admins | LMS-scoped | LMS admins only |
| Manage course creators | LMS-scoped | LMS admins only |
| Manage course owners | Course-scoped | LMS admins, course owners |
| Manage instructors | Term-scoped | LMS admins, course owners, instructors |
| Manage TAs | Term-scoped | LMS admins, course owners, instructors |
| Manage observers | Term-scoped | LMS admins, course owners, instructors |
| Manage students | Term-scoped | LMS admins, course owners, instructors, TAs |
| List term members | Term-scoped | LMS admins, course owners, instructors, TAs, observers |
Example of permission checking:
# Course owner can manage their course
course_owner = User(username="prof", groups=["lms.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=["lms.course.math101.term.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.2.0.tar.gz.
File metadata
- Download URL: e2x_hub_rbac-0.2.0.tar.gz
- Upload date:
- Size: 24.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
429cd309d133e0a770ab7372b40e137c4f81e0231a1eec1e0a89c44a4212f46d
|
|
| MD5 |
f85b739e2c050026dfc7fc2e2cca81ab
|
|
| BLAKE2b-256 |
e4a7d8693a0276871d59bd6fe1c2775a0cc19ad5bc32ff9e4b9db56f8f6d7baf
|
Provenance
The following attestation bundles were made for e2x_hub_rbac-0.2.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.2.0.tar.gz -
Subject digest:
429cd309d133e0a770ab7372b40e137c4f81e0231a1eec1e0a89c44a4212f46d - Sigstore transparency entry: 2421194461
- Sigstore integration time:
-
Permalink:
DigiKlausur/e2x-hub-rbac@5afa92c2ea28d07cf11304bca69f964de28a79dc -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/DigiKlausur
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
python-publish.yml@5afa92c2ea28d07cf11304bca69f964de28a79dc -
Trigger Event:
release
-
Statement type:
File details
Details for the file e2x_hub_rbac-0.2.0-py3-none-any.whl.
File metadata
- Download URL: e2x_hub_rbac-0.2.0-py3-none-any.whl
- Upload date:
- Size: 20.1 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 |
1b6391d4c53a4b14c82dd73be08a2b6fddf585c10e061084cb3ec93f27c46470
|
|
| MD5 |
acce0be1c1f4a1e6be4104d225e5748e
|
|
| BLAKE2b-256 |
d8c56064cc604e916ab4083b3dd726efd4bf15118e40bd5201101b377d5f8125
|
Provenance
The following attestation bundles were made for e2x_hub_rbac-0.2.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.2.0-py3-none-any.whl -
Subject digest:
1b6391d4c53a4b14c82dd73be08a2b6fddf585c10e061084cb3ec93f27c46470 - Sigstore transparency entry: 2421194535
- Sigstore integration time:
-
Permalink:
DigiKlausur/e2x-hub-rbac@5afa92c2ea28d07cf11304bca69f964de28a79dc -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/DigiKlausur
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
python-publish.yml@5afa92c2ea28d07cf11304bca69f964de28a79dc -
Trigger Event:
release
-
Statement type: