policies/password: add minimum digits
closes #1952 Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
parent
7a73ddfb60
commit
61097b9400
|
@ -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",
|
||||||
|
|
|
@ -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()
|
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)
|
||||||
|
|
|
@ -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())
|
||||||
|
|
40
schema.yml
40
schema.yml
|
@ -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
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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 ""
|
||||||
|
|
|
@ -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}
|
||||||
|
|
Reference in a new issue