diff --git a/Makefile b/Makefile
index 1943e85b4..dcd8e73ca 100644
--- a/Makefile
+++ b/Makefile
@@ -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
diff --git a/authentik/sources/ldap/api.py b/authentik/sources/ldap/api.py
index 3f6e24837..0a8849345 100644
--- a/authentik/sources/ldap/api.py
+++ b/authentik/sources/ldap/api.py
@@ -8,6 +8,7 @@ from drf_spectacular.utils import extend_schema, extend_schema_field, inline_ser
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
@@ -16,6 +17,7 @@ from authentik.admin.api.tasks import TaskSerializer
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
@@ -24,6 +26,15 @@ from authentik.sources.ldap.tasks import SYNC_CLASSES
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)
@@ -42,9 +53,11 @@ class LDAPSourceSerializer(SourceSerializer):
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",
@@ -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",
diff --git a/authentik/sources/ldap/migrations/0003_ldapsource_client_certificate_ldapsource_sni_and_more.py b/authentik/sources/ldap/migrations/0003_ldapsource_client_certificate_ldapsource_sni_and_more.py
new file mode 100644
index 000000000..7c67597bb
--- /dev/null
+++ b/authentik/sources/ldap/migrations/0003_ldapsource_client_certificate_ldapsource_sni_and_more.py
@@ -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",
+ ),
+ ),
+ ]
diff --git a/authentik/sources/ldap/models.py b/authentik/sources/ldap/models.py
index 2b4acbedf..4a6c2fd2e 100644
--- a/authentik/sources/ldap/models.py
+++ b/authentik/sources/ldap/models.py
@@ -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
@@ -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(
@@ -112,8 +124,22 @@ class LDAPSource(Source):
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,
@@ -133,8 +159,10 @@ class LDAPSource(Source):
"""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,
@@ -148,9 +176,10 @@ class LDAPSource(Source):
successful = connection.bind()
if successful:
return connection
- 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
diff --git a/blueprints/example/sources-google-ldap-mappings.yaml b/blueprints/example/sources-google-ldap-mappings.yaml
new file mode 100644
index 000000000..e070798dd
--- /dev/null
+++ b/blueprints/example/sources-google-ldap-mappings.yaml
@@ -0,0 +1,222 @@
+version: 1
+metadata:
+ labels:
+ blueprints.goauthentik.io/instantiate: "false"
+ name: Example - Google Secure LDAP mappings
+entries:
+ - identifiers:
+ managed: goauthentik.io/sources/ldap/google-uid
+ model: authentik_sources_ldap.ldappropertymapping
+ attrs:
+ name: "Google Secure LDAP Mapping: uid"
+ object_field: "username"
+ expression: |
+ return ldap.get('uid')
+ - identifiers:
+ managed: goauthentik.io/sources/ldap/google-googleuid
+ model: authentik_sources_ldap.ldappropertymapping
+ attrs:
+ name: "Google Secure LDAP Mapping: googleUid"
+ object_field: "attributes.googleUid"
+ expression: |
+ return ldap.get('googleUid')
+ - identifiers:
+ managed: goauthentik.io/sources/ldap/google-posixuid
+ model: authentik_sources_ldap.ldappropertymapping
+ attrs:
+ name: "Google Secure LDAP Mapping: posixUid"
+ object_field: "attributes.posixUid"
+ expression: |
+ return ldap.get('posixUid')
+ - identifiers:
+ managed: goauthentik.io/sources/ldap/google-cn
+ model: authentik_sources_ldap.ldappropertymapping
+ attrs:
+ name: "Google Secure LDAP Mapping: cn"
+ object_field: "name"
+ expression: |
+ return ldap.get('cn')
+ - identifiers:
+ managed: goauthentik.io/sources/ldap/google-sn
+ model: authentik_sources_ldap.ldappropertymapping
+ attrs:
+ name: "Google Secure LDAP Mapping: sn"
+ object_field: "attributes.sn"
+ expression: |
+ return list_flatten(ldap.get('sn'))
+ - identifiers:
+ managed: goauthentik.io/sources/ldap/google-givenname
+ model: authentik_sources_ldap.ldappropertymapping
+ attrs:
+ name: "Google Secure LDAP Mapping: givenName"
+ object_field: "attributes.givenName"
+ expression: |
+ return list_flatten(ldap.get('givenName'))
+ - identifiers:
+ managed: goauthentik.io/sources/ldap/google-displayname
+ model: authentik_sources_ldap.ldappropertymapping
+ attrs:
+ name: "Google Secure LDAP Mapping: displayName"
+ object_field: "attributes.displayName"
+ expression: |
+ return ldap.get('displayName')
+ - identifiers:
+ managed: goauthentik.io/sources/ldap/google-mail
+ model: authentik_sources_ldap.ldappropertymapping
+ attrs:
+ name: "Google Secure LDAP Mapping: mail"
+ object_field: "email"
+ expression: |
+ return ldap.get('mail')
+ - identifiers:
+ managed: goauthentik.io/sources/ldap/google-memberof
+ model: authentik_sources_ldap.ldappropertymapping
+ attrs:
+ name: "Google Secure LDAP Mapping: memberOf"
+ object_field: "attributes.memberOf"
+ expression: |
+ return ldap.get('memberOf')
+ - identifiers:
+ managed: goauthentik.io/sources/ldap/google-title
+ model: authentik_sources_ldap.ldappropertymapping
+ attrs:
+ name: "Google Secure LDAP Mapping: title"
+ object_field: "attributes.title"
+ expression: |
+ return ldap.get('title')
+ - identifiers:
+ managed: goauthentik.io/sources/ldap/google-employeenumber
+ model: authentik_sources_ldap.ldappropertymapping
+ attrs:
+ name: "Google Secure LDAP Mapping: employeeNumber"
+ object_field: "attributes.employeeNumber"
+ expression: |
+ return ldap.get('employeeNumber')
+ - identifiers:
+ managed: goauthentik.io/sources/ldap/google-employeetype
+ model: authentik_sources_ldap.ldappropertymapping
+ attrs:
+ name: "Google Secure LDAP Mapping: employeeType"
+ object_field: "attributes.employeeType"
+ expression: |
+ return ldap.get('employeeType')
+ - identifiers:
+ managed: goauthentik.io/sources/ldap/google-departmentnumber
+ model: authentik_sources_ldap.ldappropertymapping
+ attrs:
+ name: "Google Secure LDAP Mapping: departmentNumber"
+ object_field: "attributes.departmentNumber"
+ expression: |
+ return ldap.get('departmentNumber')
+ - identifiers:
+ managed: goauthentik.io/sources/ldap/google-physicaldeliveryofficename
+ model: authentik_sources_ldap.ldappropertymapping
+ attrs:
+ name: "Google Secure LDAP Mapping: physicalDeliveryOfficeName"
+ object_field: "attributes.physicalDeliveryOfficeName"
+ expression: |
+ return ldap.get('physicalDeliveryOfficeName')
+ - identifiers:
+ managed: goauthentik.io/sources/ldap/google-jpegphoto
+ model: authentik_sources_ldap.ldappropertymapping
+ attrs:
+ name: "Google Secure LDAP Mapping: jpegPhoto"
+ object_field: "attributes.jpegPhoto"
+ expression: |
+ return ldap.get('jpegPhoto')
+ - identifiers:
+ managed: goauthentik.io/sources/ldap/google-entryuuid
+ model: authentik_sources_ldap.ldappropertymapping
+ attrs:
+ name: "Google Secure LDAP Mapping: entryUuid"
+ object_field: "attributes.entryUuid"
+ expression: |
+ return ldap.get('entryUuid')
+ - identifiers:
+ managed: goauthentik.io/sources/ldap/google-objectsid
+ model: authentik_sources_ldap.ldappropertymapping
+ attrs:
+ name: "Google Secure LDAP Mapping: objectSid"
+ object_field: "attributes.objectSid"
+ expression: |
+ return ldap.get('objectSid')
+ - identifiers:
+ managed: goauthentik.io/sources/ldap/google-uidnumber
+ model: authentik_sources_ldap.ldappropertymapping
+ attrs:
+ name: "Google Secure LDAP Mapping: uidNumber"
+ object_field: "attributes.uidNumber"
+ expression: |
+ return ldap.get('uidNumber')
+ - identifiers:
+ managed: goauthentik.io/sources/ldap/google-gidnumber
+ model: authentik_sources_ldap.ldappropertymapping
+ attrs:
+ name: "Google Secure LDAP Mapping: gidNumber"
+ object_field: "attributes.gidNumber"
+ expression: |
+ return ldap.get('gidNumber')
+ - identifiers:
+ managed: goauthentik.io/sources/ldap/google-homedirectory
+ model: authentik_sources_ldap.ldappropertymapping
+ attrs:
+ name: "Google Secure LDAP Mapping: homeDirectory"
+ object_field: "attributes.homeDirectory"
+ expression: |
+ return ldap.get('homeDirectory')
+ - identifiers:
+ managed: goauthentik.io/sources/ldap/google-loginshell
+ model: authentik_sources_ldap.ldappropertymapping
+ attrs:
+ name: "Google Secure LDAP Mapping: loginShell"
+ object_field: "attributes.loginShell"
+ expression: |
+ return ldap.get('loginShell')
+ - identifiers:
+ managed: goauthentik.io/sources/ldap/google-gidnumber
+ model: authentik_sources_ldap.ldappropertymapping
+ attrs:
+ name: "Google Secure LDAP Mapping: gidNumber"
+ object_field: "attributes.gidNumber"
+ expression: |
+ return ldap.get('gidNumber')
+ - identifiers:
+ managed: goauthentik.io/sources/ldap/google-sshpublickey
+ model: authentik_sources_ldap.ldappropertymapping
+ attrs:
+ name: "Google Secure LDAP Mapping: sshPublicKey"
+ object_field: "attributes.sshPublicKey"
+ expression: |
+ return list_flatten(ldap.get('sshPublicKey'))
+ - identifiers:
+ managed: goauthentik.io/sources/ldap/google-description
+ model: authentik_sources_ldap.ldappropertymapping
+ attrs:
+ name: "Google Secure LDAP Mapping: description"
+ object_field: "attributes.description"
+ expression: |
+ return list_flatten(ldap.get('description'))
+ - identifiers:
+ managed: goauthentik.io/sources/ldap/google-member
+ model: authentik_sources_ldap.ldappropertymapping
+ attrs:
+ name: "Google Secure LDAP Mapping: member"
+ object_field: "attributes.member"
+ expression: |
+ return list_flatten(ldap.get('member'))
+ - identifiers:
+ managed: goauthentik.io/sources/ldap/google-memberuid
+ model: authentik_sources_ldap.ldappropertymapping
+ attrs:
+ name: "Google Secure LDAP Mapping: memberUid"
+ object_field: "attributes.memberUid"
+ expression: |
+ return list_flatten(ldap.get('memberUid'))
+ - identifiers:
+ managed: goauthentik.io/sources/ldap/google-googleadmincreated
+ model: authentik_sources_ldap.ldappropertymapping
+ attrs:
+ name: "Google Secure LDAP Mapping: googleAdminCreated"
+ object_field: "attributes.googleAdminCreated"
+ expression: |
+ return list_flatten(ldap.get('googleAdminCreated'))
diff --git a/blueprints/schema.json b/blueprints/schema.json
index 9422fbc79..edbc7f81b 100644
--- a/blueprints/schema.json
+++ b/blueprints/schema.json
@@ -4732,6 +4732,11 @@
"title": "Peer certificate",
"description": "Optionally verify the LDAP Server's Certificate against the CA Chain in this keypair."
},
+ "client_certificate": {
+ "type": "integer",
+ "title": "Client certificate",
+ "description": "Client certificate to authenticate against the LDAP Server's Certificate."
+ },
"bind_cn": {
"type": "string",
"title": "Bind CN"
@@ -4744,6 +4749,10 @@
"type": "boolean",
"title": "Enable Start TLS"
},
+ "sni": {
+ "type": "boolean",
+ "title": "Use Server URI for SNI verification"
+ },
"base_dn": {
"type": "string",
"minLength": 1,
diff --git a/locale/en/LC_MESSAGES/django.po b/locale/en/LC_MESSAGES/django.po
index 7499d20f4..a8e43fce6 100644
--- a/locale/en/LC_MESSAGES/django.po
+++ b/locale/en/LC_MESSAGES/django.po
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2023-05-21 21:59+0000\n"
+"POT-Creation-Date: 2023-06-12 12:11+0000\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME
+ ${msg("Required for servers using TLS 1.3+")} +
++ ${msg( + "Client certificate keypair to authenticate against the LDAP Server's Certificate.", + )} +
+