forked from CGM_Public/pretix_original
* Send security notification when recovery code is used or created by admin "Where to store recovery codes" is one of these problems there is no right answer to, so many people store them in a less-than-optimal place. If that's the reality we live in, this PR adds at least a little security so one notices when they get used :) * Add sentence
1188 lines
47 KiB
Python
1188 lines
47 KiB
Python
#
|
|
# This file is part of pretix (Community Edition).
|
|
#
|
|
# Copyright (C) 2014-2020 Raphael Michel and contributors
|
|
# Copyright (C) 2020-today pretix GmbH and contributors
|
|
#
|
|
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General
|
|
# Public License as published by the Free Software Foundation in version 3 of the License.
|
|
#
|
|
# ADDITIONAL TERMS APPLY: Pursuant to Section 7 of the GNU Affero General Public License, additional terms are
|
|
# applicable granting you additional permissions and placing additional restrictions on your usage of this software.
|
|
# Please refer to the pretix LICENSE file to obtain the full terms applicable to this work. If you did not receive
|
|
# this file, see <https://pretix.eu/about/en/license>.
|
|
#
|
|
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
|
|
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
|
|
# details.
|
|
#
|
|
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
|
# <https://www.gnu.org/licenses/>.
|
|
#
|
|
|
|
# This file is based on an earlier version of pretix which was released under the Apache License 2.0. The full text of
|
|
# the Apache License 2.0 can be obtained at <http://www.apache.org/licenses/LICENSE-2.0>.
|
|
#
|
|
# This file may have since been changed and any changes are released under the terms of AGPLv3 as described above. A
|
|
# full history of changes and contributors is available at <https://github.com/pretix/pretix>.
|
|
#
|
|
# This file contains Apache-licensed contributions copyrighted by: Jason Estibeiro, Lukas Bockstaller, Maico Timmerman
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software distributed under the Apache License 2.0 is
|
|
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
# License for the specific language governing permissions and limitations under the License.
|
|
|
|
import time
|
|
from datetime import datetime, timedelta
|
|
|
|
import pytest
|
|
from django.conf import settings
|
|
from django.contrib.auth.tokens import (
|
|
PasswordResetTokenGenerator, default_token_generator,
|
|
)
|
|
from django.core import mail as djmail
|
|
from django.test import RequestFactory, TestCase, override_settings
|
|
from django.utils.crypto import get_random_string
|
|
from django.utils.timezone import now
|
|
from django_otp.oath import TOTP
|
|
from django_otp.plugins.otp_static.models import StaticDevice
|
|
from django_otp.plugins.otp_totp.models import TOTPDevice
|
|
from webauthn.authentication.verify_authentication_response import (
|
|
VerifiedAuthentication,
|
|
)
|
|
|
|
from pretix.base.models import Organizer, Team, U2FDevice, User
|
|
from pretix.control.views.auth import process_login
|
|
from pretix.helpers import security
|
|
|
|
|
|
class LoginFormTest(TestCase):
|
|
|
|
def setUp(self):
|
|
self.user = User.objects.create_user('dummy@dummy.dummy', 'dummy')
|
|
|
|
def test_wrong_credentials(self):
|
|
response = self.client.post('/control/login', {
|
|
'email': 'dummy@dummy.dummy',
|
|
'password': 'foo',
|
|
})
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
def test_correct_credentials(self):
|
|
response = self.client.post('/control/login', {
|
|
'email': 'dummy@dummy.dummy',
|
|
'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
|
|
self.user.save()
|
|
|
|
response = self.client.post('/control/login', {
|
|
'email': 'dummy@dummy.dummy',
|
|
'password': 'dummy',
|
|
})
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
def test_redirect(self):
|
|
response = self.client.post('/control/login?next=/control/events/', {
|
|
'email': 'dummy@dummy.dummy',
|
|
'password': 'dummy',
|
|
})
|
|
self.assertEqual(response.status_code, 302)
|
|
self.assertIn('/control/events/', response['Location'])
|
|
|
|
def test_redirect_to_2fa(self):
|
|
self.user.require_2fa = True
|
|
self.user.save()
|
|
response = self.client.post('/control/login?next=/control/events/', {
|
|
'email': 'dummy@dummy.dummy',
|
|
'password': 'dummy',
|
|
})
|
|
self.assertEqual(response.status_code, 302)
|
|
self.assertIn('/control/login/2fa?next=/control/events/', response['Location'])
|
|
assert self.client.session['pretix_auth_2fa_user'] == self.user.pk
|
|
assert 'pretix_auth_2fa_time' in self.client.session
|
|
|
|
def test_logged_in(self):
|
|
response = self.client.post('/control/login?next=/control/events/', {
|
|
'email': 'dummy@dummy.dummy',
|
|
'password': 'dummy',
|
|
})
|
|
self.assertEqual(response.status_code, 302)
|
|
self.assertIn('/control/events/', response['Location'])
|
|
|
|
response = self.client.get('/control/login')
|
|
self.assertEqual(response.status_code, 302)
|
|
|
|
response = self.client.get('/control/login?next=/control/events/')
|
|
self.assertEqual(response.status_code, 302)
|
|
self.assertIn('/control/events/', response['Location'])
|
|
|
|
response = self.client.get('/control/login?next=//evilsite.com')
|
|
self.assertEqual(response.status_code, 302)
|
|
self.assertIn('/control/', response['Location'])
|
|
|
|
def test_logout(self):
|
|
response = self.client.post('/control/login', {
|
|
'email': 'dummy@dummy.dummy',
|
|
'password': 'dummy',
|
|
})
|
|
self.assertEqual(response.status_code, 302)
|
|
|
|
response = self.client.get('/control/logout')
|
|
self.assertEqual(response.status_code, 302)
|
|
|
|
response = self.client.get('/control/login')
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
def test_wrong_backend(self):
|
|
self.user = User.objects.create_user('hallo@example.com', 'dummy', auth_backend='test_request')
|
|
response = self.client.post('/control/login', {
|
|
'email': 'hallo@example.com',
|
|
'password': 'dummy',
|
|
})
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
def test_backends_shown(self):
|
|
response = self.client.get('/control/login')
|
|
self.assertEqual(response.status_code, 200)
|
|
assert b'Form' in response.content
|
|
assert b'pretix.eu User' in response.content
|
|
assert b'Request' not in response.content
|
|
|
|
def test_form_backend(self):
|
|
response = self.client.get('/control/login?backend=test_form')
|
|
self.assertEqual(response.status_code, 200)
|
|
assert b'name="username"' in response.content
|
|
|
|
response = self.client.post('/control/login?backend=test_form', {
|
|
'username': 'dummy',
|
|
'password': 'dummy',
|
|
})
|
|
self.assertEqual(response.status_code, 200)
|
|
assert b'alert-danger' in response.content
|
|
|
|
response = self.client.post('/control/login?backend=test_form', {
|
|
'username': 'foo',
|
|
'password': 'bar',
|
|
})
|
|
self.assertEqual(response.status_code, 302)
|
|
response = self.client.get('/control/')
|
|
assert b'foo' in response.content
|
|
|
|
def test_request_backend(self):
|
|
response = self.client.get('/control/login?backend=test_request')
|
|
self.assertEqual(response.status_code, 200)
|
|
assert b'name="email"' in response.content
|
|
|
|
response = self.client.get('/control/login', HTTP_X_LOGIN_EMAIL='hallo@example.org')
|
|
self.assertEqual(response.status_code, 302)
|
|
response = self.client.get('/control/')
|
|
assert b'hallo@example.org' in response.content
|
|
|
|
def test_custom_get_next_url(self):
|
|
response = self.client.get('/control/login?state=/control/events/', HTTP_X_LOGIN_EMAIL='hallo@example.org')
|
|
self.assertEqual(response.status_code, 302)
|
|
self.assertIn('/control/events/', response['Location'])
|
|
|
|
@override_settings(HAS_GEOIP=True)
|
|
def test_login_notice(self):
|
|
class FakeGeoIp:
|
|
def country(self, ip):
|
|
if ip == '1.2.3.4':
|
|
return {'country_code': 'DE'}
|
|
return {'country_code': 'US'}
|
|
|
|
security._geoip = FakeGeoIp()
|
|
self.client.defaults['REMOTE_ADDR'] = '1.2.3.4'
|
|
|
|
djmail.outbox = []
|
|
|
|
# No notice sent on first login
|
|
response = self.client.post('/control/login?next=/control/events/', {
|
|
'email': 'dummy@dummy.dummy',
|
|
'password': 'dummy',
|
|
}, HTTP_USER_AGENT='Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.36 (KHTML, like Gecko) '
|
|
'Chrome/41.0.2272.104 Safari/537.36')
|
|
self.assertEqual(response.status_code, 302)
|
|
|
|
assert len(djmail.outbox) == 0
|
|
|
|
response = self.client.get('/control/logout')
|
|
self.assertEqual(response.status_code, 302)
|
|
|
|
# No notice sent on subsequent login with same user agent
|
|
response = self.client.post('/control/login?next=/control/events/', {
|
|
'email': 'dummy@dummy.dummy',
|
|
'password': 'dummy',
|
|
}, HTTP_USER_AGENT='Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.36 (KHTML, like Gecko) '
|
|
'Chrome/41.0.2272.104 Safari/537.36')
|
|
self.assertEqual(response.status_code, 302)
|
|
|
|
assert len(djmail.outbox) == 0
|
|
|
|
response = self.client.get('/control/logout')
|
|
self.assertEqual(response.status_code, 302)
|
|
|
|
# Notice sent on subsequent login with other user agent
|
|
response = self.client.post('/control/login?next=/control/events/', {
|
|
'email': 'dummy@dummy.dummy',
|
|
'password': 'dummy',
|
|
}, HTTP_USER_AGENT='Mozilla/5.0 (X11; Linux x86_64; rv:124.0) Gecko/20100101 Firefox/124.0')
|
|
self.assertEqual(response.status_code, 302)
|
|
|
|
assert len(djmail.outbox) == 1
|
|
|
|
response = self.client.get('/control/logout')
|
|
self.assertEqual(response.status_code, 302)
|
|
|
|
# Notice sent on subsequent login with other country
|
|
self.client.defaults['REMOTE_ADDR'] = '4.3.2.1'
|
|
response = self.client.post('/control/login?next=/control/events/', {
|
|
'email': 'dummy@dummy.dummy',
|
|
'password': 'dummy',
|
|
}, HTTP_USER_AGENT='Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_4) AppleWebKit/537.36 (KHTML, like Gecko) '
|
|
'Chrome/41.0.2272.104 Safari/537.36')
|
|
self.assertEqual(response.status_code, 302)
|
|
|
|
assert len(djmail.outbox) == 2
|
|
|
|
|
|
class RegistrationFormTest(TestCase):
|
|
|
|
@override_settings(PRETIX_REGISTRATION=True)
|
|
def test_different_passwords(self):
|
|
response = self.client.post('/control/register', {
|
|
'email': 'dummy@dummy.dummy',
|
|
'password': 'foo',
|
|
'password_repeat': 'foobar'
|
|
})
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
@override_settings(PRETIX_REGISTRATION=True)
|
|
def test_user_attribute_similarity_passwords(self):
|
|
response = self.client.post('/control/register', {
|
|
'email': 'dummy@dummy.dummy',
|
|
'password': 'dummydummy',
|
|
'password_repeat': 'dummydummy'
|
|
})
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
@override_settings(PRETIX_REGISTRATION=True)
|
|
def test_short_passwords(self):
|
|
response = self.client.post('/control/register', {
|
|
'email': 'dummy@dummy.dummy',
|
|
'password': 'foobar',
|
|
'password_repeat': 'foobar'
|
|
})
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
@override_settings(PRETIX_REGISTRATION=True)
|
|
def test_common_passwords(self):
|
|
response = self.client.post('/control/register', {
|
|
'email': 'dummy@dummy.dummy',
|
|
'password': 'password',
|
|
'password_repeat': 'password'
|
|
})
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
response = self.client.post('/control/register', {
|
|
'email': 'dummy@dummy.dummy',
|
|
'password': 'football',
|
|
'password_repeat': 'football'
|
|
})
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
response = self.client.post('/control/register', {
|
|
'email': 'dummy@dummy.dummy',
|
|
'password': 'jennifer',
|
|
'password_repeat': 'jennifer'
|
|
})
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
@override_settings(PRETIX_REGISTRATION=True)
|
|
def test_numeric_passwords(self):
|
|
response = self.client.post('/control/register', {
|
|
'email': 'dummy@dummy.dummy',
|
|
'password': '12345678',
|
|
'password_repeat': '12345678'
|
|
})
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
response = self.client.post('/control/register', {
|
|
'email': 'dummy@dummy.dummy',
|
|
'password': '23423523452345235',
|
|
'password_repeat': '23423523452345235'
|
|
})
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
@override_settings(PRETIX_REGISTRATION=True)
|
|
def test_empty_passwords(self):
|
|
response = self.client.post('/control/register', {
|
|
'email': 'dummy@dummy.dummy',
|
|
'password': '',
|
|
'password_repeat': ''
|
|
})
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
response = self.client.post('/control/register', {
|
|
'email': 'dummy@dummy.dummy',
|
|
'password': 'f00barbarbar',
|
|
'password_repeat': ''
|
|
})
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
@override_settings(PRETIX_REGISTRATION=True)
|
|
def test_email_duplicate(self):
|
|
self.user = User.objects.create_user('dummy@dummy.dummy', 'dummy')
|
|
response = self.client.post('/control/register', {
|
|
'email': 'dummy@dummy.dummy',
|
|
'password': 'f00barbarbar',
|
|
'password_repeat': 'f00barbarbar'
|
|
})
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
@override_settings(PRETIX_REGISTRATION=True)
|
|
def test_success(self):
|
|
response = self.client.post('/control/register', {
|
|
'email': 'dummy@dummy.dummy',
|
|
'password': 'f00barbarbar',
|
|
'password_repeat': 'f00barbarbar'
|
|
})
|
|
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']
|
|
|
|
@override_settings(PRETIX_REGISTRATION=False)
|
|
def test_disabled(self):
|
|
response = self.client.post('/control/register', {
|
|
'email': 'dummy@dummy.dummy',
|
|
'password': 'f00barbarbar',
|
|
'password_repeat': 'f00barbarbar'
|
|
})
|
|
self.assertEqual(response.status_code, 403)
|
|
|
|
@override_settings(PRETIX_AUTH_BACKENDS=['tests.testdummy.auth.TestFormAuthBackend'])
|
|
def test_no_native_auth(self):
|
|
response = self.client.post('/control/register', {
|
|
'email': 'dummy@dummy.dummy',
|
|
'password': 'f00barbarbar',
|
|
'password_repeat': 'f00barbarbar'
|
|
})
|
|
self.assertEqual(response.status_code, 403)
|
|
|
|
|
|
@pytest.fixture
|
|
def class_monkeypatch(request, monkeypatch):
|
|
request.cls.monkeypatch = monkeypatch
|
|
|
|
|
|
@pytest.mark.usefixtures("class_monkeypatch")
|
|
class Login2FAFormTest(TestCase):
|
|
|
|
def setUp(self):
|
|
self.user = User.objects.create_user('dummy@dummy.dummy', 'dummy', require_2fa=True)
|
|
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):
|
|
session = self.client.session
|
|
session['pretix_auth_2fa_user'] = self.user.pk + 12
|
|
session['pretix_auth_2fa_time'] = str(int(time.time()))
|
|
session.save()
|
|
response = self.client.get('/control/login/2fa')
|
|
self.assertEqual(response.status_code, 302)
|
|
self.assertIn('/control/login', response['Location'])
|
|
|
|
def test_expired_session(self):
|
|
session = self.client.session
|
|
session['pretix_auth_2fa_user'] = self.user.pk + 12
|
|
session['pretix_auth_2fa_time'] = str(int(time.time()) - 3600)
|
|
session.save()
|
|
response = self.client.get('/control/login/2fa')
|
|
self.assertEqual(response.status_code, 302)
|
|
self.assertIn('/control/login', response['Location'])
|
|
|
|
def test_totp_invalid(self):
|
|
response = self.client.get('/control/login/2fa')
|
|
assert 'token' in response.content.decode()
|
|
d = TOTPDevice.objects.create(user=self.user, name='test')
|
|
totp = TOTP(d.bin_key, d.step, d.t0, d.digits, d.drift)
|
|
totp.time = time.time()
|
|
response = self.client.post('/control/login/2fa', {
|
|
'token': str(totp.token() + 2)
|
|
})
|
|
self.assertEqual(response.status_code, 302)
|
|
self.assertIn('/control/login/2fa', response['Location'])
|
|
|
|
def test_totp_valid(self):
|
|
response = self.client.get('/control/login/2fa')
|
|
assert 'token' in response.content.decode()
|
|
d = TOTPDevice.objects.create(user=self.user, name='test')
|
|
totp = TOTP(d.bin_key, d.step, d.t0, d.digits, d.drift)
|
|
totp.time = time.time()
|
|
response = self.client.post('/control/login/2fa?next=/control/events/', {
|
|
'token': str(totp.token())
|
|
})
|
|
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):
|
|
raise Exception("Failed")
|
|
|
|
m = self.monkeypatch
|
|
m.setattr("webauthn.verify_authentication_response", fail)
|
|
U2FDevice.objects.create(
|
|
user=self.user, name='test',
|
|
json_data='{"appId": "https://local.pretix.eu", "keyHandle": '
|
|
'"j9Rkpon1J5U3eDQMM8YqAvwEapt-m87V8qdCaImiAqmvTJ'
|
|
'-sBvnACIKKM6J_RVXF4jPtY0LGyjbHi14sxsoC5g", "publ'
|
|
'icKey": "BP5KRLUGvcHbqkCc7eJNXZ9caVXLSk4wjsq'
|
|
'L-pLEQcNqVp2E4OeDUIxI0ZLOXry9JSrLn1aAGcGowXiIyB7ynj0"}')
|
|
|
|
response = self.client.get('/control/login/2fa')
|
|
assert 'token' in response.content.decode()
|
|
response = self.client.post('/control/login/2fa', {
|
|
'token': '{"response": "true"}'
|
|
})
|
|
self.assertEqual(response.status_code, 302)
|
|
self.assertIn('/control/login/2fa', response['Location'])
|
|
|
|
m.undo()
|
|
|
|
def test_u2f_valid(self):
|
|
m = self.monkeypatch
|
|
m.setattr("webauthn.verify_authentication_response",
|
|
lambda *args, **kwargs: VerifiedAuthentication(
|
|
b'', 1, 'single_device', True,
|
|
))
|
|
|
|
U2FDevice.objects.create(
|
|
user=self.user, name='test',
|
|
json_data='{"appId": "https://local.pretix.eu", "keyHandle": '
|
|
'"j9Rkpon1J5U3eDQMM8YqAvwEapt-m87V8qdCaImiAqmvTJ'
|
|
'-sBvnACIKKM6J_RVXF4jPtY0LGyjbHi14sxsoC5g", "publ'
|
|
'icKey": "BP5KRLUGvcHbqkCc7eJNXZ9caVXLSk4wjsq'
|
|
'L-pLEQcNqVp2E4OeDUIxI0ZLOXry9JSrLn1aAGcGowXiIyB7ynj0"}')
|
|
|
|
response = self.client.get('/control/login/2fa')
|
|
assert 'token' in response.content.decode()
|
|
response = self.client.post('/control/login/2fa', {
|
|
'token': '{"response": "true"}'
|
|
})
|
|
self.assertEqual(response.status_code, 302)
|
|
self.assertIn('/control/', response['Location'])
|
|
|
|
m.undo()
|
|
|
|
def test_recovery_code_valid(self):
|
|
djmail.outbox = []
|
|
d, __ = StaticDevice.objects.get_or_create(user=self.user, name='emergency')
|
|
token = d.token_set.create(token=get_random_string(length=12, allowed_chars='1234567890'))
|
|
|
|
response = self.client.get('/control/login/2fa')
|
|
assert 'token' in response.content.decode()
|
|
response = self.client.post('/control/login/2fa', {
|
|
'token': token.token,
|
|
})
|
|
self.assertEqual(response.status_code, 302)
|
|
self.assertIn('/control/', response['Location'])
|
|
assert "recovery code" in djmail.outbox[0].body
|
|
|
|
|
|
class FakeRedis(object):
|
|
def get_redis_connection(self, connection_string):
|
|
return self
|
|
|
|
def __init__(self):
|
|
self.storage = {}
|
|
|
|
def pipeline(self):
|
|
return self
|
|
|
|
def hincrbyfloat(self, rkey, key, amount):
|
|
return self
|
|
|
|
def commit(self):
|
|
return self
|
|
|
|
def exists(self, rkey):
|
|
return rkey in self.storage
|
|
|
|
def setex(self, rkey, value, expiration):
|
|
self.storage[rkey] = value
|
|
|
|
def execute(self):
|
|
pass
|
|
|
|
|
|
@pytest.mark.usefixtures("class_monkeypatch")
|
|
class PasswordRecoveryFormTest(TestCase):
|
|
def setUp(self):
|
|
super().setUp()
|
|
self.user = User.objects.create_user('demo@demo.dummy', 'demo')
|
|
|
|
def test_unknown(self):
|
|
djmail.outbox = []
|
|
|
|
response = self.client.post('/control/forgot', {
|
|
'email': 'dummy@dummy.dummy',
|
|
})
|
|
self.assertEqual(response.status_code, 302)
|
|
assert len(djmail.outbox) == 0
|
|
|
|
def test_email_sent(self):
|
|
djmail.outbox = []
|
|
|
|
response = self.client.post('/control/forgot', {
|
|
'email': 'demo@demo.dummy',
|
|
})
|
|
self.assertEqual(response.status_code, 302)
|
|
|
|
assert len(djmail.outbox) == 1
|
|
assert djmail.outbox[0].to == [self.user.email]
|
|
assert "recover?id=%d&token=" % self.user.id in djmail.outbox[0].body
|
|
assert self.user.all_logentries[0].action_type == 'pretix.control.auth.user.forgot_password.mail_sent'
|
|
|
|
@override_settings(HAS_REDIS=True)
|
|
def test_email_reset_twice_redis(self):
|
|
fake_redis = FakeRedis()
|
|
m = self.monkeypatch
|
|
m.setattr('django_redis.get_redis_connection', fake_redis.get_redis_connection, raising=False)
|
|
m.setattr('pretix.base.metrics.redis', fake_redis, raising=False)
|
|
|
|
djmail.outbox = []
|
|
|
|
response = self.client.post('/control/forgot', {
|
|
'email': 'demo@demo.dummy',
|
|
})
|
|
self.assertEqual(response.status_code, 302)
|
|
|
|
assert len(djmail.outbox) == 1
|
|
assert djmail.outbox[0].to == [self.user.email]
|
|
assert "recover?id=%d&token=" % self.user.id in djmail.outbox[0].body
|
|
assert self.user.all_logentries[0].action_type == 'pretix.control.auth.user.forgot_password.mail_sent'
|
|
|
|
response = self.client.post('/control/forgot', {
|
|
'email': 'demo@demo.dummy',
|
|
})
|
|
self.assertEqual(response.status_code, 302)
|
|
|
|
assert len(djmail.outbox) == 1
|
|
assert self.user.all_logentries[0].action_type == 'pretix.control.auth.user.forgot_password.denied.repeated'
|
|
|
|
def test_recovery_unknown_user(self):
|
|
response = self.client.get('/control/forgot/recover?id=0&token=foo')
|
|
self.assertEqual(response.status_code, 302)
|
|
response = self.client.post(
|
|
'/control/forgot/recover?id=0&token=foo',
|
|
{
|
|
'password': 'foobar',
|
|
'password_repeat': 'foobar'
|
|
}
|
|
)
|
|
self.assertEqual(response.status_code, 302)
|
|
self.user = User.objects.get(id=self.user.id)
|
|
self.assertTrue(self.user.check_password('demo'))
|
|
|
|
def test_recovery_invalid_token(self):
|
|
response = self.client.get('/control/forgot/recover?id=%d&token=foo' % self.user.id)
|
|
self.assertEqual(response.status_code, 302)
|
|
response = self.client.post(
|
|
'/control/forgot/recover?id=%d&token=foo' % self.user.id,
|
|
{
|
|
'password': 'f00barbarbar',
|
|
'password_repeat': 'f00barbarbar'
|
|
}
|
|
)
|
|
self.assertEqual(response.status_code, 302)
|
|
self.user = User.objects.get(id=self.user.id)
|
|
self.assertTrue(self.user.check_password('demo'))
|
|
|
|
def test_recovery_expired_token(self):
|
|
class Mocked(PasswordResetTokenGenerator):
|
|
def _now(self):
|
|
return datetime.now() - timedelta(seconds=settings.PASSWORD_RESET_TIMEOUT + 3600)
|
|
|
|
generator = Mocked()
|
|
token = generator.make_token(self.user)
|
|
response = self.client.get(
|
|
'/control/forgot/recover?id=%d&token=%s' % (self.user.id, token)
|
|
)
|
|
self.assertEqual(response.status_code, 302)
|
|
response = self.client.post(
|
|
'/control/forgot/recover?id=%d&token=%s' % (self.user.id, token),
|
|
{
|
|
'password': 'f00barbarbar',
|
|
'password_repeat': 'f00barbarbar'
|
|
}
|
|
)
|
|
self.assertEqual(response.status_code, 302)
|
|
self.user = User.objects.get(id=self.user.id)
|
|
self.assertTrue(self.user.check_password('demo'))
|
|
|
|
def test_recovery_valid_token_success(self):
|
|
token = default_token_generator.make_token(self.user)
|
|
response = self.client.get('/control/forgot/recover?id=%d&token=%s' % (self.user.id, token))
|
|
self.assertEqual(response.status_code, 200)
|
|
response = self.client.post(
|
|
'/control/forgot/recover?id=%d&token=%s' % (self.user.id, token),
|
|
{
|
|
'password': 'f00barbarbar',
|
|
'password_repeat': 'f00barbarbar'
|
|
}
|
|
)
|
|
self.assertEqual(response.status_code, 302)
|
|
self.user = User.objects.get(id=self.user.id)
|
|
self.assertTrue(self.user.check_password('f00barbarbar'))
|
|
|
|
def test_recovery_valid_token_empty_passwords(self):
|
|
token = default_token_generator.make_token(self.user)
|
|
response = self.client.get('/control/forgot/recover?id=%d&token=%s' % (self.user.id, token))
|
|
self.assertEqual(response.status_code, 200)
|
|
response = self.client.post(
|
|
'/control/forgot/recover?id=%d&token=%s' % (self.user.id, token),
|
|
{
|
|
'password': 'f00barbarbar',
|
|
'password_repeat': ''
|
|
}
|
|
)
|
|
self.assertEqual(response.status_code, 200)
|
|
self.user = User.objects.get(id=self.user.id)
|
|
self.assertTrue(self.user.check_password('demo'))
|
|
|
|
token = default_token_generator.make_token(self.user)
|
|
response = self.client.get('/control/forgot/recover?id=%d&token=%s' % (self.user.id, token))
|
|
self.assertEqual(response.status_code, 200)
|
|
response = self.client.post(
|
|
'/control/forgot/recover?id=%d&token=%s' % (self.user.id, token),
|
|
{
|
|
'password': '',
|
|
'password_repeat': 'f00barbarbar'
|
|
}
|
|
)
|
|
self.assertEqual(response.status_code, 200)
|
|
self.user = User.objects.get(id=self.user.id)
|
|
self.assertTrue(self.user.check_password('demo'))
|
|
|
|
def test_recovery_valid_token_different_passwords(self):
|
|
token = default_token_generator.make_token(self.user)
|
|
response = self.client.get('/control/forgot/recover?id=%d&token=%s' % (self.user.id, token))
|
|
self.assertEqual(response.status_code, 200)
|
|
response = self.client.post(
|
|
'/control/forgot/recover?id=%d&token=%s' % (self.user.id, token),
|
|
{
|
|
'password': 'foo',
|
|
'password_repeat': 'foobar'
|
|
}
|
|
)
|
|
self.assertEqual(response.status_code, 200)
|
|
self.user = User.objects.get(id=self.user.id)
|
|
self.assertTrue(self.user.check_password('demo'))
|
|
|
|
def test_recovery_valid_token_user_attribute_similarity_passwords(self):
|
|
token = default_token_generator.make_token(self.user)
|
|
response = self.client.get('/control/forgot/recover?id=%d&token=%s' % (self.user.id, token))
|
|
self.assertEqual(response.status_code, 200)
|
|
response = self.client.post(
|
|
'/control/forgot/recover?id=%d&token=%s' % (self.user.id, token),
|
|
{
|
|
'password': 'dummydemo',
|
|
'password_repeat': 'dummydemo'
|
|
}
|
|
)
|
|
self.assertEqual(response.status_code, 200)
|
|
self.user = User.objects.get(id=self.user.id)
|
|
self.assertTrue(self.user.check_password('demo'))
|
|
|
|
def test_recovery_valid_token_password_reuse(self):
|
|
self.user.set_password("GsvdU4gGZDb4J9WgIhLNcZT9PO7CZ3")
|
|
self.user.save()
|
|
self.user.set_password("hLPqPpuZIjouGBk9xTLu1aXYqjpRYS")
|
|
self.user.save()
|
|
self.user.set_password("Jn2nQSa25ZJAc5GUI1HblrneWCXotD")
|
|
self.user.save()
|
|
self.user.set_password("cboaBj3yIfgnQeKClDgvKNvWC69cV1")
|
|
self.user.save()
|
|
self.user.set_password("Kkj8f3kGXbXmbgcwHBgf3WKmzkUOhM")
|
|
self.user.save()
|
|
|
|
assert self.user.historic_passwords.count() == 4
|
|
|
|
token = default_token_generator.make_token(self.user)
|
|
response = self.client.get('/control/forgot/recover?id=%d&token=%s' % (self.user.id, token))
|
|
self.assertEqual(response.status_code, 200)
|
|
response = self.client.post(
|
|
'/control/forgot/recover?id=%d&token=%s' % (self.user.id, token),
|
|
{
|
|
'password': 'cboaBj3yIfgnQeKClDgvKNvWC69cV1',
|
|
'password_repeat': 'cboaBj3yIfgnQeKClDgvKNvWC69cV1'
|
|
}
|
|
)
|
|
self.assertEqual(response.status_code, 200)
|
|
self.user = User.objects.get(id=self.user.id)
|
|
self.assertTrue(self.user.check_password('Kkj8f3kGXbXmbgcwHBgf3WKmzkUOhM'))
|
|
|
|
token = default_token_generator.make_token(self.user)
|
|
response = self.client.get('/control/forgot/recover?id=%d&token=%s' % (self.user.id, token))
|
|
self.assertEqual(response.status_code, 200)
|
|
response = self.client.post(
|
|
'/control/forgot/recover?id=%d&token=%s' % (self.user.id, token),
|
|
{
|
|
'password': 'GsvdU4gGZDb4J9WgIhLNcZT9PO7CZ3',
|
|
'password_repeat': 'GsvdU4gGZDb4J9WgIhLNcZT9PO7CZ3'
|
|
}
|
|
)
|
|
self.assertEqual(response.status_code, 302)
|
|
self.user = User.objects.get(id=self.user.id)
|
|
self.assertTrue(self.user.check_password('GsvdU4gGZDb4J9WgIhLNcZT9PO7CZ3'))
|
|
|
|
def test_recovery_valid_token_short_passwords(self):
|
|
token = default_token_generator.make_token(self.user)
|
|
response = self.client.get('/control/forgot/recover?id=%d&token=%s' % (self.user.id, token))
|
|
self.assertEqual(response.status_code, 200)
|
|
response = self.client.post(
|
|
'/control/forgot/recover?id=%d&token=%s' % (self.user.id, token),
|
|
{
|
|
'password': 'foobarfooba',
|
|
'password_repeat': 'foobarfooba'
|
|
}
|
|
)
|
|
self.assertEqual(response.status_code, 200)
|
|
self.user = User.objects.get(id=self.user.id)
|
|
self.assertTrue(self.user.check_password('demo'))
|
|
|
|
def test_recovery_valid_token_common_passwords(self):
|
|
token = default_token_generator.make_token(self.user)
|
|
response = self.client.get('/control/forgot/recover?id=%d&token=%s' % (self.user.id, token))
|
|
self.assertEqual(response.status_code, 200)
|
|
response = self.client.post(
|
|
'/control/forgot/recover?id=%d&token=%s' % (self.user.id, token),
|
|
{
|
|
'password': 'football',
|
|
'password_repeat': 'football'
|
|
}
|
|
)
|
|
self.assertEqual(response.status_code, 200)
|
|
self.user = User.objects.get(id=self.user.id)
|
|
self.assertTrue(self.user.check_password('demo'))
|
|
|
|
def test_recovery_valid_token_numeric_passwords(self):
|
|
token = default_token_generator.make_token(self.user)
|
|
response = self.client.get('/control/forgot/recover?id=%d&token=%s' % (self.user.id, token))
|
|
self.assertEqual(response.status_code, 200)
|
|
response = self.client.post(
|
|
'/control/forgot/recover?id=%d&token=%s' % (self.user.id, token),
|
|
{
|
|
'password': '12345678',
|
|
'password_repeat': '12345678'
|
|
}
|
|
)
|
|
self.assertEqual(response.status_code, 200)
|
|
self.user = User.objects.get(id=self.user.id)
|
|
self.assertTrue(self.user.check_password('demo'))
|
|
|
|
@override_settings(PRETIX_PASSWORD_RESET=False)
|
|
def test_disabled(self):
|
|
response = self.client.post('/control/forgot', {
|
|
'email': 'dummy@dummy.dummy',
|
|
})
|
|
self.assertEqual(response.status_code, 403)
|
|
|
|
@override_settings(PRETIX_AUTH_BACKENDS=['tests.testdummy.auth.TestFormAuthBackend'])
|
|
def test_no_native_auth(self):
|
|
response = self.client.post('/control/forgot', {
|
|
'email': 'dummy@dummy.dummy',
|
|
})
|
|
self.assertEqual(response.status_code, 403)
|
|
|
|
|
|
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_log_out_after_relative_timeout_really_enforced(self):
|
|
# Regression test added after a security problem in 1.9.1
|
|
# The problem was that, once the relative timeout happened, the user was redirected
|
|
# to /control/reauth/, but loading /control/reauth/ was already considered to be
|
|
# "session activity". Therefore, after loding /control/reauth/, the session was no longer
|
|
# in the timeout state and the user was able to access pages again without re-entering the
|
|
# password.
|
|
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)
|
|
self.assertRedirects(response, '/control/reauth/?next=/control/')
|
|
self.client.get('/control/reauth/?next=/control/')
|
|
response = self.client.get('/control/')
|
|
self.assertEqual(response.status_code, 302)
|
|
|
|
def test_plugin_auth_updates_auth_last_used(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()
|
|
|
|
request = RequestFactory().get("/")
|
|
request.session = self.client.session
|
|
process_login(request, self.user, keep_logged_in=True)
|
|
|
|
assert request.session['pretix_auth_last_used'] >= int(time.time()) - 60
|
|
|
|
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
|
|
|
|
def test_pinned_user_agent(self):
|
|
self.client.defaults['HTTP_USER_AGENT'] = 'Mozilla/5.0 (X11; Linux x86_64) ' \
|
|
'AppleWebKit/537.36 (KHTML, like Gecko) ' \
|
|
'Chrome/64.0.3282.140 Safari/537.36'
|
|
response = self.client.get('/control/')
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
self.client.defaults['HTTP_USER_AGENT'] = 'Mozilla/5.0 (X11; Linux x86_64) Something else'
|
|
response = self.client.get('/control/')
|
|
self.assertEqual(response.status_code, 302)
|
|
|
|
@override_settings(HAS_GEOIP=True)
|
|
def test_pinned_country(self):
|
|
class FakeGeoIp:
|
|
def country(self, ip):
|
|
if ip == '1.2.3.4':
|
|
return {'country_code': 'DE'}
|
|
return {'country_code': 'US'}
|
|
|
|
security._geoip = FakeGeoIp()
|
|
self.client.defaults['REMOTE_ADDR'] = '1.2.3.4'
|
|
response = self.client.get('/control/')
|
|
self.assertEqual(response.status_code, 200)
|
|
|
|
self.client.defaults['REMOTE_ADDR'] = '4.3.2.1'
|
|
response = self.client.get('/control/')
|
|
self.assertEqual(response.status_code, 302)
|
|
|
|
security._geoip = None
|
|
|
|
|
|
@pytest.fixture
|
|
def user():
|
|
user = User.objects.create_user('dummy@dummy.dummy', 'dummy')
|
|
return user
|
|
|
|
|
|
@pytest.mark.django_db
|
|
def test_impersonate(user, client):
|
|
client.login(email='dummy@dummy.dummy', password='dummy')
|
|
user.is_staff = True
|
|
user.save()
|
|
ss = user.staffsession_set.create(date_start=now(), session_key=client.session.session_key)
|
|
t1 = int(time.time()) - 5
|
|
session = client.session
|
|
session['pretix_auth_long_session'] = False
|
|
session['pretix_auth_login_time'] = t1
|
|
session['pretix_auth_last_used'] = t1
|
|
session.save()
|
|
user2 = User.objects.create_user('dummy2@dummy.dummy', 'dummy')
|
|
response = client.post('/control/users/{user}/impersonate'.format(user=user2.pk), follow=True)
|
|
assert b'dummy2@' in response.content
|
|
response = client.get('/control/global/settings/')
|
|
assert response.status_code == 403
|
|
response = client.get('/control/')
|
|
response = client.post('/control/users/impersonate/stop/', follow=True)
|
|
assert b'dummy@' in response.content
|
|
assert b'dummy2@' not in response.content
|
|
response = client.get('/control/global/settings/')
|
|
assert response.status_code == 200 # staff session is preserved
|
|
assert ss.logs.filter(url='/control/', impersonating=user2).exists()
|
|
|
|
|
|
@pytest.mark.django_db
|
|
def test_impersonate_require_recent_auth(user, client):
|
|
client.login(email='dummy@dummy.dummy', password='dummy')
|
|
user.is_staff = True
|
|
user.save()
|
|
user.staffsession_set.create(date_start=now(), session_key=client.session.session_key)
|
|
t1 = int(time.time()) - 5 * 3600
|
|
session = client.session
|
|
session['pretix_auth_long_session'] = False
|
|
session['pretix_auth_login_time'] = t1
|
|
session['pretix_auth_last_used'] = t1
|
|
session.save()
|
|
user2 = User.objects.create_user('dummy2@dummy.dummy', 'dummy')
|
|
response = client.post('/control/users/{user}/impersonate'.format(user=user2.pk), follow=True)
|
|
assert b'dummy2@' not in response.content
|
|
|
|
|
|
@pytest.mark.django_db
|
|
def test_staff_session(user, client):
|
|
client.login(email='dummy@dummy.dummy', password='dummy')
|
|
user.is_staff = True
|
|
user.save()
|
|
t1 = int(time.time()) - 5
|
|
session = client.session
|
|
session['pretix_auth_long_session'] = False
|
|
session['pretix_auth_login_time'] = t1
|
|
session['pretix_auth_last_used'] = t1
|
|
session.save()
|
|
response = client.get('/control/global/settings/')
|
|
assert response.status_code == 302
|
|
response = client.post('/control/sudo/')
|
|
assert response['Location'] == '/control/'
|
|
response = client.get('/control/global/settings/')
|
|
assert response.status_code == 200
|
|
response = client.get('/control/sudo/stop/', follow=True)
|
|
assert response.status_code == 200
|
|
response = client.get('/control/global/settings/')
|
|
assert response.status_code == 302
|
|
assert user.staffsession_set.last().logs.filter(url='/control/global/settings/').exists()
|
|
|
|
|
|
@pytest.mark.django_db
|
|
def test_staff_session_require_recent_auth(user, client):
|
|
client.login(email='dummy@dummy.dummy', password='dummy')
|
|
user.is_staff = True
|
|
user.save()
|
|
t1 = int(time.time()) - 5 * 3600
|
|
session = client.session
|
|
session['pretix_auth_long_session'] = False
|
|
session['pretix_auth_login_time'] = t1
|
|
session['pretix_auth_last_used'] = t1
|
|
session.save()
|
|
response = client.post('/control/sudo/')
|
|
assert response['Location'].startswith('/control/reauth/')
|
|
|
|
|
|
@pytest.mark.django_db
|
|
def test_staff_session_require_staff(user, client):
|
|
user.is_staff = False
|
|
user.save()
|
|
client.login(email='dummy@dummy.dummy', password='dummy')
|
|
t1 = int(time.time()) - 5
|
|
session = client.session
|
|
session['pretix_auth_long_session'] = False
|
|
session['pretix_auth_login_time'] = t1
|
|
session['pretix_auth_last_used'] = t1
|
|
session.save()
|
|
response = client.post('/control/sudo/')
|
|
assert response.status_code == 403
|
|
|
|
|
|
class Obligatory2FATest(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')
|
|
|
|
@override_settings(PRETIX_OBLIGATORY_2FA=True)
|
|
def test_enabled_2fa_not_setup(self):
|
|
response = self.client.get('/control/events/')
|
|
assert response.status_code == 302
|
|
assert response.url == '/control/settings/2fa/'
|
|
|
|
@override_settings(PRETIX_OBLIGATORY_2FA=True)
|
|
def test_enabled_2fa_setup_not_enabled(self):
|
|
U2FDevice.objects.create(user=self.user, name='test', json_data="{}", confirmed=True)
|
|
self.user.require_2fa = False
|
|
self.user.save()
|
|
|
|
response = self.client.get('/control/events/')
|
|
assert response.status_code == 302
|
|
assert response.url == '/control/settings/2fa/'
|
|
|
|
@override_settings(PRETIX_OBLIGATORY_2FA=True)
|
|
def test_enabled_2fa_setup_enabled(self):
|
|
U2FDevice.objects.create(user=self.user, name='test', json_data="{}", confirmed=True)
|
|
self.user.require_2fa = True
|
|
self.user.save()
|
|
|
|
response = self.client.get('/control/events/')
|
|
assert response.status_code == 200
|
|
|
|
@override_settings(PRETIX_OBLIGATORY_2FA="staff")
|
|
def test_staff_only(self):
|
|
self.user.require_2fa = False
|
|
self.user.save()
|
|
response = self.client.get('/control/events/')
|
|
assert response.status_code == 200
|
|
|
|
self.user.is_staff = True
|
|
self.user.save()
|
|
|
|
response = self.client.get('/control/events/')
|
|
assert response.status_code == 302
|
|
assert response.url == '/control/settings/2fa/'
|
|
|
|
@override_settings(PRETIX_OBLIGATORY_2FA=False)
|
|
def test_by_team(self):
|
|
session = self.client.session
|
|
session['pretix_auth_long_session'] = True
|
|
session['pretix_auth_login_time'] = int(time.time())
|
|
session['pretix_auth_last_used'] = int(time.time())
|
|
session.save()
|
|
|
|
organizer = Organizer.objects.create(name='Dummy', slug='dummy')
|
|
team = Team.objects.create(organizer=organizer, can_change_teams=True, name='Admin team')
|
|
team.members.add(self.user)
|
|
self.user.require_2fa = False
|
|
self.user.save()
|
|
response = self.client.get('/control/events/')
|
|
assert response.status_code == 200
|
|
|
|
team.require_2fa = True
|
|
team.save()
|
|
|
|
response = self.client.get('/control/events/')
|
|
assert response.status_code == 302
|
|
assert response.url == '/control/settings/2fa/'
|
|
|
|
response = self.client.post('/control/settings/2fa/leaveteams')
|
|
assert response.status_code == 302
|
|
assert team.members.count() == 0
|
|
|
|
response = self.client.get('/control/events/')
|
|
assert response.status_code == 200
|
|
|
|
|
|
class PasswordChangeRequiredTest(TestCase):
|
|
def setUp(self):
|
|
super().setUp()
|
|
self.user = User.objects.create_user('dummy@dummy.dummy', 'dummy')
|
|
|
|
def test_redirect_to_password_change(self):
|
|
self.user.needs_password_change = True
|
|
self.user.save()
|
|
self.client.login(email='dummy@dummy.dummy', password='dummy')
|
|
|
|
response = self.client.get('/control/events/')
|
|
|
|
self.assertEqual(response.status_code, 302)
|
|
assert self.user.needs_password_change is True
|
|
self.assertIn('/control/settings/password/change?next=/control/events/', response['Location'])
|
|
|
|
def test_redirect_to_2fa_to_password_change(self):
|
|
self.user.require_2fa = True
|
|
self.user.needs_password_change = True
|
|
self.user.save()
|
|
|
|
response = self.client.post('/control/login?next=/control/events/', {
|
|
'email': 'dummy@dummy.dummy',
|
|
'password': 'dummy',
|
|
})
|
|
|
|
self.assertEqual(response.status_code, 302)
|
|
self.assertIn('/control/login/2fa?next=/control/events/', response['Location'])
|
|
|
|
d = TOTPDevice.objects.create(user=self.user, name='test')
|
|
totp = TOTP(d.bin_key, d.step, d.t0, d.digits, d.drift)
|
|
totp.time = time.time()
|
|
|
|
self.client.post('/control/login/2fa?next=/control/events/', {
|
|
'token': str(totp.token())
|
|
})
|
|
response = self.client.get('/control/events/')
|
|
|
|
self.assertEqual(response.status_code, 302)
|
|
self.assertIn('/control/settings/password/change?next=/control/events/', response['Location'])
|