Open forrestkouakou opened 1 year ago
Hi @forrestkouakou
here is a quick example:
from jose import jwt
from ninja.security import HttpBearer
class JWT(HttpBearer):
def authenticate(self, request, token):
try:
data = jwt.decode(token, '<YOUR_SECRET>')
return data # or some parameter in decoded data
except Exception:
return None
api = NinjaAPI(auth=JWT())
this will get and decode token from Http Authentication Bearer header
Hey @vitalik, thanks but I've already done the authentication part as I mentioned. Now I'm struggling with permissions like it's done here https://www.django-rest-framework.org/api-guide/permissions/ or even https://github.com/rsinger86/drf-access-policy because ModelPermissions and ObjectPermissions are important for this project. I want to be able to define global permissions but also per-action (get, post, put, patch, delete) permissions.
In case it helps, I can share a complete jwt implementation.
@forrestkouakou well it's not an easy question without context and business logic..
the most easy way is adopt authenticator with permission logic like this:
class StaffOnlyModify:
"Simple permission that allows modify only to superuser"
def check(request, user):
if request.method == 'GET':
return
if not user.is_superuser:
raise HttpForbidden(403, "This operation not allowed")
class JWT(HttpBearer):
def __init__(self, permissions):
self.permissions = permissions
def authenticate(self, request, token):
try:
data = jwt.decode(token, '<YOUR_SECRET>')
user = get_user(data['user_id'])
self.permissions.check(request, user) # <-----
return data # or some parameter in decoded data
except Exception:
return None
auth_staff_can_modify = JWT(permissions=StaffOnlyModify())
api = NinjaAPI(auth=auth_staff_can_modify) # GLobal
@api.get('/some', auth=auth_staff_can_modify)
def foo(request):
...
but yeah, that does not check on object level that you requesting - only request attributes - this implementation should live inside the api function body where you have access to querysets and concrete objects:
@api.get('/some')
def foo(request, id: int):
obj = get_object_or_404(Some, id=id)
permission.check(obj, user)
return obj
to have a permission framework like DRF - we need some sort of class based views system - which in ninja case takes a bit controvesial thoughts across users.
I've implemented permissions using a decorator here. Not quite as robust as DRF's but it's simple and easily extended.
@vitalik below some use cases from a multivendor marketplace which is part of a bigger project I'm working on.
I have plenty of cases like these but you get the idea, I'd like to define custom permissions on models and objects that will be shared across the project.
@OtherBarry I took a look at your decorator which is quite interesting. I can see a has_permissions
method, adding a kind of has_object_permissions
will be great (I think 🤔 )
Can you please give an example of how I can use this? I guess I need to define a permission with a class and then pass it like following:
@permission_required([ClassPermission])
@forrestkouakou the primary issue with object permissions is that there's no model tied directly to views in django-ninja. You could try and infer a model by following the schemas associated with the view, but that'd be pretty messy.
My suggestion would be that you pass the relevant model as an argument to the decorator, so something like:
@router.get("/customer", response=List[CustomerSchema])
@requires_permission(CustomerModel)
def view(request) -> QuerySet[CustomerModel]:
return CustomerModel.objects.all()
You could then create a new permissions class with something like
class DefaultModelPermissions(BasePermission):
REQUEST_TYPE_PERMISSION_MAP = {
"POST": "add",
"GET": "view",
"DELETE": "delete",
"PUT": "change",
"PATCH": "change"
}
def has_permissions(self, request: AuthenticatedHttpRequest, permissions: Tuple[Type[Model], ...]) -> bool:
permission_type = self.REQUEST_TYPE_PERMISSION_MAP.get(request.method)
if permission_type is None:
raise HttpError(405)
permission_strs = []
for model in permissions:
model_str = model._meta.label_lower
permission_str = model_str.replace(".", f".{permission_type}_")
permissions_strs.append(permission_str)
return request.user.has_perms(permissions)
This will test if the user has the relevant default django permissions for the provided model(s).
It's a bit dirty (and totally untested), but it should work reasonably well, and hopefully it'll give you some ideas on implementing a cleaner version.
EDIT: Yeah looks like DRF handles it basically the same way, though much cleaner, so probably worth copying the relevant parts of that.
This approach looks good, thanks @OtherBarry I'll try to make something with the way DRF implemented this and what you suggested and maybe have a cleaner - and shareable - version.
Definitely not the best, but here's what I do:
I create a check_permission
function like so:
async def check_permission(user: object, permission: str):
permissions = [permissions.codename async for permissions in Permission.objects.select_related('content_type').filter(user=user)]
return permission in permissions
and then use it like so:
async def add_transaction(request):
user = await request.auth
perm = await check_permission(user, 'add_transaction')
if perm:
all_transactions = [transactions async for transactions in Transaction.objects.select_related('user').filter(user=user)]
return all_transactions
else:
raise HttpError(403, "Insufficient Permissions")
Decorator method would probably help my DRY violation here of if perm:
@forrestkouakou What have you done at the end? I got task to create user permissions check system, i should implement permission check on app level or on endpoint level.
For someone in the future, you can check Ninja Extra: https://eadwincode.github.io/django-ninja-extra/api_controller/api_controller_permission/
django ninja does authentication (who is making this call? If unknown or invalid, 401) really well. The existing framework provides a really nice mechanism to handle this.
But it doesn't handle authorization (I know who this is and/or their authentication is legit, but do they have permission to do this thing? If not, 403) well at all. Checking for "permissions" in the authentication check is also bad, because there isn't enough information to do it well (have to parse the request
object, for example, and the payload/position arguments/queries aren't available at that point)
What about: an small update to Operation.run
(and AsyncOperation.run
):
def run(self, request: HttpRequest, **kw: Any) -> HttpResponseBase:
error = self._run_checks(request)
if error:
return error
try:
temporal_response = self.api.create_temporal_response(request)
self._pre_permissions(request) # request.auth or # request.user is now set
values = self._get_values(request, kw, temporal_response)
self._post_permissions(request, **values) # everything parsed
result = self.view_func(request, **values)
return self._result_to_response(request, result, temporal_response)
except Exception as e:
if isinstance(e, TypeError) and "required positional argument" in str(e):
msg = "Did you fail to use functools.wraps() in a decorator?"
msg = f"{e.args[0]}: {msg}" if e.args else msg
e.args = (msg,) + e.args[1:]
return self.api.on_exception(request, e)
Would also need a bunch of plumbing code to pipe through pre/post permission checks? I'd happily post an MR for this if such a strategy has legs.
Well the team decided to move to DRF for the class-based views and I didn't get a chance to work again with django-ninja. @vitalik or others were you able to take a look at this object-level permission issue? I couldn't find an example or a permission section in the docs so I guess I'll have to figure it out myself. @OtherBarry 's suggestion was quite interesting but I couldn't dive into it unfortunately.
@jnoring-pw agreeing with your suggestion. Were you able to take a closer look recently?
django ninja does authentication (who is making this call? If unknown or invalid, 401) really well. The existing framework provides a really nice mechanism to handle this.
But it doesn't handle authorization (I know who this is and/or their authentication is legit, but do they have permission to do this thing? If not, 403) well at all. Checking for "permissions" in the authentication check is also bad, because there isn't enough information to do it well (have to parse the
request
object, for example, and the payload/position arguments/queries aren't available at that point)What about: an small update to
Operation.run
(andAsyncOperation.run
):def run(self, request: HttpRequest, **kw: Any) -> HttpResponseBase: error = self._run_checks(request) if error: return error try: temporal_response = self.api.create_temporal_response(request) self._pre_permissions(request) # request.auth or # request.user is now set values = self._get_values(request, kw, temporal_response) self._post_permissions(request, **values) # everything parsed result = self.view_func(request, **values) return self._result_to_response(request, result, temporal_response) except Exception as e: if isinstance(e, TypeError) and "required positional argument" in str(e): msg = "Did you fail to use functools.wraps() in a decorator?" msg = f"{e.args[0]}: {msg}" if e.args else msg e.args = (msg,) + e.args[1:] return self.api.on_exception(request, e)
Would also need a bunch of plumbing code to pipe through pre/post permission checks? I'd happily post an MR for this if such a strategy has legs.
@forrestkouakou No, I haven't put together code--before I'd do so, I'd probably want some sort of confirmation that it's an approach the project might entertain? @vitalik may be able to comment
I'm starting a big project with django-ninja last week, I managed to implement a JWT authentication with python-jose and one feature my project heavily relies on is permissions like it works in DRF or DRF - Access Policy or other DRF-related packages. Can someone recommend me a way to achieve this?
PS: I noticed that there is a lot of stuff not available with django-ninja and that can be a problem in long term and I don't want to go back to DRF or something like fastapi 😞