policies/password: add minimum digits
closes #1952 Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
		| @ -13,6 +13,7 @@ class PasswordPolicySerializer(PolicySerializer): | ||||
|         model = PasswordPolicy | ||||
|         fields = PolicySerializer.Meta.fields + [ | ||||
|             "password_field", | ||||
|             "amount_digits", | ||||
|             "amount_uppercase", | ||||
|             "amount_lowercase", | ||||
|             "amount_symbols", | ||||
|  | ||||
| @ -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), | ||||
|         ), | ||||
|     ] | ||||
| @ -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) | ||||
|  | ||||
| @ -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()) | ||||
|  | ||||
							
								
								
									
										40
									
								
								schema.yml
									
									
									
									
									
								
							
							
						
						
									
										40
									
								
								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 | ||||
|  | ||||
| @ -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" | ||||
|  | ||||
| @ -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" | ||||
|  | ||||
| @ -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 "" | ||||
|  | ||||
| @ -122,6 +122,18 @@ export class PasswordPolicyForm extends ModelForm<PasswordPolicy, string> { | ||||
|                             required | ||||
|                         /> | ||||
|                     </ak-form-element-horizontal> | ||||
|                     <ak-form-element-horizontal | ||||
|                         label=${t`Minimum amount of Digits`} | ||||
|                         ?required=${true} | ||||
|                         name="amountDigits" | ||||
|                     > | ||||
|                         <input | ||||
|                             type="number" | ||||
|                             value="${first(this.instance?.amountDigits, 2)}" | ||||
|                             class="pf-c-form-control" | ||||
|                             required | ||||
|                         /> | ||||
|                     </ak-form-element-horizontal> | ||||
|                     <ak-form-element-horizontal | ||||
|                         label=${t`Minimum amount of Symbols Characters`} | ||||
|                         ?required=${true} | ||||
|  | ||||
		Reference in New Issue
	
	Block a user
	 Jens Langhammer
					Jens Langhammer