authentik.outposts.signals

authentik outpost signals

  1"""authentik outpost signals"""
  2
  3from django.core.cache import cache
  4from django.db.models.signals import m2m_changed, post_save, pre_delete, pre_save
  5from django.dispatch import receiver
  6from structlog.stdlib import get_logger
  7
  8from authentik.brands.models import Brand
  9from authentik.core.models import AuthenticatedSession, Provider
 10from authentik.crypto.models import CertificateKeyPair
 11from authentik.outposts.models import Outpost, OutpostModel, OutpostServiceConnection
 12from authentik.outposts.tasks import (
 13    CACHE_KEY_OUTPOST_DOWN,
 14    outpost_controller,
 15    outpost_send_update,
 16    outpost_session_end,
 17)
 18
 19LOGGER = get_logger()
 20
 21
 22@receiver(pre_save, sender=Outpost)
 23def outpost_pre_save(sender, instance: Outpost, **_):
 24    """Pre-save checks for an outpost, if the name or config.kubernetes_namespace changes,
 25    we call down and then wait for the up after save"""
 26    old_instances = Outpost.objects.filter(pk=instance.pk)
 27    if not old_instances.exists():
 28        return
 29    old_instance = old_instances.first()
 30    dirty = False
 31    # Name changes the deployment name, need to recreate
 32    dirty += old_instance.name != instance.name
 33    # namespace requires re-create
 34    dirty += old_instance.config.kubernetes_namespace != instance.config.kubernetes_namespace
 35    if bool(dirty):
 36        LOGGER.info("Outpost needs re-deployment due to changes", instance=instance)
 37        cache.set(CACHE_KEY_OUTPOST_DOWN % instance.pk.hex, old_instance)
 38        outpost_controller.send_with_options(
 39            args=(instance.pk.hex,),
 40            kwargs={"action": "down", "from_cache": True},
 41            rel_obj=instance,
 42            uid=instance.name,
 43        )
 44
 45
 46@receiver(m2m_changed, sender=Outpost.providers.through)
 47def outpost_m2m_changed(sender, instance: Outpost | Provider, action: str, **_):
 48    """Update outpost on m2m change, when providers are added or removed"""
 49    if action not in ["post_add", "post_remove", "post_clear"]:
 50        return
 51    if isinstance(instance, Outpost):
 52        # Rebuild permissions when providers change
 53        LOGGER.debug("Rebuilding outpost service account permissions", outpost=instance)
 54        instance.build_user_permissions(instance.user)
 55        outpost_controller.send_with_options(
 56            args=(instance.pk,),
 57            rel_obj=instance.service_connection,
 58            uid=instance.name,
 59        )
 60        outpost_send_update.send_with_options(
 61            args=(instance.pk,),
 62            rel_obj=instance,
 63            uid=instance.name,
 64        )
 65    elif isinstance(instance, OutpostModel):
 66        for outpost in instance.outpost_set.all():
 67            outpost_controller.send_with_options(
 68                args=(instance.pk,),
 69                rel_obj=instance.service_connection,
 70                uid=instance.name,
 71            )
 72            outpost_send_update.send_with_options(
 73                args=(outpost.pk,),
 74                rel_obj=outpost,
 75                uid=outpost.name,
 76            )
 77
 78
 79@receiver(post_save, sender=Outpost)
 80def outpost_post_save(sender, instance: Outpost, created: bool, **_):
 81    if created:
 82        LOGGER.info("New outpost saved, ensuring initial token and user are created")
 83        _ = instance.token
 84    outpost_controller.send_with_options(
 85        args=(instance.pk,),
 86        rel_obj=instance.service_connection,
 87        uid=instance.name,
 88    )
 89    outpost_send_update.send_with_options(
 90        args=(instance.pk,),
 91        rel_obj=instance,
 92        uid=instance.name,
 93    )
 94
 95
 96def outpost_related_post_save(sender, instance: OutpostServiceConnection | OutpostModel, **_):
 97    for outpost in instance.outpost_set.all():
 98        # Rebuild permissions in case provider's required objects changed
 99        if isinstance(instance, OutpostModel):
