feat(configuration): lldap implementation (#4498)

This adds a lldap LDAP implementation which purely adds sane defaults for lldap. There are no functional differences just when the implementation option is set to 'lldap' sane defaults which should be sufficient for most use cases are set. See the documentation at https://www.authelia.com/r/ldap#defaults for more details.
pull/4499/head^2
James Elliott 2022-12-21 21:51:25 +11:00 committed by GitHub
parent d67554ab88
commit 5b8b3145ad
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 200 additions and 15 deletions

View File

@ -286,6 +286,7 @@ authentication_backend:
## Acceptable options are as follows: ## Acceptable options are as follows:
## - 'activedirectory' - for Microsoft Active Directory. ## - 'activedirectory' - for Microsoft Active Directory.
## - 'freeipa' - for FreeIPA. ## - 'freeipa' - for FreeIPA.
## - 'lldap' - for lldap.
## - 'custom' - for custom specifications of attributes and filters. ## - 'custom' - for custom specifications of attributes and filters.
## This currently defaults to 'custom' to maintain existing behaviour. ## This currently defaults to 'custom' to maintain existing behaviour.
## ##

View File

@ -48,10 +48,24 @@ Authelia primarily supports this method.
## Implementation Guide ## Implementation Guide
There are currently two implementations, `custom`, `activedirectory`, and `freeipa`. The `activedirectory` The following implementations exist:
implementation must be used if you wish to allow users to change or reset their password as Active Directory
uses a custom attribute and mechanism for this. The long term intention of this is to have logical defaults for various - `custom`:
RFC implementations of LDAP. - Not specific to any particular LDAP provider
- `activedirectory`:
- Specific configuration defaults for [Active Directory]
- Special implementation details:
- Includes a special encoding format required for changing passwords with [Active Directory]
- `freeipa`:
- Specific configuration defaults for [FreeIPA]
- No special implementation details
- `lldap`:
- Specific configuration defaults for [lldap]
- No special implementation details
[Active Directory]: https://learn.microsoft.com/en-us/windows-server/identity/ad-ds/active-directory-domain-services
[FreeIPA]: https://www.freeipa.org/
[lldap]: https://github.com/nitnelave/lldap
### Filter replacements ### Filter replacements
@ -61,14 +75,14 @@ search.
#### Users filter replacements #### Users filter replacements
| Placeholder | Phase | Replacement | | Placeholder | Phase | Replacement |
|:-------------------------:|:-------:|:--------------------------------------------------------------------------------------------------------------:| |:-------------------------:|:-------:|:----------------------------------------------------------------------------------------------------------------:|
| {username_attribute} | startup | The configured username attribute | | {username_attribute} | startup | The configured username attribute |
| {mail_attribute} | startup | The configured mail attribute | | {mail_attribute} | startup | The configured mail attribute |
| {display_name_attribute} | startup | The configured display name attribute | | {display_name_attribute} | startup | The configured display name attribute |
| {input} | search | The input into the username field | | {input} | search | The input into the username field |
| {date-time:generalized} | search | The current UTC time formatted as a LDAP generalized time in the format of `20060102150405.0Z` | | {date-time:generalized} | search | The current UTC time formatted as a LDAP generalized time in the format of `20060102150405.0Z` |
| {date-time:unix-epoch} | search | The current time formatted as a Unix epoch | | {date-time:unix-epoch} | search | The current time formatted as a Unix epoch |
| {date-time:msft-nt-epoch} | search | The current time formatted as a Microsoft NT epoch which is used by some Microsoft Active Directory attributes | | {date-time:msft-nt-epoch} | search | The current time formatted as a Microsoft NT epoch which is used by some Microsoft [Active Directory] attributes |
#### Groups filter replacements #### Groups filter replacements
@ -82,6 +96,14 @@ search.
The below tables describes the current attribute defaults for each implementation. The below tables describes the current attribute defaults for each implementation.
#### Search Base defaults
The following set defaults for the `additional_users_dn` and `additional_groups_dn` values.
| Implementation | Users | Groups |
|:--------------:|:---------:|:---------:|
| lldap | OU=people | OU=groups |
#### Attribute defaults #### Attribute defaults
This table describes the attribute defaults for each implementation. i.e. the username_attribute is described by the This table describes the attribute defaults for each implementation. i.e. the username_attribute is described by the
@ -92,6 +114,7 @@ Username column.
| custom | N/A | displayName | mail | cn | | custom | N/A | displayName | mail | cn |
| activedirectory | sAMAccountName | displayName | mail | cn | | activedirectory | sAMAccountName | displayName | mail | cn |
| freeipa | uid | displayName | mail | cn | | freeipa | uid | displayName | mail | cn |
| lldap | uid | cn | mail | cn |
#### Filter defaults #### Filter defaults
@ -113,6 +136,7 @@ the following conditions:
| custom | N/A | N/A | | custom | N/A | N/A |
| activedirectory | (&(|({username_attribute}={input})({mail_attribute}={input}))(sAMAccountType=805306368)(!(userAccountControl:1.2.840.113556.1.4.803:=2))(!(pwdLastSet=0))(|(!(accountExpires=*))(accountExpires=0)(accountExpires>={date-time:msft-nt-epoch}))) | (&(member={dn})(|(sAMAccountType=268435456)(sAMAccountType=536870912))) | | activedirectory | (&(|({username_attribute}={input})({mail_attribute}={input}))(sAMAccountType=805306368)(!(userAccountControl:1.2.840.113556.1.4.803:=2))(!(pwdLastSet=0))(|(!(accountExpires=*))(accountExpires=0)(accountExpires>={date-time:msft-nt-epoch}))) | (&(member={dn})(|(sAMAccountType=268435456)(sAMAccountType=536870912))) |
| freeipa | (&(|({username_attribute}={input})({mail_attribute}={input}))(objectClass=person)(!(nsAccountLock=TRUE))(krbPasswordExpiration>={date-time:generalized})(|(!(krbPrincipalExpiration=*))(krbPrincipalExpiration>={date-time:generalized}))) | (&(member={dn})(objectClass=groupOfNames)) | | freeipa | (&(|({username_attribute}={input})({mail_attribute}={input}))(objectClass=person)(!(nsAccountLock=TRUE))(krbPasswordExpiration>={date-time:generalized})(|(!(krbPrincipalExpiration=*))(krbPrincipalExpiration>={date-time:generalized}))) | (&(member={dn})(objectClass=groupOfNames)) |
| lldap | (&(|({username_attribute}={input})({mail_attribute}={input}))(objectClass=person)) | (&(member={dn})(objectClass=groupOfNames)) |
##### Microsoft Active Directory sAMAccountType ##### Microsoft Active Directory sAMAccountType

View File

@ -286,6 +286,7 @@ authentication_backend:
## Acceptable options are as follows: ## Acceptable options are as follows:
## - 'activedirectory' - for Microsoft Active Directory. ## - 'activedirectory' - for Microsoft Active Directory.
## - 'freeipa' - for FreeIPA. ## - 'freeipa' - for FreeIPA.
## - 'lldap' - for lldap.
## - 'custom' - for custom specifications of attributes and filters. ## - 'custom' - for custom specifications of attributes and filters.
## This currently defaults to 'custom' to maintain existing behaviour. ## This currently defaults to 'custom' to maintain existing behaviour.
## ##

View File

@ -212,3 +212,19 @@ var DefaultLDAPAuthenticationBackendConfigurationImplementationFreeIPA = LDAPAut
MinimumVersion: TLSVersion{tls.VersionTLS12}, MinimumVersion: TLSVersion{tls.VersionTLS12},
}, },
} }
// DefaultLDAPAuthenticationBackendConfigurationImplementationLLDAP represents the default LDAP config for the LDAPImplementationLLDAP Implementation.
var DefaultLDAPAuthenticationBackendConfigurationImplementationLLDAP = LDAPAuthenticationBackend{
AdditionalUsersDN: "OU=people",
AdditionalGroupsDN: "OU=groups",
UsersFilter: "(&(|({username_attribute}={input})({mail_attribute}={input}))(objectClass=person))",
UsernameAttribute: ldapAttrUserID,
MailAttribute: ldapAttrMail,
DisplayNameAttribute: ldapAttrCommonName,
GroupsFilter: "(&(member={dn})(objectClass=groupOfUniqueNames))",
GroupNameAttribute: ldapAttrCommonName,
Timeout: time.Second * 5,
TLS: &TLSConfig{
MinimumVersion: TLSVersion{tls.VersionTLS12},
},
}

