[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
This commit is contained in:
James Elliott 2021-01-04 21:28:55 +11:00 committed by GitHub
parent 3487fd392e
commit 29a900226d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 1585 additions and 703 deletions

View File

@ -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.

View File

@ -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:

View File

@ -107,14 +107,18 @@ authentication_backend:
# The url to the ldap server. Scheme can be ldap or ldaps in the format (port optional) <scheme>://<address>[:<port>].
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
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_tls_version: TLS1.2
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:

View File

@ -53,14 +53,18 @@ authentication_backend:
# The url to the ldap server. Scheme can be ldap or ldaps in the format (port optional) <scheme>://<address>[:<port>].
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
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_tls_version: TLS1.2
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

View File

@ -55,3 +55,25 @@ Examples:
* 1 hour and 30 minutes: 90m
* 1 day: 1d
* 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.

View File

@ -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

View File

@ -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

View File

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

View File

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

View File

@ -2,8 +2,8 @@ package authentication
import (
"crypto/tls"
"crypto/x509"
"fmt"
"net/url"
"strings"
"github.com/go-ldap/ldap/v3"
@ -18,114 +18,124 @@ import (
type LDAPUserProvider struct {
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{
provider := &LDAPUserProvider{
configuration: configuration,
tlsConfig: &tls.Config{InsecureSkipVerify: configuration.SkipVerify, MinVersion: minimumTLSVersion}, //nolint:gosec // Disabling InsecureSkipVerify is an informed choice by users.
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)
p.usersDN = p.configuration.BaseDN
}
if p.configuration.AdditionalGroupsDN != "" {
p.groupsDN = p.configuration.AdditionalGroupsDN + "," + p.configuration.BaseDN
} else {
p.groupsDN = p.configuration.BaseDN
}
}
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
}
newConnection = conn
}
if p.configuration.StartTLS {
if err := newConnection.StartTLS(p.tlsConfig); err != nil {
if err := conn.StartTLS(p.tlsConfig); err != nil {
return nil, err
}
}
if err := newConnection.Bind(userDN, password); err != nil {
if err := conn.Bind(userDN, password); err != nil {
return nil, err
}
return newConnection, nil
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)

View File

@ -19,19 +19,22 @@ func TestShouldCreateRawConnectionWhenSchemeIsLDAP(t *testing.T) {
mockFactory := NewMockLDAPConnectionFactory(ctrl)
mockConn := NewMockLDAPConnection(ctrl)
ldap := NewLDAPUserProviderWithFactory(schema.LDAPAuthenticationBackendConfiguration{
ldapClient := NewLDAPUserProviderWithFactory(
schema.LDAPAuthenticationBackendConfiguration{
URL: "ldap://127.0.0.1:389",
}, mockFactory)
},
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{
ldapClient := NewLDAPUserProviderWithFactory(
schema.LDAPAuthenticationBackendConfiguration{
URL: "ldaps://127.0.0.1:389",
}, mockFactory)
},
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{
ldapClient := NewLDAPUserProviderWithFactory(
schema.LDAPAuthenticationBackendConfiguration{
URL: "ldaps://127.0.0.1:389",
}, mockFactory)
},
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\\=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\\=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{
ldapClient := NewLDAPUserProviderWithFactory(
schema.LDAPAuthenticationBackendConfiguration{
URL: "ldaps://127.0.0.1:389",
GroupsFilter: "(|(member={dn})(uid={username})(uid={input}))",
}, mockFactory)
},
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,7 +149,8 @@ func TestShouldEscapeUserInput(t *testing.T) {
mockFactory := NewMockLDAPConnectionFactory(ctrl)
mockConn := NewMockLDAPConnection(ctrl)
ldapClient := NewLDAPUserProviderWithFactory(schema.LDAPAuthenticationBackendConfiguration{
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}))",
@ -145,7 +160,9 @@ func TestShouldEscapeUserInput(t *testing.T) {
Password: "password",
AdditionalUsersDN: "ou=users",
BaseDN: "dc=example,dc=com",
}, mockFactory)
},
nil,
mockFactory)
mockConn.EXPECT().
// Here we ensure that the input has been correctly escaped.
@ -164,7 +181,8 @@ func TestShouldCombineUsernameFilterAndUsersFilter(t *testing.T) {
mockFactory := NewMockLDAPConnectionFactory(ctrl)
mockConn := NewMockLDAPConnection(ctrl)
ldapClient := NewLDAPUserProviderWithFactory(schema.LDAPAuthenticationBackendConfiguration{
ldapClient := NewLDAPUserProviderWithFactory(
schema.LDAPAuthenticationBackendConfiguration{
URL: "ldap://127.0.0.1:389",
User: "cn=admin,dc=example,dc=com",
UsernameAttribute: "uid",
@ -174,7 +192,9 @@ func TestShouldCombineUsernameFilterAndUsersFilter(t *testing.T) {
BaseDN: "dc=example,dc=com",
MailAttribute: "mail",
DisplayNameAttribute: "displayname",
}, mockFactory)
},
nil,
mockFactory)
mockConn.EXPECT().
Search(NewSearchRequestMatcher("(&(uid=john)(&(objectCategory=person)(objectClass=user)))")).
@ -206,7 +226,8 @@ func TestShouldNotCrashWhenGroupsAreNotRetrievedFromLDAP(t *testing.T) {
mockFactory := NewMockLDAPConnectionFactory(ctrl)
mockConn := NewMockLDAPConnection(ctrl)
ldapClient := NewLDAPUserProviderWithFactory(schema.LDAPAuthenticationBackendConfiguration{
ldapClient := NewLDAPUserProviderWithFactory(
schema.LDAPAuthenticationBackendConfiguration{
URL: "ldap://127.0.0.1:389",
User: "cn=admin,dc=example,dc=com",
Password: "password",
@ -216,10 +237,12 @@ func TestShouldNotCrashWhenGroupsAreNotRetrievedFromLDAP(t *testing.T) {
UsersFilter: "uid={input}",
AdditionalUsersDN: "ou=users",
BaseDN: "dc=example,dc=com",
}, mockFactory)
},
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,7 +297,8 @@ func TestShouldNotCrashWhenEmailsAreNotRetrievedFromLDAP(t *testing.T) {
mockFactory := NewMockLDAPConnectionFactory(ctrl)
mockConn := NewMockLDAPConnection(ctrl)
ldapClient := NewLDAPUserProviderWithFactory(schema.LDAPAuthenticationBackendConfiguration{
ldapClient := NewLDAPUserProviderWithFactory(
schema.LDAPAuthenticationBackendConfiguration{
URL: "ldap://127.0.0.1:389",
User: "cn=admin,dc=example,dc=com",
Password: "password",
@ -282,10 +306,12 @@ func TestShouldNotCrashWhenEmailsAreNotRetrievedFromLDAP(t *testing.T) {
UsersFilter: "uid={input}",
AdditionalUsersDN: "ou=users",
BaseDN: "dc=example,dc=com",
}, mockFactory)
},
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,7 +357,8 @@ func TestShouldReturnUsernameFromLDAP(t *testing.T) {
mockFactory := NewMockLDAPConnectionFactory(ctrl)
mockConn := NewMockLDAPConnection(ctrl)
ldapClient := NewLDAPUserProviderWithFactory(schema.LDAPAuthenticationBackendConfiguration{
ldapClient := NewLDAPUserProviderWithFactory(
schema.LDAPAuthenticationBackendConfiguration{
URL: "ldap://127.0.0.1:389",
User: "cn=admin,dc=example,dc=com",
Password: "password",
@ -341,10 +368,12 @@ func TestShouldReturnUsernameFromLDAP(t *testing.T) {
UsersFilter: "uid={input}",
AdditionalUsersDN: "ou=users",
BaseDN: "dc=example,dc=com",
}, mockFactory)
},
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,7 +631,8 @@ func TestShouldCallStartTLSWhenEnabled(t *testing.T) {
mockFactory := NewMockLDAPConnectionFactory(ctrl)
mockConn := NewMockLDAPConnection(ctrl)
ldapClient := NewLDAPUserProviderWithFactory(schema.LDAPAuthenticationBackendConfiguration{
ldapClient := NewLDAPUserProviderWithFactory(
schema.LDAPAuthenticationBackendConfiguration{
URL: "ldap://127.0.0.1:389",
User: "cn=admin,dc=example,dc=com",
Password: "password",
@ -410,10 +643,12 @@ func TestShouldCallStartTLSWhenEnabled(t *testing.T) {
AdditionalUsersDN: "ou=users",
BaseDN: "dc=example,dc=com",
StartTLS: true,
}, mockFactory)
},
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,7 +736,8 @@ func TestShouldCallStartTLSWithInsecureSkipVerifyWhenSkipVerifyTrue(t *testing.T
mockFactory := NewMockLDAPConnectionFactory(ctrl)
mockConn := NewMockLDAPConnection(ctrl)
ldapClient := NewLDAPUserProviderWithFactory(schema.LDAPAuthenticationBackendConfiguration{
ldapClient := NewLDAPUserProviderWithFactory(
schema.LDAPAuthenticationBackendConfiguration{
URL: "ldap://127.0.0.1:389",
User: "cn=admin,dc=example,dc=com",
Password: "password",
@ -482,11 +748,15 @@ func TestShouldCallStartTLSWithInsecureSkipVerifyWhenSkipVerifyTrue(t *testing.T
AdditionalUsersDN: "ou=users",
BaseDN: "dc=example,dc=com",
StartTLS: true,
TLS: &schema.TLSConfig{
SkipVerify: true,
}, mockFactory)
},
},
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,7 +814,8 @@ func TestShouldReturnLDAPSAlreadySecuredWhenStartTLSAttempted(t *testing.T) {
mockFactory := NewMockLDAPConnectionFactory(ctrl)
mockConn := NewMockLDAPConnection(ctrl)
ldapClient := NewLDAPUserProviderWithFactory(schema.LDAPAuthenticationBackendConfiguration{
ldapClient := NewLDAPUserProviderWithFactory(
schema.LDAPAuthenticationBackendConfiguration{
URL: "ldaps://127.0.0.1:389",
User: "cn=admin,dc=example,dc=com",
Password: "password",
@ -555,11 +826,15 @@ func TestShouldReturnLDAPSAlreadySecuredWhenStartTLSAttempted(t *testing.T) {
AdditionalUsersDN: "ou=users",
BaseDN: "dc=example,dc=com",
StartTLS: true,
TLS: &schema.TLSConfig{
SkipVerify: true,
}, mockFactory)
},
},
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().

View File

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

View File

@ -4,9 +4,6 @@ package schema
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"`
@ -18,6 +15,10 @@ type LDAPAuthenticationBackendConfiguration struct {
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.

View File

@ -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"`

View File

@ -14,11 +14,12 @@ type SMTPNotifierConfiguration struct {
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"`
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",
},
}

View File

@ -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"`
}

View File

@ -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.
@ -111,32 +114,50 @@ func (v *Validator) Errors() map[string][]error {
// StructValidator is a validator for structs.
type StructValidator struct {
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{}
}

View File

@ -5,6 +5,7 @@ import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/authelia/authelia/internal/configuration/schema"
)
@ -21,6 +22,7 @@ func (tns *TestNestedStruct) Validate(validator *schema.StructValidator) {
type TestStruct struct {
MustBe10 int
ShouldBeAbove5 int
NotEmpty string
SetDefault string
Nested TestNestedStruct
@ -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)
}

View File

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

View File

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

View File

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

View File

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

View File

@ -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",

View File

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

View File

@ -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) {

View File

@ -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) {

View File

@ -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

View File

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

View File

@ -16,6 +16,7 @@ authentication_backend:
ldap:
implementation: activedirectory
url: ldap://sambaldap
tls:
skip_verify: true
start_tls: true
base_dn: DC=example,DC=com

View File

@ -15,6 +15,7 @@ jwt_secret: very_important_secret
authentication_backend:
ldap:
url: ldaps://openldap
tls:
skip_verify: true
base_dn: dc=example,dc=com
username_attribute: uid

View File

@ -65,7 +65,7 @@ access_control:
regulation:
max_retries: 3
find_time: 3
find_time: 5
ban_time: 10
notifier:

View File

@ -13,6 +13,7 @@ default_redirection_url: https://home.example.com:8080
authentication_backend:
ldap:
url: ldaps://ldap-service
tls:
skip_verify: true
base_dn: dc=example,dc=com
username_attribute: uid

View File

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

View File

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

View File

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

View File

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