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:
parent
d9ece98bbc
commit
2592fc3826
|
@ -1,10 +1,13 @@
|
||||||
"""Source API Views"""
|
"""Source API Views"""
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from django.http.response import Http404
|
from django.http.response import Http404
|
||||||
from django.utils.text import slugify
|
from django.utils.text import slugify
|
||||||
from django_filters.filters import AllValuesMultipleFilter
|
from django_filters.filters import AllValuesMultipleFilter
|
||||||
from django_filters.filterset import FilterSet
|
from django_filters.filterset import FilterSet
|
||||||
from drf_spectacular.utils import OpenApiResponse, extend_schema
|
from drf_spectacular.utils import OpenApiResponse, extend_schema
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
|
from rest_framework.exceptions import ValidationError
|
||||||
from rest_framework.request import Request
|
from rest_framework.request import Request
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.viewsets import ModelViewSet
|
from rest_framework.viewsets import ModelViewSet
|
||||||
|
@ -20,6 +23,16 @@ from authentik.sources.ldap.models import LDAPPropertyMapping, LDAPSource
|
||||||
class LDAPSourceSerializer(SourceSerializer):
|
class LDAPSourceSerializer(SourceSerializer):
|
||||||
"""LDAP Source Serializer"""
|
"""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:
|
class Meta:
|
||||||
model = LDAPSource
|
model = LDAPSource
|
||||||
fields = SourceSerializer.Meta.fields + [
|
fields = SourceSerializer.Meta.fields + [
|
||||||
|
|
31
authentik/sources/ldap/migrations/0012_auto_20210812_1703.py
Normal file
31
authentik/sources/ldap/migrations/0012_auto_20210812_1703.py
Normal 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.",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
|
@ -17,8 +17,8 @@ class LDAPSource(Source):
|
||||||
validators=[DomainlessURLValidator(schemes=["ldap", "ldaps"])],
|
validators=[DomainlessURLValidator(schemes=["ldap", "ldaps"])],
|
||||||
verbose_name=_("Server URI"),
|
verbose_name=_("Server URI"),
|
||||||
)
|
)
|
||||||
bind_cn = models.TextField(verbose_name=_("Bind CN"))
|
bind_cn = models.TextField(verbose_name=_("Bind CN"), blank=True)
|
||||||
bind_password = models.TextField()
|
bind_password = models.TextField(blank=True)
|
||||||
start_tls = models.BooleanField(default=False, verbose_name=_("Enable Start TLS"))
|
start_tls = models.BooleanField(default=False, verbose_name=_("Enable Start TLS"))
|
||||||
|
|
||||||
base_dn = models.TextField(verbose_name=_("Base DN"))
|
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."
|
"This can only be enabled on a single LDAP source."
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
unique=True,
|
|
||||||
)
|
)
|
||||||
sync_groups = models.BooleanField(default=True)
|
sync_groups = models.BooleanField(default=True)
|
||||||
sync_parent_group = models.ForeignKey(
|
sync_parent_group = models.ForeignKey(
|
||||||
|
|
51
authentik/sources/ldap/tests/test_api.py
Normal file
51
authentik/sources/ldap/tests/test_api.py
Normal 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())
|
|
@ -22468,7 +22468,6 @@ components:
|
||||||
description: Property mappings used for group creation/updating.
|
description: Property mappings used for group creation/updating.
|
||||||
required:
|
required:
|
||||||
- base_dn
|
- base_dn
|
||||||
- bind_cn
|
|
||||||
- component
|
- component
|
||||||
- name
|
- name
|
||||||
- pk
|
- pk
|
||||||
|
@ -22565,8 +22564,6 @@ components:
|
||||||
description: Property mappings used for group creation/updating.
|
description: Property mappings used for group creation/updating.
|
||||||
required:
|
required:
|
||||||
- base_dn
|
- base_dn
|
||||||
- bind_cn
|
|
||||||
- bind_password
|
|
||||||
- name
|
- name
|
||||||
- server_uri
|
- server_uri
|
||||||
- slug
|
- slug
|
||||||
|
|
|
@ -129,21 +129,19 @@ export class LDAPSourceForm extends ModelForm<LDAPSource, string> {
|
||||||
${t`To use SSL instead, use 'ldaps://' and disable this option.`}
|
${t`To use SSL instead, use 'ldaps://' and disable this option.`}
|
||||||
</p>
|
</p>
|
||||||
</ak-form-element-horizontal>
|
</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
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value="${ifDefined(this.instance?.bindCn)}"
|
value="${ifDefined(this.instance?.bindCn)}"
|
||||||
class="pf-c-form-control"
|
class="pf-c-form-control"
|
||||||
required
|
|
||||||
/>
|
/>
|
||||||
</ak-form-element-horizontal>
|
</ak-form-element-horizontal>
|
||||||
<ak-form-element-horizontal
|
<ak-form-element-horizontal
|
||||||
label=${t`Bind Password`}
|
label=${t`Bind Password`}
|
||||||
?required=${true}
|
|
||||||
?writeOnly=${this.instance !== undefined}
|
?writeOnly=${this.instance !== undefined}
|
||||||
name="bindPassword"
|
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>
|
||||||
<ak-form-element-horizontal label=${t`Base DN`} ?required=${true} name="baseDn">
|
<ak-form-element-horizontal label=${t`Base DN`} ?required=${true} name="baseDn">
|
||||||
<input
|
<input
|
||||||
|
|
Reference in a new issue