Issue
I want to generate an invoice for each order, and in some cases, there are two generated invoices for one order.
For those cases, the first invoice request fails and I receive a 400 error with "invalid signature" message (as I defined in my view logic), while the second remaining success.
views.py:
from django.core import signing
from django.contrib import messages
class OrderView(MyMultiFormsView):
forms = {‘create’: OrderForm}
… # view logic for get method: display a list of orders
def post(self, request, *args, **kwargs):
form = self.forms[‘create’](request.POST, request.FILES, prefix=‘create’)
if form.is_valid():
order = form.save()
messages.success(request, signing.dump(order.id))
if order.is_paid:
messages.info(request, signing.dump(order.id))
return redirect(request.get_full_path())
… # not valid: render form and show errors
class ExportView(View):
http_method_names = [‘get’]
actions = {
'invoice': {
'url_token': 'print-invoice',
'session_prop': '_invoice_token',
},
'receipt': {
'url_token': 'print-receipt',
'session_prop': '_receipt_token',
},
}
def get(self, request, *args, **kwargs):
if kwargs['action'] not in self.actions:
return render(request, '400.html', {'msg': 'undefined action'}, status=400)
action = self.actions[kwargs['action']]
if kwargs['token'] == action['url_token']:
try:
print(request.session.get(action['session_prop']))
token = request.session.pop(action['session_prop'])
sign = signing.load(token)
except:
return render(request, '400.html', {'msg': 'invalid signature', status=400)
return getattr(self, kwargs['action'])(sign)
else:
request.session[action['session_prop']] = kwargs['token']
print(request.session.__dict__)
redirect_url = request.path.replace(kwargs['token'], action['url_token'])
return redirect(redirect_url)
def invoice(self, sign):
inv = Invoice(sign)
# inv.as_file() returns a pdf file with io.BytesIO object type
return FileResponse(inv.as_file(), filename=inv.filename)
def receipt(self, sign):
rcp = Receipt(sign)
return FileResponse(rcp.as_file(), filename=rcp.filename)
Template orders.html:
<!-- template logic to render orders -->
<script>
...
{% if messages %}
{% for msg in messages %}
{% if msg.level == DEFAULT_MESSAGE_LEVELS.SUCCESS %}
window.open("{% url 'sale:export' 'invoice' msg %}", "_blank");
{% elif msg.level == DEFAULT_MESSAGE_LEVELS.INFO %}
window.open("{% url 'sale:export' 'receipt' msg %}", "_blank");
{% endif %]
{% endfor %}
{% endif %}
</script>
urls.py:
app_name = 'sale'
urls = [
...
path('orders/', views.OrderView.as_view(), name='orders'),
path('orders/export/<action>/<token>/', views.ExportView.as_view(), name='export'),
]
I am using Django's development server, and print(request.session.__dict__)
shows:
{'_SessionBase__session_key': 'ljm62w2z50jrdlolemrthk6bu9btjxry', 'accessed': True, 'modified': True, 'serializer': <class 'django.core.signing.JSONSerializer'>, 'model': <class 'django.contrib.sessions.models.Session'>, '_session_cache': {'_auth_user_id': '1', '_auth_user_backend': 'django.contrib.auth.backends.ModelBackend', '_auth_user_hash': 'c0d671f349e6efdadfe3afae7e1e21eb5e82c16e2a363d6706984a6a1d755c55', '_invoice_token': 'NTgxMzE:1oA3vp:iyEhWL3HV6Ai1xOgogB5yDQ6fMtEUE5eKgr4aGLQonU'}}
{'_SessionBase__session_key': 'ljm62w2z50jrdlolemrthk6bu9btjxry', 'accessed': True, 'modified': True, 'serializer': <class 'django.core.signing.JSONSerializer'>, 'model': <class 'django.contrib.sessions.models.Session'>, '_session_cache': {'_auth_user_id': '1', '_auth_user_backend': 'django.contrib.auth.backends.ModelBackend', '_auth_user_hash': 'c0d671f349e6efdadfe3afae7e1e21eb5e82c16e2a363d6706984a6a1d755c55', '_receipt_token': 'NTgxMzE:1oA3vp:iyEhWL3HV6Ai1xOgogB5yDQ6fMtEUE5eKgr4aGLQonU'}}
[09/Jul/2022 14:27:09] "GET /sale/orders/export/invoice/NTgxMzE:1oA3vp:iyEhWL3HV6Ai1xOgogB5yDQ6fMtEUE5eKgr4aGLQonU/ HTTP/1.1" 302 0
[09/Jul/2022 14:27:09] "GET /sale/orders/export/receipt/NTgxMzE:1oA3vp:iyEhWL3HV6Ai1xOgogB5yDQ6fMtEUE5eKgr4aGLQonU/ HTTP/1.1" 302 0
None
NTg1MTA:1oDhuT:56B_0sCC5svW0kReAxTF_Y3CHzehkbZ4B1NRDk7M4QE
Bad Request: /sale/orders/export/invoice/print-invoice/
[09/Jul/2022 14:27:09] "GET /sale/orders/export/invoice/print-invoice/ HTTP/1.1" 400 1307
[09/Jul/2022 14:27:09] "GET /sale/orders/export/receipt/print-receipt/ HTTP/1.1" 200 18804
Looks like session attribute '_invoice_token' somehow gets overridden.
Why the session attribute gets overridden? And how can I work around this?
PS: Remove the whole redirect part and use the signature token directly in the url gives me desired results, but this would be the last option, as I want to keep the token as secret as possible.
Solution
Django stores session as JSON and does not handle race conditions from concurrent requests.
#10760 (Some session data gets lost between multiple concurrent request) was closed (invalid).
You can override:
__setitem__
to use an atomic transaction, and_get_session_from_db
to do aSELECT ... FOR UPDATE
.
# mysite/session.py
import logging
from django.contrib.sessions.backends import db
from django.core.exceptions import SuspiciousOperation
from django.db import transaction
from django.utils import timezone
class SessionStore(db.SessionStore):
def __setitem__(self, key, value):
# self._session[key] = value # -
# self.modified = True # -
with transaction.atomic(): # +
self._session[key] = value # +
self.save() # +
def _get_session_from_db(self):
queryset = self.model.objects # +
if transaction.get_connection().in_atomic_block: # +
queryset = queryset.select_for_update() # +
try:
# return self.model.objects.get( # -
return queryset.get( # +
session_key=self.session_key, expire_date__gt=timezone.now()
)
except (self.model.DoesNotExist, SuspiciousOperation) as e:
if isinstance(e, SuspiciousOperation):
logger = logging.getLogger("django.security.%s" % e.__class__.__name__)
logger.warning(str(e))
self._session_key = None
Use your session engine:
# mysite/settings.py
...
SESSION_ENGINE = 'mysite.session'
Answered By - aaron
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.