From 8b12c6a01a3fce33b6d5092e927b49f347b8bd6b Mon Sep 17 00:00:00 2001 From: "gcp-cherry-pick-bot[bot]" <98988430+gcp-cherry-pick-bot[bot]@users.noreply.github.com> Date: Sat, 30 Dec 2023 15:37:36 +0100 Subject: [PATCH] outposts: fix Outpost reconcile not re-assigning managed attribute (cherry-pick #8014) (#8020) outposts: fix Outpost reconcile not re-assigning managed attribute (#8014) * outposts: fix Outpost reconcile not re-assigning managed attribute * rework reconcile to find both name and managed outpost --------- Signed-off-by: Jens Langhammer Co-authored-by: Jens L --- authentik/blueprints/apps.py | 2 +- authentik/outposts/api/outposts.py | 12 +++++++++- authentik/outposts/apps.py | 7 +++++- authentik/outposts/tests/test_api.py | 35 ++++++++++++++++++++++++++-- 4 files changed, 51 insertions(+), 5 deletions(-) diff --git a/authentik/blueprints/apps.py b/authentik/blueprints/apps.py index 90df91c00..aba14d552 100644 --- a/authentik/blueprints/apps.py +++ b/authentik/blueprints/apps.py @@ -40,7 +40,7 @@ class ManagedAppConfig(AppConfig): meth() self._logger.debug("Successfully reconciled", name=name) except (DatabaseError, ProgrammingError, InternalError) as exc: - self._logger.debug("Failed to run reconcile", name=name, exc=exc) + self._logger.warning("Failed to run reconcile", name=name, exc=exc) class AuthentikBlueprintsConfig(ManagedAppConfig): diff --git a/authentik/outposts/api/outposts.py b/authentik/outposts/api/outposts.py index 140f01fac..09862a6e7 100644 --- a/authentik/outposts/api/outposts.py +++ b/authentik/outposts/api/outposts.py @@ -18,7 +18,7 @@ from authentik.core.api.used_by import UsedByMixin from authentik.core.api.utils import PassiveSerializer, is_dict from authentik.core.models import Provider from authentik.outposts.api.service_connections import ServiceConnectionSerializer -from authentik.outposts.apps import MANAGED_OUTPOST +from authentik.outposts.apps import MANAGED_OUTPOST, MANAGED_OUTPOST_NAME from authentik.outposts.models import ( Outpost, OutpostConfig, @@ -47,6 +47,16 @@ class OutpostSerializer(ModelSerializer): source="service_connection", read_only=True ) + def validate_name(self, name: str) -> str: + """Validate name (especially for embedded outpost)""" + if not self.instance: + return name + if self.instance.managed == MANAGED_OUTPOST: + raise ValidationError("Embedded outpost's name cannot be changed") + if self.instance.name == MANAGED_OUTPOST_NAME: + self.instance.managed = MANAGED_OUTPOST + return name + def validate_providers(self, providers: list[Provider]) -> list[Provider]: """Check that all providers match the type of the outpost""" type_map = { diff --git a/authentik/outposts/apps.py b/authentik/outposts/apps.py index 6898a170a..08d1080ee 100644 --- a/authentik/outposts/apps.py +++ b/authentik/outposts/apps.py @@ -15,6 +15,7 @@ GAUGE_OUTPOSTS_LAST_UPDATE = Gauge( ["outpost", "uid", "version"], ) MANAGED_OUTPOST = "goauthentik.io/outposts/embedded" +MANAGED_OUTPOST_NAME = "authentik Embedded Outpost" class AuthentikOutpostConfig(ManagedAppConfig): @@ -39,10 +40,14 @@ class AuthentikOutpostConfig(ManagedAppConfig): OutpostType, ) + if outpost := Outpost.objects.filter(name=MANAGED_OUTPOST_NAME, managed="").first(): + outpost.managed = MANAGED_OUTPOST + outpost.save() + return outpost, updated = Outpost.objects.update_or_create( defaults={ - "name": "authentik Embedded Outpost", "type": OutpostType.PROXY, + "name": MANAGED_OUTPOST_NAME, }, managed=MANAGED_OUTPOST, ) diff --git a/authentik/outposts/tests/test_api.py b/authentik/outposts/tests/test_api.py index 5e6fb385e..3edaeb78e 100644 --- a/authentik/outposts/tests/test_api.py +++ b/authentik/outposts/tests/test_api.py @@ -2,11 +2,13 @@ from django.urls import reverse from rest_framework.test import APITestCase +from authentik.blueprints.tests import reconcile_app from authentik.core.models import PropertyMapping from authentik.core.tests.utils import create_test_admin_user, create_test_flow from authentik.lib.generators import generate_id from authentik.outposts.api.outposts import OutpostSerializer -from authentik.outposts.models import OutpostType, default_outpost_config +from authentik.outposts.apps import MANAGED_OUTPOST +from authentik.outposts.models import Outpost, OutpostType, default_outpost_config from authentik.providers.ldap.models import LDAPProvider from authentik.providers.proxy.models import ProxyProvider @@ -22,7 +24,36 @@ class TestOutpostServiceConnectionsAPI(APITestCase): self.user = create_test_admin_user() self.client.force_login(self.user) - def test_outpost_validaton(self): + @reconcile_app("authentik_outposts") + def test_managed_name_change(self): + """Test name change for embedded outpost""" + embedded_outpost = Outpost.objects.filter(managed=MANAGED_OUTPOST).first() + self.assertIsNotNone(embedded_outpost) + response = self.client.patch( + reverse("authentik_api:outpost-detail", kwargs={"pk": embedded_outpost.pk}), + {"name": "foo"}, + ) + self.assertEqual(response.status_code, 400) + self.assertJSONEqual( + response.content, {"name": ["Embedded outpost's name cannot be changed"]} + ) + + @reconcile_app("authentik_outposts") + def test_managed_without_managed(self): + """Test name change for embedded outpost""" + embedded_outpost = Outpost.objects.filter(managed=MANAGED_OUTPOST).first() + self.assertIsNotNone(embedded_outpost) + embedded_outpost.managed = "" + embedded_outpost.save() + response = self.client.patch( + reverse("authentik_api:outpost-detail", kwargs={"pk": embedded_outpost.pk}), + {"name": "foo"}, + ) + self.assertEqual(response.status_code, 200) + embedded_outpost.refresh_from_db() + self.assertEqual(embedded_outpost.managed, MANAGED_OUTPOST) + + def test_outpost_validation(self): """Test Outpost validation""" valid = OutpostSerializer( data={