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 |
required |
scope_param
|
str | None
|
The route path parameter carrying the scope object id, resolved from
|
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. |
required |
parent
|
PermissionScope | None
|
The enclosing scope, or |
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.