forked from CGM_Public/pretix_original
Add optional timeouts for backend sessions
This commit is contained in:
@@ -1,4 +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,
|
||||
@@ -15,6 +16,7 @@ class LoginForm(forms.Form):
|
||||
"""
|
||||
email = forms.EmailField(label=_("E-mail"), max_length=254)
|
||||
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."),
|
||||
@@ -29,6 +31,8 @@ class LoginForm(forms.Form):
|
||||
self.request = request
|
||||
self.user_cache = None
|
||||
super().__init__(*args, **kwargs)
|
||||
if not settings.PRETIX_LONG_SESSIONS:
|
||||
del self.fields['keep_logged_in']
|
||||
|
||||
def clean(self):
|
||||
email = self.cleaned_data.get('email')
|
||||
@@ -90,6 +94,12 @@ class RegistrationForm(forms.Form):
|
||||
}),
|
||||
required=True
|
||||
)
|
||||
keep_logged_in = forms.BooleanField(label=_("Keep me logged in"), required=False)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if not settings.PRETIX_LONG_SESSIONS:
|
||||
del self.fields['keep_logged_in']
|
||||
|
||||
def clean(self):
|
||||
password1 = self.cleaned_data.get('password', '')
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
from urllib.parse import urljoin, urlparse
|
||||
import time
|
||||
from urllib.parse import quote, urljoin, urlparse
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import REDIRECT_FIELD_NAME
|
||||
from django.core.urlresolvers import get_script_prefix, resolve
|
||||
from django.contrib.auth import REDIRECT_FIELD_NAME, logout
|
||||
from django.core.urlresolvers import get_script_prefix, resolve, reverse
|
||||
from django.http import Http404
|
||||
from django.shortcuts import redirect, resolve_url
|
||||
from django.utils.deprecation import MiddlewareMixin
|
||||
@@ -28,6 +29,24 @@ class PermissionMiddleware(MiddlewareMixin):
|
||||
"auth.invite",
|
||||
)
|
||||
|
||||
def _login_redirect(self, request):
|
||||
# Taken from django/contrib/auth/decorators.py
|
||||
path = request.build_absolute_uri()
|
||||
# urlparse chokes on lazy objects in Python 3, force to str
|
||||
resolved_login_url = force_str(
|
||||
resolve_url(settings.LOGIN_URL_CONTROL))
|
||||
# If the login url is the same scheme and net location then just
|
||||
# use the path as the "next" url.
|
||||
login_scheme, login_netloc = urlparse(resolved_login_url)[:2]
|
||||
current_scheme, current_netloc = urlparse(path)[:2]
|
||||
if ((not login_scheme or login_scheme == current_scheme) and
|
||||
(not login_netloc or login_netloc == current_netloc)):
|
||||
path = request.get_full_path()
|
||||
from django.contrib.auth.views import redirect_to_login
|
||||
|
||||
return redirect_to_login(
|
||||
path, resolved_login_url, REDIRECT_FIELD_NAME)
|
||||
|
||||
def process_request(self, request):
|
||||
url = resolve(request.path_info)
|
||||
url_name = url.url_name
|
||||
@@ -42,22 +61,18 @@ class PermissionMiddleware(MiddlewareMixin):
|
||||
if url_name in self.EXCEPTIONS:
|
||||
return
|
||||
if not request.user.is_authenticated:
|
||||
# Taken from django/contrib/auth/decorators.py
|
||||
path = request.build_absolute_uri()
|
||||
# urlparse chokes on lazy objects in Python 3, force to str
|
||||
resolved_login_url = force_str(
|
||||
resolve_url(settings.LOGIN_URL_CONTROL))
|
||||
# If the login url is the same scheme and net location then just
|
||||
# use the path as the "next" url.
|
||||
login_scheme, login_netloc = urlparse(resolved_login_url)[:2]
|
||||
current_scheme, current_netloc = urlparse(path)[:2]
|
||||
if ((not login_scheme or login_scheme == current_scheme) and
|
||||
(not login_netloc or login_netloc == current_netloc)):
|
||||
path = request.get_full_path()
|
||||
from django.contrib.auth.views import redirect_to_login
|
||||
return self._login_redirect(request)
|
||||
|
||||
return redirect_to_login(
|
||||
path, resolved_login_url, REDIRECT_FIELD_NAME)
|
||||
if not settings.PRETIX_LONG_SESSIONS or not request.session.get('pretix_auth_long_session', False):
|
||||
last_used = request.session.get('pretix_auth_last_used', 0) or time.time()
|
||||
if time.time() - request.session.get('pretix_auth_login_time', 0) > settings.PRETIX_SESSION_TIMEOUT_ABSOLUTE:
|
||||
logout(request)
|
||||
request.session['pretix_auth_login_time'] = 0
|
||||
return self._login_redirect(request)
|
||||
if time.time() - last_used > settings.PRETIX_SESSION_TIMEOUT_RELATIVE and url_name != 'user.reauth':
|
||||
return redirect(reverse('control:user.reauth') + '?next=' + quote(request.get_full_path()))
|
||||
|
||||
request.session['pretix_auth_last_used'] = int(time.time())
|
||||
|
||||
if 'event' in url.kwargs and 'organizer' in url.kwargs:
|
||||
request.event = Event.objects.filter(
|
||||
|
||||
@@ -8,24 +8,23 @@
|
||||
{% csrf_token %}
|
||||
{% bootstrap_field form.email %}
|
||||
{% bootstrap_field form.password %}
|
||||
{% if form.keep_logged_in %}
|
||||
{% bootstrap_field form.keep_logged_in %}
|
||||
{% endif %}
|
||||
<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">
|
||||
<a href="{% url "control:auth.forgot" %}" class="btn btn-link btn-block">
|
||||
{% trans "Lost password?" %}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
<button type="submit" class="btn btn-primary">
|
||||
{% trans "Log in" %}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{% if can_register %}
|
||||
<div class="form-group buttons">
|
||||
<a href="{% url "control:auth.register" %}" class="btn btn-link">
|
||||
{% if can_register %}
|
||||
<a href="{% url "control:auth.register" %}" class="btn btn-link btn-block">
|
||||
{% trans "Register" %}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
@@ -10,6 +10,9 @@
|
||||
{% bootstrap_field form.email %}
|
||||
{% bootstrap_field form.password %}
|
||||
{% bootstrap_field form.password_repeat %}
|
||||
{% if form.keep_logged_in %}
|
||||
{% bootstrap_field form.keep_logged_in %}
|
||||
{% endif %}
|
||||
<div class="form-group buttons">
|
||||
<a href="{% url "control:auth.login" %}" class="btn btn-link">
|
||||
« {% trans "Login" %}
|
||||
|
||||
@@ -1,26 +1,30 @@
|
||||
{% extends "pretixcontrol/base.html" %}
|
||||
{% extends "pretixcontrol/auth/base.html" %}
|
||||
{% load i18n %}
|
||||
{% load bootstrap3 %}
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-xs-12 col-md-4 col-md-offset-4">
|
||||
<form action="" method="post">
|
||||
{% csrf_token %}
|
||||
<h2><span class="fa fa-shield"></span> {% trans "Confirm password" %}</h2>
|
||||
<p>
|
||||
{% trans "Please confirm your password to continue with this operation. We'll remember your password for an hour or until you log out." %}
|
||||
</p>
|
||||
<div class="form-group">
|
||||
<input class="form-control" id="id_password" name="password" placeholder="{% trans "Password" %}"
|
||||
title="" type="password" required="">
|
||||
</div>
|
||||
<div class="form-group text-right">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
{% trans "Continue" %}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
<form class="form-signin" action="" method="post">
|
||||
<form action="" method="post">
|
||||
{% csrf_token %}
|
||||
<h3>{% trans "Welcome back!" %}</h3>
|
||||
<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>
|
||||
<div class="form-group">
|
||||
<input class="form-control" id="id_password" name="password" placeholder="{% trans "Password" %}"
|
||||
title="" type="password" required="" autofocus>
|
||||
</div>
|
||||
<div class="form-group text-right">
|
||||
<button type="submit" class="btn btn-primary btn-block">
|
||||
{% trans "Continue" %}
|
||||
</button>
|
||||
<a href={% url "control:auth.logout" %}"" class="btn btn-link btn-block">
|
||||
{% trans "Log in as someone else" %}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
{% endblock %}
|
||||
|
||||
@@ -42,6 +42,9 @@ def login(request):
|
||||
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()))
|
||||
@@ -93,6 +96,9 @@ def register(request):
|
||||
user.log_action('pretix.control.auth.user.created', user=user)
|
||||
auth_login(request, user)
|
||||
request.session['pretix_auth_login_time'] = int(time.time())
|
||||
request.session['pretix_auth_long_session'] = (
|
||||
settings.PRETIX_LONG_SESSIONS and form.cleaned_data.get('keep_logged_in', False)
|
||||
)
|
||||
return redirect('control:index')
|
||||
else:
|
||||
form = RegistrationForm()
|
||||
@@ -144,6 +150,9 @@ def invite(request, token):
|
||||
user.log_action('pretix.control.auth.user.created', user=user)
|
||||
auth_login(request, user)
|
||||
request.session['pretix_auth_login_time'] = int(time.time())
|
||||
request.session['pretix_auth_long_session'] = (
|
||||
settings.PRETIX_LONG_SESSIONS and form.cleaned_data.get('keep_logged_in', False)
|
||||
)
|
||||
|
||||
with transaction.atomic():
|
||||
inv.team.members.add(request.user)
|
||||
|
||||
@@ -42,7 +42,9 @@ class ReauthView(TemplateView):
|
||||
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())
|
||||
t = int(time.time())
|
||||
request.session['pretix_auth_login_time'] = t
|
||||
request.session['pretix_auth_last_used'] = t
|
||||
if "next" in request.GET and is_safe_url(request.GET.get("next")):
|
||||
return redirect(request.GET.get("next"))
|
||||
return redirect(reverse('control:index'))
|
||||
|
||||
@@ -84,6 +84,9 @@ MEDIA_URL = config.get('urls', 'media', fallback='/media/')
|
||||
PRETIX_INSTANCE_NAME = config.get('pretix', 'instance_name', fallback='pretix.de')
|
||||
PRETIX_REGISTRATION = config.getboolean('pretix', 'registration', fallback=True)
|
||||
PRETIX_PASSWORD_RESET = config.getboolean('pretix', 'password_reset', fallback=True)
|
||||
PRETIX_LONG_SESSIONS = config.getboolean('pretix', 'long_sessions', fallback=True)
|
||||
PRETIX_SESSION_TIMEOUT_RELATIVE = 3600 * 3
|
||||
PRETIX_SESSION_TIMEOUT_ABSOLUTE = 3600 * 12
|
||||
|
||||
SITE_URL = config.get('pretix', 'url', fallback='http://localhost')
|
||||
if SITE_URL.endswith('/'):
|
||||
|
||||
@@ -7,7 +7,7 @@ from django.contrib.auth.tokens import (
|
||||
PasswordResetTokenGenerator, default_token_generator,
|
||||
)
|
||||
from django.core import mail as djmail
|
||||
from django.test import TestCase
|
||||
from django.test import TestCase, override_settings
|
||||
from django_otp.oath import TOTP
|
||||
from django_otp.plugins.otp_totp.models import TOTPDevice
|
||||
from u2flib_server.jsapi import JSONDict
|
||||
@@ -33,6 +33,17 @@ class LoginFormTest(TestCase):
|
||||
'password': 'dummy',
|
||||
})
|
||||
self.assertEqual(response.status_code, 302)
|
||||
assert time.time() - self.client.session['pretix_auth_login_time'] < 60
|
||||
assert not self.client.session['pretix_auth_long_session']
|
||||
|
||||
def test_set_long_session(self):
|
||||
response = self.client.post('/control/login', {
|
||||
'email': 'dummy@dummy.dummy',
|
||||
'password': 'dummy',
|
||||
'keep_logged_in': 'on'
|
||||
})
|
||||
self.assertEqual(response.status_code, 302)
|
||||
assert self.client.session['pretix_auth_long_session']
|
||||
|
||||
def test_inactive_account(self):
|
||||
self.user.is_active = False
|
||||
@@ -187,6 +198,8 @@ class RegistrationFormTest(TestCase):
|
||||
'password_repeat': 'foobarbar'
|
||||
})
|
||||
self.assertEqual(response.status_code, 302)
|
||||
assert time.time() - self.client.session['pretix_auth_login_time'] < 60
|
||||
assert not self.client.session['pretix_auth_long_session']
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -202,6 +215,7 @@ class Login2FAFormTest(TestCase):
|
||||
session = self.client.session
|
||||
session['pretix_auth_2fa_user'] = self.user.pk
|
||||
session['pretix_auth_2fa_time'] = str(int(time.time()))
|
||||
session['pretix_auth_long_session'] = False
|
||||
session.save()
|
||||
|
||||
def test_invalid_session(self):
|
||||
@@ -245,6 +259,8 @@ class Login2FAFormTest(TestCase):
|
||||
})
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertIn('/control/events/', response['Location'])
|
||||
assert time.time() - self.client.session['pretix_auth_login_time'] < 60
|
||||
assert not self.client.session['pretix_auth_long_session']
|
||||
|
||||
def test_u2f_invalid(self):
|
||||
def fail(*args, **kwargs):
|
||||
@@ -475,3 +491,90 @@ class PasswordRecoveryFormTest(TestCase):
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.user = User.objects.get(id=self.user.id)
|
||||
self.assertTrue(self.user.check_password('demo'))
|
||||
|
||||
|
||||
class SessionTimeOutTest(TestCase):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.user = User.objects.create_user('demo@demo.dummy', 'demo')
|
||||
self.client.login(email='demo@demo.dummy', password='demo')
|
||||
|
||||
def test_log_out_after_absolute_timeout(self):
|
||||
session = self.client.session
|
||||
session['pretix_auth_long_session'] = False
|
||||
session['pretix_auth_login_time'] = int(time.time()) - 3600 * 12 - 60
|
||||
session.save()
|
||||
|
||||
response = self.client.get('/control/')
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
def test_dont_logout_before_absolute_timeout(self):
|
||||
session = self.client.session
|
||||
session['pretix_auth_long_session'] = True
|
||||
session['pretix_auth_login_time'] = int(time.time()) - 3600 * 12 + 60
|
||||
session.save()
|
||||
|
||||
response = self.client.get('/control/')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
@override_settings(PRETIX_LONG_SESSIONS=False)
|
||||
def test_ignore_long_session_if_disabled_in_config(self):
|
||||
session = self.client.session
|
||||
session['pretix_auth_long_session'] = True
|
||||
session['pretix_auth_login_time'] = int(time.time()) - 3600 * 12 - 60
|
||||
session.save()
|
||||
|
||||
response = self.client.get('/control/')
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
def test_dont_logout_in_long_session(self):
|
||||
session = self.client.session
|
||||
session['pretix_auth_long_session'] = True
|
||||
session['pretix_auth_login_time'] = int(time.time()) - 3600 * 12 - 60
|
||||
session.save()
|
||||
|
||||
response = self.client.get('/control/')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_log_out_after_relative_timeout(self):
|
||||
session = self.client.session
|
||||
session['pretix_auth_long_session'] = False
|
||||
session['pretix_auth_login_time'] = int(time.time()) - 3600 * 6
|
||||
session['pretix_auth_last_used'] = int(time.time()) - 3600 * 3 - 60
|
||||
session.save()
|
||||
|
||||
response = self.client.get('/control/')
|
||||
self.assertEqual(response.status_code, 302)
|
||||
|
||||
def test_dont_logout_before_relative_timeout(self):
|
||||
session = self.client.session
|
||||
session['pretix_auth_long_session'] = True
|
||||
session['pretix_auth_login_time'] = int(time.time()) - 3600 * 6
|
||||
session['pretix_auth_last_used'] = int(time.time()) - 3600 * 3 + 60
|
||||
session.save()
|
||||
|
||||
response = self.client.get('/control/')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_dont_logout_by_relative_in_long_session(self):
|
||||
session = self.client.session
|
||||
session['pretix_auth_long_session'] = True
|
||||
session['pretix_auth_login_time'] = int(time.time()) - 3600 * 5
|
||||
session['pretix_auth_last_used'] = int(time.time()) - 3600 * 3 - 60
|
||||
session.save()
|
||||
|
||||
response = self.client.get('/control/')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_update_session_activity(self):
|
||||
t1 = int(time.time()) - 5
|
||||
session = self.client.session
|
||||
session['pretix_auth_long_session'] = False
|
||||
session['pretix_auth_login_time'] = int(time.time()) - 3600 * 5
|
||||
session['pretix_auth_last_used'] = t1
|
||||
session.save()
|
||||
|
||||
response = self.client.get('/control/')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
assert self.client.session['pretix_auth_last_used'] > t1
|
||||
|
||||
Reference in New Issue
Block a user