sources/ldap: allow for anonymous binds, fix sync_users_password not working correctly

Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
Jens Langhammer 2021-08-12 19:09:29 +02:00
parent d9ece98bbc
commit 2592fc3826
6 changed files with 99 additions and 10 deletions

View file

@ -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 + [

View file

@ -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.",
),
),
]

View file

@ -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(

View file

@ -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())

View file

@ -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

View file

@ -129,21 +129,19 @@ export class LDAPSourceForm extends ModelForm<LDAPSource, string> {
${t`To use SSL instead, use 'ldaps://' and disable this option.`}
</p>
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${t`Bind CN`} ?required=${true} name="bindCn">
<ak-form-element-horizontal label=${t`Bind CN`} name="bindCn">
<input
type="text"
value="${ifDefined(this.instance?.bindCn)}"
class="pf-c-form-control"
required
/>
</ak-form-element-horizontal>
<ak-form-element-horizontal
label=${t`Bind Password`}
?required=${true}
?writeOnly=${this.instance !== undefined}
name="bindPassword"
>
<input type="text" value="" class="pf-c-form-control" required />
<input type="text" value="" class="pf-c-form-control" />
</ak-form-element-horizontal>
<ak-form-element-horizontal label=${t`Base DN`} ?required=${true} name="baseDn">
<input