How to log user in FastAPI requests - python

I am trying to log all my FastAPI requests (who requested, what and what is the status code) by creating a custom APIRoute-class. This is my custom Route-class:
class LoggingRoute(APIRoute):
def get_route_handler(self) -> Callable:
original_route_handler = super().get_route_handler()
async def custom_route_handler(request: Request) -> Response:
response: Response = await original_route_handler(request)
log_request(
user = request.user,
url = request.url,
method = request.method,
status_code = response.status_code
)
return response
return custom_route_handler
The logging would work if we don't want to include request.user to the logs. When trying to access the user, the following error is thrown:
AssertionError: AuthenticationMiddleware must be installed to access request.user
I guess the user-attribute of the request is not yet initialized inside the route handler. request.user works well when handling the request later.
My question is: is there any way for me to get access to request.user inside the custom_route_handler-function? If not, is there another way to implement the logging functionality with ability to log the user as well?

You must install AuthenticationMiddleware.
https://www.starlette.io/authentication/#users
Once AuthenticationMiddleware is installed the request.user interface will be available to endpoints or other middleware.
app = FastAPI(middleware=[Middleware(AuthenticationMiddleware)])

Related

Different login views in Pyramid

There are some URLs which are handled by my Pyramid application. When an unauthenticated user tries to open any URL then the user is redirected to login form:
def forbidden(request):
if request.user.keyname == 'guest':
return HTTPFound(location=request.route_url('auth.login',))
request.response.status = 403
return dict(subtitle=u"Access denied")
config.add_view(forbidden, context=HTTPForbidden, renderer='auth/forbidden.mako')
But for some urls (routes) I have to return not the login form, but a 401 Unauthorized status code with WWW-Authenticate header. How I can setup my routes to accomplish this? I am guessing that I have to use route_predicate.
You can wrap those two errors into your custom one, and raise that in place of both. Then you can handle your exception and run the desired scenario for each error. Here is an example:
class YourError(HTTPException):
def __init__(self, error):
super(YourError, self).__init__()
self.error = error
def handle(self, request):
if error is HTTPForbidden or is instance(error, HTTPForbidden):
return self._handle403(request)
elif error is HTTPUnauthorized or is instance(error, HTTPUnauthorized):
return self._handle401(request)
def _handle403(self, request):
# do your stuff for 403
def _handle401(self, request):
# do your stuff for 401
# ...modify your view
def forbidden(context, request):
return context.handle(request)
Then edit your view configuration:
config.add_view(forbidden, context=YourError, renderer='auth/forbidden.mako')
And then in other views where you need to return 403 or 401, go this way:
def another_view(request)
...
raise YourError(HTTPForbidden)
# or
raise YourError(HTTPUnauthorized)
...
Then you will only need to implement your handling logic inside the YourError class.
I came across some discussion on the Pyramid issues list that looks like it might address this problem.
That said, I think what you might be able to do is override the Forbidden view using hooks and create a custom exception handler. Then in there I think you could differentiate between 403 and 401 errors and redirect / display an appropriate response message and customize the response however you need.

Include request data in Django Rest Framework custom exception handler response data