View File

@ -67,6 +67,9 @@ const (
// LDAPImplementationFreeIPA is the string for the FreeIPA LDAP implementation. // LDAPImplementationFreeIPA is the string for the FreeIPA LDAP implementation.
LDAPImplementationFreeIPA = "freeipa" LDAPImplementationFreeIPA = "freeipa"
// LDAPImplementationLLDAP is the string for the lldap LDAP implementation.
LDAPImplementationLLDAP = "lldap"
) )
// TOTP Algorithm. // TOTP Algorithm.

View File

@ -330,6 +330,8 @@ func validateLDAPAuthenticationBackend(config *schema.AuthenticationBackend, val
implementation = &schema.DefaultLDAPAuthenticationBackendConfigurationImplementationActiveDirectory implementation = &schema.DefaultLDAPAuthenticationBackendConfigurationImplementationActiveDirectory
case schema.LDAPImplementationFreeIPA: case schema.LDAPImplementationFreeIPA:
implementation = &schema.DefaultLDAPAuthenticationBackendConfigurationImplementationFreeIPA implementation = &schema.DefaultLDAPAuthenticationBackendConfigurationImplementationFreeIPA
case schema.LDAPImplementationLLDAP:
implementation = &schema.DefaultLDAPAuthenticationBackendConfigurationImplementationLLDAP
default: default:
validator.Push(fmt.Errorf(errFmtLDAPAuthBackendImplementation, config.LDAP.Implementation, strings.Join(validLDAPImplementations, "', '"))) validator.Push(fmt.Errorf(errFmtLDAPAuthBackendImplementation, config.LDAP.Implementation, strings.Join(validLDAPImplementations, "', '")))
} }
@ -383,6 +385,14 @@ func ldapImplementationShouldSetStr(config, implementation string) bool {
} }
func setDefaultImplementationLDAPAuthenticationBackendProfileAttributes(config *schema.LDAPAuthenticationBackend, implementation *schema.LDAPAuthenticationBackend) { func setDefaultImplementationLDAPAuthenticationBackendProfileAttributes(config *schema.LDAPAuthenticationBackend, implementation *schema.LDAPAuthenticationBackend) {
if ldapImplementationShouldSetStr(config.AdditionalUsersDN, implementation.AdditionalUsersDN) {
config.AdditionalUsersDN = implementation.AdditionalUsersDN
}
if ldapImplementationShouldSetStr(config.AdditionalGroupsDN, implementation.AdditionalGroupsDN) {
config.AdditionalGroupsDN = implementation.AdditionalGroupsDN
}
if ldapImplementationShouldSetStr(config.UsersFilter, implementation.UsersFilter) { if ldapImplementationShouldSetStr(config.UsersFilter, implementation.UsersFilter) {
config.UsersFilter = implementation.UsersFilter config.UsersFilter = implementation.UsersFilter
} }

View File

@ -609,7 +609,7 @@ func (suite *LDAPAuthenticationBackendSuite) TestShouldRaiseErrorWhenImplementat
suite.Assert().Len(suite.validator.Warnings(), 0) suite.Assert().Len(suite.validator.Warnings(), 0)
suite.Require().Len(suite.validator.Errors(), 1) suite.Require().Len(suite.validator.Errors(), 1)
suite.Assert().EqualError(suite.validator.Errors()[0], "authentication_backend: ldap: option 'implementation' is configured as 'masd' but must be one of the following values: 'custom', 'activedirectory', 'freeipa'") suite.Assert().EqualError(suite.validator.Errors()[0], "authentication_backend: ldap: option 'implementation' is configured as 'masd' but must be one of the following values: 'custom', 'activedirectory', 'freeipa', 'lldap'")
} }
func (suite *LDAPAuthenticationBackendSuite) TestShouldRaiseErrorWhenURLNotProvided() { func (suite *LDAPAuthenticationBackendSuite) TestShouldRaiseErrorWhenURLNotProvided() {
@ -906,6 +906,12 @@ func (suite *ActiveDirectoryAuthenticationBackendSuite) TestShouldSetActiveDirec
suite.Assert().Equal( suite.Assert().Equal(
schema.DefaultLDAPAuthenticationBackendConfigurationImplementationActiveDirectory.Timeout, schema.DefaultLDAPAuthenticationBackendConfigurationImplementationActiveDirectory.Timeout,
suite.config.LDAP.Timeout) suite.config.LDAP.Timeout)
suite.Assert().Equal(
schema.DefaultLDAPAuthenticationBackendConfigurationImplementationActiveDirectory.AdditionalUsersDN,
suite.config.LDAP.AdditionalUsersDN)
suite.Assert().Equal(
schema.DefaultLDAPAuthenticationBackendConfigurationImplementationActiveDirectory.AdditionalGroupsDN,
suite.config.LDAP.AdditionalGroupsDN)
suite.Assert().Equal( suite.Assert().Equal(
schema.DefaultLDAPAuthenticationBackendConfigurationImplementationActiveDirectory.UsersFilter, schema.DefaultLDAPAuthenticationBackendConfigurationImplementationActiveDirectory.UsersFilter,
suite.config.LDAP.UsersFilter) suite.config.LDAP.UsersFilter)
@ -934,12 +940,20 @@ func (suite *ActiveDirectoryAuthenticationBackendSuite) TestShouldOnlySetDefault
suite.config.LDAP.DisplayNameAttribute = "name" suite.config.LDAP.DisplayNameAttribute = "name"
suite.config.LDAP.GroupsFilter = "(&(member={dn})(objectClass=group)(objectCategory=group))" suite.config.LDAP.GroupsFilter = "(&(member={dn})(objectClass=group)(objectCategory=group))"
suite.config.LDAP.GroupNameAttribute = "distinguishedName" suite.config.LDAP.GroupNameAttribute = "distinguishedName"
suite.config.LDAP.AdditionalUsersDN = "OU=test"
suite.config.LDAP.AdditionalGroupsDN = "OU=grps"
ValidateAuthenticationBackend(&suite.config, suite.validator) ValidateAuthenticationBackend(&suite.config, suite.validator)
suite.Assert().NotEqual( suite.Assert().NotEqual(
schema.DefaultLDAPAuthenticationBackendConfigurationImplementationActiveDirectory.Timeout, schema.DefaultLDAPAuthenticationBackendConfigurationImplementationActiveDirectory.Timeout,
suite.config.LDAP.Timeout) suite.config.LDAP.Timeout)
suite.Assert().NotEqual(
schema.DefaultLDAPAuthenticationBackendConfigurationImplementationActiveDirectory.AdditionalUsersDN,
suite.config.LDAP.AdditionalUsersDN)
suite.Assert().NotEqual(
schema.DefaultLDAPAuthenticationBackendConfigurationImplementationActiveDirectory.AdditionalGroupsDN,
suite.config.LDAP.AdditionalGroupsDN)
suite.Assert().NotEqual( suite.Assert().NotEqual(
schema.DefaultLDAPAuthenticationBackendConfigurationImplementationActiveDirectory.UsersFilter, schema.DefaultLDAPAuthenticationBackendConfigurationImplementationActiveDirectory.UsersFilter,
suite.config.LDAP.UsersFilter) suite.config.LDAP.UsersFilter)
@ -1009,6 +1023,12 @@ func (suite *FreeIPAAuthenticationBackendSuite) TestShouldSetDefaults() {
suite.Assert().Equal( suite.Assert().Equal(
schema.DefaultLDAPAuthenticationBackendConfigurationImplementationFreeIPA.Timeout, schema.DefaultLDAPAuthenticationBackendConfigurationImplementationFreeIPA.Timeout,
suite.config.LDAP.Timeout) suite.config.LDAP.Timeout)
suite.Assert().Equal(
schema.DefaultLDAPAuthenticationBackendConfigurationImplementationFreeIPA.AdditionalUsersDN,
suite.config.LDAP.AdditionalUsersDN)
suite.Assert().Equal(
schema.DefaultLDAPAuthenticationBackendConfigurationImplementationFreeIPA.AdditionalGroupsDN,
suite.config.LDAP.AdditionalGroupsDN)
suite.Assert().Equal( suite.Assert().Equal(
schema.DefaultLDAPAuthenticationBackendConfigurationImplementationFreeIPA.UsersFilter, schema.DefaultLDAPAuthenticationBackendConfigurationImplementationFreeIPA.UsersFilter,
suite.config.LDAP.UsersFilter) suite.config.LDAP.UsersFilter)
@ -1037,12 +1057,20 @@ func (suite *FreeIPAAuthenticationBackendSuite) TestShouldOnlySetDefaultsIfNotMa
suite.config.LDAP.DisplayNameAttribute = "gecos" suite.config.LDAP.DisplayNameAttribute = "gecos"
suite.config.LDAP.GroupsFilter = "(&(member={dn})(objectClass=posixgroup))" suite.config.LDAP.GroupsFilter = "(&(member={dn})(objectClass=posixgroup))"
suite.config.LDAP.GroupNameAttribute = "groupName" suite.config.LDAP.GroupNameAttribute = "groupName"
suite.config.LDAP.AdditionalUsersDN = "OU=people"
suite.config.LDAP.AdditionalGroupsDN = "OU=grp"
ValidateAuthenticationBackend(&suite.config, suite.validator) ValidateAuthenticationBackend(&suite.config, suite.validator)
suite.Assert().NotEqual( suite.Assert().NotEqual(
schema.DefaultLDAPAuthenticationBackendConfigurationImplementationFreeIPA.Timeout, schema.DefaultLDAPAuthenticationBackendConfigurationImplementationFreeIPA.Timeout,
suite.config.LDAP.Timeout) suite.config.LDAP.Timeout)
suite.Assert().NotEqual(
schema.DefaultLDAPAuthenticationBackendConfigurationImplementationFreeIPA.AdditionalUsersDN,
suite.config.LDAP.AdditionalUsersDN)
suite.Assert().NotEqual(
schema.DefaultLDAPAuthenticationBackendConfigurationImplementationFreeIPA.AdditionalGroupsDN,
suite.config.LDAP.AdditionalGroupsDN)
suite.Assert().NotEqual( suite.Assert().NotEqual(
schema.DefaultLDAPAuthenticationBackendConfigurationImplementationFreeIPA.UsersFilter, schema.DefaultLDAPAuthenticationBackendConfigurationImplementationFreeIPA.UsersFilter,
suite.config.LDAP.UsersFilter) suite.config.LDAP.UsersFilter)
@ -1066,3 +1094,105 @@ func (suite *FreeIPAAuthenticationBackendSuite) TestShouldOnlySetDefaultsIfNotMa
func TestFreeIPAAuthenticationBackend(t *testing.T) { func TestFreeIPAAuthenticationBackend(t *testing.T) {
suite.Run(t, new(FreeIPAAuthenticationBackendSuite)) suite.Run(t, new(FreeIPAAuthenticationBackendSuite))
} }
type LLDAPAuthenticationBackendSuite struct {
suite.Suite
config schema.AuthenticationBackend
validator *schema.StructValidator
}
func (suite *LLDAPAuthenticationBackendSuite) SetupTest() {
suite.validator = schema.NewStructValidator()
suite.config = schema.AuthenticationBackend{}
suite.config.LDAP = &schema.LDAPAuthenticationBackend{}
suite.config.LDAP.Implementation = schema.LDAPImplementationLLDAP
suite.config.LDAP.URL = testLDAPURL
suite.config.LDAP.User = testLDAPUser
suite.config.LDAP.Password = testLDAPPassword
suite.config.LDAP.BaseDN = testLDAPBaseDN
suite.config.LDAP.TLS = schema.DefaultLDAPAuthenticationBackendConfigurationImplementationLLDAP.TLS
}
func (suite *LLDAPAuthenticationBackendSuite) TestShouldSetDefaults() {
ValidateAuthenticationBackend(&suite.config, suite.validator)
suite.Assert().Len(suite.validator.Warnings(), 0)
suite.Assert().Len(suite.validator.Errors(), 0)
suite.Assert().Equal(
schema.DefaultLDAPAuthenticationBackendConfigurationImplementationLLDAP.Timeout,
suite.config.LDAP.Timeout)
suite.Assert().Equal(
schema.DefaultLDAPAuthenticationBackendConfigurationImplementationLLDAP.AdditionalUsersDN,
suite.config.LDAP.AdditionalUsersDN)
suite.Assert().Equal(
schema.DefaultLDAPAuthenticationBackendConfigurationImplementationLLDAP.AdditionalGroupsDN,
suite.config.LDAP.AdditionalGroupsDN)
suite.Assert().Equal(
schema.DefaultLDAPAuthenticationBackendConfigurationImplementationLLDAP.UsersFilter,
suite.config.LDAP.UsersFilter)
suite.Assert().Equal(
schema.DefaultLDAPAuthenticationBackendConfigurationImplementationLLDAP.UsernameAttribute,
suite.config.LDAP.UsernameAttribute)
suite.Assert().Equal(
schema.DefaultLDAPAuthenticationBackendConfigurationImplementationLLDAP.DisplayNameAttribute,
suite.config.LDAP.DisplayNameAttribute)
suite.Assert().Equal(
schema.DefaultLDAPAuthenticationBackendConfigurationImplementationLLDAP.MailAttribute,
suite.config.LDAP.MailAttribute)
suite.Assert().Equal(
schema.DefaultLDAPAuthenticationBackendConfigurationImplementationLLDAP.GroupsFilter,
suite.config.LDAP.GroupsFilter)
suite.Assert().Equal(
schema.DefaultLDAPAuthenticationBackendConfigurationImplementationLLDAP.GroupNameAttribute,
suite.config.LDAP.GroupNameAttribute)
}
func (suite *LLDAPAuthenticationBackendSuite) TestShouldOnlySetDefaultsIfNotManuallyConfigured() {
suite.config.LDAP.Timeout = time.Second * 2
suite.config.LDAP.UsersFilter = "(&({username_attribute}={input})(objectClass=Person)(!(nsAccountLock=TRUE)))"
suite.config.LDAP.UsernameAttribute = "username"
suite.config.LDAP.MailAttribute = "m"
suite.config.LDAP.DisplayNameAttribute = "given"
suite.config.LDAP.GroupsFilter = "(&(member={dn})(objectClass=posixGroup))"
suite.config.LDAP.GroupNameAttribute = "grp"
suite.config.LDAP.AdditionalUsersDN = "OU=no"
suite.config.LDAP.AdditionalGroupsDN = "OU=yes"
ValidateAuthenticationBackend(&suite.config, suite.validator)
suite.Assert().NotEqual(
schema.DefaultLDAPAuthenticationBackendConfigurationImplementationLLDAP.Timeout,
suite.config.LDAP.Timeout)
suite.Assert().NotEqual(
schema.DefaultLDAPAuthenticationBackendConfigurationImplementationLLDAP.AdditionalUsersDN,
suite.config.LDAP.AdditionalUsersDN)
suite.Assert().NotEqual(
schema.DefaultLDAPAuthenticationBackendConfigurationImplementationLLDAP.AdditionalGroupsDN,
suite.config.LDAP.AdditionalGroupsDN)
suite.Assert().NotEqual(
schema.DefaultLDAPAuthenticationBackendConfigurationImplementationLLDAP.Timeout,
suite.config.LDAP.Timeout)
suite.Assert().NotEqual(
schema.DefaultLDAPAuthenticationBackendConfigurationImplementationLLDAP.UsersFilter,
suite.config.LDAP.UsersFilter)
suite.Assert().NotEqual(
schema.DefaultLDAPAuthenticationBackendConfigurationImplementationLLDAP.UsernameAttribute,
suite.config.LDAP.UsernameAttribute)
suite.Assert().NotEqual(
schema.DefaultLDAPAuthenticationBackendConfigurationImplementationLLDAP.DisplayNameAttribute,
suite.config.LDAP.DisplayNameAttribute)
suite.Assert().NotEqual(
schema.DefaultLDAPAuthenticationBackendConfigurationImplementationLLDAP.MailAttribute,
suite.config.LDAP.MailAttribute)
suite.Assert().NotEqual(
schema.DefaultLDAPAuthenticationBackendConfigurationImplementationLLDAP.GroupsFilter,
suite.config.LDAP.GroupsFilter)
suite.Assert().NotEqual(
schema.DefaultLDAPAuthenticationBackendConfigurationImplementationLLDAP.GroupNameAttribute,
suite.config.LDAP.GroupNameAttribute)
}
func TestLLDAPAuthenticationBackend(t *testing.T) {
suite.Run(t, new(LLDAPAuthenticationBackendSuite))
}

View File

@ -323,7 +323,7 @@ const (
) )
var ( var (
validLDAPImplementations = []string{schema.LDAPImplementationCustom, schema.LDAPImplementationActiveDirectory, schema.LDAPImplementationFreeIPA} validLDAPImplementations = []string{schema.LDAPImplementationCustom, schema.LDAPImplementationActiveDirectory, schema.LDAPImplementationFreeIPA, schema.LDAPImplementationLLDAP}
) )
var ( var (