100            LOGGER.info(
101                "Provider changed, rebuilding permissions and sending update",
102                outpost=outpost.name,
103                provider=instance.name if hasattr(instance, "name") else str(instance),
104            )
105            outpost.build_user_permissions(outpost.user)
106        LOGGER.debug("Sending update to outpost", outpost=outpost.name, trigger="provider_change")
107        outpost_send_update.send_with_options(
108            args=(outpost.pk,),
109            rel_obj=outpost,
110            uid=outpost.name,
111        )
112
113
114post_save.connect(outpost_related_post_save, sender=OutpostServiceConnection, weak=False)
115for subclass in OutpostModel.__subclasses__():
116    post_save.connect(outpost_related_post_save, sender=subclass, weak=False)
117
118
119def outpost_reverse_related_post_save(sender, instance: CertificateKeyPair | Brand, **_):
120    for field in instance._meta.get_fields():
121        # Each field is checked if it has a `related_model` attribute (when ForeginKeys or M2Ms)
122        # are used, and if it has a value
123        if not hasattr(field, "related_model"):
124            continue
125        if not field.related_model:
126            continue
127        if not issubclass(field.related_model, OutpostModel):
128            continue
129
130        field_name = f"{field.name}_set"
131        if not hasattr(instance, field_name):
132            continue
133
134        LOGGER.debug("triggering outpost update from field", field=field.name)
135        # Because the Outpost Model has an M2M to Provider,
136        # we have to iterate over the entire QS
137        for reverse in getattr(instance, field_name).all():
138            if isinstance(reverse, OutpostModel):
139                for outpost in reverse.outpost_set.all():
140                    outpost_send_update.send_with_options(
141                        args=(outpost.pk,),
142                        rel_obj=outpost,
143                        uid=outpost.name,
144                    )
145
146
147post_save.connect(outpost_reverse_related_post_save, sender=Brand, weak=False)
148post_save.connect(outpost_reverse_related_post_save, sender=CertificateKeyPair, weak=False)
149
150
151@receiver(pre_delete, sender=Outpost)
152def outpost_pre_delete_cleanup(sender, instance: Outpost, **_):
153    """Ensure that Outpost's user is deleted (which will delete the token through cascade)"""
154    instance.user.delete()
155    cache.set(CACHE_KEY_OUTPOST_DOWN % instance.pk.hex, instance)
156    outpost_controller.send_with_options(
157        args=(instance.pk.hex,),
158        kwargs={"action": "down", "from_cache": True},
159        uid=instance.name,
160    )
161
162
163@receiver(pre_delete, sender=AuthenticatedSession)
164def outpost_logout_revoke(sender: type[AuthenticatedSession], instance: AuthenticatedSession, **_):
165    """Catch logout by expiring sessions being deleted"""
166    if Outpost.objects.exists():
167        outpost_session_end.send(instance.session.session_key)
LOGGER = <BoundLoggerLazyProxy(logger=None, wrapper_class=None, processors=None, context_class=None, initial_values={}, logger_factory_args=())>
@receiver(pre_save, sender=Outpost)
def outpost_pre_save(sender, instance: authentik.outposts.models.Outpost, **_):
23@receiver(pre_save, sender=Outpost)
24def outpost_pre_save(sender, instance: Outpost, **_):
25    """Pre-save checks for an outpost, if the name or config.kubernetes_namespace changes,
26    we call down and then wait for the up after save"""
27    old_instances = Outpost.objects.filter(pk=instance.pk)
28    if not old_instances.exists():
29        return
30    old_instance = old_instances.first()
31    dirty = False
32    # Name changes the deployment name, need to recreate
33    dirty += old_instance.name != instance.name
34    # namespace requires re-create
35    dirty += old_instance.config.kubernetes_namespace != instance.config.kubernetes_namespace
36    if bool(dirty):
37        LOGGER.info("Outpost needs re-deployment due to changes", instance=instance)
38        cache.set(CACHE_KEY_OUTPOST_DOWN % instance.pk.hex, old_instance)
39        outpost_controller.send_with_options(
40            args=(instance.pk.hex,),
41            kwargs={"action": "down", "from_cache": True},
42            rel_obj=instance,
43            uid=instance.name,
44        )

Pre-save checks for an outpost, if the name or config.kubernetes_namespace changes, we call down and then wait for the up after save

@receiver(m2m_changed, sender=Outpost.providers.through)
def outpost_m2m_changed( sender, instance: authentik.outposts.models.Outpost | authentik.core.models.Provider, action: str, **_):
47@receiver(m2m_changed, sender=Outpost.providers.through)
48def outpost_m2m_changed(sender, instance: Outpost | Provider, action: str, **_):
49    """Update outpost on m2m change, when providers are added or removed"""
50    if action not in ["post_add", "post_remove", "post_clear"]:
51        return
52    if isinstance(instance, Outpost):
53        # Rebuild permissions when providers change
54        LOGGER.debug("Rebuilding outpost service account permissions", outpost=instance)
55        instance.build_user_permissions(instance.user)
56        outpost_controller.send_with_options(
57            args=(instance.pk,),
58            rel_obj=instance.service_connection,
59            uid=instance.name,
60        )
61        outpost_send_update.send_with_options(
62            args=(instance.pk,),
63            rel_obj=instance,
64            uid=instance.name,
65        )
66    elif isinstance(instance, OutpostModel):
67        for outpost in instance.outpost_set.all():
68            outpost_controller.send_with_options(
69                args=(instance.pk,),
70                rel_obj=instance.service_connection,
71                uid=instance.name,
72            )
73            outpost_send_update.send_with_options(
74                args=(outpost.pk,),
75                rel_obj=outpost,
76                uid=outpost.name,
77            )