Tech used:
http://www.django-rest-framework.org
Exceptions: http://www.django-rest-framework.org/api-guide/exceptions/
Included rest_framework default example in custom exceptions.py file:
from rest_framework.views import exception_handler
import sys
def custom_exception_handler(exc, context=None):
# Call REST framework's default exception handler first,
# to get the standard error response.
response = exception_handler(exc)
# Now add the HTTP status code to the response and rename detail to error
if response is not None:
response.data['status_code'] = response.status_code
response.data['request'] = request
response.data['error'] = response.data.get('detail')
del response.data['detail']
return response
This sends basic error info like "Http404" etc, but no request data, like ip address, etc.
Best way to add my request into the response? Thanks in advance.
UPDATE (and solved):
So, I was initially trying to solve this using DjangoRestFramework 2.4.x, but that version doesn't have the request or context data options for the custom exception handler. Upgrading to 3.1.3 made it easy to add the data into the response. New code now looks like this (using version 3.1.3):
def custom_exception_handler(exc, request):
# Call REST framework's default exception handler first,
# to get the standard error response.
response = exception_handler(exc, request)
# Send error to rollbar
rollbar.report_exc_info(sys.exc_info(), request)
# Now add the HTTP status code to the response and rename detail to error
if response is not None:
response.data['status_code'] = response.status_code
response.data['error'] = response.data.get('detail')
del response.data['detail']
return response
This should work for your case.
from rest_framework.views import exception_handler
import sys
def custom_exception_handler(exc, context=None):
# Call REST framework's default exception handler first,
# to get the standard error response.
response = exception_handler(exc)
# Now add the HTTP status code to the response and rename detail to error
if response is not None:
response.data['status_code'] = response.status_code
response.data['request'] = context['request']
response.data['error'] = response.data.get('detail')
del response.data['detail']
return response
You can access the request from the context passed to the custom_exception_handler. This was added in DRF 3.1.0. Also refer this issue where it was resolved.
If you are using DRF<3.1, there would be no request in the context of exception handler. You can upgrade to DRF 3.1.3(latest version in PyPI) and then easily access the request in context.
Taken from DRF 3.1.1 source code:
def get_exception_handler_context(self):
"""
Returns a dict that is passed through to EXCEPTION_HANDLER,
as the `context` argument.
"""
return {
'view': self,
'args': getattr(self, 'args', ()),
'kwargs': getattr(self, 'kwargs', {}),
'request': getattr(self, 'request', None)
}
Also, you need to configure the exception handler in your settings.py file.
REST_FRAMEWORK = {
'EXCEPTION_HANDLER': 'my_project.my_app.utils.custom_exception_handler'
}
If it is not specified, the 'EXCEPTION_HANDLER' setting defaults to the standard exception handler provided by REST framework:
REST_FRAMEWORK = {
'EXCEPTION_HANDLER': 'rest_framework.views.exception_handler'
}
Note:
Exception handler will only be called for responses generated by
raised exceptions. It will not be used for any responses returned
directly by the view, such as the HTTP_400_BAD_REQUEST responses that
are returned by the generic views when serializer validation fails.

User Sessions in an oauth2 django app

I use django, django rest framework and ember.js; my entire application thereforce communicates via ajax.
Authentication is done via oauth2 and a token is send in the headers within every request.
Everythings nice and shiny but file downloads.
At one point users can download a pdf and I don't know how to apply authentication there - because on the file download I cannot send and headers, it's just a link.
I thought of adding SessionAuthentication to that particular rest api call, but the session always flags the incoming user as anyonymous.
How can I force django to create a session on top of the oauth2 token flow?
I tried login(request, user), but it somehow does not kick in.
I ended up with signed tickets, e.g. i send back a token, that is able to bypass auth for a defined timeframe. Therefore the ajax app can first request the token and then fire again a standard get request with the token attached.
Here's the basic idea, that I mixin to views:
class DownloadableMixin():
"""
Manages a ticket response, where a ticket is a signed response that gives a user limited access to a resource
for a time frame of 5 secs.
Therefore, file downloads can request a ticket for a resource and gets a ticket in the response that he can
use for non-ajax file-downloads.
"""
MAX_AGE = 5
def check_ticket(self, request):
signer = TimestampSigner()
try:
unsigned_ticket = signer.unsign(request.QUERY_PARAMS['ticket'], max_age=self.__class__.MAX_AGE)
except SignatureExpired:
return False
except BadSignature:
return False
if self.get_requested_file_name() == unsigned_ticket:
return True
return False
def get_ticket(self):
signer = TimestampSigner()
return signer.sign(self.get_requested_file_name())
def has_ticket(self, request):
return 'ticket' in request.QUERY_PARAMS
def requires_ticket(self, request):
return 'download' in request.QUERY_PARAMS
def get_requested_file_name(self):
raise NotImplementedError('Extending classes must define the requested file name.')

Django POST request to my view from Pyres worker - CSRF token

