outposts
Signed-off-by: Marc 'risson' Schmitt <marc.schmitt@risson.space>
This commit is contained in:
@ -212,14 +212,14 @@ def apply_blueprint(instance_pk: UUID):
|
||||
if not valid:
|
||||
instance.status = BlueprintInstanceStatus.ERROR
|
||||
instance.save()
|
||||
self.error(*logs)
|
||||
self.logs(logs)
|
||||
return
|
||||
with capture_logs() as logs:
|
||||
applied = importer.apply()
|
||||
if not applied:
|
||||
instance.status = BlueprintInstanceStatus.ERROR
|
||||
instance.save()
|
||||
self.error(*logs)
|
||||
self.logs(logs)
|
||||
return
|
||||
instance.status = BlueprintInstanceStatus.SUCCESSFUL
|
||||
instance.last_applied_hash = file_hash
|
||||
|
||||
@ -101,7 +101,13 @@ class KubernetesController(BaseController):
|
||||
all_logs = []
|
||||
for reconcile_key in self.reconcile_order:
|
||||
if reconcile_key in self.outpost.config.kubernetes_disabled_components:
|
||||
all_logs += [f"{reconcile_key.title()}: Disabled"]
|
||||
all_logs.append(
|
||||
LogEvent(
|
||||
log_level="info",
|
||||
event=f"{reconcile_key.title()}: Disabled",
|
||||
logger=str(type(self)),
|
||||
)
|
||||
)
|
||||
continue
|
||||
with capture_logs() as logs:
|
||||
reconciler_cls = self.reconcilers.get(reconcile_key)
|
||||
@ -134,7 +140,13 @@ class KubernetesController(BaseController):
|
||||
all_logs = []
|
||||
for reconcile_key in self.reconcile_order:
|
||||
if reconcile_key in self.outpost.config.kubernetes_disabled_components:
|
||||
all_logs += [f"{reconcile_key.title()}: Disabled"]
|
||||
all_logs.append(
|
||||
LogEvent(
|
||||
log_level="info",
|
||||
event=f"{reconcile_key.title()}: Disabled",
|
||||
logger=str(type(self)),
|
||||
)
|
||||
)
|
||||
continue
|
||||
with capture_logs() as logs:
|
||||
reconciler_cls = self.reconcilers.get(reconcile_key)
|
||||
|
||||
@ -1,37 +1,28 @@
|
||||
"""authentik outpost signals"""
|
||||
|
||||
from django.contrib.auth.signals import user_logged_out
|
||||
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.dispatch import receiver
|
||||
from django.http import HttpRequest
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
from authentik.brands.models import Brand
|
||||
from authentik.core.models import AuthenticatedSession, Provider, User
|
||||
from authentik.core.models import AuthenticatedSession, Provider
|
||||
from authentik.crypto.models import CertificateKeyPair
|
||||
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,
|
||||
outpost_session_end,
|
||||
outposts_and_related_update_dispatch,
|
||||
)
|
||||
|
||||
LOGGER = get_logger()
|
||||
UPDATE_TRIGGERING_MODELS = (
|
||||
Outpost,
|
||||
OutpostServiceConnection,
|
||||
Provider,
|
||||
CertificateKeyPair,
|
||||
Brand,
|
||||
)
|
||||
|
||||
|
||||
@receiver(pre_save, sender=Outpost)
|
||||
def pre_save_outpost(sender, instance: Outpost, **_):
|
||||
def outpost_pre_save(sender, instance: Outpost, **_):
|
||||
"""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"""
|
||||
old_instances = Outpost.objects.filter(pk=instance.pk)
|
||||
@ -54,55 +45,39 @@ def pre_save_outpost(sender, instance: Outpost, **_):
|
||||
|
||||
|
||||
@receiver(m2m_changed, sender=Outpost.providers.through)
|
||||
def m2m_changed_update(sender, instance: Model, action: str, **_):
|
||||
def outpost_m2m_changed(sender, instance: Model, action: str, **_):
|
||||
"""Update outpost on m2m change, when providers are added or removed"""
|
||||
if action in ["post_add", "post_remove", "post_clear"]:
|
||||
outpost_post_save.send_with_options(
|
||||
args=(class_to_path(instance.__class__), instance.pk),
|
||||
# TODO: how do we get the outpost here, if it makes sense
|
||||
rel_obj=None,
|
||||
)
|
||||
outposts_and_related_update_dispatch.send(class_to_path(instance.__class__), instance.pk)
|
||||
|
||||
|
||||
@receiver(post_save)
|
||||
def post_save_update(sender, instance: Model, created: bool, **_):
|
||||
def outposts_and_related_post_save(sender, instance: Model, created: bool, **_):
|
||||
"""If an Outpost is saved, Ensure that token is created/updated
|
||||
|
||||
If an OutpostModel, or a model that is somehow connected to an OutpostModel is saved,
|
||||
we send a message down the relevant OutpostModels WS connection to trigger an update"""
|
||||
if instance.__module__ == "django.db.migrations.recorder":
|
||||
return
|
||||
if instance.__module__ == "__fake__":
|
||||
return
|
||||
if not isinstance(instance, UPDATE_TRIGGERING_MODELS):
|
||||
return
|
||||
if isinstance(instance, Outpost) and created:
|
||||
LOGGER.info("New outpost saved, ensuring initial token and user are created")
|
||||
_ = instance.token
|
||||
outpost_post_save.send_with_options(
|
||||
args=(class_to_path(instance.__class__), instance.pk),
|
||||
# TODO: how do we get the outpost here, if it makes sense
|
||||
rel_obj=None,
|
||||
)
|
||||
outposts_and_related_update_dispatch.send(class_to_path(instance.__class__), instance.pk)
|
||||
|
||||
|
||||
post_save.connect(outposts_and_related_post_save, sender=Outpost, weak=False)
|
||||
post_save.connect(outposts_and_related_post_save, sender=OutpostServiceConnection, weak=False)
|
||||
post_save.connect(outposts_and_related_post_save, sender=Provider, weak=False)
|
||||
post_save.connect(outposts_and_related_post_save, sender=CertificateKeyPair, weak=False)
|
||||
post_save.connect(outposts_and_related_post_save, sender=Brand, weak=False)
|
||||
|
||||
|
||||
@receiver(pre_delete, sender=Outpost)
|
||||
def pre_delete_cleanup(sender, instance: Outpost, **_):
|
||||
def outpost_pre_delete_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.send(instance.pk.hex, action="down", from_cache=True)
|
||||
|
||||
|
||||
@receiver(user_logged_out)
|
||||
def logout_revoke_direct(sender: type[User], request: HttpRequest, **_):
|
||||
"""Catch logout by direct logout and forward to providers"""
|
||||
if not request.session or not request.session.session_key:
|
||||
return
|
||||
outpost_session_end.send(request.session.session_key)
|
||||
|
||||
|
||||
@receiver(pre_delete, sender=AuthenticatedSession)
|
||||
def logout_revoke(sender: type[AuthenticatedSession], instance: AuthenticatedSession, **_):
|
||||
def outpost_logout_revoke(sender: type[AuthenticatedSession], instance: AuthenticatedSession, **_):
|
||||
"""Catch logout by expiring sessions being deleted"""
|
||||
outpost_session_end.send(instance.session.session_key)
|
||||
|
||||
@ -138,8 +138,7 @@ def outpost_controller(outpost_pk: str, action: str = "up", from_cache: bool = F
|
||||
else:
|
||||
if from_cache:
|
||||
cache.delete(CACHE_KEY_OUTPOST_DOWN % outpost_pk)
|
||||
for log in logs:
|
||||
self.info(log)
|
||||
self.logs(logs)
|
||||
|
||||
|
||||
@actor(description=_("Ensure that all Outposts have valid Service Accounts and Tokens."))
|
||||
@ -155,17 +154,18 @@ def outpost_token_ensurer():
|
||||
self.info(f"Successfully checked {len(all_outposts)} Outposts.")
|
||||
|
||||
|
||||
@actor(description=_("If an Outpost is saved, ensure that token is created/updated."))
|
||||
def outpost_post_save(model_class: str, model_pk: Any):
|
||||
@actor(description=_("Dispatch tasks to update outposts when related objects are updated."))
|
||||
def outposts_and_related_update_dispatch(model_class: str, pk: Any):
|
||||
"""If an Outpost is saved, Ensure that token is created/updated
|
||||
|
||||
If an OutpostModel, or a model that is somehow connected to an OutpostModel is saved,
|
||||
we send a message down the relevant OutpostModels WS connection to trigger an update"""
|
||||
|
||||
model: Model = path_to_class(model_class)
|
||||
try:
|
||||
instance = model.objects.get(pk=model_pk)
|
||||
instance = model.objects.get(pk=pk)
|
||||
except model.DoesNotExist:
|
||||
LOGGER.warning("Model does not exist", model=model, pk=model_pk)
|
||||
LOGGER.warning("Model does not exist", model=model, pk=pk)
|
||||
return
|
||||
|
||||
if isinstance(instance, Outpost):
|
||||
@ -175,7 +175,7 @@ def outpost_post_save(model_class: str, model_pk: Any):
|
||||
|
||||
if isinstance(instance, OutpostModel | Outpost):
|
||||
LOGGER.debug("triggering outpost update from outpostmodel/outpost", instance=instance)
|
||||
outpost_send_update(instance)
|
||||
outposts_and_related_send_update(instance)
|
||||
|
||||
if isinstance(instance, OutpostServiceConnection):
|
||||
LOGGER.debug("triggering ServiceConnection state update", instance=instance)
|
||||
@ -200,27 +200,29 @@ def outpost_post_save(model_class: str, model_pk: Any):
|
||||
# Because the Outpost Model has an M2M to Provider,
|
||||
# we have to iterate over the entire QS
|
||||
for reverse in getattr(instance, field_name).all():
|
||||
outpost_send_update(reverse)
|
||||
outposts_and_related_send_update(reverse)
|
||||
|
||||
|
||||
def outpost_send_update(model_instance: Model):
|
||||
"""Send outpost update to all registered outposts, regardless to which authentik
|
||||
instance they are connected"""
|
||||
channel_layer = get_channel_layer()
|
||||
def outposts_and_related_send_update(model_instance: Model):
|
||||
"""Send outpost update to all related outposts"""
|
||||
if isinstance(model_instance, OutpostModel):
|
||||
for outpost in model_instance.outpost_set.all():
|
||||
_outpost_single_update(outpost, channel_layer)
|
||||
outpost_send_update.send_with_options(args=(outpost.pk,), rel_obj=outpost)
|
||||
elif isinstance(model_instance, Outpost):
|
||||
_outpost_single_update(model_instance, channel_layer)
|
||||
outpost = model_instance
|
||||
outpost_send_update.send_with_options(args=(outpost.pk,), rel_obj=outpost)
|
||||
|
||||
|
||||
def _outpost_single_update(outpost: Outpost, layer=None):
|
||||
"""Update outpost instances connected to a single outpost"""
|
||||
@actor(description=_("Send update to outpost"))
|
||||
def outpost_send_update(pk: Any):
|
||||
"""Update outpost instance"""
|
||||
outpost = Outpost.objects.filter(pk=pk).first()
|
||||
if not outpost:
|
||||
return
|
||||
# Ensure token again, because this function is called when anything related to an
|
||||
# OutpostModel is saved, so we can be sure permissions are right
|
||||
_ = outpost.token
|
||||
outpost.build_user_permissions(outpost.user)
|
||||
if not layer: # pragma: no cover
|
||||
layer = get_channel_layer()
|
||||
group = OUTPOST_GROUP % {"outpost_pk": str(outpost.pk)}
|
||||
LOGGER.debug("sending update", channel=group, outpost=outpost)
|
||||
|
||||
@ -107,6 +107,10 @@ class Task(SerializerModel, TaskBase):
|
||||
)
|
||||
return sanitize_item(log)
|
||||
|
||||
def logs(self, logs: list[LogEvent]):
|
||||
for log in logs:
|
||||
self._messages.append(sanitize_item(log))
|
||||
|
||||
def log(
|
||||
self,
|
||||
logger: str,
|
||||
@ -117,7 +121,6 @@ class Task(SerializerModel, TaskBase):
|
||||
):
|
||||
self._messages: list
|
||||
self._messages.append(
|
||||
sanitize_item(
|
||||
self._make_message(
|
||||
logger,
|
||||
log_level,
|
||||
@ -125,7 +128,6 @@ class Task(SerializerModel, TaskBase):
|
||||
**attributes,
|
||||
)
|
||||
)
|
||||
)
|
||||
if save:
|
||||
self.save()
|
||||
|
||||
|
||||
@ -4,6 +4,8 @@ import "@goauthentik/admin/outposts/OutpostForm";
|
||||
import "@goauthentik/admin/outposts/OutpostHealth";
|
||||
import "@goauthentik/admin/outposts/OutpostHealthSimple";
|
||||
import "@goauthentik/admin/rbac/ObjectPermissionModal";
|
||||
import "@goauthentik/admin/system-tasks/ScheduleList";
|
||||
import "@goauthentik/admin/system-tasks/TaskList";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import { PFSize } from "@goauthentik/common/enums.js";
|
||||
import { PFColor } from "@goauthentik/elements/Label";
|
||||
@ -24,6 +26,7 @@ import { ifDefined } from "lit/directives/if-defined.js";
|
||||
import PFDescriptionList from "@patternfly/patternfly/components/DescriptionList/description-list.css";
|
||||
|
||||
import {
|
||||
ModelEnum,
|
||||
Outpost,
|
||||
OutpostHealth,
|
||||
OutpostTypeEnum,
|
||||
@ -163,6 +166,7 @@ export class OutpostListPage extends TablePage<Outpost> {
|
||||
}
|
||||
|
||||
renderExpanded(item: Outpost): TemplateResult {
|
||||
const [appLabel, modelName] = ModelEnum.AuthentikOutpostsOutpost.split(".");
|
||||
return html`<td role="cell" colspan="5">
|
||||
<div class="pf-c-table__expandable-row-content">
|
||||
<h3>
|
||||
@ -181,6 +185,38 @@ export class OutpostListPage extends TablePage<Outpost> {
|
||||
</div>`;
|
||||
})}
|
||||
</dl>
|
||||
<dl class="pf-c-description-list pf-m-horizontal">
|
||||
<div class="pf-c-description-list__group">
|
||||
<dt class="pf-c-description-list__term">
|
||||
<span class="pf-c-description-list__text">${msg("Schedules")}</span>
|
||||
</dt>
|
||||
<dd class="pf-c-description-list__description">
|
||||
<div class="pf-c-description-list__text">
|
||||
<ak-schedule-list
|
||||
.relObjAppLabel=${appLabel}
|
||||
.relObjModel=${modelName}
|
||||
.relObjId="${item.pk}"
|
||||
></ak-schedule-list>
|
||||
</div>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<dl class="pf-c-description-list pf-m-horizontal">
|
||||
<div class="pf-c-description-list__group">
|
||||
<dt class="pf-c-description-list__term">
|
||||
<span class="pf-c-description-list__text">${msg("Tasks")}</span>
|
||||
</dt>
|
||||
<dd class="pf-c-description-list__description">
|
||||
<div class="pf-c-description-list__text">
|
||||
<ak-task-list
|
||||
.relObjAppLabel=${appLabel}
|
||||
.relObjModel=${modelName}
|
||||
.relObjId="${item.pk}"
|
||||
></ak-task-list>
|
||||
</div>
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
</td>`;
|
||||
}
|
||||
|
||||
@ -8,7 +8,6 @@ import "@goauthentik/admin/system-tasks/TaskList";
|
||||
import { DEFAULT_CONFIG } from "@goauthentik/common/api/config";
|
||||
import "@goauthentik/components/ak-status-label";
|
||||
import { PFColor } from "@goauthentik/elements/Label";
|
||||
import "@goauthentik/elements/Tabs";
|
||||
import "@goauthentik/elements/buttons/SpinnerButton";
|
||||
import "@goauthentik/elements/forms/DeleteBulkForm";
|
||||
import "@goauthentik/elements/forms/ModalForm";
|
||||
@ -117,44 +116,38 @@ export class OutpostServiceConnectionListPage extends TablePage<ServiceConnectio
|
||||
const [appLabel, modelName] = item.metaModelName.split(".");
|
||||
return html` <td role="cell" colspan="5">
|
||||
<div class="pf-c-table__expandable-row-content">
|
||||
<div class="pf-c-content">
|
||||
<ak-tabs>
|
||||
<section
|
||||
slot="page-schedules"
|
||||
data-tab-title="${msg("Schedules")}"
|
||||
class="pf-c-page__main-section pf-m-no-padding-mobile"
|
||||
>
|
||||
<div class="pf-l-grid pf-m-gutter">
|
||||
<div
|
||||
class="pf-l-grid__item pf-m-12-col pf-m-12-col-on-xl pf-m-12-col-on-2xl"
|
||||
>
|
||||
<dl class="pf-c-description-list pf-m-horizontal">
|
||||
<div class="pf-c-description-list__group">
|
||||
<dt class="pf-c-description-list__term">
|
||||
<span class="pf-c-description-list__text">${msg("Schedules")}</span>
|
||||
</dt>
|
||||
<dd class="pf-c-description-list__description">
|
||||
<div class="pf-c-description-list__text">
|
||||
<ak-schedule-list
|
||||
.relObjAppLabel=${appLabel}
|
||||
.relObjModel=${modelName}
|
||||
.relObjId="${item.pk}"
|
||||
></ak-schedule-list>
|
||||
</div>
|
||||
</dd>
|
||||
</div>
|
||||
</section>
|
||||
<section
|
||||
slot="page-tasks"
|
||||
data-tab-title="${msg("Tasks")}"
|
||||
class="pf-c-page__main-section pf-m-no-padding-mobile"
|
||||
>
|
||||
<div class="pf-l-grid pf-m-gutter">
|
||||
<div
|
||||
class="pf-l-grid__item pf-m-12-col pf-m-12-col-on-xl pf-m-12-col-on-2xl"
|
||||
>
|
||||
</dl>
|
||||
<dl class="pf-c-description-list pf-m-horizontal">
|
||||
<div class="pf-c-description-list__group">
|
||||
<dt class="pf-c-description-list__term">
|
||||
<span class="pf-c-description-list__text">${msg("Tasks")}</span>
|
||||
</dt>
|
||||
<dd class="pf-c-description-list__description">
|
||||
<div class="pf-c-description-list__text">
|
||||
<ak-task-list
|
||||
.relObjAppLabel=${appLabel}
|
||||
.relObjModel=${modelName}
|
||||
.relObjId="${item.pk}"
|
||||
></ak-task-list>
|
||||
</div>
|
||||
</dd>
|
||||
</div>
|
||||
</section>
|
||||
</ak-tabs>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
</td>`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user