[SECURITY] Tokens for downloading answer attachments

This commit is contained in:
Raphael Michel
2017-08-20 16:59:45 +02:00
parent 5c91352bae
commit 1a42a54d98
10 changed files with 132 additions and 14 deletions

View File

@@ -12,11 +12,10 @@ from django.db import models
from django.db.models import F, Sum
from django.db.models.signals import post_delete
from django.dispatch import receiver
from django.urls import reverse
from django.utils.crypto import get_random_string
from django.utils.encoding import escape_uri_path
from django.utils.functional import cached_property
from django.utils.html import escape
from django.utils.safestring import mark_safe
from django.utils.timezone import make_aware, now
from django.utils.translation import pgettext_lazy, ugettext_lazy as _
from django_countries.fields import CountryField
@@ -493,7 +492,19 @@ class QuestionAnswer(models.Model):
)
@property
def file_link(self):
def backend_file_url(self):
if self.file:
if self.orderposition:
return reverse('control:event.order.download.answer', kwargs={
'code': self.orderposition.order.code,
'event': self.orderposition.order.event.slug,
'organizer': self.orderposition.order.event.organizer.slug,
'answer': self.pk,
})
return ""
@property
def frontend_file_url(self):
from pretix.multidomain.urlreverse import eventreverse
if self.file:
@@ -508,12 +519,13 @@ class QuestionAnswer(models.Model):
'answer': self.pk,
})
return mark_safe("<a href='{}'>{}</a>".format(
url,
escape(self.file.name.split('.', 1)[-1])
))
return url
return ""
@property
def file_name(self):
return self.file.name.split('.', 1)[-1]
def __str__(self):
if self.question.type == Question.TYPE_BOOLEAN and self.answer == "True":
return str(_("Yes"))

View File

@@ -1,5 +1,7 @@
from django import template
from pretix.helpers.safedownload import get_token
from ..views.redirect import safelink as sl
register = template.Library()
@@ -8,3 +10,8 @@ register = template.Library()
@register.simple_tag
def safelink(url):
return sl(url)
@register.simple_tag
def answer_token(request, answer):
return get_token(request, answer)

View File

@@ -2,6 +2,7 @@
{% load i18n %}
{% load bootstrap3 %}
{% load eventurl %}
{% load safelink %}
{% block title %}
{% blocktrans trimmed with code=order.code %}
Order details: {{ code }}
@@ -210,7 +211,10 @@
<dd>
{% if q.answer %}
{% if q.answer.file %}
<span class="fa fa-file"></span> {{ q.answer.file_link }}
<span class="fa fa-file"></span>
<a href="{{ q.answer.backend_file_url }}?token={% answer_token request q.answer %}">
{{ q.answer.file_name }}
</a>
{% else %}
{{ q.answer|linebreaksbr }}
{% endif %}

View File

