Permissions¶
Reference
Every option on this page, with types and defaults: configuration reference.
Authentication is Ninja's auth=. Permissions decide what the caller may do:
class IsPublished(BasePermission[Post]):
message = "Not published yet."
def has_object_permission(self, request: HttpRequest, obj: Post, /) -> bool:
return obj.status == Post.Status.PUBLISHED
@get("/{pk}", permissions=[IsAuthenticated(), IsOwner("author") | IsStaff() | IsPublished()])
def retrieve(self, request: HttpRequest, post: Instance[Post]) -> Post: ...
- Permissions run after authentication and validation, before the controller is built. A
denial raises
HttpError(permission.status_code, permission.message): 403 by default and 401 forIsAuthenticated. - A list requires every permission and reports the first denial.
&,|and~build immutableAllOf,AnyOfandNotobjects that are safe to share between threads. - Object permissions run in
get_object, inInstance[...], or when you callself.check_object_permissions(request, obj). Lists are not filtered per object: usescope_queryset_to_ownerorget_queryset. BasePermission[T]is contravariant: aBasePermission[Model]guards any model, whileIsPublishedonly type-checks on posts.async def has_permissionworks on async operations; on sync operations it is a startup error.- On async streaming operations, permissions and bindings are checked before response headers; denials become regular HTTP errors (Streaming).
- Operation
permissions=[...]replace the controller's.permissions=Also(IsStaff())adds to them.
Built-in permissions¶
| Permission | Allows |
|---|---|
AllowAny, DenyAll |
everyone, no one |
IsAuthenticated |
request.auth is set, or request.user is authenticated (401) |
IsAuthenticatedOrReadOnly |
safe methods for everyone |
IsReadOnly |
safe methods only (combine: IsReadOnly() \| IsStaff()) |
IsStaff, IsSuperuser |
user flags |
HasDjangoPermission("app.perm", ...) |
Django permissions |
DjangoModelPermissions() |
view/add/change/delete for the controller's model by HTTP method |
IsOwner("field") |
object-level: obj.<field> is the current user; "project__owner" follows relations |
as_permission(policy, User) |
object-level: a service-layer Policy (Layers) |
HasRule("app.perm"), HasRule(predicate) |
a django-rules permission or predicate, request- and object-level (below) |
Safe methods are GET, HEAD, OPTIONS and QUERY.
Typed users¶
@get("/me", response=UserOut)
def me(self, request: AuthedRequest[User]) -> User:
return request.auth
current_user(request, User) returns the user or raises 401, and acurrent_user is the
async version. container.scoped(User, authenticated_user(User)) injects the user into
services. request_context(User) builds a RequestContext for them.
Combining request and object rules¶
For an object, each permission leaf must pass both its request check and its object
check. IsStaff() | IsOwner() therefore means staff or the actual owner; a failed
request branch cannot reappear as an allowed object branch. Before lookup, object
checks are unknown and remain unknown through ~, &, and |. Such decisions are
deferred until get_object, Instance, or check_object_permissions receives an
object. Object rules do not filter lists: configure queryset scoping for list access.
Sync and async operations use the same Boolean semantics.
django-rules¶
ninja_devx.contrib.rules.HasRule (pip install "ninja-devx[rules]") checks a rule from
django-rules without adding its authentication
backend:
import rules
from ninja_devx.contrib.rules import HasRule
@rules.predicate
def is_author(user, article):
return article is None or article.author_id == user.pk
rules.add_perm("blog.change_article", is_author | rules.is_staff)
class ArticleController(CRUDController[Article, ArticleOut, ArticleIn]):
options = ControllerOptions(permissions=[IsAuthenticated(), HasRule("blog.change_article")])
@post("/{pk}/publish", permissions=Also(HasRule(is_author)))
def publish(self, request, pk: int): ...
- A string is a name in rules' permission set; anything else is a predicate (plain callables
taking
(user)or(user, obj)are wrapped). - The rule runs with the user before the operation, and with the user and the object when
the operation loads one. Before that, two-argument predicates receive
Nonefor the object, as everywhere in rules; list operations never load one, sois_authorabove allows lists and checks each detail object. - Anonymous callers are tested as
AnonymousUser. Async operations run the predicates in a thread. - Rules cannot filter querysets. For lists restricted to the objects a user may see, use
object permissions or
get_queryset.