diff --git a/authentik/policies/password/api.py b/authentik/policies/password/api.py index 17163eebf..4b9810a8d 100644 --- a/authentik/policies/password/api.py +++ b/authentik/policies/password/api.py @@ -13,6 +13,7 @@ class PasswordPolicySerializer(PolicySerializer): model = PasswordPolicy fields = PolicySerializer.Meta.fields + [ "password_field", + "amount_digits", "amount_uppercase", "amount_lowercase", "amount_symbols", diff --git a/authentik/policies/password/migrations/0003_passwordpolicy_amount_digits.py b/authentik/policies/password/migrations/0003_passwordpolicy_amount_digits.py new file mode 100644 index 000000000..7e9130e8f --- /dev/null +++ b/authentik/policies/password/migrations/0003_passwordpolicy_amount_digits.py @@ -0,0 +1,38 @@ +# Generated by Django 4.0 on 2021-12-18 14:54 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("authentik_policies_password", "0002_passwordpolicy_password_field"), + ] + + operations = [ + migrations.AddField( + model_name="passwordpolicy", + name="amount_digits", + field=models.PositiveIntegerField(default=0), + ), + migrations.AlterField( + model_name="passwordpolicy", + name="amount_lowercase", + field=models.PositiveIntegerField(default=0), + ), + migrations.AlterField( + model_name="passwordpolicy", + name="amount_symbols", + field=models.PositiveIntegerField(default=0), + ), + migrations.AlterField( + model_name="passwordpolicy", + name="amount_uppercase", + field=models.PositiveIntegerField(default=0), + ), + migrations.AlterField( + model_name="passwordpolicy", + name="length_min", + field=models.PositiveIntegerField(default=0), + ), + ] diff --git a/authentik/policies/password/models.py b/authentik/policies/password/models.py index 788c84667..77c37ffb8 100644 --- a/authentik/policies/password/models.py +++ b/authentik/policies/password/models.py @@ -13,6 +13,7 @@ from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT LOGGER = get_logger() RE_LOWER = re.compile("[a-z]") RE_UPPER = re.compile("[A-Z]") +RE_DIGITS = re.compile("[0-9]") class PasswordPolicy(Policy): @@ -23,10 +24,11 @@ class PasswordPolicy(Policy): help_text=_("Field key to check, field keys defined in Prompt stages are available."), ) - amount_uppercase = models.IntegerField(default=0) - amount_lowercase = models.IntegerField(default=0) - amount_symbols = models.IntegerField(default=0) - length_min = models.IntegerField(default=0) + amount_digits = models.PositiveIntegerField(default=0) + amount_uppercase = models.PositiveIntegerField(default=0) + amount_lowercase = models.PositiveIntegerField(default=0) + amount_symbols = models.PositiveIntegerField(default=0) + length_min = models.PositiveIntegerField(default=0) symbol_charset = models.TextField(default=r"!\"#$%&'()*+,-./:;<=>?@[\]^_`{|}~ ") error_message = models.TextField() @@ -40,6 +42,7 @@ class PasswordPolicy(Policy): def component(self) -> str: return "ak-policy-password-form" + # pylint: disable=too-many-return-statements def passes(self, request: PolicyRequest) -> PolicyResult: if ( self.password_field not in request.context @@ -62,6 +65,9 @@ class PasswordPolicy(Policy): LOGGER.debug("password failed", reason="length") return PolicyResult(False, self.error_message) + if self.amount_digits > 0 and len(RE_DIGITS.findall(password)) < self.amount_digits: + LOGGER.debug("password failed", reason="amount_digits") + return PolicyResult(False, self.error_message) if self.amount_lowercase > 0 and len(RE_LOWER.findall(password)) < self.amount_lowercase: LOGGER.debug("password failed", reason="amount_lowercase") return PolicyResult(False, self.error_message) diff --git a/authentik/policies/password/tests/test_policy.py b/authentik/policies/password/tests/test_policy.py index ca09be8f6..cef0c8156 100644 --- a/authentik/policies/password/tests/test_policy.py +++ b/authentik/policies/password/tests/test_policy.py @@ -13,6 +13,7 @@ class TestPasswordPolicy(TestCase): def setUp(self) -> None: self.policy = PasswordPolicy.objects.create( name="test_false", + amount_digits=1, amount_uppercase=1, amount_lowercase=2, amount_symbols=3, @@ -38,7 +39,7 @@ class TestPasswordPolicy(TestCase): def test_failed_lowercase(self): """not enough lowercase""" request = PolicyRequest(get_anonymous_user()) - request.context["password"] = "TTTTTTTTTTTTTTTTTTTTTTTe" # nosec + request.context["password"] = "1TTTTTTTTTTTTTTTTTTTTTTe" # nosec result: PolicyResult = self.policy.passes(request) self.assertFalse(result.passing) self.assertEqual(result.messages, ("test message",)) @@ -46,15 +47,23 @@ class TestPasswordPolicy(TestCase): def test_failed_uppercase(self): """not enough uppercase""" request = PolicyRequest(get_anonymous_user()) - request.context["password"] = "tttttttttttttttttttttttE" # nosec + request.context["password"] = "1tttttttttttttttttttttE" # nosec result: PolicyResult = self.policy.passes(request) self.assertFalse(result.passing) self.assertEqual(result.messages, ("test message",)) def test_failed_symbols(self): - """not enough uppercase""" + """not enough symbols""" request = PolicyRequest(get_anonymous_user()) - request.context["password"] = "TETETETETETETETETETETETETe!!!" # nosec + request.context["password"] = "1ETETETETETETETETETETETETe!!!" # nosec + result: PolicyResult = self.policy.passes(request) + self.assertFalse(result.passing) + self.assertEqual(result.messages, ("test message",)) + + def test_failed_digits(self): + """not enough digits""" + request = PolicyRequest(get_anonymous_user()) + request.context["password"] = "TETETETETETETETETETETE1e!!!" # nosec result: PolicyResult = self.policy.passes(request) self.assertFalse(result.passing) self.assertEqual(result.messages, ("test message",)) @@ -62,7 +71,7 @@ class TestPasswordPolicy(TestCase): def test_true(self): """Positive password case""" request = PolicyRequest(get_anonymous_user()) - request.context["password"] = generate_key() + "ee!!!" # nosec + request.context["password"] = generate_key() + "1ee!!!" # nosec result: PolicyResult = self.policy.passes(request) self.assertTrue(result.passing) self.assertEqual(result.messages, tuple()) diff --git a/schema.yml b/schema.yml index 52e6d924b..4ab7f2eb5 100644 --- a/schema.yml +++ b/schema.yml @@ -8309,6 +8309,10 @@ paths: operationId: policies_password_list description: Password Policy Viewset parameters: + - in: query + name: amount_digits + schema: + type: integer - in: query name: amount_lowercase schema: @@ -26413,22 +26417,26 @@ components: type: string description: Field key to check, field keys defined in Prompt stages are available. + amount_digits: + type: integer + maximum: 2147483647 + minimum: 0 amount_uppercase: type: integer maximum: 2147483647 - minimum: -2147483648 + minimum: 0 amount_lowercase: type: integer maximum: 2147483647 - minimum: -2147483648 + minimum: 0 amount_symbols: type: integer maximum: 2147483647 - minimum: -2147483648 + minimum: 0 length_min: type: integer maximum: 2147483647 - minimum: -2147483648 + minimum: 0 symbol_charset: type: string error_message: @@ -26457,22 +26465,26 @@ components: minLength: 1 description: Field key to check, field keys defined in Prompt stages are available. + amount_digits: + type: integer + maximum: 2147483647 + minimum: 0 amount_uppercase: type: integer maximum: 2147483647 - minimum: -2147483648 + minimum: 0 amount_lowercase: type: integer maximum: 2147483647 - minimum: -2147483648 + minimum: 0 amount_symbols: type: integer maximum: 2147483647 - minimum: -2147483648 + minimum: 0 length_min: type: integer maximum: 2147483647 - minimum: -2147483648 + minimum: 0 symbol_charset: type: string minLength: 1 @@ -27630,22 +27642,26 @@ components: minLength: 1 description: Field key to check, field keys defined in Prompt stages are available. + amount_digits: + type: integer + maximum: 2147483647 + minimum: 0 amount_uppercase: type: integer maximum: 2147483647 - minimum: -2147483648 + minimum: 0 amount_lowercase: type: integer maximum: 2147483647 - minimum: -2147483648 + minimum: 0 amount_symbols: type: integer maximum: 2147483647 - minimum: -2147483648 + minimum: 0 length_min: type: integer maximum: 2147483647 - minimum: -2147483648 + minimum: 0 symbol_charset: type: string minLength: 1 diff --git a/web/src/locales/en.po b/web/src/locales/en.po index 75444ade2..1dd03838b 100644 --- a/web/src/locales/en.po +++ b/web/src/locales/en.po @@ -2836,6 +2836,10 @@ msgstr "Messages" msgid "Metadata" msgstr "Metadata" +#: src/pages/policies/password/PasswordPolicyForm.ts +msgid "Minimum amount of Digits" +msgstr "Minimum amount of Digits" + #: src/pages/policies/password/PasswordPolicyForm.ts msgid "Minimum amount of Lowercase Characters" msgstr "Minimum amount of Lowercase Characters" diff --git a/web/src/locales/fr_FR.po b/web/src/locales/fr_FR.po index c205efd53..a60a1daea 100644 --- a/web/src/locales/fr_FR.po +++ b/web/src/locales/fr_FR.po @@ -2815,6 +2815,10 @@ msgstr "Messages" msgid "Metadata" msgstr "Métadonnées" +#: src/pages/policies/password/PasswordPolicyForm.ts +msgid "Minimum amount of Digits" +msgstr "" + #: src/pages/policies/password/PasswordPolicyForm.ts msgid "Minimum amount of Lowercase Characters" msgstr "Nombre minimum de caractères minuscules" diff --git a/web/src/locales/pseudo-LOCALE.po b/web/src/locales/pseudo-LOCALE.po index 91378e09d..cc142fc00 100644 --- a/web/src/locales/pseudo-LOCALE.po +++ b/web/src/locales/pseudo-LOCALE.po @@ -2826,6 +2826,10 @@ msgstr "" msgid "Metadata" msgstr "" +#: src/pages/policies/password/PasswordPolicyForm.ts +msgid "Minimum amount of Digits" +msgstr "" + #: src/pages/policies/password/PasswordPolicyForm.ts msgid "Minimum amount of Lowercase Characters" msgstr "" diff --git a/web/src/pages/policies/password/PasswordPolicyForm.ts b/web/src/pages/policies/password/PasswordPolicyForm.ts index 2c97e4b9c..62c266bcd 100644 --- a/web/src/pages/policies/password/PasswordPolicyForm.ts +++ b/web/src/pages/policies/password/PasswordPolicyForm.ts @@ -122,6 +122,18 @@ export class PasswordPolicyForm extends ModelForm { required /> + + +