core: don't delete expired tokens, rotate their key
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
		| @ -381,6 +381,13 @@ class ExpiringModel(models.Model): | |||||||
|     expires = models.DateTimeField(default=default_token_duration) |     expires = models.DateTimeField(default=default_token_duration) | ||||||
|     expiring = models.BooleanField(default=True) |     expiring = models.BooleanField(default=True) | ||||||
|  |  | ||||||
|  |     def expire_action(self, *args, **kwargs): | ||||||
|  |         """Handler which is called when this object is expired. By | ||||||
|  |         default the object is deleted. This is less efficient compared | ||||||
|  |         to bulk deleting objects, but classes like Token() need to change | ||||||
|  |         values instead of being deleted.""" | ||||||
|  |         return self.delete(*args, **kwargs) | ||||||
|  |  | ||||||
|     @classmethod |     @classmethod | ||||||
|     def filter_not_expired(cls, **kwargs) -> QuerySet: |     def filter_not_expired(cls, **kwargs) -> QuerySet: | ||||||
|         """Filer for tokens which are not expired yet or are not expiring, |         """Filer for tokens which are not expired yet or are not expiring, | ||||||
| @ -425,6 +432,18 @@ class Token(ManagedModel, ExpiringModel): | |||||||
|     user = models.ForeignKey("User", on_delete=models.CASCADE, related_name="+") |     user = models.ForeignKey("User", on_delete=models.CASCADE, related_name="+") | ||||||
|     description = models.TextField(default="", blank=True) |     description = models.TextField(default="", blank=True) | ||||||
|  |  | ||||||
|  |     def expire_action(self, *args, **kwargs): | ||||||
|  |         """Handler which is called when this object is expired.""" | ||||||
|  |         from authentik.events.models import Event, EventAction | ||||||
|  |  | ||||||
|  |         self.key = default_token_key() | ||||||
|  |         self.save(*args, **kwargs) | ||||||
|  |         Event.new( | ||||||
|  |             action=EventAction.SECRET_ROTATE, | ||||||
|  |             token=self, | ||||||
|  |             message=f"Token {self.identifier}'s secret was rotated.", | ||||||
|  |         ).save() | ||||||
|  |  | ||||||
|     def __str__(self): |     def __str__(self): | ||||||
|         description = f"{self.identifier}" |         description = f"{self.identifier}" | ||||||
|         if self.expiring: |         if self.expiring: | ||||||
|  | |||||||
| @ -26,14 +26,16 @@ def clean_expired_models(self: MonitoredTask): | |||||||
|     messages = [] |     messages = [] | ||||||
|     for cls in ExpiringModel.__subclasses__(): |     for cls in ExpiringModel.__subclasses__(): | ||||||
|         cls: ExpiringModel |         cls: ExpiringModel | ||||||
|         amount, _ = ( |         objects = ( | ||||||
|             cls.objects.all() |             cls.objects.all() | ||||||
|             .exclude(expiring=False) |             .exclude(expiring=False) | ||||||
|             .exclude(expiring=True, expires__gt=now()) |             .exclude(expiring=True, expires__gt=now()) | ||||||
|             .delete() |  | ||||||
|         ) |         ) | ||||||
|         LOGGER.debug("Deleted expired models", model=cls, amount=amount) |         for obj in objects: | ||||||
|         messages.append(f"Deleted {amount} expired {cls._meta.verbose_name_plural}") |             obj.expire_action() | ||||||
|  |         amount = objects.count() | ||||||
|  |         LOGGER.debug("Expired models", model=cls, amount=amount) | ||||||
|  |         messages.append(f"Expired {amount} {cls._meta.verbose_name_plural}") | ||||||
|     self.set_status(TaskResult(TaskResultStatus.SUCCESSFUL, messages)) |     self.set_status(TaskResult(TaskResultStatus.SUCCESSFUL, messages)) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | |||||||
| @ -1,18 +0,0 @@ | |||||||
| """authentik core task tests""" |  | ||||||
| from django.test import TestCase |  | ||||||
| from django.utils.timezone import now |  | ||||||
| from guardian.shortcuts import get_anonymous_user |  | ||||||
|  |  | ||||||
| from authentik.core.models import Token |  | ||||||
| from authentik.core.tasks import clean_expired_models |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestTasks(TestCase): |  | ||||||
|     """Test Tasks""" |  | ||||||
|  |  | ||||||
|     def test_token_cleanup(self): |  | ||||||
|         """Test Token cleanup task""" |  | ||||||
|         Token.objects.create(expires=now(), user=get_anonymous_user()) |  | ||||||
|         self.assertEqual(Token.objects.all().count(), 1) |  | ||||||
|         clean_expired_models.delay().get() |  | ||||||
|         self.assertEqual(Token.objects.all().count(), 0) |  | ||||||
| @ -1,5 +1,7 @@ | |||||||
| """Test token API""" | """Test token API""" | ||||||
| from django.urls.base import reverse | from django.urls.base import reverse | ||||||
|  | from django.utils.timezone import now | ||||||
|  | from guardian.shortcuts import get_anonymous_user | ||||||
| from rest_framework.test import APITestCase | from rest_framework.test import APITestCase | ||||||
|  |  | ||||||
| from authentik.core.models import ( | from authentik.core.models import ( | ||||||
| @ -8,6 +10,7 @@ from authentik.core.models import ( | |||||||
|     TokenIntents, |     TokenIntents, | ||||||
|     User, |     User, | ||||||
| ) | ) | ||||||
|  | from authentik.core.tasks import clean_expired_models | ||||||
|  |  | ||||||
|  |  | ||||||
| class TestTokenAPI(APITestCase): | class TestTokenAPI(APITestCase): | ||||||
| @ -41,3 +44,11 @@ class TestTokenAPI(APITestCase): | |||||||
|         self.assertEqual(token.user, self.user) |         self.assertEqual(token.user, self.user) | ||||||
|         self.assertEqual(token.intent, TokenIntents.INTENT_API) |         self.assertEqual(token.intent, TokenIntents.INTENT_API) | ||||||
|         self.assertEqual(token.expiring, False) |         self.assertEqual(token.expiring, False) | ||||||
|  |  | ||||||
|  |     def test_token_expire(self): | ||||||
|  |         """Test Token expire task""" | ||||||
|  |         token: Token = Token.objects.create(expires=now(), user=get_anonymous_user()) | ||||||
|  |         key = token.key | ||||||
|  |         clean_expired_models.delay().get() | ||||||
|  |         token.refresh_from_db() | ||||||
|  |         self.assertNotEqual(key, token.key) | ||||||
|  | |||||||
							
								
								
									
										47
									
								
								authentik/events/migrations/0017_alter_event_action.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								authentik/events/migrations/0017_alter_event_action.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,47 @@ | |||||||
|  | # Generated by Django 3.2.5 on 2021-07-14 19:15 | ||||||
|  |  | ||||||
|  | from django.db import migrations, models | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Migration(migrations.Migration): | ||||||
|  |  | ||||||
|  |     dependencies = [ | ||||||
|  |         ("authentik_events", "0016_add_tenant"), | ||||||
|  |     ] | ||||||
|  |  | ||||||
|  |     operations = [ | ||||||
|  |         migrations.AlterField( | ||||||
|  |             model_name="event", | ||||||
|  |             name="action", | ||||||
|  |             field=models.TextField( | ||||||
|  |                 choices=[ | ||||||
|  |                     ("login", "Login"), | ||||||
|  |                     ("login_failed", "Login Failed"), | ||||||
|  |                     ("logout", "Logout"), | ||||||
|  |                     ("user_write", "User Write"), | ||||||
|  |                     ("suspicious_request", "Suspicious Request"), | ||||||
|  |                     ("password_set", "Password Set"), | ||||||
|  |                     ("secret_view", "Secret View"), | ||||||
|  |                     ("secret_rotate", "Secret Rotate"), | ||||||
|  |                     ("invitation_used", "Invite Used"), | ||||||
|  |                     ("authorize_application", "Authorize Application"), | ||||||
|  |                     ("source_linked", "Source Linked"), | ||||||
|  |                     ("impersonation_started", "Impersonation Started"), | ||||||
|  |                     ("impersonation_ended", "Impersonation Ended"), | ||||||
|  |                     ("policy_execution", "Policy Execution"), | ||||||
|  |                     ("policy_exception", "Policy Exception"), | ||||||
|  |                     ("property_mapping_exception", "Property Mapping Exception"), | ||||||
|  |                     ("system_task_execution", "System Task Execution"), | ||||||
|  |                     ("system_task_exception", "System Task Exception"), | ||||||
|  |                     ("system_exception", "System Exception"), | ||||||
|  |                     ("configuration_error", "Configuration Error"), | ||||||
|  |                     ("model_created", "Model Created"), | ||||||
|  |                     ("model_updated", "Model Updated"), | ||||||
|  |                     ("model_deleted", "Model Deleted"), | ||||||
|  |                     ("email_sent", "Email Sent"), | ||||||
|  |                     ("update_available", "Update Available"), | ||||||
|  |                     ("custom_", "Custom Prefix"), | ||||||
|  |                 ] | ||||||
|  |             ), | ||||||
|  |         ), | ||||||
|  |     ] | ||||||
| @ -62,6 +62,7 @@ class EventAction(models.TextChoices): | |||||||
|     PASSWORD_SET = "password_set"  # noqa # nosec |     PASSWORD_SET = "password_set"  # noqa # nosec | ||||||
|  |  | ||||||
|     SECRET_VIEW = "secret_view"  # noqa # nosec |     SECRET_VIEW = "secret_view"  # noqa # nosec | ||||||
|  |     SECRET_ROTATE = "secret_rotate"  # noqa # nosec | ||||||
|  |  | ||||||
|     INVITE_USED = "invitation_used" |     INVITE_USED = "invitation_used" | ||||||
|  |  | ||||||
|  | |||||||
| @ -0,0 +1,49 @@ | |||||||
|  | # Generated by Django 3.2.5 on 2021-07-14 19:15 | ||||||
|  |  | ||||||
|  | from django.db import migrations, models | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Migration(migrations.Migration): | ||||||
|  |  | ||||||
|  |     dependencies = [ | ||||||
|  |         ("authentik_policies_event_matcher", "0017_alter_eventmatcherpolicy_action"), | ||||||
|  |     ] | ||||||
|  |  | ||||||
|  |     operations = [ | ||||||
|  |         migrations.AlterField( | ||||||
|  |             model_name="eventmatcherpolicy", | ||||||
|  |             name="action", | ||||||
|  |             field=models.TextField( | ||||||
|  |                 blank=True, | ||||||
|  |                 choices=[ | ||||||
|  |                     ("login", "Login"), | ||||||
|  |                     ("login_failed", "Login Failed"), | ||||||
|  |                     ("logout", "Logout"), | ||||||
|  |                     ("user_write", "User Write"), | ||||||
|  |                     ("suspicious_request", "Suspicious Request"), | ||||||
|  |                     ("password_set", "Password Set"), | ||||||
|  |                     ("secret_view", "Secret View"), | ||||||
|  |                     ("secret_rotate", "Secret Rotate"), | ||||||
|  |                     ("invitation_used", "Invite Used"), | ||||||
|  |                     ("authorize_application", "Authorize Application"), | ||||||
|  |                     ("source_linked", "Source Linked"), | ||||||
|  |                     ("impersonation_started", "Impersonation Started"), | ||||||
|  |                     ("impersonation_ended", "Impersonation Ended"), | ||||||
|  |                     ("policy_execution", "Policy Execution"), | ||||||
|  |                     ("policy_exception", "Policy Exception"), | ||||||
|  |                     ("property_mapping_exception", "Property Mapping Exception"), | ||||||
|  |                     ("system_task_execution", "System Task Execution"), | ||||||
|  |                     ("system_task_exception", "System Task Exception"), | ||||||
|  |                     ("system_exception", "System Exception"), | ||||||
|  |                     ("configuration_error", "Configuration Error"), | ||||||
|  |                     ("model_created", "Model Created"), | ||||||
|  |                     ("model_updated", "Model Updated"), | ||||||
|  |                     ("model_deleted", "Model Deleted"), | ||||||
|  |                     ("email_sent", "Email Sent"), | ||||||
|  |                     ("update_available", "Update Available"), | ||||||
|  |                     ("custom_", "Custom Prefix"), | ||||||
|  |                 ], | ||||||
|  |                 help_text="Match created events with this action type. When left empty, all action types will be matched.", | ||||||
|  |             ), | ||||||
|  |         ), | ||||||
|  |     ] | ||||||
| @ -19441,6 +19441,7 @@ components: | |||||||
|       - suspicious_request |       - suspicious_request | ||||||
|       - password_set |       - password_set | ||||||
|       - secret_view |       - secret_view | ||||||
|  |       - secret_rotate | ||||||
|       - invitation_used |       - invitation_used | ||||||
|       - authorize_application |       - authorize_application | ||||||
|       - source_linked |       - source_linked | ||||||
|  | |||||||
		Reference in New Issue
	
	Block a user
	 Jens Langhammer
					Jens Langhammer