diff --git a/authentik/lib/models.py b/authentik/lib/models.py index 795cf8c4b..3c7ba9c65 100644 --- a/authentik/lib/models.py +++ b/authentik/lib/models.py @@ -68,9 +68,9 @@ class DomainlessURLValidator(URLValidator): ) self.schemes = ["http", "https", "blank"] + list(self.schemes) - def __call__(self, value): + def __call__(self, value: str): # Check if the scheme is valid. scheme = value.split("://")[0].lower() if scheme not in self.schemes: value = "default" + value - return super().__call__(value) + super().__call__(value) diff --git a/authentik/sources/ldap/models.py b/authentik/sources/ldap/models.py index c5a6c123a..53912c5de 100644 --- a/authentik/sources/ldap/models.py +++ b/authentik/sources/ldap/models.py @@ -3,7 +3,7 @@ from typing import Optional, Type from django.db import models from django.utils.translation import gettext_lazy as _ -from ldap3 import ALL, Connection, Server +from ldap3 import ALL, ROUND_ROBIN, Connection, Server, ServerPool from rest_framework.serializers import Serializer from authentik.core.models import Group, PropertyMapping, Source @@ -12,11 +12,22 @@ from authentik.lib.models import DomainlessURLValidator LDAP_TIMEOUT = 15 +class MultiURLValidator(DomainlessURLValidator): + """Same as DomainlessURLValidator but supports multiple URLs separated with a comma.""" + + def __call__(self, value: str): + if "," in value: + for url in value.split(","): + super().__call__(url) + else: + super().__call__(value) + + class LDAPSource(Source): """Federate LDAP Directory with authentik, or create new accounts in LDAP.""" server_uri = models.TextField( - validators=[DomainlessURLValidator(schemes=["ldap", "ldaps"])], + validators=[MultiURLValidator(schemes=["ldap", "ldaps"])], verbose_name=_("Server URI"), ) bind_cn = models.TextField(verbose_name=_("Bind CN"), blank=True) @@ -88,9 +99,15 @@ class LDAPSource(Source): def connection(self) -> Connection: """Get a fully connected and bound LDAP Connection""" if not self._connection: - server = Server(self.server_uri, get_info=ALL, connect_timeout=LDAP_TIMEOUT) + servers = [] + if "," in self.server_uri: + for server in self.server_uri.split(","): + servers.append(Server(server, get_info=ALL, connect_timeout=LDAP_TIMEOUT)) + else: + servers = [Server(self.server_uri, get_info=ALL, connect_timeout=LDAP_TIMEOUT)] + pool = ServerPool(servers, ROUND_ROBIN, active=True, exhaust=True) self._connection = Connection( - server, + pool, raise_exceptions=True, user=self.bind_cn, password=self.bind_password, diff --git a/web/src/locales/en.po b/web/src/locales/en.po index c261429d2..15bc01096 100644 --- a/web/src/locales/en.po +++ b/web/src/locales/en.po @@ -4199,6 +4199,10 @@ msgstr "Sources" msgid "Sources of identities, which can either be synced into authentik's database, or can be used by users to authenticate and enroll themselves." msgstr "Sources of identities, which can either be synced into authentik's database, or can be used by users to authenticate and enroll themselves." +#: src/pages/sources/ldap/LDAPSourceForm.ts +msgid "Specify multiple server URIs by separating them with a comma." +msgstr "Specify multiple server URIs by separating them with a comma." + #: src/pages/flows/BoundStagesList.ts #: src/pages/flows/StageBindingForm.ts msgid "Stage" diff --git a/web/src/locales/fr_FR.po b/web/src/locales/fr_FR.po index 22615c3ad..39c9ccbc5 100644 --- a/web/src/locales/fr_FR.po +++ b/web/src/locales/fr_FR.po @@ -4162,6 +4162,10 @@ msgstr "Sources" msgid "Sources of identities, which can either be synced into authentik's database, or can be used by users to authenticate and enroll themselves." msgstr "Sources d'identités, qui peuvent soit être synchronisées dans la base de données d'Authentik, soit être utilisées par les utilisateurs pour s'authentifier et s'inscrire." +#: src/pages/sources/ldap/LDAPSourceForm.ts +msgid "Specify multiple server URIs by separating them with a comma." +msgstr "" + #: src/pages/flows/BoundStagesList.ts #: src/pages/flows/StageBindingForm.ts msgid "Stage" diff --git a/web/src/locales/pseudo-LOCALE.po b/web/src/locales/pseudo-LOCALE.po index f58cab15b..102f50448 100644 --- a/web/src/locales/pseudo-LOCALE.po +++ b/web/src/locales/pseudo-LOCALE.po @@ -4191,6 +4191,10 @@ msgstr "" msgid "Sources of identities, which can either be synced into authentik's database, or can be used by users to authenticate and enroll themselves." msgstr "" +#: src/pages/sources/ldap/LDAPSourceForm.ts +msgid "Specify multiple server URIs by separating them with a comma." +msgstr "" + #: src/pages/flows/BoundStagesList.ts #: src/pages/flows/StageBindingForm.ts msgid "Stage" diff --git a/web/src/pages/sources/ldap/LDAPSourceForm.ts b/web/src/pages/sources/ldap/LDAPSourceForm.ts index 3eccb6051..9c8982711 100644 --- a/web/src/pages/sources/ldap/LDAPSourceForm.ts +++ b/web/src/pages/sources/ldap/LDAPSourceForm.ts @@ -124,6 +124,9 @@ export class LDAPSourceForm extends ModelForm { class="pf-c-form-control" required /> +

