diff --git a/authentik/blueprints/api.py b/authentik/blueprints/api.py index e22bd18e1..4ae847106 100644 --- a/authentik/blueprints/api.py +++ b/authentik/blueprints/api.py @@ -13,6 +13,7 @@ from rest_framework.viewsets import ModelViewSet from authentik.api.decorators import permission_required from authentik.blueprints.models import BlueprintInstance from authentik.blueprints.v1.importer import Importer +from authentik.blueprints.v1.oci import OCI_PREFIX from authentik.blueprints.v1.tasks import apply_blueprint, blueprints_find_dict from authentik.core.api.used_by import UsedByMixin from authentik.core.api.utils import PassiveSerializer @@ -36,7 +37,7 @@ class BlueprintInstanceSerializer(ModelSerializer): def validate_path(self, path: str) -> str: """Ensure the path (if set) specified is retrievable""" - if path == "": + if path == "" or path.startswith(OCI_PREFIX): return path files: list[dict] = blueprints_find_dict.delay().get() if path not in [file["path"] for file in files]: diff --git a/authentik/blueprints/models.py b/authentik/blueprints/models.py index 012e4986c..130b13264 100644 --- a/authentik/blueprints/models.py +++ b/authentik/blueprints/models.py @@ -8,7 +8,7 @@ from django.utils.translation import gettext_lazy as _ from rest_framework.serializers import Serializer from structlog import get_logger -from authentik.blueprints.v1.oci import BlueprintOCIClient, OCIException +from authentik.blueprints.v1.oci import OCI_PREFIX, BlueprintOCIClient, OCIException from authentik.lib.config import CONFIG from authentik.lib.models import CreatedUpdatedModel, SerializerModel from authentik.lib.sentry import SentryIgnoredException @@ -72,7 +72,7 @@ class BlueprintInstance(SerializerModel, ManagedModel, CreatedUpdatedModel): def retrieve_oci(self) -> str: """Get blueprint from an OCI registry""" - client = BlueprintOCIClient(self.path.replace("oci://", "https://")) + client = BlueprintOCIClient(self.path.replace(OCI_PREFIX, "https://")) try: manifests = client.fetch_manifests() return client.fetch_blobs(manifests) @@ -90,7 +90,7 @@ class BlueprintInstance(SerializerModel, ManagedModel, CreatedUpdatedModel): def retrieve(self) -> str: """Retrieve blueprint contents""" - if self.path.startswith("oci://"): + if self.path.startswith(OCI_PREFIX): return self.retrieve_oci() if self.path != "": return self.retrieve_file() diff --git a/authentik/blueprints/tests/test_v1_api.py b/authentik/blueprints/tests/test_v1_api.py index 33bdd1b03..6ee4bd7f6 100644 --- a/authentik/blueprints/tests/test_v1_api.py +++ b/authentik/blueprints/tests/test_v1_api.py @@ -44,6 +44,14 @@ class TestBlueprintsV1API(APITestCase): ), ) + def test_api_oci(self): + """Test validation with OCI path""" + res = self.client.post( + reverse("authentik_api:blueprintinstance-list"), + data={"name": "foo", "path": "oci://foo/bar"}, + ) + self.assertEqual(res.status_code, 201) + def test_api_blank(self): """Test blank""" res = self.client.post( diff --git a/authentik/blueprints/v1/oci.py b/authentik/blueprints/v1/oci.py index bda7bcf73..d02f3fa2f 100644 --- a/authentik/blueprints/v1/oci.py +++ b/authentik/blueprints/v1/oci.py @@ -19,6 +19,7 @@ from authentik.lib.sentry import SentryIgnoredException from authentik.lib.utils.http import authentik_user_agent OCI_MEDIA_TYPE = "application/vnd.goauthentik.blueprint.v1+yaml" +OCI_PREFIX = "oci://" class OCIException(SentryIgnoredException): diff --git a/authentik/blueprints/v1/tasks.py b/authentik/blueprints/v1/tasks.py index 3a278ef3c..757eb556c 100644 --- a/authentik/blueprints/v1/tasks.py +++ b/authentik/blueprints/v1/tasks.py @@ -28,6 +28,7 @@ from authentik.blueprints.models import ( from authentik.blueprints.v1.common import BlueprintLoader, BlueprintMetadata, EntryInvalidError from authentik.blueprints.v1.importer import Importer from authentik.blueprints.v1.labels import LABEL_AUTHENTIK_INSTANTIATE +from authentik.blueprints.v1.oci import OCI_PREFIX from authentik.events.monitored_tasks import ( MonitoredTask, TaskResult, @@ -228,7 +229,7 @@ def apply_blueprint(self: MonitoredTask, instance_pk: str): def clear_failed_blueprints(): """Remove blueprints which couldn't be fetched""" # Exclude OCI blueprints as those might be temporarily unavailable - for blueprint in BlueprintInstance.objects.exclude(path__startswith="oci://"): + for blueprint in BlueprintInstance.objects.exclude(path__startswith=OCI_PREFIX): try: blueprint.retrieve() except BlueprintRetrievalFailed: