RBAC made easy for Ascender Framework
UndoreRBAC is a lightweight, configurable role-based access control (RBAC) system designed for seamless integration with the Ascender Framework.
Its goal is to separate permission-evaluation logic and priority rules from data storage, provide a flexible manager interface for fetching permissions/roles, and offer an easy-to-configure permission map.
- RBAC Map - a YAML file that declares all available permissions and their configuration (
default,explicit,children). - RBAC Manager - the user-implemented bridge between your database and UndoreRBAC. You implement methods for authentication and for fetching roles/permissions.
- RBACGate - You will use this the most: an object that represents a single user’s access state; it performs all the comparison, inheritance, and override logic.
- Permissions carry a boolean
value(True/False). This allows both granting and explicit denial of permissions.
-
Priority (from lower to higher; later items win in conflicts):
- children permissions
- roles (shared permissions) - roles with higher
prioritytake precedence - scoped permissions (permissions assigned directly to the user)
- wildcards (regardless of which hierarchy level they are assigned to)
-
Wildcard (
*), e.g.users.*, means “everything underusers.” - but a wildcard can be overridden by a permission markedexplicitin the RBAC Map. -
explicit: true- a permission marked explicit ignores wildcard/override propagation. Use with caution.
Wildcards are the highest in priority. Even if a wildcard permission is assigned to a lower priority role or children permission, it CANNOT be overwritten by a higher priority permission (if it is not a wildcard itself)
For example, if user has scoped permission users.manage on value True and also has a role, which has a wildcard permission users.* on value False, access will be denied,
regardless of the fact that scoped permissions are higher in the hierarchy
BaseRBACManager is an abstract class. You must implement:
authorize(token: str, request: Request | None = None, custom_meta: dict | None = None) -> user_idfetch_user_access(user_id: Any, custom_meta: dict | None = None) -> Access
where Access contains:
{
"permissions": list[IRBACPermission],
"roles": list[IRBACRole],
"user": Any | None
}Note
- Make sure
fetch_user_accessreturns data in a predictable order if your logic depends on creation time or role priority. - The library can enforce
require_sorted_permissionsin RBACConfig by default, so it’s best if the manager returns sorted data.
Permissions can be declared in two styles: nested YAML or dot-notation. Example:
users:
delete:
view:
other:
auth.login:
audit.export:
_config:
default: false
explicit: true
children:
- users.view: true
- users.delete: false_config options:
default- the default boolean value for this permission when the user has no record for it.explicit- iftrue, this permission cannot be obtained only via wildcard/children inheritance.children- a list ofpermission:valuepairs that are applied automatically when this permission is present.
In your bootstrap.py:
from undore_rbac.interfaces.config import RBACConfig
from undore_rbac.rbac_module import RbacModule
from shared.custom_rbac_manager import CustomRBACManager
import os
appBootstrap: IBootstrap = {
"providers": [
RbacModule.for_root(
RBACConfig(
rbac_manager=CustomRBACManager(),
rbac_map_path=os.path.join(BASE_PATH, "rbac_map.yml"),
require_sorted_permissions=True
)
)
]
}Notes
rbac_map_pathshould point to the YAML file you prepared.require_sorted_permissions=Truetells the library to expect manager-provided permission records increated_atorder
Note: Refer to official
Ascender Frameworkdocs forGuardandParamGuardendpoint usage examples
class RBACGuard(Guard):
def __init__(self, *permissions: str):
self.permissions = permissions
def __post_init__(self, rbac: RbacService):
self.rbac = rbac
self.manager = rbac.manager
async def can_activate(self, request: Request, token: HTTPAuthorizationCredentials = Security(HTTPBearer())):
user_id = await self.manager.authorize(token.credentials, request=request, custom_meta={"org_id": 123})
gate = await RBACGate.from_user_id(user_id, custom_meta={"org_id": 123})
status, reason = gate.check_access(self.permissions)
if status is False:
raise InsufficientPermissions(request_url=request.url.path, required_permission=reason)class RBACParamGuard(ParamGuard):
def __init__(self, *permissions: str):
self.permissions = permissions
def __post_init__(self, rbac: RbacService):
self.rbac = rbac
self.manager = rbac.manager
async def credentials_guard(self, request: Request, token: HTTPAuthorizationCredentials = Security(HTTPBearer())):
user_id = await self.manager.authorize(token.credentials, request=request)
if self.permissions:
gate = await RBACGate.from_user_id(user_id, custom_meta={"org_id": 123})
status, reason = gate.check_access(self.permissions)
if status is False:
raise InsufficientPermissions(request_url=request.url.path, required_permission=reason)
user = gate.user
else: # Save performance if permission check is not needed
user = ... # Your user GET logic
# Your pydantic model for creds kwarg in endpoint
return AuthCredentials(
user=user # You can also pass the gate here to reuse it later
)Note:
gate.useris not populated automatically by the library. If you wantgate.useravailable, yourfetch_user_accessimplementation must return auserfield inside theAccessobject.
- Collect all permission records (scoped + shared) and roles for the user.
RBACGatecalculateschildren(using the RBAC Map) for every permission.- Permissions are then applied in this order:
- Children (applied first),
- Roles (applied next - consider
role.priorityand assignmentcreated_at), - Scoped permissions assigned directly to the user (applied last - strongest).
- Wildcards
- When conflicting permissions have the same effective priority, the most recent record (by
created_at, or the order provided by the manager) wins.- If you rely on DB timestamps or insertion order, ensure
fetch_user_accessreturns results in the expected order (Enablingrequire_sorted_permissionsrises an exception if the sorting is wrong).
- If you rely on DB timestamps or insertion order, ensure
- Log concise check summaries at debug level (do not log tokens or sensitive data).
- Avoid overusing
explicit: true- it can silently block wildcard inheritance causing confusing denials. - Cache
Accessper-request (e.g., inrequest.state) or use ParamGuard to prevent multiple DB hits in the same request. - Be careful with wildcards: Always keep in mind that wildcards override EVERYTHING and they don't care about higher-priority roles and permissions
- Wildcard permissions not taking effect - check if the target permission has
_config.explicit: truein the RBAC Map. Permissions must be sorted by created_atException - ensure permissions are sorted as exception suggests or turn of this requirement in RBACConfig (not recommended)- Heavy DB workload - cache
Accessfor the lifetime of the request or use ParamGuard to do a single fetch.
Undore <github.com/Undore>