+ ${t`Specify multiple server URIs by separating them with a comma.`} +

diff --git a/website/integrations/sources/active-directory/index.md b/website/integrations/sources/active-directory/index.md index faa9087af..769d3eb5d 100644 --- a/website/integrations/sources/active-directory/index.md +++ b/website/integrations/sources/active-directory/index.md @@ -43,6 +43,10 @@ Use these settings: For authentik to be able to write passwords back to Active Directory, make sure to use `ldaps://` + You can specify multiple servers by separating URIs with a comma, like `ldap://dc1.ad.company,ldap://dc2.ad.company`. + + When using a DNS entry with multiple Records, authentik will select a random entry when first connecting. + - Bind CN: `@ad.company` - Bind Password: The password you've given the user above - Base DN: The base DN which you want authentik to sync diff --git a/website/integrations/sources/freeipa/index.md b/website/integrations/sources/freeipa/index.md index 146109d0f..193521c33 100644 --- a/website/integrations/sources/freeipa/index.md +++ b/website/integrations/sources/freeipa/index.md @@ -30,7 +30,7 @@ The following placeholders will be used: ``` $ ldapmodify -x -D "cn=Directory Manager" -W -h ipa1.freeipa.company -p 389 - + dn: cn=ipa_pwd_extop,cn=plugins,cn=config changetype: modify add: passSyncManagersDNs @@ -45,6 +45,11 @@ In authentik, create a new LDAP Source in Resources -> Sources. Use these settings: - Server URI: `ldaps://ipa1.freeipa.company` + + You can specify multiple servers by separating URIs with a comma, like `ldap://ipa1.freeipa.company,ldap://ipa2.freeipa.company`. + + When using a DNS entry with multiple Records, authentik will select a random entry when first connecting. + - Bind CN: `uid=svc_authentik,cn=users,cn=accounts,dc=freeipa,dc=company` - Bind Password: The password you've given the user above - Base DN: `dc=freeipa,dc=company` diff --git a/website/integrations/sources/ldap/index.md b/website/integrations/sources/ldap/index.md index a4eef3086..d373af381 100644 --- a/website/integrations/sources/ldap/index.md +++ b/website/integrations/sources/ldap/index.md @@ -15,6 +15,11 @@ For FreeIPA, follow the [FreeIPA Integration](../freeipa/index.md) ::: - Server URI: URI to your LDAP server/Domain Controller. + + You can specify multiple servers by separating URIs with a comma, like `ldap://ldap1.company,ldap://ldap2.company`. + + When using a DNS entry with multiple Records, authentik will select a random entry when first connecting. + - Bind CN: CN of the bind user. This can also be a UPN in the format of `user@domain.tld`. - Bind password: Password used during the bind process. - Enable StartTLS: Enables StartTLS functionality. To use LDAPS instead, use port `636`.