Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

policies/password: password uniqueness history #13453

Open
wants to merge 56 commits into
base: main
Choose a base branch
from
Open
Changes from 1 commit
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
eb56da4
Add UniquePasswordPolicy model and migration
verkaufer Jul 23, 2024
798af64
Add UserPasswordHistory model and migration.
verkaufer Jul 23, 2024
f57d7fb
Add serializer for UniquePasswordPolicy
verkaufer Jul 24, 2024
7ee7115
Add UniquePasswordPolicy config to schema
verkaufer Jul 24, 2024
771ef97
Implement UniquePasswordPolicy algorithm
verkaufer Jul 23, 2024
8de2221
fix: Return error message when UniquePasswordPolicy fails
verkaufer Jul 27, 2024
f067fd9
on_user_write signal now records user's password to UserPasswordHisto…
verkaufer Jul 27, 2024
ff79ad6
Introduce PasswordPolicyUniquenessForm to PolicyWizard
verkaufer Jul 27, 2024
909aaaa
Update UniquePasswordPolicy component property
verkaufer Jul 27, 2024
94f7d69
Delete all UserPasswordHistory records when UniquePasswordPolicy not …
verkaufer Jul 27, 2024
d678c0c
Trim the UserPasswordHistory table when user updates their own password.
verkaufer Jul 27, 2024
be3b144
fix: Exit purge_password_history if there are 2 or more existing bind…
verkaufer Jul 31, 2024
e942c3e
Add comment to root policies/app.py explaining app structure
verkaufer Aug 2, 2024
ee06b8a
Move UniquePasswordPolicy to its own policies sub-app
verkaufer Aug 2, 2024
8c93822
Move unique password policy tasks and signals into policies.unique_pa…
verkaufer Aug 4, 2024
4d812c0
Move signal handler for password history updates to unique_password app
verkaufer Aug 4, 2024
0a6bcef
purge_password_history_table now checks for other bindings during asy…
verkaufer Aug 4, 2024
8da0e12
Add BoundPolicyQuerySet to filter for enabled policy bindings related…
verkaufer Aug 4, 2024
836efed
Add flow test for unique password policy
verkaufer Aug 4, 2024
9f6a7d8
move PasswordPolicyUniquenessForm to its own module; rename to Unique…
verkaufer Aug 4, 2024
32cbdd9
fix: UniquePasswordPolicyViewset uses correct ModelViewSet class
verkaufer Aug 9, 2024
a3ac49e
Move UserPasswordHistory and store old password in CharField
verkaufer Aug 9, 2024
7676939
Move test for user_write
verkaufer Aug 9, 2024
61edb52
fix: Resolve schema conflict, UniquePasswordPolicy app now inherits f…
verkaufer Aug 17, 2024
4ff7b9b
fix: UniquePasswordPolicy form defaults numHistoricalPasswords to 1
verkaufer Aug 17, 2024
741c1bb
Merge branch 'main' into pr/10631
melizeche Mar 10, 2025
58a64b5
Fix imports and linting errors
melizeche Mar 10, 2025
50da439
Fix __str__ for tests
melizeche Mar 10, 2025
d4342d5
Add #nosec in tests to please bandit
melizeche Mar 10, 2025
c4b31fc
Merge branch 'main' into feature/unique_passwords
melizeche Mar 11, 2025
d62fa76
Improve error message
melizeche Mar 18, 2025
5681f4b
Merge branch 'main' into feature/unique_passwords
melizeche Mar 18, 2025
eeba846
update schema.yml
melizeche Mar 18, 2025
97c316f
fix policy not correctly checking bindings
BeryJu Mar 19, 2025
42ed2ee
Improve tests
melizeche Mar 19, 2025
d7c2c3d
Add more tests
melizeche Mar 19, 2025
e071160
Move policy in_use logic to the model, add tests
melizeche Mar 19, 2025
35c74a7
fix linting
melizeche Mar 19, 2025
57d1af1
Use 1 as default for num_historical_passwords
melizeche Mar 24, 2025
c28ae0d
Simplify __str__, remove unnecessary JIT import
melizeche Mar 24, 2025
616dc1b
make delete query more readable
melizeche Mar 24, 2025
12f8e1a
lint fix
melizeche Mar 24, 2025
0e15681
move trim_all_password_histories and check_and_purge_password_history…
melizeche Mar 24, 2025
a19084a
Use exclude instead of difference because of errors
melizeche Mar 24, 2025
927169d
Use password_changed signal instead of user_write
melizeche Mar 24, 2025
e4cf79f
Fix tests
melizeche Mar 24, 2025
8953b9a
lint fix
melizeche Mar 24, 2025
cecbfbb
update migration file
melizeche Mar 24, 2025
72633cc
Add null case for our test runner
melizeche Mar 24, 2025
37ad8de
Add more tests for trim and purge password history
melizeche Mar 24, 2025
3187091
Configure as enterprise feature
melizeche Mar 26, 2025
537d157
Better wording for the docstring
melizeche Mar 26, 2025
4460220
fix wrong import
melizeche Mar 26, 2025
6590b17
Shorter docstring wording
melizeche Mar 26, 2025
54d80eb
Better wording in UniquePasswordPolicyForm
melizeche Mar 26, 2025
a46bb13
save hibp related things
BeryJu Mar 26, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Delete all UserPasswordHistory records when UniquePasswordPolicy not …
…bound to anything
verkaufer authored and David Gunter committed Aug 17, 2024
commit 94f7d69b3a5697794ef1fda982ab6fc80512d3f2
18 changes: 18 additions & 0 deletions authentik/core/tasks.py
Original file line number Diff line number Diff line change
@@ -15,6 +15,7 @@
AuthenticatedSession,
ExpiringModel,
User,
UserPasswordHistory,
)
from authentik.events.system_tasks import SystemTask, TaskStatus, prefill_task
from authentik.lib.config import CONFIG
@@ -92,3 +93,20 @@ def clean_temporary_users(self: SystemTask):
deleted_users += 1
messages.append(f"Successfully deleted {deleted_users} users.")
self.set_status(TaskStatus.SUCCESSFUL, *messages)


