Rbac - NinjaStack Docs
Description: Schema-first agentic backend framework documentation

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 — `*:*`
- editor — `read:*`, `write:*`
- viewer — `read:*`

### `` 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`

    123
    124
    125
    126
    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`

    140
    141
    142
    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`

    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    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`

    155
    156
    157
    158
    159
    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`

    161
    162
    163
    164
    165
    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`

     81
     82
     83
     84
     85
     86
     87
     88
     89
     90
     91
     92
     93
     94
     95
     96
     97
     98
     99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    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`

    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    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)