policies/password: add minimum digits

closes #1952

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
Jens Langhammer 2021-12-18 16:15:56 +01:00
parent 7a73ddfb60
commit 61097b9400
9 changed files with 115 additions and 21 deletions

View file

@ -13,6 +13,7 @@ class PasswordPolicySerializer(PolicySerializer):
model = PasswordPolicy model = PasswordPolicy
fields = PolicySerializer.Meta.fields + [ fields = PolicySerializer.Meta.fields + [
"password_field", "password_field",
"amount_digits",
"amount_uppercase", "amount_uppercase",
"amount_lowercase", "amount_lowercase",
"amount_symbols", "amount_symbols",

View file

@ -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),
),
]

View file

@ -13,6 +13,7 @@ from authentik.stages.prompt.stage import PLAN_CONTEXT_PROMPT
LOGGER = get_logger() LOGGER = get_logger()
RE_LOWER = re.compile("[a-z]") RE_LOWER = re.compile("[a-z]")
RE_UPPER = re.compile("[A-Z]") RE_UPPER = re.compile("[A-Z]")
RE_DIGITS = re.compile("[0-9]")
class PasswordPolicy(Policy): class PasswordPolicy(Policy):
@ -23,10 +24,11 @@ class PasswordPolicy(Policy):
help_text=_("Field key to check, field keys defined in Prompt stages are available."), help_text=_("Field key to check, field keys defined in Prompt stages are available."),
) )
amount_uppercase = models.IntegerField(default=0) amount_digits = models.PositiveIntegerField(default=0)
amount_lowercase = models.IntegerField(default=0) amount_uppercase = models.PositiveIntegerField(default=0)
amount_symbols = models.IntegerField(default=0) amount_lowercase = models.PositiveIntegerField(default=0)
length_min = models.IntegerField(default=0) amount_symbols = models.PositiveIntegerField(default=0)
length_min = models.PositiveIntegerField(default=0)
symbol_charset = models.TextField(default=r"!\"#$%&'()*+,-./:;<=>?@[\]^_`{|}~ ") symbol_charset = models.TextField(default=r"!\"#$%&'()*+,-./:;<=>?@[\]^_`{|}~ ")
error_message = models.TextField() error_message = models.TextField()
@ -40,6 +42,7 @@ class PasswordPolicy(Policy):
def component(self) -> str: def component(self) -> str:
return "ak-policy-password-form" return "ak-policy-password-form"
# pylint: disable=too-many-return-statements
def passes(self, request: PolicyRequest) -> PolicyResult: def passes(self, request: PolicyRequest) -> PolicyResult:
if ( if (
self.password_field not in request.context self.password_field not in request.context
@ -62,6 +65,9 @@ class PasswordPolicy(Policy):
LOGGER.debug("password failed", reason="length") LOGGER.debug("password failed", reason="length")
return PolicyResult(False, self.error_message) 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: if self.amount_lowercase > 0 and len(RE_LOWER.findall(password)) < self.amount_lowercase:
LOGGER.debug("password failed", reason="amount_lowercase") LOGGER.debug("password failed", reason="amount_lowercase")
return PolicyResult(False, self.error_message) return PolicyResult(False, self.error_message)

View file

@ -13,6 +13,7 @@ class TestPasswordPolicy(TestCase):
def setUp(self) -> None: def setUp(self) -> None:
self.policy = PasswordPolicy.objects.create( self.policy = PasswordPolicy.objects.create(
name="test_false", name="test_false",
amount_digits=1,
amount_uppercase=1, amount_uppercase=1,
amount_lowercase=2, amount_lowercase=2,
amount_symbols=3, amount_symbols=3,
@ -38,7 +39,7 @@ class TestPasswordPolicy(TestCase):
def test_failed_lowercase(self): def test_failed_lowercase(self):
"""not enough lowercase""" """not enough lowercase"""
request = PolicyRequest(get_anonymous_user()) request = PolicyRequest(get_anonymous_user())
request.context["password"] = "TTTTTTTTTTTTTTTTTTTTTTTe" # nosec request.context["password"] = "1TTTTTTTTTTTTTTTTTTTTTTe" # nosec
result: PolicyResult = self.policy.passes(request) result: PolicyResult = self.policy.passes(request)
self.assertFalse(result.passing) self.assertFalse(result.passing)
self.assertEqual(result.messages, ("test message",)) self.assertEqual(result.messages, ("test message",))
@ -46,15 +47,23 @@ class TestPasswordPolicy(TestCase):
def test_failed_uppercase(self): def test_failed_uppercase(self):
"""not enough uppercase""" """not enough uppercase"""
request = PolicyRequest(get_anonymous_user()) request = PolicyRequest(get_anonymous_user())
request.context["password"] = "tttttttttttttttttttttttE" # nosec request.context["password"] = "1tttttttttttttttttttttE" # nosec
result: PolicyResult = self.policy.passes(request) result: PolicyResult = self.policy.passes(request)
self.assertFalse(result.passing) self.assertFalse(result.passing)
self.assertEqual(result.messages, ("test message",)) self.assertEqual(result.messages, ("test message",))
def test_failed_symbols(self): def test_failed_symbols(self):
"""not enough uppercase""" """not enough symbols"""
request = PolicyRequest(get_anonymous_user()) 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) result: PolicyResult = self.policy.passes(request)
self.assertFalse(result.passing) self.assertFalse(result.passing)
self.assertEqual(result.messages, ("test message",)) self.assertEqual(result.messages, ("test message",))
@ -62,7 +71,7 @@ class TestPasswordPolicy(TestCase):
def test_true(self): def test_true(self):
"""Positive password case""" """Positive password case"""
request = PolicyRequest(get_anonymous_user()) 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) result: PolicyResult = self.policy.passes(request)
self.assertTrue(result.passing) self.assertTrue(result.passing)
self.assertEqual(result.messages, tuple()) self.assertEqual(result.messages, tuple())

View file

@ -8309,6 +8309,10 @@ paths:
operationId: policies_password_list operationId: policies_password_list
description: Password Policy Viewset description: Password Policy Viewset
parameters: parameters:
- in: query
name: amount_digits
schema:
type: integer
- in: query - in: query
name: amount_lowercase name: amount_lowercase
schema: schema:
@ -26413,22 +26417,26 @@ components:
type: string type: string
description: Field key to check, field keys defined in Prompt stages are description: Field key to check, field keys defined in Prompt stages are
available. available.
amount_digits:
type: integer
maximum: 2147483647
minimum: 0
amount_uppercase: amount_uppercase:
type: integer type: integer
maximum: 2147483647 maximum: 2147483647
minimum: -2147483648 minimum: 0
amount_lowercase: amount_lowercase:
type: integer type: integer
maximum: 2147483647 maximum: 2147483647
minimum: -2147483648 minimum: 0
amount_symbols: amount_symbols:
type: integer type: integer
maximum: 2147483647 maximum: 2147483647
minimum: -2147483648 minimum: 0
length_min: length_min:
type: integer type: integer
maximum: 2147483647 maximum: 2147483647
minimum: -2147483648 minimum: 0
symbol_charset: symbol_charset:
type: string type: string
error_message: error_message:
@ -26457,22 +26465,26 @@ components:
minLength: 1 minLength: 1
description: Field key to check, field keys defined in Prompt stages are description: Field key to check, field keys defined in Prompt stages are
available. available.
amount_digits:
type: integer
maximum: 2147483647
minimum: 0
amount_uppercase: amount_uppercase:
type: integer type: integer
maximum: 2147483647 maximum: 2147483647
minimum: -2147483648 minimum: 0
amount_lowercase: amount_lowercase:
type: integer type: integer
maximum: 2147483647 maximum: 2147483647
minimum: -2147483648 minimum: 0
amount_symbols: amount_symbols:
type: integer type: integer
maximum: 2147483647 maximum: 2147483647
minimum: -2147483648 minimum: 0
length_min: length_min:
type: integer type: integer
maximum: 2147483647 maximum: 2147483647
minimum: -2147483648 minimum: 0
symbol_charset: symbol_charset:
type: string type: string
minLength: 1 minLength: 1
@ -27630,22 +27642,26 @@ components:
minLength: 1 minLength: 1
description: Field key to check, field keys defined in Prompt stages are description: Field key to check, field keys defined in Prompt stages are
available. available.
amount_digits:
type: integer
maximum: 2147483647
minimum: 0
amount_uppercase: amount_uppercase:
type: integer type: integer
maximum: 2147483647 maximum: 2147483647
minimum: -2147483648 minimum: 0
amount_lowercase: amount_lowercase:
type: integer type: integer
maximum: 2147483647 maximum: 2147483647
minimum: -2147483648 minimum: 0
amount_symbols: amount_symbols:
type: integer type: integer
maximum: 2147483647 maximum: 2147483647
minimum: -2147483648 minimum: 0
length_min: length_min:
type: integer type: integer
maximum: 2147483647 maximum: 2147483647
minimum: -2147483648 minimum: 0
symbol_charset: symbol_charset:
type: string type: string
minLength: 1 minLength: 1

View file

@ -2836,6 +2836,10 @@ msgstr "Messages"
msgid "Metadata" msgid "Metadata"
msgstr "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 #: src/pages/policies/password/PasswordPolicyForm.ts
msgid "Minimum amount of Lowercase Characters" msgid "Minimum amount of Lowercase Characters"
msgstr "Minimum amount of Lowercase Characters" msgstr "Minimum amount of Lowercase Characters"

View file

@ -2815,6 +2815,10 @@ msgstr "Messages"
msgid "Metadata" msgid "Metadata"
msgstr "Métadonnées" msgstr "Métadonnées"
#: src/pages/policies/password/PasswordPolicyForm.ts
msgid "Minimum amount of Digits"
msgstr ""
#: src/pages/policies/password/PasswordPolicyForm.ts #: src/pages/policies/password/PasswordPolicyForm.ts
msgid "Minimum amount of Lowercase Characters" msgid "Minimum amount of Lowercase Characters"
msgstr "Nombre minimum de caractères minuscules" msgstr "Nombre minimum de caractères minuscules"

View file

@ -2826,6 +2826,10 @@ msgstr ""
msgid "Metadata" msgid "Metadata"
msgstr "" msgstr ""
#: src/pages/policies/password/PasswordPolicyForm.ts
msgid "Minimum amount of Digits"
msgstr ""
#: src/pages/policies/password/PasswordPolicyForm.ts #: src/pages/policies/password/PasswordPolicyForm.ts
msgid "Minimum amount of Lowercase Characters" msgid "Minimum amount of Lowercase Characters"
msgstr "" msgstr ""

View file

@ -122,6 +122,18 @@ export class PasswordPolicyForm extends ModelForm<PasswordPolicy, string> {
required required
/> />
</ak-form-element-horizontal> </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 <ak-form-element-horizontal
label=${t`Minimum amount of Symbols Characters`} label=${t`Minimum amount of Symbols Characters`}
?required=${true} ?required=${true}