-
Notifications
You must be signed in to change notification settings - Fork 24
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Create base password validator per user type (#64)
* feat: Create base password validator per user type * First part of feedback * Feedbacks * Black and some pylint * Rename validators in settings * Merge branch 'main' into reset_password * Stop using setup class * Revert "Stop using setup class" This reverts commit 1134b88. * Feedback and pylint * Black and pylint * Feedback
- Loading branch information
1 parent
279ff34
commit fb47ef5
Showing
13 changed files
with
329 additions
and
17 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
""" | ||
© Ocado Group | ||
Created on 30/01/2024 at 12:28:00(+00:00). | ||
""" | ||
|
||
from .student import StudentPasswordValidator | ||
from .independent import IndependentPasswordValidator | ||
from .teacher import TeacherPasswordValidator |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
""" | ||
© Ocado Group | ||
Created on 30/01/2024 at 17:41:00(+00:00). | ||
""" | ||
|
||
import typing as t | ||
|
||
from ...models.user import User | ||
|
||
|
||
# pylint: disable-next=too-few-public-methods | ||
class PasswordValidator: | ||
"""Base class for all password validators""" | ||
|
||
# pylint: disable-next=missing-function-docstring | ||
def validate(self, password: str, user: t.Optional[User] = None): | ||
raise NotImplementedError() |
3 changes: 0 additions & 3 deletions
3
codeforlife/user/auth/password_validators/dependent_student.py
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
""" | ||
© Ocado Group | ||
Created on 30/01/2024 at 12:30:00(+00:00). | ||
""" | ||
|
||
import re | ||
|
||
from django.core.exceptions import ValidationError | ||
from django.utils.translation import gettext as _ | ||
|
||
from .base import PasswordValidator | ||
|
||
|
||
# pylint: disable-next=missing-class-docstring | ||
class IndependentPasswordValidator(PasswordValidator): | ||
def validate(self, password, user=None): | ||
if user.teacher is None and user.student is None: | ||
min_length = 8 | ||
|
||
if len(password) < min_length: | ||
raise ValidationError( | ||
_( | ||
f"Your password must be at least {min_length} " | ||
f"characters long." | ||
), | ||
code="password_too_short", | ||
) | ||
|
||
if not re.search(r"[A-Z]", password): | ||
raise ValidationError( | ||
_("Your password must have at least 1 uppercase letter."), | ||
code="password_no_uppercase", | ||
) | ||
|
||
if not re.search(r"[a-z]", password): | ||
raise ValidationError( | ||
_("Your password must have at least 1 lowercase letter."), | ||
code="password_no_lowercase", | ||
) | ||
|
||
if not re.search(r"[0-9]", password): | ||
raise ValidationError( | ||
_("Your password must have at least 1 digit."), | ||
code="password_no_digit", | ||
) |
3 changes: 0 additions & 3 deletions
3
codeforlife/user/auth/password_validators/independent_student.py
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
""" | ||
© Ocado Group | ||
Created on 30/01/2024 at 12:28:00(+00:00). | ||
""" | ||
|
||
from django.core.exceptions import ValidationError | ||
from django.utils.translation import gettext as _ | ||
|
||
from .base import PasswordValidator | ||
|
||
|
||
# pylint: disable-next=missing-class-docstring | ||
class StudentPasswordValidator(PasswordValidator): | ||
def validate(self, password, user=None): | ||
if user.student is not None: | ||
min_length = 6 | ||
|
||
if len(password) < min_length: | ||
raise ValidationError( | ||
_( | ||
f"Your password must be at least {min_length} " | ||
f"characters long." | ||
), | ||
code="password_too_short", | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,53 @@ | ||
# TODO: https://docs.djangoproject.com/en/3.2/topics/auth/passwords/#writing-your-own-validator | ||
class TeacherPasswordValidator: | ||
pass | ||
""" | ||
© Ocado Group | ||
Created on 30/01/2024 at 12:32:00(+00:00). | ||
""" | ||
|
||
import re | ||
|
||
from django.core.exceptions import ValidationError | ||
from django.utils.translation import gettext as _ | ||
|
||
from .base import PasswordValidator | ||
|
||
|
||
# pylint: disable-next=missing-class-docstring | ||
class TeacherPasswordValidator(PasswordValidator): | ||
def validate(self, password, user=None): | ||
if user.teacher is not None: | ||
min_length = 10 | ||
|
||
if len(password) < min_length: | ||
raise ValidationError( | ||
_( | ||
f"Your password needs to be at least {min_length} " | ||
f"characters long." | ||
), | ||
code="password_too_short", | ||
) | ||
|
||
if not re.search(r"[A-Z]", password): | ||
raise ValidationError( | ||
_("Your password must have at least 1 uppercase letter."), | ||
code="password_no_uppercase", | ||
) | ||
|
||
if not re.search(r"[a-z]", password): | ||
raise ValidationError( | ||
_("Your password must have at least 1 lowercase letter."), | ||
code="password_no_lowercase", | ||
) | ||
|
||
if not re.search(r"[0-9]", password): | ||
raise ValidationError( | ||
_("Your password must have at least 1 digit."), | ||
code="password_no_digit", | ||
) | ||
|
||
if not re.search( | ||
r"[!@#$%^&*()_+\-=\[\]{};':\"\\|,.<>\/?]", password | ||
): | ||
raise ValidationError( | ||
_("Your password must have at least 1 special character."), | ||
code="password_no_special_character", | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
""" | ||
© Ocado Group | ||
Created on 30/01/2024 at 12:36:00(+00:00). | ||
""" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
""" | ||
© Ocado Group | ||
Created on 30/01/2024 at 19:01:00(+00:00). | ||
Base test case for all password validators. | ||
""" | ||
|
||
from django.core.exceptions import ValidationError | ||
from django.test import TestCase | ||
|
||
|
||
class PasswordValidatorTestCase(TestCase): | ||
"""Base for all password validator test cases.""" | ||
|
||
def assert_raises_validation_error(self, code: str, *args, **kwargs): | ||
"""Assert code block raises a validation error. | ||
Args: | ||
code: The validation code to assert. | ||
Returns: | ||
The assert-raises context which will auto-assert the code. | ||
""" | ||
|
||
context = self.assertRaises(ValidationError, *args, **kwargs) | ||
|
||
class ContextWrapper: | ||
"""Wrap context to assert code on exit.""" | ||
|
||
def __init__(self, context): | ||
self.context = context | ||
|
||
def __enter__(self, *args, **kwargs): | ||
return self.context.__enter__(*args, **kwargs) | ||
|
||
def __exit__(self, *args, **kwargs): | ||
value = self.context.__exit__(*args, **kwargs) | ||
assert self.context.exception.code == code | ||
return value | ||
|
||
return ContextWrapper(context) |
48 changes: 48 additions & 0 deletions
48
codeforlife/user/tests/auth/password_validators/test_independent.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
""" | ||
© Ocado Group | ||
Created on 30/01/2024 at 12:36:00(+00:00). | ||
""" | ||
|
||
from .base import PasswordValidatorTestCase | ||
from ....auth.password_validators import IndependentPasswordValidator | ||
from ....models.user import User | ||
|
||
|
||
class TestIndependentPasswordValidator(PasswordValidatorTestCase): | ||
@classmethod | ||
def setUpClass(cls): | ||
cls.user = User.objects.filter( | ||
new_teacher__isnull=True, new_student__isnull=True | ||
).first() | ||
assert cls.user is not None | ||
|
||
cls.validator = IndependentPasswordValidator() | ||
super(TestIndependentPasswordValidator, cls).setUpClass() | ||
|
||
def test_validate__password_too_short(self): | ||
"""Password cannot be too short""" | ||
password = "fxwSn4}" | ||
|
||
with self.assert_raises_validation_error("password_too_short"): | ||
self.validator.validate(password, self.user) | ||
|
||
def test_validate__password_no_uppercase(self): | ||
"""Password must contain an uppercase char""" | ||
password = ">28v*@a)-{" | ||
|
||
with self.assert_raises_validation_error("password_no_uppercase"): | ||
self.validator.validate(password, self.user) | ||
|
||
def test_validate__password_no_lowercase(self): | ||
"""Password must contain a lowercase char""" | ||
password = "F:6]LH!_5>" | ||
|
||
with self.assert_raises_validation_error("password_no_lowercase"): | ||
self.validator.validate(password, self.user) | ||
|
||
def test_validate__password_no_digit(self): | ||
"""Password must contain a digit""" | ||
password = "{$#FJdxGvs" | ||
|
||
with self.assert_raises_validation_error("password_no_digit"): | ||
self.validator.validate(password, self.user) |
25 changes: 25 additions & 0 deletions
25
codeforlife/user/tests/auth/password_validators/test_student.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
""" | ||
© Ocado Group | ||
Created on 30/01/2024 at 12:36:00(+00:00). | ||
""" | ||
|
||
from .base import PasswordValidatorTestCase | ||
from ....auth.password_validators import StudentPasswordValidator | ||
from ....models.user import User | ||
|
||
|
||
class TestStudentPasswordValidator(PasswordValidatorTestCase): | ||
@classmethod | ||
def setUpClass(cls): | ||
cls.user = User.objects.filter(new_student__isnull=False).first() | ||
assert cls.user is not None | ||
|
||
cls.validator = StudentPasswordValidator() | ||
super(TestStudentPasswordValidator, cls).setUpClass() | ||
|
||
def test_validate__password_too_short(self): | ||
"""Password cannot be too short""" | ||
password = "fxwSn" | ||
|
||
with self.assert_raises_validation_error("password_too_short"): | ||
self.validator.validate(password, self.user) |
55 changes: 55 additions & 0 deletions
55
codeforlife/user/tests/auth/password_validators/test_teacher.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
""" | ||
© Ocado Group | ||
Created on 30/01/2024 at 12:36:00(+00:00). | ||
""" | ||
|
||
from .base import PasswordValidatorTestCase | ||
from ....auth.password_validators import TeacherPasswordValidator | ||
from ....models.user import User | ||
|
||
|
||
class TestTeacherPasswordValidator(PasswordValidatorTestCase): | ||
@classmethod | ||
def setUpClass(cls): | ||
cls.user = User.objects.filter(new_teacher__isnull=False).first() | ||
assert cls.user is not None | ||
|
||
cls.validator = TeacherPasswordValidator() | ||
super(TestTeacherPasswordValidator, cls).setUpClass() | ||
|
||
def test_validate__password_too_short(self): | ||
"""Password cannot be too short""" | ||
password = "fxwSn4}PW" | ||
|
||
with self.assert_raises_validation_error("password_too_short"): | ||
self.validator.validate(password, self.user) | ||
|
||
def test_validate__password_no_uppercase(self): | ||
"""Password must contain an uppercase char""" | ||
password = ">28v*@a)-{" | ||
|
||
with self.assert_raises_validation_error("password_no_uppercase"): | ||
self.validator.validate(password, self.user) | ||
|
||
def test_validate__password_no_lowercase(self): | ||
"""Password must contain a lowercase char""" | ||
password = "F:6]LH!_5>" | ||
|
||
with self.assert_raises_validation_error("password_no_lowercase"): | ||
self.validator.validate(password, self.user) | ||
|
||
def test_validate__password_no_digit(self): | ||
"""Password must contain a digit""" | ||
password = "{$#FJdxGvs" | ||
|
||
with self.assert_raises_validation_error("password_no_digit"): | ||
self.validator.validate(password, self.user) | ||
|
||
def test_validate__password_no_special_character(self): | ||
"""Password must contain a special char""" | ||
password = "kR48SsAwrE" | ||
|
||
with self.assert_raises_validation_error( | ||
"password_no_special_character" | ||
): | ||
self.validator.validate(password, self.user) |