Athena implements a code-first RBAC (Role-Based Access Control) system. Permissions are defined in code rather than stored in the database, optimizing for simplicity and performance.
When schools need configurable permissions, the system can be migrated to a database-backed model.
Role.TEACHER: { "read:own_students", # Only their assigned students "write:grades", "write:attendance", "read:own_grades", "write:activities", "read:schedule", "read:communications",}
def has_permission(roles: list[str], permission: str) -> bool: """Check if any of the user's roles grants the requested permission.""" for role in roles: try: r = Role(role) perms = ROLE_PERMISSIONS.get(r, set()) # Wildcard permissions if permission.startswith("read:") and "read:all" in perms: return True if permission.startswith("write:") and "write:all" in perms: return True if permission.startswith("delete:") and "delete:all" in perms: return True # Exact match if permission in perms: return True except ValueError: continue return False
The AuthContext dataclass provides complete user context:
@dataclass(slots=True)class AuthContext: user: User # Current user payload: TokenPayload # JWT payload membership: SchoolMembership | None = None # Active school membership school: School | None = None # Current school memberships: list[SchoolMembership] = [] # All user memberships @property def roles(self) -> list[str]: """Combined roles from token and membership.""" combined = [*self.payload.roles] if self.membership: combined.extend(self.membership.roles or []) return list(dict.fromkeys(combined)) @property def school_id(self) -> uuid.UUID | None: """Current school ID from context.""" if self.school is not None: return self.school.id if self.membership is not None: return self.membership.school_id return None
@router.get("/grades")async def list_grades( auth: AuthContext = Depends(require_permissions( "read:all", "read:grades", "read:own_grades" )), db: AsyncSession = Depends(get_db),): # User needs at least ONE of these permissions ...
from app.deps import get_current_tenant@router.patch("/settings")async def update_settings( body: SettingsUpdate, tenant: School = Depends(get_current_tenant), _: AuthContext = Depends(require_permissions("config:institution")), db: AsyncSession = Depends(get_db),): # Both school context and permission are required ...
get_current_tenant() raises 403 if no school is active in the request context.