Update outpost on m2m change, when providers are added or removed

@receiver(post_save, sender=Outpost)
def outpost_post_save( sender, instance: authentik.outposts.models.Outpost, created: bool, **_):
80@receiver(post_save, sender=Outpost)
81def outpost_post_save(sender, instance: Outpost, created: bool, **_):
82    if created:
83        LOGGER.info("New outpost saved, ensuring initial token and user are created")
84        _ = instance.token
85    outpost_controller.send_with_options(
86        args=(instance.pk,),
87        rel_obj=instance.service_connection,
88        uid=instance.name,
89    )
90    outpost_send_update.send_with_options(
91        args=(instance.pk,),
92        rel_obj=instance,
93        uid=instance.name,
94    )
def outpost_related_post_save( sender, instance: authentik.outposts.models.OutpostServiceConnection | authentik.outposts.models.OutpostModel, **_):
 97def outpost_related_post_save(sender, instance: OutpostServiceConnection | OutpostModel, **_):
 98    for outpost in instance.outpost_set.all():
 99        # Rebuild permissions in case provider's required objects changed
100        if isinstance(instance, OutpostModel):
101            LOGGER.info(
102                "Provider changed, rebuilding permissions and sending update",
103                outpost=outpost.name,
104                provider=instance.name if hasattr(instance, "name") else str(instance),
105            )
106            outpost.build_user_permissions(outpost.user)
107        LOGGER.debug("Sending update to outpost", outpost=outpost.name, trigger="provider_change")
108        outpost_send_update.send_with_options(
109            args=(outpost.pk,),
110            rel_obj=outpost,
111            uid=outpost.name,
112        )
def outpost_reverse_related_post_save( sender, instance: authentik.crypto.models.CertificateKeyPair | authentik.brands.models.Brand, **_):
120def outpost_reverse_related_post_save(sender, instance: CertificateKeyPair | Brand, **_):
121    for field in instance._meta.get_fields():
122        # Each field is checked if it has a `related_model` attribute (when ForeginKeys or M2Ms)
123        # are used, and if it has a value
124        if not hasattr(field, "related_model"):
125            continue
126        if not field.related_model:
127            continue
128        if not issubclass(field.related_model, OutpostModel):
129            continue
130
131        field_name = f"{field.name}_set"
132        if not hasattr(instance, field_name):
133            continue
134
135        LOGGER.debug("triggering outpost update from field", field=field.name)
136        # Because the Outpost Model has an M2M to Provider,
137        # we have to iterate over the entire QS
138        for reverse in getattr(instance, field_name).all():
139            if isinstance(reverse, OutpostModel):
140                for outpost in reverse.outpost_set.all():
141                    outpost_send_update.send_with_options(
142                        args=(outpost.pk,),
143                        rel_obj=outpost,
144                        uid=outpost.name,
145                    )
@receiver(pre_delete, sender=Outpost)
def outpost_pre_delete_cleanup(sender, instance: authentik.outposts.models.Outpost, **_):
152@receiver(pre_delete, sender=Outpost)
153def outpost_pre_delete_cleanup(sender, instance: Outpost, **_):
154    """Ensure that Outpost's user is deleted (which will delete the token through cascade)"""
155    instance.user.delete()
156    cache.set(CACHE_KEY_OUTPOST_DOWN % instance.pk.hex, instance)
157    outpost_controller.send_with_options(
158        args=(instance.pk.hex,),
159        kwargs={"action": "down", "from_cache": True},
160        uid=instance.name,
161    )

Ensure that Outpost's user is deleted (which will delete the token through cascade)

@receiver(pre_delete, sender=AuthenticatedSession)
def outpost_logout_revoke( sender: type[authentik.core.models.AuthenticatedSession], instance: authentik.core.models.AuthenticatedSession, **_):
164@receiver(pre_delete, sender=AuthenticatedSession)
165def outpost_logout_revoke(sender: type[AuthenticatedSession], instance: AuthenticatedSession, **_):
166    """Catch logout by expiring sessions being deleted"""
167    if Outpost.objects.exists():
168        outpost_session_end.send(instance.session.session_key)

Catch logout by expiring sessions being deleted