From 29a900226d8ac85fa04e11af022a9561dae9e652 Mon Sep 17 00:00:00 2001 From: James Elliott Date: Mon, 4 Jan 2021 21:28:55 +1100 Subject: [PATCH] [FEATURE] Enhance LDAP/SMTP TLS Configuration and Unify Them (#1557) * add new directive in the global scope `certificates_directory` which is used to bulk load certs and trust them in Authelia * this is in ADDITION to system certs and are trusted by both LDAP and SMTP * added a shared TLSConfig struct to be used by both SMTP and LDAP, and anything else in the future that requires tuning the TLS * remove usage of deprecated LDAP funcs Dial and DialTLS in favor of DialURL which is also easier to use * use the server name from LDAP URL or SMTP host when validating the certificate unless otherwise defined in the TLS section * added temporary translations from the old names to the new ones for all deprecated options * added docs * updated example configuration * final deprecations to be done in 4.28.0 * doc updates * fix misc linting issues * uniform deprecation notices for ease of final removal * added additional tests covering previously uncovered areas and the new configuration options * add non-fatal to certificate loading when system certs could not be loaded * adjust timeout of Suite ShortTimeouts * add warnings pusher for the StructValidator * make the schema suites uninform * utilize the warnings in the StructValidator * fix test suite usage for skip_verify * extract LDAP filter parsing into it's own function to make it possible to test * test LDAP filter parsing * update ErrorContainer interface * add tests to the StructValidator * add NewTLSConfig test * move baseDN for users/groups into parsed values * add tests to cover many of the outstanding areas in LDAP * add explicit deferred LDAP conn close to UpdatePassword * add some basic testing to SMTP notifier * suggestions from code review --- BREAKING.md | 11 +- cmd/authelia/main.go | 39 +- config.template.yml | 34 +- docs/configuration/authentication/ldap.md | 27 +- docs/configuration/index.md | 24 +- docs/configuration/miscellaneous.md | 10 + docs/configuration/notifier/smtp.md | 45 +- .../authentication/ldap_connection_factory.go | 19 +- .../ldap_connection_factory_mock.go | 30 +- internal/authentication/ldap_user_provider.go | 158 +++--- .../authentication/ldap_user_provider_test.go | 533 +++++++++++++----- internal/configuration/reader.go | 7 + .../configuration/schema/authentication.go | 37 +- .../configuration/schema/configuration.go | 1 + internal/configuration/schema/notifier.go | 28 +- internal/configuration/schema/shared.go | 8 + internal/configuration/schema/validator.go | 27 +- .../configuration/schema/validator_test.go | 49 +- .../configuration/validator/authentication.go | 70 ++- .../validator/authentication_test.go | 387 +++++++++---- .../configuration/validator/configuration.go | 11 + .../validator/configuration_test.go | 37 ++ internal/configuration/validator/const.go | 17 +- internal/configuration/validator/notifier.go | 22 + .../configuration/validator/notifier_test.go | 154 +++-- .../configuration/validator/storage_test.go | 91 +-- internal/notification/smtp_notifier.go | 50 +- internal/notification/smtp_notifier_test.go | 57 ++ .../suites/ActiveDirectory/configuration.yml | 3 +- internal/suites/LDAP/configuration.yml | 3 +- .../suites/ShortTimeouts/configuration.yml | 2 +- .../kube/authelia/configs/configuration.yml | 3 +- internal/utils/certificates.go | 91 +++ internal/utils/certificates_test.go | 134 +++++ internal/utils/strings.go | 17 - internal/utils/strings_test.go | 52 -- 36 files changed, 1585 insertions(+), 703 deletions(-) create mode 100644 internal/configuration/schema/shared.go create mode 100644 internal/notification/smtp_notifier_test.go create mode 100644 internal/utils/certificates.go create mode 100644 internal/utils/certificates_test.go diff --git a/BREAKING.md b/BREAKING.md index c4864b06..5a9a6ed6 100644 --- a/BREAKING.md +++ b/BREAKING.md @@ -6,10 +6,19 @@ recommended not to use the 'latest' Docker image tag blindly but pick a version and read this documentation before upgrading. This is where you will get information about breaking changes and about what you should do to overcome those changes. +## Breaking in v4.25.0 + +### Deprecation Notice(s) +* All of these deprecations will be fully removed in release 4.28.0 +* The SMTP notifiers `trusted_cert` option has been deprecated (replaced by global certificates_directory) +* The SMTP notifiers `disable_verify_cert` option has been deprecated (replaced by `notifier.smtp.tls.skip_verify`) +* The LDAP authentication backends `skip_verify` option has been deprecated (replaced by `authentication_backend.ldap.tls.skip_verify`) +* The LDAP authentication backends `minimum_tls_version` option has been deprecated (replaced by `authentication_backend.ldap.tls.minimum_version`) + ## Breaking in v4.24.0 ### Deprecation Notice(s) -* LDAP User Provider Filters (final removal in 4.27.0): +* LDAP User Provider Filters (final removal in 4.28.0): * User Filters containing `{0}` are being deprecated and will generate warnings. Replaced with `{input}`. * Group Filters containing `{0}` or `{1}` are being deprecated and will generate warnings. Replaced with `{input}` and `{username}` respectively. diff --git a/cmd/authelia/main.go b/cmd/authelia/main.go index 1c8fa18b..63e8f845 100644 --- a/cmd/authelia/main.go +++ b/cmd/authelia/main.go @@ -35,6 +35,21 @@ func startServer() { os.Exit(1) } + autheliaCertPool, errs, nonFatalErrs := utils.NewX509CertPool(config.CertificatesDirectory, config) + if len(errs) > 0 { + for _, err := range errs { + logging.Logger().Error(err) + } + + os.Exit(2) + } + + if len(nonFatalErrs) > 0 { + for _, err := range nonFatalErrs { + logging.Logger().Warn(err) + } + } + if err := logging.InitializeLogger(config.LogFormat, config.LogFilePath); err != nil { logging.Logger().Fatalf("Cannot initialize logger: %v", err) } @@ -55,17 +70,6 @@ func startServer() { logging.Logger().Info("===> Authelia is running in development mode. <===") } - var userProvider authentication.UserProvider - - switch { - case config.AuthenticationBackend.File != nil: - userProvider = authentication.NewFileUserProvider(config.AuthenticationBackend.File) - case config.AuthenticationBackend.Ldap != nil: - userProvider = authentication.NewLDAPUserProvider(*config.AuthenticationBackend.Ldap) - default: - logging.Logger().Fatalf("Unrecognized authentication backend") - } - var storageProvider storage.Provider switch { @@ -79,11 +83,22 @@ func startServer() { logging.Logger().Fatalf("Unrecognized storage backend") } + var userProvider authentication.UserProvider + + switch { + case config.AuthenticationBackend.File != nil: + userProvider = authentication.NewFileUserProvider(config.AuthenticationBackend.File) + case config.AuthenticationBackend.Ldap != nil: + userProvider = authentication.NewLDAPUserProvider(*config.AuthenticationBackend.Ldap, autheliaCertPool) + default: + logging.Logger().Fatalf("Unrecognized authentication backend") + } + var notifier notification.Notifier switch { case config.Notifier.SMTP != nil: - notifier = notification.NewSMTPNotifier(*config.Notifier.SMTP) + notifier = notification.NewSMTPNotifier(*config.Notifier.SMTP, autheliaCertPool) case config.Notifier.FileSystem != nil: notifier = notification.NewFileNotifier(*config.Notifier.FileSystem) default: diff --git a/config.template.yml b/config.template.yml index e60feabb..2de0e344 100644 --- a/config.template.yml +++ b/config.template.yml @@ -107,14 +107,18 @@ authentication_backend: # The url to the ldap server. Scheme can be ldap or ldaps in the format (port optional) ://
[:]. url: ldap://127.0.0.1 - # Skip verifying the server certificate (to allow a self-signed certificate). - skip_verify: false - # Use StartTLS with the LDAP connection. start_tls: false - # Minimum TLS version for either Secure LDAP or LDAP StartTLS. - minimum_tls_version: TLS1.2 + tls: + # Server Name for certificate validation (in case it's not set correctly in the URL). + # server_name: ldap.example.com + + # Skip verifying the server certificate (to allow a self-signed certificate). + skip_verify: false + + # Minimum TLS version for either Secure LDAP or LDAP StartTLS. + minimum_version: TLS1.2 # The base dn for every entries. base_dn: dc=example,dc=com @@ -406,13 +410,7 @@ notifier: # [Security] By default Authelia will: # - force all SMTP connections over TLS including unauthenticated connections # - use the disable_require_tls boolean value to disable this requirement (only works for unauthenticated connections) - # - validate the SMTP server x509 certificate during the TLS handshake against the hosts trusted certificates - # - trusted_cert option: - # - this is a string value, that may specify the path of a PEM format cert, it is completely optional - # - if it is not set, a blank string, or an invalid path; will still trust the host machine/containers cert store - # - defaults to the host machine (or docker container's) trusted certificate chain for validation - # - use the trusted_cert string value to specify the path of a PEM format public cert to trust in addition to the hosts trusted certificates - # - use the disable_verify_cert boolean value to disable the validation (prefer the trusted_cert option as it's more secure) + # - validate the SMTP server x509 certificate during the TLS handshake against the hosts trusted certificates (configure in tls section) smtp: username: test # Password can also be set using a secret: https://docs.authelia.com/configuration/secrets.html @@ -427,11 +425,19 @@ notifier: subject: "[Authelia] {title}" # This address is used during the startup check to verify the email configuration is correct. It's not important what it is except if your email server only allows local delivery. startup_check_address: test@authelia.com - trusted_cert: "" disable_require_tls: false - disable_verify_cert: false disable_html_emails: false + tls: + # Server Name for certificate validation (in case you are using the IP or non-FQDN in the host option). + # server_name: smtp.example.com + + # Skip verifying the server certificate (to allow a self-signed certificate). + skip_verify: false + + # Minimum TLS version for either StartTLS or SMTPS. + minimum_version: TLS1.2 + # Sending an email using a Gmail account is as simple as the next section. # You need to create an app password by following: https://support.google.com/accounts/answer/185833?hl=en ## smtp: diff --git a/docs/configuration/authentication/ldap.md b/docs/configuration/authentication/ldap.md index e5ac7aca..2fdbb53d 100644 --- a/docs/configuration/authentication/ldap.md +++ b/docs/configuration/authentication/ldap.md @@ -52,15 +52,19 @@ authentication_backend: # The url to the ldap server. Scheme can be ldap or ldaps in the format (port optional) ://
[:]. url: ldap://127.0.0.1 - - # Skip verifying the server certificate (to allow a self-signed certificate). - skip_verify: false # Use StartTLS with the LDAP connection. start_tls: false - # Minimum TLS version for either Secure LDAP or LDAP StartTLS. - minimum_tls_version: TLS1.2 + tls: + # Server Name for certificate validation (in case it's not set correctly in the URL). + # server_name: ldap.example.com + + # Skip verifying the server certificate (to allow a self-signed certificate). + skip_verify: false + + # Minimum TLS version for either Secure LDAP or LDAP StartTLS. + minimum_version: TLS1.2 # The base dn for every entries. base_dn: dc=example,dc=com @@ -139,24 +143,15 @@ url: ldap://[fd00:1111:2222:3333::1] ## TLS Settings -### Skip Verify - -The key `skip_verify` disables checking the authenticity of the TLS certificate. You should not disable this, instead -you should add the certificate that signed the certificate of your LDAP server to the machines certificate PKI trust. -For docker you can just add this to the hosts trusted store. - ### Start TLS The key `start_tls` enables use of the LDAP StartTLS process which is not commonly used. You should only configure this if you know you need it. The initial connection will be over plain text, and Authelia will try to upgrade it with the LDAP server. LDAPS URL's are slightly more secure. -### Minimum TLS Version +### TLS (section) -The key `minimum_tls_version` controls the minimum TLS version Authelia will use when opening LDAP connections. -The possible values are `TLS1.3`, `TLS1.2`, `TLS1.1`, `TLS1.0`. Anything other than `TLS1.3` or `TLS1.2` -are very old and deprecated. You should avoid using these and upgrade your LDAP solution instead of decreasing -this value. +The key `tls` is a map of options for tuning TLS options. You can see how to configure the tls section [here](../index.md#tls-configuration). ## Implementation diff --git a/docs/configuration/index.md b/docs/configuration/index.md index 5f791f2e..584a4a63 100644 --- a/docs/configuration/index.md +++ b/docs/configuration/index.md @@ -54,4 +54,26 @@ denoting the unit of time measurement. The table below describes the units of ti Examples: * 1 hour and 30 minutes: 90m * 1 day: 1d -* 10 hours: 10h \ No newline at end of file +* 10 hours: 10h + +## TLS Configuration + +Various sections of the configuration use a uniform configuration section called TLS. Notably LDAP and SMTP. +This section documents the usage. + +### Server Name + +The key `server_name` overrides the name checked against the certificate in the verification process. Useful if you +require to use a direct IP address for the address of the backend service but want to verify a specific SNI. + +### Skip Verify + +The key `skip_verify` completely negates validating the certificate of the backend service. This is not recommended, +instead you should tweak the `server_name` option, and the global option [certificates_directory](./miscellaneous.md#certificates-directory). + +### Minimum Version + +The key `minimum_version` controls the minimum TLS version Authelia will use when opening TLS connections. +The possible values are `TLS1.3`, `TLS1.2`, `TLS1.1`, `TLS1.0`. Anything other than `TLS1.3` or `TLS1.2` +are very old and deprecated. You should avoid using these and upgrade your backend service instead of decreasing +this value. \ No newline at end of file diff --git a/docs/configuration/miscellaneous.md b/docs/configuration/miscellaneous.md index 780ae50c..6b8ab542 100644 --- a/docs/configuration/miscellaneous.md +++ b/docs/configuration/miscellaneous.md @@ -38,6 +38,16 @@ tls_key: /config/ssl/key.pem tls_cert: /config/ssl/cert.pem ``` +## Certificates Directory + +`optional: true` + +This option defines the location of additional certificates to load into the trust chain specifically for Authelia. +This currently affects both the SMTP notifier and the LDAP authentication backend. The certificates should all be in the +PEM format and end with the extension `.pem` or `.crt`. You can either add the individual certificates public key +or the CA public key which signed them (don't add the private key). + + ## Log ### Log level diff --git a/docs/configuration/notifier/smtp.md b/docs/configuration/notifier/smtp.md index 13ec90c8..aac4494b 100644 --- a/docs/configuration/notifier/smtp.md +++ b/docs/configuration/notifier/smtp.md @@ -7,7 +7,6 @@ nav_order: 2 --- # SMTP - **Authelia** can send emails to users through an SMTP server. It can be configured as described below. @@ -29,13 +28,7 @@ notifier: # [Security] By default Authelia will: # - force all SMTP connections over TLS including unauthenticated connections # - use the disable_require_tls boolean value to disable this requirement (only works for unauthenticated connections) - # - validate the SMTP server x509 certificate during the TLS handshake against the hosts trusted certificates - # - trusted_cert option: - # - this is a string value, that may specify the path of a PEM format cert, it is completely optional - # - if it is not set, a blank string, or an invalid path; will still trust the host machine/containers cert store - # - defaults to the host machine (or docker container's) trusted certificate chain for validation - # - use the trusted_cert string value to specify the path of a PEM format public cert to trust in addition to the hosts trusted certificates - # - use the disable_verify_cert boolean value to disable the validation (prefer the trusted_cert option as it's more secure) + # - validate the SMTP server x509 certificate during the TLS handshake against the hosts trusted certificates (configure in tls section) smtp: username: test # Password can also be set using a secret: https://docs.authelia.com/configuration/secrets.html @@ -50,19 +43,37 @@ notifier: subject: "[Authelia] {title}" # This address is used during the startup check to verify the email configuration is correct. It's not important what it is except if your email server only allows local delivery. startup_check_address: test@authelia.com - trusted_cert: "" disable_require_tls: false - disable_verify_cert: false disable_html_emails: false + + tls: + # Server Name for certificate validation (in case you are using the IP or non-FQDN in the host option). + # server_name: smtp.example.com + + # Skip verifying the server certificate (to allow a self-signed certificate). + skip_verify: false + + # Minimum TLS version for either StartTLS or SMTPS. + minimum_version: TLS1.2 + + # Sending an email using a Gmail account is as simple as the next section. + # You need to create an app password by following: https://support.google.com/accounts/answer/185833?hl=en + ## smtp: + ## username: myaccount@gmail.com + ## # Password can also be set using a secret: https://docs.authelia.com/configuration/secrets.html + ## password: yourapppassword + ## sender: admin@example.com + ## host: smtp.gmail.com + ## port: 587 ``` ## Configuration options - Most configuration options are self-explanatory, however here is an explanation of the ones that may not be as obvious. ### host If utilising an IPv6 literal address it must be enclosed by square brackets and quoted: + ```yaml host: "[fd00:1111:2222:3333::1]" ``` @@ -79,21 +90,14 @@ be included in all emails as it is the internal descriptor for the contents of t For security reasons the default settings for Authelia require the SMTP connection is encrypted by TLS. See [security] for more information. This option disables this measure (not recommended). -### disable_verify_cert -For security reasons Authelia only trusts certificates valid according to the OS's PKI chain. See [security] for more information. -This option disables this measure (not recommended). - ### disable_html_emails This option forces Authelia to only send plain text email via the notifier. This is the default for the file based notifier, but some users may wish to use plain text for security reasons. -### trusted_cert -This option allows you to specify the file path to a public key portion of a X509 certificate in order to trust it, or -certificates signed with the private key portion of the X509 certificate. This is an alternative to `disable_verify_cert` -that is much more secure. This is not required if your certificate is trusted by the operating system PKI. +### TLS (section) +The key `tls` is a map of options for tuning TLS options. You can see how to configure the tls section [here](../index.md#tls-configuration). ## Using Gmail - You need to generate an app password in order to use Gmail SMTP servers. The process is described [here](https://support.google.com/accounts/answer/185833?hl=en) @@ -109,7 +113,6 @@ notifier: ``` ## Loading a password from a secret instead of inside the configuration - Password can also be defined using a [secret](../secrets.md). [security]: ../../security/measures.md#notifier-security-measures-smtp \ No newline at end of file diff --git a/internal/authentication/ldap_connection_factory.go b/internal/authentication/ldap_connection_factory.go index 168dba46..9cc8b839 100644 --- a/internal/authentication/ldap_connection_factory.go +++ b/internal/authentication/ldap_connection_factory.go @@ -57,8 +57,7 @@ func (lc *LDAPConnectionImpl) StartTLS(config *tls.Config) error { // LDAPConnectionFactory an interface of factory of ldap connections. type LDAPConnectionFactory interface { - DialTLS(network, addr string, config *tls.Config) (LDAPConnection, error) - Dial(network, addr string) (LDAPConnection, error) + DialURL(addr string, opts ldap.DialOpt) (LDAPConnection, error) } // LDAPConnectionFactoryImpl the production implementation of an ldap connection factory. @@ -69,19 +68,9 @@ func NewLDAPConnectionFactoryImpl() *LDAPConnectionFactoryImpl { return &LDAPConnectionFactoryImpl{} } -// DialTLS contact ldap server over TLS. -func (lcf *LDAPConnectionFactoryImpl) DialTLS(network, addr string, config *tls.Config) (LDAPConnection, error) { - conn, err := ldap.DialTLS(network, addr, config) - if err != nil { - return nil, err - } - - return NewLDAPConnectionImpl(conn), nil -} - -// Dial contact ldap server over raw tcp. -func (lcf *LDAPConnectionFactoryImpl) Dial(network, addr string) (LDAPConnection, error) { - conn, err := ldap.Dial(network, addr) +// DialURL creates a connection from an LDAP URL when successful. +func (lcf *LDAPConnectionFactoryImpl) DialURL(addr string, opts ldap.DialOpt) (LDAPConnection, error) { + conn, err := ldap.DialURL(addr, opts) if err != nil { return nil, err } diff --git a/internal/authentication/ldap_connection_factory_mock.go b/internal/authentication/ldap_connection_factory_mock.go index 7e662c8e..477c9717 100644 --- a/internal/authentication/ldap_connection_factory_mock.go +++ b/internal/authentication/ldap_connection_factory_mock.go @@ -4,9 +4,10 @@ package authentication import ( tls "crypto/tls" + reflect "reflect" + ldap "github.com/go-ldap/ldap/v3" gomock "github.com/golang/mock/gomock" - reflect "reflect" ) // MockLDAPConnection is a mock of LDAPConnection interface @@ -124,32 +125,17 @@ func (m *MockLDAPConnectionFactory) EXPECT() *MockLDAPConnectionFactoryMockRecor return m.recorder } -// DialTLS mocks base method -func (m *MockLDAPConnectionFactory) DialTLS(network, addr string, config *tls.Config) (LDAPConnection, error) { +// DialURL mocks base method +func (m *MockLDAPConnectionFactory) DialURL(addr string, opts ldap.DialOpt) (LDAPConnection, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "DialTLS", network, addr, config) + ret := m.ctrl.Call(m, "DialURL", addr, opts) ret0, _ := ret[0].(LDAPConnection) ret1, _ := ret[1].(error) return ret0, ret1 } -// DialTLS indicates an expected call of DialTLS -func (mr *MockLDAPConnectionFactoryMockRecorder) DialTLS(network, addr, config interface{}) *gomock.Call { +// DialURL indicates an expected call of DialURL +func (mr *MockLDAPConnectionFactoryMockRecorder) DialURL(addr, opts interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DialTLS", reflect.TypeOf((*MockLDAPConnectionFactory)(nil).DialTLS), network, addr, config) -} - -// Dial mocks base method -func (m *MockLDAPConnectionFactory) Dial(network, addr string) (LDAPConnection, error) { - m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "Dial", network, addr) - ret0, _ := ret[0].(LDAPConnection) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// Dial indicates an expected call of Dial -func (mr *MockLDAPConnectionFactoryMockRecorder) Dial(network, addr interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Dial", reflect.TypeOf((*MockLDAPConnectionFactory)(nil).Dial), network, addr) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DialURL", reflect.TypeOf((*MockLDAPConnectionFactory)(nil).DialURL), addr, opts) } diff --git a/internal/authentication/ldap_user_provider.go b/internal/authentication/ldap_user_provider.go index eb6a7609..27af621c 100644 --- a/internal/authentication/ldap_user_provider.go +++ b/internal/authentication/ldap_user_provider.go @@ -2,8 +2,8 @@ package authentication import ( "crypto/tls" + "crypto/x509" "fmt" - "net/url" "strings" "github.com/go-ldap/ldap/v3" @@ -16,116 +16,126 @@ import ( // LDAPUserProvider is a provider using a LDAP or AD as a user database. type LDAPUserProvider struct { - configuration schema.LDAPAuthenticationBackendConfiguration - tlsConfig *tls.Config - + configuration schema.LDAPAuthenticationBackendConfiguration + tlsConfig *tls.Config + dialOpts ldap.DialOpt connectionFactory LDAPConnectionFactory + usersDN string + groupsDN string } // NewLDAPUserProvider creates a new instance of LDAPUserProvider. -func NewLDAPUserProvider(configuration schema.LDAPAuthenticationBackendConfiguration) *LDAPUserProvider { - minimumTLSVersion, _ := utils.TLSStringToTLSConfigVersion(configuration.MinimumTLSVersion) - - // TODO: RELEASE-4.27.0 Deprecated Completely in this release. - logger := logging.Logger() - - if strings.Contains(configuration.UsersFilter, "{0}") { - logger.Warnf("DEPRECATION NOTICE: LDAP Users Filter will no longer support replacing `{0}` in 4.27.0. Please use `{input}` instead.") - - configuration.UsersFilter = strings.ReplaceAll(configuration.UsersFilter, "{0}", "{input}") +func NewLDAPUserProvider(configuration schema.LDAPAuthenticationBackendConfiguration, certPool *x509.CertPool) *LDAPUserProvider { + if configuration.TLS == nil { + configuration.TLS = schema.DefaultLDAPAuthenticationBackendConfiguration.TLS } - if strings.Contains(configuration.GroupsFilter, "{0}") { - logger.Warnf("DEPRECATION NOTICE: LDAP Groups Filter will no longer support replacing `{0}` in 4.27.0. Please use `{input}` instead.") + tlsConfig := utils.NewTLSConfig(configuration.TLS, tls.VersionTLS12, certPool) - configuration.GroupsFilter = strings.ReplaceAll(configuration.GroupsFilter, "{0}", "{input}") + var dialOpts ldap.DialOpt + + if tlsConfig != nil { + dialOpts = ldap.DialWithTLSConfig(tlsConfig) } - if strings.Contains(configuration.GroupsFilter, "{1}") { - logger.Warnf("DEPRECATION NOTICE: LDAP Groups Filter will no longer support replacing `{1}` in 4.27.0. Please use `{username}` instead.") - - configuration.GroupsFilter = strings.ReplaceAll(configuration.GroupsFilter, "{1}", "{username}") - } - // TODO: RELEASE-4.27.0 Deprecated Completely in this release. - - configuration.UsersFilter = strings.ReplaceAll(configuration.UsersFilter, "{username_attribute}", configuration.UsernameAttribute) - configuration.UsersFilter = strings.ReplaceAll(configuration.UsersFilter, "{mail_attribute}", configuration.MailAttribute) - configuration.UsersFilter = strings.ReplaceAll(configuration.UsersFilter, "{display_name_attribute}", configuration.DisplayNameAttribute) - - return &LDAPUserProvider{ - configuration: configuration, - tlsConfig: &tls.Config{InsecureSkipVerify: configuration.SkipVerify, MinVersion: minimumTLSVersion}, //nolint:gosec // Disabling InsecureSkipVerify is an informed choice by users. - + provider := &LDAPUserProvider{ + configuration: configuration, + tlsConfig: tlsConfig, + dialOpts: dialOpts, connectionFactory: NewLDAPConnectionFactoryImpl(), } + + provider.parseDynamicConfiguration() + + return provider } // NewLDAPUserProviderWithFactory creates a new instance of LDAPUserProvider with existing factory. -func NewLDAPUserProviderWithFactory(configuration schema.LDAPAuthenticationBackendConfiguration, connectionFactory LDAPConnectionFactory) *LDAPUserProvider { - provider := NewLDAPUserProvider(configuration) +func NewLDAPUserProviderWithFactory(configuration schema.LDAPAuthenticationBackendConfiguration, certPool *x509.CertPool, connectionFactory LDAPConnectionFactory) *LDAPUserProvider { + provider := NewLDAPUserProvider(configuration, certPool) provider.connectionFactory = connectionFactory return provider } -func (p *LDAPUserProvider) connect(userDN string, password string) (LDAPConnection, error) { - var newConnection LDAPConnection +func (p *LDAPUserProvider) parseDynamicConfiguration() { + logger := logging.Logger() // Deprecated: This is temporary for deprecation notice purposes. TODO: Remove in 4.28. - ldapURL, err := url.Parse(p.configuration.URL) + // Deprecated: This is temporary for deprecation notice purposes. TODO: Remove in 4.28. + if strings.Contains(p.configuration.UsersFilter, "{0}") { + logger.Warnf("DEPRECATION NOTICE: LDAP Users Filter will no longer support replacing `{0}` in 4.28.0. Please use `{input}` instead.") - if err != nil { - return nil, fmt.Errorf("Unable to parse URL to LDAP: %s", ldapURL) + p.configuration.UsersFilter = strings.ReplaceAll(p.configuration.UsersFilter, "{0}", "{input}") } - if ldapURL.Scheme == "ldaps" { - logging.Logger().Trace("LDAP client starts a TLS session") + // Deprecated: This is temporary for deprecation notice purposes. TODO: Remove in 4.28. + if strings.Contains(p.configuration.GroupsFilter, "{0}") { + logger.Warnf("DEPRECATION NOTICE: LDAP Groups Filter will no longer support replacing `{0}` in 4.28.0. Please use `{input}` instead.") - conn, err := p.connectionFactory.DialTLS("tcp", ldapURL.Host, p.tlsConfig) - if err != nil { - return nil, err - } + p.configuration.GroupsFilter = strings.ReplaceAll(p.configuration.GroupsFilter, "{0}", "{input}") + } - newConnection = conn + // Deprecated: This is temporary for deprecation notice purposes. TODO: Remove in 4.28. + if strings.Contains(p.configuration.GroupsFilter, "{1}") { + logger.Warnf("DEPRECATION NOTICE: LDAP Groups Filter will no longer support replacing `{1}` in 4.28.0. Please use `{username}` instead.") + + p.configuration.GroupsFilter = strings.ReplaceAll(p.configuration.GroupsFilter, "{1}", "{username}") + } + + p.configuration.UsersFilter = strings.ReplaceAll(p.configuration.UsersFilter, "{username_attribute}", p.configuration.UsernameAttribute) + p.configuration.UsersFilter = strings.ReplaceAll(p.configuration.UsersFilter, "{mail_attribute}", p.configuration.MailAttribute) + p.configuration.UsersFilter = strings.ReplaceAll(p.configuration.UsersFilter, "{display_name_attribute}", p.configuration.DisplayNameAttribute) + + if p.configuration.AdditionalUsersDN != "" { + p.usersDN = p.configuration.AdditionalUsersDN + "," + p.configuration.BaseDN } else { - logging.Logger().Trace("LDAP client starts a session over raw TCP") - conn, err := p.connectionFactory.Dial("tcp", ldapURL.Host) - if err != nil { - return nil, err - } - newConnection = conn + p.usersDN = p.configuration.BaseDN } - if p.configuration.StartTLS { - if err := newConnection.StartTLS(p.tlsConfig); err != nil { - return nil, err - } + if p.configuration.AdditionalGroupsDN != "" { + p.groupsDN = p.configuration.AdditionalGroupsDN + "," + p.configuration.BaseDN + } else { + p.groupsDN = p.configuration.BaseDN } +} - if err := newConnection.Bind(userDN, password); err != nil { +func (p *LDAPUserProvider) connect(userDN string, password string) (LDAPConnection, error) { + conn, err := p.connectionFactory.DialURL(p.configuration.URL, p.dialOpts) + if err != nil { return nil, err } - return newConnection, nil + if p.configuration.StartTLS { + if err := conn.StartTLS(p.tlsConfig); err != nil { + return nil, err + } + } + + if err := conn.Bind(userDN, password); err != nil { + return nil, err + } + + return conn, nil } // CheckUserPassword checks if provided password matches for the given user. func (p *LDAPUserProvider) CheckUserPassword(inputUsername string, password string) (bool, error) { - adminClient, err := p.connect(p.configuration.User, p.configuration.Password) + conn, err := p.connect(p.configuration.User, p.configuration.Password) if err != nil { return false, err } - defer adminClient.Close() + defer conn.Close() - profile, err := p.getUserProfile(adminClient, inputUsername) + profile, err := p.getUserProfile(conn, inputUsername) if err != nil { return false, err } - conn, err := p.connect(profile.DN, password) + userConn, err := p.connect(profile.DN, password) if err != nil { return false, fmt.Errorf("Authentication of user %s failed. Cause: %s", inputUsername, err) } - defer conn.Close() + defer userConn.Close() return true, nil } @@ -159,11 +169,6 @@ func (p *LDAPUserProvider) getUserProfile(conn LDAPConnection, inputUsername str userFilter := p.resolveUsersFilter(p.configuration.UsersFilter, inputUsername) logging.Logger().Tracef("Computed user filter is %s", userFilter) - baseDN := p.configuration.BaseDN - if p.configuration.AdditionalUsersDN != "" { - baseDN = p.configuration.AdditionalUsersDN + "," + baseDN - } - attributes := []string{"dn", p.configuration.DisplayNameAttribute, p.configuration.MailAttribute, @@ -171,7 +176,7 @@ func (p *LDAPUserProvider) getUserProfile(conn LDAPConnection, inputUsername str // Search for the given username. searchRequest := ldap.NewSearchRequest( - baseDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, + p.usersDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 1, 0, false, userFilter, attributes, nil, ) @@ -252,14 +257,9 @@ func (p *LDAPUserProvider) GetDetails(inputUsername string) (*UserDetails, error logging.Logger().Tracef("Computed groups filter is %s", groupsFilter) - groupBaseDN := p.configuration.BaseDN - if p.configuration.AdditionalGroupsDN != "" { - groupBaseDN = p.configuration.AdditionalGroupsDN + "," + groupBaseDN - } - // Search for the given username. searchGroupRequest := ldap.NewSearchRequest( - groupBaseDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, + p.groupsDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, groupsFilter, []string{p.configuration.GroupNameAttribute}, nil, ) @@ -290,13 +290,13 @@ func (p *LDAPUserProvider) GetDetails(inputUsername string) (*UserDetails, error // UpdatePassword update the password of the given user. func (p *LDAPUserProvider) UpdatePassword(inputUsername string, newPassword string) error { - client, err := p.connect(p.configuration.User, p.configuration.Password) - + conn, err := p.connect(p.configuration.User, p.configuration.Password) if err != nil { return fmt.Errorf("Unable to update password. Cause: %s", err) } + defer conn.Close() - profile, err := p.getUserProfile(client, inputUsername) + profile, err := p.getUserProfile(conn, inputUsername) if err != nil { return fmt.Errorf("Unable to update password. Cause: %s", err) @@ -315,7 +315,7 @@ func (p *LDAPUserProvider) UpdatePassword(inputUsername string, newPassword stri modifyRequest.Replace("userPassword", []string{newPassword}) } - err = client.Modify(modifyRequest) + err = conn.Modify(modifyRequest) if err != nil { return fmt.Errorf("Unable to update password. Cause: %s", err) diff --git a/internal/authentication/ldap_user_provider_test.go b/internal/authentication/ldap_user_provider_test.go index b4c3ab6e..4036415f 100644 --- a/internal/authentication/ldap_user_provider_test.go +++ b/internal/authentication/ldap_user_provider_test.go @@ -19,19 +19,22 @@ func TestShouldCreateRawConnectionWhenSchemeIsLDAP(t *testing.T) { mockFactory := NewMockLDAPConnectionFactory(ctrl) mockConn := NewMockLDAPConnection(ctrl) - ldap := NewLDAPUserProviderWithFactory(schema.LDAPAuthenticationBackendConfiguration{ - URL: "ldap://127.0.0.1:389", - }, mockFactory) + ldapClient := NewLDAPUserProviderWithFactory( + schema.LDAPAuthenticationBackendConfiguration{ + URL: "ldap://127.0.0.1:389", + }, + nil, + mockFactory) mockFactory.EXPECT(). - Dial(gomock.Eq("tcp"), gomock.Eq("127.0.0.1:389")). + DialURL(gomock.Eq("ldap://127.0.0.1:389"), gomock.Any()). Return(mockConn, nil) mockConn.EXPECT(). Bind(gomock.Eq("cn=admin,dc=example,dc=com"), gomock.Eq("password")). Return(nil) - _, err := ldap.connect("cn=admin,dc=example,dc=com", "password") + _, err := ldapClient.connect("cn=admin,dc=example,dc=com", "password") require.NoError(t, err) } @@ -43,19 +46,22 @@ func TestShouldCreateTLSConnectionWhenSchemeIsLDAPS(t *testing.T) { mockFactory := NewMockLDAPConnectionFactory(ctrl) mockConn := NewMockLDAPConnection(ctrl) - ldap := NewLDAPUserProviderWithFactory(schema.LDAPAuthenticationBackendConfiguration{ - URL: "ldaps://127.0.0.1:389", - }, mockFactory) + ldapClient := NewLDAPUserProviderWithFactory( + schema.LDAPAuthenticationBackendConfiguration{ + URL: "ldaps://127.0.0.1:389", + }, + nil, + mockFactory) mockFactory.EXPECT(). - DialTLS(gomock.Eq("tcp"), gomock.Eq("127.0.0.1:389"), gomock.Any()). + DialURL(gomock.Eq("ldaps://127.0.0.1:389"), gomock.Any()). Return(mockConn, nil) mockConn.EXPECT(). Bind(gomock.Eq("cn=admin,dc=example,dc=com"), gomock.Eq("password")). Return(nil) - _, err := ldap.connect("cn=admin,dc=example,dc=com", "password") + _, err := ldapClient.connect("cn=admin,dc=example,dc=com", "password") require.NoError(t, err) } @@ -65,26 +71,30 @@ func TestEscapeSpecialCharsFromUserInput(t *testing.T) { defer ctrl.Finish() mockFactory := NewMockLDAPConnectionFactory(ctrl) - ldap := NewLDAPUserProviderWithFactory(schema.LDAPAuthenticationBackendConfiguration{ - URL: "ldaps://127.0.0.1:389", - }, mockFactory) + + ldapClient := NewLDAPUserProviderWithFactory( + schema.LDAPAuthenticationBackendConfiguration{ + URL: "ldaps://127.0.0.1:389", + }, + nil, + mockFactory) // No escape - assert.Equal(t, "xyz", ldap.ldapEscape("xyz")) + assert.Equal(t, "xyz", ldapClient.ldapEscape("xyz")) // Escape - assert.Equal(t, "test\\,abc", ldap.ldapEscape("test,abc")) - assert.Equal(t, "test\\5cabc", ldap.ldapEscape("test\\abc")) - assert.Equal(t, "test\\2aabc", ldap.ldapEscape("test*abc")) - assert.Equal(t, "test \\28abc\\29", ldap.ldapEscape("test (abc)")) - assert.Equal(t, "test\\#abc", ldap.ldapEscape("test#abc")) - assert.Equal(t, "test\\+abc", ldap.ldapEscape("test+abc")) - assert.Equal(t, "test\\abc", ldap.ldapEscape("test>abc")) - assert.Equal(t, "test\\;abc", ldap.ldapEscape("test;abc")) - assert.Equal(t, "test\\\"abc", ldap.ldapEscape("test\"abc")) - assert.Equal(t, "test\\=abc", ldap.ldapEscape("test=abc")) - assert.Equal(t, "test\\,\\5c\\28abc\\29", ldap.ldapEscape("test,\\(abc)")) + assert.Equal(t, "test\\,abc", ldapClient.ldapEscape("test,abc")) + assert.Equal(t, "test\\5cabc", ldapClient.ldapEscape("test\\abc")) + assert.Equal(t, "test\\2aabc", ldapClient.ldapEscape("test*abc")) + assert.Equal(t, "test \\28abc\\29", ldapClient.ldapEscape("test (abc)")) + assert.Equal(t, "test\\#abc", ldapClient.ldapEscape("test#abc")) + assert.Equal(t, "test\\+abc", ldapClient.ldapEscape("test+abc")) + assert.Equal(t, "test\\abc", ldapClient.ldapEscape("test>abc")) + assert.Equal(t, "test\\;abc", ldapClient.ldapEscape("test;abc")) + assert.Equal(t, "test\\\"abc", ldapClient.ldapEscape("test\"abc")) + assert.Equal(t, "test\\=abc", ldapClient.ldapEscape("test=abc")) + assert.Equal(t, "test\\,\\5c\\28abc\\29", ldapClient.ldapEscape("test,\\(abc)")) } func TestEscapeSpecialCharsInGroupsFilter(t *testing.T) { @@ -92,10 +102,14 @@ func TestEscapeSpecialCharsInGroupsFilter(t *testing.T) { defer ctrl.Finish() mockFactory := NewMockLDAPConnectionFactory(ctrl) - ldap := NewLDAPUserProviderWithFactory(schema.LDAPAuthenticationBackendConfiguration{ - URL: "ldaps://127.0.0.1:389", - GroupsFilter: "(|(member={dn})(uid={username})(uid={input}))", - }, mockFactory) + + ldapClient := NewLDAPUserProviderWithFactory( + schema.LDAPAuthenticationBackendConfiguration{ + URL: "ldaps://127.0.0.1:389", + GroupsFilter: "(|(member={dn})(uid={username})(uid={input}))", + }, + nil, + mockFactory) profile := ldapUserProfile{ DN: "cn=john (external),dc=example,dc=com", @@ -104,10 +118,10 @@ func TestEscapeSpecialCharsInGroupsFilter(t *testing.T) { Emails: []string{"john.doe@authelia.com"}, } - filter, _ := ldap.resolveGroupsFilter("john", &profile) + filter, _ := ldapClient.resolveGroupsFilter("john", &profile) assert.Equal(t, "(|(member=cn=john \\28external\\29,dc=example,dc=com)(uid=john)(uid=john))", filter) - filter, _ = ldap.resolveGroupsFilter("john#=(abc,def)", &profile) + filter, _ = ldapClient.resolveGroupsFilter("john#=(abc,def)", &profile) assert.Equal(t, "(|(member=cn=john \\28external\\29,dc=example,dc=com)(uid=john)(uid=john\\#\\=\\28abc\\,def\\29))", filter) } @@ -135,17 +149,20 @@ func TestShouldEscapeUserInput(t *testing.T) { mockFactory := NewMockLDAPConnectionFactory(ctrl) mockConn := NewMockLDAPConnection(ctrl) - ldapClient := NewLDAPUserProviderWithFactory(schema.LDAPAuthenticationBackendConfiguration{ - URL: "ldap://127.0.0.1:389", - User: "cn=admin,dc=example,dc=com", - UsersFilter: "(|({username_attribute}={input})({mail_attribute}={input}))", - UsernameAttribute: "uid", - MailAttribute: "mail", - DisplayNameAttribute: "displayname", - Password: "password", - AdditionalUsersDN: "ou=users", - BaseDN: "dc=example,dc=com", - }, mockFactory) + ldapClient := NewLDAPUserProviderWithFactory( + schema.LDAPAuthenticationBackendConfiguration{ + URL: "ldap://127.0.0.1:389", + User: "cn=admin,dc=example,dc=com", + UsersFilter: "(|({username_attribute}={input})({mail_attribute}={input}))", + UsernameAttribute: "uid", + MailAttribute: "mail", + DisplayNameAttribute: "displayname", + Password: "password", + AdditionalUsersDN: "ou=users", + BaseDN: "dc=example,dc=com", + }, + nil, + mockFactory) mockConn.EXPECT(). // Here we ensure that the input has been correctly escaped. @@ -164,17 +181,20 @@ func TestShouldCombineUsernameFilterAndUsersFilter(t *testing.T) { mockFactory := NewMockLDAPConnectionFactory(ctrl) mockConn := NewMockLDAPConnection(ctrl) - ldapClient := NewLDAPUserProviderWithFactory(schema.LDAPAuthenticationBackendConfiguration{ - URL: "ldap://127.0.0.1:389", - User: "cn=admin,dc=example,dc=com", - UsernameAttribute: "uid", - UsersFilter: "(&({username_attribute}={input})(&(objectCategory=person)(objectClass=user)))", - Password: "password", - AdditionalUsersDN: "ou=users", - BaseDN: "dc=example,dc=com", - MailAttribute: "mail", - DisplayNameAttribute: "displayname", - }, mockFactory) + ldapClient := NewLDAPUserProviderWithFactory( + schema.LDAPAuthenticationBackendConfiguration{ + URL: "ldap://127.0.0.1:389", + User: "cn=admin,dc=example,dc=com", + UsernameAttribute: "uid", + UsersFilter: "(&({username_attribute}={input})(&(objectCategory=person)(objectClass=user)))", + Password: "password", + AdditionalUsersDN: "ou=users", + BaseDN: "dc=example,dc=com", + MailAttribute: "mail", + DisplayNameAttribute: "displayname", + }, + nil, + mockFactory) mockConn.EXPECT(). Search(NewSearchRequestMatcher("(&(uid=john)(&(objectCategory=person)(objectClass=user)))")). @@ -206,20 +226,23 @@ func TestShouldNotCrashWhenGroupsAreNotRetrievedFromLDAP(t *testing.T) { mockFactory := NewMockLDAPConnectionFactory(ctrl) mockConn := NewMockLDAPConnection(ctrl) - ldapClient := NewLDAPUserProviderWithFactory(schema.LDAPAuthenticationBackendConfiguration{ - URL: "ldap://127.0.0.1:389", - User: "cn=admin,dc=example,dc=com", - Password: "password", - UsernameAttribute: "uid", - MailAttribute: "mail", - DisplayNameAttribute: "displayname", - UsersFilter: "uid={input}", - AdditionalUsersDN: "ou=users", - BaseDN: "dc=example,dc=com", - }, mockFactory) + ldapClient := NewLDAPUserProviderWithFactory( + schema.LDAPAuthenticationBackendConfiguration{ + URL: "ldap://127.0.0.1:389", + User: "cn=admin,dc=example,dc=com", + Password: "password", + UsernameAttribute: "uid", + MailAttribute: "mail", + DisplayNameAttribute: "displayname", + UsersFilter: "uid={input}", + AdditionalUsersDN: "ou=users", + BaseDN: "dc=example,dc=com", + }, + nil, + mockFactory) mockFactory.EXPECT(). - Dial(gomock.Eq("tcp"), gomock.Eq("127.0.0.1:389")). + DialURL(gomock.Eq("ldap://127.0.0.1:389"), gomock.Any()). Return(mockConn, nil) mockConn.EXPECT(). @@ -274,18 +297,21 @@ func TestShouldNotCrashWhenEmailsAreNotRetrievedFromLDAP(t *testing.T) { mockFactory := NewMockLDAPConnectionFactory(ctrl) mockConn := NewMockLDAPConnection(ctrl) - ldapClient := NewLDAPUserProviderWithFactory(schema.LDAPAuthenticationBackendConfiguration{ - URL: "ldap://127.0.0.1:389", - User: "cn=admin,dc=example,dc=com", - Password: "password", - UsernameAttribute: "uid", - UsersFilter: "uid={input}", - AdditionalUsersDN: "ou=users", - BaseDN: "dc=example,dc=com", - }, mockFactory) + ldapClient := NewLDAPUserProviderWithFactory( + schema.LDAPAuthenticationBackendConfiguration{ + URL: "ldap://127.0.0.1:389", + User: "cn=admin,dc=example,dc=com", + Password: "password", + UsernameAttribute: "uid", + UsersFilter: "uid={input}", + AdditionalUsersDN: "ou=users", + BaseDN: "dc=example,dc=com", + }, + nil, + mockFactory) mockFactory.EXPECT(). - Dial(gomock.Eq("tcp"), gomock.Eq("127.0.0.1:389")). + DialURL(gomock.Eq("ldap://127.0.0.1:389"), gomock.Any()). Return(mockConn, nil) mockConn.EXPECT(). @@ -331,20 +357,23 @@ func TestShouldReturnUsernameFromLDAP(t *testing.T) { mockFactory := NewMockLDAPConnectionFactory(ctrl) mockConn := NewMockLDAPConnection(ctrl) - ldapClient := NewLDAPUserProviderWithFactory(schema.LDAPAuthenticationBackendConfiguration{ - URL: "ldap://127.0.0.1:389", - User: "cn=admin,dc=example,dc=com", - Password: "password", - UsernameAttribute: "uid", - MailAttribute: "mail", - DisplayNameAttribute: "displayname", - UsersFilter: "uid={input}", - AdditionalUsersDN: "ou=users", - BaseDN: "dc=example,dc=com", - }, mockFactory) + ldapClient := NewLDAPUserProviderWithFactory( + schema.LDAPAuthenticationBackendConfiguration{ + URL: "ldap://127.0.0.1:389", + User: "cn=admin,dc=example,dc=com", + Password: "password", + UsernameAttribute: "uid", + MailAttribute: "mail", + DisplayNameAttribute: "displayname", + UsersFilter: "uid={input}", + AdditionalUsersDN: "ou=users", + BaseDN: "dc=example,dc=com", + }, + nil, + mockFactory) mockFactory.EXPECT(). - Dial(gomock.Eq("tcp"), gomock.Eq("127.0.0.1:389")). + DialURL(gomock.Eq("ldap://127.0.0.1:389"), gomock.Any()). Return(mockConn, nil) mockConn.EXPECT(). @@ -392,6 +421,209 @@ func TestShouldReturnUsernameFromLDAP(t *testing.T) { assert.Equal(t, details.Username, "John") } +func TestShouldUpdateUserPassword(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockFactory := NewMockLDAPConnectionFactory(ctrl) + mockConn := NewMockLDAPConnection(ctrl) + + ldapClient := NewLDAPUserProviderWithFactory( + schema.LDAPAuthenticationBackendConfiguration{ + URL: "ldap://127.0.0.1:389", + User: "cn=admin,dc=example,dc=com", + Password: "password", + UsernameAttribute: "uid", + MailAttribute: "mail", + DisplayNameAttribute: "displayname", + UsersFilter: "uid={input}", + AdditionalUsersDN: "ou=users", + BaseDN: "dc=example,dc=com", + }, + nil, + mockFactory) + + modifyRequest := ldap.NewModifyRequest("uid=test,dc=example,dc=com", nil) + modifyRequest.Replace("userPassword", []string{"password"}) + + gomock.InOrder( + mockFactory.EXPECT(). + DialURL(gomock.Eq("ldap://127.0.0.1:389"), gomock.Any()). + Return(mockConn, nil), + mockConn.EXPECT(). + Bind(gomock.Eq("cn=admin,dc=example,dc=com"), gomock.Eq("password")). + Return(nil), + mockConn.EXPECT(). + Search(gomock.Any()). + Return(&ldap.SearchResult{ + Entries: []*ldap.Entry{ + { + DN: "uid=test,dc=example,dc=com", + Attributes: []*ldap.EntryAttribute{ + { + Name: "displayname", + Values: []string{"John Doe"}, + }, + { + Name: "mail", + Values: []string{"test@example.com"}, + }, + { + Name: "uid", + Values: []string{"John"}, + }, + }, + }, + }, + }, nil), + mockConn.EXPECT(). + Modify(modifyRequest). + Return(nil), + mockConn.EXPECT(). + Close(), + ) + + err := ldapClient.UpdatePassword("john", "password") + + require.NoError(t, err) +} + +func TestShouldCheckValidUserPassword(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockFactory := NewMockLDAPConnectionFactory(ctrl) + mockConn := NewMockLDAPConnection(ctrl) + + ldapClient := NewLDAPUserProviderWithFactory( + schema.LDAPAuthenticationBackendConfiguration{ + URL: "ldap://127.0.0.1:389", + User: "cn=admin,dc=example,dc=com", + Password: "password", + UsernameAttribute: "uid", + MailAttribute: "mail", + DisplayNameAttribute: "displayname", + UsersFilter: "uid={input}", + AdditionalUsersDN: "ou=users", + BaseDN: "dc=example,dc=com", + }, + nil, + mockFactory) + + gomock.InOrder( + mockFactory.EXPECT(). + DialURL(gomock.Eq("ldap://127.0.0.1:389"), gomock.Any()). + Return(mockConn, nil), + mockConn.EXPECT(). + Bind(gomock.Eq("cn=admin,dc=example,dc=com"), gomock.Eq("password")). + Return(nil), + mockConn.EXPECT(). + Search(gomock.Any()). + Return(&ldap.SearchResult{ + Entries: []*ldap.Entry{ + { + DN: "uid=test,dc=example,dc=com", + Attributes: []*ldap.EntryAttribute{ + { + Name: "displayname", + Values: []string{"John Doe"}, + }, + { + Name: "mail", + Values: []string{"test@example.com"}, + }, + { + Name: "uid", + Values: []string{"John"}, + }, + }, + }, + }, + }, nil), + mockFactory.EXPECT(). + DialURL(gomock.Eq("ldap://127.0.0.1:389"), gomock.Any()). + Return(mockConn, nil), + mockConn.EXPECT(). + Bind(gomock.Eq("uid=test,dc=example,dc=com"), gomock.Eq("password")). + Return(nil), + mockConn.EXPECT(). + Close().Times(2), + ) + + valid, err := ldapClient.CheckUserPassword("john", "password") + + assert.True(t, valid) + require.NoError(t, err) +} + +func TestShouldCheckInvalidUserPassword(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockFactory := NewMockLDAPConnectionFactory(ctrl) + mockConn := NewMockLDAPConnection(ctrl) + + ldapClient := NewLDAPUserProviderWithFactory( + schema.LDAPAuthenticationBackendConfiguration{ + URL: "ldap://127.0.0.1:389", + User: "cn=admin,dc=example,dc=com", + Password: "password", + UsernameAttribute: "uid", + MailAttribute: "mail", + DisplayNameAttribute: "displayname", + UsersFilter: "uid={input}", + AdditionalUsersDN: "ou=users", + BaseDN: "dc=example,dc=com", + }, + nil, + mockFactory) + + gomock.InOrder( + mockFactory.EXPECT(). + DialURL(gomock.Eq("ldap://127.0.0.1:389"), gomock.Any()). + Return(mockConn, nil), + mockConn.EXPECT(). + Bind(gomock.Eq("cn=admin,dc=example,dc=com"), gomock.Eq("password")). + Return(nil), + mockConn.EXPECT(). + Search(gomock.Any()). + Return(&ldap.SearchResult{ + Entries: []*ldap.Entry{ + { + DN: "uid=test,dc=example,dc=com", + Attributes: []*ldap.EntryAttribute{ + { + Name: "displayname", + Values: []string{"John Doe"}, + }, + { + Name: "mail", + Values: []string{"test@example.com"}, + }, + { + Name: "uid", + Values: []string{"John"}, + }, + }, + }, + }, + }, nil), + mockFactory.EXPECT(). + DialURL(gomock.Eq("ldap://127.0.0.1:389"), gomock.Any()). + Return(mockConn, nil), + mockConn.EXPECT(). + Bind(gomock.Eq("uid=test,dc=example,dc=com"), gomock.Eq("password")). + Return(errors.New("Invalid username or password")), + mockConn.EXPECT(). + Close(), + ) + + valid, err := ldapClient.CheckUserPassword("john", "password") + + assert.False(t, valid) + require.EqualError(t, err, "Authentication of user john failed. Cause: Invalid username or password") +} + func TestShouldCallStartTLSWhenEnabled(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() @@ -399,21 +631,24 @@ func TestShouldCallStartTLSWhenEnabled(t *testing.T) { mockFactory := NewMockLDAPConnectionFactory(ctrl) mockConn := NewMockLDAPConnection(ctrl) - ldapClient := NewLDAPUserProviderWithFactory(schema.LDAPAuthenticationBackendConfiguration{ - URL: "ldap://127.0.0.1:389", - User: "cn=admin,dc=example,dc=com", - Password: "password", - UsernameAttribute: "uid", - MailAttribute: "mail", - DisplayNameAttribute: "displayname", - UsersFilter: "uid={input}", - AdditionalUsersDN: "ou=users", - BaseDN: "dc=example,dc=com", - StartTLS: true, - }, mockFactory) + ldapClient := NewLDAPUserProviderWithFactory( + schema.LDAPAuthenticationBackendConfiguration{ + URL: "ldap://127.0.0.1:389", + User: "cn=admin,dc=example,dc=com", + Password: "password", + UsernameAttribute: "uid", + MailAttribute: "mail", + DisplayNameAttribute: "displayname", + UsersFilter: "uid={input}", + AdditionalUsersDN: "ou=users", + BaseDN: "dc=example,dc=com", + StartTLS: true, + }, + nil, + mockFactory) mockFactory.EXPECT(). - Dial(gomock.Eq("tcp"), gomock.Eq("127.0.0.1:389")). + DialURL(gomock.Eq("ldap://127.0.0.1:389"), gomock.Any()). Return(mockConn, nil) mockConn.EXPECT(). @@ -464,6 +699,36 @@ func TestShouldCallStartTLSWhenEnabled(t *testing.T) { assert.Equal(t, details.Username, "john") } +func TestShouldParseDynamicConfiguration(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockFactory := NewMockLDAPConnectionFactory(ctrl) + + ldapClient := NewLDAPUserProviderWithFactory( + schema.LDAPAuthenticationBackendConfiguration{ + URL: "ldap://127.0.0.1:389", + User: "cn=admin,dc=example,dc=com", + Password: "password", + UsernameAttribute: "uid", + MailAttribute: "mail", + DisplayNameAttribute: "displayname", + UsersFilter: "(&(|({username_attribute}={0})({mail_attribute}={0})({display_name_attribute}={0}))(objectCategory=person)(objectClass=user)(!userAccountControl:1.2.840.113556.1.4.803:=2)(!pwdLastSet=0))", + GroupsFilter: "(&(|(member={dn})(member={0})(member={1}))(objectClass=group))", + AdditionalUsersDN: "ou=users", + AdditionalGroupsDN: "ou=groups", + BaseDN: "dc=example,dc=com", + StartTLS: true, + }, + nil, + mockFactory) + + assert.Equal(t, "(&(|(uid={input})(mail={input})(displayname={input}))(objectCategory=person)(objectClass=user)(!userAccountControl:1.2.840.113556.1.4.803:=2)(!pwdLastSet=0))", ldapClient.configuration.UsersFilter) + assert.Equal(t, "(&(|(member={dn})(member={input})(member={username}))(objectClass=group))", ldapClient.configuration.GroupsFilter) + assert.Equal(t, "ou=users,dc=example,dc=com", ldapClient.usersDN) + assert.Equal(t, "ou=groups,dc=example,dc=com", ldapClient.groupsDN) +} + func TestShouldCallStartTLSWithInsecureSkipVerifyWhenSkipVerifyTrue(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() @@ -471,22 +736,27 @@ func TestShouldCallStartTLSWithInsecureSkipVerifyWhenSkipVerifyTrue(t *testing.T mockFactory := NewMockLDAPConnectionFactory(ctrl) mockConn := NewMockLDAPConnection(ctrl) - ldapClient := NewLDAPUserProviderWithFactory(schema.LDAPAuthenticationBackendConfiguration{ - URL: "ldap://127.0.0.1:389", - User: "cn=admin,dc=example,dc=com", - Password: "password", - UsernameAttribute: "uid", - MailAttribute: "mail", - DisplayNameAttribute: "displayname", - UsersFilter: "uid={input}", - AdditionalUsersDN: "ou=users", - BaseDN: "dc=example,dc=com", - StartTLS: true, - SkipVerify: true, - }, mockFactory) + ldapClient := NewLDAPUserProviderWithFactory( + schema.LDAPAuthenticationBackendConfiguration{ + URL: "ldap://127.0.0.1:389", + User: "cn=admin,dc=example,dc=com", + Password: "password", + UsernameAttribute: "uid", + MailAttribute: "mail", + DisplayNameAttribute: "displayname", + UsersFilter: "uid={input}", + AdditionalUsersDN: "ou=users", + BaseDN: "dc=example,dc=com", + StartTLS: true, + TLS: &schema.TLSConfig{ + SkipVerify: true, + }, + }, + nil, + mockFactory) mockFactory.EXPECT(). - Dial(gomock.Eq("tcp"), gomock.Eq("127.0.0.1:389")). + DialURL(gomock.Eq("ldap://127.0.0.1:389"), gomock.Any()). Return(mockConn, nil) mockConn.EXPECT(). @@ -544,22 +814,27 @@ func TestShouldReturnLDAPSAlreadySecuredWhenStartTLSAttempted(t *testing.T) { mockFactory := NewMockLDAPConnectionFactory(ctrl) mockConn := NewMockLDAPConnection(ctrl) - ldapClient := NewLDAPUserProviderWithFactory(schema.LDAPAuthenticationBackendConfiguration{ - URL: "ldaps://127.0.0.1:389", - User: "cn=admin,dc=example,dc=com", - Password: "password", - UsernameAttribute: "uid", - MailAttribute: "mail", - DisplayNameAttribute: "displayname", - UsersFilter: "uid={input}", - AdditionalUsersDN: "ou=users", - BaseDN: "dc=example,dc=com", - StartTLS: true, - SkipVerify: true, - }, mockFactory) + ldapClient := NewLDAPUserProviderWithFactory( + schema.LDAPAuthenticationBackendConfiguration{ + URL: "ldaps://127.0.0.1:389", + User: "cn=admin,dc=example,dc=com", + Password: "password", + UsernameAttribute: "uid", + MailAttribute: "mail", + DisplayNameAttribute: "displayname", + UsersFilter: "uid={input}", + AdditionalUsersDN: "ou=users", + BaseDN: "dc=example,dc=com", + StartTLS: true, + TLS: &schema.TLSConfig{ + SkipVerify: true, + }, + }, + nil, + mockFactory) mockFactory.EXPECT(). - DialTLS(gomock.Eq("tcp"), gomock.Eq("127.0.0.1:389"), gomock.Any()). + DialURL(gomock.Eq("ldaps://127.0.0.1:389"), gomock.Any()). Return(mockConn, nil) mockConn.EXPECT(). diff --git a/internal/configuration/reader.go b/internal/configuration/reader.go index 6eeb868b..b64047ab 100644 --- a/internal/configuration/reader.go +++ b/internal/configuration/reader.go @@ -12,6 +12,7 @@ import ( "github.com/authelia/authelia/internal/configuration/schema" "github.com/authelia/authelia/internal/configuration/validator" + "github.com/authelia/authelia/internal/logging" ) // Read a YAML configuration and create a Configuration object out of it. @@ -78,6 +79,12 @@ func Read(configPath string) (*schema.Configuration, []error) { return nil, val.Errors() } + if val.HasWarnings() { + for _, warn := range val.Warnings() { + logging.Logger().Warnf(warn.Error()) + } + } + return &configuration, nil } diff --git a/internal/configuration/schema/authentication.go b/internal/configuration/schema/authentication.go index ee1c1d62..15e4a6f8 100644 --- a/internal/configuration/schema/authentication.go +++ b/internal/configuration/schema/authentication.go @@ -2,22 +2,23 @@ package schema // LDAPAuthenticationBackendConfiguration represents the configuration related to LDAP server. type LDAPAuthenticationBackendConfiguration struct { - Implementation string `mapstructure:"implementation"` - URL string `mapstructure:"url"` - SkipVerify bool `mapstructure:"skip_verify"` - StartTLS bool `mapstructure:"start_tls"` - MinimumTLSVersion string `mapstructure:"minimum_tls_version"` - BaseDN string `mapstructure:"base_dn"` - AdditionalUsersDN string `mapstructure:"additional_users_dn"` - UsersFilter string `mapstructure:"users_filter"` - AdditionalGroupsDN string `mapstructure:"additional_groups_dn"` - GroupsFilter string `mapstructure:"groups_filter"` - GroupNameAttribute string `mapstructure:"group_name_attribute"` - UsernameAttribute string `mapstructure:"username_attribute"` - MailAttribute string `mapstructure:"mail_attribute"` - DisplayNameAttribute string `mapstructure:"display_name_attribute"` - User string `mapstructure:"user"` - Password string `mapstructure:"password"` + Implementation string `mapstructure:"implementation"` + URL string `mapstructure:"url"` + BaseDN string `mapstructure:"base_dn"` + AdditionalUsersDN string `mapstructure:"additional_users_dn"` + UsersFilter string `mapstructure:"users_filter"` + AdditionalGroupsDN string `mapstructure:"additional_groups_dn"` + GroupsFilter string `mapstructure:"groups_filter"` + GroupNameAttribute string `mapstructure:"group_name_attribute"` + UsernameAttribute string `mapstructure:"username_attribute"` + MailAttribute string `mapstructure:"mail_attribute"` + DisplayNameAttribute string `mapstructure:"display_name_attribute"` + User string `mapstructure:"user"` + Password string `mapstructure:"password"` + StartTLS bool `mapstructure:"start_tls"` + TLS *TLSConfig `mapstructure:"tls"` + SkipVerify *bool `mapstructure:"skip_verify"` // Deprecated: Replaced with LDAPAuthenticationBackendConfiguration.TLS.SkipVerify. TODO: Remove in 4.28. + MinimumTLSVersion string `mapstructure:"minimum_tls_version"` // Deprecated: Replaced with LDAPAuthenticationBackendConfiguration.TLS.MinimumVersion. TODO: Remove in 4.28. } // FileAuthenticationBackendConfiguration represents the configuration related to file-based backend. @@ -78,7 +79,9 @@ var DefaultLDAPAuthenticationBackendConfiguration = LDAPAuthenticationBackendCon MailAttribute: "mail", DisplayNameAttribute: "displayname", GroupNameAttribute: "cn", - MinimumTLSVersion: "TLS1.2", + TLS: &TLSConfig{ + MinimumVersion: "TLS1.2", + }, } // DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration represents the default LDAP config for the MSAD Implementation. diff --git a/internal/configuration/schema/configuration.go b/internal/configuration/schema/configuration.go index eae5a1fa..92601a65 100644 --- a/internal/configuration/schema/configuration.go +++ b/internal/configuration/schema/configuration.go @@ -6,6 +6,7 @@ type Configuration struct { Port int `mapstructure:"port"` TLSCert string `mapstructure:"tls_cert"` TLSKey string `mapstructure:"tls_key"` + CertificatesDirectory string `mapstructure:"certificates_directory"` LogLevel string `mapstructure:"log_level"` LogFormat string `mapstructure:"log_format"` LogFilePath string `mapstructure:"log_file_path"` diff --git a/internal/configuration/schema/notifier.go b/internal/configuration/schema/notifier.go index b3817c8c..58c5428c 100644 --- a/internal/configuration/schema/notifier.go +++ b/internal/configuration/schema/notifier.go @@ -7,18 +7,19 @@ type FileSystemNotifierConfiguration struct { // SMTPNotifierConfiguration represents the configuration of the SMTP server to send emails with. type SMTPNotifierConfiguration struct { - Host string `mapstructure:"host"` - Port int `mapstructure:"port"` - Username string `mapstructure:"username"` - Password string `mapstructure:"password"` - Identifier string `mapstructure:"identifier"` - Sender string `mapstructure:"sender"` - Subject string `mapstructure:"subject"` - TrustedCert string `mapstructure:"trusted_cert"` - StartupCheckAddress string `mapstructure:"startup_check_address"` - DisableVerifyCert bool `mapstructure:"disable_verify_cert"` - DisableRequireTLS bool `mapstructure:"disable_require_tls"` - DisableHTMLEmails bool `mapstructure:"disable_html_emails"` + Host string `mapstructure:"host"` + Port int `mapstructure:"port"` + Username string `mapstructure:"username"` + Password string `mapstructure:"password"` + Identifier string `mapstructure:"identifier"` + Sender string `mapstructure:"sender"` + Subject string `mapstructure:"subject"` + StartupCheckAddress string `mapstructure:"startup_check_address"` + DisableRequireTLS bool `mapstructure:"disable_require_tls"` + DisableHTMLEmails bool `mapstructure:"disable_html_emails"` + TLS *TLSConfig `mapstructure:"tls"` + TrustedCert string `mapstructure:"trusted_cert"` // Deprecated: Replaced with Global Option CertificatesDirectory. TODO: Remove in 4.28. + DisableVerifyCert *bool `mapstructure:"disable_verify_cert"` // Deprecated: Replaced with LDAPAuthenticationBackendConfiguration.TLS.SkipVerify. TODO: Remove in 4.28. } // NotifierConfiguration represents the configuration of the notifier to use when sending notifications to users. @@ -32,4 +33,7 @@ type NotifierConfiguration struct { var DefaultSMTPNotifierConfiguration = SMTPNotifierConfiguration{ Subject: "[Authelia] {title}", Identifier: "localhost", + TLS: &TLSConfig{ + MinimumVersion: "TLS1.2", + }, } diff --git a/internal/configuration/schema/shared.go b/internal/configuration/schema/shared.go new file mode 100644 index 00000000..46a48c9e --- /dev/null +++ b/internal/configuration/schema/shared.go @@ -0,0 +1,8 @@ +package schema + +// TLSConfig is a representation of the TLS configuration. +type TLSConfig struct { + MinimumVersion string `mapstructure:"minimum_version"` + SkipVerify bool `mapstructure:"skip_verify"` + ServerName string `mapstructure:"server_name"` +} diff --git a/internal/configuration/schema/validator.go b/internal/configuration/schema/validator.go index bd290b91..a37e3eec 100644 --- a/internal/configuration/schema/validator.go +++ b/internal/configuration/schema/validator.go @@ -10,8 +10,11 @@ import ( // ErrorContainer represents a container where we can add errors and retrieve them. type ErrorContainer interface { Push(err error) + PushWarning(err error) HasErrors() bool + HasWarnings() bool Errors() []error + Warnings() []error } // Validator represents the validator interface. @@ -110,33 +113,51 @@ func (v *Validator) Errors() map[string][]error { // StructValidator is a validator for structs. type StructValidator struct { - errors []error + errors []error + warnings []error } // NewStructValidator is a constructor of struct validator. func NewStructValidator() *StructValidator { val := new(StructValidator) val.errors = make([]error, 0) + val.warnings = make([]error, 0) return val } -// Push an error in the validator. +// Push an error to the validator. func (v *StructValidator) Push(err error) { v.errors = append(v.errors, err) } +// PushWarning error to the validator. +func (v *StructValidator) PushWarning(err error) { + v.warnings = append(v.warnings, err) +} + // HasErrors checks whether the validator contains errors. func (v *StructValidator) HasErrors() bool { return len(v.errors) > 0 } +// HasWarnings checks whether the validator contains warning errors. +func (v *StructValidator) HasWarnings() bool { + return len(v.warnings) > 0 +} + // Errors returns the errors. func (v *StructValidator) Errors() []error { return v.errors } -// Clear errors. +// Warnings returns the warnings. +func (v *StructValidator) Warnings() []error { + return v.warnings +} + +// Clear errors and warnings. func (v *StructValidator) Clear() { v.errors = []error{} + v.warnings = []error{} } diff --git a/internal/configuration/schema/validator_test.go b/internal/configuration/schema/validator_test.go index 421abd2c..2e1371b2 100644 --- a/internal/configuration/schema/validator_test.go +++ b/internal/configuration/schema/validator_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/authelia/authelia/internal/configuration/schema" ) @@ -20,13 +21,14 @@ func (tns *TestNestedStruct) Validate(validator *schema.StructValidator) { } type TestStruct struct { - MustBe10 int - NotEmpty string - SetDefault string - Nested TestNestedStruct - Nested2 TestNestedStruct - NilPtr *int - NestedPtr *TestNestedStruct + MustBe10 int + ShouldBeAbove5 int + NotEmpty string + SetDefault string + Nested TestNestedStruct + Nested2 TestNestedStruct + NilPtr *int + NestedPtr *TestNestedStruct } func (ts *TestStruct) Validate(validator *schema.StructValidator) { @@ -38,6 +40,10 @@ func (ts *TestStruct) Validate(validator *schema.StructValidator) { validator.Push(fmt.Errorf("NotEmpty must not be empty")) } + if ts.ShouldBeAbove5 <= 5 { + validator.PushWarning(fmt.Errorf("ShouldBeAbove5 should be above 5")) + } + if ts.SetDefault == "" { ts.SetDefault = "xyz" } @@ -79,3 +85,32 @@ func TestValidator(t *testing.T) { assert.Equal(t, "xyz", s.SetDefault) } + +func TestStructValidator(t *testing.T) { + validator := schema.NewStructValidator() + s := TestStruct{ + MustBe10: 5, + ShouldBeAbove5: 2, + NotEmpty: "", + NestedPtr: &TestNestedStruct{}, + } + s.Validate(validator) + + assert.True(t, validator.HasWarnings()) + assert.True(t, validator.HasErrors()) + + require.Len(t, validator.Warnings(), 1) + require.Len(t, validator.Errors(), 2) + + assert.EqualError(t, validator.Warnings()[0], "ShouldBeAbove5 should be above 5") + assert.EqualError(t, validator.Errors()[0], "MustBe10 must be 10") + assert.EqualError(t, validator.Errors()[1], "NotEmpty must not be empty") + + validator.Clear() + + assert.False(t, validator.HasWarnings()) + assert.False(t, validator.HasErrors()) + + assert.Len(t, validator.Warnings(), 0) + assert.Len(t, validator.Errors(), 0) +} diff --git a/internal/configuration/validator/authentication.go b/internal/configuration/validator/authentication.go index 98f5f209..98a07043 100644 --- a/internal/configuration/validator/authentication.go +++ b/internal/configuration/validator/authentication.go @@ -72,30 +72,31 @@ func validateFileAuthenticationBackend(configuration *schema.FileAuthenticationB } } -func validateLdapURL(ldapURL string, validator *schema.StructValidator) string { - u, err := url.Parse(ldapURL) +// Wrapper for test purposes to exclude the hostname from the return. +func validateLdapURLSimple(ldapURL string, validator *schema.StructValidator) (finalURL string) { + finalURL, _ = validateLdapURL(ldapURL, validator) + + return finalURL +} + +func validateLdapURL(ldapURL string, validator *schema.StructValidator) (finalURL string, hostname string) { + parsedURL, err := url.Parse(ldapURL) if err != nil { validator.Push(errors.New("Unable to parse URL to ldap server. The scheme is probably missing: ldap:// or ldaps://")) - return "" + return "", "" } - if !(u.Scheme == schemeLDAP || u.Scheme == schemeLDAPS) { + if !(parsedURL.Scheme == schemeLDAP || parsedURL.Scheme == schemeLDAPS) { validator.Push(errors.New("Unknown scheme for ldap url, should be ldap:// or ldaps://")) - return "" + return "", "" } - if u.Scheme == schemeLDAP && u.Port() == "" { - u.Host += ":389" - } else if u.Scheme == schemeLDAPS && u.Port() == "" { - u.Host += ":636" + if !parsedURL.IsAbs() { + validator.Push(fmt.Errorf("URL to LDAP %s is still not absolute, it should be something like ldap://127.0.0.1:389", parsedURL.String())) } - if !u.IsAbs() { - validator.Push(fmt.Errorf("URL to LDAP %s is still not absolute, it should be something like ldap://127.0.0.1:389", u.String())) - } - - return u.String() + return parsedURL.String(), parsedURL.Hostname() } //nolint:gocyclo // TODO: Consider refactoring/simplifying, time permitting. @@ -104,10 +105,35 @@ func validateLdapAuthenticationBackend(configuration *schema.LDAPAuthenticationB configuration.Implementation = schema.DefaultLDAPAuthenticationBackendConfiguration.Implementation } - if configuration.MinimumTLSVersion == "" { - configuration.MinimumTLSVersion = schema.DefaultLDAPAuthenticationBackendConfiguration.MinimumTLSVersion - } else if _, err := utils.TLSStringToTLSConfigVersion(configuration.MinimumTLSVersion); err != nil { - validator.Push(fmt.Errorf("error occurred validating the LDAP minimum_tls_version key with value %s: %v", configuration.MinimumTLSVersion, err)) + nilTLS := configuration.TLS == nil + if nilTLS { + configuration.TLS = schema.DefaultLDAPAuthenticationBackendConfiguration.TLS + } + + // Deprecated. Maps deprecated values to the new ones. TODO: Remove in 4.28 (if block). + if configuration.SkipVerify != nil { + validator.PushWarning(errors.New("DEPRECATED: LDAP Auth Backend `skip_verify` option has been replaced by `authentication_backend.ldap.tls.skip_verify` (will be removed in 4.28.0)")) + + if nilTLS { + configuration.TLS.SkipVerify = *configuration.SkipVerify + } + } + + // Deprecated. Maps deprecated values to the new ones. TODO: Remove in 4.28 (if block). + if configuration.MinimumTLSVersion != "" { + validator.PushWarning(errors.New("DEPRECATED: LDAP Auth Backend `minimum_tls_version` option has been replaced by `authentication_backend.ldap.tls.minimum_version` (will be removed in 4.28.0)")) + + if nilTLS { + configuration.TLS.MinimumVersion = configuration.MinimumTLSVersion + } + } + + if configuration.TLS.MinimumVersion == "" { + configuration.TLS.MinimumVersion = schema.DefaultLDAPAuthenticationBackendConfiguration.TLS.MinimumVersion + } + + if _, err := utils.TLSStringToTLSConfigVersion(configuration.TLS.MinimumVersion); err != nil { + validator.Push(fmt.Errorf("error occurred validating the LDAP minimum_tls_version key with value %s: %v", configuration.TLS.MinimumVersion, err)) } switch configuration.Implementation { @@ -122,7 +148,13 @@ func validateLdapAuthenticationBackend(configuration *schema.LDAPAuthenticationB if configuration.URL == "" { validator.Push(errors.New("Please provide a URL to the LDAP server")) } else { - configuration.URL = validateLdapURL(configuration.URL, validator) + ldapURL, serverName := validateLdapURL(configuration.URL, validator) + + configuration.URL = ldapURL + + if configuration.TLS.ServerName == "" { + configuration.TLS.ServerName = serverName + } } // TODO: see if it's possible to disable this check if disable_reset_password is set and when anonymous/user binding is supported (#101 and #387) diff --git a/internal/configuration/validator/authentication_test.go b/internal/configuration/validator/authentication_test.go index 7da18d0d..cf01525f 100644 --- a/internal/configuration/validator/authentication_test.go +++ b/internal/configuration/validator/authentication_test.go @@ -41,93 +41,127 @@ func (suite *FileBasedAuthenticationBackend) SetupTest() { } func (suite *FileBasedAuthenticationBackend) TestShouldValidateCompleteConfiguration() { ValidateAuthenticationBackend(&suite.configuration, suite.validator) - assert.Len(suite.T(), suite.validator.Errors(), 0) + + suite.Assert().False(suite.validator.HasWarnings()) + suite.Assert().False(suite.validator.HasErrors()) } func (suite *FileBasedAuthenticationBackend) TestShouldRaiseErrorWhenNoPathProvided() { suite.configuration.File.Path = "" + ValidateAuthenticationBackend(&suite.configuration, suite.validator) - require.Len(suite.T(), suite.validator.Errors(), 1) - assert.EqualError(suite.T(), suite.validator.Errors()[0], "Please provide a `path` for the users database in `authentication_backend`") + + suite.Assert().False(suite.validator.HasWarnings()) + suite.Require().Len(suite.validator.Errors(), 1) + + suite.Assert().EqualError(suite.validator.Errors()[0], "Please provide a `path` for the users database in `authentication_backend`") } func (suite *FileBasedAuthenticationBackend) TestShouldRaiseErrorWhenMemoryNotMoreThanEightTimesParallelism() { suite.configuration.File.Password.Memory = 8 suite.configuration.File.Password.Parallelism = 2 + ValidateAuthenticationBackend(&suite.configuration, suite.validator) - require.Len(suite.T(), suite.validator.Errors(), 1) - assert.EqualError(suite.T(), suite.validator.Errors()[0], "Memory for argon2id must be 16 or more (parallelism * 8), you configured memory as 8 and parallelism as 2") + + suite.Assert().False(suite.validator.HasWarnings()) + suite.Require().Len(suite.validator.Errors(), 1) + + suite.Assert().EqualError(suite.validator.Errors()[0], "Memory for argon2id must be 16 or more (parallelism * 8), you configured memory as 8 and parallelism as 2") } func (suite *FileBasedAuthenticationBackend) TestShouldSetDefaultConfigurationWhenBlank() { suite.configuration.File.Password = &schema.PasswordConfiguration{} - assert.Equal(suite.T(), 0, suite.configuration.File.Password.KeyLength) - assert.Equal(suite.T(), 0, suite.configuration.File.Password.Iterations) - assert.Equal(suite.T(), 0, suite.configuration.File.Password.SaltLength) - assert.Equal(suite.T(), "", suite.configuration.File.Password.Algorithm) - assert.Equal(suite.T(), 0, suite.configuration.File.Password.Memory) - assert.Equal(suite.T(), 0, suite.configuration.File.Password.Parallelism) + suite.Assert().Equal(0, suite.configuration.File.Password.KeyLength) + suite.Assert().Equal(0, suite.configuration.File.Password.Iterations) + suite.Assert().Equal(0, suite.configuration.File.Password.SaltLength) + suite.Assert().Equal("", suite.configuration.File.Password.Algorithm) + suite.Assert().Equal(0, suite.configuration.File.Password.Memory) + suite.Assert().Equal(0, suite.configuration.File.Password.Parallelism) ValidateAuthenticationBackend(&suite.configuration, suite.validator) - assert.Len(suite.T(), suite.validator.Errors(), 0) - assert.Equal(suite.T(), schema.DefaultPasswordConfiguration.KeyLength, suite.configuration.File.Password.KeyLength) - assert.Equal(suite.T(), schema.DefaultPasswordConfiguration.Iterations, suite.configuration.File.Password.Iterations) - assert.Equal(suite.T(), schema.DefaultPasswordConfiguration.SaltLength, suite.configuration.File.Password.SaltLength) - assert.Equal(suite.T(), schema.DefaultPasswordConfiguration.Algorithm, suite.configuration.File.Password.Algorithm) - assert.Equal(suite.T(), schema.DefaultPasswordConfiguration.Memory, suite.configuration.File.Password.Memory) - assert.Equal(suite.T(), schema.DefaultPasswordConfiguration.Parallelism, suite.configuration.File.Password.Parallelism) + suite.Assert().False(suite.validator.HasWarnings()) + suite.Assert().False(suite.validator.HasErrors()) + + suite.Assert().Equal(schema.DefaultPasswordConfiguration.KeyLength, suite.configuration.File.Password.KeyLength) + suite.Assert().Equal(schema.DefaultPasswordConfiguration.Iterations, suite.configuration.File.Password.Iterations) + suite.Assert().Equal(schema.DefaultPasswordConfiguration.SaltLength, suite.configuration.File.Password.SaltLength) + suite.Assert().Equal(schema.DefaultPasswordConfiguration.Algorithm, suite.configuration.File.Password.Algorithm) + suite.Assert().Equal(schema.DefaultPasswordConfiguration.Memory, suite.configuration.File.Password.Memory) + suite.Assert().Equal(schema.DefaultPasswordConfiguration.Parallelism, suite.configuration.File.Password.Parallelism) } func (suite *FileBasedAuthenticationBackend) TestShouldSetDefaultConfigurationWhenOnlySHA512Set() { suite.configuration.File.Password = &schema.PasswordConfiguration{} - assert.Equal(suite.T(), "", suite.configuration.File.Password.Algorithm) + suite.Assert().Equal("", suite.configuration.File.Password.Algorithm) suite.configuration.File.Password.Algorithm = "sha512" ValidateAuthenticationBackend(&suite.configuration, suite.validator) - assert.Len(suite.T(), suite.validator.Errors(), 0) - assert.Equal(suite.T(), schema.DefaultPasswordSHA512Configuration.KeyLength, suite.configuration.File.Password.KeyLength) - assert.Equal(suite.T(), schema.DefaultPasswordSHA512Configuration.Iterations, suite.configuration.File.Password.Iterations) - assert.Equal(suite.T(), schema.DefaultPasswordSHA512Configuration.SaltLength, suite.configuration.File.Password.SaltLength) - assert.Equal(suite.T(), schema.DefaultPasswordSHA512Configuration.Algorithm, suite.configuration.File.Password.Algorithm) - assert.Equal(suite.T(), schema.DefaultPasswordSHA512Configuration.Memory, suite.configuration.File.Password.Memory) - assert.Equal(suite.T(), schema.DefaultPasswordSHA512Configuration.Parallelism, suite.configuration.File.Password.Parallelism) + suite.Assert().False(suite.validator.HasWarnings()) + suite.Assert().False(suite.validator.HasErrors()) + + suite.Assert().Equal(schema.DefaultPasswordSHA512Configuration.KeyLength, suite.configuration.File.Password.KeyLength) + suite.Assert().Equal(schema.DefaultPasswordSHA512Configuration.Iterations, suite.configuration.File.Password.Iterations) + suite.Assert().Equal(schema.DefaultPasswordSHA512Configuration.SaltLength, suite.configuration.File.Password.SaltLength) + suite.Assert().Equal(schema.DefaultPasswordSHA512Configuration.Algorithm, suite.configuration.File.Password.Algorithm) + suite.Assert().Equal(schema.DefaultPasswordSHA512Configuration.Memory, suite.configuration.File.Password.Memory) + suite.Assert().Equal(schema.DefaultPasswordSHA512Configuration.Parallelism, suite.configuration.File.Password.Parallelism) } func (suite *FileBasedAuthenticationBackend) TestShouldRaiseErrorWhenKeyLengthTooLow() { suite.configuration.File.Password.KeyLength = 1 + ValidateAuthenticationBackend(&suite.configuration, suite.validator) - require.Len(suite.T(), suite.validator.Errors(), 1) - assert.EqualError(suite.T(), suite.validator.Errors()[0], "Key length for argon2id must be 16, you configured 1") + + suite.Assert().False(suite.validator.HasWarnings()) + suite.Require().Len(suite.validator.Errors(), 1) + + suite.Assert().EqualError(suite.validator.Errors()[0], "Key length for argon2id must be 16, you configured 1") } func (suite *FileBasedAuthenticationBackend) TestShouldRaiseErrorWhenSaltLengthTooLow() { suite.configuration.File.Password.SaltLength = -1 + ValidateAuthenticationBackend(&suite.configuration, suite.validator) - require.Len(suite.T(), suite.validator.Errors(), 1) - assert.EqualError(suite.T(), suite.validator.Errors()[0], "The salt length must be 2 or more, you configured -1") + + suite.Assert().False(suite.validator.HasWarnings()) + suite.Require().Len(suite.validator.Errors(), 1) + + suite.Assert().EqualError(suite.validator.Errors()[0], "The salt length must be 2 or more, you configured -1") } func (suite *FileBasedAuthenticationBackend) TestShouldRaiseErrorWhenBadAlgorithmDefined() { suite.configuration.File.Password.Algorithm = "bogus" + ValidateAuthenticationBackend(&suite.configuration, suite.validator) - require.Len(suite.T(), suite.validator.Errors(), 1) - assert.EqualError(suite.T(), suite.validator.Errors()[0], "Unknown hashing algorithm supplied, valid values are argon2id and sha512, you configured 'bogus'") + + suite.Assert().False(suite.validator.HasWarnings()) + suite.Require().Len(suite.validator.Errors(), 1) + + suite.Assert().EqualError(suite.validator.Errors()[0], "Unknown hashing algorithm supplied, valid values are argon2id and sha512, you configured 'bogus'") } func (suite *FileBasedAuthenticationBackend) TestShouldRaiseErrorWhenIterationsTooLow() { suite.configuration.File.Password.Iterations = -1 + ValidateAuthenticationBackend(&suite.configuration, suite.validator) - require.Len(suite.T(), suite.validator.Errors(), 1) - assert.EqualError(suite.T(), suite.validator.Errors()[0], "The number of iterations specified is invalid, must be 1 or more, you configured -1") + + suite.Assert().False(suite.validator.HasWarnings()) + suite.Require().Len(suite.validator.Errors(), 1) + + suite.Assert().EqualError(suite.validator.Errors()[0], "The number of iterations specified is invalid, must be 1 or more, you configured -1") } func (suite *FileBasedAuthenticationBackend) TestShouldRaiseErrorWhenParallelismTooLow() { suite.configuration.File.Password.Parallelism = -1 + ValidateAuthenticationBackend(&suite.configuration, suite.validator) - require.Len(suite.T(), suite.validator.Errors(), 1) - assert.EqualError(suite.T(), suite.validator.Errors()[0], "Parallelism for argon2id must be 1 or more, you configured -1") + + suite.Assert().False(suite.validator.HasWarnings()) + suite.Require().Len(suite.validator.Errors(), 1) + + suite.Assert().EqualError(suite.validator.Errors()[0], "Parallelism for argon2id must be 1 or more, you configured -1") } func (suite *FileBasedAuthenticationBackend) TestShouldSetDefaultValues() { @@ -136,13 +170,17 @@ func (suite *FileBasedAuthenticationBackend) TestShouldSetDefaultValues() { suite.configuration.File.Password.SaltLength = 0 suite.configuration.File.Password.Memory = 0 suite.configuration.File.Password.Parallelism = 0 + ValidateAuthenticationBackend(&suite.configuration, suite.validator) - assert.Len(suite.T(), suite.validator.Errors(), 0) - assert.Equal(suite.T(), schema.DefaultPasswordConfiguration.Algorithm, suite.configuration.File.Password.Algorithm) - assert.Equal(suite.T(), schema.DefaultPasswordConfiguration.Iterations, suite.configuration.File.Password.Iterations) - assert.Equal(suite.T(), schema.DefaultPasswordConfiguration.SaltLength, suite.configuration.File.Password.SaltLength) - assert.Equal(suite.T(), schema.DefaultPasswordConfiguration.Memory, suite.configuration.File.Password.Memory) - assert.Equal(suite.T(), schema.DefaultPasswordConfiguration.Parallelism, suite.configuration.File.Password.Parallelism) + + suite.Assert().False(suite.validator.HasWarnings()) + suite.Assert().False(suite.validator.HasErrors()) + + suite.Assert().Equal(schema.DefaultPasswordConfiguration.Algorithm, suite.configuration.File.Password.Algorithm) + suite.Assert().Equal(schema.DefaultPasswordConfiguration.Iterations, suite.configuration.File.Password.Iterations) + suite.Assert().Equal(schema.DefaultPasswordConfiguration.SaltLength, suite.configuration.File.Password.SaltLength) + suite.Assert().Equal(schema.DefaultPasswordConfiguration.Memory, suite.configuration.File.Password.Memory) + suite.Assert().Equal(schema.DefaultPasswordConfiguration.Parallelism, suite.configuration.File.Password.Parallelism) } func TestFileBasedAuthenticationBackend(t *testing.T) { @@ -171,158 +209,289 @@ func (suite *LdapAuthenticationBackendSuite) SetupTest() { func (suite *LdapAuthenticationBackendSuite) TestShouldValidateCompleteConfiguration() { ValidateAuthenticationBackend(&suite.configuration, suite.validator) - assert.Len(suite.T(), suite.validator.Errors(), 0) + + suite.Assert().False(suite.validator.HasWarnings()) + suite.Assert().False(suite.validator.HasErrors()) } func (suite *LdapAuthenticationBackendSuite) TestShouldRaiseErrorWhenImplementationIsInvalidMSAD() { suite.configuration.Ldap.Implementation = "masd" + ValidateAuthenticationBackend(&suite.configuration, suite.validator) - require.Len(suite.T(), suite.validator.Errors(), 1) - assert.EqualError(suite.T(), suite.validator.Errors()[0], "authentication backend ldap implementation must be blank or one of the following values `custom`, `activedirectory`") + + suite.Assert().False(suite.validator.HasWarnings()) + suite.Require().Len(suite.validator.Errors(), 1) + + suite.Assert().EqualError(suite.validator.Errors()[0], "authentication backend ldap implementation must be blank or one of the following values `custom`, `activedirectory`") } func (suite *LdapAuthenticationBackendSuite) TestShouldRaiseErrorWhenURLNotProvided() { suite.configuration.Ldap.URL = "" ValidateAuthenticationBackend(&suite.configuration, suite.validator) - require.Len(suite.T(), suite.validator.Errors(), 1) - assert.EqualError(suite.T(), suite.validator.Errors()[0], "Please provide a URL to the LDAP server") + + suite.Assert().False(suite.validator.HasWarnings()) + suite.Require().Len(suite.validator.Errors(), 1) + + suite.Assert().EqualError(suite.validator.Errors()[0], "Please provide a URL to the LDAP server") } func (suite *LdapAuthenticationBackendSuite) TestShouldRaiseErrorWhenUserNotProvided() { suite.configuration.Ldap.User = "" + ValidateAuthenticationBackend(&suite.configuration, suite.validator) - require.Len(suite.T(), suite.validator.Errors(), 1) - assert.EqualError(suite.T(), suite.validator.Errors()[0], "Please provide a user name to connect to the LDAP server") + + suite.Assert().False(suite.validator.HasWarnings()) + suite.Require().Len(suite.validator.Errors(), 1) + + suite.Assert().EqualError(suite.validator.Errors()[0], "Please provide a user name to connect to the LDAP server") } func (suite *LdapAuthenticationBackendSuite) TestShouldRaiseErrorWhenPasswordNotProvided() { suite.configuration.Ldap.Password = "" + ValidateAuthenticationBackend(&suite.configuration, suite.validator) - require.Len(suite.T(), suite.validator.Errors(), 1) - assert.EqualError(suite.T(), suite.validator.Errors()[0], "Please provide a password to connect to the LDAP server") + + suite.Assert().False(suite.validator.HasWarnings()) + suite.Require().Len(suite.validator.Errors(), 1) + + suite.Assert().EqualError(suite.validator.Errors()[0], "Please provide a password to connect to the LDAP server") } func (suite *LdapAuthenticationBackendSuite) TestShouldRaiseErrorWhenBaseDNNotProvided() { suite.configuration.Ldap.BaseDN = "" + ValidateAuthenticationBackend(&suite.configuration, suite.validator) - require.Len(suite.T(), suite.validator.Errors(), 1) - assert.EqualError(suite.T(), suite.validator.Errors()[0], "Please provide a base DN to connect to the LDAP server") + + suite.Assert().False(suite.validator.HasWarnings()) + suite.Assert().Len(suite.validator.Errors(), 1) + + suite.Assert().EqualError(suite.validator.Errors()[0], "Please provide a base DN to connect to the LDAP server") } func (suite *LdapAuthenticationBackendSuite) TestShouldRaiseOnEmptyGroupsFilter() { suite.configuration.Ldap.GroupsFilter = "" + ValidateAuthenticationBackend(&suite.configuration, suite.validator) - require.Len(suite.T(), suite.validator.Errors(), 1) - assert.EqualError(suite.T(), suite.validator.Errors()[0], "Please provide a groups filter with `groups_filter` attribute") + + suite.Assert().False(suite.validator.HasWarnings()) + suite.Require().Len(suite.validator.Errors(), 1) + + suite.Assert().EqualError(suite.validator.Errors()[0], "Please provide a groups filter with `groups_filter` attribute") } func (suite *LdapAuthenticationBackendSuite) TestShouldRaiseOnEmptyUsersFilter() { suite.configuration.Ldap.UsersFilter = "" + ValidateAuthenticationBackend(&suite.configuration, suite.validator) - require.Len(suite.T(), suite.validator.Errors(), 1) - assert.EqualError(suite.T(), suite.validator.Errors()[0], "Please provide a users filter with `users_filter` attribute") + + suite.Assert().False(suite.validator.HasWarnings()) + suite.Require().Len(suite.validator.Errors(), 1) + + suite.Assert().EqualError(suite.validator.Errors()[0], "Please provide a users filter with `users_filter` attribute") } func (suite *LdapAuthenticationBackendSuite) TestShouldNotRaiseOnEmptyUsernameAttribute() { suite.configuration.Ldap.UsernameAttribute = "" + ValidateAuthenticationBackend(&suite.configuration, suite.validator) - assert.Len(suite.T(), suite.validator.Errors(), 0) + + suite.Assert().False(suite.validator.HasWarnings()) + suite.Assert().False(suite.validator.HasErrors()) } func (suite *LdapAuthenticationBackendSuite) TestShouldRaiseOnBadRefreshInterval() { suite.configuration.RefreshInterval = "blah" + ValidateAuthenticationBackend(&suite.configuration, suite.validator) - require.Len(suite.T(), suite.validator.Errors(), 1) - assert.EqualError(suite.T(), suite.validator.Errors()[0], "Auth Backend `refresh_interval` is configured to 'blah' but it must be either a duration notation or one of 'disable', or 'always'. Error from parser: Could not convert the input string of blah into a duration") + + suite.Assert().False(suite.validator.HasWarnings()) + suite.Require().Len(suite.validator.Errors(), 1) + + suite.Assert().EqualError(suite.validator.Errors()[0], "Auth Backend `refresh_interval` is configured to 'blah' but it must be either a duration notation or one of 'disable', or 'always'. Error from parser: Could not convert the input string of blah into a duration") } func (suite *LdapAuthenticationBackendSuite) TestShouldSetDefaultImplementation() { ValidateAuthenticationBackend(&suite.configuration, suite.validator) - assert.Len(suite.T(), suite.validator.Errors(), 0) - assert.Equal(suite.T(), schema.LDAPImplementationCustom, suite.configuration.Ldap.Implementation) + + suite.Assert().False(suite.validator.HasWarnings()) + suite.Assert().False(suite.validator.HasErrors()) + + suite.Assert().Equal(schema.LDAPImplementationCustom, suite.configuration.Ldap.Implementation) } func (suite *LdapAuthenticationBackendSuite) TestShouldSetDefaultGroupNameAttribute() { ValidateAuthenticationBackend(&suite.configuration, suite.validator) - assert.Len(suite.T(), suite.validator.Errors(), 0) - assert.Equal(suite.T(), "cn", suite.configuration.Ldap.GroupNameAttribute) + + suite.Assert().False(suite.validator.HasWarnings()) + suite.Assert().False(suite.validator.HasErrors()) + + suite.Assert().Equal("cn", suite.configuration.Ldap.GroupNameAttribute) } func (suite *LdapAuthenticationBackendSuite) TestShouldSetDefaultMailAttribute() { ValidateAuthenticationBackend(&suite.configuration, suite.validator) - require.Len(suite.T(), suite.validator.Errors(), 0) - assert.Equal(suite.T(), "mail", suite.configuration.Ldap.MailAttribute) + + suite.Assert().False(suite.validator.HasWarnings()) + suite.Assert().False(suite.validator.HasErrors()) + + suite.Assert().Equal("mail", suite.configuration.Ldap.MailAttribute) } func (suite *LdapAuthenticationBackendSuite) TestShouldSetDefaultDisplayNameAttribute() { ValidateAuthenticationBackend(&suite.configuration, suite.validator) - require.Len(suite.T(), suite.validator.Errors(), 0) - assert.Equal(suite.T(), "displayname", suite.configuration.Ldap.DisplayNameAttribute) + + suite.Assert().False(suite.validator.HasWarnings()) + suite.Assert().False(suite.validator.HasErrors()) + + suite.Assert().Equal("displayname", suite.configuration.Ldap.DisplayNameAttribute) } func (suite *LdapAuthenticationBackendSuite) TestShouldSetDefaultRefreshInterval() { ValidateAuthenticationBackend(&suite.configuration, suite.validator) - require.Len(suite.T(), suite.validator.Errors(), 0) - assert.Equal(suite.T(), "5m", suite.configuration.RefreshInterval) + + suite.Assert().False(suite.validator.HasWarnings()) + suite.Assert().False(suite.validator.HasErrors()) + + suite.Assert().Equal("5m", suite.configuration.RefreshInterval) } func (suite *LdapAuthenticationBackendSuite) TestShouldRaiseWhenUsersFilterDoesNotContainEnclosingParenthesis() { suite.configuration.Ldap.UsersFilter = "{username_attribute}={input}" + ValidateAuthenticationBackend(&suite.configuration, suite.validator) - require.Len(suite.T(), suite.validator.Errors(), 1) - assert.EqualError(suite.T(), suite.validator.Errors()[0], "The users filter should contain enclosing parenthesis. For instance {username_attribute}={input} should be ({username_attribute}={input})") + + suite.Assert().False(suite.validator.HasWarnings()) + suite.Require().Len(suite.validator.Errors(), 1) + + suite.Assert().EqualError(suite.validator.Errors()[0], "The users filter should contain enclosing parenthesis. For instance {username_attribute}={input} should be ({username_attribute}={input})") } func (suite *LdapAuthenticationBackendSuite) TestShouldRaiseWhenGroupsFilterDoesNotContainEnclosingParenthesis() { suite.configuration.Ldap.GroupsFilter = "cn={input}" + ValidateAuthenticationBackend(&suite.configuration, suite.validator) - require.Len(suite.T(), suite.validator.Errors(), 1) - assert.EqualError(suite.T(), suite.validator.Errors()[0], "The groups filter should contain enclosing parenthesis. For instance cn={input} should be (cn={input})") + + suite.Assert().False(suite.validator.HasWarnings()) + suite.Require().Len(suite.validator.Errors(), 1) + + suite.Assert().EqualError(suite.validator.Errors()[0], "The groups filter should contain enclosing parenthesis. For instance cn={input} should be (cn={input})") } func (suite *LdapAuthenticationBackendSuite) TestShouldRaiseWhenUsersFilterDoesNotContainUsernameAttribute() { suite.configuration.Ldap.UsersFilter = "(&({mail_attribute}={input})(objectClass=person))" ValidateAuthenticationBackend(&suite.configuration, suite.validator) - require.Len(suite.T(), suite.validator.Errors(), 1) - assert.EqualError(suite.T(), suite.validator.Errors()[0], "Unable to detect {username_attribute} placeholder in users_filter, your configuration is broken. Please review configuration options listed at https://docs.authelia.com/configuration/authentication/ldap.html") + + suite.Assert().False(suite.validator.HasWarnings()) + suite.Require().Len(suite.validator.Errors(), 1) + + suite.Assert().EqualError(suite.validator.Errors()[0], "Unable to detect {username_attribute} placeholder in users_filter, your configuration is broken. Please review configuration options listed at https://docs.authelia.com/configuration/authentication/ldap.html") } func (suite *LdapAuthenticationBackendSuite) TestShouldHelpDetectNoInputPlaceholder() { suite.configuration.Ldap.UsersFilter = "(&({username_attribute}={mail_attribute})(objectClass=person))" + ValidateAuthenticationBackend(&suite.configuration, suite.validator) - require.Len(suite.T(), suite.validator.Errors(), 1) - assert.EqualError(suite.T(), suite.validator.Errors()[0], "Unable to detect {input} placeholder in users_filter, your configuration might be broken. Please review configuration options listed at https://docs.authelia.com/configuration/authentication/ldap.html") + + suite.Assert().False(suite.validator.HasWarnings()) + suite.Require().Len(suite.validator.Errors(), 1) + + suite.Assert().EqualError(suite.validator.Errors()[0], "Unable to detect {input} placeholder in users_filter, your configuration might be broken. Please review configuration options listed at https://docs.authelia.com/configuration/authentication/ldap.html") } func (suite *LdapAuthenticationBackendSuite) TestShouldAdaptLDAPURL() { - assert.Equal(suite.T(), "", validateLdapURL("127.0.0.1", suite.validator)) - require.Len(suite.T(), suite.validator.Errors(), 1) - assert.EqualError(suite.T(), suite.validator.Errors()[0], "Unknown scheme for ldap url, should be ldap:// or ldaps://") + suite.Assert().Equal("", validateLdapURLSimple("127.0.0.1", suite.validator)) - assert.Equal(suite.T(), "", validateLdapURL("127.0.0.1:636", suite.validator)) - require.Len(suite.T(), suite.validator.Errors(), 2) - assert.EqualError(suite.T(), suite.validator.Errors()[1], "Unable to parse URL to ldap server. The scheme is probably missing: ldap:// or ldaps://") + suite.Assert().False(suite.validator.HasWarnings()) + suite.Require().Len(suite.validator.Errors(), 1) - assert.Equal(suite.T(), "ldap://127.0.0.1:389", validateLdapURL("ldap://127.0.0.1", suite.validator)) - assert.Equal(suite.T(), "ldap://127.0.0.1:390", validateLdapURL("ldap://127.0.0.1:390", suite.validator)) - assert.Equal(suite.T(), "ldap://127.0.0.1:389/abc", validateLdapURL("ldap://127.0.0.1/abc", suite.validator)) - assert.Equal(suite.T(), "ldap://127.0.0.1:389/abc?test=abc&x=y", validateLdapURL("ldap://127.0.0.1/abc?test=abc&x=y", suite.validator)) + suite.Assert().EqualError(suite.validator.Errors()[0], "Unknown scheme for ldap url, should be ldap:// or ldaps://") - assert.Equal(suite.T(), "ldaps://127.0.0.1:390", validateLdapURL("ldaps://127.0.0.1:390", suite.validator)) - assert.Equal(suite.T(), "ldaps://127.0.0.1:636", validateLdapURL("ldaps://127.0.0.1", suite.validator)) + suite.Assert().Equal("", validateLdapURLSimple("127.0.0.1:636", suite.validator)) + + suite.Assert().False(suite.validator.HasWarnings()) + suite.Require().Len(suite.validator.Errors(), 2) + suite.Assert().EqualError(suite.validator.Errors()[1], "Unable to parse URL to ldap server. The scheme is probably missing: ldap:// or ldaps://") + + suite.Assert().Equal("ldap://127.0.0.1", validateLdapURLSimple("ldap://127.0.0.1", suite.validator)) + suite.Assert().Equal("ldap://127.0.0.1:390", validateLdapURLSimple("ldap://127.0.0.1:390", suite.validator)) + suite.Assert().Equal("ldap://127.0.0.1/abc", validateLdapURLSimple("ldap://127.0.0.1/abc", suite.validator)) + suite.Assert().Equal("ldap://127.0.0.1/abc?test=abc&x=y", validateLdapURLSimple("ldap://127.0.0.1/abc?test=abc&x=y", suite.validator)) + + suite.Assert().Equal("ldaps://127.0.0.1:390", validateLdapURLSimple("ldaps://127.0.0.1:390", suite.validator)) + suite.Assert().Equal("ldaps://127.0.0.1", validateLdapURLSimple("ldaps://127.0.0.1", suite.validator)) } func (suite *LdapAuthenticationBackendSuite) TestShouldDefaultTLS12() { ValidateAuthenticationBackend(&suite.configuration, suite.validator) - assert.Len(suite.T(), suite.validator.Errors(), 0) - assert.Equal(suite.T(), schema.DefaultLDAPAuthenticationBackendConfiguration.MinimumTLSVersion, suite.configuration.Ldap.MinimumTLSVersion) + + suite.Assert().False(suite.validator.HasWarnings()) + suite.Assert().False(suite.validator.HasErrors()) + + suite.Assert().Equal(schema.DefaultLDAPAuthenticationBackendConfiguration.MinimumTLSVersion, suite.configuration.Ldap.MinimumTLSVersion) } func (suite *LdapAuthenticationBackendSuite) TestShouldNotAllowInvalidTLSValue() { - suite.configuration.Ldap.MinimumTLSVersion = "SSL2.0" + suite.configuration.Ldap.TLS = &schema.TLSConfig{ + MinimumVersion: "SSL2.0", + } + ValidateAuthenticationBackend(&suite.configuration, suite.validator) - require.Len(suite.T(), suite.validator.Errors(), 1) - assert.EqualError(suite.T(), suite.validator.Errors()[0], "error occurred validating the LDAP minimum_tls_version key with value SSL2.0: supplied TLS version isn't supported") + + suite.Assert().False(suite.validator.HasWarnings()) + suite.Require().Len(suite.validator.Errors(), 1) + + suite.Assert().EqualError(suite.validator.Errors()[0], "error occurred validating the LDAP minimum_tls_version key with value SSL2.0: supplied TLS version isn't supported") +} + +// Deprecated: Temporary Test. TODO: Remove in 4.28 (Whole Test). +func (suite *LdapAuthenticationBackendSuite) TestShouldReturnDeprecationWarningsAndNoMappingFor428() { + var skipVerify = true + + suite.configuration.Ldap.MinimumTLSVersion = "TLS1.0" + suite.configuration.Ldap.SkipVerify = &skipVerify + suite.configuration.Ldap.TLS = nil + suite.configuration.Ldap.TLS = &schema.TLSConfig{ + ServerName: "golang.org", + MinimumVersion: "", + } + + ValidateAuthenticationBackend(&suite.configuration, suite.validator) + + // Should not override since TLS schema is defined + suite.Assert().Equal(false, suite.configuration.Ldap.TLS.SkipVerify) + suite.Assert().Equal(schema.DefaultLDAPAuthenticationBackendConfiguration.TLS.MinimumVersion, suite.configuration.Ldap.TLS.MinimumVersion) + + suite.Assert().False(suite.validator.HasErrors()) + suite.Require().Len(suite.validator.Warnings(), 2) + + warnings := suite.validator.Warnings() + + suite.Assert().EqualError(warnings[0], "DEPRECATED: LDAP Auth Backend `skip_verify` option has been replaced by `authentication_backend.ldap.tls.skip_verify` (will be removed in 4.28.0)") + suite.Assert().EqualError(warnings[1], "DEPRECATED: LDAP Auth Backend `minimum_tls_version` option has been replaced by `authentication_backend.ldap.tls.minimum_version` (will be removed in 4.28.0)") +} + +// Deprecated: Temporary Test. TODO: Remove in 4.28 (Whole Test). +func (suite *LdapAuthenticationBackendSuite) TestShouldReturnDeprecationWarningsAndMappingFor428() { + var skipVerify = true + + tlsVersion := "TLS1.1" + + suite.configuration.Ldap.MinimumTLSVersion = tlsVersion + suite.configuration.Ldap.SkipVerify = &skipVerify + + ValidateAuthenticationBackend(&suite.configuration, suite.validator) + + // Should override since TLS schema is not defined + suite.Assert().Equal(true, suite.configuration.Ldap.TLS.SkipVerify) + suite.Assert().Equal(tlsVersion, suite.configuration.Ldap.TLS.MinimumVersion) + + suite.Assert().False(suite.validator.HasErrors()) + suite.Require().Len(suite.validator.Warnings(), 2) + + warnings := suite.validator.Warnings() + + suite.Assert().EqualError(warnings[0], "DEPRECATED: LDAP Auth Backend `skip_verify` option has been replaced by `authentication_backend.ldap.tls.skip_verify` (will be removed in 4.28.0)") + suite.Assert().EqualError(warnings[1], "DEPRECATED: LDAP Auth Backend `minimum_tls_version` option has been replaced by `authentication_backend.ldap.tls.minimum_version` (will be removed in 4.28.0)") } func TestLdapAuthenticationBackend(t *testing.T) { @@ -344,29 +513,31 @@ func (suite *ActiveDirectoryAuthenticationBackendSuite) SetupTest() { suite.configuration.Ldap.User = testLDAPUser suite.configuration.Ldap.Password = testLDAPPassword suite.configuration.Ldap.BaseDN = testLDAPBaseDN + suite.configuration.Ldap.TLS = schema.DefaultLDAPAuthenticationBackendConfiguration.TLS } func (suite *ActiveDirectoryAuthenticationBackendSuite) TestShouldSetActiveDirectoryDefaults() { ValidateAuthenticationBackend(&suite.configuration, suite.validator) - assert.Len(suite.T(), suite.validator.Errors(), 0) + suite.Assert().False(suite.validator.HasWarnings()) + suite.Assert().False(suite.validator.HasErrors()) - assert.Equal(suite.T(), + suite.Assert().Equal( suite.configuration.Ldap.UsersFilter, schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.UsersFilter) - assert.Equal(suite.T(), + suite.Assert().Equal( suite.configuration.Ldap.UsernameAttribute, schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.UsernameAttribute) - assert.Equal(suite.T(), + suite.Assert().Equal( suite.configuration.Ldap.DisplayNameAttribute, schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.DisplayNameAttribute) - assert.Equal(suite.T(), + suite.Assert().Equal( suite.configuration.Ldap.MailAttribute, schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.MailAttribute) - assert.Equal(suite.T(), + suite.Assert().Equal( suite.configuration.Ldap.GroupsFilter, schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.GroupsFilter) - assert.Equal(suite.T(), + suite.Assert().Equal( suite.configuration.Ldap.GroupNameAttribute, schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.GroupNameAttribute) } @@ -381,22 +552,22 @@ func (suite *ActiveDirectoryAuthenticationBackendSuite) TestShouldOnlySetDefault ValidateAuthenticationBackend(&suite.configuration, suite.validator) - assert.NotEqual(suite.T(), + suite.Assert().NotEqual( suite.configuration.Ldap.UsersFilter, schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.UsersFilter) - assert.NotEqual(suite.T(), + suite.Assert().NotEqual( suite.configuration.Ldap.UsernameAttribute, schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.UsernameAttribute) - assert.NotEqual(suite.T(), + suite.Assert().NotEqual( suite.configuration.Ldap.DisplayNameAttribute, schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.DisplayNameAttribute) - assert.NotEqual(suite.T(), + suite.Assert().NotEqual( suite.configuration.Ldap.MailAttribute, schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.MailAttribute) - assert.NotEqual(suite.T(), + suite.Assert().NotEqual( suite.configuration.Ldap.GroupsFilter, schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.GroupsFilter) - assert.NotEqual(suite.T(), + suite.Assert().NotEqual( suite.configuration.Ldap.GroupNameAttribute, schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.GroupNameAttribute) } diff --git a/internal/configuration/validator/configuration.go b/internal/configuration/validator/configuration.go index 99890342..a881226b 100644 --- a/internal/configuration/validator/configuration.go +++ b/internal/configuration/validator/configuration.go @@ -3,6 +3,7 @@ package validator import ( "fmt" "net/url" + "os" "github.com/authelia/authelia/internal/configuration/schema" ) @@ -11,6 +12,7 @@ var defaultPort = 8080 var defaultLogLevel = "info" // ValidateConfiguration and adapt the configuration read from file. +//nolint:gocyclo // This function is likely to always have lots of if/else statements, as long as we keep the flow clean it should be understandable. func ValidateConfiguration(configuration *schema.Configuration, validator *schema.StructValidator) { if configuration.Host == "" { configuration.Host = "0.0.0.0" @@ -26,6 +28,15 @@ func ValidateConfiguration(configuration *schema.Configuration, validator *schem validator.Push(fmt.Errorf("No TLS key provided, please check the \"tls_key\" which has been configured")) } + if configuration.CertificatesDirectory != "" { + info, err := os.Stat(configuration.CertificatesDirectory) + if err != nil { + validator.Push(fmt.Errorf("Error checking certificate directory: %v", err)) + } else if !info.IsDir() { + validator.Push(fmt.Errorf("The path %s specified for certificate_directory is not a directory", configuration.CertificatesDirectory)) + } + } + if configuration.LogLevel == "" { configuration.LogLevel = defaultLogLevel } diff --git a/internal/configuration/validator/configuration_test.go b/internal/configuration/validator/configuration_test.go index 6811030c..dd036f8d 100644 --- a/internal/configuration/validator/configuration_test.go +++ b/internal/configuration/validator/configuration_test.go @@ -152,3 +152,40 @@ func TestShouldRaiseErrorWithBadDefaultRedirectionURL(t *testing.T) { require.Len(t, validator.Errors(), 1) assert.EqualError(t, validator.Errors()[0], "Unable to parse default redirection url") } + +func TestShouldNotOverrideCertificatesDirectoryAndShouldPassWhenBlank(t *testing.T) { + validator := schema.NewStructValidator() + config := newDefaultConfig() + ValidateConfiguration(&config, validator) + require.Len(t, validator.Errors(), 0) + + require.Equal(t, "", config.CertificatesDirectory) +} + +func TestShouldRaiseErrorOnInvalidCertificatesDirectory(t *testing.T) { + validator := schema.NewStructValidator() + config := newDefaultConfig() + config.CertificatesDirectory = "not-a-real-file.go" + + ValidateConfiguration(&config, validator) + + require.Len(t, validator.Errors(), 1) + assert.EqualError(t, validator.Errors()[0], "Error checking certificate directory: stat not-a-real-file.go: no such file or directory") + + validator = schema.NewStructValidator() + config.CertificatesDirectory = "const.go" + ValidateConfiguration(&config, validator) + + require.Len(t, validator.Errors(), 1) + assert.EqualError(t, validator.Errors()[0], "The path const.go specified for certificate_directory is not a directory") +} + +func TestShouldNotRaiseErrorOnValidCertificatesDirectory(t *testing.T) { + validator := schema.NewStructValidator() + config := newDefaultConfig() + config.CertificatesDirectory = "../../suites/common/ssl" + + ValidateConfiguration(&config, validator) + + require.Len(t, validator.Errors(), 0) +} diff --git a/internal/configuration/validator/const.go b/internal/configuration/validator/const.go index acbdea81..af42914b 100644 --- a/internal/configuration/validator/const.go +++ b/internal/configuration/validator/const.go @@ -11,6 +11,7 @@ var validKeys = []string{ "jwt_secret", "tls_key", "tls_cert", + "certificates_directory", // Server Keys. "server.read_buffer_size", @@ -72,9 +73,12 @@ var validKeys = []string{ "notifier.smtp.subject", "notifier.smtp.startup_check_address", "notifier.smtp.disable_require_tls", - "notifier.smtp.disable_verify_cert", - "notifier.smtp.trusted_cert", + "notifier.smtp.trusted_cert", // TODO: Deprecated: Remove in 4.28. "notifier.smtp.disable_html_emails", + "notifier.smtp.tls.minimum_version", + "notifier.smtp.tls.skip_verify", + "notifier.smtp.tls.server_name", + "notifier.smtp.disable_verify_cert", // TODO: Deprecated: Remove in 4.28. // Regulation Keys. "regulation.max_retries", @@ -93,9 +97,6 @@ var validKeys = []string{ // LDAP Authentication Backend Keys. "authentication_backend.ldap.implementation", "authentication_backend.ldap.url", - "authentication_backend.ldap.skip_verify", - "authentication_backend.ldap.start_tls", - "authentication_backend.ldap.minimum_tls_version", "authentication_backend.ldap.base_dn", "authentication_backend.ldap.username_attribute", "authentication_backend.ldap.additional_users_dn", @@ -107,6 +108,12 @@ var validKeys = []string{ "authentication_backend.ldap.display_name_attribute", "authentication_backend.ldap.user", "authentication_backend.ldap.password", + "authentication_backend.ldap.start_tls", + "authentication_backend.ldap.tls.minimum_version", + "authentication_backend.ldap.tls.skip_verify", + "authentication_backend.ldap.tls.server_name", + "authentication_backend.ldap.skip_verify", // TODO: Deprecated: Remove in 4.28. + "authentication_backend.ldap.minimum_tls_version", // TODO: Deprecated: Remove in 4.28. // File Authentication Backend Keys. "authentication_backend.file.path", diff --git a/internal/configuration/validator/notifier.go b/internal/configuration/validator/notifier.go index 7084f42f..24ee5d7e 100644 --- a/internal/configuration/validator/notifier.go +++ b/internal/configuration/validator/notifier.go @@ -1,12 +1,14 @@ package validator import ( + "errors" "fmt" "github.com/authelia/authelia/internal/configuration/schema" ) // ValidateNotifier validates and update notifier configuration. +//nolint:gocyclo // TODO: Remove in 4.28. Should be able to remove this during the removal of deprecated config. func ValidateNotifier(configuration *schema.NotifierConfiguration, validator *schema.StructValidator) { if configuration.SMTP == nil && configuration.FileSystem == nil { validator.Push(fmt.Errorf("Notifier should be either `smtp` or `filesystem`")) @@ -51,6 +53,26 @@ func ValidateNotifier(configuration *schema.NotifierConfiguration, validator *sc configuration.SMTP.Identifier = schema.DefaultSMTPNotifierConfiguration.Identifier } + if configuration.SMTP.TLS == nil { + configuration.SMTP.TLS = schema.DefaultSMTPNotifierConfiguration.TLS + + // Deprecated. Maps deprecated values to the new ones. TODO: Remove in 4.28. + if configuration.SMTP.DisableVerifyCert != nil { + validator.PushWarning(errors.New("DEPRECATED: SMTP Notifier `disable_verify_cert` option has been replaced by `notifier.smtp.tls.skip_verify` (will be removed in 4.28.0)")) + + configuration.SMTP.TLS.SkipVerify = *configuration.SMTP.DisableVerifyCert + } + } + + // Deprecated. Maps deprecated values to the new ones. TODO: Remove in 4.28. + if configuration.SMTP.TrustedCert != "" { + validator.PushWarning(errors.New("DEPRECATED: SMTP Notifier `trusted_cert` option has been replaced by the global option `certificates_directory` (will be removed in 4.28.0)")) + } + + if configuration.SMTP.TLS.ServerName == "" { + configuration.SMTP.TLS.ServerName = configuration.SMTP.Host + } + return } } diff --git a/internal/configuration/validator/notifier_test.go b/internal/configuration/validator/notifier_test.go index dcd6e4a1..85ff7602 100644 --- a/internal/configuration/validator/notifier_test.go +++ b/internal/configuration/validator/notifier_test.go @@ -10,12 +10,13 @@ import ( type NotifierSuite struct { suite.Suite - configuration schema.NotifierConfiguration + validator *schema.StructValidator } -func (s *NotifierSuite) SetupTest() { - s.configuration.SMTP = &schema.SMTPNotifierConfiguration{ +func (suite *NotifierSuite) SetupTest() { + suite.validator = schema.NewStructValidator() + suite.configuration.SMTP = &schema.SMTPNotifierConfiguration{ Username: "john", Password: "password", Sender: "admin@example.com", @@ -24,96 +25,129 @@ func (s *NotifierSuite) SetupTest() { } } -func (s *NotifierSuite) TestShouldEnsureAtLeastSMTPOrFilesystemIsProvided() { - validator := schema.NewStructValidator() - ValidateNotifier(&s.configuration, validator) +func (suite *NotifierSuite) TestShouldEnsureAtLeastSMTPOrFilesystemIsProvided() { + ValidateNotifier(&suite.configuration, suite.validator) - errors := validator.Errors() - s.Require().Len(errors, 0) + suite.Assert().False(suite.validator.HasWarnings()) + suite.Assert().False(suite.validator.HasErrors()) - s.configuration.SMTP = nil + suite.configuration.SMTP = nil - ValidateNotifier(&s.configuration, validator) + ValidateNotifier(&suite.configuration, suite.validator) - errors = validator.Errors() - s.Require().Len(errors, 1) - s.Assert().EqualError(errors[0], "Notifier should be either `smtp` or `filesystem`") + suite.Assert().False(suite.validator.HasWarnings()) + suite.Require().True(suite.validator.HasErrors()) + + suite.Assert().Len(suite.validator.Errors(), 1) + + suite.Assert().EqualError(suite.validator.Errors()[0], "Notifier should be either `smtp` or `filesystem`") } -func (s *NotifierSuite) TestShouldEnsureEitherSMTPOrFilesystemIsProvided() { - validator := schema.NewStructValidator() - ValidateNotifier(&s.configuration, validator) +func (suite *NotifierSuite) TestShouldEnsureEitherSMTPOrFilesystemIsProvided() { + ValidateNotifier(&suite.configuration, suite.validator) - errors := validator.Errors() - s.Require().Len(errors, 0) + suite.Assert().False(suite.validator.HasErrors()) - s.configuration.FileSystem = &schema.FileSystemNotifierConfiguration{ + suite.configuration.FileSystem = &schema.FileSystemNotifierConfiguration{ Filename: "test", } - ValidateNotifier(&s.configuration, validator) + ValidateNotifier(&suite.configuration, suite.validator) - errors = validator.Errors() - s.Require().Len(errors, 1) - s.Assert().EqualError(errors[0], "Notifier should be either `smtp` or `filesystem`") + suite.Assert().False(suite.validator.HasWarnings()) + suite.Require().True(suite.validator.HasErrors()) + + suite.Assert().Len(suite.validator.Errors(), 1) + + suite.Assert().EqualError(suite.validator.Errors()[0], "Notifier should be either `smtp` or `filesystem`") } -func (s *NotifierSuite) TestShouldEnsureFilenameOfFilesystemNotifierIsProvided() { - validator := schema.NewStructValidator() - - s.configuration.SMTP = nil - s.configuration.FileSystem = &schema.FileSystemNotifierConfiguration{ +func (suite *NotifierSuite) TestShouldEnsureFilenameOfFilesystemNotifierIsProvided() { + suite.configuration.SMTP = nil + suite.configuration.FileSystem = &schema.FileSystemNotifierConfiguration{ Filename: "test", } - ValidateNotifier(&s.configuration, validator) + ValidateNotifier(&suite.configuration, suite.validator) - errors := validator.Errors() - s.Require().Len(errors, 0) + suite.Assert().False(suite.validator.HasWarnings()) + suite.Assert().False(suite.validator.HasErrors()) - s.configuration.FileSystem.Filename = "" + suite.configuration.FileSystem.Filename = "" - ValidateNotifier(&s.configuration, validator) + ValidateNotifier(&suite.configuration, suite.validator) - errors = validator.Errors() - s.Require().Len(errors, 1) - s.Assert().EqualError(errors[0], "Filename of filesystem notifier must not be empty") + suite.Assert().False(suite.validator.HasWarnings()) + suite.Require().True(suite.validator.HasErrors()) + + suite.Assert().Len(suite.validator.Errors(), 1) + + suite.Assert().EqualError(suite.validator.Errors()[0], "Filename of filesystem notifier must not be empty") } -func (s *NotifierSuite) TestShouldEnsureHostAndPortOfSMTPNotifierAreProvided() { - s.configuration.FileSystem = nil - validator := schema.NewStructValidator() - ValidateNotifier(&s.configuration, validator) +func (suite *NotifierSuite) TestShouldEnsureHostAndPortOfSMTPNotifierAreProvided() { + suite.configuration.FileSystem = nil + ValidateNotifier(&suite.configuration, suite.validator) - errors := validator.Errors() - s.Require().Len(errors, 0) + suite.Assert().False(suite.validator.HasWarnings()) + suite.Assert().False(suite.validator.HasErrors()) - s.configuration.SMTP.Host = "" - s.configuration.SMTP.Port = 0 + suite.configuration.SMTP.Host = "" + suite.configuration.SMTP.Port = 0 - ValidateNotifier(&s.configuration, validator) + ValidateNotifier(&suite.configuration, suite.validator) - errors = validator.Errors() - s.Require().Len(errors, 2) - s.Assert().EqualError(errors[0], "Host of SMTP notifier must be provided") - s.Assert().EqualError(errors[1], "Port of SMTP notifier must be provided") + suite.Assert().False(suite.validator.HasWarnings()) + suite.Assert().True(suite.validator.HasErrors()) + + errors := suite.validator.Errors() + + suite.Require().Len(errors, 2) + + suite.Assert().EqualError(errors[0], "Host of SMTP notifier must be provided") + suite.Assert().EqualError(errors[1], "Port of SMTP notifier must be provided") } -func (s *NotifierSuite) TestShouldEnsureSenderOfSMTPNotifierAreProvided() { - s.configuration.FileSystem = nil +func (suite *NotifierSuite) TestShouldEnsureSenderOfSMTPNotifierAreProvided() { + suite.configuration.FileSystem = nil - validator := schema.NewStructValidator() - ValidateNotifier(&s.configuration, validator) + ValidateNotifier(&suite.configuration, suite.validator) - errors := validator.Errors() - s.Require().Len(errors, 0) + suite.Assert().False(suite.validator.HasWarnings()) + suite.Assert().False(suite.validator.HasErrors()) - s.configuration.SMTP.Sender = "" + suite.configuration.SMTP.Sender = "" - ValidateNotifier(&s.configuration, validator) + ValidateNotifier(&suite.configuration, suite.validator) - errors = validator.Errors() - s.Require().Len(errors, 1) - s.Assert().EqualError(errors[0], "Sender of SMTP notifier must be provided") + suite.Assert().False(suite.validator.HasWarnings()) + suite.Require().True(suite.validator.HasErrors()) + + suite.Assert().Len(suite.validator.Errors(), 1) + + suite.Assert().EqualError(suite.validator.Errors()[0], "Sender of SMTP notifier must be provided") +} + +// Deprecated: Temporary Test. TODO: Remove in 4.28 (Whole Test). +func (suite *NotifierSuite) TestShouldReturnDeprecationWarningsFor428() { + var disableVerifyCert = true + + suite.configuration.SMTP.TrustedCert = "/tmp" + suite.configuration.SMTP.DisableVerifyCert = &disableVerifyCert + + ValidateNotifier(&suite.configuration, suite.validator) + + suite.Require().True(suite.validator.HasWarnings()) + suite.Assert().False(suite.validator.HasErrors()) + + warnings := suite.validator.Warnings() + + suite.Require().Len(warnings, 2) + + suite.Assert().EqualError(warnings[0], "DEPRECATED: SMTP Notifier `disable_verify_cert` option has been replaced by `notifier.smtp.tls.skip_verify` (will be removed in 4.28.0)") + suite.Assert().EqualError(warnings[1], "DEPRECATED: SMTP Notifier `trusted_cert` option has been replaced by the global option `certificates_directory` (will be removed in 4.28.0)") + + // Should override since TLS schema is not defined + suite.Assert().Equal(true, suite.configuration.SMTP.TLS.SkipVerify) } func TestNotifierSuite(t *testing.T) { diff --git a/internal/configuration/validator/storage_test.go b/internal/configuration/validator/storage_test.go index 952235e4..a9f44200 100644 --- a/internal/configuration/validator/storage_test.go +++ b/internal/configuration/validator/storage_test.go @@ -10,80 +10,87 @@ import ( type StorageSuite struct { suite.Suite - configuration schema.StorageConfiguration + validator *schema.StructValidator } -func (s *StorageSuite) SetupTest() { - s.configuration.Local = &schema.LocalStorageConfiguration{ +func (suite *StorageSuite) SetupTest() { + suite.validator = schema.NewStructValidator() + suite.configuration.Local = &schema.LocalStorageConfiguration{ Path: "/this/is/a/path", } } -func (s *StorageSuite) TestShouldValidateOneStorageIsConfigured() { - validator := schema.NewStructValidator() - s.configuration.Local = nil +func (suite *StorageSuite) TestShouldValidateOneStorageIsConfigured() { + suite.configuration.Local = nil - ValidateStorage(s.configuration, validator) + ValidateStorage(suite.configuration, suite.validator) - s.Require().Len(validator.Errors(), 1) - s.Assert().EqualError(validator.Errors()[0], "A storage configuration must be provided. It could be 'local', 'mysql' or 'postgres'") + suite.Assert().False(suite.validator.HasWarnings()) + suite.Require().Len(suite.validator.Errors(), 1) + suite.Assert().EqualError(suite.validator.Errors()[0], "A storage configuration must be provided. It could be 'local', 'mysql' or 'postgres'") } -func (s *StorageSuite) TestShouldValidateLocalPathIsProvided() { - validator := schema.NewStructValidator() - s.configuration.Local.Path = "" +func (suite *StorageSuite) TestShouldValidateLocalPathIsProvided() { + suite.configuration.Local.Path = "" - ValidateStorage(s.configuration, validator) + ValidateStorage(suite.configuration, suite.validator) - s.Require().Len(validator.Errors(), 1) - s.Assert().EqualError(validator.Errors()[0], "A file path must be provided with key 'path'") + suite.Assert().False(suite.validator.HasWarnings()) + suite.Require().Len(suite.validator.Errors(), 1) - validator = schema.NewStructValidator() - s.configuration.Local.Path = "/myapth" - ValidateStorage(s.configuration, validator) - s.Require().Len(validator.Errors(), 0) + suite.Assert().EqualError(suite.validator.Errors()[0], "A file path must be provided with key 'path'") + + suite.validator.Clear() + suite.configuration.Local.Path = "/myapth" + + ValidateStorage(suite.configuration, suite.validator) + + suite.Assert().False(suite.validator.HasWarnings()) + suite.Assert().False(suite.validator.HasErrors()) } -func (s *StorageSuite) TestShouldValidateSQLUsernamePasswordAndDatabaseAreProvided() { - validator := schema.NewStructValidator() - s.configuration.MySQL = &schema.MySQLStorageConfiguration{} - ValidateStorage(s.configuration, validator) +func (suite *StorageSuite) TestShouldValidateSQLUsernamePasswordAndDatabaseAreProvided() { + suite.configuration.MySQL = &schema.MySQLStorageConfiguration{} + ValidateStorage(suite.configuration, suite.validator) - s.Require().Len(validator.Errors(), 2) - s.Assert().EqualError(validator.Errors()[0], "Username and password must be provided") - s.Assert().EqualError(validator.Errors()[1], "A database must be provided") + suite.Require().Len(suite.validator.Errors(), 2) + suite.Assert().EqualError(suite.validator.Errors()[0], "Username and password must be provided") + suite.Assert().EqualError(suite.validator.Errors()[1], "A database must be provided") - validator = schema.NewStructValidator() - s.configuration.MySQL = &schema.MySQLStorageConfiguration{ + suite.validator.Clear() + suite.configuration.MySQL = &schema.MySQLStorageConfiguration{ SQLStorageConfiguration: schema.SQLStorageConfiguration{ Username: "myuser", Password: "pass", Database: "database", }, } - ValidateStorage(s.configuration, validator) + ValidateStorage(suite.configuration, suite.validator) - s.Require().Len(validator.Errors(), 0) + suite.Assert().False(suite.validator.HasWarnings()) + suite.Assert().False(suite.validator.HasErrors()) } -func (s *StorageSuite) TestShouldValidatePostgresSSLModeIsDisableByDefault() { - validator := schema.NewStructValidator() - s.configuration.PostgreSQL = &schema.PostgreSQLStorageConfiguration{ +func (suite *StorageSuite) TestShouldValidatePostgresSSLModeIsDisableByDefault() { + suite.configuration.PostgreSQL = &schema.PostgreSQLStorageConfiguration{ SQLStorageConfiguration: schema.SQLStorageConfiguration{ Username: "myuser", Password: "pass", Database: "database", }, } - ValidateStorage(s.configuration, validator) - s.Assert().Equal("disable", s.configuration.PostgreSQL.SSLMode) + ValidateStorage(suite.configuration, suite.validator) + + suite.Assert().False(suite.validator.HasWarnings()) + suite.Assert().False(suite.validator.HasErrors()) + + suite.Assert().Equal("disable", suite.configuration.PostgreSQL.SSLMode) } -func (s *StorageSuite) TestShouldValidatePostgresSSLModeMustBeValid() { - validator := schema.NewStructValidator() - s.configuration.PostgreSQL = &schema.PostgreSQLStorageConfiguration{ +func (suite *StorageSuite) TestShouldValidatePostgresSSLModeMustBeValid() { + suite.configuration.PostgreSQL = &schema.PostgreSQLStorageConfiguration{ SQLStorageConfiguration: schema.SQLStorageConfiguration{ Username: "myuser", Password: "pass", @@ -91,10 +98,12 @@ func (s *StorageSuite) TestShouldValidatePostgresSSLModeMustBeValid() { }, SSLMode: "unknown", } - ValidateStorage(s.configuration, validator) - s.Require().Len(validator.Errors(), 1) - s.Assert().EqualError(validator.Errors()[0], "SSL mode must be 'disable', 'require', 'verify-ca', or 'verify-full'") + ValidateStorage(suite.configuration, suite.validator) + + suite.Assert().False(suite.validator.HasWarnings()) + suite.Require().Len(suite.validator.Errors(), 1) + suite.Assert().EqualError(suite.validator.Errors()[0], "SSL mode must be 'disable', 'require', 'verify-ca', or 'verify-full'") } func TestShouldRunStorageSuite(t *testing.T) { diff --git a/internal/notification/smtp_notifier.go b/internal/notification/smtp_notifier.go index 2faec77c..db9bcc2c 100644 --- a/internal/notification/smtp_notifier.go +++ b/internal/notification/smtp_notifier.go @@ -5,7 +5,6 @@ import ( "crypto/x509" "errors" "fmt" - "io/ioutil" "net/smtp" "strings" "time" @@ -23,8 +22,6 @@ type SMTPNotifier struct { identifier string host string port int - trustedCert string - disableVerifyCert bool disableRequireTLS bool address string subject string @@ -34,7 +31,7 @@ type SMTPNotifier struct { } // NewSMTPNotifier creates a SMTPNotifier using the notifier configuration. -func NewSMTPNotifier(configuration schema.SMTPNotifierConfiguration) *SMTPNotifier { +func NewSMTPNotifier(configuration schema.SMTPNotifierConfiguration, certPool *x509.CertPool) *SMTPNotifier { notifier := &SMTPNotifier{ username: configuration.Username, password: configuration.Password, @@ -42,59 +39,16 @@ func NewSMTPNotifier(configuration schema.SMTPNotifierConfiguration) *SMTPNotifi identifier: configuration.Identifier, host: configuration.Host, port: configuration.Port, - trustedCert: configuration.TrustedCert, - disableVerifyCert: configuration.DisableVerifyCert, disableRequireTLS: configuration.DisableRequireTLS, address: fmt.Sprintf("%s:%d", configuration.Host, configuration.Port), subject: configuration.Subject, startupCheckAddress: configuration.StartupCheckAddress, + tlsConfig: utils.NewTLSConfig(configuration.TLS, tls.VersionTLS12, certPool), } - notifier.initializeTLSConfig() return notifier } -func (n *SMTPNotifier) initializeTLSConfig() { - // Do not allow users to disable verification of certs if they have also set a trusted cert that was loaded - // The second part of this check happens in the Configure Cert Pool code block - logging.Logger().Debug("Notifier SMTP client initializing TLS configuration") - - // Configure Cert Pool - certPool, err := x509.SystemCertPool() - if err != nil || certPool == nil { - certPool = x509.NewCertPool() - } - - if n.trustedCert != "" { - logging.Logger().Debugf("Notifier SMTP client attempting to load certificate from %s", n.trustedCert) - - if exists, err := utils.FileExists(n.trustedCert); exists { - pem, err := ioutil.ReadFile(n.trustedCert) - if err != nil { - logging.Logger().Warnf("Notifier SMTP failed to load cert from file with error: %s", err) - } else { - if ok := certPool.AppendCertsFromPEM(pem); !ok { - logging.Logger().Warn("Notifier SMTP failed to import cert loaded from file") - } else { - logging.Logger().Debug("Notifier SMTP successfully loaded certificate") - if n.disableVerifyCert { - logging.Logger().Warn("Notifier SMTP when trusted_cert is specified we force disable_verify_cert to false, if you want to disable certificate validation please comment/delete trusted_cert from your config") - n.disableVerifyCert = false - } - } - } - } else { - logging.Logger().Warnf("Notifier SMTP failed to load cert from file (file does not exist) with error: %s", err) - } - } - - n.tlsConfig = &tls.Config{ - InsecureSkipVerify: n.disableVerifyCert, //nolint:gosec // This is an intended config, we never default true, provide alternate options, and we constantly warn the user. - ServerName: n.host, - RootCAs: certPool, - } -} - // Do startTLS if available (some servers only provide the auth extension after, and encryption is preferred). func (n *SMTPNotifier) startTLS() error { // Only start if not already encrypted diff --git a/internal/notification/smtp_notifier_test.go b/internal/notification/smtp_notifier_test.go new file mode 100644 index 00000000..e9c2126a --- /dev/null +++ b/internal/notification/smtp_notifier_test.go @@ -0,0 +1,57 @@ +package notification + +import ( + "crypto/tls" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/authelia/authelia/internal/configuration/schema" + "github.com/authelia/authelia/internal/configuration/validator" +) + +func TestShouldConfigureSMTPNotifierWithTLS11AndDefaultHostname(t *testing.T) { + config := &schema.NotifierConfiguration{ + DisableStartupCheck: true, + SMTP: &schema.SMTPNotifierConfiguration{ + Host: "smtp.example.com", + Port: 25, + TLS: &schema.TLSConfig{ + MinimumVersion: "TLS1.1", + }, + }, + } + + sv := schema.NewStructValidator() + validator.ValidateNotifier(config, sv) + + notifier := NewSMTPNotifier(*config.SMTP, nil) + + assert.Equal(t, "smtp.example.com", notifier.tlsConfig.ServerName) + assert.Equal(t, uint16(tls.VersionTLS11), notifier.tlsConfig.MinVersion) + assert.False(t, notifier.tlsConfig.InsecureSkipVerify) + assert.Equal(t, "smtp.example.com:25", notifier.address) +} + +func TestShouldConfigureSMTPNotifierWithServerNameOverrideAndDefaultTLS12(t *testing.T) { + config := &schema.NotifierConfiguration{ + DisableStartupCheck: true, + SMTP: &schema.SMTPNotifierConfiguration{ + Host: "smtp.example.com", + Port: 25, + TLS: &schema.TLSConfig{ + ServerName: "smtp.golang.org", + }, + }, + } + + sv := schema.NewStructValidator() + validator.ValidateNotifier(config, sv) + + notifier := NewSMTPNotifier(*config.SMTP, nil) + + assert.Equal(t, "smtp.golang.org", notifier.tlsConfig.ServerName) + assert.Equal(t, uint16(tls.VersionTLS12), notifier.tlsConfig.MinVersion) + assert.False(t, notifier.tlsConfig.InsecureSkipVerify) + assert.Equal(t, "smtp.example.com:25", notifier.address) +} diff --git a/internal/suites/ActiveDirectory/configuration.yml b/internal/suites/ActiveDirectory/configuration.yml index fc84792c..cfe72910 100644 --- a/internal/suites/ActiveDirectory/configuration.yml +++ b/internal/suites/ActiveDirectory/configuration.yml @@ -16,7 +16,8 @@ authentication_backend: ldap: implementation: activedirectory url: ldap://sambaldap - skip_verify: true + tls: + skip_verify: true start_tls: true base_dn: DC=example,DC=com username_attribute: sAMAccountName diff --git a/internal/suites/LDAP/configuration.yml b/internal/suites/LDAP/configuration.yml index b0c11d78..c9a9f728 100644 --- a/internal/suites/LDAP/configuration.yml +++ b/internal/suites/LDAP/configuration.yml @@ -15,7 +15,8 @@ jwt_secret: very_important_secret authentication_backend: ldap: url: ldaps://openldap - skip_verify: true + tls: + skip_verify: true base_dn: dc=example,dc=com username_attribute: uid additional_users_dn: ou=users diff --git a/internal/suites/ShortTimeouts/configuration.yml b/internal/suites/ShortTimeouts/configuration.yml index 2954d7eb..812a1c6c 100644 --- a/internal/suites/ShortTimeouts/configuration.yml +++ b/internal/suites/ShortTimeouts/configuration.yml @@ -65,7 +65,7 @@ access_control: regulation: max_retries: 3 - find_time: 3 + find_time: 5 ban_time: 10 notifier: diff --git a/internal/suites/example/kube/authelia/configs/configuration.yml b/internal/suites/example/kube/authelia/configs/configuration.yml index 6be69cd9..2fe7ce3d 100644 --- a/internal/suites/example/kube/authelia/configs/configuration.yml +++ b/internal/suites/example/kube/authelia/configs/configuration.yml @@ -13,7 +13,8 @@ default_redirection_url: https://home.example.com:8080 authentication_backend: ldap: url: ldaps://ldap-service - skip_verify: true + tls: + skip_verify: true base_dn: dc=example,dc=com username_attribute: uid additional_users_dn: ou=users diff --git a/internal/utils/certificates.go b/internal/utils/certificates.go new file mode 100644 index 00000000..a7d4be7f --- /dev/null +++ b/internal/utils/certificates.go @@ -0,0 +1,91 @@ +package utils + +import ( + "crypto/tls" + "crypto/x509" + "fmt" + "io/ioutil" + "path" + "strings" + + "github.com/authelia/authelia/internal/configuration/schema" +) + +// NewTLSConfig generates a tls.Config from a schema.TLSConfig and a x509.CertPool. +func NewTLSConfig(config *schema.TLSConfig, defaultMinVersion uint16, certPool *x509.CertPool) (tlsConfig *tls.Config) { + minVersion, err := TLSStringToTLSConfigVersion(config.MinimumVersion) + if err != nil { + minVersion = defaultMinVersion + } + + return &tls.Config{ + ServerName: config.ServerName, + InsecureSkipVerify: config.SkipVerify, //nolint:gosec // Informed choice by user. Off by default. + MinVersion: minVersion, + RootCAs: certPool, + } +} + +//nolint:gocyclo // TODO: Remove in 4.28. Should be able to remove the nolint during the removal of deprecated config. +// NewX509CertPool generates a x509.CertPool from the system PKI and the directory specified. +func NewX509CertPool(directory string, config *schema.Configuration) (certPool *x509.CertPool, errors []error, nonFatalErrors []error) { + certPool, err := x509.SystemCertPool() + if err != nil { + nonFatalErrors = append(nonFatalErrors, fmt.Errorf("could not load system certificate pool which may result in untruested certificate issues: %v", err)) + certPool = x509.NewCertPool() + } + + if directory != "" { + certsFileInfo, err := ioutil.ReadDir(directory) + if err != nil { + errors = append(errors, fmt.Errorf("could not read certificates from directory %v", err)) + } else { + for _, certFileInfo := range certsFileInfo { + nameLower := strings.ToLower(certFileInfo.Name()) + + if !certFileInfo.IsDir() && (strings.HasSuffix(nameLower, ".cer") || strings.HasSuffix(nameLower, ".pem")) { + certBytes, err := ioutil.ReadFile(path.Join(directory, certFileInfo.Name())) + if err != nil { + errors = append(errors, fmt.Errorf("could not read certificate %v", err)) + } else if ok := certPool.AppendCertsFromPEM(certBytes); !ok { + errors = append(errors, fmt.Errorf("could not import certificate %s", certFileInfo.Name())) + } + } + } + } + } + + // Deprecated. Maps deprecated values to the new ones. TODO: Remove in 4.28. + if config != nil && config.Notifier != nil && config.Notifier.SMTP != nil && config.Notifier.SMTP.TrustedCert != "" { + nonFatalErrors = append(nonFatalErrors, fmt.Errorf("defining the trusted cert in the SMTP notifier is deprecated and will be removed in 4.28.0, please use the global certificates_directory instead")) + + if exists, _ := FileExists(config.Notifier.SMTP.TrustedCert); exists { + pem, err := ioutil.ReadFile(config.Notifier.SMTP.TrustedCert) + if err != nil { + errors = append(errors, fmt.Errorf("failed to read legacy SMTP trusted_cert (see the new certificates_directory option) certificate %s with error: %s", config.Notifier.SMTP.TrustedCert, err)) + } else if ok := certPool.AppendCertsFromPEM(pem); !ok { + errors = append(errors, fmt.Errorf("could not import legacy SMTP trusted_cert (see the new certificates_directory option) certificate %s", config.Notifier.SMTP.TrustedCert)) + } + } else { + errors = append(errors, fmt.Errorf("could not import legacy SMTP trusted_cert (see the new certificates_directory option) certificate %s (file does not exist)", config.Notifier.SMTP.TrustedCert)) + } + } + + return certPool, errors, nonFatalErrors +} + +// TLSStringToTLSConfigVersion returns a go crypto/tls version for a tls.Config based on string input. +func TLSStringToTLSConfigVersion(input string) (version uint16, err error) { + switch strings.ToUpper(input) { + case "TLS1.3", TLS13: + return tls.VersionTLS13, nil + case "TLS1.2", TLS12: + return tls.VersionTLS12, nil + case "TLS1.1", TLS11: + return tls.VersionTLS11, nil + case "TLS1.0", TLS10: + return tls.VersionTLS10, nil + } + + return 0, ErrTLSVersionNotSupported +} diff --git a/internal/utils/certificates_test.go b/internal/utils/certificates_test.go new file mode 100644 index 00000000..4bc5d2bd --- /dev/null +++ b/internal/utils/certificates_test.go @@ -0,0 +1,134 @@ +package utils + +import ( + "crypto/tls" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/authelia/authelia/internal/configuration/schema" +) + +func TestShouldSetupDefaultTLSMinVersionOnErr(t *testing.T) { + schemaTLSConfig := &schema.TLSConfig{ + MinimumVersion: "NotAVersion", + ServerName: "golang.org", + SkipVerify: true, + } + + tlsConfig := NewTLSConfig(schemaTLSConfig, tls.VersionTLS12, nil) + + assert.Equal(t, uint16(tls.VersionTLS12), tlsConfig.MinVersion) + assert.Equal(t, "golang.org", tlsConfig.ServerName) + assert.True(t, tlsConfig.InsecureSkipVerify) +} + +func TestShouldReturnCorrectTLSVersions(t *testing.T) { + tls13 := uint16(tls.VersionTLS13) + tls12 := uint16(tls.VersionTLS12) + tls11 := uint16(tls.VersionTLS11) + tls10 := uint16(tls.VersionTLS10) + + version, err := TLSStringToTLSConfigVersion(TLS13) + assert.Equal(t, tls13, version) + assert.NoError(t, err) + + version, err = TLSStringToTLSConfigVersion("TLS" + TLS13) + assert.Equal(t, tls13, version) + assert.NoError(t, err) + + version, err = TLSStringToTLSConfigVersion(TLS12) + assert.Equal(t, tls12, version) + assert.NoError(t, err) + + version, err = TLSStringToTLSConfigVersion("TLS" + TLS12) + assert.Equal(t, tls12, version) + assert.NoError(t, err) + + version, err = TLSStringToTLSConfigVersion(TLS11) + assert.Equal(t, tls11, version) + assert.NoError(t, err) + + version, err = TLSStringToTLSConfigVersion("TLS" + TLS11) + assert.Equal(t, tls11, version) + assert.NoError(t, err) + + version, err = TLSStringToTLSConfigVersion(TLS10) + assert.Equal(t, tls10, version) + assert.NoError(t, err) + + version, err = TLSStringToTLSConfigVersion("TLS" + TLS10) + assert.Equal(t, tls10, version) + assert.NoError(t, err) +} + +func TestShouldReturnZeroAndErrorOnInvalidTLSVersions(t *testing.T) { + version, err := TLSStringToTLSConfigVersion("TLS1.4") + assert.Error(t, err) + assert.Equal(t, uint16(0), version) + assert.EqualError(t, err, "supplied TLS version isn't supported") + + version, err = TLSStringToTLSConfigVersion("SSL3.0") + assert.Error(t, err) + assert.Equal(t, uint16(0), version) + assert.EqualError(t, err, "supplied TLS version isn't supported") +} + +func TestShouldReturnErrWhenX509DirectoryNotExist(t *testing.T) { + pool, errs, nonFatalErrs := NewX509CertPool("/tmp/asdfzyxabc123/not/a/real/dir", nil) + assert.NotNil(t, pool) + assert.Len(t, nonFatalErrs, 0) + require.Len(t, errs, 1) + assert.EqualError(t, errs[0], "could not read certificates from directory open /tmp/asdfzyxabc123/not/a/real/dir: no such file or directory") +} + +func TestShouldNotReturnErrWhenX509DirectoryExist(t *testing.T) { + pool, errs, nonFatalErrs := NewX509CertPool("/tmp", nil) + assert.NotNil(t, pool) + assert.Len(t, nonFatalErrs, 0) + assert.Len(t, errs, 0) +} + +func TestShouldRaiseNonFatalErrWhenNotifierTrustedCertConfigured(t *testing.T) { + config := &schema.Configuration{ + Notifier: &schema.NotifierConfiguration{ + SMTP: &schema.SMTPNotifierConfiguration{ + TrustedCert: "../suites/common/ssl/cert.pem", + }, + }, + } + + pool, errs, nonFatalErrs := NewX509CertPool("/tmp", config) + assert.NotNil(t, pool) + require.Len(t, nonFatalErrs, 1) + assert.Len(t, errs, 0) + + assert.EqualError(t, nonFatalErrs[0], "defining the trusted cert in the SMTP notifier is deprecated and will be removed in 4.28.0, please use the global certificates_directory instead") +} + +func TestShouldRaiseErrAndNonFatalErrWhenNotifierTrustedCertConfiguredAndNotExist(t *testing.T) { + config := &schema.Configuration{ + Notifier: &schema.NotifierConfiguration{ + SMTP: &schema.SMTPNotifierConfiguration{ + TrustedCert: "/tmp/asdfzyxabc123/not/a/real/cert.pem", + }, + }, + } + + pool, errs, nonFatalErrs := NewX509CertPool("/tmp", config) + assert.NotNil(t, pool) + require.Len(t, nonFatalErrs, 1) + require.Len(t, errs, 1) + + assert.EqualError(t, errs[0], "could not import legacy SMTP trusted_cert (see the new certificates_directory option) certificate /tmp/asdfzyxabc123/not/a/real/cert.pem (file does not exist)") + assert.EqualError(t, nonFatalErrs[0], "defining the trusted cert in the SMTP notifier is deprecated and will be removed in 4.28.0, please use the global certificates_directory instead") +} + +func TestShouldReadCertsFromDirectoryButNotKeys(t *testing.T) { + pool, errs, nonFatalErrs := NewX509CertPool("../suites/common/ssl/", nil) + assert.NotNil(t, pool) + require.Len(t, errs, 1) + assert.Len(t, nonFatalErrs, 0) + assert.EqualError(t, errs[0], "could not import certificate key.pem") +} diff --git a/internal/utils/strings.go b/internal/utils/strings.go index 092873b2..8f5b86c9 100644 --- a/internal/utils/strings.go +++ b/internal/utils/strings.go @@ -1,7 +1,6 @@ package utils import ( - "crypto/tls" "math/rand" "strings" "time" @@ -104,19 +103,3 @@ func RandomString(n int, characters []rune) (randomString string) { return string(b) } - -// TLSStringToTLSConfigVersion returns a go crypto/tls version for a tls.Config based on string input. -func TLSStringToTLSConfigVersion(input string) (version uint16, err error) { - switch strings.ToUpper(input) { - case "TLS1.3", TLS13: - return tls.VersionTLS13, nil - case "TLS1.2", TLS12: - return tls.VersionTLS12, nil - case "TLS1.1", TLS11: - return tls.VersionTLS11, nil - case "TLS1.0", TLS10: - return tls.VersionTLS10, nil - } - - return 0, ErrTLSVersionNotSupported -} diff --git a/internal/utils/strings_test.go b/internal/utils/strings_test.go index 9e6cbf05..8ec896a1 100644 --- a/internal/utils/strings_test.go +++ b/internal/utils/strings_test.go @@ -1,7 +1,6 @@ package utils import ( - "crypto/tls" "testing" "github.com/stretchr/testify/assert" @@ -83,54 +82,3 @@ func TestShouldNotFindStringInSliceContains(t *testing.T) { s := IsStringInSliceContains(a, b) assert.False(t, s) } - -func TestShouldReturnCorrectTLSVersions(t *testing.T) { - tls13 := uint16(tls.VersionTLS13) - tls12 := uint16(tls.VersionTLS12) - tls11 := uint16(tls.VersionTLS11) - tls10 := uint16(tls.VersionTLS10) - - version, err := TLSStringToTLSConfigVersion(TLS13) - assert.Equal(t, tls13, version) - assert.NoError(t, err) - - version, err = TLSStringToTLSConfigVersion("TLS" + TLS13) - assert.Equal(t, tls13, version) - assert.NoError(t, err) - - version, err = TLSStringToTLSConfigVersion(TLS12) - assert.Equal(t, tls12, version) - assert.NoError(t, err) - - version, err = TLSStringToTLSConfigVersion("TLS" + TLS12) - assert.Equal(t, tls12, version) - assert.NoError(t, err) - - version, err = TLSStringToTLSConfigVersion(TLS11) - assert.Equal(t, tls11, version) - assert.NoError(t, err) - - version, err = TLSStringToTLSConfigVersion("TLS" + TLS11) - assert.Equal(t, tls11, version) - assert.NoError(t, err) - - version, err = TLSStringToTLSConfigVersion(TLS10) - assert.Equal(t, tls10, version) - assert.NoError(t, err) - - version, err = TLSStringToTLSConfigVersion("TLS" + TLS10) - assert.Equal(t, tls10, version) - assert.NoError(t, err) -} - -func TestShouldReturnZeroAndErrorOnInvalidTLSVersions(t *testing.T) { - version, err := TLSStringToTLSConfigVersion("TLS1.4") - assert.Error(t, err) - assert.Equal(t, uint16(0), version) - assert.EqualError(t, err, "supplied TLS version isn't supported") - - version, err = TLSStringToTLSConfigVersion("SSL3.0") - assert.Error(t, err) - assert.Equal(t, uint16(0), version) - assert.EqualError(t, err, "supplied TLS version isn't supported") -}