@CELERY_APP.task(bind=True, base=SystemTask)
@prefill_task
def purge_password_history_table(self: SystemTask):
"""Remove all entries from the core.models.UserPasswordHistory table"""
messages = []
try:
# n.b. a performance optimization to execute TRUNCATE
# instead of all().delete() would eliminate any FK checks.
UserPasswordHistory.objects.all().delete()
except Exception as err:
LOGGER.debug("Failed to purge core.models.UserPasswordHistory table.")
self.set_error(err)
return
messages.append("Successfully purged core.models.UserPasswordHistory")
self.set_status(TaskStatus.SUCCESSFUL, *messages)
18 changes: 17 additions & 1 deletion authentik/core/tests/test_tasks.py
Original file line number Diff line number Diff line change
@@ -12,8 +12,13 @@
Token,
TokenIntents,
User,
UserPasswordHistory,
)
from authentik.core.tasks import (
clean_expired_models,
clean_temporary_users,
purge_password_history_table,
)
from authentik.core.tasks import clean_expired_models, clean_temporary_users
from authentik.core.tests.utils import create_test_admin_user
from authentik.lib.generators import generate_id

@@ -49,3 +54,14 @@ def test_clean_temporary_users(self):
)
clean_temporary_users.delay().get()
self.assertFalse(User.objects.filter(username=username))

def test_purge_password_history_table(self):
"""Tests the task empties the core.models.UserPasswordHistory table"""
UserPasswordHistory.objects.bulk_create(
[
UserPasswordHistory(user=self.user, change={"old_password": "hunter1"}),
UserPasswordHistory(user=self.user, change={"old_password": "hunter2"}),
]
)
purge_password_history_table.delay().get()
self.assertFalse(UserPasswordHistory.objects.all())
22 changes: 21 additions & 1 deletion authentik/policies/signals.py
Original file line number Diff line number Diff line change
@@ -2,12 +2,13 @@

from django.core.cache import cache
from django.db import connection
from django.db.models.signals import post_save
from django.db.models.signals import post_delete, post_save
from django.dispatch import receiver
from structlog.stdlib import get_logger

from authentik.core.api.applications import user_app_cache_key
from authentik.core.models import Group, User
from authentik.core.tasks import purge_password_history_table
from authentik.policies.apps import GAUGE_POLICIES_CACHED
from authentik.policies.models import Policy, PolicyBinding, PolicyBindingModel
from authentik.policies.types import CACHE_PREFIX
@@ -42,3 +43,22 @@ def invalidate_policy_cache(sender, instance, **_):
# Also delete user application cache
keys = cache.keys(user_app_cache_key("*")) or []
cache.delete_many(keys)


@receiver(post_delete, sender=PolicyBinding)
def purge_password_history(sender, instance, **_):
from authentik.policies.password.models import UniquePasswordPolicy

if not isinstance(instance.policy, UniquePasswordPolicy):
return

unique_password_policies = UniquePasswordPolicy.objects.all()

policy_binding_qs = PolicyBinding.objects.filter(policy__in=unique_password_policies).filter(
enabled=True
)

if policy_binding_qs.exists():
# No-op; At least one UniquePasswordPolicy binding still exists
return
purge_password_history_table.delay()