@@ -128,6 +128,9 @@ urlpatterns = [
name='event.order.regeninvoice'),
url(r'^orders/(?P<code>[0-9A-Z]+)/invoices/(?P<id>\d+)/reissue$', orders.OrderInvoiceReissue.as_view(),
name='event.order.reissueinvoice'),
url(r'^orders/(?P<code>[0-9A-Z]+)/answer/(?P<answer>[^/]+)/$',
orders.AnswerDownload.as_view(),
name='event.order.download.answer'),
url(r'^orders/(?P<code>[0-9A-Z]+)/extend$', orders.OrderExtend.as_view(),
name='event.order.extend'),
url(r'^orders/(?P<code>[0-9A-Z]+)/contact$', orders.OrderContactChange.as_view(),

View File

@@ -1,3 +1,5 @@
import mimetypes
import os
from datetime import timedelta
import pytz
@@ -6,7 +8,7 @@ from django.contrib import messages
from django.core.urlresolvers import reverse
from django.db.models import Count
from django.http import FileResponse, Http404, HttpResponseNotAllowed
from django.shortcuts import redirect, render
from django.shortcuts import get_object_or_404, redirect, render
from django.utils.formats import date_format
from django.utils.functional import cached_property
from django.utils.timezone import now
@@ -19,8 +21,8 @@ from i18nfield.strings import LazyI18nString
from pretix.base.i18n import language
from pretix.base.models import (
CachedCombinedTicket, CachedFile, CachedTicket, Invoice, InvoiceAddress,
Item, ItemVariation, LogEntry, Order, Quota, generate_position_secret,
generate_secret,
Item, ItemVariation, LogEntry, Order, QuestionAnswer, Quota,
generate_position_secret, generate_secret,
)
from pretix.base.models.event import SubEvent
from pretix.base.services.export import export
@@ -42,6 +44,7 @@ from pretix.control.forms.orders import (
OrderMailForm, OrderPositionAddForm, OrderPositionChangeForm,
)
from pretix.control.permissions import EventPermissionRequiredMixin
from pretix.helpers.safedownload import check_token
from pretix.multidomain.urlreverse import build_absolute_uri
from pretix.presale.signals import question_form_fields
@@ -741,6 +744,29 @@ class OrderEmailHistory(EventPermissionRequiredMixin, OrderViewMixin, ListView):
return qs
class AnswerDownload(EventPermissionRequiredMixin, OrderViewMixin, ListView):
permission = 'can_view_orders'
def get(self, request, *args, **kwargs):
answid = kwargs.get('answer')
token = request.GET.get('token', '')
answer = get_object_or_404(QuestionAnswer, orderposition__order=self.order, id=answid)
if not answer.file:
raise Http404()
if not check_token(request, answer, token):
raise Http404(_("This link is no longer valid. Please go back, refresh the page, and try again."))
ftype, ignored = mimetypes.guess_type(answer.file.name)
resp = FileResponse(answer.file, content_type=ftype or 'application/binary')
resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}-{}"'.format(
self.request.event.slug.upper(), self.order.code,
answer.orderposition.positionid,
os.path.basename(answer.file.name).split('.', 1)[1]
)
return resp
class OverView(EventPermissionRequiredMixin, TemplateView):
template_name = 'pretixcontrol/orders/overview.html'
permission = 'can_view_orders'

View File

@@ -0,0 +1,18 @@
import hashlib
from django.core.signing import BadSignature, TimestampSigner
def get_token(request, answer):
payload = '{}:{}'.format(request.session.session_key, answer.pk)
signer = TimestampSigner()
return signer.sign(hashlib.sha1(payload.encode()).hexdigest())
def check_token(request, answer, token):
payload = hashlib.sha1('{}:{}'.format(request.session.session_key, answer.pk).encode()).hexdigest()
signer = TimestampSigner()
try:
return payload == signer.unsign(token, max_age=3600 * 24)
except BadSignature:
return False

View File

@@ -1,5 +1,6 @@
{% load i18n %}
{% load eventurl %}
{% load safelink %}
{% for line in cart.positions %}
<div class="row cart-row {% if download and line.item.admission|default:event.settings.ticket_download_nonadm %}has-downloads{% endif %}">
<div class="product">
@@ -33,7 +34,10 @@
<dd>
{% if q.answer %}
{% if q.answer.file %}
<span class="fa fa-file"></span> {{ q.answer.file_link }}
<span class="fa fa-file"></span>
<a href="{{ q.answer.frontend_file_url }}?token={% answer_token request q.answer %}">
{{ q.answer.file_name }}
</a>
{% else %}
{{ q.answer|linebreaksbr }}
{% endif %}

View File

@@ -22,6 +22,7 @@ from pretix.base.services.tickets import (
get_cachedticket_for_order, get_cachedticket_for_position,
)
from pretix.base.signals import register_ticket_outputs
from pretix.helpers.safedownload import check_token
from pretix.multidomain.urlreverse import eventreverse
from pretix.presale.forms.checkout import InvoiceAddressForm
from pretix.presale.views import CartMixin, EventViewMixin
@@ -502,11 +503,15 @@ class OrderCancelDo(EventViewMixin, OrderDetailMixin, AsyncAction, View):
class AnswerDownload(EventViewMixin, OrderDetailMixin, View):
def get(self, request, *args, **kwargs):
answid = kwargs.get('answer')
token = request.GET.get('token', '')
answer = get_object_or_404(QuestionAnswer, orderposition__order=self.order, id=answid)
if not answer.file:
return Http404()
raise Http404()
if not check_token(request, answer, token):
raise Http404(_("This link is no longer valid. Please go back, refresh the page, and try again."))
ftype, _ = mimetypes.guess_type(answer.file.name)
ftype, ignored = mimetypes.guess_type(answer.file.name)
resp = FileResponse(answer.file, content_type=ftype or 'application/binary')
resp['Content-Disposition'] = 'attachment; filename="{}-{}-{}-{}"'.format(
self.request.event.slug.upper(), self.order.code,