Problem with adding role based access decorator to the Flask function - flask

I want to implement role based access to resources, for that purpose i have decorator functions:
def permission_required(permission):
def decorator(f):
#wraps(f)
def decorated_function(*args, **kwargs):
if not current_user.can(permission):
return jsonify({'message' : 'You dont have right permission!'}), 401
return f(*args, **kwargs)
return decorated_function
return decorator
def admin_required(f):
return permission_required(Permission.ADMIN)(f)
def token_required(f):
#wraps(f)
def decorated(*args, **kwargs):
token = None
if 'x-access-token' in request.headers:
token = request.headers['x-access-token']
if not token:
return jsonify({'message' : 'Token is missing !!'}), 401
try:
data = jwt.decode(token, current_app.config['SECRET_KEY'], algorithms=["HS256"])
current_user = db_session.query(Account).filter_by(public_id=data['public_id']).first()
except:
return jsonify({
'message' : 'Token is invalid !!'
}), 401
return f(current_user, *args, **kwargs)
return decorated
which are called above test function:
#auth.route('/testing', methods=['GET'])
#token_required
#admin_required
def testing(current_user):
print(current_user.can(Permission.ADMIN))
return "successful"
#token_required function works fine but when I add #admin_required decorator it fails and throws
following error:
File "/home/pi/backend/src/api/auth.py", line 43, in decorated
return f(current_user, *args, **kwargs)
File "/home/pi/backend/src/api/auth.py", line 19, in decorated_function
if not current_user.can(permission):
NameError: name 'current_user' is not defined
Kindly, advice what's wrong and how can I fix that

Related

how to customize the response of a api from retrieve function in mixin

I'm a beginner to Django, i have written a class-based API view with mixin. the functionality is simple i.e fetch the data of the given id.Im pasting the code below.
class GenericAPi(generics.GenericAPIView,mixins.ListModelMixin,mixins.RetrieveModelMixin):
serializer_class=ArticleSerializer
queryset=Article.objects.all()
lookup_field="id"
def get(self,request,id):
if id:
data=self.retrieve(request)
return Response({"data":data.data,"status":data.status_code})
else:
return self.list(request)
this is the response I'm getting
{"id":5,"title":"loream","author":"me"}
then I navigate to the retrieve function in the mixin, to make some changes in the response.
def retrieve(self, request, *args, **kwargs):
print('Retrieving')
instance = self.get_object()
serializer = self.get_serializer(instance)
return Response({"result":serializer.data})
and then I make a call to API, but still, I'm getting the same response.
How to customize the response in the retrieve function itself.
I need response like this.
{"result":{"id":5,"title":"loream","author":"ipsum"}}
I think you don't have to customize retrieve function in the RetrieveModelMixin.
class GenericAPi(generics.GenericAPIView,mixins.ListModelMixin,mixins.RetrieveModelMixin):
serializer_class=ArticleSerializer
queryset=Article.objects.all()
lookup_field="id"
def get(self, request, id):
if id:
try:
article = Article.objects.get(pk = id)
return Response({"result": ArticleSerializer(article).data})
except Article.DoesNotExist:
return Response(status = status.HTTP_404_NOT_FOUND)
return
return self.list(request, *args, **kwargs)

Django - PUT endpoint authenticator error "wrapped_view() missing 1 required positional argument: 'request'"

So I'm trying to create a PUT endpoint for editing post data. In the endpoint the post id is given in the URL then the new post dated is inserted into the entity. The issue I'm running into is that request isn't coming through on the authenticator (I'm using Cognito to authenticate, not super important for the error). So even though you can see I'm clearly passing in data, the request isn't coming through on the wrapped_view in the cognito_authenticator function. Why is this happening? The error I'm getting is:
"wrapped_view() missing 1 required positional argument: 'request'"
Test.py
def test_edit(self):
response = self.client.put(reverse('edit_post_by_id', kwargs={'post_id': str(self.post.uuid)}),
data={'body': 'updated text #update'},
content_type='application/json',
**{'HTTP_AUTHORIZATION': f'bearer {self.cognito.access_token}'})
self.assertEqual(response.status_code, status.HTTP_200_OK)
View.py
#api_view(['PUT'])
#method_decorator(cognito_authenticator)
def edit_post(request, post_id):
try:
post = Post.objects.get(pk=post_id)
except Post.DoesNotExist:
return JsonResponse(dict(error=f'Post id: {post_id} does not exists'), status=status.HTTP_400_BAD_REQUEST)
authenticator
def cognito_authenticator(view_func=None):
if view_func is None:
return partial(cognito_authenticator)
#wraps(view_func)
def wrapped_view(request, *args, **kwargs):
# Check the cognito token from the request.
auth = request.headers.get("Authorization", None)
if not auth:
return Response(dict(error='Authorization header expected'), status=status.HTTP_401_UNAUTHORIZED)
parts = auth.split()
if parts[0].lower() != "bearer":
return Response(dict(error='Authorization header must start with bearer'),
status=status.HTTP_401_UNAUTHORIZED)
elif len(parts) == 1:
return Response(dict(error='Token not found'), status=status.HTTP_401_UNAUTHORIZED)
elif len(parts) > 2:
return Response(dict(error='Authorization header must be Bearer token'),
status=status.HTTP_401_UNAUTHORIZED)
token = parts[1]
try:
res = decode_cognito_jwt(token)
except Exception:
# Fail if invalid
return Response("Invalid JWT", status=status.HTTP_401_UNAUTHORIZED) # Or HttpResponseForbidden()
else:
# Proceed with the view if valid
return view_func(request, *args, **kwargs)
return wrapped_view
With function-based views, you don't need to use method_decorator so:
#api_view(['PUT'])
#cognito_authenticator
def edit_post(request, post_id):
...

