forked from CGM_Public/pretix_original
* Drag-and-drop: Force csrf_token to be present * Rough design * Missing file * b.visble * Forms * Docs * Tests * Fix variable
This commit is contained in:
109
src/pretix/base/auth.py
Normal file
109
src/pretix/base/auth.py
Normal file
@@ -0,0 +1,109 @@
|
||||
from collections import OrderedDict
|
||||
from importlib import import_module
|
||||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import authenticate
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
|
||||
def get_auth_backends():
|
||||
backends = {}
|
||||
for b in settings.PRETIX_AUTH_BACKENDS:
|
||||
mod, name = b.rsplit('.', 1)
|
||||
b = getattr(import_module(mod), name)()
|
||||
backends[b.identifier] = b
|
||||
return backends
|
||||
|
||||
|
||||
class BaseAuthBackend:
|
||||
"""
|
||||
This base class defines the interface that needs to be implemented by every class that supplies
|
||||
an authentication method to pretix. Please note that pretix authentication backends are different
|
||||
from plain Django authentication backends! Be sure to read the documentation chapter on authentication
|
||||
backends before you implement one.
|
||||
"""
|
||||
|
||||
@property
|
||||
def identifier(self):
|
||||
"""
|
||||
A short and unique identifier for this authentication backend.
|
||||
This should only contain lowercase letters and in most cases will
|
||||
be the same as your package name.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def verbose_name(self):
|
||||
"""
|
||||
A human-readable name of this authentication backend.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@property
|
||||
def visible(self):
|
||||
"""
|
||||
Whether or not this backend can be selected by users actively. Set this to ``False``
|
||||
if you only implement ``request_authenticate``.
|
||||
"""
|
||||
return True
|
||||
|
||||
@property
|
||||
def login_form_fields(self) -> dict:
|
||||
"""
|
||||
This property may return form fields that the user needs to fill in to log in.
|
||||
"""
|
||||
return {}
|
||||
|
||||
def form_authenticate(self, request, form_data):
|
||||
"""
|
||||
This method will be called after the user filled in the login form. ``request`` will contain
|
||||
the current request and ``form_data`` the input for the form fields defined in ``login_form_fields``.
|
||||
You are expected to either return a ``User`` object (if login was successful) or ``None``.
|
||||
"""
|
||||
return
|
||||
|
||||
def request_authenticate(self, request):
|
||||
"""
|
||||
This method will be called when the user opens the login form. If the user already has a valid session
|
||||
according to your login mechanism, for example a cookie set by a different system or HTTP header set by a
|
||||
reverse proxy, you can directly return a ``User`` object that will be logged in.
|
||||
|
||||
``request`` will contain the current request.
|
||||
You are expected to either return a ``User`` object (if login was successful) or ``None``.
|
||||
"""
|
||||
return
|
||||
|
||||
def authentication_url(self, request):
|
||||
"""
|
||||
This method will be called to populate the URL for your authentication method's tab on the login page.
|
||||
For example, if your method works through OAuth, you could return the URL of the OAuth authorization URL the
|
||||
user needs to visit.
|
||||
|
||||
If you return ``None`` (the default), the link will point to a page that shows the form defined by
|
||||
``login_form_fields``.
|
||||
"""
|
||||
return
|
||||
|
||||
|
||||
class NativeAuthBackend(BaseAuthBackend):
|
||||
identifier = 'native'
|
||||
verbose_name = _('pretix User')
|
||||
|
||||
@property
|
||||
def login_form_fields(self) -> dict:
|
||||
"""
|
||||
This property may return form fields that the user needs to fill in
|
||||
to log in.
|
||||
"""
|
||||
d = OrderedDict([
|
||||
('email', forms.EmailField(label=_("E-mail"), max_length=254,
|
||||
widget=forms.EmailInput(attrs={'autofocus': 'autofocus'}))),
|
||||
('password', forms.CharField(label=_("Password"), widget=forms.PasswordInput)),
|
||||
])
|
||||
return d
|
||||
|
||||
def form_authenticate(self, request, form_data):
|
||||
u = authenticate(request=request, email=form_data['email'].lower(), password=form_data['password'])
|
||||
if u and u.auth_backend == self.identifier:
|
||||
return u
|
||||
@@ -1,6 +1,5 @@
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import authenticate
|
||||
from django.contrib.auth.password_validation import (
|
||||
password_validators_help_texts, validate_password,
|
||||
)
|
||||
@@ -14,32 +13,33 @@ class LoginForm(forms.Form):
|
||||
Base class for authenticating users. Extend this to get a form that accepts
|
||||
username/password logins.
|
||||
"""
|
||||
email = forms.EmailField(label=_("E-mail"), max_length=254, widget=forms.EmailInput(attrs={'autofocus': 'autofocus'}))
|
||||
password = forms.CharField(label=_("Password"), widget=forms.PasswordInput)
|
||||
keep_logged_in = forms.BooleanField(label=_("Keep me logged in"), required=False)
|
||||
|
||||
error_messages = {
|
||||
'invalid_login': _("Please enter a correct email address and password."),
|
||||
'invalid_login': _("This combination of credentials is not known to our system."),
|
||||
'inactive': _("This account is inactive.")
|
||||
}
|
||||
|
||||
def __init__(self, request=None, *args, **kwargs):
|
||||
def __init__(self, backend, request=None, *args, **kwargs):
|
||||
"""
|
||||
The 'request' parameter is set for custom auth use by subclasses.
|
||||
The form data comes in via the standard 'data' kwarg.
|
||||
"""
|
||||
self.request = request
|
||||
self.user_cache = None
|
||||
self.backend = backend
|
||||
super().__init__(*args, **kwargs)
|
||||
for k, f in backend.login_form_fields.items():
|
||||
self.fields[k] = f
|
||||
|
||||
if not settings.PRETIX_LONG_SESSIONS:
|
||||
del self.fields['keep_logged_in']
|
||||
else:
|
||||
self.fields.move_to_end('keep_logged_in')
|
||||
|
||||
def clean(self):
|
||||
email = self.cleaned_data.get('email')
|
||||
password = self.cleaned_data.get('password')
|
||||
|
||||
if email and password:
|
||||
self.user_cache = authenticate(request=self.request, email=email.lower(), password=password)
|
||||
if all(k in self.cleaned_data for k, f in self.fields.items() if f.required):
|
||||
self.user_cache = self.backend.form_authenticate(self.request, self.cleaned_data)
|
||||
if self.user_cache is None:
|
||||
raise forms.ValidationError(
|
||||
self.error_messages['invalid_login'],
|
||||
@@ -181,3 +181,44 @@ class PasswordForgotForm(forms.Form):
|
||||
|
||||
def clean_email(self):
|
||||
return self.cleaned_data['email']
|
||||
|
||||
|
||||
class ReauthForm(forms.Form):
|
||||
error_messages = {
|
||||
'invalid_login': _("This combination of credentials is not known to our system."),
|
||||
'inactive': _("This account is inactive.")
|
||||
}
|
||||
|
||||
def __init__(self, backend, user, request=None, *args, **kwargs):
|
||||
"""
|
||||
The 'request' parameter is set for custom auth use by subclasses.
|
||||
The form data comes in via the standard 'data' kwarg.
|
||||
"""
|
||||
self.request = request
|
||||
self.user = user
|
||||
self.backend = backend
|
||||
super().__init__(*args, **kwargs)
|
||||
for k, f in backend.login_form_fields.items():
|
||||
self.fields[k] = f
|
||||
if 'email' in self.fields:
|
||||
self.fields['email'].disabled = True
|
||||
|
||||
def clean(self):
|
||||
self.cleaned_data['email'] = self.user.email
|
||||
user_cache = self.backend.form_authenticate(self.request, self.cleaned_data)
|
||||
if user_cache != self.user:
|
||||
raise forms.ValidationError(
|
||||
self.error_messages['invalid_login'],
|
||||
code='invalid_login'
|
||||
)
|
||||
else:
|
||||
self.confirm_login_allowed(user_cache)
|
||||
|
||||
return self.cleaned_data
|
||||
|
||||
def confirm_login_allowed(self, user: User):
|
||||
if not user.is_active:
|
||||
raise forms.ValidationError(
|
||||
self.error_messages['inactive'],
|
||||
code='inactive',
|
||||
)
|
||||
|
||||
@@ -56,6 +56,11 @@ class UserSettingsForm(forms.ModelForm):
|
||||
self.user = kwargs.pop('user')
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['email'].required = True
|
||||
if self.user.auth_backend != 'native':
|
||||
del self.fields['old_pw']
|
||||
del self.fields['new_pw']
|
||||
del self.fields['new_pw_repeat']
|
||||
self.fields['email'].disabled = True
|
||||
|
||||
def clean_old_pw(self):
|
||||
old_pw = self.cleaned_data.get('old_pw')
|
||||
|
||||
17
src/pretix/base/migrations/0137_auto_20191015_1141.py
Normal file
17
src/pretix/base/migrations/0137_auto_20191015_1141.py
Normal file
@@ -0,0 +1,17 @@
|
||||
# Generated by Django 2.2.4 on 2019-10-15 11:41
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('pretixbase', '0136_auto_20190918_1742'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='user',
|
||||
name='auth_backend',
|
||||
field=models.CharField(default='native', max_length=255),
|
||||
),
|
||||
]
|
||||
@@ -109,6 +109,7 @@ class User(AbstractBaseUser, PermissionsMixin, LoggingMixin):
|
||||
help_text=_('If turned off, you will not get any notifications.')
|
||||
)
|
||||
notifications_token = models.CharField(max_length=255, default=generate_notifications_token)
|
||||
auth_backend = models.CharField(max_length=255, default='native')
|
||||
|
||||
objects = UserManager()
|
||||
|
||||
|
||||
@@ -56,6 +56,10 @@ class UserEditForm(forms.ModelForm):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.fields['email'].required = True
|
||||
self.fields['last_login'].disabled = True
|
||||
if self.instance and self.instance.auth_backend != 'native':
|
||||
del self.fields['new_pw']
|
||||
del self.fields['new_pw_repeat']
|
||||
self.fields['email'].disabled = True
|
||||
|
||||
def clean_email(self):
|
||||
email = self.cleaned_data['email']
|
||||
|
||||
@@ -2,28 +2,40 @@
|
||||
{% load bootstrap3 %}
|
||||
{% load i18n %}
|
||||
{% load static %}
|
||||
{% load urlreplace %}
|
||||
{% block content %}
|
||||
<form class="form-signin" action="" method="post">
|
||||
{% bootstrap_form_errors form type='all' layout='inline' %}
|
||||
{% csrf_token %}
|
||||
{% bootstrap_field form.email %}
|
||||
{% bootstrap_field form.password %}
|
||||
{% if form.keep_logged_in %}
|
||||
{% bootstrap_field form.keep_logged_in %}
|
||||
{% if backends|length > 1 %}
|
||||
<ul class="nav nav-pills">
|
||||
{% for b in backends %}
|
||||
{% if b.visible %}
|
||||
<li class="{% if backend.identifier == b.identifier %}active{% endif %}">
|
||||
<a href="{% if b.url %}{{ b.url }}{% else %}?{% url_replace request "backend" b.identifier %}{% endif %}">
|
||||
{{ b.verbose_name }}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<br>
|
||||
{% endif %}
|
||||
{% csrf_token %}
|
||||
{% bootstrap_form form %}
|
||||
<div class="form-group buttons">
|
||||
<button type="submit" class="btn btn-primary btn-block">
|
||||
{% trans "Log in" %}
|
||||
</button>
|
||||
{% if can_reset %}
|
||||
<a href="{% url "control:auth.forgot" %}" class="btn btn-link btn-block">
|
||||
{% trans "Lost password?" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if can_register %}
|
||||
<a href="{% url "control:auth.register" %}" class="btn btn-link btn-block">
|
||||
{% trans "Register" %}
|
||||
</a>
|
||||
{% if backend.identifier == "native" %}
|
||||
{% if can_reset %}
|
||||
<a href="{% url "control:auth.forgot" %}" class="btn btn-link btn-block">
|
||||
{% trans "Lost password?" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if can_register %}
|
||||
<a href="{% url "control:auth.register" %}" class="btn btn-link btn-block">
|
||||
{% trans "Register" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -10,14 +10,9 @@
|
||||
<p>
|
||||
{% trans "We just want to make sure it's really you. Please re-enter your password to continue." %}
|
||||
</p>
|
||||
<div class="form-group">
|
||||
<input class="form-control"
|
||||
value="{{ request.user.get_full_name }}" disabled>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<input class="form-control" id="id_password" name="password" placeholder="{% trans "Password" %}"
|
||||
title="" type="password" required="" autofocus>
|
||||
</div>
|
||||
{% bootstrap_form form %}
|
||||
<input class="form-control" id="webauthn-response" name="webauthn"
|
||||
type="hidden">
|
||||
{% if jsondata %}
|
||||
<div class="sr-only alert alert-danger" id="webauthn-error">
|
||||
{% trans "WebAuthn failed. Check that the correct authentication device is correctly plugged in." %}
|
||||
@@ -45,6 +40,7 @@
|
||||
<script type="text/javascript" src="{% static "jquery/js/jquery-2.1.1.min.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixcontrol/js/base64js.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/webauthn.js" %}"></script>
|
||||
<script type="text/javascript" src="{% static "pretixcontrol/js/ui/focus.js" %}"></script>
|
||||
{% endcompress %}
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
@@ -33,10 +33,14 @@
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Login settings" %}</legend>
|
||||
{% bootstrap_field form.old_pw layout='horizontal' %}
|
||||
{% if form.old_pw %}
|
||||
{% bootstrap_field form.old_pw layout='horizontal' %}
|
||||
{% endif %}
|
||||
{% bootstrap_field form.email layout='horizontal' %}
|
||||
{% bootstrap_field form.new_pw layout='horizontal' %}
|
||||
{% bootstrap_field form.new_pw_repeat layout='horizontal' %}
|
||||
{% if form.new_pw %}
|
||||
{% bootstrap_field form.new_pw layout='horizontal' %}
|
||||
{% bootstrap_field form.new_pw_repeat layout='horizontal' %}
|
||||
{% endif %}
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label" for="id_new_pw_repeat">{% trans "Two-factor authentication" %}</label>
|
||||
<div class="col-md-9 static-form-row">
|
||||
|
||||
@@ -5,10 +5,12 @@
|
||||
{% block content %}
|
||||
<h1>{% trans "User" %} {{ user.email }}</h1>
|
||||
<p>
|
||||
<form action="{% url "control:users.reset" id=user.pk %}" method="post" class="form-inline helper-display-inline">
|
||||
{% csrf_token %}
|
||||
<button class="btn btn-default">{% trans "Send password reset email" %}</button>
|
||||
</form>
|
||||
{% if user.auth_backend == "native" %}
|
||||
<form action="{% url "control:users.reset" id=user.pk %}" method="post" class="form-inline helper-display-inline">
|
||||
{% csrf_token %}
|
||||
<button class="btn btn-default">{% trans "Send password reset email" %}</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
<form action="{% url "control:users.impersonate" id=user.pk %}" method="post" class="form-inline helper-display-inline">
|
||||
{% csrf_token %}
|
||||
<button class="btn btn-default">{% trans "Impersonate user" %}</button>
|
||||
@@ -30,9 +32,17 @@
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{% trans "Log-in settings" %}</legend>
|
||||
<div class="form-group">
|
||||
<label class="col-md-3 control-label">{% trans "Authentication backend" %}</label>
|
||||
<div class="col-md-9">
|
||||
<input name="text" value="{{ backend }}" class="form-control" disabled>
|
||||
</div>
|
||||
</div>
|
||||
{% bootstrap_field form.email layout='control' %}
|
||||
{% bootstrap_field form.new_pw layout='control' %}
|
||||
{% bootstrap_field form.new_pw_repeat layout='control' %}
|
||||
{% if form.new_pw %}
|
||||
{% bootstrap_field form.new_pw layout='control' %}
|
||||
{% bootstrap_field form.new_pw_repeat layout='control' %}
|
||||
{% endif %}
|
||||
{% bootstrap_field form.last_login layout='control' %}
|
||||
{% bootstrap_field form.require_2fa layout='control' %}
|
||||
</fieldset>
|
||||
|
||||
@@ -20,6 +20,7 @@ from django.utils.translation import ugettext_lazy as _
|
||||
from django.views.generic import TemplateView
|
||||
from django_otp import match_token
|
||||
|
||||
from pretix.base.auth import get_auth_backends
|
||||
from pretix.base.forms.auth import (
|
||||
LoginForm, PasswordForgotForm, PasswordRecoverForm, RegistrationForm,
|
||||
)
|
||||
@@ -30,38 +31,59 @@ from pretix.helpers.webauthn import generate_challenge
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def process_login(request, user, keep_logged_in):
|
||||
"""
|
||||
This method allows you to return a response to a successful log-in. This will set all session values correctly
|
||||
and redirect to either the URL specified in the ``next`` parameter, or the 2FA login screen, or the dashboard.
|
||||
|
||||
:return: This method returns a ``HttpResponse``.
|
||||
"""
|
||||
request.session['pretix_auth_long_session'] = settings.PRETIX_LONG_SESSIONS and keep_logged_in
|
||||
if user.require_2fa:
|
||||
request.session['pretix_auth_2fa_user'] = user.pk
|
||||
request.session['pretix_auth_2fa_time'] = str(int(time.time()))
|
||||
twofa_url = reverse('control:auth.login.2fa')
|
||||
if "next" in request.GET and is_safe_url(request.GET.get("next"), allowed_hosts=None):
|
||||
twofa_url += '?next=' + quote(request.GET.get('next'))
|
||||
return redirect(twofa_url)
|
||||
else:
|
||||
auth_login(request, user)
|
||||
request.session['pretix_auth_login_time'] = int(time.time())
|
||||
if "next" in request.GET and is_safe_url(request.GET.get("next"), allowed_hosts=None):
|
||||
return redirect(request.GET.get("next"))
|
||||
return redirect(reverse('control:index'))
|
||||
|
||||
|
||||
def login(request):
|
||||
"""
|
||||
Render and process a most basic login form. Takes an URL as GET
|
||||
parameter "next" for redirection after successful login
|
||||
"""
|
||||
ctx = {}
|
||||
backenddict = get_auth_backends()
|
||||
backends = sorted(backenddict.values(), key=lambda b: (b.identifier != "native", b.verbose_name))
|
||||
for b in backends:
|
||||
u = b.request_authenticate(request)
|
||||
if u and u.auth_backend == b.identifier:
|
||||
return process_login(request, u, False)
|
||||
b.url = b.authentication_url(request)
|
||||
|
||||
backend = backenddict.get(request.GET.get('backend', 'native'), backends[0])
|
||||
if not backend.visible:
|
||||
backend = [b for b in backends if b.visible][0]
|
||||
if request.user.is_authenticated:
|
||||
return redirect(request.GET.get("next", 'control:index'))
|
||||
if request.method == 'POST':
|
||||
form = LoginForm(data=request.POST)
|
||||
if form.is_valid() and form.user_cache:
|
||||
request.session['pretix_auth_long_session'] = (
|
||||
settings.PRETIX_LONG_SESSIONS and form.cleaned_data.get('keep_logged_in', False)
|
||||
)
|
||||
if form.user_cache.require_2fa:
|
||||
request.session['pretix_auth_2fa_user'] = form.user_cache.pk
|
||||
request.session['pretix_auth_2fa_time'] = str(int(time.time()))
|
||||
twofa_url = reverse('control:auth.login.2fa')
|
||||
if "next" in request.GET and is_safe_url(request.GET.get("next"), allowed_hosts=None):
|
||||
twofa_url += '?next=' + quote(request.GET.get('next'))
|
||||
return redirect(twofa_url)
|
||||
else:
|
||||
auth_login(request, form.user_cache)
|
||||
request.session['pretix_auth_login_time'] = int(time.time())
|
||||
if "next" in request.GET and is_safe_url(request.GET.get("next"), allowed_hosts=None):
|
||||
return redirect(request.GET.get("next"))
|
||||
return redirect(reverse('control:index'))
|
||||
form = LoginForm(backend=backend, data=request.POST)
|
||||
if form.is_valid() and form.user_cache and form.user_cache.auth_backend == backend.identifier:
|
||||
return process_login(request, form.user_cache, form.cleaned_data.get('keep_logged_in', False))
|
||||
else:
|
||||
form = LoginForm()
|
||||
form = LoginForm(backend=backend)
|
||||
ctx['form'] = form
|
||||
ctx['can_register'] = settings.PRETIX_REGISTRATION
|
||||
ctx['can_reset'] = settings.PRETIX_PASSWORD_RESET
|
||||
ctx['backends'] = backends
|
||||
ctx['backend'] = backend
|
||||
return render(request, 'pretixcontrol/auth/login.html', ctx)
|
||||
|
||||
|
||||
@@ -83,7 +105,7 @@ def register(request):
|
||||
"""
|
||||
Render and process a basic registration form.
|
||||
"""
|
||||
if not settings.PRETIX_REGISTRATION:
|
||||
if not settings.PRETIX_REGISTRATION or 'native' not in get_auth_backends():
|
||||
raise PermissionDenied('Registration is disabled')
|
||||
ctx = {}
|
||||
if request.user.is_authenticated:
|
||||
@@ -116,6 +138,9 @@ def invite(request, token):
|
||||
"""
|
||||
ctx = {}
|
||||
|
||||
if 'native' not in get_auth_backends():
|
||||
raise PermissionDenied('Invites are disabled')
|
||||
|
||||
try:
|
||||
inv = TeamInvite.objects.get(token=token)
|
||||
except TeamInvite.DoesNotExist:
|
||||
@@ -185,7 +210,7 @@ class Forgot(TemplateView):
|
||||
template_name = 'pretixcontrol/auth/forgot.html'
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
if not settings.PRETIX_PASSWORD_RESET:
|
||||
if not settings.PRETIX_PASSWORD_RESET or 'native' not in get_auth_backends():
|
||||
raise PermissionDenied('Password reset is disabled')
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
@@ -257,15 +282,15 @@ class Recover(TemplateView):
|
||||
}
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
if not settings.PRETIX_PASSWORD_RESET:
|
||||
raise PermissionDenied('Password reset is disabled')
|
||||
if not settings.PRETIX_PASSWORD_RESET or 'native' not in get_auth_backends():
|
||||
raise PermissionDenied('Registration is disabled')
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
if request.user.is_authenticated:
|
||||
return redirect(request.GET.get("next", 'control:index'))
|
||||
try:
|
||||
user = User.objects.get(id=self.request.GET.get('id'))
|
||||
user = User.objects.get(id=self.request.GET.get('id'), auth_backend='native')
|
||||
except User.DoesNotExist:
|
||||
return self.invalid('unknownuser')
|
||||
if not default_token_generator.check_token(user, self.request.GET.get('token')):
|
||||
@@ -279,7 +304,7 @@ class Recover(TemplateView):
|
||||
def post(self, request, *args, **kwargs):
|
||||
if self.form.is_valid():
|
||||
try:
|
||||
user = User.objects.get(id=self.request.GET.get('id'))
|
||||
user = User.objects.get(id=self.request.GET.get('id'), auth_backend='native')
|
||||
except User.DoesNotExist:
|
||||
return self.invalid('unknownuser')
|
||||
if not default_token_generator.check_token(user, self.request.GET.get('token')):
|
||||
|
||||
@@ -20,6 +20,7 @@ from django.views.generic import (
|
||||
)
|
||||
|
||||
from pretix.api.models import WebHook
|
||||
from pretix.base.auth import get_auth_backends
|
||||
from pretix.base.models import Device, Organizer, Team, TeamInvite, User
|
||||
from pretix.base.models.event import Event, EventMetaProperty
|
||||
from pretix.base.models.organizer import TeamAPIToken
|
||||
@@ -602,6 +603,9 @@ class TeamMemberView(OrganizerDetailViewMixin, OrganizerPermissionRequiredMixin,
|
||||
if self.object.invites.filter(email__iexact=self.add_form.cleaned_data['user']).exists():
|
||||
messages.error(self.request, _('This user already has been invited for this team.'))
|
||||
return self.get(request, *args, **kwargs)
|
||||
if 'native' not in get_auth_backends():
|
||||
messages.error(self.request, _('Users need to have a pretix account before they can be invited.'))
|
||||
return self.get(request, *args, **kwargs)
|
||||
|
||||
invite = self.object.invites.create(email=self.add_form.cleaned_data['user'])
|
||||
self._send_invite(invite)
|
||||
|
||||
@@ -23,6 +23,8 @@ from django.views.generic import FormView, ListView, TemplateView, UpdateView
|
||||
from django_otp.plugins.otp_static.models import StaticDevice
|
||||
from django_otp.plugins.otp_totp.models import TOTPDevice
|
||||
|
||||
from pretix.base.auth import get_auth_backends
|
||||
from pretix.base.forms.auth import ReauthForm
|
||||
from pretix.base.forms.user import User2FADeviceAddForm, UserSettingsForm
|
||||
from pretix.base.models import (
|
||||
Event, LogEntry, NotificationSetting, U2FDevice, User, WebAuthnDevice,
|
||||
@@ -54,13 +56,13 @@ class ReauthView(TemplateView):
|
||||
template_name = 'pretixcontrol/user/reauth.html'
|
||||
|
||||
def post(self, request, *args, **kwargs):
|
||||
password = request.POST.get("password", "")
|
||||
r = request.POST.get("webauthn", "")
|
||||
valid = False
|
||||
|
||||
if 'webauthn_challenge' in self.request.session and password.startswith('{'):
|
||||
if 'webauthn_challenge' in self.request.session and r.startswith('{'):
|
||||
challenge = self.request.session['webauthn_challenge']
|
||||
|
||||
resp = json.loads(password)
|
||||
resp = json.loads(r)
|
||||
try:
|
||||
devices = [WebAuthnDevice.objects.get(user=self.request.user, credential_id=resp.get("id"))]
|
||||
except WebAuthnDevice.DoesNotExist:
|
||||
@@ -93,7 +95,7 @@ class ReauthView(TemplateView):
|
||||
valid = True
|
||||
break
|
||||
|
||||
valid = valid or request.user.check_password(password)
|
||||
valid = valid or self.form.is_valid()
|
||||
|
||||
if valid:
|
||||
t = int(time.time())
|
||||
@@ -106,6 +108,14 @@ class ReauthView(TemplateView):
|
||||
messages.error(request, _('The password you entered was invalid, please try again.'))
|
||||
return self.get(request, *args, **kwargs)
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
u = get_auth_backends()[request.user.auth_backend].request_authenticate(request)
|
||||
if u and u == request.user:
|
||||
if "next" in request.GET and is_safe_url(request.GET.get("next"), allowed_hosts=None):
|
||||
return redirect(request.GET.get("next"))
|
||||
return redirect(reverse('control:index'))
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data()
|
||||
if 'webauthn_challenge' in self.request.session:
|
||||
@@ -127,8 +137,21 @@ class ReauthView(TemplateView):
|
||||
'appid': get_u2f_appid(self.request)
|
||||
}
|
||||
ctx['jsondata'] = json.dumps(ad)
|
||||
ctx['form'] = self.form
|
||||
return ctx
|
||||
|
||||
@cached_property
|
||||
def form(self):
|
||||
return ReauthForm(
|
||||
user=self.request.user,
|
||||
backend=get_auth_backends()[self.request.user.auth_backend],
|
||||
request=self.request,
|
||||
data=self.request.POST if self.request.method == "POST" else None,
|
||||
initial={
|
||||
'email': self.request.user.email,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class UserSettings(UpdateView):
|
||||
model = User
|
||||
|
||||
@@ -11,6 +11,7 @@ from django.views import View
|
||||
from django.views.generic import ListView, TemplateView
|
||||
from hijack.helpers import login_user, release_hijack
|
||||
|
||||
from pretix.base.auth import get_auth_backends
|
||||
from pretix.base.models import User
|
||||
from pretix.base.services.mail import SendMailException
|
||||
from pretix.control.forms.filter import UserFilterForm
|
||||
@@ -52,6 +53,10 @@ class UserEditView(AdministratorPermissionRequiredMixin, RecentAuthenticationReq
|
||||
def get_context_data(self, **kwargs):
|
||||
ctx = super().get_context_data(**kwargs)
|
||||
ctx['teams'] = self.object.teams.select_related('organizer')
|
||||
b = get_auth_backends()
|
||||
ctx['backend'] = (
|
||||
b[self.object.auth_backend].verbose_name if self.object.auth_backend in b else self.object.auth_backend
|
||||
)
|
||||
return ctx
|
||||
|
||||
def get_success_url(self):
|
||||
|
||||
@@ -61,6 +61,8 @@ DEBUG = config.getboolean('django', 'debug', fallback=debug_fallback)
|
||||
|
||||
PDFTK = config.get('tools', 'pdftk', fallback=None)
|
||||
|
||||
PRETIX_AUTH_BACKENDS = config.get('pretix', 'auth_backends', fallback='pretix.base.auth.NativeAuthBackend').split(',')
|
||||
|
||||
db_backend = config.get('database', 'backend', fallback='sqlite3')
|
||||
if db_backend == 'postgresql_psycopg2':
|
||||
db_backend = 'postgresql'
|
||||
|
||||
3
src/pretix/static/pretixcontrol/js/ui/focus.js
Normal file
3
src/pretix/static/pretixcontrol/js/ui/focus.js
Normal file
@@ -0,0 +1,3 @@
|
||||
$(function () {
|
||||
$("input, select, textarea").not(":disabled").focus();
|
||||
});
|
||||
@@ -163,6 +163,7 @@ const startLogin = async (e) => {
|
||||
const transformedAssertionForServer = transformAssertionForServer(assertion);
|
||||
|
||||
// post the assertion to the server for verification.
|
||||
$("input, select, textarea").prop("required", false);
|
||||
$("#webauthn-response, #id_password").val(JSON.stringify(transformedAssertionForServer));
|
||||
$("#webauthn-form").submit();
|
||||
};
|
||||
|
||||
@@ -32,6 +32,10 @@ TEMPLATES[0]['OPTIONS']['loaders'] = (
|
||||
DEBUG = True
|
||||
DEBUG_PROPAGATE_EXCEPTIONS = True
|
||||
|
||||
PRETIX_AUTH_BACKENDS = [
|
||||
'pretix.base.auth.NativeAuthBackend',
|
||||
]
|
||||
|
||||
PASSWORD_HASHERS = ['django.contrib.auth.hashers.MD5PasswordHasher']
|
||||
|
||||
# Disable celery
|
||||
|
||||
Reference in New Issue
Block a user