feat(configuration): add error and warn log levels (#2050)

This is so levels like warn and error can be used to exclude info or warn messages. Additionally there is a reasonable refactoring of logging moving the log config options to the logging key because there are a significant number of log options now. This also decouples the expvars and pprof handlers from the log level, and they are now configured by server.enable_expvars and server.enable_pprof at any logging level.
pull/1944/head^2
James Elliott 2021-06-01 14:09:50 +10:00 committed by GitHub
parent 4cfda7eece
commit cef35fadcd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
57 changed files with 482 additions and 195 deletions

View File

@ -52,11 +52,17 @@ func startServer() {
} }
} }
if err := logging.InitializeLogger(config.LogFormat, config.LogFilePath, config.LogKeepStdout); err != nil { if err := logging.InitializeLogger(config.Logging.Format, config.Logging.FilePath, config.Logging.KeepStdout); err != nil {
logger.Fatalf("Cannot initialize logger: %v", err) logger.Fatalf("Cannot initialize logger: %v", err)
} }
switch config.LogLevel { switch config.Logging.Level {
case "error":
logger.Info("Logging severity set to error")
logging.SetLevel(logrus.ErrorLevel)
case "warn":
logger.Info("Logging severity set to warn")
logging.SetLevel(logrus.WarnLevel)
case "info": case "info":
logger.Info("Logging severity set to info") logger.Info("Logging severity set to info")
logging.SetLevel(logrus.InfoLevel) logging.SetLevel(logrus.InfoLevel)

View File

@ -34,17 +34,24 @@ server:
## Must be alphanumeric chars and should not contain any slashes. ## Must be alphanumeric chars and should not contain any slashes.
path: "" path: ""
## Enables the pprof endpoint.
enable_pprof: false
## Enables the expvars endpoint.
enable_expvars: false
## Level of verbosity for logs: info, debug, trace. ## Level of verbosity for logs: info, debug, trace.
log_level: debug logging:
level: debug
## Format the logs are written as: json, text. ## Format the logs are written as: json, text.
# log_format: json # format: json
## File path where the logs will be written. If not set logs are written to stdout. ## File path where the logs will be written. If not set logs are written to stdout.
# log_file_path: /config/authelia.log # file_path: /config/authelia.log
## Whether to also log to stdout when a log_file_path is defined. ## Whether to also log to stdout when a log_file_path is defined.
# log_keep_stdout: false # keep_stdout: false
## The secret used to generate JWT tokens when validating user identity by email confirmation. JWT Secret can also be ## The secret used to generate JWT tokens when validating user identity by email confirmation. JWT Secret can also be
## set using a secret: https://www.authelia.com/docs/configuration/secrets.html ## set using a secret: https://www.authelia.com/docs/configuration/secrets.html

View File

@ -2,7 +2,7 @@
layout: default layout: default
title: Identity Providers title: Identity Providers
parent: Configuration parent: Configuration
nav_order: 12 nav_order: 3
has_children: true has_children: true
--- ---

View File

@ -0,0 +1,106 @@
---
layout: default
title: Logging
parent: Configuration
nav_order: 4
---
# Logging
The logging section tunes the logging settings.
## Configuration
```yaml
logging:
level: info
format: text
file_path: ""
keep_stdout: false
```
## Options
### level
<div markdown="1">
type: string
{: .label .label-config .label-purple }
default: info
{: .label .label-config .label-blue }
required: no
{: .label .label-config .label-green }
</div>
Defines the level of logs used by Authelia. This level can be set to `trace`, `debug`, `info`, `warn`, or `error`. When
setting level to `trace`, you will generate a large amount of log entries and expose the `/debug/vars` and
`/debug/pprof/` endpoints which should not be enabled in production.
```yaml
logging:
level: debug
```
### format
<div markdown="1">
type: string
{: .label .label-config .label-purple }
default: text
{: .label .label-config .label-blue }
required: no
{: .label .label-config .label-green }
</div>
Defines the format of the logs written by Authelia. This format can be set to `json` or `text`.
```yaml
logging:
format: json
```
#### JSON format
```
{"level":"info","msg":"Logging severity set to info","time":"2020-01-01T00:00:00+11:00"}
{"level":"info","msg":"Authelia is listening for non-TLS connections on 0.0.0.0:9091","time":"2020-01-01T00:00:00+11:00"}
```
#### Text format
```
time="2020-01-01T00:00:00+11:00" level=info msg="Logging severity set to info"
time="2020-01-01T00:00:00+11:00" level=info msg="Authelia is listening for non-TLS connections on 0.0.0.0:9091"
```
### file_path
<div markdown="1">
type: string (path)
{: .label .label-config .label-purple }
default: ""
{: .label .label-config .label-blue }
required: no
{: .label .label-config .label-green }
</div>
Logs can be stored in a file when file path is provided. Otherwise logs are written to standard output. When setting the
level to `debug` or `trace` this will generate large amount of log entries. Administrators will need to ensure that
they rotate and/or truncate the logs over time to prevent significant long-term disk usage.
```yaml
logging:
file_path: /config/authelia.log
```
### keep_stdout
<div markdown="1">
type: boolean
{: .label .label-config .label-purple }
default: false
{: .label .label-config .label-blue }
required: no
{: .label .label-config .label-green }
</div>
Overrides the behaviour to redirect logging only to the `file_path`. If set to `true` logs will be written to both
standard output, and the defined logging location.
```yaml
logging:
keep_stdout: true
```

View File

@ -2,7 +2,7 @@
layout: default layout: default
title: Miscellaneous title: Miscellaneous
parent: Configuration parent: Configuration
nav_order: 3 nav_order: 5
--- ---
# Miscellaneous # Miscellaneous
@ -93,88 +93,6 @@ key or the CA public key which signed them (don't add the private key).
certificates_directory: /config/certs/ certificates_directory: /config/certs/
``` ```
## Logging
### log_level
<div markdown="1">
type: string
{: .label .label-config .label-purple }
default: info
{: .label .label-config .label-blue }
required: no
{: .label .label-config .label-green }
</div>
Defines the level of logs used by Authelia. This level can be set to `trace`, `debug` or `info`. When setting log_level
to `trace`, you will generate a large amount of log entries and expose the `/debug/vars` and `/debug/pprof/` endpoints
which should not be enabled in production.
```yaml
log_level: debug
```
### log_format
<div markdown="1">
type: string
{: .label .label-config .label-purple }
default: text
{: .label .label-config .label-blue }
required: no
{: .label .label-config .label-green }
</div>
Defines the format of the logs written by Authelia. This format can be set to `json` or `text`.
```yaml
log_format: json
```
#### JSON format
```
{"level":"info","msg":"Logging severity set to info","time":"2020-01-01T00:00:00+11:00"}
{"level":"info","msg":"Authelia is listening for non-TLS connections on 0.0.0.0:9091","time":"2020-01-01T00:00:00+11:00"}
```
#### Text format
```
time="2020-01-01T00:00:00+11:00" level=info msg="Logging severity set to info"
time="2020-01-01T00:00:00+11:00" level=info msg="Authelia is listening for non-TLS connections on 0.0.0.0:9091"
```
### log_file_path
<div markdown="1">
type: string (path)
{: .label .label-config .label-purple }
default: ""
{: .label .label-config .label-blue }
required: no
{: .label .label-config .label-green }
</div>
Logs can be stored in a file when file path is provided. Otherwise logs are written to standard output. When setting the
log_level to `debug` or `trace` this will generate large amount of log entries. Administrators will need to ensure that
they rotate and/or truncate the logs over time to prevent significant long-term disk usage.
```yaml
log_file_path: /config/authelia.log
```
### log_keep_stdout
<div markdown="1">
type: boolean
{: .label .label-config .label-purple }
default: false
{: .label .label-config .label-blue }
required: no
{: .label .label-config .label-green }
</div>
Overrides the behaviour to redirect logging only to the `log_file_path`. If set to `true` logs will be written to both
standard output, and the defined logging location.
```yaml
log_keep_stdout: true
```
## jwt_secret ## jwt_secret
<div markdown="1"> <div markdown="1">
type: string type: string

View File

@ -2,7 +2,7 @@
layout: default layout: default
title: Notifier title: Notifier
parent: Configuration parent: Configuration
nav_order: 6 nav_order: 8
has_children: true has_children: true
--- ---

View File

@ -2,7 +2,7 @@
layout: default layout: default
title: Time-based One-Time Password title: Time-based One-Time Password
parent: Configuration parent: Configuration
nav_order: 4 nav_order: 6
--- ---
# Time-based One-Time Password # Time-based One-Time Password

View File

@ -2,7 +2,7 @@
layout: default layout: default
title: Regulation title: Regulation
parent: Configuration parent: Configuration
nav_order: 5 nav_order: 7
--- ---
# Regulation # Regulation

View File

@ -2,7 +2,7 @@
layout: default layout: default
title: Secrets title: Secrets
parent: Configuration parent: Configuration
nav_order: 6 nav_order: 8
--- ---
# Secrets # Secrets

View File

@ -2,7 +2,7 @@
layout: default layout: default
title: Server title: Server
parent: Configuration parent: Configuration
nav_order: 7 nav_order: 9
--- ---
# Server # Server
@ -16,6 +16,8 @@ server:
read_buffer_size: 4096 read_buffer_size: 4096
write_buffer_size: 4096 write_buffer_size: 4096
path: "" path: ""
enable_pprof: false
enable_expvars: false
``` ```
## Options ## Options
@ -72,6 +74,31 @@ server:
path: authelia path: authelia
``` ```
### enable_pprof
<div markdown="1">
type: boolean
{: .label .label-config .label-purple }
default: false
{: .label .label-config .label-blue }
required: no
{: .label .label-config .label-green }
</div>
Enables the go pprof endpoints.
### enable_expvars
<div markdown="1">
type: boolean
{: .label .label-config .label-purple }
default: false
{: .label .label-config .label-blue }
required: no
{: .label .label-config .label-green }
</div>
Enables the go expvars endpoints.
## Additional Notes ## Additional Notes
### Buffer Sizes ### Buffer Sizes

View File

@ -2,7 +2,7 @@
layout: default layout: default
title: Session title: Session
parent: Configuration parent: Configuration
nav_order: 8 nav_order: 10
has_children: true has_children: true
--- ---

View File

@ -2,7 +2,7 @@
layout: default layout: default
title: Storage backends title: Storage backends
parent: Configuration parent: Configuration
nav_order: 10 nav_order: 12
has_children: true has_children: true
--- ---

View File

@ -2,7 +2,7 @@
layout: default layout: default
title: Theme title: Theme
parent: Configuration parent: Configuration
nav_order: 11 nav_order: 13
--- ---
# Theme # Theme

View File

@ -5,7 +5,8 @@
host: 0.0.0.0 host: 0.0.0.0
port: 9091 port: 9091
log_level: debug logging:
level: debug
# This secret can also be set using the env variables AUTHELIA_JWT_SECRET_FILE # This secret can also be set using the env variables AUTHELIA_JWT_SECRET_FILE
jwt_secret: a_very_important_secret jwt_secret: a_very_important_secret
default_redirection_url: https://public.example.com default_redirection_url: https://public.example.com

View File

@ -5,7 +5,8 @@
host: 0.0.0.0 host: 0.0.0.0
port: 9091 port: 9091
log_level: debug logging:
level: debug
jwt_secret: a_very_important_secret jwt_secret: a_very_important_secret
default_redirection_url: https://public.example.com default_redirection_url: https://public.example.com
totp: totp:

View File

@ -34,17 +34,24 @@ server:
## Must be alphanumeric chars and should not contain any slashes. ## Must be alphanumeric chars and should not contain any slashes.
path: "" path: ""
## Enables the pprof endpoint.
enable_pprof: false
## Enables the expvars endpoint.
enable_expvars: false
## Level of verbosity for logs: info, debug, trace. ## Level of verbosity for logs: info, debug, trace.
log_level: debug logging:
level: debug
## Format the logs are written as: json, text. ## Format the logs are written as: json, text.
# log_format: json # format: json
## File path where the logs will be written. If not set logs are written to stdout. ## File path where the logs will be written. If not set logs are written to stdout.
# log_file_path: /config/authelia.log # file_path: /config/authelia.log
## Whether to also log to stdout when a log_file_path is defined. ## Whether to also log to stdout when a log_file_path is defined.
# log_keep_stdout: false # keep_stdout: false
## The secret used to generate JWT tokens when validating user identity by email confirmation. JWT Secret can also be ## The secret used to generate JWT tokens when validating user identity by email confirmation. JWT Secret can also be
## set using a secret: https://www.authelia.com/docs/configuration/secrets.html ## set using a secret: https://www.authelia.com/docs/configuration/secrets.html

View File

@ -164,7 +164,7 @@ func TestShouldErrorParseBadConfigFile(t *testing.T) {
require.Len(t, errors, 1) require.Len(t, errors, 1)
require.EqualError(t, errors[0], "Error malformed yaml: line 24: did not find expected alphabetic or numeric character") require.EqualError(t, errors[0], "Error malformed yaml: line 25: did not find expected alphabetic or numeric character")
} }
func TestShouldParseConfigFile(t *testing.T) { func TestShouldParseConfigFile(t *testing.T) {
@ -185,7 +185,7 @@ func TestShouldParseConfigFile(t *testing.T) {
require.Len(t, errors, 0) require.Len(t, errors, 0)
assert.Equal(t, 9091, config.Port) assert.Equal(t, 9091, config.Port)
assert.Equal(t, "debug", config.LogLevel) assert.Equal(t, "debug", config.Logging.Level)
assert.Equal(t, "https://home.example.com:8080/", config.DefaultRedirectionURL) assert.Equal(t, "https://home.example.com:8080/", config.DefaultRedirectionURL)
assert.Equal(t, "authelia.com", config.TOTP.Issuer) assert.Equal(t, "authelia.com", config.TOTP.Issuer)
assert.Equal(t, "secret_from_env", config.JWTSecret) assert.Equal(t, "secret_from_env", config.JWTSecret)
@ -221,7 +221,7 @@ func TestShouldParseAltConfigFile(t *testing.T) {
require.Len(t, errors, 0) require.Len(t, errors, 0)
assert.Equal(t, 9091, config.Port) assert.Equal(t, 9091, config.Port)
assert.Equal(t, "debug", config.LogLevel) assert.Equal(t, "debug", config.Logging.Level)
assert.Equal(t, "https://home.example.com:8080/", config.DefaultRedirectionURL) assert.Equal(t, "https://home.example.com:8080/", config.DefaultRedirectionURL)
assert.Equal(t, "authelia.com", config.TOTP.Issuer) assert.Equal(t, "authelia.com", config.TOTP.Issuer)
assert.Equal(t, "secret_from_env", config.JWTSecret) assert.Equal(t, "secret_from_env", config.JWTSecret)
@ -253,7 +253,7 @@ func TestShouldNotParseConfigFileWithOldOrUnexpectedKeys(t *testing.T) {
return errors[i].Error() < errors[j].Error() return errors[i].Error() < errors[j].Error()
}) })
assert.EqualError(t, errors[0], "config key not expected: loggy_file") assert.EqualError(t, errors[0], "config key not expected: loggy_file")
assert.EqualError(t, errors[1], "invalid configuration key 'logs_level' was replaced by 'log_level'") assert.EqualError(t, errors[1], "invalid configuration key 'logs_level' was replaced by 'logging.level'")
} }
func TestShouldValidateConfigurationTemplate(t *testing.T) { func TestShouldValidateConfigurationTemplate(t *testing.T) {

View File

@ -8,13 +8,16 @@ type Configuration struct {
TLSCert string `mapstructure:"tls_cert"` TLSCert string `mapstructure:"tls_cert"`
TLSKey string `mapstructure:"tls_key"` TLSKey string `mapstructure:"tls_key"`
CertificatesDirectory string `mapstructure:"certificates_directory"` CertificatesDirectory string `mapstructure:"certificates_directory"`
LogLevel string `mapstructure:"log_level"`
LogFormat string `mapstructure:"log_format"`
LogFilePath string `mapstructure:"log_file_path"`
LogKeepStdout bool `mapstructure:"log_keep_stdout"`
JWTSecret string `mapstructure:"jwt_secret"` JWTSecret string `mapstructure:"jwt_secret"`
DefaultRedirectionURL string `mapstructure:"default_redirection_url"` DefaultRedirectionURL string `mapstructure:"default_redirection_url"`
// TODO: DEPRECATED START. Remove in 4.33.0.
LogLevel string `mapstructure:"log_level"`
LogFormat string `mapstructure:"log_format"`
LogFilePath string `mapstructure:"log_file_path"`
// TODO: DEPRECATED END. Remove in 4.33.0.
Logging LoggingConfiguration `mapstructure:"logging"`
IdentityProviders IdentityProvidersConfiguration `mapstructure:"identity_providers"` IdentityProviders IdentityProvidersConfiguration `mapstructure:"identity_providers"`
AuthenticationBackend AuthenticationBackendConfiguration `mapstructure:"authentication_backend"` AuthenticationBackend AuthenticationBackendConfiguration `mapstructure:"authentication_backend"`
Session SessionConfiguration `mapstructure:"session"` Session SessionConfiguration `mapstructure:"session"`

View File

@ -0,0 +1,15 @@
package schema
// LoggingConfiguration represents the logging configuration.
type LoggingConfiguration struct {
Level string `mapstructure:"level"`
Format string `mapstructure:"format"`
FilePath string `mapstructure:"file_path"`
KeepStdout bool `mapstructure:"keep_stdout"`
}
// DefaultLoggingConfiguration is the default logging configuration.
var DefaultLoggingConfiguration = LoggingConfiguration{
Level: "info",
Format: "text",
}

View File

@ -5,6 +5,8 @@ type ServerConfiguration struct {
Path string `mapstructure:"path"` Path string `mapstructure:"path"`
ReadBufferSize int `mapstructure:"read_buffer_size"` ReadBufferSize int `mapstructure:"read_buffer_size"`
WriteBufferSize int `mapstructure:"write_buffer_size"` WriteBufferSize int `mapstructure:"write_buffer_size"`
EnablePprof bool `mapstructure:"enable_endpoint_pprof"`
EnableExpvars bool `mapstructure:"enable_endpoint_expvars"`
} }
// DefaultServerConfiguration represents the default values of the ServerConfiguration. // DefaultServerConfiguration represents the default values of the ServerConfiguration.

View File

@ -2,7 +2,8 @@
host: 127.0.0.1 host: 127.0.0.1
port: 9091 port: 9091
log_level: debug logging:
level: debug
default_redirection_url: https://home.example.com:8080/ default_redirection_url: https://home.example.com:8080/
totp: totp:

View File

@ -2,7 +2,8 @@
host: 127.0.0.1 host: 127.0.0.1
port: 9091 port: 9091
log_level: debug logging:
level: debug
default_redirection_url: https://home.example.com:8080/ default_redirection_url: https://home.example.com:8080/
totp: totp:

View File

@ -1,7 +1,8 @@
--- ---
host: 0.0.0.0 host: 0.0.0.0
port: 9091 port: 9091
log_level: debug logging:
level: debug
jwt_secret: RUtG9TnbXrOl1XLLmDgySw1DGgx9QcrtepIf1uDDBlBVKFZxkVBruYKBi32PvaU jwt_secret: RUtG9TnbXrOl1XLLmDgySw1DGgx9QcrtepIf1uDDBlBVKFZxkVBruYKBi32PvaU

View File

@ -3,7 +3,8 @@ host: 127.0.0.1
port: 9091 port: 9091
jwt_secret: secret_from_config jwt_secret: secret_from_config
log_level: debug logging:
level: debug
default_redirection_url: https://home.example.com:8080/ default_redirection_url: https://home.example.com:8080/
totp: totp:

View File

@ -50,7 +50,11 @@ func IsNetworkValid(network string) (isValid bool) {
} }
// ValidateAccessControl validates access control configuration. // ValidateAccessControl validates access control configuration.
func ValidateAccessControl(configuration schema.AccessControlConfiguration, validator *schema.StructValidator) { func ValidateAccessControl(configuration *schema.AccessControlConfiguration, validator *schema.StructValidator) {
if configuration.DefaultPolicy == "" {
configuration.DefaultPolicy = denyPolicy
}
if !IsPolicyValid(configuration.DefaultPolicy) { if !IsPolicyValid(configuration.DefaultPolicy) {
validator.Push(fmt.Errorf("'default_policy' must either be 'deny', 'two_factor', 'one_factor' or 'bypass'")) validator.Push(fmt.Errorf("'default_policy' must either be 'deny', 'two_factor', 'one_factor' or 'bypass'"))
} }

View File

@ -24,7 +24,7 @@ func (suite *AccessControl) SetupTest() {
} }
func (suite *AccessControl) TestShouldValidateCompleteConfiguration() { func (suite *AccessControl) TestShouldValidateCompleteConfiguration() {
ValidateAccessControl(suite.configuration, suite.validator) ValidateAccessControl(&suite.configuration, suite.validator)
suite.Assert().False(suite.validator.HasWarnings()) suite.Assert().False(suite.validator.HasWarnings())
suite.Assert().False(suite.validator.HasErrors()) suite.Assert().False(suite.validator.HasErrors())
@ -33,7 +33,7 @@ func (suite *AccessControl) TestShouldValidateCompleteConfiguration() {
func (suite *AccessControl) TestShouldRaiseErrorInvalidDefaultPolicy() { func (suite *AccessControl) TestShouldRaiseErrorInvalidDefaultPolicy() {
suite.configuration.DefaultPolicy = testInvalidPolicy suite.configuration.DefaultPolicy = testInvalidPolicy
ValidateAccessControl(suite.configuration, suite.validator) ValidateAccessControl(&suite.configuration, suite.validator)
suite.Assert().False(suite.validator.HasWarnings()) suite.Assert().False(suite.validator.HasWarnings())
suite.Require().Len(suite.validator.Errors(), 1) suite.Require().Len(suite.validator.Errors(), 1)
@ -49,7 +49,7 @@ func (suite *AccessControl) TestShouldRaiseErrorInvalidNetworkGroupNetwork() {
}, },
} }
ValidateAccessControl(suite.configuration, suite.validator) ValidateAccessControl(&suite.configuration, suite.validator)
suite.Assert().False(suite.validator.HasWarnings()) suite.Assert().False(suite.validator.HasWarnings())
suite.Require().Len(suite.validator.Errors(), 1) suite.Require().Len(suite.validator.Errors(), 1)

View File

@ -9,7 +9,6 @@ import (
) )
var defaultPort = 9091 var defaultPort = 9091
var defaultLogLevel = "info"
// ValidateConfiguration and adapt the configuration read from file. // 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. //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.
@ -37,10 +36,6 @@ func ValidateConfiguration(configuration *schema.Configuration, validator *schem
} }
} }
if configuration.LogLevel == "" {
configuration.LogLevel = defaultLogLevel
}
if configuration.JWTSecret == "" { if configuration.JWTSecret == "" {
validator.Push(fmt.Errorf("Provide a JWT secret using \"jwt_secret\" key")) validator.Push(fmt.Errorf("Provide a JWT secret using \"jwt_secret\" key"))
} }
@ -52,25 +47,19 @@ func ValidateConfiguration(configuration *schema.Configuration, validator *schem
} }
} }
if configuration.Theme == "" {
configuration.Theme = "light"
}
ValidateTheme(configuration, validator) ValidateTheme(configuration, validator)
if configuration.TOTP == nil { if configuration.TOTP == nil {
configuration.TOTP = &schema.DefaultTOTPConfiguration configuration.TOTP = &schema.DefaultTOTPConfiguration
} }
ValidateLogging(configuration, validator)
ValidateTOTP(configuration.TOTP, validator) ValidateTOTP(configuration.TOTP, validator)
ValidateAuthenticationBackend(&configuration.AuthenticationBackend, validator) ValidateAuthenticationBackend(&configuration.AuthenticationBackend, validator)
if configuration.AccessControl.DefaultPolicy == "" { ValidateAccessControl(&configuration.AccessControl, validator)
configuration.AccessControl.DefaultPolicy = denyPolicy
}
ValidateAccessControl(configuration.AccessControl, validator)
ValidateRules(configuration.AccessControl, validator) ValidateRules(configuration.AccessControl, validator)