Django Rest Framework: Return response from mixin's dispatch method

In order to interact with slack, a server needs to be able to validate requests based on some cryptographic hashing. If this check returns false, the server should respond with a 400. It seems sensible to do this as a mixin:
class SlackValidationMixin:
def dispatch(self, request, *args, **kwargs):
if validate_slack_request(request):
return super().dispatch(request, *args, **kwargs)
else:
return Response(status=status.HTTP_400_BAD_REQUEST)
This gives the error "accepted_renderer not set on Response"
Based on a SO question, I added the following:
class SlackValidationMixin:
def dispatch(self, request, *args, **kwargs):
if validate_slack_request(request):
return super().dispatch(request, *args, **kwargs)
else:
response = Response(status=status.HTTP_400_BAD_REQUEST)
response.accepted_renderer = JSONRenderer
response.accepted_media_type = "application/json"
response.renderer_context = {}
return response
But this gives the error: AttributeError: 'NoneType' object has no attribute 'get_indent'
Why does it need an accepted_renderer, given that it is only responding with an HTTP status code, with no additional data? What is the easiest way of getting around this?
Following suggestion in answer to make EmptyResponse object inheriting from Response:
Traceback (most recent call last):
File "path/lib/python3.8/site-packages/django/core/handlers/exception.py", line 34, in inner
response = get_response(request)
File "path/lib/python3.8/site-packages/django/utils/deprecation.py", line 96, in __call__
response = self.process_response(request, response)
File "path/lib/python3.8/site-packages/django/middleware/common.py", line 106, in process_response
if response.status_code == 404:
AttributeError: 'dict' object has no attribute 'status_code'
At first the solution: your second approach is fine, you only need to instantiate the JSONResponse class (DRF does this in the get_renderers method of views.APIView):
response.accepted_renderer = JSONRenderer()
Background:
Django WSGIHandler (inherited from Basehandler) calls response.render() to render the response
DRF Response (inherited from SimpleTemplateResponse) object has a render method that gets the rendered content via the rendered_content property (which calls the render method of the renderer with the passed data, media type and context)
In the initial content-negotiation stage, the renderer is set according to the DEFAULT_RENDERER_CLASSES/APIView.renderer_classes setting and the Aceept header passed by client; the selected renderer is set in the HttpRequest object as accepted_renderer and the media type as request.accepted_media_type attributes
If the renderer needs any extra context, the Response object also needs the renderer_context attribute; for example, views.APIView sets the current view, request, and arguments as renderer_context dict
Now it should be clear why you need the attributes with Response object -- to get the renderer, media type and to pass any extra context that might be needed by the selected renderer.
You've added an answer, where you're setting the above mentioned attributes and then from the renderer returning an empty dict as response. If you want to follow that route, a much easier and cleaner option would be to create a subclass of Response and return an empty dict from the render method e.g.:
class EmptyResponse(rest_framework.response.Response):
def render(self):
# You can have your own rendered content here
self.content = b''
return self
Now only returning the EmptyResponse object would do, no need to add the renderer related attributes:
class SlackValidationMixin:
def dispatch(self, request, *args, **kwargs):
if validate_slack_request(request):
return super().dispatch(request, *args, **kwargs)
else:
return EmptyResponse(status=status.HTTP_400_BAD_REQUEST)
Now, unless you're adding some custom content, the deferred rendering is not needed; you can directly return HttpResponse object:
from django.http import HttpResponse
class SlackValidationMixin:
def dispatch(self, request, *args, **kwargs):
if validate_slack_request(request):
return super().dispatch(request, *args, **kwargs)
else:
return HttpResponse(status=status.HTTP_400_BAD_REQUEST)
And if you want, you can pass the content (as bytes) while initializing HttpResponse. But if for some reason, you need lazy rendering, you need to use Response.render.
Creating a renderer that returns nothing seems to get this to work. I would be surprised if this were the 'correct' way, but it gets the job done.
class NoneRenderer(BaseRenderer):
def render(self, *args, **kwargs):
return {}
class SlackValidationMixin:
def dispatch(self, request, *args, **kwargs):
if validate_slack_request(request):
return super().dispatch(request, *args, **kwargs)
else:
response = Response(status=status.HTTP_400_BAD_REQUEST)
response.accepted_renderer = NoneRenderer
response.accepted_media_type = "*/*"
response.renderer_context = {}
return response

Django custom cache_page decorater returns error on the only first run afterwards it's ok

