mirror of
https://github.com/pretix/pretix.git
synced 2026-09-14 16:24:43 +00:00
Stricten password validation to match PCI DSS requirements (#4467)
* Stricten password validation to match PCI DSS requirements * Review fix * Fix a file header
This commit is contained in:
+63
-1
@@ -32,13 +32,16 @@
|
|||||||
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
# 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.
|
# License for the specific language governing permissions and limitations under the License.
|
||||||
|
|
||||||
|
import string
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
from importlib import import_module
|
from importlib import import_module
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth import authenticate
|
from django.contrib.auth import authenticate
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.contrib.auth.hashers import check_password, make_password
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.utils.translation import gettext_lazy as _, ngettext
|
||||||
|
|
||||||
|
|
||||||
def get_auth_backends():
|
def get_auth_backends():
|
||||||
@@ -160,3 +163,62 @@ class NativeAuthBackend(BaseAuthBackend):
|
|||||||
u = authenticate(request=request, email=form_data['email'].lower(), password=form_data['password'])
|
u = authenticate(request=request, email=form_data['email'].lower(), password=form_data['password'])
|
||||||
if u and u.auth_backend == self.identifier:
|
if u and u.auth_backend == self.identifier:
|
||||||
return u
|
return u
|
||||||
|
|
||||||
|
|
||||||
|
class NumericAndAlphabeticPasswordValidator:
|
||||||
|
|
||||||
|
def validate(self, password, user=None):
|
||||||
|
has_numeric = any(c in string.digits for c in password)
|
||||||
|
has_alpha = any(c in string.ascii_letters for c in password)
|
||||||
|
if not has_numeric or not has_alpha:
|
||||||
|
raise ValidationError(
|
||||||
|
_(
|
||||||
|
"Your password must contain both numeric and alphabetic characters.",
|
||||||
|
),
|
||||||
|
code="password_numeric_and_alphabetic",
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_help_text(self):
|
||||||
|
return _(
|
||||||
|
"Your password must contain both numeric and alphabetic characters.",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class HistoryPasswordValidator:
|
||||||
|
|
||||||
|
def __init__(self, history_length=4):
|
||||||
|
self.history_length = history_length
|
||||||
|
|
||||||
|
def validate(self, password, user=None):
|
||||||
|
from pretix.base.models import User
|
||||||
|
|
||||||
|
if not user or not user.pk or not isinstance(user, User):
|
||||||
|
return
|
||||||
|
|
||||||
|
for hp in user.historic_passwords.order_by("-created")[:self.history_length]:
|
||||||
|
if check_password(password, hp.password):
|
||||||
|
raise ValidationError(
|
||||||
|
ngettext(
|
||||||
|
"Your password may not be the same as your previous password.",
|
||||||
|
"Your password may not be the same as one of your %(history_length)s previous passwords.",
|
||||||
|
self.history_length,
|
||||||
|
),
|
||||||
|
code="password_history",
|
||||||
|
params={"history_length": self.history_length},
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_help_text(self):
|
||||||
|
return ngettext(
|
||||||
|
"Your password may not be the same as your previous password.",
|
||||||
|
"Your password may not be the same as one of your %(history_length)s previous passwords.",
|
||||||
|
self.history_length,
|
||||||
|
) % {"history_length": self.history_length}
|
||||||
|
|
||||||
|
def password_changed(self, password, user=None):
|
||||||
|
if not user:
|
||||||
|
pass
|
||||||
|
|
||||||
|
user.historic_passwords.create(password=make_password(password))
|
||||||
|
user.historic_passwords.filter(
|
||||||
|
pk__in=user.historic_passwords.order_by("-created")[self.history_length:].values_list("pk", flat=True),
|
||||||
|
).delete()
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# Generated by Django 4.2.15 on 2024-09-16 15:10
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("pretixbase", "0269_order_api_meta"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="HistoricPassword",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"id",
|
||||||
|
models.BigAutoField(
|
||||||
|
auto_created=True, primary_key=True, serialize=False
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("created", models.DateTimeField(auto_now_add=True)),
|
||||||
|
("password", models.CharField(max_length=128)),
|
||||||
|
(
|
||||||
|
"user",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="historic_passwords",
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -654,3 +654,9 @@ class WebAuthnDevice(Device):
|
|||||||
@property
|
@property
|
||||||
def webauthnpubkey(self):
|
def webauthnpubkey(self):
|
||||||
return websafe_decode(self.pub_key)
|
return websafe_decode(self.pub_key)
|
||||||
|
|
||||||
|
|
||||||
|
class HistoricPassword(models.Model):
|
||||||
|
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="historic_passwords")
|
||||||
|
created = models.DateTimeField(auto_now_add=True)
|
||||||
|
password = models.CharField(verbose_name=_("Password"), max_length=128)
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
# You should have received a copy of the GNU Affero General Public License along with this program. If not, see
|
# 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/>.
|
# <https://www.gnu.org/licenses/>.
|
||||||
#
|
#
|
||||||
|
import functools
|
||||||
import hashlib
|
import hashlib
|
||||||
import ipaddress
|
import ipaddress
|
||||||
import random
|
import random
|
||||||
@@ -27,7 +28,7 @@ from django import forms
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth.hashers import check_password
|
from django.contrib.auth.hashers import check_password
|
||||||
from django.contrib.auth.password_validation import (
|
from django.contrib.auth.password_validation import (
|
||||||
password_validators_help_texts, validate_password,
|
get_password_validators, password_validators_help_texts, validate_password,
|
||||||
)
|
)
|
||||||
from django.contrib.auth.tokens import PasswordResetTokenGenerator
|
from django.contrib.auth.tokens import PasswordResetTokenGenerator
|
||||||
from django.core import signing
|
from django.core import signing
|
||||||
@@ -271,6 +272,11 @@ class RegistrationForm(forms.Form):
|
|||||||
return customer
|
return customer
|
||||||
|
|
||||||
|
|
||||||
|
@functools.lru_cache(maxsize=None)
|
||||||
|
def get_customer_password_validators():
|
||||||
|
return get_password_validators(settings.CUSTOMER_AUTH_PASSWORD_VALIDATORS)
|
||||||
|
|
||||||
|
|
||||||
class SetPasswordForm(forms.Form):
|
class SetPasswordForm(forms.Form):
|
||||||
required_css_class = 'required'
|
required_css_class = 'required'
|
||||||
error_messages = {
|
error_messages = {
|
||||||
@@ -311,7 +317,7 @@ class SetPasswordForm(forms.Form):
|
|||||||
|
|
||||||
def clean_password(self):
|
def clean_password(self):
|
||||||
password1 = self.cleaned_data.get('password', '')
|
password1 = self.cleaned_data.get('password', '')
|
||||||
if validate_password(password1, user=self.customer) is not None:
|
if validate_password(password1, user=self.customer, password_validators=get_customer_password_validators()) is not None:
|
||||||
raise forms.ValidationError(_(password_validators_help_texts()), code='pw_invalid')
|
raise forms.ValidationError(_(password_validators_help_texts()), code='pw_invalid')
|
||||||
return password1
|
return password1
|
||||||
|
|
||||||
@@ -405,7 +411,7 @@ class ChangePasswordForm(forms.Form):
|
|||||||
|
|
||||||
def clean_password(self):
|
def clean_password(self):
|
||||||
password1 = self.cleaned_data.get('password', '')
|
password1 = self.cleaned_data.get('password', '')
|
||||||
if validate_password(password1, user=self.customer) is not None:
|
if validate_password(password1, user=self.customer, password_validators=get_customer_password_validators()) is not None:
|
||||||
raise forms.ValidationError(_(password_validators_help_texts()), code='pw_invalid')
|
raise forms.ValidationError(_(password_validators_help_texts()), code='pw_invalid')
|
||||||
return password1
|
return password1
|
||||||
|
|
||||||
|
|||||||
+42
-1
@@ -714,6 +714,10 @@ BOOTSTRAP3 = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
PASSWORD_HASHERS = [
|
PASSWORD_HASHERS = [
|
||||||
|
# Note that when updating this, all user passwords will be re-hashed on next login, however,
|
||||||
|
# the HistoricPassword model will not be changed automatically. In case a serious issue with a hasher
|
||||||
|
# comes to light, dropping the contents of the HistoricPassword table might be the more risk-adequate
|
||||||
|
# decision.
|
||||||
"django.contrib.auth.hashers.Argon2PasswordHasher",
|
"django.contrib.auth.hashers.Argon2PasswordHasher",
|
||||||
"django.contrib.auth.hashers.PBKDF2PasswordHasher",
|
"django.contrib.auth.hashers.PBKDF2PasswordHasher",
|
||||||
"django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher",
|
"django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher",
|
||||||
@@ -725,7 +729,44 @@ AUTH_PASSWORD_VALIDATORS = [
|
|||||||
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
|
||||||
|
"OPTIONS": {
|
||||||
|
# To fulfill per PCI DSS requirement 8.3.6
|
||||||
|
"min_length": 12,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
# To fulfill per PCI DSS requirement 8.3.6
|
||||||
|
'NAME': 'pretix.base.auth.NumericAndAlphabeticPasswordValidator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"NAME": "pretix.base.auth.HistoryPasswordValidator",
|
||||||
|
"OPTIONS": {
|
||||||
|
# To fulfill per PCI DSS requirement 8.3.7
|
||||||
|
"history_length": 4,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
CUSTOMER_AUTH_PASSWORD_VALIDATORS = [
|
||||||
|
# For customer accounts, we apply a little less strict requirements to provide a risk-adequate
|
||||||
|
# user experience.
|
||||||
|
{
|
||||||
|
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
|
||||||
|
"OPTIONS": {
|
||||||
|
"min_length": 8,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'NAME': 'pretix.base.auth.NumericAndAlphabeticPasswordValidator',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||||
|
|||||||
@@ -337,7 +337,7 @@ class RegistrationFormTest(TestCase):
|
|||||||
|
|
||||||
response = self.client.post('/control/register', {
|
response = self.client.post('/control/register', {
|
||||||
'email': 'dummy@dummy.dummy',
|
'email': 'dummy@dummy.dummy',
|
||||||
'password': 'foobarbar',
|
'password': 'f00barbarbar',
|
||||||
'password_repeat': ''
|
'password_repeat': ''
|
||||||
})
|
})
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
@@ -347,8 +347,8 @@ class RegistrationFormTest(TestCase):
|
|||||||
self.user = User.objects.create_user('dummy@dummy.dummy', 'dummy')
|
self.user = User.objects.create_user('dummy@dummy.dummy', 'dummy')
|
||||||
response = self.client.post('/control/register', {
|
response = self.client.post('/control/register', {
|
||||||
'email': 'dummy@dummy.dummy',
|
'email': 'dummy@dummy.dummy',
|
||||||
'password': 'foobarbar',
|
'password': 'f00barbarbar',
|
||||||
'password_repeat': 'foobarbar'
|
'password_repeat': 'f00barbarbar'
|
||||||
})
|
})
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
@@ -356,8 +356,8 @@ class RegistrationFormTest(TestCase):
|
|||||||
def test_success(self):
|
def test_success(self):
|
||||||
response = self.client.post('/control/register', {
|
response = self.client.post('/control/register', {
|
||||||
'email': 'dummy@dummy.dummy',
|
'email': 'dummy@dummy.dummy',
|
||||||
'password': 'foobarbar',
|
'password': 'f00barbarbar',
|
||||||
'password_repeat': 'foobarbar'
|
'password_repeat': 'f00barbarbar'
|
||||||
})
|
})
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
assert time.time() - self.client.session['pretix_auth_login_time'] < 60
|
assert time.time() - self.client.session['pretix_auth_login_time'] < 60
|
||||||
@@ -367,8 +367,8 @@ class RegistrationFormTest(TestCase):
|
|||||||
def test_disabled(self):
|
def test_disabled(self):
|
||||||
response = self.client.post('/control/register', {
|
response = self.client.post('/control/register', {
|
||||||
'email': 'dummy@dummy.dummy',
|
'email': 'dummy@dummy.dummy',
|
||||||
'password': 'foobarbar',
|
'password': 'f00barbarbar',
|
||||||
'password_repeat': 'foobarbar'
|
'password_repeat': 'f00barbarbar'
|
||||||
})
|
})
|
||||||
self.assertEqual(response.status_code, 403)
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
@@ -376,8 +376,8 @@ class RegistrationFormTest(TestCase):
|
|||||||
def test_no_native_auth(self):
|
def test_no_native_auth(self):
|
||||||
response = self.client.post('/control/register', {
|
response = self.client.post('/control/register', {
|
||||||
'email': 'dummy@dummy.dummy',
|
'email': 'dummy@dummy.dummy',
|
||||||
'password': 'foobarbar',
|
'password': 'f00barbarbar',
|
||||||
'password_repeat': 'foobarbar'
|
'password_repeat': 'f00barbarbar'
|
||||||
})
|
})
|
||||||
self.assertEqual(response.status_code, 403)
|
self.assertEqual(response.status_code, 403)
|
||||||
|
|
||||||
@@ -593,8 +593,8 @@ class PasswordRecoveryFormTest(TestCase):
|
|||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
'/control/forgot/recover?id=%d&token=foo' % self.user.id,
|
'/control/forgot/recover?id=%d&token=foo' % self.user.id,
|
||||||
{
|
{
|
||||||
'password': 'foobarbar',
|
'password': 'f00barbarbar',
|
||||||
'password_repeat': 'foobarbar'
|
'password_repeat': 'f00barbarbar'
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
@@ -615,8 +615,8 @@ class PasswordRecoveryFormTest(TestCase):
|
|||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
'/control/forgot/recover?id=%d&token=%s' % (self.user.id, token),
|
'/control/forgot/recover?id=%d&token=%s' % (self.user.id, token),
|
||||||
{
|
{
|
||||||
'password': 'foobarbar',
|
'password': 'f00barbarbar',
|
||||||
'password_repeat': 'foobarbar'
|
'password_repeat': 'f00barbarbar'
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
@@ -630,13 +630,13 @@ class PasswordRecoveryFormTest(TestCase):
|
|||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
'/control/forgot/recover?id=%d&token=%s' % (self.user.id, token),
|
'/control/forgot/recover?id=%d&token=%s' % (self.user.id, token),
|
||||||
{
|
{
|
||||||
'password': 'foobarbar',
|
'password': 'f00barbarbar',
|
||||||
'password_repeat': 'foobarbar'
|
'password_repeat': 'f00barbarbar'
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
self.user = User.objects.get(id=self.user.id)
|
self.user = User.objects.get(id=self.user.id)
|
||||||
self.assertTrue(self.user.check_password('foobarbar'))
|
self.assertTrue(self.user.check_password('f00barbarbar'))
|
||||||
|
|
||||||
def test_recovery_valid_token_empty_passwords(self):
|
def test_recovery_valid_token_empty_passwords(self):
|
||||||
token = default_token_generator.make_token(self.user)
|
token = default_token_generator.make_token(self.user)
|
||||||
@@ -645,7 +645,7 @@ class PasswordRecoveryFormTest(TestCase):
|
|||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
'/control/forgot/recover?id=%d&token=%s' % (self.user.id, token),
|
'/control/forgot/recover?id=%d&token=%s' % (self.user.id, token),
|
||||||
{
|
{
|
||||||
'password': 'foobarbar',
|
'password': 'f00barbarbar',
|
||||||
'password_repeat': ''
|
'password_repeat': ''
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -660,7 +660,7 @@ class PasswordRecoveryFormTest(TestCase):
|
|||||||
'/control/forgot/recover?id=%d&token=%s' % (self.user.id, token),
|
'/control/forgot/recover?id=%d&token=%s' % (self.user.id, token),
|
||||||
{
|
{
|
||||||
'password': '',
|
'password': '',
|
||||||
'password_repeat': 'foobarbar'
|
'password_repeat': 'f00barbarbar'
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
@@ -697,6 +697,48 @@ class PasswordRecoveryFormTest(TestCase):
|
|||||||
self.user = User.objects.get(id=self.user.id)
|
self.user = User.objects.get(id=self.user.id)
|
||||||
self.assertTrue(self.user.check_password('demo'))
|
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):
|
def test_recovery_valid_token_short_passwords(self):
|
||||||
token = default_token_generator.make_token(self.user)
|
token = default_token_generator.make_token(self.user)
|
||||||
response = self.client.get('/control/forgot/recover?id=%d&token=%s' % (self.user.id, token))
|
response = self.client.get('/control/forgot/recover?id=%d&token=%s' % (self.user.id, token))
|
||||||
@@ -704,8 +746,8 @@ class PasswordRecoveryFormTest(TestCase):
|
|||||||
response = self.client.post(
|
response = self.client.post(
|
||||||
'/control/forgot/recover?id=%d&token=%s' % (self.user.id, token),
|
'/control/forgot/recover?id=%d&token=%s' % (self.user.id, token),
|
||||||
{
|
{
|
||||||
'password': 'foobar',
|
'password': 'foobarfooba',
|
||||||
'password_repeat': 'foobar'
|
'password_repeat': 'foobarfooba'
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|||||||
@@ -349,8 +349,8 @@ def test_invite_new_user(event, admin_team, client):
|
|||||||
assert b'<form' in resp.content
|
assert b'<form' in resp.content
|
||||||
resp = client.post('/control/invite/{}'.format(i.token), {
|
resp = client.post('/control/invite/{}'.format(i.token), {
|
||||||
'email': 'dummy@example.org',
|
'email': 'dummy@example.org',
|
||||||
'password': 'asdsdgfgjh',
|
'password': 'asdsdgfgjh1234567',
|
||||||
'password_repeat': 'asdsdgfgjh'
|
'password_repeat': 'asdsdgfgjh1234567'
|
||||||
}, follow=True)
|
}, follow=True)
|
||||||
|
|
||||||
assert b'alert-success' in resp.content
|
assert b'alert-success' in resp.content
|
||||||
|
|||||||
@@ -112,8 +112,8 @@ class UserSettingsTest(SoupTest):
|
|||||||
self.user.auth_backend = 'test_request'
|
self.user.auth_backend = 'test_request'
|
||||||
self.user.save()
|
self.user.save()
|
||||||
self.save({
|
self.save({
|
||||||
'new_pw': 'foobarbar',
|
'new_pw': 'f00barbarbar',
|
||||||
'new_pw_repeat': 'foobarbar',
|
'new_pw_repeat': 'f00barbarbar',
|
||||||
'old_pw': 'barfoofoo',
|
'old_pw': 'barfoofoo',
|
||||||
})
|
})
|
||||||
pw = self.user.password
|
pw = self.user.password
|
||||||
@@ -122,13 +122,13 @@ class UserSettingsTest(SoupTest):
|
|||||||
|
|
||||||
def test_change_password_success(self):
|
def test_change_password_success(self):
|
||||||
doc = self.save({
|
doc = self.save({
|
||||||
'new_pw': 'foobarbar',
|
'new_pw': 'f00barbarbar',
|
||||||
'new_pw_repeat': 'foobarbar',
|
'new_pw_repeat': 'f00barbarbar',
|
||||||
'old_pw': 'barfoofoo',
|
'old_pw': 'barfoofoo',
|
||||||
})
|
})
|
||||||
assert doc.select(".alert-success")
|
assert doc.select(".alert-success")
|
||||||
self.user = User.objects.get(pk=self.user.pk)
|
self.user = User.objects.get(pk=self.user.pk)
|
||||||
assert self.user.check_password("foobarbar")
|
assert self.user.check_password("f00barbarbar")
|
||||||
|
|
||||||
def test_change_password_short(self):
|
def test_change_password_short(self):
|
||||||
doc = self.save({
|
doc = self.save({
|
||||||
@@ -171,6 +171,28 @@ class UserSettingsTest(SoupTest):
|
|||||||
})
|
})
|
||||||
assert doc.select(".alert-danger")
|
assert doc.select(".alert-danger")
|
||||||
|
|
||||||
|
def test_change_password_history(self):
|
||||||
|
doc = self.save({
|
||||||
|
'new_pw': 'qvuSpukdKWUV7m7PoRrWwpCd2Taij9',
|
||||||
|
'new_pw_repeat': 'qvuSpukdKWUV7m7PoRrWwpCd2Taij9',
|
||||||
|
'old_pw': 'barfoofoo',
|
||||||
|
})
|
||||||
|
assert doc.select(".alert-success")
|
||||||
|
|
||||||
|
doc = self.save({
|
||||||
|
'new_pw': '9UQl4lSwHLMVUIMgw0L1X8XEFmyvdn',
|
||||||
|
'new_pw_repeat': '9UQl4lSwHLMVUIMgw0L1X8XEFmyvdn',
|
||||||
|
'old_pw': 'qvuSpukdKWUV7m7PoRrWwpCd2Taij9',
|
||||||
|
})
|
||||||
|
assert doc.select(".alert-success")
|
||||||
|
|
||||||
|
doc = self.save({
|
||||||
|
'new_pw': 'qvuSpukdKWUV7m7PoRrWwpCd2Taij9',
|
||||||
|
'new_pw_repeat': 'qvuSpukdKWUV7m7PoRrWwpCd2Taij9',
|
||||||
|
'old_pw': '9UQl4lSwHLMVUIMgw0L1X8XEFmyvdn',
|
||||||
|
})
|
||||||
|
assert doc.select(".alert-danger")
|
||||||
|
|
||||||
def test_needs_password_change(self):
|
def test_needs_password_change(self):
|
||||||
self.user.needs_password_change = True
|
self.user.needs_password_change = True
|
||||||
self.user.save()
|
self.user.save()
|
||||||
@@ -187,8 +209,8 @@ class UserSettingsTest(SoupTest):
|
|||||||
self.user.needs_password_change = True
|
self.user.needs_password_change = True
|
||||||
self.user.save()
|
self.user.save()
|
||||||
self.save({
|
self.save({
|
||||||
'new_pw': 'foobarbar',
|
'new_pw': 'f00barbarbar',
|
||||||
'new_pw_repeat': 'foobarbar',
|
'new_pw_repeat': 'f00barbarbar',
|
||||||
'old_pw': 'barfoofoo'
|
'old_pw': 'barfoofoo'
|
||||||
})
|
})
|
||||||
self.user.refresh_from_db()
|
self.user.refresh_from_db()
|
||||||
|
|||||||
Reference in New Issue
Block a user