django-ninja
django-ninja copied to clipboard
[proposal] Authorization ( permissions ) handling
In the docs I can see how to handle Authentication but not how to handle Authorization ( permissions ) After I have identified a logged user I want to return 403 if that user hasn't enough permission to do it DRF has a great way to handle it, using BasePermission.has_permission and BasePermission.has_object_permission
Maybe this library could handle permissions with something like this:
from ninja import Schema
from ninja.permissions import BasePermission
class CanSeeFruitsPerm(BasePermission):
def has_permission(self, request, view):
return request.user.has_perm("can_see_fruits")
class AppleOut(Schema):
color: str
quality: str
@api.get("/apples", auth=BasicAuth(), perm=CanSeeFruitsPerm(), response=List[AppleOut])
def list_apples(request):
return Apple.objects.all()
What do you think about it?
Thank you
Great proposal! Wouldn't this mean a lot repetition though?
Another idea would be to add an optional parameter to the __init__
of the Ninja authentication base classes, and having Ninja run a has_permission
method if a perm
is specified.
Then we can specify a permission when instantiating it.
For example
# inherit from base class BasicAuth
class UserAuth(BasicAuth):
def authenticate(self, request, username, password):
....
# Ninja runs this for you if a `perm` is specified on init
def has_permission(self, request, perm):
return request.user.has_perm(perm)
@api.get("/apples", auth=UserAuth(perm="can_see_fruits"), response=List[AppleOut])
def list_apples(request):
return Apple.objects.all()
I'm glad you like this proposal
I think they are different things, one permission could be used for unauthenticated users too, or the same permission could be used for different authentication types
What do you think about?
I find myself repeating some sort of permission check against the authenticated user. It would be nice if Ninja supported permission checks like the following:
@router.get(
url_name="resorce-get",
path="/{user}/resource",
response=ResourceSchema,
auth=[auth1(), auth2(), ....].
permissions=[perm1(), perm2(), ...] # <- This
)
def get_resource(
request: HttpRequest,
user: str,
**kwargs
):
....
This would hook nicely into Operation._run_checks
If something like this is welcome, I wouldn't mind trying to submit a PR for it.
So what's the best method to approach different levels of permissions for users in Django ninja?
To throw out another idea; it would've been nice with an approach similar to Django's user_passes_test
. Though it could come with the adjustment of returning a 403 forbidden response if test didn't pass.
In case django ninja could support decorating an operation, it could look like something below (e.g. login_required
and user_passes_test
would probably have to be reimplemented to not trigger a redirect, like Django's do)
def email_check(user):
return user.email.endswith('@example.com')
@api.get("/apples", auth=UserAuth(), response=List[AppleOut])
@login_required
@user_passes_test(email_check)
@permission_required("can_see_fruits", raise_exception=True)
def list_apples(request):
return Apple.objects.all()
And yes, I think we have to re-implement a few decorators here.
I thought I could get around this with raise_exception=True
and catching Django's PermissionDenied
:
from django.contrib.auth.decorators import permission_required
from django.core.exceptions import PermissionDenied
from ninja import Router
api = NinjaAPI(auth=[some_auth()])
@api.exception_handler(PermissionDenied)
def django_permission_denied(
request: HttpRequest, exc: PermissionDenied
) -> HttpResponse:
return api.create_response(...)
...
@router.get("/") # <- mypy error
@permission_required('app.some_permission', raise_exception=True) # note: raise_exception=True
def get_something(request: HttpRequest) -> ...:
...
But this is not going to work out, as I'm getting this mypy error:
error: Value of type variable "_VIEW" of function cannot be "Callable[[HttpRequest], ...]" [type-var]
due to
https://github.com/typeddjango/django-stubs/blob/b81b1bf3868eaab9fecebdd56ed817ffcb07fad8/django-stubs/contrib/auth/decorators.pyi#L8
And I actually think django-stubs is right in assuming the response to be of type HttpResponseBase
.
I have made https://github.com/vitalik/django-ninja/discussions/776 as an alternative way to handle permissions :)
that's mostly inspied from django-rest-framework
Why can't this be implemented using django builtin permission_required
decorator?
from django.contrib.auth.decorators import permission_required
@permission_required("app.view_model")
@api.get("/model")
def model_get(request):
...
This apparently doesn't work as of 0.22.2
Why can't this be implemented using django builtin
permission_required
decorator?
cause, permission_required redirects to a login-page, which is not really the best idea for API clients
https://github.com/django/django/blob/f7389c4b07ceeb036436e065898e411b247bca78/django/contrib/auth/decorators.py#L36