fix: redis sentinel secret missing (#1839)

* fix: redis sentinel secret missing

* refactor: use consts for authentication_backend.file.password errs

* fix: unit test for new default port

* test: cover additional misses

* test: fix windows/linux specific test error

* test: more windows specific tests

* test: remove superfluous url.IsAbs

* test: validator 100% coverage
pull/1845/head
James Elliott 2021-03-22 20:04:09 +11:00 committed by GitHub
parent 7ccbaaffe3
commit a44f0cf959
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 315 additions and 122 deletions

View File

@ -31,16 +31,17 @@ Here is the list of the environment variables which are considered
secrets and can be defined. Any other option defined using an secrets and can be defined. Any other option defined using an
environment variable will not be replaced. environment variable will not be replaced.
|Configuration Key |Environment Variable | |Configuration Key |Environment Variable |
|:----------------------------------:|:------------------------------------------------:| |:-----------------------------------------------:|:------------------------------------------------:|
|jwt_secret |AUTHELIA_JWT_SECRET_FILE | |jwt_secret |AUTHELIA_JWT_SECRET_FILE |
|duo_api.secret_key |AUTHELIA_DUO_API_SECRET_KEY_FILE | |duo_api.secret_key |AUTHELIA_DUO_API_SECRET_KEY_FILE |
|session.secret |AUTHELIA_SESSION_SECRET_FILE | |session.secret |AUTHELIA_SESSION_SECRET_FILE |
|session.redis.password |AUTHELIA_SESSION_REDIS_PASSWORD_FILE | |session.redis.password |AUTHELIA_SESSION_REDIS_PASSWORD_FILE |
|storage.mysql.password |AUTHELIA_STORAGE_MYSQL_PASSWORD_FILE | |session.redis.high_availability.sentinel_password|AUTHELIA_REDIS_HIGH_AVAILABILITY_SENTINEL_PASSWORD|
|storage.postgres.password |AUTHELIA_STORAGE_POSTGRES_PASSWORD_FILE | |storage.mysql.password |AUTHELIA_STORAGE_MYSQL_PASSWORD_FILE |
|notifier.smtp.password |AUTHELIA_NOTIFIER_SMTP_PASSWORD_FILE | |storage.postgres.password |AUTHELIA_STORAGE_POSTGRES_PASSWORD_FILE |
|authentication_backend.ldap.password|AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PASSWORD_FILE| |notifier.smtp.password |AUTHELIA_NOTIFIER_SMTP_PASSWORD_FILE |
|authentication_backend.ldap.password |AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PASSWORD_FILE|
## Secrets in configuration file ## Secrets in configuration file

View File

@ -4,6 +4,7 @@ import (
"io/ioutil" "io/ioutil"
"log" "log"
"os" "os"
"runtime"
"strings" "strings"
"testing" "testing"
@ -34,6 +35,10 @@ func WithDatabase(content []byte, f func(path string)) {
} }
func TestShouldErrorPermissionsOnLocalFS(t *testing.T) { func TestShouldErrorPermissionsOnLocalFS(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("skipping test due to being on windows")
}
_ = os.Mkdir("/tmp/noperms/", 0000) _ = os.Mkdir("/tmp/noperms/", 0000)
errors := checkDatabase("/tmp/noperms/users_database.yml") errors := checkDatabase("/tmp/noperms/users_database.yml")

View File

@ -0,0 +1,3 @@
package configuration
const windows = "windows"

View File

@ -55,14 +55,10 @@ func Read(configPath string) (*schema.Configuration, []error) {
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
viper.BindEnv("authelia.jwt_secret.file") //nolint:errcheck // TODO: Legacy code, consider refactoring time permitting. // Dynamically load the secret env names from the SecretNames map.
viper.BindEnv("authelia.duo_api.secret_key.file") //nolint:errcheck // TODO: Legacy code, consider refactoring time permitting. for _, secretName := range validator.SecretNames {
viper.BindEnv("authelia.session.secret.file") //nolint:errcheck // TODO: Legacy code, consider refactoring time permitting. _ = viper.BindEnv(validator.SecretNameToEnvName(secretName))
viper.BindEnv("authelia.authentication_backend.ldap.password.file") //nolint:errcheck // TODO: Legacy code, consider refactoring time permitting. }
viper.BindEnv("authelia.notifier.smtp.password.file") //nolint:errcheck // TODO: Legacy code, consider refactoring time permitting.
viper.BindEnv("authelia.session.redis.password.file") //nolint:errcheck // TODO: Legacy code, consider refactoring time permitting.
viper.BindEnv("authelia.storage.mysql.password.file") //nolint:errcheck // TODO: Legacy code, consider refactoring time permitting.
viper.BindEnv("authelia.storage.postgres.password.file") //nolint:errcheck // TODO: Legacy code, consider refactoring time permitting.
viper.SetConfigFile(configPath) viper.SetConfigFile(configPath)

View File

@ -4,6 +4,7 @@ import (
"io/ioutil" "io/ioutil"
"os" "os"
"path" "path"
"runtime"
"sort" "sort"
"testing" "testing"
@ -27,6 +28,7 @@ func resetEnv() {
_ = os.Unsetenv("AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PASSWORD_FILE") _ = os.Unsetenv("AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PASSWORD_FILE")
_ = os.Unsetenv("AUTHELIA_NOTIFIER_SMTP_PASSWORD_FILE") _ = os.Unsetenv("AUTHELIA_NOTIFIER_SMTP_PASSWORD_FILE")
_ = os.Unsetenv("AUTHELIA_SESSION_REDIS_PASSWORD_FILE") _ = os.Unsetenv("AUTHELIA_SESSION_REDIS_PASSWORD_FILE")
_ = os.Unsetenv("AUTHELIA_SESSION_REDIS_HIGH_AVAILABILITY_SENTINEL_PASSWORD_FILE")
_ = os.Unsetenv("AUTHELIA_STORAGE_MYSQL_PASSWORD_FILE") _ = os.Unsetenv("AUTHELIA_STORAGE_MYSQL_PASSWORD_FILE")
_ = os.Unsetenv("AUTHELIA_STORAGE_POSTGRES_PASSWORD_FILE") _ = os.Unsetenv("AUTHELIA_STORAGE_POSTGRES_PASSWORD_FILE")
} }
@ -49,6 +51,7 @@ func setupEnv(t *testing.T) string {
createTestingTempFile(t, dir, "authentication", "ldap_secret_from_env") createTestingTempFile(t, dir, "authentication", "ldap_secret_from_env")
createTestingTempFile(t, dir, "notifier", "smtp_secret_from_env") createTestingTempFile(t, dir, "notifier", "smtp_secret_from_env")
createTestingTempFile(t, dir, "redis", "redis_secret_from_env") createTestingTempFile(t, dir, "redis", "redis_secret_from_env")
createTestingTempFile(t, dir, "redis-sentinel", "redis-sentinel_secret_from_env")
createTestingTempFile(t, dir, "mysql", "mysql_secret_from_env") createTestingTempFile(t, dir, "mysql", "mysql_secret_from_env")
createTestingTempFile(t, dir, "postgres", "postgres_secret_from_env") createTestingTempFile(t, dir, "postgres", "postgres_secret_from_env")
@ -65,7 +68,56 @@ func TestShouldErrorNoConfigPath(t *testing.T) {
require.EqualError(t, errors[0], "No config file path provided") require.EqualError(t, errors[0], "No config file path provided")
} }
func TestShouldErrorSecretNotExist(t *testing.T) {
dir := "/path/not/exist"
require.NoError(t, os.Setenv("AUTHELIA_JWT_SECRET_FILE", dir+"jwt"))
require.NoError(t, os.Setenv("AUTHELIA_DUO_API_SECRET_KEY_FILE", dir+"duo"))
require.NoError(t, os.Setenv("AUTHELIA_SESSION_SECRET_FILE", dir+"session"))
require.NoError(t, os.Setenv("AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PASSWORD_FILE", dir+"authentication"))
require.NoError(t, os.Setenv("AUTHELIA_NOTIFIER_SMTP_PASSWORD_FILE", dir+"notifier"))
require.NoError(t, os.Setenv("AUTHELIA_SESSION_REDIS_PASSWORD_FILE", dir+"redis"))
require.NoError(t, os.Setenv("AUTHELIA_SESSION_REDIS_HIGH_AVAILABILITY_SENTINEL_PASSWORD_FILE", dir+"redis-sentinel"))
require.NoError(t, os.Setenv("AUTHELIA_STORAGE_MYSQL_PASSWORD_FILE", dir+"mysql"))
require.NoError(t, os.Setenv("AUTHELIA_STORAGE_POSTGRES_PASSWORD_FILE", dir+"postgres"))
_, errors := Read("./test_resources/config.yml")
require.Len(t, errors, 12)
if runtime.GOOS == windows {
assert.EqualError(t, errors[0], "error loading secret file (jwt_secret): open /path/not/existjwt: The system cannot find the path specified.")
assert.EqualError(t, errors[1], "error loading secret file (session.secret): open /path/not/existsession: The system cannot find the path specified.")
assert.EqualError(t, errors[2], "error loading secret file (duo_api.secret_key): open /path/not/existduo: The system cannot find the path specified.")
assert.EqualError(t, errors[3], "error loading secret file (session.redis.password): open /path/not/existredis: The system cannot find the path specified.")
assert.EqualError(t, errors[4], "error loading secret file (session.redis.high_availability.sentinel_password): open /path/not/existredis-sentinel: The system cannot find the path specified.")
assert.EqualError(t, errors[5], "error loading secret file (authentication_backend.ldap.password): open /path/not/existauthentication: The system cannot find the path specified.")
assert.EqualError(t, errors[6], "error loading secret file (notifier.smtp.password): open /path/not/existnotifier: The system cannot find the path specified.")
assert.EqualError(t, errors[7], "error loading secret file (storage.mysql.password): open /path/not/existmysql: The system cannot find the path specified.")
} else {
assert.EqualError(t, errors[0], "error loading secret file (jwt_secret): open /path/not/existjwt: no such file or directory")
assert.EqualError(t, errors[1], "error loading secret file (session.secret): open /path/not/existsession: no such file or directory")
assert.EqualError(t, errors[2], "error loading secret file (duo_api.secret_key): open /path/not/existduo: no such file or directory")
assert.EqualError(t, errors[3], "error loading secret file (session.redis.password): open /path/not/existredis: no such file or directory")
assert.EqualError(t, errors[4], "error loading secret file (session.redis.high_availability.sentinel_password): open /path/not/existredis-sentinel: no such file or directory")
assert.EqualError(t, errors[5], "error loading secret file (authentication_backend.ldap.password): open /path/not/existauthentication: no such file or directory")
assert.EqualError(t, errors[6], "error loading secret file (notifier.smtp.password): open /path/not/existnotifier: no such file or directory")
assert.EqualError(t, errors[7], "error loading secret file (storage.mysql.password): open /path/not/existmysql: no such file or directory")
}
assert.EqualError(t, errors[8], "Provide a JWT secret using \"jwt_secret\" key")
assert.EqualError(t, errors[9], "Please provide a password to connect to the LDAP server")
assert.EqualError(t, errors[10], "The session secret must be set when using the redis sentinel session provider")
assert.EqualError(t, errors[11], "the SQL username and password must be provided")
}
func TestShouldErrorPermissionsOnLocalFS(t *testing.T) { func TestShouldErrorPermissionsOnLocalFS(t *testing.T) {
if runtime.GOOS == windows {
t.Skip("skipping test due to being on windows")
}
resetEnv()
_ = os.Mkdir("/tmp/noperms/", 0000) _ = os.Mkdir("/tmp/noperms/", 0000)
_, errors := Read("/tmp/noperms/configuration.yml") _, errors := Read("/tmp/noperms/configuration.yml")
@ -88,12 +140,23 @@ func TestShouldErrorAndGenerateConfigFile(t *testing.T) {
} }
func TestShouldErrorPermissionsConfigFile(t *testing.T) { func TestShouldErrorPermissionsConfigFile(t *testing.T) {
resetEnv()
_ = ioutil.WriteFile("/tmp/authelia/permissions.yml", []byte{}, 0000) // nolint:gosec _ = ioutil.WriteFile("/tmp/authelia/permissions.yml", []byte{}, 0000) // nolint:gosec
_, errors := Read("/tmp/authelia/permissions.yml") _, errors := Read("/tmp/authelia/permissions.yml")
require.Len(t, errors, 1) if runtime.GOOS == windows {
require.Len(t, errors, 5)
assert.EqualError(t, errors[0], "Provide a JWT secret using \"jwt_secret\" key")
assert.EqualError(t, errors[1], "Please provide `ldap` or `file` object in `authentication_backend`")
assert.EqualError(t, errors[2], "Set domain of the session object")
assert.EqualError(t, errors[3], "A storage configuration must be provided. It could be 'local', 'mysql' or 'postgres'")
assert.EqualError(t, errors[4], "A notifier configuration must be provided")
} else {
require.Len(t, errors, 1)
require.EqualError(t, errors[0], "Failed to open /tmp/authelia/permissions.yml: permission denied") assert.EqualError(t, errors[0], "Failed to open /tmp/authelia/permissions.yml: permission denied")
}
} }
func TestShouldErrorParseBadConfigFile(t *testing.T) { func TestShouldErrorParseBadConfigFile(t *testing.T) {
@ -113,6 +176,7 @@ func TestShouldParseConfigFile(t *testing.T) {
require.NoError(t, os.Setenv("AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PASSWORD_FILE", dir+"authentication")) require.NoError(t, os.Setenv("AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PASSWORD_FILE", dir+"authentication"))
require.NoError(t, os.Setenv("AUTHELIA_NOTIFIER_SMTP_PASSWORD_FILE", dir+"notifier")) require.NoError(t, os.Setenv("AUTHELIA_NOTIFIER_SMTP_PASSWORD_FILE", dir+"notifier"))
require.NoError(t, os.Setenv("AUTHELIA_SESSION_REDIS_PASSWORD_FILE", dir+"redis")) require.NoError(t, os.Setenv("AUTHELIA_SESSION_REDIS_PASSWORD_FILE", dir+"redis"))
require.NoError(t, os.Setenv("AUTHELIA_SESSION_REDIS_HIGH_AVAILABILITY_SENTINEL_PASSWORD_FILE", dir+"redis-sentinel"))
require.NoError(t, os.Setenv("AUTHELIA_STORAGE_MYSQL_PASSWORD_FILE", dir+"mysql")) require.NoError(t, os.Setenv("AUTHELIA_STORAGE_MYSQL_PASSWORD_FILE", dir+"mysql"))
require.NoError(t, os.Setenv("AUTHELIA_STORAGE_POSTGRES_PASSWORD_FILE", dir+"postgres")) require.NoError(t, os.Setenv("AUTHELIA_STORAGE_POSTGRES_PASSWORD_FILE", dir+"postgres"))
@ -134,10 +198,15 @@ func TestShouldParseConfigFile(t *testing.T) {
assert.Equal(t, "ldap_secret_from_env", config.AuthenticationBackend.Ldap.Password) assert.Equal(t, "ldap_secret_from_env", config.AuthenticationBackend.Ldap.Password)
assert.Equal(t, "smtp_secret_from_env", config.Notifier.SMTP.Password) assert.Equal(t, "smtp_secret_from_env", config.Notifier.SMTP.Password)
assert.Equal(t, "redis_secret_from_env", config.Session.Redis.Password) assert.Equal(t, "redis_secret_from_env", config.Session.Redis.Password)
assert.Equal(t, "redis-sentinel_secret_from_env", config.Session.Redis.HighAvailability.SentinelPassword)
assert.Equal(t, "mysql_secret_from_env", config.Storage.MySQL.Password) assert.Equal(t, "mysql_secret_from_env", config.Storage.MySQL.Password)
assert.Equal(t, "deny", config.AccessControl.DefaultPolicy) assert.Equal(t, "deny", config.AccessControl.DefaultPolicy)
assert.Len(t, config.AccessControl.Rules, 12) assert.Len(t, config.AccessControl.Rules, 12)
require.NotNil(t, config.Session)
require.NotNil(t, config.Session.Redis)
require.NotNil(t, config.Session.Redis.HighAvailability)
} }
func TestShouldParseAltConfigFile(t *testing.T) { func TestShouldParseAltConfigFile(t *testing.T) {

View File

@ -101,6 +101,8 @@ session:
redis: redis:
host: 127.0.0.1 host: 127.0.0.1
port: 6379 port: 6379
high_availability:
sentinel_name: test
regulation: regulation:
max_retries: 3 max_retries: 3

View File

@ -92,10 +92,6 @@ func validateLdapURL(ldapURL string, validator *schema.StructValidator) (finalUR
return "", "" return "", ""
} }
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()))
}
return parsedURL.String(), parsedURL.Hostname() return parsedURL.String(), parsedURL.Hostname()
} }

View File

@ -10,7 +10,22 @@ import (
"github.com/authelia/authelia/internal/configuration/schema" "github.com/authelia/authelia/internal/configuration/schema"
) )
func TestShouldRaiseErrorsWhenNoBackendProvided(t *testing.T) { func TestShouldRaiseErrorWhenBothBackendsProvided(t *testing.T) {
validator := schema.NewStructValidator()
backendConfig := schema.AuthenticationBackendConfiguration{}
backendConfig.Ldap = &schema.LDAPAuthenticationBackendConfiguration{}
backendConfig.File = &schema.FileAuthenticationBackendConfiguration{
Path: "/tmp",
}
ValidateAuthenticationBackend(&backendConfig, validator)
require.Len(t, validator.Errors(), 1)
assert.EqualError(t, validator.Errors()[0], "You cannot provide both `ldap` and `file` objects in `authentication_backend`")
}
func TestShouldRaiseErrorWhenNoBackendProvided(t *testing.T) {
validator := schema.NewStructValidator() validator := schema.NewStructValidator()
backendConfig := schema.AuthenticationBackendConfiguration{} backendConfig := schema.AuthenticationBackendConfiguration{}

View File

@ -8,7 +8,7 @@ import (
"github.com/authelia/authelia/internal/configuration/schema" "github.com/authelia/authelia/internal/configuration/schema"
) )
var defaultPort = 8080 var defaultPort = 9091
var defaultLogLevel = "info" var defaultLogLevel = "info"
// ValidateConfiguration and adapt the configuration read from file. // ValidateConfiguration and adapt the configuration read from file.

View File

@ -1,6 +1,7 @@
package validator package validator
import ( import (
"runtime"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -54,7 +55,7 @@ func TestShouldValidateAndUpdatePort(t *testing.T) {
ValidateConfiguration(&config, validator) ValidateConfiguration(&config, validator)
require.Len(t, validator.Errors(), 0) require.Len(t, validator.Errors(), 0)
assert.Equal(t, 8080, config.Port) assert.Equal(t, 9091, config.Port)
} }
func TestShouldValidateAndUpdateHost(t *testing.T) { func TestShouldValidateAndUpdateHost(t *testing.T) {
@ -170,7 +171,12 @@ func TestShouldRaiseErrorOnInvalidCertificatesDirectory(t *testing.T) {
ValidateConfiguration(&config, validator) ValidateConfiguration(&config, validator)
require.Len(t, validator.Errors(), 1) 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")
if runtime.GOOS == "windows" {
assert.EqualError(t, validator.Errors()[0], "Error checking certificate directory: CreateFile not-a-real-file.go: The system cannot find the file specified.")
} else {
assert.EqualError(t, validator.Errors()[0], "Error checking certificate directory: stat not-a-real-file.go: no such file or directory")
}
validator = schema.NewStructValidator() validator = schema.NewStructValidator()
config.CertificatesDirectory = "const.go" config.CertificatesDirectory = "const.go"

View File

@ -1,7 +1,57 @@
package validator package validator
const (
errFmtSessionSecretRedisProvider = "The session secret must be set when using the %s session provider"
errFmtSessionRedisPortRange = "The port must be between 1 and 65535 for the %s session provider"
errFmtSessionRedisHostRequired = "The host must be provided when using the %s session provider"
errFmtSessionRedisHostOrNodesRequired = "Either the host or a node must be provided when using the %s session provider"
errFileHashing = "config key incorrect: authentication_backend.file.hashing should be authentication_backend.file.password"
errFilePHashing = "config key incorrect: authentication_backend.file.password_hashing should be authentication_backend.file.password"
errFilePOptions = "config key incorrect: authentication_backend.file.password_options should be authentication_backend.file.password"
denyPolicy = "deny"
bypassPolicy = "bypass"
argon2id = "argon2id"
sha512 = "sha512"
schemeLDAP = "ldap"
schemeLDAPS = "ldaps"
testBadTimer = "-1"
testInvalidPolicy = "invalid"
testJWTSecret = "a_secret"
testLDAPBaseDN = "base_dn"
testLDAPPassword = "password"
testLDAPURL = "ldap://ldap"
testLDAPUser = "user"
testModeDisabled = "disable"
testTLSCert = "/tmp/cert.pem"
testTLSKey = "/tmp/key.pem"
errAccessControlInvalidPolicyWithSubjects = "Policy [bypass] for domain %s with subjects %s is invalid. It is " +
"not supported to configure both policy bypass and subjects. For more information see: " +
"https://www.authelia.com/docs/configuration/access-control.html#combining-subjects-and-the-bypass-policy"
)
var validRequestMethods = []string{"GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "TRACE", "CONNECT", "OPTIONS"} var validRequestMethods = []string{"GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "TRACE", "CONNECT", "OPTIONS"}
// SecretNames contains a map of secret names.
var SecretNames = map[string]string{
"JWTSecret": "jwt_secret",
"SessionSecret": "session.secret",
"DUOSecretKey": "duo_api.secret_key",
"RedisPassword": "session.redis.password",
"RedisSentinelPassword": "session.redis.high_availability.sentinel_password",
"LDAPPassword": "authentication_backend.ldap.password",
"SMTPPassword": "notifier.smtp.password",
"MySQLPassword": "storage.mysql.password",
"PostgreSQLPassword": "storage.postgres.password",
}
// validKeys is a list of valid keys that are not secret names. For the sake of consistency please place any secret in
// the secret names map and reuse it in relevant sections.
var validKeys = []string{ var validKeys = []string{
// Root Keys. // Root Keys.
"host", "host",
@ -10,7 +60,6 @@ var validKeys = []string{
"log_format", "log_format",
"log_file_path", "log_file_path",
"default_redirection_url", "default_redirection_url",
"jwt_secret",
"theme", "theme",
"tls_key", "tls_key",
"tls_cert", "tls_cert",
@ -33,7 +82,6 @@ var validKeys = []string{
// Session Keys. // Session Keys.
"session.name", "session.name",
"session.secret",
"session.expiration", "session.expiration",
"session.inactivity", "session.inactivity",
"session.remember_me_duration", "session.remember_me_duration",
@ -43,7 +91,6 @@ var validKeys = []string{
"session.redis.host", "session.redis.host",
"session.redis.port", "session.redis.port",
"session.redis.username", "session.redis.username",
"session.redis.password",
"session.redis.database_index", "session.redis.database_index",
"session.redis.maximum_active_connections", "session.redis.maximum_active_connections",
"session.redis.minimum_idle_connections", "session.redis.minimum_idle_connections",
@ -51,7 +98,6 @@ var validKeys = []string{
"session.redis.tls.skip_verify", "session.redis.tls.skip_verify",
"session.redis.tls.server_name", "session.redis.tls.server_name",
"session.redis.high_availability.sentinel_name", "session.redis.high_availability.sentinel_name",
"session.redis.high_availability.sentinel_password",
"session.redis.high_availability.nodes", "session.redis.high_availability.nodes",
"session.redis.high_availability.route_by_latency", "session.redis.high_availability.route_by_latency",
"session.redis.high_availability.route_randomly", "session.redis.high_availability.route_randomly",
@ -69,14 +115,12 @@ var validKeys = []string{
"storage.mysql.port", "storage.mysql.port",
"storage.mysql.database", "storage.mysql.database",
"storage.mysql.username", "storage.mysql.username",
"storage.mysql.password",
// PostgreSQL Storage Keys. // PostgreSQL Storage Keys.
"storage.postgres.host", "storage.postgres.host",
"storage.postgres.port", "storage.postgres.port",
"storage.postgres.database", "storage.postgres.database",
"storage.postgres.username", "storage.postgres.username",
"storage.postgres.password",
"storage.postgres.sslmode", "storage.postgres.sslmode",
// FileSystem Notifier Keys. // FileSystem Notifier Keys.
@ -85,7 +129,6 @@ var validKeys = []string{
// SMTP Notifier Keys. // SMTP Notifier Keys.
"notifier.smtp.username", "notifier.smtp.username",
"notifier.smtp.password",
"notifier.smtp.host", "notifier.smtp.host",
"notifier.smtp.port", "notifier.smtp.port",
"notifier.smtp.identifier", "notifier.smtp.identifier",
@ -108,7 +151,6 @@ var validKeys = []string{
// DUO API Keys. // DUO API Keys.
"duo_api.hostname", "duo_api.hostname",
"duo_api.integration_key", "duo_api.integration_key",
"duo_api.secret_key",
// Authentication Backend Keys. // Authentication Backend Keys.
"authentication_backend.disable_reset_password", "authentication_backend.disable_reset_password",
@ -127,7 +169,6 @@ var validKeys = []string{
"authentication_backend.ldap.mail_attribute", "authentication_backend.ldap.mail_attribute",
"authentication_backend.ldap.display_name_attribute", "authentication_backend.ldap.display_name_attribute",
"authentication_backend.ldap.user", "authentication_backend.ldap.user",
"authentication_backend.ldap.password",
"authentication_backend.ldap.start_tls", "authentication_backend.ldap.start_tls",
"authentication_backend.ldap.tls.minimum_version", "authentication_backend.ldap.tls.minimum_version",
"authentication_backend.ldap.tls.skip_verify", "authentication_backend.ldap.tls.skip_verify",
@ -143,73 +184,28 @@ var validKeys = []string{
"authentication_backend.file.password.salt_length", "authentication_backend.file.password.salt_length",
"authentication_backend.file.password.memory", "authentication_backend.file.password.memory",
"authentication_backend.file.password.parallelism", "authentication_backend.file.password.parallelism",
// Secret Keys.
"authelia.jwt_secret",
"authelia.duo_api.secret_key",
"authelia.session.secret",
"authelia.authentication_backend.ldap.password",
"authelia.notifier.smtp.password",
"authelia.session.redis.password",
"authelia.storage.mysql.password",
"authelia.storage.postgres.password",
"authelia.jwt_secret.file",
"authelia.duo_api.secret_key.file",
"authelia.session.secret.file",
"authelia.authentication_backend.ldap.password.file",
"authelia.notifier.smtp.password.file",
"authelia.session.redis.password.file",
"authelia.storage.mysql.password.file",
"authelia.storage.postgres.password.file",
} }
var specificErrorKeys = map[string]string{ var specificErrorKeys = map[string]string{
"logs_file_path": "config key replaced: logs_file is now log_file", "logs_file_path": "config key replaced: logs_file is now log_file",
"logs_level": "config key replaced: logs_level is now log_level", "logs_level": "config key replaced: logs_level is now log_level",
"google_analytics": "config key removed: google_analytics - this functionality has been deprecated", "google_analytics": "config key removed: google_analytics - this functionality has been deprecated",
"authentication_backend.file.password_options.algorithm": "config key incorrect: authentication_backend.file.password_options should be authentication_backend.file.password", "authentication_backend.file.password_options.algorithm": errFilePOptions,
"authentication_backend.file.password_options.iterations": "config key incorrect: authentication_backend.file.password_options should be authentication_backend.file.password", "authentication_backend.file.password_options.iterations": errFilePOptions,
"authentication_backend.file.password_options.key_length": "config key incorrect: authentication_backend.file.password_options should be authentication_backend.file.password", "authentication_backend.file.password_options.key_length": errFilePOptions,
"authentication_backend.file.password_options.salt_length": "config key incorrect: authentication_backend.file.password_options should be authentication_backend.file.password", "authentication_backend.file.password_options.salt_length": errFilePOptions,
"authentication_backend.file.password_options.memory": "config key incorrect: authentication_backend.file.password_options should be authentication_backend.file.password", "authentication_backend.file.password_options.memory": errFilePOptions,
"authentication_backend.file.password_options.parallelism": "config key incorrect: authentication_backend.file.password_options should be authentication_backend.file.password", "authentication_backend.file.password_options.parallelism": errFilePOptions,
"authentication_backend.file.password_hashing.algorithm": "config key incorrect: authentication_backend.file.password_hashing should be authentication_backend.file.password", "authentication_backend.file.password_hashing.algorithm": errFilePHashing,
"authentication_backend.file.password_hashing.iterations": "config key incorrect: authentication_backend.file.password_hashing should be authentication_backend.file.password", "authentication_backend.file.password_hashing.iterations": errFilePHashing,
"authentication_backend.file.password_hashing.key_length": "config key incorrect: authentication_backend.file.password_hashing should be authentication_backend.file.password", "authentication_backend.file.password_hashing.key_length": errFilePHashing,
"authentication_backend.file.password_hashing.salt_length": "config key incorrect: authentication_backend.file.password_hashing should be authentication_backend.file.password", "authentication_backend.file.password_hashing.salt_length": errFilePHashing,
"authentication_backend.file.password_hashing.memory": "config key incorrect: authentication_backend.file.password_hashing should be authentication_backend.file.password", "authentication_backend.file.password_hashing.memory": errFilePHashing,
"authentication_backend.file.password_hashing.parallelism": "config key incorrect: authentication_backend.file.password_hashing should be authentication_backend.file.password", "authentication_backend.file.password_hashing.parallelism": errFilePHashing,
"authentication_backend.file.hashing.algorithm": "config key incorrect: authentication_backend.file.hashing should be authentication_backend.file.password", "authentication_backend.file.hashing.algorithm": errFileHashing,
"authentication_backend.file.hashing.iterations": "config key incorrect: authentication_backend.file.hashing should be authentication_backend.file.password", "authentication_backend.file.hashing.iterations": errFileHashing,
"authentication_backend.file.hashing.key_length": "config key incorrect: authentication_backend.file.hashing should be authentication_backend.file.password", "authentication_backend.file.hashing.key_length": errFileHashing,
"authentication_backend.file.hashing.salt_length": "config key incorrect: authentication_backend.file.hashing should be authentication_backend.file.password", "authentication_backend.file.hashing.salt_length": errFileHashing,
"authentication_backend.file.hashing.memory": "config key incorrect: authentication_backend.file.hashing should be authentication_backend.file.password", "authentication_backend.file.hashing.memory": errFileHashing,
"authentication_backend.file.hashing.parallelism": "config key incorrect: authentication_backend.file.hashing should be authentication_backend.file.password", "authentication_backend.file.hashing.parallelism": errFileHashing,
} }
const errFmtSessionSecretRedisProvider = "The session secret must be set when using the %s session provider"
const errFmtSessionRedisPortRange = "The port must be between 1 and 65535 for the %s session provider"
const errFmtSessionRedisHostRequired = "The host must be provided when using the %s session provider"
const errFmtSessionRedisHostOrNodesRequired = "Either the host or a node must be provided when using the %s session provider"
const denyPolicy = "deny"
const bypassPolicy = "bypass"
const argon2id = "argon2id"
const sha512 = "sha512"
const schemeLDAP = "ldap"
const schemeLDAPS = "ldaps"
const testBadTimer = "-1"
const testInvalidPolicy = "invalid"
const testJWTSecret = "a_secret"
const testLDAPBaseDN = "base_dn"
const testLDAPPassword = "password"
const testLDAPURL = "ldap://ldap"
const testLDAPUser = "user"
const testModeDisabled = "disable"
const testTLSCert = "/tmp/cert.pem"
const testTLSKey = "/tmp/key.pem"
const errAccessControlInvalidPolicyWithSubjects = "Policy [bypass] for domain %s with subjects %s is invalid. It is not supported to configure both policy bypass and subjects. For more information see: https://www.authelia.com/docs/configuration/access-control.html#combining-subjects-and-the-bypass-policy"

View File

@ -17,6 +17,10 @@ func ValidateKeys(validator *schema.StructValidator, keys []string) {
continue continue
} }
if isSecretKey(key) {
continue
}
if err, ok := specificErrorKeys[key]; ok { if err, ok := specificErrorKeys[key]; ok {
if !utils.IsStringInSlice(err, errStrings) { if !utils.IsStringInSlice(err, errStrings) {
errStrings = append(errStrings, err) errStrings = append(errStrings, err)

View File

@ -10,39 +10,59 @@ import (
"github.com/authelia/authelia/internal/configuration/schema" "github.com/authelia/authelia/internal/configuration/schema"
) )
// SecretNameToEnvName converts a secret name into the env name.
func SecretNameToEnvName(secretName string) (envName string) {
return "authelia." + secretName + ".file"
}
func isSecretKey(value string) (isSecretKey bool) {
for _, secretKey := range SecretNames {
if value == secretKey || value == SecretNameToEnvName(secretKey) {
return true
}
}
return false
}
// ValidateSecrets checks that secrets are either specified by config file/env or by file references. // ValidateSecrets checks that secrets are either specified by config file/env or by file references.
func ValidateSecrets(configuration *schema.Configuration, validator *schema.StructValidator, viper *viper.Viper) { func ValidateSecrets(configuration *schema.Configuration, validator *schema.StructValidator, viper *viper.Viper) {
configuration.JWTSecret = getSecretValue("jwt_secret", validator, viper) configuration.JWTSecret = getSecretValue(SecretNames["JWTSecret"], validator, viper)
configuration.Session.Secret = getSecretValue("session.secret", validator, viper) configuration.Session.Secret = getSecretValue(SecretNames["SessionSecret"], validator, viper)
if configuration.DuoAPI != nil { if configuration.DuoAPI != nil {
configuration.DuoAPI.SecretKey = getSecretValue("duo_api.secret_key", validator, viper) configuration.DuoAPI.SecretKey = getSecretValue(SecretNames["DUOSecretKey"], validator, viper)
} }
if configuration.Session.Redis != nil { if configuration.Session.Redis != nil {
configuration.Session.Redis.Password = getSecretValue("session.redis.password", validator, viper) configuration.Session.Redis.Password = getSecretValue(SecretNames["RedisPassword"], validator, viper)
if configuration.Session.Redis.HighAvailability != nil {
configuration.Session.Redis.HighAvailability.SentinelPassword =
getSecretValue(SecretNames["RedisSentinelPassword"], validator, viper)
}
} }
if configuration.AuthenticationBackend.Ldap != nil { if configuration.AuthenticationBackend.Ldap != nil {
configuration.AuthenticationBackend.Ldap.Password = getSecretValue("authentication_backend.ldap.password", validator, viper) configuration.AuthenticationBackend.Ldap.Password = getSecretValue(SecretNames["LDAPPassword"], validator, viper)
} }
if configuration.Notifier != nil && configuration.Notifier.SMTP != nil { if configuration.Notifier != nil && configuration.Notifier.SMTP != nil {
configuration.Notifier.SMTP.Password = getSecretValue("notifier.smtp.password", validator, viper) configuration.Notifier.SMTP.Password = getSecretValue(SecretNames["SMTPPassword"], validator, viper)
} }
if configuration.Storage.MySQL != nil { if configuration.Storage.MySQL != nil {
configuration.Storage.MySQL.Password = getSecretValue("storage.mysql.password", validator, viper) configuration.Storage.MySQL.Password = getSecretValue(SecretNames["MySQLPassword"], validator, viper)
} }
if configuration.Storage.PostgreSQL != nil { if configuration.Storage.PostgreSQL != nil {
configuration.Storage.PostgreSQL.Password = getSecretValue("storage.postgres.password", validator, viper) configuration.Storage.PostgreSQL.Password = getSecretValue(SecretNames["PostgreSQLPassword"], validator, viper)
} }
} }
func getSecretValue(name string, validator *schema.StructValidator, viper *viper.Viper) string { func getSecretValue(name string, validator *schema.StructValidator, viper *viper.Viper) string {
configValue := viper.GetString(name) configValue := viper.GetString(name)
fileEnvValue := viper.GetString("authelia." + name + ".file") fileEnvValue := viper.GetString(SecretNameToEnvName(name))
// Error Checking. // Error Checking.
if fileEnvValue != "" && configValue != "" { if fileEnvValue != "" && configValue != "" {

View File

@ -0,0 +1,18 @@
package validator
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestShouldValidateCorrectSecretKeys(t *testing.T) {
assert.True(t, isSecretKey("jwt_secret"))
assert.True(t, isSecretKey("authelia.jwt_secret.file"))
assert.False(t, isSecretKey("totp.issuer"))
}
func TestShouldCreateCorrectSecretEnvNames(t *testing.T) {
assert.Equal(t, "authelia.jwt_secret.file", SecretNameToEnvName("jwt_secret"))
assert.Equal(t, "authelia.not_a_real_secret.file", SecretNameToEnvName("not_a_real_secret"))
}

View File

@ -18,6 +18,18 @@ func TestShouldSetDefaultConfig(t *testing.T) {
assert.Equal(t, defaultWriteBufferSize, config.WriteBufferSize) assert.Equal(t, defaultWriteBufferSize, config.WriteBufferSize)
} }
func TestShouldParsePathCorrectly(t *testing.T) {
validator := schema.NewStructValidator()
config := schema.ServerConfiguration{
Path: "apple",
}
ValidateServer(&config, validator)
require.Len(t, validator.Errors(), 0)
assert.Equal(t, "/apple", config.Path)
}
func TestShouldRaiseOnNegativeValues(t *testing.T) { func TestShouldRaiseOnNegativeValues(t *testing.T) {
validator := schema.NewStructValidator() validator := schema.NewStructValidator()
config := schema.ServerConfiguration{ config := schema.ServerConfiguration{

View File

@ -24,11 +24,11 @@ func ValidateStorage(configuration schema.StorageConfiguration, validator *schem
func validateSQLConfiguration(configuration *schema.SQLStorageConfiguration, validator *schema.StructValidator) { func validateSQLConfiguration(configuration *schema.SQLStorageConfiguration, validator *schema.StructValidator) {
if configuration.Password == "" || configuration.Username == "" { if configuration.Password == "" || configuration.Username == "" {
validator.Push(errors.New("Username and password must be provided")) validator.Push(errors.New("the SQL username and password must be provided"))
} }
if configuration.Database == "" { if configuration.Database == "" {
validator.Push(errors.New("A database must be provided")) validator.Push(errors.New("the SQL database must be provided"))
} }
} }

View File

@ -55,8 +55,8 @@ func (suite *StorageSuite) TestShouldValidateSQLUsernamePasswordAndDatabaseArePr
ValidateStorage(suite.configuration, suite.validator) ValidateStorage(suite.configuration, suite.validator)
suite.Require().Len(suite.validator.Errors(), 2) 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()[0], "the SQL username and password must be provided")
suite.Assert().EqualError(suite.validator.Errors()[1], "A database must be provided") suite.Assert().EqualError(suite.validator.Errors()[1], "the SQL database must be provided")
suite.validator.Clear() suite.validator.Clear()
suite.configuration.MySQL = &schema.MySQLStorageConfiguration{ suite.configuration.MySQL = &schema.MySQLStorageConfiguration{

View File

@ -2,6 +2,7 @@ package utils
import ( import (
"crypto/tls" "crypto/tls"
"runtime"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -78,15 +79,34 @@ func TestShouldReturnZeroAndErrorOnInvalidTLSVersions(t *testing.T) {
func TestShouldReturnErrWhenX509DirectoryNotExist(t *testing.T) { func TestShouldReturnErrWhenX509DirectoryNotExist(t *testing.T) {
pool, errs, nonFatalErrs := NewX509CertPool("/tmp/asdfzyxabc123/not/a/real/dir", nil) pool, errs, nonFatalErrs := NewX509CertPool("/tmp/asdfzyxabc123/not/a/real/dir", nil)
assert.NotNil(t, pool) assert.NotNil(t, pool)
assert.Len(t, nonFatalErrs, 0)
if runtime.GOOS == windows {
require.Len(t, nonFatalErrs, 1)
assert.EqualError(t, nonFatalErrs[0], "could not load system certificate pool which may result in untrusted certificate issues: crypto/x509: system root pool is not available on Windows")
} else {
assert.Len(t, nonFatalErrs, 0)
}
require.Len(t, errs, 1) 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")
if runtime.GOOS == windows {
assert.EqualError(t, errs[0], "could not read certificates from directory open /tmp/asdfzyxabc123/not/a/real/dir: The system cannot find the path specified.")
} else {
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) { func TestShouldNotReturnErrWhenX509DirectoryExist(t *testing.T) {
pool, errs, nonFatalErrs := NewX509CertPool("/tmp", nil) pool, errs, nonFatalErrs := NewX509CertPool("/tmp", nil)
assert.NotNil(t, pool) assert.NotNil(t, pool)
assert.Len(t, nonFatalErrs, 0)
if runtime.GOOS == windows {
require.Len(t, nonFatalErrs, 1)
assert.EqualError(t, nonFatalErrs[0], "could not load system certificate pool which may result in untrusted certificate issues: crypto/x509: system root pool is not available on Windows")
} else {
assert.Len(t, nonFatalErrs, 0)
}
assert.Len(t, errs, 0) assert.Len(t, errs, 0)
} }
@ -101,10 +121,20 @@ func TestShouldRaiseNonFatalErrWhenNotifierTrustedCertConfigured(t *testing.T) {
pool, errs, nonFatalErrs := NewX509CertPool("/tmp", config) pool, errs, nonFatalErrs := NewX509CertPool("/tmp", config)
assert.NotNil(t, pool) 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") index := 0
if runtime.GOOS == windows {
require.Len(t, nonFatalErrs, 2)
assert.EqualError(t, nonFatalErrs[0], "could not load system certificate pool which may result in untrusted certificate issues: crypto/x509: system root pool is not available on Windows")
index = 1
} else {
require.Len(t, nonFatalErrs, 1)
}
assert.Len(t, errs, 0)
assert.EqualError(t, nonFatalErrs[index], "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) { func TestShouldRaiseErrAndNonFatalErrWhenNotifierTrustedCertConfiguredAndNotExist(t *testing.T) {
@ -118,17 +148,35 @@ func TestShouldRaiseErrAndNonFatalErrWhenNotifierTrustedCertConfiguredAndNotExis
pool, errs, nonFatalErrs := NewX509CertPool("/tmp", config) pool, errs, nonFatalErrs := NewX509CertPool("/tmp", config)
assert.NotNil(t, pool) assert.NotNil(t, pool)
require.Len(t, nonFatalErrs, 1)
index := 0
if runtime.GOOS == windows {
require.Len(t, nonFatalErrs, 2)
assert.EqualError(t, nonFatalErrs[0], "could not load system certificate pool which may result in untrusted certificate issues: crypto/x509: system root pool is not available on Windows")
index = 1
} else {
require.Len(t, nonFatalErrs, 1)
}
require.Len(t, errs, 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, 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") assert.EqualError(t, nonFatalErrs[index], "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) { func TestShouldReadCertsFromDirectoryButNotKeys(t *testing.T) {
pool, errs, nonFatalErrs := NewX509CertPool("../suites/common/ssl/", nil) pool, errs, nonFatalErrs := NewX509CertPool("../suites/common/ssl/", nil)
assert.NotNil(t, pool) assert.NotNil(t, pool)
require.Len(t, errs, 1) require.Len(t, errs, 1)
assert.Len(t, nonFatalErrs, 0)
if runtime.GOOS == "windows" {
require.Len(t, nonFatalErrs, 1)
assert.EqualError(t, nonFatalErrs[0], "could not load system certificate pool which may result in untrusted certificate issues: crypto/x509: system root pool is not available on Windows")
} else {
assert.Len(t, nonFatalErrs, 0)
}
assert.EqualError(t, errs[0], "could not import certificate key.pem") assert.EqualError(t, errs[0], "could not import certificate key.pem")
} }

View File

@ -25,6 +25,8 @@ const Year = Day * 365
// Month is an int based representation of the time unit. // Month is an int based representation of the time unit.
const Month = Year / 12 const Month = Year / 12
const windows = "windows"
// RFC3339Zero is the default value for time.Time.Unix(). // RFC3339Zero is the default value for time.Time.Unix().
const RFC3339Zero = int64(-62135596800) const RFC3339Zero = int64(-62135596800)