diff --git a/authentik/sources/ldap/api.py b/authentik/sources/ldap/api.py index 014c75cf2..22dfcb572 100644 --- a/authentik/sources/ldap/api.py +++ b/authentik/sources/ldap/api.py @@ -1,10 +1,13 @@ """Source API Views""" +from typing import Any + from django.http.response import Http404 from django.utils.text import slugify from django_filters.filters import AllValuesMultipleFilter from django_filters.filterset import FilterSet from drf_spectacular.utils import OpenApiResponse, extend_schema from rest_framework.decorators import action +from rest_framework.exceptions import ValidationError from rest_framework.request import Request from rest_framework.response import Response from rest_framework.viewsets import ModelViewSet @@ -20,6 +23,16 @@ from authentik.sources.ldap.models import LDAPPropertyMapping, LDAPSource class LDAPSourceSerializer(SourceSerializer): """LDAP Source Serializer""" + def validate(self, attrs: dict[str, Any]) -> dict[str, Any]: + """Check that only a single source has password_sync on""" + sync_users_password = attrs.get("sync_users_password", True) + if sync_users_password: + if LDAPSource.objects.filter(sync_users_password=True).exists(): + raise ValidationError( + "Only a single LDAP Source with password synchronization is allowed" + ) + return super().validate(attrs) + class Meta: model = LDAPSource fields = SourceSerializer.Meta.fields + [ diff --git a/authentik/sources/ldap/migrations/0012_auto_20210812_1703.py b/authentik/sources/ldap/migrations/0012_auto_20210812_1703.py new file mode 100644 index 000000000..fc440c38e --- /dev/null +++ b/authentik/sources/ldap/migrations/0012_auto_20210812_1703.py @@ -0,0 +1,31 @@ +# Generated by Django 3.2.5 on 2021-08-12 17:03 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("authentik_sources_ldap", "0011_ldapsource_property_mappings_group"), + ] + + operations = [ + migrations.AlterField( + model_name="ldapsource", + name="bind_cn", + field=models.TextField(blank=True, verbose_name="Bind CN"), + ), + migrations.AlterField( + model_name="ldapsource", + name="bind_password", + field=models.TextField(blank=True), + ), + migrations.AlterField( + model_name="ldapsource", + name="sync_users_password", + field=models.BooleanField( + default=True, + help_text="When a user changes their password, sync it back to LDAP. This can only be enabled on a single LDAP source.", + ), + ), + ] diff --git a/authentik/sources/ldap/models.py b/authentik/sources/ldap/models.py index d9e0d60f9..fb83adf98 100644 --- a/authentik/sources/ldap/models.py +++ b/authentik/sources/ldap/models.py @@ -17,8 +17,8 @@ class LDAPSource(Source): validators=[DomainlessURLValidator(schemes=["ldap", "ldaps"])], verbose_name=_("Server URI"), ) - bind_cn = models.TextField(verbose_name=_("Bind CN")) - bind_password = models.TextField() + bind_cn = models.TextField(verbose_name=_("Bind CN"), blank=True) + bind_password = models.TextField(blank=True) start_tls = models.BooleanField(default=False, verbose_name=_("Enable Start TLS")) base_dn = models.TextField(verbose_name=_("Base DN")) @@ -64,7 +64,6 @@ class LDAPSource(Source): "This can only be enabled on a single LDAP source." ) ), - unique=True, ) sync_groups = models.BooleanField(default=True) sync_parent_group = models.ForeignKey( diff --git a/authentik/sources/ldap/tests/test_api.py b/authentik/sources/ldap/tests/test_api.py new file mode 100644 index 000000000..5419a6df8 --- /dev/null +++ b/authentik/sources/ldap/tests/test_api.py @@ -0,0 +1,51 @@ +"""LDAP Source API tests""" +from rest_framework.test import APITestCase + +from authentik.providers.oauth2.generators import generate_client_secret +from authentik.sources.ldap.api import LDAPSourceSerializer +from authentik.sources.ldap.models import LDAPSource + +LDAP_PASSWORD = generate_client_secret() + + +class LDAPAPITests(APITestCase): + """LDAP API tests""" + + def test_sync_users_password_valid(self): + """Check that single source with sync_users_password is valid""" + serializer = LDAPSourceSerializer( + data={ + "name": "foo", + "slug": " foo", + "server_uri": "ldaps://1.2.3.4", + "bind_cn": "", + "bind_password": LDAP_PASSWORD, + "base_dn": "dc=foo", + "sync_users_password": True, + } + ) + self.assertTrue(serializer.is_valid()) + + def test_sync_users_password_invalid(self): + """Ensure only a single source with password sync can be created""" + LDAPSource.objects.create( + name="foo", + slug="foo", + server_uri="ldaps://1.2.3.4", + bind_cn="", + bind_password=LDAP_PASSWORD, + base_dn="dc=foo", + sync_users_password=True, + ) + serializer = LDAPSourceSerializer( + data={ + "name": "foo", + "slug": " foo", + "server_uri": "ldaps://1.2.3.4", + "bind_cn": "", + "bind_password": LDAP_PASSWORD, + "base_dn": "dc=foo", + "sync_users_password": False, + } + ) + self.assertFalse(serializer.is_valid()) diff --git a/schema.yml b/schema.yml index 7d00e2d5a..e1a57ceae 100644 --- a/schema.yml +++ b/schema.yml @@ -22468,7 +22468,6 @@ components: description: Property mappings used for group creation/updating. required: - base_dn - - bind_cn - component - name - pk @@ -22565,8 +22564,6 @@ components: description: Property mappings used for group creation/updating. required: - base_dn - - bind_cn - - bind_password - name - server_uri - slug diff --git a/web/src/pages/sources/ldap/LDAPSourceForm.ts b/web/src/pages/sources/ldap/LDAPSourceForm.ts index bac9ac5d2..84dde8e6a 100644 --- a/web/src/pages/sources/ldap/LDAPSourceForm.ts +++ b/web/src/pages/sources/ldap/LDAPSourceForm.ts @@ -129,21 +129,19 @@ export class LDAPSourceForm extends ModelForm { ${t`To use SSL instead, use 'ldaps://' and disable this option.`}

- + - +