Skip to content

Permissions

The scoped-RBAC public API. See the permissions guide for the model and how to extend it; this page is the generated reference.

Core API

sparkth.lib.permissions

Public API for the permissions framework.

Application code and plugins import the permissions surface from here, never from sparkth.core.permissions or the hook modules directly. A plugin declares its permissions and scope kinds from its __init__ with Permission.create() and PermissionScope.create() (or ObjectlessPermissionScope.create() for a singleton scope)::

from sparkth.lib.permissions import Permission
from sparkth.lib.permissions.scopes import GLOBAL, PermissionScope

PermissionScope.create("course", parent=GLOBAL)
Permission.create("course.grade")

Permission

Permission(name: str)

create classmethod

create(name: str) -> Permission

Create a permission and register it on the PERMISSIONS hook.

Use this, not the bare Permission(name) constructor — the constructor does not register (it is internal/test-only), and an unregistered permission authorizes nothing.

require

require(
    permission_scope: PermissionScope,
    scope_param: str | None = None,
) -> Callable[..., Awaitable[User]]

Return a FastAPI dependency enforcing this permission at a scope.

Parameters:

Name Type Description Default
permission_scope PermissionScope

The scope to check at, as a declared PermissionScope object (e.g. GLOBAL, WHITELIST, your COURSE). Pass the registered instance, not its name — this keeps scope references object-based, like the RBAC engine functions, so a typo is an import error rather than a silent lookup miss.

required
scope_param str | None

The route path parameter carrying the scope object id, resolved from request.path_params per request. Omit for an objectless scope (it names no object). Supplying it for an objectless scope, or omitting it for an object-bearing one, is a wiring error and raises ValueError at definition time.

None

require_in_global_scope

require_in_global_scope() -> Callable[..., Awaitable[User]]

Return a FastAPI dependency enforcing this permission at the GLOBAL scope.

The dependency resolves the current user, checks the permission with no scope object id, returns the authenticated User on success, and raises 403 otherwise.

assign_role async

assign_role(
    user_id: int,
    role_name: str,
    permission_scope: PermissionScope,
    scope_object_id: str | None,
    session: AsyncSession,
) -> RoleAssignment

Return the active assignment of role_name to user_id at the given permission scope, creating it if absent.

Idempotent and race-safe: if a concurrent assign inserts the same (user, role, scope) between the existence check and our insert, the unique index rejects ours; we recover by re-querying and returning the winner instead of surfacing the IntegrityError.

Raises RoleNotFound if the role does not exist. Raises InvalidScopeObjectId if the (permission_scope, scope_object_id) pairing is invalid (see PermissionScope.validate_object_id).

can async

can(
    user: User,
    permission: Permission,
    permission_scope: PermissionScope,
    scope_object_id: str | None,
    session: AsyncSession,
) -> bool

Return whether user holds permission at the given permission scope.

get_permission

get_permission(name: str) -> Permission

Return the registered permission named name, or raise PermissionNotFound.

get_permission_scope

get_permission_scope(name: str) -> PermissionScope

Return the registered scope kind named name, or raise PermissionScopeNotFound.

has_role async

has_role(
    user: User,
    role_name: str,
    permission_scope: PermissionScope,
    scope_object_id: str | None,
    session: AsyncSession,
) -> bool

Return whether user holds an active assignment of role_name at the given permission scope.

revoke_role async

revoke_role(
    user_id: int,
    role_name: str,
    permission_scope: PermissionScope,
    scope_object_id: str | None,
    session: AsyncSession,
) -> None

Soft-delete all active assignments of role_name for user_id at the given permission scope.

Scopes

sparkth.lib.permissions.scopes

ObjectlessPermissionScope

ObjectlessPermissionScope(
    name: str, parent: PermissionScope | None = None
)

Bases: PermissionScope

A singleton scope that names no object — there is exactly one (e.g. global, whitelist), so its role_assignment rows carry scope_object_id = NULL.

PermissionScope

PermissionScope(
    name: str, parent: PermissionScope | None = None
)

A kind of boundary a role can be assigned at that names one object of its kind.

This is the common case (e.g. course, quiz — many instances, each identified by a scope_object_id). The rarer singleton case that names no object (global, whitelist) is :class:ObjectlessPermissionScope, a subclass that overrides the object-id rules, the require() wiring, and how the scope cascades.

Declared from a plugin's __init__ (or sparkth.core.permissions.scopes for core scopes) via create(). Each scope may have a single parent, forming a chain from a specific kind up to a broader one; a scope with no parent is a root. The hierarchy lets a grant at an ancestor scope cascade to its descendants — see :meth:scope_chain.

Create a scope.

Parameters:

Name Type Description Default
name str

The scope kind's identifier (e.g. course).

required
parent PermissionScope | None

The enclosing scope, or None if this scope is a root.

None

create classmethod

create(
    name: str, parent: PermissionScope | None = None
) -> PermissionScope

Create a scope kind and register it on the PERMISSION_SCOPES hook.

Use this, not the bare constructor — the constructor does not register (it is internal/test-only). A parent must already be registered; ancestors are never auto-created, so passing an unregistered parent raises PermissionScopeNotFound.

get_parents

get_parents() -> list[PermissionScope]

Return this scope's ancestors, nearest first.

Walks up the hierarchy and returns [parent, grandparent, …] ending at the root. Returns an empty list when this scope has no parent.

scope_chain

scope_chain(
    scope_object_id: str | None,
) -> list[tuple[str, str | None]]

The (scope name, object id) pairs a grant may occupy to satisfy a check at THIS scope with scope_object_id.

Always includes this scope itself carrying the checked id, then each ancestor's cascade contribution (see :meth:_cascade_pairs). The read-side checks (can / has_role) match a user's active assignments against these pairs — a grant at any one authorizes.

validate_object_id

validate_object_id(scope_object_id: str | None) -> None

Raise InvalidScopeObjectId unless scope_object_id names an object.

An object-bearing scope must name which object; enforced by assign_role in place of a DB CHECK, so the database stays ignorant of the scope vocabulary (which lives in the PERMISSION_SCOPES hook).

validate_scope_param

validate_scope_param(scope_param: str | None) -> None

Raise ValueError unless a route names the path parameter carrying the object id.

Called by Permission.require at route-definition/import time so a misconfiguration fails fast at startup rather than as a silent per-request denial.

Hooks

sparkth.lib.permissions.hooks

Exceptions

sparkth.lib.permissions.exceptions

InvalidScopeObjectId

InvalidScopeObjectId(
    scope: str, scope_object_id: str | None
)

Bases: Exception

Raised when a (scope, object id) pairing is invalid.

An objectless scope must name no object (scope_object_id is None); an object-bearing scope must name one. Enforced in assign_role in place of the former ck_role_assignment_scope DB CHECK, so the database stays ignorant of the scope vocabulary (which lives in the PERMISSION_SCOPES hook).

PermissionNotFound

PermissionNotFound(permission: str)

Bases: Exception

Raised when a permission referenced by name is not registered.

PermissionScopeNotFound

PermissionScopeNotFound(name: str)

Bases: Exception

Raised when a permission scope referenced by name is not registered.

RoleAlreadyExists

RoleAlreadyExists(name: str)

Bases: Exception

Raised when creating or renaming a role to a name that is already taken.

RoleInUse

RoleInUse(role_id: int)

Bases: Exception

Raised when deleting a role that still has active assignments.

RoleNotFound

RoleNotFound(role_name: str)

Bases: Exception

Raised when a role referenced by name does not exist.