Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ web-check-compile:
cd web && npm run tsc

web-i18n-extract:
cd web && npm run extract
cd web && npm run extract-locales

#########################
## Website
Expand Down
15 changes: 15 additions & 0 deletions authentik/sources/ldap/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from rest_framework.decorators import action
from rest_framework.exceptions import ValidationError
from rest_framework.fields import DictField, ListField
from rest_framework.relations import PrimaryKeyRelatedField
from rest_framework.request import Request
from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet
Expand All @@ -16,6 +17,7 @@
from authentik.core.api.propertymappings import PropertyMappingSerializer
from authentik.core.api.sources import SourceSerializer
from authentik.core.api.used_by import UsedByMixin
from authentik.crypto.models import CertificateKeyPair
from authentik.events.monitored_tasks import TaskInfo
from authentik.sources.ldap.models import LDAPPropertyMapping, LDAPSource
from authentik.sources.ldap.tasks import SYNC_CLASSES
Expand All @@ -24,6 +26,15 @@
class LDAPSourceSerializer(SourceSerializer):
"""LDAP Source Serializer"""

client_certificate = PrimaryKeyRelatedField(
allow_null=True,
help_text="Client certificate to authenticate against the LDAP Server's Certificate.",
queryset=CertificateKeyPair.objects.exclude(
key_data__exact="",
),
required=False,
)

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)
Expand All @@ -42,9 +53,11 @@ class Meta:
fields = SourceSerializer.Meta.fields + [
"server_uri",
"peer_certificate",
"client_certificate",
"bind_cn",
"bind_password",
"start_tls",
"sni",
"base_dn",
"additional_user_dn",
"additional_group_dn",
Expand Down Expand Up @@ -75,7 +88,9 @@ class LDAPSourceViewSet(UsedByMixin, ModelViewSet):
"server_uri",
"bind_cn",
"peer_certificate",
"client_certificate",
"start_tls",
"sni",
"base_dn",
"additional_user_dn",
"additional_group_dn",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Generated by Django 4.1.7 on 2023-06-06 18:33

import django.db.models.deletion
from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("authentik_crypto", "0004_alter_certificatekeypair_name"),
("authentik_sources_ldap", "0002_auto_20211203_0900"),
]

operations = [
migrations.AddField(
model_name="ldapsource",
name="client_certificate",
field=models.ForeignKey(
default=None,
help_text="Client certificate to authenticate against the LDAP Server's Certificate.",
null=True,
on_delete=django.db.models.deletion.SET_DEFAULT,
related_name="ldap_client_certificates",
to="authentik_crypto.certificatekeypair",
),
),
migrations.AddField(
model_name="ldapsource",
name="sni",
field=models.BooleanField(
default=False, verbose_name="Use Server URI for SNI verification"
),
),
migrations.AlterField(
model_name="ldapsource",
name="peer_certificate",
field=models.ForeignKey(
default=None,
help_text="Optionally verify the LDAP Server's Certificate against the CA Chain in this keypair.",
null=True,
on_delete=django.db.models.deletion.SET_DEFAULT,
related_name="ldap_peer_certificates",
to="authentik_crypto.certificatekeypair",
),
),
]
37 changes: 33 additions & 4 deletions authentik/sources/ldap/models.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
"""authentik LDAP Models"""
from os import chmod
from ssl import CERT_REQUIRED
from tempfile import NamedTemporaryFile, mkdtemp
from typing import Optional

from django.db import models
from django.utils.translation import gettext_lazy as _
from ldap3 import ALL, NONE, RANDOM, Connection, Server, ServerPool, Tls
from ldap3.core.exceptions import LDAPSchemaError
from ldap3.core.exceptions import LDAPInsufficientAccessRightsResult, LDAPSchemaError
from rest_framework.serializers import Serializer

from authentik.core.models import Group, PropertyMapping, Source
Expand Down Expand Up @@ -39,14 +41,24 @@ class LDAPSource(Source):
on_delete=models.SET_DEFAULT,
default=None,
null=True,
related_name="ldap_peer_certificates",
help_text=_(
"Optionally verify the LDAP Server's Certificate against the CA Chain in this keypair."
),
)
client_certificate = models.ForeignKey(
CertificateKeyPair,
on_delete=models.SET_DEFAULT,
default=None,
null=True,
related_name="ldap_client_certificates",
help_text=_("Client certificate to authenticate against the LDAP Server's Certificate."),
)

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"))
sni = models.BooleanField(default=False, verbose_name=_("Use Server URI for SNI verification"))

base_dn = models.TextField(verbose_name=_("Base DN"))
additional_user_dn = models.TextField(
Expand Down Expand Up @@ -112,8 +124,22 @@ def server(self, **kwargs) -> Server:
if self.peer_certificate:
tls_kwargs["ca_certs_data"] = self.peer_certificate.certificate_data
tls_kwargs["validate"] = CERT_REQUIRED
if self.client_certificate:
temp_dir = mkdtemp()
with NamedTemporaryFile(mode="w", delete=False, dir=temp_dir) as temp_cert:
temp_cert.write(self.client_certificate.certificate_data)
certificate_file = temp_cert.name
chmod(certificate_file, 0o600)
with NamedTemporaryFile(mode="w", delete=False, dir=temp_dir) as temp_key:
temp_key.write(self.client_certificate.key_data)
private_key_file = temp_key.name
chmod(private_key_file, 0o600)
tls_kwargs["local_private_key_file"] = private_key_file
tls_kwargs["local_certificate_file"] = certificate_file
if ciphers := CONFIG.y("ldap.tls.ciphers", None):
tls_kwargs["ciphers"] = ciphers.strip()
if self.sni:
tls_kwargs["sni"] = self.server_uri.split(",", maxsplit=1)[0].strip()
server_kwargs = {
"get_info": ALL,
"connect_timeout": LDAP_TIMEOUT,
Expand All @@ -133,8 +159,10 @@ def connection(
"""Get a fully connected and bound LDAP Connection"""
server_kwargs = server_kwargs or {}
connection_kwargs = connection_kwargs or {}
connection_kwargs.setdefault("user", self.bind_cn)
connection_kwargs.setdefault("password", self.bind_password)
if self.bind_cn is not None:
connection_kwargs.setdefault("user", self.bind_cn)
if self.bind_password is not None:
connection_kwargs.setdefault("password", self.bind_password)
connection = Connection(
self.server(**server_kwargs),
raise_exceptions=True,
Expand All @@ -146,9 +174,10 @@ def connection(
connection.start_tls(read_server_info=False)
try:
connection.bind()
except LDAPSchemaError as exc:
except (LDAPSchemaError, LDAPInsufficientAccessRightsResult) as exc:
# Schema error, so try connecting without schema info
# See https://github.com/goauthentik/authentik/issues/4590
# See also https://github.com/goauthentik/authentik/issues/3399
if server_kwargs.get("get_info", ALL) == NONE:
raise exc
server_kwargs["get_info"] = NONE
Expand Down
Loading