View File

@ -14,8 +14,8 @@ func newDefaultConfig() schema.Configuration {
config := schema.Configuration{} config := schema.Configuration{}
config.Host = "127.0.0.1" config.Host = "127.0.0.1"
config.Port = 9090 config.Port = 9090
config.LogLevel = "info" config.Logging.Level = "info"
config.LogFormat = "text" config.Logging.Format = "text"
config.JWTSecret = testJWTSecret config.JWTSecret = testJWTSecret
config.AuthenticationBackend.File = &schema.FileAuthenticationBackendConfiguration{ config.AuthenticationBackend.File = &schema.FileAuthenticationBackendConfiguration{
Path: "/a/path", Path: "/a/path",
@ -48,7 +48,7 @@ func TestShouldNotUpdateConfig(t *testing.T) {
require.Len(t, validator.Errors(), 0) require.Len(t, validator.Errors(), 0)
assert.Equal(t, 9090, config.Port) assert.Equal(t, 9090, config.Port)
assert.Equal(t, "info", config.LogLevel) assert.Equal(t, "info", config.Logging.Level)
} }
func TestShouldValidateAndUpdatePort(t *testing.T) { func TestShouldValidateAndUpdatePort(t *testing.T) {
@ -73,17 +73,6 @@ func TestShouldValidateAndUpdateHost(t *testing.T) {
assert.Equal(t, "0.0.0.0", config.Host) assert.Equal(t, "0.0.0.0", config.Host)
} }
func TestShouldValidateAndUpdateLogsLevel(t *testing.T) {
validator := schema.NewStructValidator()
config := newDefaultConfig()
config.LogLevel = ""
ValidateConfiguration(&config, validator)
require.Len(t, validator.Errors(), 0)
assert.Equal(t, "info", config.LogLevel)
}
func TestShouldEnsureNotifierConfigIsProvided(t *testing.T) { func TestShouldEnsureNotifierConfigIsProvided(t *testing.T) {
validator := schema.NewStructValidator() validator := schema.NewStructValidator()
config := newDefaultConfig() config := newDefaultConfig()

View File

@ -1,11 +1,16 @@
package validator package validator
const ( const (
errFmtDeprecatedConfigurationKey = "[DEPRECATED] The %s configuration option is deprecated and will be " +
"removed in %s, please use %s instead"
errFmtReplacedConfigurationKey = "invalid configuration key '%s' was replaced by '%s'"
errFmtLoggingLevelInvalid = "the log level '%s' is invalid, must be one of: %s"
errFmtSessionSecretRedisProvider = "The session secret must be set when using the %s session provider" 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" 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" 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" errFmtSessionRedisHostOrNodesRequired = "Either the host or a node must be provided when using the %s session provider"
errFmtReplacedConfigurationKey = "invalid configuration key '%s' was replaced by '%s'"
errOAuthOIDCServerClientRedirectURIFmt = "OIDC Server Client redirect URI %s has an invalid scheme %s, should be http or https" errOAuthOIDCServerClientRedirectURIFmt = "OIDC Server Client redirect URI %s has an invalid scheme %s, should be http or https"
errOAuthOIDCServerClientRedirectURICantBeParsedFmt = "OIDC Client with ID '%s' has an invalid redirect URI '%s' could not be parsed: %v" errOAuthOIDCServerClientRedirectURICantBeParsedFmt = "OIDC Client with ID '%s' has an invalid redirect URI '%s' could not be parsed: %v"
@ -43,6 +48,7 @@ const (
"https://www.authelia.com/docs/configuration/access-control.html#combining-subjects-and-the-bypass-policy" "https://www.authelia.com/docs/configuration/access-control.html#combining-subjects-and-the-bypass-policy"
) )
var validLoggingLevels = []string{"trace", "debug", "info", "warn", "error"}
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. // SecretNames contains a map of secret names.
@ -66,19 +72,30 @@ var validKeys = []string{
// Root Keys. // Root Keys.
"host", "host",
"port", "port",
"log_level",
"log_format",
"log_file_path",
"default_redirection_url", "default_redirection_url",
"theme", "theme",
"tls_key", "tls_key",
"tls_cert", "tls_cert",
"certificates_directory", "certificates_directory",
// Logging keys.
"logging.level",
"logging.format",
"logging.file_path",
"logging.keep_stdout",
// TODO: DEPRECATED START. Remove in 4.33.0.
"log_level",
"log_format",
"log_file_path",
// TODO: DEPRECATED END. Remove in 4.33.0.
// Server Keys. // Server Keys.
"server.read_buffer_size", "server.read_buffer_size",
"server.write_buffer_size", "server.write_buffer_size",
"server.path", "server.path",
"server.enable_pprof",
"server.enable_expvars",
// TOTP Keys. // TOTP Keys.
"totp.issuer", "totp.issuer",
@ -200,8 +217,8 @@ var replacedKeys = map[string]string{
"authentication_backend.ldap.skip_verify": "authentication_backend.ldap.tls.skip_verify", "authentication_backend.ldap.skip_verify": "authentication_backend.ldap.tls.skip_verify",
"authentication_backend.ldap.minimum_tls_version": "authentication_backend.ldap.tls.minimum_version", "authentication_backend.ldap.minimum_tls_version": "authentication_backend.ldap.tls.minimum_version",
"notifier.smtp.disable_verify_cert": "notifier.smtp.tls.skip_verify", "notifier.smtp.disable_verify_cert": "notifier.smtp.tls.skip_verify",
"logs_file_path": "log_file", "logs_file_path": "logging.file_path",
"logs_level": "log_level", "logs_level": "logging.level",
} }
var specificErrorKeys = map[string]string{ var specificErrorKeys = map[string]string{

View File

@ -106,8 +106,8 @@ func TestReplacedErrors(t *testing.T) {
assert.EqualError(t, errs[0], fmt.Sprintf(errFmtReplacedConfigurationKey, "authentication_backend.ldap.skip_verify", "authentication_backend.ldap.tls.skip_verify")) assert.EqualError(t, errs[0], fmt.Sprintf(errFmtReplacedConfigurationKey, "authentication_backend.ldap.skip_verify", "authentication_backend.ldap.tls.skip_verify"))
assert.EqualError(t, errs[1], fmt.Sprintf(errFmtReplacedConfigurationKey, "authentication_backend.ldap.minimum_tls_version", "authentication_backend.ldap.tls.minimum_version")) assert.EqualError(t, errs[1], fmt.Sprintf(errFmtReplacedConfigurationKey, "authentication_backend.ldap.minimum_tls_version", "authentication_backend.ldap.tls.minimum_version"))
assert.EqualError(t, errs[2], fmt.Sprintf(errFmtReplacedConfigurationKey, "notifier.smtp.disable_verify_cert", "notifier.smtp.tls.skip_verify")) assert.EqualError(t, errs[2], fmt.Sprintf(errFmtReplacedConfigurationKey, "notifier.smtp.disable_verify_cert", "notifier.smtp.tls.skip_verify"))
assert.EqualError(t, errs[3], fmt.Sprintf(errFmtReplacedConfigurationKey, "logs_file_path", "log_file")) assert.EqualError(t, errs[3], fmt.Sprintf(errFmtReplacedConfigurationKey, "logs_file_path", "logging.file_path"))
assert.EqualError(t, errs[4], fmt.Sprintf(errFmtReplacedConfigurationKey, "logs_level", "log_level")) assert.EqualError(t, errs[4], fmt.Sprintf(errFmtReplacedConfigurationKey, "logs_level", "logging.level"))
} }
func TestSecretKeysDontRaiseErrors(t *testing.T) { func TestSecretKeysDontRaiseErrors(t *testing.T) {

View File

@ -0,0 +1,53 @@
package validator
import (
"fmt"
"strings"
"github.com/authelia/authelia/internal/configuration/schema"
"github.com/authelia/authelia/internal/utils"
)
// ValidateLogging validates the logging configuration.
func ValidateLogging(configuration *schema.Configuration, validator *schema.StructValidator) {
applyDeprecatedLoggingConfiguration(configuration, validator) // TODO: DEPRECATED LINE. Remove in 4.33.0.
if configuration.Logging.Level == "" {
configuration.Logging.Level = schema.DefaultLoggingConfiguration.Level
}
if configuration.Logging.Format == "" {
configuration.Logging.Format = schema.DefaultLoggingConfiguration.Format
}
if !utils.IsStringInSlice(configuration.Logging.Level, validLoggingLevels) {
validator.Push(fmt.Errorf(errFmtLoggingLevelInvalid, configuration.Logging.Level, strings.Join(validLoggingLevels, ", ")))
}
}
// TODO: DEPRECATED FUNCTION. Remove in 4.33.0.
func applyDeprecatedLoggingConfiguration(configuration *schema.Configuration, validator *schema.StructValidator) {
if configuration.LogLevel != "" {
validator.PushWarning(fmt.Errorf(errFmtDeprecatedConfigurationKey, "log_level", "4.33.0", "logging.level"))
if configuration.Logging.Level == "" {
configuration.Logging.Level = configuration.LogLevel
}
}
if configuration.LogFormat != "" {
validator.PushWarning(fmt.Errorf(errFmtDeprecatedConfigurationKey, "log_format", "4.33.0", "logging.format"))
if configuration.Logging.Format == "" {
configuration.Logging.Format = configuration.LogFormat
}
}
if configuration.LogFilePath != "" {
validator.PushWarning(fmt.Errorf(errFmtDeprecatedConfigurationKey, "log_file_path", "4.33.0", "logging.file_path"))
if configuration.Logging.FilePath == "" {
configuration.Logging.FilePath = configuration.LogFilePath
}
}
}

View File

@ -0,0 +1,111 @@
package validator
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/authelia/authelia/internal/configuration/schema"
)
func TestShouldSetDefaultLoggingValues(t *testing.T) {
config := &schema.Configuration{}
validator := schema.NewStructValidator()
ValidateLogging(config, validator)
assert.Len(t, validator.Warnings(), 0)
assert.Len(t, validator.Errors(), 0)
require.NotNil(t, config.Logging.KeepStdout)
assert.Equal(t, "", config.LogLevel)
assert.Equal(t, "", config.LogFormat)
assert.Equal(t, "", config.LogFilePath)
assert.Equal(t, "info", config.Logging.Level)
assert.Equal(t, "text", config.Logging.Format)
assert.Equal(t, "", config.Logging.FilePath)
}
func TestShouldRaiseErrorOnInvalidLoggingLevel(t *testing.T) {
config := &schema.Configuration{
Logging: schema.LoggingConfiguration{
Level: "TRACE",
},
}
validator := schema.NewStructValidator()
ValidateLogging(config, validator)
assert.Len(t, validator.Warnings(), 0)
require.Len(t, validator.Errors(), 1)
assert.EqualError(t, validator.Errors()[0], "the log level 'TRACE' is invalid, must be one of: trace, debug, info, warn, error")
}
// TODO: DEPRECATED TEST. Remove in 4.33.0.
func TestShouldMigrateDeprecatedLoggingConfig(t *testing.T) {
config := &schema.Configuration{
LogLevel: "trace",
LogFormat: "json",
LogFilePath: "/a/b/c",
}
validator := schema.NewStructValidator()
ValidateLogging(config, validator)
assert.Len(t, validator.Errors(), 0)
require.Len(t, validator.Warnings(), 3)
require.NotNil(t, config.Logging.KeepStdout)
assert.Equal(t, "trace", config.LogLevel)
assert.Equal(t, "json", config.LogFormat)
assert.Equal(t, "/a/b/c", config.LogFilePath)
assert.Equal(t, "trace", config.Logging.Level)
assert.Equal(t, "json", config.Logging.Format)
assert.Equal(t, "/a/b/c", config.Logging.FilePath)
assert.EqualError(t, validator.Warnings()[0], fmt.Sprintf(errFmtDeprecatedConfigurationKey, "log_level", "4.33.0", "logging.level"))
assert.EqualError(t, validator.Warnings()[1], fmt.Sprintf(errFmtDeprecatedConfigurationKey, "log_format", "4.33.0", "logging.format"))
assert.EqualError(t, validator.Warnings()[2], fmt.Sprintf(errFmtDeprecatedConfigurationKey, "log_file_path", "4.33.0", "logging.file_path"))
}
func TestShouldRaiseErrorsAndNotOverwriteConfigurationWhenUsingDeprecatedLoggingConfig(t *testing.T) {
config := &schema.Configuration{
Logging: schema.LoggingConfiguration{
Level: "info",
Format: "text",
FilePath: "/x/y/z",
KeepStdout: true,
},
LogLevel: "debug",
LogFormat: "json",
LogFilePath: "/a/b/c",
}
validator := schema.NewStructValidator()
ValidateLogging(config, validator)
require.NotNil(t, config.Logging.KeepStdout)
assert.Equal(t, "info", config.Logging.Level)
assert.Equal(t, "text", config.Logging.Format)
assert.True(t, config.Logging.KeepStdout)
assert.Equal(t, "/x/y/z", config.Logging.FilePath)
assert.Len(t, validator.Errors(), 0)
require.Len(t, validator.Warnings(), 3)
assert.EqualError(t, validator.Warnings()[0], fmt.Sprintf(errFmtDeprecatedConfigurationKey, "log_level", "4.33.0", "logging.level"))
assert.EqualError(t, validator.Warnings()[1], fmt.Sprintf(errFmtDeprecatedConfigurationKey, "log_format", "4.33.0", "logging.format"))
assert.EqualError(t, validator.Warnings()[2], fmt.Sprintf(errFmtDeprecatedConfigurationKey, "log_file_path", "4.33.0", "logging.file_path"))
}

View File

@ -9,6 +9,10 @@ import (
// ValidateTheme validates and update Theme configuration. // ValidateTheme validates and update Theme configuration.
func ValidateTheme(configuration *schema.Configuration, validator *schema.StructValidator) { func ValidateTheme(configuration *schema.Configuration, validator *schema.StructValidator) {
if configuration.Theme == "" {
configuration.Theme = "light"
}
validThemes := regexp.MustCompile("light|dark|grey") validThemes := regexp.MustCompile("light|dark|grey")
if !validThemes.MatchString(configuration.Theme) { if !validThemes.MatchString(configuration.Theme) {
validator.Push(fmt.Errorf("Theme: %s is not valid, valid themes are: \"light\", \"dark\" or \"grey\"", configuration.Theme)) validator.Push(fmt.Errorf("Theme: %s is not valid, valid themes are: \"light\", \"dark\" or \"grey\"", configuration.Theme))

View File

@ -339,7 +339,7 @@ func verifySessionHasUpToDateProfile(ctx *middlewares.AutheliaCtx, targetURL *ur
} }
} else { } else {
ctx.Logger.Debugf("Updated profile detected for %s.", userSession.Username) ctx.Logger.Debugf("Updated profile detected for %s.", userSession.Username)
if ctx.Configuration.LogLevel == "trace" { if ctx.Configuration.Logging.Level == "trace" {
generateVerifySessionHasUpToDateProfileTraceLogs(ctx, userSession, details) generateVerifySessionHasUpToDateProfileTraceLogs(ctx, userSession, details)
} }
userSession.Emails = details.Emails userSession.Emails = details.Emails

View File

@ -127,9 +127,11 @@ func registerRoutes(configuration schema.Configuration, providers middlewares.Pr
middlewares.RequireFirstFactor(handlers.SecondFactorDuoPost(duoAPI)))) middlewares.RequireFirstFactor(handlers.SecondFactorDuoPost(duoAPI))))
} }
// If trace is set, enable pprofhandler and expvarhandler. if configuration.Server.EnablePprof {
if configuration.LogLevel == "trace" {
r.GET("/debug/pprof/{name?}", pprofhandler.PprofHandler) r.GET("/debug/pprof/{name?}", pprofhandler.PprofHandler)
}
if configuration.Server.EnableExpvars {
r.GET("/debug/vars", expvarhandler.ExpvarHandler) r.GET("/debug/vars", expvarhandler.ExpvarHandler)
} }

View File

@ -9,7 +9,8 @@ tls_key: /config/ssl/key.pem
theme: grey theme: grey
log_level: debug logging:
level: debug
default_redirection_url: https://home.example.com:8080/ default_redirection_url: https://home.example.com:8080/

View File

@ -7,7 +7,8 @@ port: 9091
tls_cert: /config/ssl/cert.pem tls_cert: /config/ssl/cert.pem
tls_key: /config/ssl/key.pem tls_key: /config/ssl/key.pem
log_level: debug logging:
level: debug
jwt_secret: unsecure_secret jwt_secret: unsecure_secret

View File

@ -7,7 +7,8 @@ port: 9091
tls_cert: /config/ssl/cert.pem tls_cert: /config/ssl/cert.pem
tls_key: /config/ssl/key.pem tls_key: /config/ssl/key.pem
log_level: debug logging:
level: debug
jwt_secret: unsecure_secret jwt_secret: unsecure_secret

View File

@ -7,7 +7,8 @@ port: 9091
tls_cert: /config/ssl/cert.pem tls_cert: /config/ssl/cert.pem
tls_key: /config/ssl/key.pem tls_key: /config/ssl/key.pem
log_level: debug logging:
level: debug
default_redirection_url: https://home.example.com:8080/ default_redirection_url: https://home.example.com:8080/

View File

@ -7,7 +7,8 @@ port: 9091
tls_cert: /config/ssl/cert.pem tls_cert: /config/ssl/cert.pem
tls_key: /config/ssl/key.pem tls_key: /config/ssl/key.pem
log_level: trace logging:
level: trace
default_redirection_url: https://home.example.com:8080/ default_redirection_url: https://home.example.com:8080/

View File

@ -7,7 +7,8 @@ port: 9091
tls_cert: /config/ssl/cert.pem tls_cert: /config/ssl/cert.pem
tls_key: /config/ssl/key.pem tls_key: /config/ssl/key.pem
log_level: debug logging:
level: debug
jwt_secret: unsecure_secret jwt_secret: unsecure_secret

View File

@ -7,7 +7,8 @@ port: 9091
tls_cert: /config/ssl/cert.pem tls_cert: /config/ssl/cert.pem
tls_key: /config/ssl/key.pem tls_key: /config/ssl/key.pem
log_level: debug logging:
level: debug
jwt_secret: unsecure_secret jwt_secret: unsecure_secret

View File

@ -9,7 +9,8 @@ tls_key: /config/ssl/key.pem
theme: dark theme: dark
log_level: debug logging:
level: debug
default_redirection_url: https://home.example.com:8080/ default_redirection_url: https://home.example.com:8080/

View File

@ -7,7 +7,8 @@ port: 9091
tls_cert: /config/ssl/cert.pem tls_cert: /config/ssl/cert.pem
tls_key: /config/ssl/key.pem tls_key: /config/ssl/key.pem
log_level: debug logging:
level: debug
default_redirection_url: https://home.example.com:8080/ default_redirection_url: https://home.example.com:8080/

View File

@ -7,7 +7,8 @@ port: 9091
tls_cert: /config/ssl/cert.pem tls_cert: /config/ssl/cert.pem
tls_key: /config/ssl/key.pem tls_key: /config/ssl/key.pem
log_level: debug logging:
level: debug
default_redirection_url: https://home.example.com:8080/ default_redirection_url: https://home.example.com:8080/

View File

@ -7,7 +7,8 @@ port: 9091
tls_cert: /config/ssl/cert.pem tls_cert: /config/ssl/cert.pem
tls_key: /config/ssl/key.pem tls_key: /config/ssl/key.pem
log_level: debug logging:
level: debug
jwt_secret: unsecure_password jwt_secret: unsecure_password

View File

@ -3,7 +3,8 @@ port: 9091
tls_cert: /config/ssl/cert.pem tls_cert: /config/ssl/cert.pem
tls_key: /config/ssl/key.pem tls_key: /config/ssl/key.pem
log_level: debug logging:
level: debug
jwt_secret: unsecure_secret jwt_secret: unsecure_secret

View File

@ -3,7 +3,8 @@ port: 9091
tls_cert: /config/ssl/cert.pem tls_cert: /config/ssl/cert.pem
tls_key: /config/ssl/key.pem tls_key: /config/ssl/key.pem
log_level: debug logging:
level: debug
jwt_secret: unsecure_secret jwt_secret: unsecure_secret

View File

@ -7,7 +7,8 @@ port: 9091
tls_cert: /config/ssl/cert.pem tls_cert: /config/ssl/cert.pem
tls_key: /config/ssl/key.pem tls_key: /config/ssl/key.pem
log_level: debug logging:
level: debug
default_redirection_url: https://home.example.com:8080/ default_redirection_url: https://home.example.com:8080/

View File

@ -10,7 +10,8 @@ tls_key: /config/ssl/key.pem
server: server:
path: auth path: auth
log_level: debug logging:
level: debug
jwt_secret: unsecure_secret jwt_secret: unsecure_secret

View File

@ -7,7 +7,8 @@ port: 9091
tls_cert: /config/ssl/cert.pem tls_cert: /config/ssl/cert.pem
tls_key: /config/ssl/key.pem tls_key: /config/ssl/key.pem
log_level: debug logging:
level: debug
default_redirection_url: https://home.example.com:8080/ default_redirection_url: https://home.example.com:8080/

View File

@ -7,7 +7,8 @@ port: 9091
tls_cert: /config/ssl/cert.pem tls_cert: /config/ssl/cert.pem
tls_key: /config/ssl/key.pem tls_key: /config/ssl/key.pem
log_level: debug logging:
level: debug
jwt_secret: unsecure_secret jwt_secret: unsecure_secret

View File

@ -7,7 +7,8 @@ port: 9091
tls_cert: /config/ssl/cert.pem tls_cert: /config/ssl/cert.pem
tls_key: /config/ssl/key.pem tls_key: /config/ssl/key.pem
log_level: debug logging:
level: debug
authentication_backend: authentication_backend:
file: file:

View File

@ -7,7 +7,8 @@ port: 9091
tls_cert: /config/ssl/cert.pem tls_cert: /config/ssl/cert.pem
tls_key: /config/ssl/key.pem tls_key: /config/ssl/key.pem
log_level: debug logging:
level: debug
jwt_secret: unsecure_secret jwt_secret: unsecure_secret

View File

@ -7,7 +7,8 @@ port: 9091
tls_cert: /config/ssl/cert.pem tls_cert: /config/ssl/cert.pem
tls_key: /config/ssl/key.pem tls_key: /config/ssl/key.pem
log_level: debug logging:
level: debug
jwt_secret: unsecure_secret jwt_secret: unsecure_secret

View File

@ -7,7 +7,8 @@ port: 443
tls_cert: /config/ssl/cert.pem tls_cert: /config/ssl/cert.pem
tls_key: /config/ssl/key.pem tls_key: /config/ssl/key.pem
log_level: debug logging:
level: debug
default_redirection_url: https://home.example.com:8080 default_redirection_url: https://home.example.com:8080

View File

@ -19,9 +19,9 @@ func IsStringAlphaNumeric(input string) bool {
} }
// IsStringInSlice checks if a single string is in a slice of strings. // IsStringInSlice checks if a single string is in a slice of strings.
func IsStringInSlice(a string, slice []string) (inSlice bool) { func IsStringInSlice(needle string, haystack []string) (inSlice bool) {
for _, b := range slice { for _, b := range haystack {
if b == a { if b == needle {
return true return true
} }
} }
@ -30,9 +30,9 @@ func IsStringInSlice(a string, slice []string) (inSlice bool) {
} }
// IsStringInSliceFold checks if a single string is in a slice of strings but uses strings.EqualFold to compare them. // IsStringInSliceFold checks if a single string is in a slice of strings but uses strings.EqualFold to compare them.
func IsStringInSliceFold(a string, slice []string) (inSlice bool) { func IsStringInSliceFold(needle string, haystack []string) (inSlice bool) {
for _, b := range slice { for _, b := range haystack {
if strings.EqualFold(b, a) { if strings.EqualFold(b, needle) {
return true return true
} }
} }
@ -41,9 +41,9 @@ func IsStringInSliceFold(a string, slice []string) (inSlice bool) {
} }
// IsStringInSliceContains checks if a single string is in an array of strings. // IsStringInSliceContains checks if a single string is in an array of strings.
func IsStringInSliceContains(a string, list []string) (inSlice bool) { func IsStringInSliceContains(needle string, haystack []string) (inSlice bool) {
for _, b := range list { for _, b := range haystack {
if strings.Contains(a, b) { if strings.Contains(needle, b) {
return true return true
} }
} }