I created custom cache_page decorator for my app. It doesnt work on the first run and throwing error related to middleware:
content_encoding = response.get("Content-Encoding", "")
AttributeError: 'bool' object has no attribute 'get'
But on the second and further run, it works because the cache has been set. I installed django debug_toolbar and added cors middleware to my middlewares. Can anyone help on this? Here is my custom decorator func:
def cache_page(timeout):
"""custom cache page decorator"""
def decorator(func):
#wraps(func)
def wrapper(request, *args, **kwargs):
print("wrapp", request)
cache_key = hashlib.md5(
iri_to_uri(request.build_absolute_uri()).encode('ascii')
).hexdigest()
cached_data = cache.get(cache_key)
if cached_data is not None:
return cached_data
response = func(request, *args, **kwargs)
if (isinstance(response, Response) and response.status_code in (200, 301, 302, 304)):
cache_timeout = timeout() if callable(timeout) else timeout
if hasattr(response, 'render') and callable(response.render):
response.add_post_render_callback(
lambda r: cache.set(cache_key, r, cache_timeout)
)
else:
cache.set(cache_key, response, cache_timeout)
return response
return wrapper
return decorator
Tested with Django 2.2b:
I used this Mixin:
class CacheKeyDispatchMixin:
def dispatch(self, *args, **kwargs):
if self.request.method == 'GET' or self.request.method == 'HEAD':
url_to_cache = '/{0}{1}'.format(get_language(), self.request.get_full_path())
cache_hash = calculate_xxxhash(url_to_cache)
data = cache.get(cache_hash)
if not data:
response = super(CacheKeyDispatchMixin, self).dispatch(*args, **kwargs)
if response.status_code == 200:
response.render()
cache.set(cache_hash, response)
logger.info('Cache added {0} ({1})'.format(url_to_cache, cache_hash))
return response
logger.info('Cache hit {0} ({1}).'.format(url_to_cache, cache_hash))
return data
return super(CacheKeyDispatchMixin, self).dispatch(*args, **kwargs)
Basically you can call the render() before caching it.

Customize Status code response from Django Rest Framework serializer

The scenario is quite straight-forward:
Say i have a user model where email should be unique. I did a custom validation for this like.
def validate_email(self, value):
if value is not None:
exist_email = User.objects.filter(email=value).first()
if exist_email:
raise serializers.ValidationError("This Email is already taken")
return value
from rest_framework response when input validation occur we should return status_code_400 for BAD_REQUEST but in this scenario we should or we need to return status_code_409 for conflicting entry. What is the best way to customize status_code response from serializer_errors validation.
I think is better to define custom exception_handler like:
settings.py
REST_FRAMEWORK = {
'EXCEPTION_HANDLER': 'myproject.common.custom_classes.handler.exception_handler',
}
handler.py
def exception_handler(exc, context):
# Custom exception hanfling
if isinstance(exc, UniqueEmailException):
set_rollback()
data = {'detail': exc.detail}
return Response(data, status=exc.status_code)
elif isinstance(exc, (exceptions.APIException, ValidationError)):
headers = {}
if getattr(exc, 'auth_header', None):
headers['WWW-Authenticate'] = exc.auth_header
if getattr(exc, 'wait', None):
headers['Retry-After'] = '%d' % exc.wait
if hasattr(exc, 'error_dict') and isinstance(exc, ValidationError):
exc.status_code = HTTP_400_BAD_REQUEST
data = exc.message_dict
elif isinstance(exc.detail, (list, dict)):
data = exc.detail
else:
data = {'detail': exc.detail}
set_rollback()
return Response(data, status=exc.status_code, headers=headers)
elif isinstance(exc, Http404):
msg = _('Not found.')
data = {'detail': six.text_type(msg)}
set_rollback()
return Response(data, status=status.HTTP_404_NOT_FOUND)
return None
exceptions.py
class UniqueEmailException(APIException):
status_code = status.HTTP_409_CONFLICT
default_detail = 'Error Message'
And finally the validator:
def validate_email(self, value):
if value is not None:
exist_email = User.objects.filter(email=value).first()
if exist_email:
raise UniqueEmailException()
return value
I would go for intercepting ValidationError exception and return the Response object with 409 status code:
try:
serializer.is_valid(raise_exception=True)
except ValidationError, msg:
if str(msg) == "This Email is already taken":
return Response(
{'ValidationError': str(msg)},
status=status.HTTP_409_CONFLICT
)
return Response(
{'ValidationError': str(msg)},
status=status.HTTP_400_BAD_REQUEST
)
Short answer:
You can't return custom response codes from a serializer.
This is because the serializer is just that: A Serializer. It doesn't, or shouldn't, deal with HTTP at all. It's just for formatting data, usually as JSON, but it'll usually do HTML for showing your API, and one or two other formats.
Long answer:
One way to accomplish this is to raise something (doesn't matter what, but make it descriptive) in your serializer, and add code to your view to catch the error. Your view can return a custom response code with a custom response body as you like it.
Like this:
add something like this to your view class:
def create(self, request, *args, **kwargs):
try:
return super().create(request, *args, **kwargs)
except ValidationError as x:
return Response(x.args, status=status.HTTP_409_CONFLICT)