From d0d80b4f6689df1ac441015bd43ad9c268faad4f Mon Sep 17 00:00:00 2001 From: James Elliott Date: Wed, 21 Dec 2022 21:07:00 +1100 Subject: [PATCH] feat(configuration): freeipa ldap implementation (#4482) This adds a FreeIPA LDAP implementation which purely adds sane defaults for FreeIPA. There are no functional differences just when the implementation option is set to 'freeipa' 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. Closes #2177, Closes #2161 --- config.template.yml | 5 +- docs/content/en/reference/guides/ldap.md | 12 ++- internal/configuration/config.template.yml | 5 +- .../configuration/schema/authentication.go | 30 ++++-- internal/configuration/schema/const.go | 10 ++ .../configuration/validator/authentication.go | 4 +- .../validator/authentication_test.go | 95 ++++++++++++++++++- internal/configuration/validator/const.go | 51 +++++----- internal/configuration/validator/log.go | 4 +- 9 files changed, 166 insertions(+), 50 deletions(-) diff --git a/config.template.yml b/config.template.yml index 3cb29ac83..1ee495844 100644 --- a/config.template.yml +++ b/config.template.yml @@ -284,8 +284,9 @@ authentication_backend: # ldap: ## The LDAP implementation, this affects elements like the attribute utilised for resetting a password. ## Acceptable options are as follows: - ## - 'activedirectory' - For Microsoft Active Directory. - ## - 'custom' - For custom specifications of attributes and filters. + ## - 'activedirectory' - for Microsoft Active Directory. + ## - 'freeipa' - for FreeIPA. + ## - 'custom' - for custom specifications of attributes and filters. ## This currently defaults to 'custom' to maintain existing behaviour. ## ## Depending on the option here certain other values in this section have a default value, notably all of the diff --git a/docs/content/en/reference/guides/ldap.md b/docs/content/en/reference/guides/ldap.md index 97d9281f6..523cc258e 100644 --- a/docs/content/en/reference/guides/ldap.md +++ b/docs/content/en/reference/guides/ldap.md @@ -10,6 +10,8 @@ menu: parent: "guides" weight: 220 toc: true +aliases: + - /r/ldap --- ## Binding @@ -46,10 +48,10 @@ Authelia primarily supports this method. ## Implementation Guide -There are currently two implementations, `custom` and `activedirectory`. 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 for this, and an input format other implementations do not use. The long term -intention of this is to have logical defaults for various RFC implementations of LDAP. +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. ### Filter replacements @@ -86,6 +88,7 @@ Username column. |:---------------:|:--------------:|:------------:|:----:|:----------:| | custom | N/A | displayName | mail | cn | | activedirectory | sAMAccountName | displayName | mail | cn | +| freeipa | uid | displayName | mail | cn | #### Filter defaults @@ -98,6 +101,7 @@ value is not 0 which means the password requires changing at the next login. |:---------------:|:---------------------------------------------------------------------------------------------------------------------------------------------------------------:|:----------------------------------------------------------------------------:| | 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))) | (&(member={dn})(|(sAMAccountType=268435456)(sAMAccountType=536870912))) | +| freeipa | (&(|({username_attribute}={input})({mail_attribute}={input}))(objectClass=person)(!(nsAccountLock=TRUE))) | (&(member={dn})(objectClass=groupOfNames)) | ##### Microsoft Active Directory sAMAccountType diff --git a/internal/configuration/config.template.yml b/internal/configuration/config.template.yml index 3cb29ac83..1ee495844 100644 --- a/internal/configuration/config.template.yml +++ b/internal/configuration/config.template.yml @@ -284,8 +284,9 @@ authentication_backend: # ldap: ## The LDAP implementation, this affects elements like the attribute utilised for resetting a password. ## Acceptable options are as follows: - ## - 'activedirectory' - For Microsoft Active Directory. - ## - 'custom' - For custom specifications of attributes and filters. + ## - 'activedirectory' - for Microsoft Active Directory. + ## - 'freeipa' - for FreeIPA. + ## - 'custom' - for custom specifications of attributes and filters. ## This currently defaults to 'custom' to maintain existing behaviour. ## ## Depending on the option here certain other values in this section have a default value, notably all of the diff --git a/internal/configuration/schema/authentication.go b/internal/configuration/schema/authentication.go index 5eeba76b0..1d7bf181b 100644 --- a/internal/configuration/schema/authentication.go +++ b/internal/configuration/schema/authentication.go @@ -175,24 +175,38 @@ var DefaultCIPasswordConfig = Password{ // DefaultLDAPAuthenticationBackendConfigurationImplementationCustom represents the default LDAP config. var DefaultLDAPAuthenticationBackendConfigurationImplementationCustom = LDAPAuthenticationBackend{ - UsernameAttribute: "uid", - MailAttribute: "mail", - DisplayNameAttribute: "displayName", - GroupNameAttribute: "cn", + UsernameAttribute: ldapAttrUserID, + MailAttribute: ldapAttrMail, + DisplayNameAttribute: ldapAttrDisplayName, + GroupNameAttribute: ldapAttrCommonName, Timeout: time.Second * 5, TLS: &TLSConfig{ MinimumVersion: TLSVersion{tls.VersionTLS12}, }, } -// DefaultLDAPAuthenticationBackendConfigurationImplementationActiveDirectory represents the default LDAP config for the MSAD Implementation. +// DefaultLDAPAuthenticationBackendConfigurationImplementationActiveDirectory represents the default LDAP config for the LDAPImplementationActiveDirectory Implementation. var DefaultLDAPAuthenticationBackendConfigurationImplementationActiveDirectory = LDAPAuthenticationBackend{ UsersFilter: "(&(|({username_attribute}={input})({mail_attribute}={input}))(sAMAccountType=805306368)(!(userAccountControl:1.2.840.113556.1.4.803:=2))(!(pwdLastSet=0)))", UsernameAttribute: "sAMAccountName", - MailAttribute: "mail", - DisplayNameAttribute: "displayName", + MailAttribute: ldapAttrMail, + DisplayNameAttribute: ldapAttrDisplayName, GroupsFilter: "(&(member={dn})(|(sAMAccountType=268435456)(sAMAccountType=536870912)))", - GroupNameAttribute: "cn", + GroupNameAttribute: ldapAttrCommonName, + Timeout: time.Second * 5, + TLS: &TLSConfig{ + MinimumVersion: TLSVersion{tls.VersionTLS12}, + }, +} + +// DefaultLDAPAuthenticationBackendConfigurationImplementationFreeIPA represents the default LDAP config for the LDAPImplementationFreeIPA Implementation. +var DefaultLDAPAuthenticationBackendConfigurationImplementationFreeIPA = LDAPAuthenticationBackend{ + UsersFilter: "(&(|({username_attribute}={input})({mail_attribute}={input}))(objectClass=person)(!(nsAccountLock=TRUE)))", + UsernameAttribute: ldapAttrUserID, + MailAttribute: ldapAttrMail, + DisplayNameAttribute: ldapAttrDisplayName, + GroupsFilter: "(&(member={dn})(objectClass=groupOfNames))", + 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 3015c1527..378473bfc 100644 --- a/internal/configuration/schema/const.go +++ b/internal/configuration/schema/const.go @@ -64,6 +64,9 @@ const ( // LDAPImplementationActiveDirectory is the string for the Active Directory LDAP implementation. LDAPImplementationActiveDirectory = "activedirectory" + + // LDAPImplementationFreeIPA is the string for the FreeIPA LDAP implementation. + LDAPImplementationFreeIPA = "freeipa" ) // TOTP Algorithm. @@ -99,3 +102,10 @@ const ( blockCERTIFICATE = "CERTIFICATE" blockRSAPRIVATEKEY = "RSA PRIVATE KEY" ) + +const ( + ldapAttrMail = "mail" + ldapAttrUserID = "uid" + ldapAttrDisplayName = "displayName" + ldapAttrCommonName = "cn" +) diff --git a/internal/configuration/validator/authentication.go b/internal/configuration/validator/authentication.go index 5aa74bba6..158513655 100644 --- a/internal/configuration/validator/authentication.go +++ b/internal/configuration/validator/authentication.go @@ -328,8 +328,10 @@ func validateLDAPAuthenticationBackend(config *schema.AuthenticationBackend, val implementation = &schema.DefaultLDAPAuthenticationBackendConfigurationImplementationCustom case schema.LDAPImplementationActiveDirectory: implementation = &schema.DefaultLDAPAuthenticationBackendConfigurationImplementationActiveDirectory + case schema.LDAPImplementationFreeIPA: + implementation = &schema.DefaultLDAPAuthenticationBackendConfigurationImplementationFreeIPA default: - validator.Push(fmt.Errorf(errFmtLDAPAuthBackendImplementation, config.LDAP.Implementation, strings.Join([]string{schema.LDAPImplementationCustom, schema.LDAPImplementationActiveDirectory}, "', '"))) + validator.Push(fmt.Errorf(errFmtLDAPAuthBackendImplementation, config.LDAP.Implementation, strings.Join(validLDAPImplementations, "', '"))) } configDefaultTLS := &schema.TLSConfig{} diff --git a/internal/configuration/validator/authentication_test.go b/internal/configuration/validator/authentication_test.go index 61482b53a..53a0745ea 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'") + 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'") } func (suite *LDAPAuthenticationBackendSuite) TestShouldRaiseErrorWhenURLNotProvided() { @@ -875,7 +875,7 @@ func (suite *LDAPAuthenticationBackendSuite) TestShouldNotAllowTLSVerMinGreaterT suite.Assert().EqualError(suite.validator.Errors()[0], "authentication_backend: ldap: tls: option combination of 'minimum_version' and 'maximum_version' is invalid: minimum version TLS1.3 is greater than the maximum version TLS1.2") } -func TestLdapAuthenticationBackend(t *testing.T) { +func TestLDAPAuthenticationBackend(t *testing.T) { suite.Run(t, new(LDAPAuthenticationBackendSuite)) } @@ -894,7 +894,7 @@ func (suite *ActiveDirectoryAuthenticationBackendSuite) SetupTest() { suite.config.LDAP.User = testLDAPUser suite.config.LDAP.Password = testLDAPPassword suite.config.LDAP.BaseDN = testLDAPBaseDN - suite.config.LDAP.TLS = schema.DefaultLDAPAuthenticationBackendConfigurationImplementationCustom.TLS + suite.config.LDAP.TLS = schema.DefaultLDAPAuthenticationBackendConfigurationImplementationActiveDirectory.TLS } func (suite *ActiveDirectoryAuthenticationBackendSuite) TestShouldSetActiveDirectoryDefaults() { @@ -904,7 +904,7 @@ func (suite *ActiveDirectoryAuthenticationBackendSuite) TestShouldSetActiveDirec suite.Assert().Len(suite.validator.Errors(), 0) suite.Assert().Equal( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationCustom.Timeout, + schema.DefaultLDAPAuthenticationBackendConfigurationImplementationActiveDirectory.Timeout, suite.config.LDAP.Timeout) suite.Assert().Equal( schema.DefaultLDAPAuthenticationBackendConfigurationImplementationActiveDirectory.UsersFilter, @@ -938,7 +938,7 @@ func (suite *ActiveDirectoryAuthenticationBackendSuite) TestShouldOnlySetDefault ValidateAuthenticationBackend(&suite.config, suite.validator) suite.Assert().NotEqual( - schema.DefaultLDAPAuthenticationBackendConfigurationImplementationCustom.Timeout, + schema.DefaultLDAPAuthenticationBackendConfigurationImplementationActiveDirectory.Timeout, suite.config.LDAP.Timeout) suite.Assert().NotEqual( schema.DefaultLDAPAuthenticationBackendConfigurationImplementationActiveDirectory.UsersFilter, @@ -981,3 +981,88 @@ func (suite *ActiveDirectoryAuthenticationBackendSuite) TestShouldRaiseErrorOnIn func TestActiveDirectoryAuthenticationBackend(t *testing.T) { suite.Run(t, new(ActiveDirectoryAuthenticationBackendSuite)) } + +type FreeIPAAuthenticationBackendSuite struct { + suite.Suite + config schema.AuthenticationBackend + validator *schema.StructValidator +} + +func (suite *FreeIPAAuthenticationBackendSuite) SetupTest() { + suite.validator = schema.NewStructValidator() + suite.config = schema.AuthenticationBackend{} + suite.config.LDAP = &schema.LDAPAuthenticationBackend{} + suite.config.LDAP.Implementation = schema.LDAPImplementationFreeIPA + 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.DefaultLDAPAuthenticationBackendConfigurationImplementationFreeIPA.TLS +} + +func (suite *FreeIPAAuthenticationBackendSuite) TestShouldSetDefaults() { + ValidateAuthenticationBackend(&suite.config, suite.validator) + + suite.Assert().Len(suite.validator.Warnings(), 0) + suite.Assert().Len(suite.validator.Errors(), 0) + + suite.Assert().Equal( + schema.DefaultLDAPAuthenticationBackendConfigurationImplementationFreeIPA.Timeout, + suite.config.LDAP.Timeout) + suite.Assert().Equal( + schema.DefaultLDAPAuthenticationBackendConfigurationImplementationFreeIPA.UsersFilter, + suite.config.LDAP.UsersFilter) + suite.Assert().Equal( + schema.DefaultLDAPAuthenticationBackendConfigurationImplementationFreeIPA.UsernameAttribute, + suite.config.LDAP.UsernameAttribute) + suite.Assert().Equal( + schema.DefaultLDAPAuthenticationBackendConfigurationImplementationFreeIPA.DisplayNameAttribute, + suite.config.LDAP.DisplayNameAttribute) + suite.Assert().Equal( + schema.DefaultLDAPAuthenticationBackendConfigurationImplementationFreeIPA.MailAttribute, + suite.config.LDAP.MailAttribute) + suite.Assert().Equal( + schema.DefaultLDAPAuthenticationBackendConfigurationImplementationFreeIPA.GroupsFilter, + suite.config.LDAP.GroupsFilter) + suite.Assert().Equal( + schema.DefaultLDAPAuthenticationBackendConfigurationImplementationFreeIPA.GroupNameAttribute, + suite.config.LDAP.GroupNameAttribute) +} + +func (suite *FreeIPAAuthenticationBackendSuite) TestShouldOnlySetDefaultsIfNotManuallyConfigured() { + suite.config.LDAP.Timeout = time.Second * 2 + suite.config.LDAP.UsersFilter = "(&({username_attribute}={input})(objectClass=person)(!(nsAccountLock=TRUE)))" + suite.config.LDAP.UsernameAttribute = "dn" + suite.config.LDAP.MailAttribute = "email" + suite.config.LDAP.DisplayNameAttribute = "gecos" + suite.config.LDAP.GroupsFilter = "(&(member={dn})(objectClass=posixgroup))" + suite.config.LDAP.GroupNameAttribute = "groupName" + + ValidateAuthenticationBackend(&suite.config, suite.validator) + + suite.Assert().NotEqual( + schema.DefaultLDAPAuthenticationBackendConfigurationImplementationFreeIPA.Timeout, + suite.config.LDAP.Timeout) + suite.Assert().NotEqual( + schema.DefaultLDAPAuthenticationBackendConfigurationImplementationFreeIPA.UsersFilter, + suite.config.LDAP.UsersFilter) + suite.Assert().NotEqual( + schema.DefaultLDAPAuthenticationBackendConfigurationImplementationFreeIPA.UsernameAttribute, + suite.config.LDAP.UsernameAttribute) + suite.Assert().NotEqual( + schema.DefaultLDAPAuthenticationBackendConfigurationImplementationFreeIPA.DisplayNameAttribute, + suite.config.LDAP.DisplayNameAttribute) + suite.Assert().NotEqual( + schema.DefaultLDAPAuthenticationBackendConfigurationImplementationFreeIPA.MailAttribute, + suite.config.LDAP.MailAttribute) + suite.Assert().NotEqual( + schema.DefaultLDAPAuthenticationBackendConfigurationImplementationFreeIPA.GroupsFilter, + suite.config.LDAP.GroupsFilter) + suite.Assert().NotEqual( + schema.DefaultLDAPAuthenticationBackendConfigurationImplementationFreeIPA.GroupNameAttribute, + suite.config.LDAP.GroupNameAttribute) +} + +func TestFreeIPAAuthenticationBackend(t *testing.T) { + suite.Run(t, new(FreeIPAAuthenticationBackendSuite)) +} diff --git a/internal/configuration/validator/const.go b/internal/configuration/validator/const.go index 63e618bc0..19e48e84a 100644 --- a/internal/configuration/validator/const.go +++ b/internal/configuration/validator/const.go @@ -5,6 +5,8 @@ import ( "github.com/go-webauthn/webauthn/protocol" + "github.com/authelia/authelia/v4/internal/configuration/schema" + "github.com/authelia/authelia/v4/internal/oidc" ) @@ -311,32 +313,6 @@ const ( errFilePOptions = "config key incorrect: authentication_backend.file.password_options should be authentication_backend.file.password" ) -var validArgon2Variants = []string{"argon2id", "id", "argon2i", "i", "argon2d", "d"} - -var validSHA2CryptVariants = []string{digestSHA256, digestSHA512} - -var validPBKDF2Variants = []string{digestSHA1, digestSHA224, digestSHA256, digestSHA384, digestSHA512} - -var validBCryptVariants = []string{"standard", digestSHA256} - -var validHashAlgorithms = []string{hashSHA2Crypt, hashPBKDF2, hashSCrypt, hashBCrypt, hashArgon2} - -var validStoragePostgreSQLSSLModes = []string{"disable", "require", "verify-ca", "verify-full"} - -var validThemeNames = []string{"light", "dark", "grey", "auto"} - -var validSessionSameSiteValues = []string{"none", "lax", "strict"} - -var validLoLevels = []string{"trace", "debug", "info", "warn", "error"} - -var validWebauthnConveyancePreferences = []string{string(protocol.PreferNoAttestation), string(protocol.PreferIndirectAttestation), string(protocol.PreferDirectAttestation)} - -var validWebauthnUserVerificationRequirement = []string{string(protocol.VerificationDiscouraged), string(protocol.VerificationPreferred), string(protocol.VerificationRequired)} - -var validRFC7231HTTPMethodVerbs = []string{"GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "TRACE", "CONNECT", "OPTIONS"} - -var validRFC4918HTTPMethodVerbs = []string{"COPY", "LOCK", "MKCOL", "MOVE", "PROPFIND", "PROPPATCH", "UNLOCK"} - const ( operatorPresent = "present" operatorAbsent = "absent" @@ -346,6 +322,29 @@ const ( operatorNotPattern = "not pattern" ) +var ( + validLDAPImplementations = []string{schema.LDAPImplementationCustom, schema.LDAPImplementationActiveDirectory, schema.LDAPImplementationFreeIPA} +) + +var ( + validArgon2Variants = []string{"argon2id", "id", "argon2i", "i", "argon2d", "d"} + validSHA2CryptVariants = []string{digestSHA256, digestSHA512} + validPBKDF2Variants = []string{digestSHA1, digestSHA224, digestSHA256, digestSHA384, digestSHA512} + validBCryptVariants = []string{"standard", digestSHA256} + validHashAlgorithms = []string{hashSHA2Crypt, hashPBKDF2, hashSCrypt, hashBCrypt, hashArgon2} +) + +var ( + validStoragePostgreSQLSSLModes = []string{"disable", "require", "verify-ca", "verify-full"} + validThemeNames = []string{"light", "dark", "grey", "auto"} + validSessionSameSiteValues = []string{"none", "lax", "strict"} + validLogLevels = []string{"trace", "debug", "info", "warn", "error"} + validWebauthnConveyancePreferences = []string{string(protocol.PreferNoAttestation), string(protocol.PreferIndirectAttestation), string(protocol.PreferDirectAttestation)} + validWebauthnUserVerificationRequirement = []string{string(protocol.VerificationDiscouraged), string(protocol.VerificationPreferred), string(protocol.VerificationRequired)} + validRFC7231HTTPMethodVerbs = []string{"GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "TRACE", "CONNECT", "OPTIONS"} + validRFC4918HTTPMethodVerbs = []string{"COPY", "LOCK", "MKCOL", "MOVE", "PROPFIND", "PROPPATCH", "UNLOCK"} +) + var ( validACLHTTPMethodVerbs = append(validRFC7231HTTPMethodVerbs, validRFC4918HTTPMethodVerbs...) validACLRulePolicies = []string{policyBypass, policyOneFactor, policyTwoFactor, policyDeny} diff --git a/internal/configuration/validator/log.go b/internal/configuration/validator/log.go index 1522f6804..5c7a0761b 100644 --- a/internal/configuration/validator/log.go +++ b/internal/configuration/validator/log.go @@ -18,7 +18,7 @@ func ValidateLog(config *schema.Configuration, validator *schema.StructValidator config.Log.Format = schema.DefaultLoggingConfiguration.Format } - if !utils.IsStringInSlice(config.Log.Level, validLoLevels) { - validator.Push(fmt.Errorf(errFmtLoggingLevelInvalid, strings.Join(validLoLevels, "', '"), config.Log.Level)) + if !utils.IsStringInSlice(config.Log.Level, validLogLevels) { + validator.Push(fmt.Errorf(errFmtLoggingLevelInvalid, strings.Join(validLogLevels, "', '"), config.Log.Level)) } }