From a4853a1e0998d1eaba11288dda4f8eaa17c0393b Mon Sep 17 00:00:00 2001 From: Jens Langhammer Date: Tue, 23 Apr 2024 20:36:59 +0200 Subject: [PATCH] migrate outpost to soft-delete Signed-off-by: Jens Langhammer --- authentik/lib/models.py | 9 ++++++--- .../migrations/0022_outpost_deleted_at.py | 18 ++++++++++++++++++ authentik/outposts/models.py | 6 +++--- authentik/outposts/signals.py | 11 +++++------ authentik/outposts/tasks.py | 16 +++++----------- 5 files changed, 37 insertions(+), 23 deletions(-) create mode 100644 authentik/outposts/migrations/0022_outpost_deleted_at.py diff --git a/authentik/lib/models.py b/authentik/lib/models.py index bd594b65f8..644b7755de 100644 --- a/authentik/lib/models.py +++ b/authentik/lib/models.py @@ -3,11 +3,14 @@ from typing import Any from django.db import models -from django.db.models.signals import post_delete, pre_delete +from django.dispatch import Signal from django.utils import timezone from model_utils.managers import InheritanceManager from rest_framework.serializers import BaseSerializer +pre_soft_delete = Signal() +post_soft_delete = Signal() + class SerializerModel(models.Model): """Base Abstract Model which has a serializer""" @@ -83,7 +86,7 @@ class SoftDeleteModel(models.Model): return self.deleted_at is not None def delete(self, using: Any = ..., keep_parents: bool = ...) -> tuple[int, dict[str, int]]: - pre_delete.send(sender=self.__class__, instance=self) + pre_soft_delete.send(sender=self.__class__, instance=self) now = timezone.now() self.deleted_at = now self.save( @@ -91,7 +94,7 @@ class SoftDeleteModel(models.Model): "deleted_at", ] ) - post_delete.send(sender=self.__class__, instance=self) + post_soft_delete.send(sender=self.__class__, instance=self) return tuple() def force_delete(self, using: Any = ...): diff --git a/authentik/outposts/migrations/0022_outpost_deleted_at.py b/authentik/outposts/migrations/0022_outpost_deleted_at.py new file mode 100644 index 0000000000..b5ebb61aeb --- /dev/null +++ b/authentik/outposts/migrations/0022_outpost_deleted_at.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.4 on 2024-04-23 21:00 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("authentik_outposts", "0021_alter_outpost_type"), + ] + + operations = [ + migrations.AddField( + model_name="outpost", + name="deleted_at", + field=models.DateTimeField(blank=True, null=True), + ), + ] diff --git a/authentik/outposts/models.py b/authentik/outposts/models.py index 29984ebd62..cff9f3066c 100644 --- a/authentik/outposts/models.py +++ b/authentik/outposts/models.py @@ -33,7 +33,7 @@ from authentik.core.models import ( from authentik.crypto.models import CertificateKeyPair from authentik.events.models import Event, EventAction from authentik.lib.config import CONFIG -from authentik.lib.models import InheritanceForeignKey, SerializerModel +from authentik.lib.models import InheritanceForeignKey, SerializerModel, SoftDeleteModel from authentik.lib.sentry import SentryIgnoredException from authentik.lib.utils.errors import exception_to_string from authentik.outposts.controllers.k8s.utils import get_namespace @@ -131,7 +131,7 @@ class OutpostServiceConnection(models.Model): verbose_name = _("Outpost Service-Connection") verbose_name_plural = _("Outpost Service-Connections") - def __str__(self) -> __version__: + def __str__(self): return f"Outpost service connection {self.name}" @property @@ -241,7 +241,7 @@ class KubernetesServiceConnection(SerializerModel, OutpostServiceConnection): return "ak-service-connection-kubernetes-form" -class Outpost(SerializerModel, ManagedModel): +class Outpost(SoftDeleteModel, SerializerModel, ManagedModel): """Outpost instance which manages a service user and token""" uuid = models.UUIDField(default=uuid4, editable=False, primary_key=True) diff --git a/authentik/outposts/signals.py b/authentik/outposts/signals.py index 73d05a4b9a..3474e16156 100644 --- a/authentik/outposts/signals.py +++ b/authentik/outposts/signals.py @@ -2,13 +2,14 @@ from django.core.cache import cache from django.db.models import Model -from django.db.models.signals import m2m_changed, post_save, pre_delete, pre_save +from django.db.models.signals import m2m_changed, post_save, pre_save from django.dispatch import receiver from structlog.stdlib import get_logger from authentik.brands.models import Brand from authentik.core.models import Provider from authentik.crypto.models import CertificateKeyPair +from authentik.lib.models import post_soft_delete from authentik.lib.utils.reflection import class_to_path from authentik.outposts.models import Outpost, OutpostServiceConnection from authentik.outposts.tasks import CACHE_KEY_OUTPOST_DOWN, outpost_controller, outpost_post_save @@ -67,9 +68,7 @@ def post_save_update(sender, instance: Model, created: bool, **_): outpost_post_save.delay(class_to_path(instance.__class__), instance.pk) -@receiver(pre_delete, sender=Outpost) -def pre_delete_cleanup(sender, instance: Outpost, **_): +@receiver(post_soft_delete, sender=Outpost) +def outpost_cleanup(sender, instance: Outpost, **_): """Ensure that Outpost's user is deleted (which will delete the token through cascade)""" - instance.user.delete() - cache.set(CACHE_KEY_OUTPOST_DOWN % instance.pk.hex, instance) - outpost_controller.delay(instance.pk.hex, action="down", from_cache=True) + outpost_controller.delay(instance.pk.hex, action="down") diff --git a/authentik/outposts/tasks.py b/authentik/outposts/tasks.py index cb4dac236a..71f73d1582 100644 --- a/authentik/outposts/tasks.py +++ b/authentik/outposts/tasks.py @@ -129,17 +129,10 @@ def outpost_controller_all(): @CELERY_APP.task(bind=True, base=SystemTask) -def outpost_controller( - self: SystemTask, outpost_pk: str, action: str = "up", from_cache: bool = False -): +def outpost_controller(self: SystemTask, outpost_pk: str, action: str = "up"): """Create/update/monitor/delete the deployment of an Outpost""" logs = [] - if from_cache: - outpost: Outpost = cache.get(CACHE_KEY_OUTPOST_DOWN % outpost_pk) - LOGGER.debug("Getting outpost from cache to delete") - else: - outpost: Outpost = Outpost.objects.filter(pk=outpost_pk).first() - LOGGER.debug("Getting outpost from DB") + outpost: Outpost = Outpost.objects.filter(pk=outpost_pk).first() if not outpost: LOGGER.warning("No outpost") return @@ -155,9 +148,10 @@ def outpost_controller( except (ControllerException, ServiceConnectionInvalid) as exc: self.set_error(exc) else: - if from_cache: - cache.delete(CACHE_KEY_OUTPOST_DOWN % outpost_pk) self.set_status(TaskStatus.SUCCESSFUL, *logs) + finally: + if action == "down": + outpost.force_delete() @CELERY_APP.task(bind=True, base=SystemTask)