Skip to content

Rbac

rbac

Role-based access control: roles, permissions, and enforcement.

Permission format: action:scope where scope is a domain name or domain.entity pair. The wildcard * matches everything.

Examples:

  • read:Orders — read any entity in the Orders domain
  • write:Billing.Invoice — write the Invoice entity in Billing
  • delete:* — delete anything
  • *:* — superuser (all actions, all scopes)
Built-in roles
  • admin*:*
  • editorread:*, write:*
  • viewerread:*

Action

Bases: str, Enum

Permission actions.

RoleDefinition

Bases: BaseModel

A named role with a list of permission strings.

RBACConfig

Bases: BaseModel

Declarative RBAC configuration (lives under rbac in auth.json).

RBACPolicy

RBACPolicy(config: RBACConfig | None = None)

Resolves roles to permissions and checks access.

Merges built-in roles with any custom roles from config. Custom roles with the same name as a built-in role override the built-in.

Source code in libs/ninja-auth/src/ninja_auth/rbac.py
def __init__(self, config: RBACConfig | None = None) -> None:
    self.config = config or RBACConfig()
    self._role_permissions: dict[str, list[str]] = {}
    self._build_role_map()

roles

roles() -> list[str]

Return all known role names.

Source code in libs/ninja-auth/src/ninja_auth/rbac.py
def roles(self) -> list[str]:
    """Return all known role names."""
    return list(self._role_permissions.keys())

permissions_for_roles

permissions_for_roles(roles: list[str]) -> list[str]

Return the union of permissions granted by roles.

Source code in libs/ninja-auth/src/ninja_auth/rbac.py
def permissions_for_roles(self, roles: list[str]) -> list[str]:
    """Return the union of permissions granted by *roles*."""
    seen: set[str] = set()
    result: list[str] = []
    for role in roles:
        for perm in self._role_permissions.get(role, []):
            if perm not in seen:
                seen.add(perm)
                result.append(perm)
    return result

is_allowed

is_allowed(
    permissions: list[str],
    action: str,
    domain: str,
    entity: str | None = None,
) -> bool

Check whether permissions grant action on domain (optionally entity).

Source code in libs/ninja-auth/src/ninja_auth/rbac.py
def is_allowed(self, permissions: list[str], action: str, domain: str, entity: str | None = None) -> bool:
    """Check whether *permissions* grant *action* on *domain* (optionally *entity*)."""
    scope = f"{domain}.{entity}" if entity else domain
    required = f"{action}:{scope}"
    return any(permission_matches(grant, required) for grant in permissions)

check

check(
    permissions: list[str],
    action: str,
    domain: str,
    entity: str | None = None,
) -> None

Like :meth:is_allowed but raises :class:PermissionError on denial.

Source code in libs/ninja-auth/src/ninja_auth/rbac.py
def check(self, permissions: list[str], action: str, domain: str, entity: str | None = None) -> None:
    """Like :meth:`is_allowed` but raises :class:`PermissionError` on denial."""
    if not self.is_allowed(permissions, action, domain, entity):
        scope = f"{domain}.{entity}" if entity else domain
        raise PermissionError(f"Permission denied: {action}:{scope}")

permission_matches

permission_matches(grant: str, required: str) -> bool

Return True if grant satisfies required.

Wildcards
  • * in the action position matches any action.
  • * in the scope position matches any scope.
  • DomainName in grant scope matches DomainName.AnyEntity.
Source code in libs/ninja-auth/src/ninja_auth/rbac.py
def permission_matches(grant: str, required: str) -> bool:
    """Return True if *grant* satisfies *required*.

    Wildcards:
        - ``*`` in the action position matches any action.
        - ``*`` in the scope position matches any scope.
        - ``DomainName`` in grant scope matches ``DomainName.AnyEntity``.
    """
    g_action, g_scope = _parse_permission(grant)
    r_action, r_scope = _parse_permission(required)

    if not g_action or not r_action:
        return False

    # Action match
    if g_action != "*" and g_action != r_action:
        return False

    # Scope match
    if g_scope == "*":
        return True
    if g_scope == r_scope:
        return True
    # Domain-level grant covers domain.entity
    if "." not in g_scope and r_scope.startswith(f"{g_scope}."):
        return True

    return False

require_domain_permission

require_domain_permission(
    action: str,
    domain: str,
    entity: str | None = None,
    *,
    policy: RBACPolicy | None = None,
) -> None

Raise :class:PermissionError if the current user lacks the permission.

Imports current_user_context lazily to avoid circular imports.

Source code in libs/ninja-auth/src/ninja_auth/rbac.py
def require_domain_permission(
    action: str,
    domain: str,
    entity: str | None = None,
    *,
    policy: RBACPolicy | None = None,
) -> None:
    """Raise :class:`PermissionError` if the current user lacks the permission.

    Imports ``current_user_context`` lazily to avoid circular imports.
    """
    from ninja_auth.agent_context import current_user_context

    ctx = current_user_context()
    if not ctx.is_authenticated:
        raise PermissionError("Authenticated user context required")

    _policy = policy or RBACPolicy()
    _policy.check(ctx.permissions, action, domain, entity)