diff --git a/config.template.yml b/config.template.yml index 1ee495844..7764dc4dc 100644 --- a/config.template.yml +++ b/config.template.yml @@ -286,6 +286,7 @@ authentication_backend: ## Acceptable options are as follows: ## - 'activedirectory' - for Microsoft Active Directory. ## - 'freeipa' - for FreeIPA. + ## - 'lldap' - for lldap. ## - 'custom' - for custom specifications of attributes and filters. ## This currently defaults to 'custom' to maintain existing behaviour. ## diff --git a/docs/content/en/reference/guides/ldap.md b/docs/content/en/reference/guides/ldap.md index 0d38b709d..d6ccec9b8 100644 --- a/docs/content/en/reference/guides/ldap.md +++ b/docs/content/en/reference/guides/ldap.md @@ -48,10 +48,24 @@ Authelia primarily supports this method. ## Implementation Guide -There are currently two implementations, `custom`, `activedirectory`, and `freeipa`. The `activedirectory` -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 -RFC implementations of LDAP. +The following implementations exist: + +- `custom`: + - 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 @@ -60,15 +74,15 @@ search. #### Users filter replacements -| Placeholder | Phase | Replacement | -|:-------------------------:|:-------:|:--------------------------------------------------------------------------------------------------------------:| -| {username_attribute} | startup | The configured username attribute | -| {mail_attribute} | startup | The configured mail attribute | -| {display_name_attribute} | startup | The configured display name attribute | -| {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: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 | +| Placeholder | Phase | Replacement | +|:-------------------------:|:-------:|:----------------------------------------------------------------------------------------------------------------:| +| {username_attribute} | startup | The configured username attribute | +| {mail_attribute} | startup | The configured mail attribute | +| {display_name_attribute} | startup | The configured display name attribute | +| {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: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 | #### Groups filter replacements @@ -82,6 +96,14 @@ search. 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 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 | | activedirectory | sAMAccountName | displayName | mail | cn | | freeipa | uid | displayName | mail | cn | +| lldap | uid | cn | mail | cn | #### Filter defaults @@ -113,6 +136,7 @@ the following conditions: | 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))) | | 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 diff --git a/internal/configuration/config.template.yml b/internal/configuration/config.template.yml index 1ee495844..7764dc4dc 100644 --- a/internal/configuration/config.template.yml +++ b/internal/configuration/config.template.yml @@ -286,6 +286,7 @@ authentication_backend: ## Acceptable options are as follows: ## - 'activedirectory' - for Microsoft Active Directory. ## - 'freeipa' - for FreeIPA. + ## - 'lldap' - for lldap. ## - 'custom' - for custom specifications of attributes and filters. ## This currently defaults to 'custom' to maintain existing behaviour. ## diff --git a/internal/configuration/schema/authentication.go b/internal/configuration/schema/authentication.go index d830071ac..98e514f4a 100644 --- a/internal/configuration/schema/authentication.go +++ b/internal/configuration/schema/authentication.go @@ -212,3 +212,19 @@ var DefaultLDAPAuthenticationBackendConfigurationImplementationFreeIPA = LDAPAut 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}, + }, +} diff --git a/internal/configuration/schema/const.go b/internal/configuration/schema/const.go index 378473bfc..1578327cc 100644 --- a/internal/configuration/schema/const.go +++ b/internal/configuration/schema/const.go @@ -67,6 +67,9 @@ const ( // LDAPImplementationFreeIPA is the string for the FreeIPA LDAP implementation. LDAPImplementationFreeIPA = "freeipa" + + // LDAPImplementationLLDAP is the string for the lldap LDAP implementation. + LDAPImplementationLLDAP = "lldap" ) // TOTP Algorithm. diff --git a/internal/configuration/validator/authentication.go b/internal/configuration/validator/authentication.go index 158513655..0106c4e37 100644 --- a/internal/configuration/validator/authentication.go +++ b/internal/configuration/validator/authentication.go @@ -330,6 +330,8 @@ func validateLDAPAuthenticationBackend(config *schema.AuthenticationBackend, val implementation = &schema.DefaultLDAPAuthenticationBackendConfigurationImplementationActiveDirectory case schema.LDAPImplementationFreeIPA: implementation = &schema.DefaultLDAPAuthenticationBackendConfigurationImplementationFreeIPA + case schema.LDAPImplementationLLDAP: + implementation = &schema.DefaultLDAPAuthenticationBackendConfigurationImplementationLLDAP default: 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) { + 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) { config.UsersFilter = implementation.UsersFilter } diff --git a/internal/configuration/validator/authentication_test.go b/internal/configuration/validator/authentication_test.go index 53a0745ea..e2305c56a 100644 --- a/internal/configuration/validator/authentication_test.go +++ b/internal/configuration/validator/authentication_test.go @@ -609,7 +609,7 @@ func (suite *LDAPAuthenticationBackendSuite) TestShouldRaiseErrorWhenImplementat suite.Assert().Len(suite.validator.Warnings(), 0) 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() { @@ -906,6 +906,12 @@ func (suite *ActiveDirectoryAuthenticationBackendSuite) TestShouldSetActiveDirec suite.Assert().Equal( schema.DefaultLDAPAuthenticationBackendConfigurationImplementationActiveDirectory.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( schema.DefaultLDAPAuthenticationBackendConfigurationImplementationActiveDirectory.UsersFilter, suite.config.LDAP.UsersFilter) @@ -934,12 +940,20 @@ func (suite *ActiveDirectoryAuthenticationBackendSuite) TestShouldOnlySetDefault suite.config.LDAP.DisplayNameAttribute = "name" suite.config.LDAP.GroupsFilter = "(&(member={dn})(objectClass=group)(objectCategory=group))" suite.config.LDAP.GroupNameAttribute = "distinguishedName" + suite.config.LDAP.AdditionalUsersDN = "OU=test" + suite.config.LDAP.AdditionalGroupsDN = "OU=grps" ValidateAuthenticationBackend(&suite.config, suite.validator) suite.Assert().NotEqual( schema.DefaultLDAPAuthenticationBackendConfigurationImplementationActiveDirectory.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( schema.DefaultLDAPAuthenticationBackendConfigurationImplementationActiveDirectory.UsersFilter, suite.config.LDAP.UsersFilter) @@ -1009,6 +1023,12 @@ func (suite *FreeIPAAuthenticationBackendSuite) TestShouldSetDefaults() { suite.Assert().Equal( schema.DefaultLDAPAuthenticationBackendConfigurationImplementationFreeIPA.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( schema.DefaultLDAPAuthenticationBackendConfigurationImplementationFreeIPA.UsersFilter, suite.config.LDAP.UsersFilter) @@ -1037,12 +1057,20 @@ func (suite *FreeIPAAuthenticationBackendSuite) TestShouldOnlySetDefaultsIfNotMa suite.config.LDAP.DisplayNameAttribute = "gecos" suite.config.LDAP.GroupsFilter = "(&(member={dn})(objectClass=posixgroup))" suite.config.LDAP.GroupNameAttribute = "groupName" + suite.config.LDAP.AdditionalUsersDN = "OU=people" + suite.config.LDAP.AdditionalGroupsDN = "OU=grp" ValidateAuthenticationBackend(&suite.config, suite.validator) suite.Assert().NotEqual( schema.DefaultLDAPAuthenticationBackendConfigurationImplementationFreeIPA.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( schema.DefaultLDAPAuthenticationBackendConfigurationImplementationFreeIPA.UsersFilter, suite.config.LDAP.UsersFilter) @@ -1066,3 +1094,105 @@ func (suite *FreeIPAAuthenticationBackendSuite) TestShouldOnlySetDefaultsIfNotMa func TestFreeIPAAuthenticationBackend(t *testing.T) { 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)) +} diff --git a/internal/configuration/validator/const.go b/internal/configuration/validator/const.go index 19e48e84a..014e33583 100644 --- a/internal/configuration/validator/const.go +++ b/internal/configuration/validator/const.go @@ -323,7 +323,7 @@ const ( ) var ( - validLDAPImplementations = []string{schema.LDAPImplementationCustom, schema.LDAPImplementationActiveDirectory, schema.LDAPImplementationFreeIPA} + validLDAPImplementations = []string{schema.LDAPImplementationCustom, schema.LDAPImplementationActiveDirectory, schema.LDAPImplementationFreeIPA, schema.LDAPImplementationLLDAP} ) var (