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)) } }