diff --git a/authentik/core/api/sources.py b/authentik/core/api/sources.py index 93154c1559..fb6b2c356e 100644 --- a/authentik/core/api/sources.py +++ b/authentik/core/api/sources.py @@ -5,6 +5,7 @@ from collections.abc import Iterable from drf_spectacular.utils import OpenApiResponse, extend_schema from rest_framework import mixins from rest_framework.decorators import action +from rest_framework.exceptions import ValidationError from rest_framework.fields import CharField, ReadOnlyField, SerializerMethodField from rest_framework.parsers import MultiPartParser from rest_framework.request import Request @@ -154,6 +155,17 @@ class SourceViewSet( matching_sources.append(source_settings.validated_data) return Response(matching_sources) + def destroy(self, request: Request, *args, **kwargs): + """Prevent deletion of built-in sources""" + instance: Source = self.get_object() + + if instance.managed == Source.MANAGED_INBUILT: + raise ValidationError( + {"detail": "Built-in sources cannot be deleted"}, code="protected" + ) + + return super().destroy(request, *args, **kwargs) + class UserSourceConnectionSerializer(SourceSerializer): """User source connection""" diff --git a/authentik/core/apps.py b/authentik/core/apps.py index 6fff9cb89f..87f7992682 100644 --- a/authentik/core/apps.py +++ b/authentik/core/apps.py @@ -32,5 +32,5 @@ class AuthentikCoreConfig(ManagedAppConfig): "name": "authentik Built-in", "slug": "authentik-built-in", }, - managed="goauthentik.io/sources/inbuilt", + managed=Source.MANAGED_INBUILT, ) diff --git a/authentik/core/models.py b/authentik/core/models.py index 2aa09fd5cd..17feb8a400 100644 --- a/authentik/core/models.py +++ b/authentik/core/models.py @@ -678,6 +678,8 @@ class SourceGroupMatchingModes(models.TextChoices): class Source(ManagedModel, SerializerModel, PolicyBindingModel): """Base Authentication source, i.e. an OAuth Provider, SAML Remote or LDAP Server""" + MANAGED_INBUILT = "goauthentik.io/sources/inbuilt" + name = models.TextField(help_text=_("Source's display Name.")) slug = models.SlugField(help_text=_("Internal source name, used in URLs."), unique=True) diff --git a/web/src/admin/sources/SourceListPage.ts b/web/src/admin/sources/SourceListPage.ts index a9af5d2336..dae995212e 100644 --- a/web/src/admin/sources/SourceListPage.ts +++ b/web/src/admin/sources/SourceListPage.ts @@ -57,10 +57,13 @@ export class SourceListPage extends TablePage { } renderToolbarSelected(): TemplateResult { - const disabled = this.selectedElements.length < 1; + const disabled = + this.selectedElements.length < 1 || + this.selectedElements.some((item) => item.component === ""); + const nonBuiltInSources = this.selectedElements.filter((item) => item.component !== ""); return html` { return new SourcesApi(DEFAULT_CONFIG).sourcesAllUsedByList({ slug: item.slug,