From 5b8b3145ade0f2108448b16bb9979835cbf8fef1 Mon Sep 17 00:00:00 2001 From: James Elliott Date: Wed, 21 Dec 2022 21:51:25 +1100 Subject: [PATCH] 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. --- config.template.yml | 1 + docs/content/en/reference/guides/ldap.md | 50 +++++-- internal/configuration/config.template.yml | 1 + .../configuration/schema/authentication.go | 16 +++ internal/configuration/schema/const.go | 3 + .../configuration/validator/authentication.go | 10 ++ .../validator/authentication_test.go | 132 +++++++++++++++++- internal/configuration/validator/const.go | 2 +- 8 files changed, 200 insertions(+), 15 deletions(-) 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 (