I'm using Pyres workers to do some processing of data users enter in a form. Their processing is done by a view on my form, which I make a POST request to, with data including the data to process and a CSRF middleware token for the user. My issue is that this is apparently not enough, as Django still rejects my request with a 403 forbidden.
Relevant code:
Form handler:
def handler(request):
if(request.method == "POST"):
if(request.POST.__contains__("taskdata")):
#valid post of the form
taskdata = escape(request.POST.get("taskdata",""))
t = TaskData(data=taskdata, time_added=timezone.now(), token=request.POST.get("csrfmiddlewaretoken",""))
t.save()
r = ResQ(server="127.0.0.1:6379")
r.enqueue(TaskData, t.id)
return HttpResponse(t.id)
else:
#invalid post of the form
raise Http404
else:
raise Http404
Pyres worker job:
#staticmethod
def perform(taskData_id):
#Get the taskData from this id, test it for tasky stuff
task_data = TaskData.objects.get(pk=taskData_id)
post_data = [('id',task_data.id),('data',task_data.data), ('csrfmiddlewaretoken',task_data.token)] # a sequence of two element tuples
result = urllib2.urlopen('http://127.0.0.1:8000/tasks/nlp/process/', urllib.urlencode(post_data))
content = result.read()
return
View being posted to by that job:
def process(request):
if(request.method == "POST"):
return HttpResponse("HEY, it works!")
if(request.POST.__contains__("data") and request.POST.__contains__("id")):
#valid post to the form by the model
#taskdata = escape(request.POST.get("taskdata",""))
#data = get_times(taskdata)
return HttpResponse("Hey from process!")
#return HttpResponse(json.dumps(data))
else:
#invalid post of the form
raise Http404
else:
raise Http404
What I'm basically trying to do is save some raw data at form submission, along with the CSRF token for it. The workers then send that data + token to a processing view.
Unfortunately, posting the token doesn't seem to be enough.
Does anybody know what the csrf protection actually looks for, and how I can make my Pyres workers compliant?
(Suggested tag: pyres)
I think I see the problem.
The way Django's CSRF protection works is by generating a nonce, then setting a cookie to the value of the nonce, and ensuring the csrfmiddlewaretoken POST value matches the value of the cookie. The rationale is that it makes it a stateless system, which works without any persistent session data.
The problem is that the request you make in the Pyres worker job...
result = urllib2.urlopen('http://127.0.0.1:8000/tasks/nlp/process/',
urllib.urlencode(post_data))
...is coming from the server, not the client, so it won't have the cookie set.
Assuming the /tasks/nlp/process/ URL is protected such that it can only be accessed by the server, then it's probably simplest to make the process() view exempt from CSRF checking with...
#csrf_exempt
def process(request):
...
...otherwise you'll have to manually grab the cookie value in the handler() view, and pass it on to the Pyres worker job.
Update
To ensure the process() method can only be called by the server, one simple way would be to check the request object with something like...
#csrf_exempt
def process(request):
if request.META['REMOTE_ADDR'] != '127.0.0.1':
# Return some error response here.
# 403 is traditional for access denied, but I prefer sending 404
# so 'hackers' can't infer the existence of any 'hidden' URLs
# from the response code
raise Http404
# Now do the thing
....
...although there may be some built-in decorator or somesuch to do this for you.

How do you add csrf validation to pyramid?

I'm passing in a csrf_token for every post and xhr request and want to validate the token against the session csrf token. If they don't match, I throw a 401.
I've used the NewResponse subscriber in pyramid to inspect the request and validate the csrf token in the request params against the token in the session. The validation works but it still calls the view so it def does not work as it should.
Any suggestions on the proper way to do this?
#subscriber(NewResponse)
def new_response(event):
"""Check the csrf_token if the user is authenticated and the
request is a post or xhr req.
"""
request = event.request
response = event.response
user = getattr(request, 'user', None)
# For now all xhr request are csrf protected.
if (user and user.is_authenticated()) and \
(request.method == "POST" or request.is_xhr) and \
(not request.params.get('csrf_token') or \
request.params.get('csrf_token') != unicode(request.session.get_csrf_token())):
response.status = '401 Unauthorized'
response.app_iter = []
The NewResponse subscriber is called after your view is invoked.
You want to be using an event that is invoked earlier, for example NewRequest or ContextFound. In Pyramid 1.0, you'll need to use ContextFound to properly handle things because you cannot raise exceptions in NewRequest events (this is fixed in 1.1).
The way to do this with a ContextFound event is to register an exception view for HTTPException objects like this:
config.add_view(lambda ctx, req: ctx, 'pyramid.httpexceptions.HTTPException')
Basically this will return the exception as the response object when you raise it, which is perfectly valid for HTTPException objects which are valid Pyramid Response objects.
You can then register your event and deal with the CSRF validation:
#subscriber(ContextFound)
def csrf_validation_event(event):
request = event.request
user = getattr(request, 'user', None)
csrf = request.params.get('csrf_token')
if (request.method == 'POST' or request.is_xhr) and \
(user and user.is_authenticated()) and \
(csrf != unicode(request.session.get_csrf_token())):
raise HTTPUnauthorized
Pyramid contains its own CSRF validation, which is probably a better choice.
Given your session stored CSRF tokens, this would result in the following configuration:
from pyramid.csrf import SessionCSRFStoragePolicy
def includeme(config):
# ...
config.set_csrf_storage_policy(SessionCSRFStoragePolicy())
config.set_default_csrf_options(require_csrf=True)

Categories