Files
pretix_cgo/src/pretix/control/views/user.py
Ian Williams 60e178d821 Fix #443 -- Show private key in 2FA wizard (#490)
* Syncing fork to upstream (#1)

Sync master with master of pretix/pretix@300f8f6

* Automatically sort new products to the end

* Drop "squash your commits" from the dev guide

* Add variation descriptions and allow to order addons

* Link to Django's runserver options in dev docs

* Allow <br> tags in rich text

* Copy from event: deal with deleted items

* Make validate_cart useful together with addons

* Fix collapsing panels in the addon choice step

* Button text change if addons are present

* Update translations

* Squash migrations and bump version

* Ticket PDFs: Do not hide attendee name if code is hidden

* Add a user guide on payments

* Link PayPal and Stripe documentation in the respective forms

* Hide payment fees if they are all equal to 0.00

* Refs #39 -- New concept of "teams" (#478)

* New models

* CRUD UI

* UI for adding/removing team members

* Log display for teams

* Fix invitations, move frontend

* Drop old models (incomplete)

* Drop more old stuff

* Drop even more old stuff

* Fix tests

* Fix permission test

* flake8 fix

* Add tests fore the new code

* Rebase migrations

* Fix typo in method name

* Update translations

* Force ordering of events on dashboard

* Fix typos in events

* Prepare the pretixdroid API for an async mode in the app

* Pretixdroid tests: Ignore microseconds (chopped by mysql)

* pretixdroid API: Add related lookups

* Add idempotenty nonces to pretixdroid API

* pretixdroid: force-accepting unpaids and time display

* Marked webfonts as binary files (#487)

Webfonts now listed as binary in `.gitattributes`.
Works on pretix/pretix#486

* Fix #456 -- Allow products to be excluded from ticket-generation (#483)

* Added non-admission setting to event

`ticket_download_nonadm` now setting in storage. Still need logic for
order page/PDF generation.
Works on pretix/pretix#456.

* Download button considers `ticket_download_nonadm`

Modified Django tags to look at item admission attribute and
`ticket_download_nonadm` setting.
Works on pretix/pretix#456.

* Ticket output for non-admission disabled

PDFs/etc. will only be permitted/generated for items with the
`admission` attribute, or if the `ticket_download_nonadm` event setting
is true. Applies to single and whole-order ticket downloads.
Works on pretix/pretix#456.

* Fixed product exclusion in PDF output

Forgot PDF output was a plugin, now includes same check as base
`BaseTicketOutput.generate_order`.
Works on pretix/pretix#456

* Mail signature (#485)

* added signature field -- no function yet

* added mail signature feature

* fixed style issue

* fixed problem with signature default

* added unit test for mail signatures

* added unit test for mail signatures

* [WIP] Fix #447 -- Sendmail plugin: Create new mail based on an old one (#476)

* send old email content to the new one

* error key event

* test commit

* query bad ID

* query bad ID

* query bad ID

* query bad ID

* Update pretixdroid API version

* Refs #447 -- Extend copying old mails to subject and receipients

* Fixed bugs and added test for date range rendering (#488)

* fixed bug for same dates, added unit check for daterange

* fixed local language override in unit test

* Fix #297 -- pretixdroid: Show metrics in the control panel (#481)

* add checkin status page

add dashboard widget
add checkin page under orders

* modify checkin logic

added new fields in checkin page
added filter items

* add tests for checkins & minor improvement

* support addin_product & noadm setting logic

* remove name ordering check test case

* Fix #379 -- Add logo to event organizers (#431)

* [WIP] Add logo to event organizers.

* Fix indentation issues.

* Refactor code

Refactor code

Refactor code

* Add new migration

* Take files into account for organizer sform (settings form)

* Fix grammer

* Make bootstrap form errors specific to each fieldset

* Display logo on organizer's page

* Fix PR issues

Fix PR issues

Fix PR issues

* Reorder imports

* Remove conflicting migration

* Fix rebase conflict

* Fix #41 -- Drag-and-drop ticket editor

Undo/redo

Useful toolbox

Font selection

Add text content

Use hex for colors

JS-side dump and load

Save

Load layout, proper undo/redo

First steps to Python rendering

More PDF rendering

Copy and paste

Buttons for keyboard actions

Splash Screen

Block unbeforeunload in dirty state

Remove debugging output

Preview

Upload new PDFs via the editor

Fix bugs during PDF reload, link in settings form

New default ticket

Add OpenSans BI

Custom fonts, fix tests

* Added bootstrap-colorpicker

* Allow inline PDF display in CSP header

* Add fontpack to list of plugins

* Update German translation

* Add ticketoutputpdf's assets to MANIFEST.i

* Fix migration of old ticket styles

* Fix iCal download URL

* Multi-line location field, new field for admission time

* Admission date and time in editor

* Remove icon from "add to calendar"

* Try to fix PDF display problems in Safari

* Proxy cachedfiles that are used as editor previews

* Check Event.presale_is_running in more places

* Fix CSS generation with an empty color field

* Fix missing placeholders and reformat the sendmail view

* Fix bug that lead to wrong payment amount when switching payment method to PayPal later

* Update translation

* Revert "Syncing fork to upstream (#1)"

This reverts commit 847d409a00a201d1a03b193a1b4e0bb23c62631e.
Merged wrong, my bad.

* Formatted OTP secret

New variable `secretGrouped` in `2fa_confirm_totp.html`, user-friendly
version of OTP secret (split every 4 characters).
Works on pretix/pretix#443.

* Adds manual secret entry OTP setup screen

`secretGrouped` exposed in user-friendly fashion. Includes short
instructions, copy-to-clipboard button, and js to hide instructions
unless user clicks on "Can't scan the barcode?" link.
Works on pretix/pretix#443.

* Minor indentation issuer

Fixed indentation issue (L40).
Works on pretix/pretix#443.

* Minor spacing issues

L265 of `user.py` failing flake8 tests, minor spacing fixes.

* Fixes indentation in `2fa_confirm_totp.html`

Per https://github.com/pretix/pretix/pull/490#discussion_r116165041,
fixes an issue with indentation.
Works on pretix/pretix#443, member of pretix/pretix#490.

* Removed `aria-*` attributes

Per https://github.com/pretix/pretix/pull/490#discussion_r116165115,
removes `aria` attributes from sub-tutorial.
Works on pretix/pretix#443, member of pretix/pretix#490.

* Pretix capitalization issue

Per https://github.com/pretix/pretix/pull/490#discussion_r116165193,
fixes an issue with capitalization of pretix.
Works on pretix/pretix#443, member of pretix/pretix#490.
2017-05-17 09:31:28 +02:00

353 lines
15 KiB
Python

import base64
import logging
import time
from urllib.parse import quote
from django.conf import settings
from django.contrib import messages
from django.contrib.auth import update_session_auth_hash
from django.core.urlresolvers import reverse
from django.shortcuts import get_object_or_404, redirect
from django.utils.crypto import get_random_string
from django.utils.functional import cached_property
from django.utils.http import is_safe_url
from django.utils.translation import ugettext_lazy as _
from django.views.generic import FormView, TemplateView, UpdateView
from django_otp.plugins.otp_static.models import StaticDevice
from django_otp.plugins.otp_totp.models import TOTPDevice
from u2flib_server import u2f
from u2flib_server.jsapi import DeviceRegistration
from pretix.base.forms.user import User2FADeviceAddForm, UserSettingsForm
from pretix.base.models import U2FDevice, User
from pretix.control.views.auth import get_u2f_appid
REAL_DEVICE_TYPES = (TOTPDevice, U2FDevice)
logger = logging.getLogger(__name__)
class RecentAuthenticationRequiredMixin:
max_time = 3600
def dispatch(self, request, *args, **kwargs):
tdelta = time.time() - request.session.get('pretix_auth_login_time', 0)
if tdelta > self.max_time:
return redirect(reverse('control:user.reauth') + '?next=' + quote(request.get_full_path()))
return super().dispatch(request, *args, **kwargs)
class ReauthView(TemplateView):
template_name = 'pretixcontrol/user/reauth.html'
def post(self, request, *args, **kwargs):
password = request.POST.get("password", "")
if request.user.check_password(password):
request.session['pretix_auth_login_time'] = int(time.time())
if "next" in request.GET and is_safe_url(request.GET.get("next")):
return redirect(request.GET.get("next"))
return redirect(reverse('control:index'))
else:
messages.error(request, _('The password you entered was invalid, please try again.'))
return self.get(request, *args, **kwargs)
class UserSettings(UpdateView):
model = User
form_class = UserSettingsForm
template_name = 'pretixcontrol/user/settings.html'
def get_object(self, queryset=None):
self._old_email = self.request.user.email
return self.request.user
def get_form_kwargs(self):
kwargs = super().get_form_kwargs()
kwargs['user'] = self.request.user
return kwargs
def form_invalid(self, form):
messages.error(self.request, _('Your changes could not be saved. See below for details.'))
return super().form_invalid(form)
def form_valid(self, form):
messages.success(self.request, _('Your changes have been saved.'))
data = {}
for k in form.changed_data:
if k not in ('old_pw', 'new_pw_repeat'):
if 'new_pw' == k:
data['new_pw'] = True
else:
data[k] = form.cleaned_data[k]
msgs = []
if 'new_pw' in form.changed_data:
msgs.append(_('Your password has been changed.'))
if 'email' in form.changed_data:
msgs.append(_('Your email address has been changed to {email}.').format(email=form.cleaned_data['email']))
if msgs:
self.request.user.send_security_notice(msgs, email=form.cleaned_data['email'])
if self._old_email != form.cleaned_data['email']:
self.request.user.send_security_notice(msgs, email=self._old_email)
sup = super().form_valid(form)
self.request.user.log_action('pretix.user.settings.changed', user=self.request.user, data=data)
update_session_auth_hash(self.request, self.request.user)
return sup
def get_success_url(self):
return reverse('control:user.settings')
class UserHistoryView(TemplateView):
template_name = 'pretixcontrol/user/history.html'
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
ctx['user'] = self.request.user
return ctx
class User2FAMainView(RecentAuthenticationRequiredMixin, TemplateView):
template_name = 'pretixcontrol/user/2fa_main.html'
def get_context_data(self, **kwargs):
ctx = super().get_context_data()
try:
ctx['static_tokens'] = StaticDevice.objects.get(user=self.request.user, name='emergency').token_set.all()
except StaticDevice.DoesNotExist:
d = StaticDevice.objects.create(user=self.request.user, name='emergency')
for i in range(10):
d.token_set.create(token=get_random_string(length=12, allowed_chars='1234567890'))
ctx['static_tokens'] = d.token_set.all()
ctx['devices'] = []
for dt in REAL_DEVICE_TYPES:
objs = list(dt.objects.filter(user=self.request.user, confirmed=True))
for obj in objs:
if dt == TOTPDevice:
obj.devicetype = 'totp'
elif dt == U2FDevice:
obj.devicetype = 'u2f'
ctx['devices'] += objs
return ctx
class User2FADeviceAddView(RecentAuthenticationRequiredMixin, FormView):
form_class = User2FADeviceAddForm
template_name = 'pretixcontrol/user/2fa_add.html'
def form_valid(self, form):
if form.cleaned_data['devicetype'] == 'totp':
dev = TOTPDevice.objects.create(user=self.request.user, confirmed=False, name=form.cleaned_data['name'])
elif form.cleaned_data['devicetype'] == 'u2f':
if not self.request.is_secure():
messages.error(self.request, _('U2F devices are only available if pretix is served via HTTPS.'))
return self.get(self.request, self.args, self.kwargs)
dev = U2FDevice.objects.create(user=self.request.user, confirmed=False, name=form.cleaned_data['name'])
return redirect(reverse('control:user.settings.2fa.confirm.' + form.cleaned_data['devicetype'], kwargs={
'device': dev.pk
}))
def form_invalid(self, form):
messages.error(self.request, _('We could not save your changes. See below for details.'))
return super().form_invalid(form)
class User2FADeviceDeleteView(RecentAuthenticationRequiredMixin, TemplateView):
template_name = 'pretixcontrol/user/2fa_delete.html'
@cached_property
def device(self):
if self.kwargs['devicetype'] == 'totp':
return get_object_or_404(TOTPDevice, user=self.request.user, pk=self.kwargs['device'], confirmed=True)
elif self.kwargs['devicetype'] == 'u2f':
return get_object_or_404(U2FDevice, user=self.request.user, pk=self.kwargs['device'], confirmed=True)
def get_context_data(self, **kwargs):
ctx = super().get_context_data()
ctx['device'] = self.device
return ctx
def post(self, request, *args, **kwargs):
self.request.user.log_action('pretix.user.settings.2fa.device.deleted', user=self.request.user, data={
'id': self.device.pk,
'name': self.device.name,
'devicetype': self.kwargs['devicetype']
})
self.device.delete()
msgs = [
_('A two-factor authentication device has been removed from your account.')
]
if not any(dt.objects.filter(user=self.request.user, confirmed=True) for dt in REAL_DEVICE_TYPES):
self.request.user.require_2fa = False
self.request.user.save()
self.request.user.log_action('pretix.user.settings.2fa.disabled', user=self.request.user)
msgs.append(_('Two-factor authentication has been disabled.'))
self.request.user.send_security_notice(msgs)
messages.success(request, _('The device has been removed.'))
return redirect(reverse('control:user.settings.2fa'))
class User2FADeviceConfirmU2FView(RecentAuthenticationRequiredMixin, TemplateView):
template_name = 'pretixcontrol/user/2fa_confirm_u2f.html'
@property
def app_id(self):
return get_u2f_appid(self.request)
@cached_property
def device(self):
return get_object_or_404(U2FDevice, user=self.request.user, pk=self.kwargs['device'], confirmed=False)
def get_context_data(self, **kwargs):
ctx = super().get_context_data()
ctx['device'] = self.device
devices = [DeviceRegistration.wrap(device.json_data)
for device in U2FDevice.objects.filter(confirmed=True, user=self.request.user)]
enroll = u2f.start_register(self.app_id, devices)
self.request.session['_u2f_enroll'] = enroll.json
ctx['jsondata'] = enroll.json
return ctx
def post(self, request, *args, **kwargs):
try:
binding, cert = u2f.complete_register(self.request.session.pop('_u2f_enroll'),
request.POST.get('token'),
[self.app_id])
self.device.json_data = binding.json
self.device.confirmed = True
self.device.save()
self.request.user.log_action('pretix.user.settings.2fa.device.added', user=self.request.user, data={
'id': self.device.pk,
'devicetype': 'u2f',
'name': self.device.name,
})
self.request.user.send_security_notice([
_('A new two-factor authentication device has been added to your account.')
])
note = ''
if not self.request.user.require_2fa:
note = ' ' + str(_('Please note that you still need to enable two-factor authentication for your '
'account using the buttons below to make a second factor required for logging '
'into your accont.'))
messages.success(request, str(_('The device has been verified and can now be used.')) + note)
return redirect(reverse('control:user.settings.2fa'))
except Exception:
messages.error(request, _('The registration could not be completed. Please try again.'))
logger.exception('U2F registration failed')
return redirect(reverse('control:user.settings.2fa.confirm.u2f', kwargs={
'device': self.device.pk
}))
class User2FADeviceConfirmTOTPView(RecentAuthenticationRequiredMixin, TemplateView):
template_name = 'pretixcontrol/user/2fa_confirm_totp.html'
@cached_property
def device(self):
return get_object_or_404(TOTPDevice, user=self.request.user, pk=self.kwargs['device'], confirmed=False)
def get_context_data(self, **kwargs):
ctx = super().get_context_data()
ctx['secret'] = base64.b32encode(self.device.bin_key).decode('utf-8')
ctx['secretGrouped'] = " ".join([ctx['secret'].lower()[(i * 4): (i + 1) * 4] for i in range(len(ctx['secret']) // 4)])
ctx['qrdata'] = 'otpauth://totp/{label}%3A%20{user}?issuer={label}&secret={secret}&digits={digits}'.format(
label=quote(settings.PRETIX_INSTANCE_NAME), user=quote(self.request.user.email),
secret=ctx['secret'],
digits=self.device.digits
)
ctx['device'] = self.device
return ctx
def post(self, request, *args, **kwargs):
token = request.POST.get('token', '')
if self.device.verify_token(token):
self.device.confirmed = True
self.device.save()
self.request.user.log_action('pretix.user.settings.2fa.device.added', user=self.request.user, data={
'id': self.device.pk,
'name': self.device.name,
'devicetype': 'totp'
})
self.request.user.send_security_notice([
_('A new two-factor authentication device has been added to your account.')
])
note = ''
if not self.request.user.require_2fa:
note = ' ' + str(_('Please note that you still need to enable two-factor authentication for your '
'account using the buttons below to make a second factor required for logging '
'into your accont.'))
messages.success(request, str(_('The device has been verified and can now be used.')) + note)
return redirect(reverse('control:user.settings.2fa'))
else:
messages.error(request, _('The code you entered was not valid. If this problem persists, please check '
'that the date and time of your phone are configured correctly.'))
return redirect(reverse('control:user.settings.2fa.confirm.totp', kwargs={
'device': self.device.pk
}))
class User2FAEnableView(RecentAuthenticationRequiredMixin, TemplateView):
template_name = 'pretixcontrol/user/2fa_enable.html'
def dispatch(self, request, *args, **kwargs):
if not any(dt.objects.filter(user=self.request.user, confirmed=True) for dt in REAL_DEVICE_TYPES):
messages.error(request, _('Please configure at least one device before enabling two-factor '
'authentication.'))
return redirect(reverse('control:user.settings.2fa'))
return super().dispatch(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
self.request.user.require_2fa = True
self.request.user.save()
self.request.user.log_action('pretix.user.settings.2fa.enabled', user=self.request.user)
messages.success(request, _('Two-factor authentication is now enabled for your account.'))
self.request.user.send_security_notice([
_('Two-factor authentication has been enabled.')
])
return redirect(reverse('control:user.settings.2fa'))
class User2FADisableView(RecentAuthenticationRequiredMixin, TemplateView):
template_name = 'pretixcontrol/user/2fa_disable.html'
def post(self, request, *args, **kwargs):
self.request.user.require_2fa = False
self.request.user.save()
self.request.user.log_action('pretix.user.settings.2fa.disabled', user=self.request.user)
messages.success(request, _('Two-factor authentication is now disabled for your account.'))
self.request.user.send_security_notice([
_('Two-factor authentication has been disabled.')
])
return redirect(reverse('control:user.settings.2fa'))
class User2FARegenerateEmergencyView(RecentAuthenticationRequiredMixin, TemplateView):
template_name = 'pretixcontrol/user/2fa_regenemergency.html'
def post(self, request, *args, **kwargs):
d = StaticDevice.objects.get(user=self.request.user, name='emergency')
d.token_set.all().delete()
for i in range(10):
d.token_set.create(token=get_random_string(length=12, allowed_chars='1234567890'))
self.request.user.log_action('pretix.user.settings.2fa.regenemergency', user=self.request.user)
self.request.user.send_security_notice([
_('Your two-factor emergency codes have been regenerated.')
])
messages.success(request, _('Your emergency codes have been newly generated. Remember to store them in a safe '
'place in case you lose access to your devices.'))
return redirect(reverse('control:user.settings.2fa'))