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 )
@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