core: don't delete expired tokens, rotate their key
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
parent
6f98833150
commit
aa701c5725
|
@ -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)
|
||||||
|
|
|
@ -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