feat(configuration): replace viper with koanf (#2053)

This commit replaces github.com/spf13/viper with github.com/knadh/koanf. Koanf is very similar library to viper, with less dependencies and several quality of life differences. This also allows most config options to be defined by ENV. Lastly it also enables the use of split configuration files which can be configured by setting the --config flag multiple times.

Co-authored-by: Amir Zarrinkafsh <nightah@me.com>
pull/2229/head
James Elliott 2021-08-03 19:55:21 +10:00 committed by GitHub
parent 3d656eb5db
commit a7e867a699
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
76 changed files with 2550 additions and 1469 deletions

View File

@ -12,7 +12,7 @@ func getXFlags(branch, build, extra string) (flags []string, err error) {
if branch == "" {
out, _, err := utils.RunCommandAndReturnOutput("git rev-parse --abbrev-ref HEAD")
if err != nil {
return flags, err
return flags, fmt.Errorf("error getting branch with git rev-parse: %w", err)
}
if out == "" {
@ -24,17 +24,17 @@ func getXFlags(branch, build, extra string) (flags []string, err error) {
gitTagCommit, _, err := utils.RunCommandAndReturnOutput("git rev-list --tags --max-count=1")
if err != nil {
return flags, err
return flags, fmt.Errorf("error getting tag commit with git rev-list: %w", err)
}
tag, _, err := utils.RunCommandAndReturnOutput("git describe --tags --abbrev=0 " + gitTagCommit)
tag, _, err := utils.RunCommandAndReturnOutput(fmt.Sprintf("git describe --tags --abbrev=0 %s", gitTagCommit))
if err != nil {
return flags, err
return flags, fmt.Errorf("error getting tag with git describe: %w", err)
}
commit, _, err := utils.RunCommandAndReturnOutput("git rev-parse HEAD")
if err != nil {
return flags, err
return flags, fmt.Errorf("error getting commit with git rev-parse: %w", err)
}
var states []string

View File

@ -136,7 +136,7 @@ func main() {
cobraCommands = append(cobraCommands, command)
}
cobraCommands = append(cobraCommands, commands.HashPasswordCmd, commands.CertificatesCmd, commands.RSACmd, xflagsCmd)
cobraCommands = append(cobraCommands, commands.NewHashPasswordCmd(), commands.NewCertificatesCmd(), commands.NewRSACmd(), xflagsCmd)
rootCmd.PersistentFlags().BoolVar(&buildkite, "buildkite", false, "Set CI flag for Buildkite")
rootCmd.PersistentFlags().StringVar(&logLevel, "log-level", "info", "Set the log level for the command")

View File

@ -1,21 +0,0 @@
package main
const fmtAutheliaLong = `authelia %s
An open-source authentication and authorization server providing
two-factor authentication and single sign-on (SSO) for your
applications via a web portal.
Documentation is available at: https://www.authelia.com/docs
`
const fmtAutheliaBuild = `Last Tag: %s
State: %s
Branch: %s
Commit: %s
Build Number: %s
Build OS: %s
Build Arch: %s
Build Date: %s
Extra: %s
`

View File

@ -1,188 +1,14 @@
package main
import (
"fmt"
"os"
"runtime"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/authelia/authelia/internal/authentication"
"github.com/authelia/authelia/internal/authorization"
"github.com/authelia/authelia/internal/commands"
"github.com/authelia/authelia/internal/configuration"
"github.com/authelia/authelia/internal/logging"
"github.com/authelia/authelia/internal/middlewares"
"github.com/authelia/authelia/internal/notification"
"github.com/authelia/authelia/internal/oidc"
"github.com/authelia/authelia/internal/regulation"
"github.com/authelia/authelia/internal/server"
"github.com/authelia/authelia/internal/session"
"github.com/authelia/authelia/internal/storage"
"github.com/authelia/authelia/internal/utils"
)
var configPathFlag string
//nolint:gocyclo // TODO: Consider refactoring/simplifying, time permitting.
func startServer() {
logger := logging.Logger()
config, errs := configuration.Read(configPathFlag)
if len(errs) > 0 {
for _, err := range errs {
logger.Error(err)
}
os.Exit(1)
}
autheliaCertPool, errs, nonFatalErrs := utils.NewX509CertPool(config.CertificatesDirectory)
if len(errs) > 0 {
for _, err := range errs {
logger.Error(err)
}
os.Exit(2)
}
if len(nonFatalErrs) > 0 {
for _, err := range nonFatalErrs {
logger.Warn(err)
}
}
if err := logging.InitializeLogger(config.Logging.Format, config.Logging.FilePath, config.Logging.KeepStdout); err != nil {
logger.Fatalf("Cannot initialize logger: %v", err)
}
logger.Infof("Authelia %s is starting", utils.Version())
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":
logger.Info("Logging severity set to info")
logging.SetLevel(logrus.InfoLevel)
case "debug":
logger.Info("Logging severity set to debug")
logging.SetLevel(logrus.DebugLevel)
case "trace":
logger.Info("Logging severity set to trace")
logging.SetLevel(logrus.TraceLevel)
}
if os.Getenv("ENVIRONMENT") == "dev" {
logger.Info("===> Authelia is running in development mode. <===")
}
var storageProvider storage.Provider
switch {
case config.Storage.PostgreSQL != nil:
storageProvider = storage.NewPostgreSQLProvider(*config.Storage.PostgreSQL)
case config.Storage.MySQL != nil:
storageProvider = storage.NewMySQLProvider(*config.Storage.MySQL)
case config.Storage.Local != nil:
storageProvider = storage.NewSQLiteProvider(config.Storage.Local.Path)
default:
logger.Fatalf("Unrecognized storage backend")
}
var (
userProvider authentication.UserProvider
err error
)
switch {
case config.AuthenticationBackend.File != nil:
userProvider = authentication.NewFileUserProvider(config.AuthenticationBackend.File)
case config.AuthenticationBackend.LDAP != nil:
userProvider, err = authentication.NewLDAPUserProvider(config.AuthenticationBackend, autheliaCertPool)
if err != nil {
logger.Fatalf("Failed to Check LDAP Authentication Backend: %v", err)
}
default:
logger.Fatalf("Unrecognized authentication backend")
}
var notifier notification.Notifier
switch {
case config.Notifier.SMTP != nil:
notifier = notification.NewSMTPNotifier(*config.Notifier.SMTP, autheliaCertPool)
case config.Notifier.FileSystem != nil:
notifier = notification.NewFileNotifier(*config.Notifier.FileSystem)
default:
logger.Fatalf("Unrecognized notifier")
}
if !config.Notifier.DisableStartupCheck {
_, err = notifier.StartupCheck()
if err != nil {
logger.Fatalf("Error during notifier startup check: %s", err)
}
}
clock := utils.RealClock{}
authorizer := authorization.NewAuthorizer(config)
sessionProvider := session.NewProvider(config.Session, autheliaCertPool)
regulator := regulation.NewRegulator(config.Regulation, storageProvider, clock)
oidcProvider, err := oidc.NewOpenIDConnectProvider(config.IdentityProviders.OIDC)
if err != nil {
logger.Fatalf("Error initializing OpenID Connect Provider: %+v", err)
}
providers := middlewares.Providers{
Authorizer: authorizer,
UserProvider: userProvider,
Regulator: regulator,
OpenIDConnect: oidcProvider,
StorageProvider: storageProvider,
Notifier: notifier,
SessionProvider: sessionProvider,
}
server.StartServer(*config, providers)
}
func main() {
logger := logging.Logger()
version := utils.Version()
rootCmd := &cobra.Command{
Use: "authelia",
Run: func(cmd *cobra.Command, args []string) {
startServer()
},
Version: version,
Short: fmt.Sprintf("authelia %s", version),
Long: fmt.Sprintf(fmtAutheliaLong, version),
}
rootCmd.Flags().StringVar(&configPathFlag, "config", "", "Configuration file")
buildCmd := &cobra.Command{
Use: "build",
Short: "Show the build of Authelia",
Run: func(cmd *cobra.Command, args []string) {
fmt.Printf(fmtAutheliaBuild, utils.BuildTag, utils.BuildState, utils.BuildBranch, utils.BuildCommit,
utils.BuildNumber, runtime.GOOS, runtime.GOARCH, utils.BuildDate, utils.BuildExtra)
},
}
rootCmd.AddCommand(buildCmd, commands.HashPasswordCmd,
commands.ValidateConfigCmd, commands.CertificatesCmd,
commands.RSACmd)
if err := rootCmd.Execute(); err != nil {
if err := commands.NewRootCmd().Execute(); err != nil {
logger.Fatal(err)
}
}

View File

@ -29,21 +29,16 @@ default_redirection_url: https://home.example.com/
## Server Configuration
##
server:
## The address to listen on.
host: 0.0.0.0
## The port to listen on.
port: 9091
## Authelia by default doesn't accept TLS communication on the server port. This section overrides this behaviour.
tls:
## The path to the DER base64/PEM format private key.
key: ""
# key: /config/ssl/key.pem
## The path to the DER base64/PEM format public certificate.
certificate: ""
# certificate: /config/ssl/cert.pem
## Set the single level path Authelia listens on.
## Must be alphanumeric chars and should not contain any slashes.
path: ""
## Buffers usually should be configured to be the same value.
## Explanation at https://www.authelia.com/docs/configuration/server.html
@ -52,16 +47,23 @@ server:
read_buffer_size: 4096
write_buffer_size: 4096
## Set the single level path Authelia listens on.
## Must be alphanumeric chars and should not contain any slashes.
path: ""
## Enables the pprof endpoint.
enable_pprof: false
## Enables the expvars endpoint.
enable_expvars: false
## Authelia by default doesn't accept TLS communication on the server port. This section overrides this behaviour.
tls:
## The path to the DER base64/PEM format private key.
key: ""
## The path to the DER base64/PEM format public certificate.
certificate: ""
##
## Log Configuration
##
log:
## Level of verbosity for logs: info, debug, trace.
level: debug

View File

@ -6,17 +6,61 @@ has_children: true
---
# Configuration
Authelia has several methods of configuration available to it. The order of precedence is as follows:
Authelia uses a YAML file as configuration file. A template with all possible options can be
found [here](https://github.com/authelia/authelia/blob/master/config.template.yml), at the root of the repository.
1. [Secrets](./secrets.md)
2. [Environment Variables](#environment)
3. [Files](#files) (in order of them being specified)
This order of precedence puts higher weight on things higher in the list. This means anything specified in the
[files](#files) is overridden by [environment variables](#environment) if specified, and anything specified by
[environment variables](#environment) is overridden by [secrets](./secrets.md) if specified.
## Files
When running **Authelia**, you can specify your configuration by passing the file path as shown below.
```console
$ authelia --config config.custom.yml
```
## Documentation
You can have multiple configuration files which will be merged in the order specified. If duplicate keys are specified
the last one to be specified is the one that takes precedence. Example:
```console
$ authelia --config config.yml --config config-acl.yml --config config-other.yml
$ authelia --config config.yml,config-acl.yml,config-other.yml
```
Authelia's configuration files use the YAML format. A template with all possible options can be found at the root of the
repository [here](https://github.com/authelia/authelia/blob/master/config.template.yml).
## Environment
You may also provide the configuration by using environment variables. Environment variables are applied after the
configuration file meaning anything specified as part of the environment overrides the configuration files. The
environment variables must be prefixed with `AUTHELIA_`.
_**Please Note:** It is not possible to configure_ the _access control rules section or OpenID Connect identity provider
section using environment variables at this time._
_**Please Note:** There are compatability issues with Kubernetes and this particular configuration option. You must ensure you
have the `enableServiceLinks: false` setting in your pod spec._
Underscores replace indented configuration sections or subkeys. For example the following environment variables replace
the configuration snippet that follows it:
```
AUTHELIA_LOG_LEVEL=info
AUTHELIA_SERVER_READ_BUFFER_SIZE=4096
```
```yaml
log:
level: info
server:
read_buffer_size: 4096
```
# Documentation
We document the configuration in two ways:
@ -33,7 +77,7 @@ We document the configuration in two ways:
- The `required` label changes color. When required it will be red, when not required it will be green, when the
required state depends on another configuration value it is yellow.
## Validation
# Validation
Authelia validates the configuration when it starts. This process checks multiple factors including configuration keys
that don't exist, configuration keys that have changed, the values of the keys are valid, and that a configuration
@ -50,7 +94,7 @@ integrations, it only checks that your configuration syntax is valid.
$ authelia validate-config configuration.yml
```
## Duration Notation Format
# Duration Notation Format
We have implemented a string based notation for configuration options that take a duration. This section describes its
usage. You can use this implementation in: session for expiration, inactivity, and remember_me_duration; and regulation
@ -74,12 +118,12 @@ Examples:
* 1 day: 1d
* 10 hours: 10h
## TLS Configuration
# 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
## Server Name
<div markdown="1">
type: string
{: .label .label-config .label-purple }
@ -92,7 +136,7 @@ required: no
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
## Skip Verify
<div markdown="1">
type: boolean
{: .label .label-config .label-purple }
@ -103,9 +147,9 @@ required: no
</div>
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).
instead you should tweak the `server_name` option, and the global option [certificates directory](./miscellaneous.md#certificates_directory).
### Minimum Version
## Minimum Version
<div markdown="1">
type: string
{: .label .label-config .label-purple }

View File

@ -7,32 +7,30 @@ nav_order: 10
# Secrets
Configuration of Authelia requires some secrets and passwords.
Even if they can be set in the configuration file, the recommended
way to set secrets is to use environment variables as described
below.
Configuration of Authelia requires some secrets and passwords. Even if they can be set in the configuration file or
standard environment variables, the recommended way to set secrets is to use environment variables as described below.
## Environment variables
A secret can be configured using an environment variable with the
prefix AUTHELIA_ followed by the path of the option capitalized
and with dots replaced by underscores followed by the suffix _FILE.
A secret value can be loaded by Authelia when the configuration key ends with one of the following words: `key`,
`secret`, `password`, or `token`.
The contents of the environment variable must be a path to a file
containing the secret data. This file must be readable by the
user the Authelia daemon is running as.
If you take the expected environment variable for the configuration option with the `_FILE` suffix at the end. The value
of these environment variables must be the path of a file that is readable by the Authelia process, if they are not,
Authelia will fail to load. Authelia will automatically remove the newlines from the end of the files contents.
For instance the LDAP password can be defined in the configuration
at the path **authentication_backend.ldap.password**, so this password
could alternatively be set using the environment variable called
**AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PASSWORD_FILE**.
**AUTHELIA__AUTHENTICATION_BACKEND_LDAP_PASSWORD_FILE**.
Here is the list of the environment variables which are considered
secrets and can be defined. Any other option defined using an
environment variable will not be replaced.
Here is the list of the environment variables which are considered secrets and can be defined. Please note that only
secrets can be loaded into the configuration if they end with one of the suffixes above, you can set the value of any
other configuration using the environment but instead of loading a file the value of the environment variable is used.
|Configuration Key |Environment Variable |
|:-----------------------------------------------:|:------------------------------------------------------:|
|tls_key |AUTHELIA_TLS_KEY_FILE |
|jwt_secret |AUTHELIA_JWT_SECRET_FILE |
|duo_api.secret_key |AUTHELIA_DUO_API_SECRET_KEY_FILE |
|session.secret |AUTHELIA_SESSION_SECRET_FILE |
@ -47,21 +45,21 @@ environment variable will not be replaced.
## Secrets in configuration file
If for some reason you prefer keeping the secrets in the configuration
file, be sure to apply the right permissions to the file in order to
prevent secret leaks if an another application gets compromised on your
server. The UNIX permissions should probably be something like 600.
If for some reason you decide on keeping the secrets in the configuration file, it is strongly recommended that you
ensure the permissions of the configuration file are appropriately set so that other users or processes cannot access
this file. Generally the UNIX permissions that are appropriate are 0600.
## Secrets exposed in an environment variable
**DEPRECATION NOTICE:** This backwards compatibility feature **has been removed** in 4.18.0+.
In all versions 4.30.0+ you can technically set secrets using the environment variables without the `_FILE` suffix by
setting the value to the value you wish to set in configuration, however we strongly urge people not to use this option
and instead use the file-based secrets above.
Prior to implementing file secrets you were able to define the
values of secrets in the environment variables themselves
in plain text instead of referencing a file. **This is no longer available
as an option**, please see the table above for the file based replacements. See
[this article](https://diogomonica.com/2017/03/27/why-you-shouldnt-use-env-variables-for-secret-data/)
for reasons why this was removed.
Prior to implementing file secrets the only way you were able to define secret values was either via configuration or
via environment variables in plain text.
See [this article](https://diogomonica.com/2017/03/27/why-you-shouldnt-use-env-variables-for-secret-data/) for reasons
why setting them via the file counterparts is highly encouraged.
## Docker

View File

@ -15,9 +15,9 @@ The server section configures and tunes the http server module Authelia uses.
server:
host: 0.0.0.0
port: 9091
path: ""
read_buffer_size: 4096
write_buffer_size: 4096
path: ""
enable_pprof: false
enable_expvars: false
tls:
@ -58,30 +58,6 @@ required: no
Defines the port to listen on. See also [host](#host).
### read_buffer_size
<div markdown="1">
type: integer
{: .label .label-config .label-purple }
default: 4096
{: .label .label-config .label-blue }
required: no
{: .label .label-config .label-green }
</div>
Configures the maximum request size. The default of 4096 is generally sufficient for most use cases.
### write_buffer_size
<div markdown="1">
type: integer
{: .label .label-config .label-purple }
default: 4096
{: .label .label-config .label-blue }
required: no
{: .label .label-config .label-green }
</div>
Configures the maximum response size. The default of 4096 is generally sufficient for most use cases.
### path
<div markdown="1">
type: string
@ -110,6 +86,30 @@ server:
path: authelia
```
### read_buffer_size
<div markdown="1">
type: integer
{: .label .label-config .label-purple }
default: 4096
{: .label .label-config .label-blue }
required: no
{: .label .label-config .label-green }
</div>
Configures the maximum request size. The default of 4096 is generally sufficient for most use cases.
### write_buffer_size
<div markdown="1">
type: integer
{: .label .label-config .label-purple }
default: 4096
{: .label .label-config .label-blue }
required: no
{: .label .label-config .label-green }
</div>
Configures the maximum response size. The default of 4096 is generally sufficient for most use cases.
### enable_pprof
<div markdown="1">
type: boolean

3
go.mod
View File

@ -18,6 +18,8 @@ require (
github.com/golang/mock v1.6.0
github.com/google/uuid v1.3.0
github.com/jackc/pgx/v4 v4.13.0
github.com/knadh/koanf v1.1.1
github.com/mitchellh/mapstructure v1.4.1
github.com/ory/fosite v0.40.2
github.com/ory/herodot v0.9.7
github.com/otiai10/copy v1.6.0
@ -26,7 +28,6 @@ require (
github.com/simia-tech/crypt v0.5.0
github.com/sirupsen/logrus v1.8.1
github.com/spf13/cobra v1.2.1
github.com/spf13/viper v1.8.1
github.com/stretchr/testify v1.7.0
github.com/tebeka/selenium v0.9.9
github.com/tstranex/u2f v1.0.0

6
go.sum
View File

@ -817,6 +817,7 @@ github.com/jmoiron/sqlx v1.2.0/go.mod h1:1FEQNm3xlJgrMD+FBdI9+xvCksHtbpVBBw5dYhB
github.com/jmoiron/sqlx v1.3.1/go.mod h1:2BljVx/86SuTyjE+aPYlHCTNvZrnJXghYGpNiXLBMCQ=
github.com/joeshaw/multierror v0.0.0-20140124173710-69b34d4ec901/go.mod h1:Z86h9688Y0wesXCyonoVr47MasHilkuLMqGhRZ4Hpak=
github.com/joho/godotenv v1.2.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
@ -854,6 +855,8 @@ github.com/klauspost/compress v1.12.2 h1:2KCfW3I9M7nSc5wOqXAlW2v2U6v+w6cbjvbfp+O
github.com/klauspost/compress v1.12.2/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
github.com/knadh/koanf v0.14.1-0.20201201075439-e0853799f9ec/go.mod h1:H5mEFsTeWizwFXHKtsITL5ipsLTuAMQoGuQpp+1JL9U=
github.com/knadh/koanf v1.0.0/go.mod h1:vrMMuhIH0k7EoxiMbVfFlRvJYmxcT2Eha3DH8Tx5+X4=
github.com/knadh/koanf v1.1.1 h1:doO5UBvSXcmngdr/u54HKe+Uz4ZZw0/YHVzSsnE3vD4=
github.com/knadh/koanf v1.1.1/go.mod h1:xpPTwMhsA/aaQLAilyCCqfpEiY1gpa160AiCuWHJUjY=
github.com/konsorten/go-windows-terminal-sequences v0.0.0-20180402223658-b729f2633dfe/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
@ -947,6 +950,7 @@ github.com/microcosm-cc/bluemonday v1.0.2/go.mod h1:iVP4YcDBq+n/5fb23BhYFvIMq/le
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
@ -963,6 +967,7 @@ github.com/mitchellh/mapstructure v1.3.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RR
github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxdASFVQag=
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/moby/term v0.0.0-20200915141129-7f0af18e79f2/go.mod h1:TjQg8pa4iejrUrjiz0MCtMV38jdMNW4doKSiBrEvCQQ=
github.com/moby/term v0.0.0-20201216013528-df9cb8a40635/go.mod h1:FBS0z0QWA44HXygs7VXDUOGoN/1TV3RuWkLO04am3wc=
@ -1257,7 +1262,6 @@ github.com/spf13/viper v1.3.1/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DM
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE=
github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
github.com/spf13/viper v1.8.1 h1:Kq1fyeebqsBfbjZj4EL7gj2IO0mMaiyjYUWcUsl2O44=
github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns=
github.com/sqs/goreturns v0.0.0-20181028201513-538ac6014518 h1:iD+PFTQwKEmbwSdwfvP5ld2WEI/g7qbdhmHJ2ASfYGs=
github.com/sqs/goreturns v0.0.0-20181028201513-538ac6014518/go.mod h1:CKI4AZ4XmGV240rTHfO0hfE83S6/a3/Q1siZJ/vXf7A=

View File

@ -0,0 +1,29 @@
package commands
import (
"fmt"
"runtime"
"github.com/spf13/cobra"
"github.com/authelia/authelia/internal/utils"
)
func newBuildInfoCmd() (cmd *cobra.Command) {
cmd = &cobra.Command{
Use: "build-info",
Short: "Show the build information of Authelia",
Long: buildLong,
RunE: cmdBuildInfoRunE,
Args: cobra.NoArgs,
}
return cmd
}
func cmdBuildInfoRunE(_ *cobra.Command, _ []string) (err error) {
_, err = fmt.Printf(fmtAutheliaBuild, utils.BuildTag, utils.BuildState, utils.BuildBranch, utils.BuildCommit,
utils.BuildNumber, runtime.GOOS, runtime.GOARCH, utils.BuildDate, utils.BuildExtra)
return err
}

View File

@ -9,67 +9,192 @@ import (
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"fmt"
"log"
"math/big"
"net"
"os"
"path"
"strings"
"path/filepath"
"time"
"github.com/spf13/cobra"
)
var (
host string
validFrom string
validFor time.Duration
isCA bool
rsaBits int
ecdsaCurve string
ed25519Key bool
certificateTargetDirectory string
)
// NewCertificatesCmd returns a new Certificates Cmd.
func NewCertificatesCmd() (cmd *cobra.Command) {
cmd = &cobra.Command{
Use: "certificates",
Short: "Commands related to certificate generation",
Args: cobra.NoArgs,
}
func init() {
CertificatesGenerateCmd.PersistentFlags().StringVar(&host, "host", "", "Comma-separated hostnames and IPs to generate a certificate for")
err := CertificatesGenerateCmd.MarkPersistentFlagRequired("host")
cmd.PersistentFlags().StringSlice("host", []string{}, "Comma-separated hostnames and IPs to generate a certificate for")
err := cmd.MarkPersistentFlagRequired("host")
if err != nil {
log.Fatal(err)
}
CertificatesGenerateCmd.PersistentFlags().StringVar(&validFrom, "start-date", "", "Creation date formatted as Jan 1 15:04:05 2011")
CertificatesGenerateCmd.PersistentFlags().DurationVar(&validFor, "duration", 365*24*time.Hour, "Duration that certificate is valid for")
CertificatesGenerateCmd.PersistentFlags().BoolVar(&isCA, "ca", false, "Whether this cert should be its own Certificate Authority")
CertificatesGenerateCmd.PersistentFlags().IntVar(&rsaBits, "rsa-bits", 2048, "Size of RSA key to generate. Ignored if --ecdsa-curve is set")
CertificatesGenerateCmd.PersistentFlags().StringVar(&ecdsaCurve, "ecdsa-curve", "", "ECDSA curve to use to generate a key. Valid values are P224, P256 (recommended), P384, P521")
CertificatesGenerateCmd.PersistentFlags().BoolVar(&ed25519Key, "ed25519", false, "Generate an Ed25519 key")
CertificatesGenerateCmd.PersistentFlags().StringVar(&certificateTargetDirectory, "dir", "", "Target directory where the certificate and keys will be stored")
cmd.AddCommand(newCertificatesGenerateCmd())
CertificatesCmd.AddCommand(CertificatesGenerateCmd)
return cmd
}
func publicKey(priv interface{}) interface{} {
switch k := priv.(type) {
case *rsa.PrivateKey:
return &k.PublicKey
case *ecdsa.PrivateKey:
return &k.PublicKey
case ed25519.PrivateKey:
return k.Public().(ed25519.PublicKey)
default:
return nil
}
func newCertificatesGenerateCmd() (cmd *cobra.Command) {
cmd = &cobra.Command{
Use: "generate",
Short: "Generate a self-signed certificate",
Args: cobra.NoArgs,
Run: cmdCertificatesGenerateRun,
}
//nolint:gocyclo // TODO: Consider refactoring/simplifying, time permitting.
func generateSelfSignedCertificate(cmd *cobra.Command, args []string) {
cmd.Flags().String("start-date", "", "Creation date formatted as Jan 1 15:04:05 2011")
cmd.Flags().Duration("duration", 365*24*time.Hour, "Duration that certificate is valid for")
cmd.Flags().Bool("ca", false, "Whether this cert should be its own Certificate Authority")
cmd.Flags().Int("rsa-bits", 2048, "Size of RSA key to generate. Ignored if --ecdsa-curve is set")
cmd.Flags().String("ecdsa-curve", "", "ECDSA curve to use to generate a key. Valid values are P224, P256 (recommended), P384, P521")
cmd.Flags().Bool("ed25519", false, "Generate an Ed25519 key")
cmd.Flags().String("dir", "", "Target directory where the certificate and keys will be stored")
return cmd
}
func cmdCertificatesGenerateRun(cmd *cobra.Command, _ []string) {
// implementation retrieved from https://golang.org/src/crypto/tls/generate_cert.go
var priv interface{}
ecdsaCurve, err := cmd.Flags().GetString("ecdsa-curve")
if err != nil {
fmt.Printf("Failed to parse ecdsa-curve flag: %v\n", err)
os.Exit(1)
}
var err error
ed25519Key, err := cmd.Flags().GetBool("ed25519")
if err != nil {
fmt.Printf("Failed to parse ed25519 flag: %v\n", err)
os.Exit(1)
}
rsaBits, err := cmd.Flags().GetInt("rsa-bits")
if err != nil {
fmt.Printf("Failed to parse rsa-bits flag: %v\n", err)
os.Exit(1)
}
hosts, err := cmd.Flags().GetStringSlice("host")
if err != nil {
fmt.Printf("Failed to parse host flag: %v\n", err)
os.Exit(1)
}
validFrom, err := cmd.Flags().GetString("start-date")
if err != nil {
fmt.Printf("Failed to parse start-date flag: %v\n", err)
os.Exit(1)
}
validFor, err := cmd.Flags().GetDuration("duration")
if err != nil {
fmt.Printf("Failed to parse duration flag: %v\n", err)
os.Exit(1)
}
isCA, err := cmd.Flags().GetBool("ca")
if err != nil {
fmt.Printf("Failed to parse ca flag: %v\n", err)
os.Exit(1)
}
certificateTargetDirectory, err := cmd.Flags().GetString("dir")
if err != nil {
fmt.Printf("Failed to parse dir flag: %v\n", err)
os.Exit(1)
}
cmdCertificatesGenerateRunExtended(hosts, ecdsaCurve, validFrom, certificateTargetDirectory, ed25519Key, isCA, rsaBits, validFor)
}
func cmdCertificatesGenerateRunExtended(hosts []string, ecdsaCurve, validFrom, certificateTargetDirectory string, ed25519Key, isCA bool, rsaBits int, validFor time.Duration) {
priv, err := getPrivateKey(ecdsaCurve, ed25519Key, rsaBits)
if err != nil {
fmt.Printf("Failed to generate private key: %v\n", err)
os.Exit(1)
}
var notBefore time.Time
switch len(validFrom) {
case 0:
notBefore = time.Now()
default:
notBefore, err = time.Parse("Jan 2 15:04:05 2006", validFrom)
if err != nil {
fmt.Printf("Failed to parse start date: %v\n", err)
os.Exit(1)
}
}
notAfter := notBefore.Add(validFor)
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
if err != nil {
fmt.Printf("Failed to generate serial number: %v\n", err)
os.Exit(1)
}
template := x509.Certificate{
SerialNumber: serialNumber,
Subject: pkix.Name{
Organization: []string{"Acme Co"},
},
NotBefore: notBefore,
NotAfter: notAfter,
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
}
for _, h := range hosts {
if ip := net.ParseIP(h); ip != nil {
template.IPAddresses = append(template.IPAddresses, ip)
} else {
template.DNSNames = append(template.DNSNames, h)
}
}
if isCA {
template.IsCA = true
template.KeyUsage |= x509.KeyUsageCertSign
}
certPath := filepath.Join(certificateTargetDirectory, "cert.pem")
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, publicKey(priv), priv)
if err != nil {
fmt.Printf("Failed to create certificate: %v\n", err)
os.Exit(1)
}
writePEM(derBytes, "CERTIFICATE", certPath)
fmt.Printf("Certificate Public Key written to %s\n", certPath)
keyPath := filepath.Join(certificateTargetDirectory, "key.pem")
privBytes, err := x509.MarshalPKCS8PrivateKey(priv)
if err != nil {
fmt.Printf("Failed to marshal private key: %v\n", err)
os.Exit(1)
}
writePEM(privBytes, "PRIVATE KEY", keyPath)
fmt.Printf("Certificate Private Key written to %s\n", keyPath)
}
func getPrivateKey(ecdsaCurve string, ed25519Key bool, rsaBits int) (priv interface{}, err error) {
switch ecdsaCurve {
case "":
if ed25519Key {
@ -86,114 +211,39 @@ func generateSelfSignedCertificate(cmd *cobra.Command, args []string) {
case "P521":
priv, err = ecdsa.GenerateKey(elliptic.P521(), rand.Reader)
default:
log.Fatalf("Unrecognized elliptic curve: %q", ecdsaCurve)
err = fmt.Errorf("unrecognized elliptic curve: %q", ecdsaCurve)
}
return priv, err
}
func writePEM(bytes []byte, blockType, path string) {
keyOut, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
log.Fatalf("Failed to generate private key: %v", err)
fmt.Printf("Failed to open %s for writing: %v\n", path, err)
os.Exit(1)
}
var notBefore time.Time
if len(validFrom) == 0 {
notBefore = time.Now()
} else {
notBefore, err = time.Parse("Jan 2 15:04:05 2006", validFrom)
if err != nil {
log.Fatalf("Failed to parse creation date: %v", err)
}
}
notAfter := notBefore.Add(validFor)
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
if err != nil {
log.Fatalf("Failed to generate serial number: %v", err)
}
template := x509.Certificate{
SerialNumber: serialNumber,
Subject: pkix.Name{
Organization: []string{"Acme Co"},
},
NotBefore: notBefore,
NotAfter: notAfter,
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
}
hosts := strings.Split(host, ",")
for _, h := range hosts {
if ip := net.ParseIP(h); ip != nil {
template.IPAddresses = append(template.IPAddresses, ip)
} else {
template.DNSNames = append(template.DNSNames, h)
}
}
if isCA {
template.IsCA = true
template.KeyUsage |= x509.KeyUsageCertSign
}
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, publicKey(priv), priv)
if err != nil {
log.Fatalf("Failed to create certificate: %v", err)
}
certPath := path.Join(certificateTargetDirectory, "cert.pem")
certOut, err := os.Create(certPath)
if err != nil {
log.Fatalf("Failed to open %s for writing: %v", certPath, err)
}
if err := pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes}); err != nil {
log.Fatalf("Failed to write data to cert.pem: %v", err)
}
if err := certOut.Close(); err != nil {
log.Fatalf("Error closing %s: %v", certPath, err)
}
log.Printf("wrote %s\n", certPath)
keyPath := path.Join(certificateTargetDirectory, "key.pem")
keyOut, err := os.OpenFile(keyPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
log.Fatalf("Failed to open %s for writing: %v", keyPath, err)
return
}
privBytes, err := x509.MarshalPKCS8PrivateKey(priv)
if err != nil {
log.Fatalf("Unable to marshal private key: %v", err)
}
if err := pem.Encode(keyOut, &pem.Block{Type: "PRIVATE KEY", Bytes: privBytes}); err != nil {
log.Fatalf("Failed to write data to %s: %v", keyPath, err)
if err := pem.Encode(keyOut, &pem.Block{Type: blockType, Bytes: bytes}); err != nil {
fmt.Printf("Failed to write data to %s: %v\n", path, err)
os.Exit(1)
}
if err := keyOut.Close(); err != nil {
log.Fatalf("Error closing %s: %v", keyPath, err)
fmt.Printf("Error closing %s: %v\n", path, err)
os.Exit(1)
}
}
log.Printf("wrote %s\n", keyPath)
func publicKey(priv interface{}) interface{} {
switch k := priv.(type) {
case *rsa.PrivateKey:
return &k.PublicKey
case *ecdsa.PrivateKey:
return &k.PublicKey
case ed25519.PrivateKey:
return k.Public().(ed25519.PublicKey)
default:
return nil
}
// CertificatesCmd certificate helper command.
var CertificatesCmd = &cobra.Command{
Use: "certificates",
Short: "Commands related to certificate generation",
}
// CertificatesGenerateCmd certificate generation command.
var CertificatesGenerateCmd = &cobra.Command{
Use: "generate",
Short: "Generate a self-signed certificate",
Run: generateSelfSignedCertificate,
}

View File

@ -0,0 +1,45 @@
package commands
import (
"fmt"
"os"
"github.com/spf13/cobra"
)
func newCompletionCmd() (cmd *cobra.Command) {
cmd = &cobra.Command{
Use: "completion [bash|zsh|fish|powershell]",
Short: "Generate completion script",
Long: completionLong,
Args: cobra.ExactValidArgs(1),
ValidArgs: []string{"bash", "zsh", "fish", "powershell"},
DisableFlagsInUseLine: true,
Run: cmdCompletionRun,
}
return cmd
}
func cmdCompletionRun(cmd *cobra.Command, args []string) {
var err error
switch args[0] {
case "bash":
err = cmd.Root().GenBashCompletion(os.Stdout)
case "zsh":
err = cmd.Root().GenZshCompletion(os.Stdout)
case "fish":
err = cmd.Root().GenFishCompletion(os.Stdout, true)
case "powershell":
err = cmd.Root().GenPowerShellCompletionWithDesc(os.Stdout)
default:
fmt.Printf("Invalid shell provided for completion command: %s\n", args[0])
os.Exit(1)
}
if err != nil {
fmt.Printf("Error generating completion: %v\n", err)
os.Exit(1)
}
}

View File

@ -0,0 +1,75 @@
package commands
import (
"os"
"github.com/spf13/cobra"
"github.com/authelia/authelia/internal/configuration"
"github.com/authelia/authelia/internal/configuration/schema"
"github.com/authelia/authelia/internal/configuration/validator"
"github.com/authelia/authelia/internal/logging"
)
// cmdWithConfigFlags is used for commands which require access to the configuration to add the flag to the command.
func cmdWithConfigFlags(cmd *cobra.Command) {
cmd.Flags().StringSliceP("config", "c", []string{}, "Configuration files")
}
var config *schema.Configuration
func newCmdWithConfigPreRun(ensureConfigExists, validateKeys, validateConfiguration bool) func(cmd *cobra.Command, args []string) {
return func(cmd *cobra.Command, _ []string) {
logger := logging.Logger()
configs, err := cmd.Root().Flags().GetStringSlice("config")
if err != nil {
logger.Fatalf("Error reading flags: %v", err)
}
if ensureConfigExists && len(configs) == 1 {
created, err := configuration.EnsureConfigurationExists(configs[0])
if err != nil {
logger.Fatal(err)
}
if created {
logger.Warnf("Configuration did not exist so a default one has been generated at %s, you will need to configure this", configs[0])
os.Exit(0)
}
}
var keys []string
val := schema.NewStructValidator()
keys, config, err = configuration.Load(val, configuration.NewDefaultSources(configs, configuration.DefaultEnvPrefix, configuration.DefaultEnvDelimiter)...)
if err != nil {
logger.Fatalf("Error occurred loading configuration: %v", err)
}
if validateKeys {
validator.ValidateKeys(keys, configuration.DefaultEnvPrefix, val)
}
if validateConfiguration {
validator.ValidateConfiguration(config, val)
}
warnings := val.Warnings()
if len(warnings) != 0 {
for _, warning := range warnings {
logger.Warnf("Configuration: %+v", warning)
}
}
errs := val.Errors()
if len(errs) != 0 {
for _, err := range errs {
logger.Errorf("Configuration: %+v", err)
}
logger.Fatalf("Can't continue due to the errors loading the configuration")
}
}
}

View File

@ -0,0 +1,77 @@
package commands
const cmdAutheliaExample = `authelia --config /etc/authelia/config.yml --config /etc/authelia/access-control.yml
authelia --config /etc/authelia/config.yml,/etc/authelia/access-control.yml
authelia --config /etc/authelia/config/
`
const fmtAutheliaLong = `authelia %s
An open-source authentication and authorization server providing
two-factor authentication and single sign-on (SSO) for your
applications via a web portal.
Documentation is available at: https://www.authelia.com/docs
`
const fmtAutheliaBuild = `Last Tag: %s
State: %s
Branch: %s
Commit: %s
Build Number: %s
Build OS: %s
Build Arch: %s
Build Date: %s
Extra: %s
`
const buildLong = `Show the build information of Authelia
This outputs detailed version information about the specific version
of the Authelia binary. This information is embedded into Authelia
by the continuous integration.
This could be vital in debugging if you're not using a particular
tagged build of Authelia. It's suggested to provide it along with
your issue.
`
const completionLong = `To load completions:
Bash:
$ source <(authelia completion bash)
# To load completions for each session, execute once:
# Linux:
$ authelia completion bash > /etc/bash_completion.d/authelia
# macOS:
$ authelia completion bash > /usr/local/etc/bash_completion.d/authelia
Zsh:
# If shell completion is not already enabled in your environment,
# you will need to enable it. You can execute the following once:
$ echo "autoload -U compinit; compinit" >> ~/.zshrc
# To load completions for each session, execute once:
$ authelia completion zsh > "${fpath[1]}/_authelia"
# You will need to start a new shell for this setup to take effect.
fish:
$ authelia completion fish | source
# To load completions for each session, execute once:
$ authelia completion fish > ~/.config/fish/completions/authelia.fish
PowerShell:
PS> authelia completion powershell | Out-String | Invoke-Expression
# To load completions for every new session, run:
PS> authelia completion powershell > authelia.ps1
# and source this file from your PowerShell profile.
`

View File

@ -2,60 +2,67 @@ package commands
import (
"fmt"
"log"
"github.com/simia-tech/crypt"
"github.com/spf13/cobra"
"github.com/authelia/authelia/internal/authentication"
"github.com/authelia/authelia/internal/configuration/schema"
"github.com/authelia/authelia/internal/logging"
)
func init() {
HashPasswordCmd.Flags().BoolP("sha512", "z", false, fmt.Sprintf("use sha512 as the algorithm (changes iterations to %d, change with -i)", schema.DefaultPasswordSHA512Configuration.Iterations))
HashPasswordCmd.Flags().IntP("iterations", "i", schema.DefaultPasswordConfiguration.Iterations, "set the number of hashing iterations")
HashPasswordCmd.Flags().StringP("salt", "s", "", "set the salt string")
HashPasswordCmd.Flags().IntP("memory", "m", schema.DefaultPasswordConfiguration.Memory, "[argon2id] set the amount of memory param (in MB)")
HashPasswordCmd.Flags().IntP("parallelism", "p", schema.DefaultPasswordConfiguration.Parallelism, "[argon2id] set the parallelism param")
HashPasswordCmd.Flags().IntP("key-length", "k", schema.DefaultPasswordConfiguration.KeyLength, "[argon2id] set the key length param")
HashPasswordCmd.Flags().IntP("salt-length", "l", schema.DefaultPasswordConfiguration.SaltLength, "set the auto-generated salt length")
}
// HashPasswordCmd password hashing command.
var HashPasswordCmd = &cobra.Command{
// NewHashPasswordCmd returns a new Hash Password Cmd.
func NewHashPasswordCmd() (cmd *cobra.Command) {
cmd = &cobra.Command{
Use: "hash-password [password]",
Short: "Hash a password to be used in file-based users database. Default algorithm is argon2id.",
Run: func(cobraCmd *cobra.Command, args []string) {
sha512, _ := cobraCmd.Flags().GetBool("sha512")
iterations, _ := cobraCmd.Flags().GetInt("iterations")
salt, _ := cobraCmd.Flags().GetString("salt")
keyLength, _ := cobraCmd.Flags().GetInt("key-length")
saltLength, _ := cobraCmd.Flags().GetInt("salt-length")
memory, _ := cobraCmd.Flags().GetInt("memory")
parallelism, _ := cobraCmd.Flags().GetInt("parallelism")
Args: cobra.MinimumNArgs(1),
Run: cmdHashPasswordRun,
}
var err error
var hash string
var algorithm authentication.CryptAlgo
cmd.Flags().BoolP("sha512", "z", false, fmt.Sprintf("use sha512 as the algorithm (changes iterations to %d, change with -i)", schema.DefaultPasswordSHA512Configuration.Iterations))
cmd.Flags().IntP("iterations", "i", schema.DefaultPasswordConfiguration.Iterations, "set the number of hashing iterations")
cmd.Flags().StringP("salt", "s", "", "set the salt string")
cmd.Flags().IntP("memory", "m", schema.DefaultPasswordConfiguration.Memory, "[argon2id] set the amount of memory param (in MB)")
cmd.Flags().IntP("parallelism", "p", schema.DefaultPasswordConfiguration.Parallelism, "[argon2id] set the parallelism param")
cmd.Flags().IntP("key-length", "k", schema.DefaultPasswordConfiguration.KeyLength, "[argon2id] set the key length param")
cmd.Flags().IntP("salt-length", "l", schema.DefaultPasswordConfiguration.SaltLength, "set the auto-generated salt length")
return cmd
}
func cmdHashPasswordRun(cmd *cobra.Command, args []string) {
sha512, _ := cmd.Flags().GetBool("sha512")
iterations, _ := cmd.Flags().GetInt("iterations")
salt, _ := cmd.Flags().GetString("salt")
keyLength, _ := cmd.Flags().GetInt("key-length")
saltLength, _ := cmd.Flags().GetInt("salt-length")
memory, _ := cmd.Flags().GetInt("memory")
parallelism, _ := cmd.Flags().GetInt("parallelism")
var (
hash string
algorithm authentication.CryptAlgo
)
if sha512 {
if iterations == schema.DefaultPasswordConfiguration.Iterations {
iterations = schema.DefaultPasswordSHA512Configuration.Iterations
}
algorithm = authentication.HashingAlgorithmSHA512
} else {
algorithm = authentication.HashingAlgorithmArgon2id
}
if salt != "" {
salt = crypt.Base64Encoding.EncodeToString([]byte(salt))
}
hash, err = authentication.HashPassword(args[0], salt, algorithm, iterations, memory*1024, parallelism, keyLength, saltLength)
hash, err := authentication.HashPassword(args[0], salt, algorithm, iterations, memory*1024, parallelism, keyLength, saltLength)
if err != nil {
log.Fatalf("Error occurred during hashing: %s\n", err)
} else {
logging.Logger().Fatalf("Error occurred during hashing: %v\n", err)
}
fmt.Printf("Password hash: %s\n", hash)
}
},
Args: cobra.MinimumNArgs(1),
}

View File

@ -0,0 +1,155 @@
package commands
import (
"fmt"
"os"
"github.com/spf13/cobra"
"github.com/authelia/authelia/internal/authentication"
"github.com/authelia/authelia/internal/authorization"
"github.com/authelia/authelia/internal/configuration/schema"
"github.com/authelia/authelia/internal/logging"
"github.com/authelia/authelia/internal/middlewares"
"github.com/authelia/authelia/internal/notification"
"github.com/authelia/authelia/internal/oidc"
"github.com/authelia/authelia/internal/regulation"
"github.com/authelia/authelia/internal/server"
"github.com/authelia/authelia/internal/session"
"github.com/authelia/authelia/internal/storage"
"github.com/authelia/authelia/internal/utils"
)
// NewRootCmd returns a new Root Cmd.
func NewRootCmd() (cmd *cobra.Command) {
version := utils.Version()
cmd = &cobra.Command{
Use: "authelia",
Example: cmdAutheliaExample,
Short: fmt.Sprintf("authelia %s", version),
Long: fmt.Sprintf(fmtAutheliaLong, version),
Version: version,
Args: cobra.NoArgs,
PreRun: newCmdWithConfigPreRun(true, true, true),
Run: cmdRootRun,
}
cmdWithConfigFlags(cmd)
cmd.AddCommand(
newBuildInfoCmd(),
NewCertificatesCmd(),
newCompletionCmd(),
NewHashPasswordCmd(),
NewRSACmd(),
newValidateConfigCmd(),
)
return cmd
}
func cmdRootRun(_ *cobra.Command, _ []string) {
logger := logging.Logger()
logger.Infof("Authelia %s is starting", utils.Version())
if os.Getenv("ENVIRONMENT") == "dev" {
logger.Info("===> Authelia is running in development mode. <===")
}
if err := logging.InitializeLogger(config.Log, true); err != nil {
logger.Fatalf("Cannot initialize logger: %v", err)
}
providers, warnings, errors := getProviders(config)
if len(warnings) != 0 {
for _, err := range warnings {
logger.Warn(err)
}
}
if len(errors) != 0 {
for _, err := range errors {
logger.Error(err)
}
logger.Fatalf("Errors occurred provisioning providers.")
}
server.Start(*config, providers)
}
func getProviders(config *schema.Configuration) (providers middlewares.Providers, warnings []error, errors []error) {
autheliaCertPool, warnings, errors := utils.NewX509CertPool(config.CertificatesDirectory)
if len(warnings) != 0 || len(errors) != 0 {
return providers, warnings, errors
}
var storageProvider storage.Provider
switch {
case config.Storage.PostgreSQL != nil:
storageProvider = storage.NewPostgreSQLProvider(*config.Storage.PostgreSQL)
case config.Storage.MySQL != nil:
storageProvider = storage.NewMySQLProvider(*config.Storage.MySQL)
case config.Storage.Local != nil:
storageProvider = storage.NewSQLiteProvider(config.Storage.Local.Path)
default:
errors = append(errors, fmt.Errorf("unrecognized storage provider"))
}
var (
userProvider authentication.UserProvider
err error
)
switch {
case config.AuthenticationBackend.File != nil:
userProvider = authentication.NewFileUserProvider(config.AuthenticationBackend.File)
case config.AuthenticationBackend.LDAP != nil:
userProvider, err = authentication.NewLDAPUserProvider(config.AuthenticationBackend, autheliaCertPool)
if err != nil {
errors = append(errors, fmt.Errorf("failed to check LDAP authentication backend: %w", err))
}
default:
errors = append(errors, fmt.Errorf("unrecognized user provider"))
}
var notifier notification.Notifier
switch {
case config.Notifier.SMTP != nil:
notifier = notification.NewSMTPNotifier(*config.Notifier.SMTP, autheliaCertPool)
case config.Notifier.FileSystem != nil:
notifier = notification.NewFileNotifier(*config.Notifier.FileSystem)
default:
errors = append(errors, fmt.Errorf("unrecognized notifier provider"))
}
if notifier != nil {
if _, err := notifier.StartupCheck(); err != nil {
errors = append(errors, fmt.Errorf("failed to check notification provider: %w", err))
}
}
clock := utils.RealClock{}
authorizer := authorization.NewAuthorizer(config)
sessionProvider := session.NewProvider(config.Session, autheliaCertPool)
regulator := regulation.NewRegulator(config.Regulation, storageProvider, clock)
oidcProvider, err := oidc.NewOpenIDConnectProvider(config.IdentityProviders.OIDC)
if err != nil {
errors = append(errors, err)
}
return middlewares.Providers{
Authorizer: authorizer,
UserProvider: userProvider,
Regulator: regulator,
OpenIDConnect: oidcProvider,
StorageProvider: storageProvider,
Notifier: notifier,
SessionProvider: sessionProvider,
}, warnings, errors
}

View File

@ -1,79 +1,106 @@
package commands
import (
"log"
"fmt"
"os"
"path"
"path/filepath"
"github.com/spf13/cobra"
"github.com/authelia/authelia/internal/utils"
)
var rsaTargetDirectory string
func init() {
RSAGenerateCmd.PersistentFlags().StringVar(&rsaTargetDirectory, "dir", "", "Target directory where the keypair will be stored")
RSACmd.AddCommand(RSAGenerateCmd)
// NewRSACmd returns a new RSA Cmd.
func NewRSACmd() (cmd *cobra.Command) {
cmd = &cobra.Command{
Use: "rsa",
Short: "Commands related to rsa keypair generation",
Args: cobra.NoArgs,
}
func generateRSAKeypair(cmd *cobra.Command, args []string) {
privateKey, publicKey := utils.GenerateRsaKeyPair(2048)
cmd.AddCommand(newRSAGenerateCmd())
keyPath := path.Join(rsaTargetDirectory, "key.pem")
keyOut, err := os.OpenFile(keyPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
return cmd
}
func newRSAGenerateCmd() (cmd *cobra.Command) {
cmd = &cobra.Command{
Use: "generate",
Short: "Generate a RSA keypair",
Args: cobra.NoArgs,
Run: cmdRSAGenerateRun,
}
cmd.Flags().StringP("dir", "d", "", "Target directory where the keypair will be stored")
cmd.Flags().IntP("key-size", "b", 2048, "Sets the key size in bits")
return cmd
}
func cmdRSAGenerateRun(cmd *cobra.Command, _ []string) {
bits, err := cmd.Flags().GetInt("key-size")
if err != nil {
log.Fatalf("Failed to open %s for writing: %v", keyPath, err)
fmt.Printf("Failed to parse key-size flag: %v\n", err)
return
}
privateKey, publicKey := utils.GenerateRsaKeyPair(bits)
rsaTargetDirectory, err := cmd.Flags().GetString("dir")
if err != nil {
fmt.Printf("Failed to parse dir flag: %v\n", err)
return
}
keyPath := filepath.Join(rsaTargetDirectory, "key.pem")
keyOut, err := os.OpenFile(keyPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
fmt.Printf("Failed to open %s for writing: %v\n", keyPath, err)
return
}
defer func() {
if err := keyOut.Close(); err != nil {
fmt.Printf("Unable to close private key file: %v\n", err)
os.Exit(1)
}
}()
_, err = keyOut.WriteString(utils.ExportRsaPrivateKeyAsPemStr(privateKey))
if err != nil {
log.Fatalf("Unable to write private key: %v", err)
fmt.Printf("Failed to write private key: %v\n", err)
return
}
if err := keyOut.Close(); err != nil {
log.Fatalf("Unable to close private key file: %v", err)
return
}
fmt.Printf("RSA Private Key written to %s\n", keyPath)
keyPath = path.Join(rsaTargetDirectory, "key.pub")
keyOut, err = os.OpenFile(keyPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
certPath := filepath.Join(rsaTargetDirectory, "key.pub")
certOut, err := os.OpenFile(certPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
if err != nil {
log.Fatalf("Failed to open %s for writing: %v", keyPath, err)
fmt.Printf("Failed to open %s for writing: %v\n", keyPath, err)
return
}
defer func() {
if err := certOut.Close(); err != nil {
fmt.Printf("Failed to close public key file: %v\n", err)
os.Exit(1)
}
}()
publicPem, err := utils.ExportRsaPublicKeyAsPemStr(publicKey)
if err != nil {
log.Fatalf("Unable to marshal public key: %v", err)
fmt.Printf("Failed to marshal public key: %v\n", err)
return
}
_, err = keyOut.WriteString(publicPem)
_, err = certOut.WriteString(publicPem)
if err != nil {
log.Fatalf("Unable to write private key: %v", err)
fmt.Printf("Failed to write private key: %v\n", err)
return
}
if err := keyOut.Close(); err != nil {
log.Fatalf("Unable to close public key file: %v", err)
return
}
}
// RSACmd RSA helper command.
var RSACmd = &cobra.Command{
Use: "rsa",
Short: "Commands related to rsa keypair generation",
}
// RSAGenerateCmd certificate generation command.
var RSAGenerateCmd = &cobra.Command{
Use: "generate",
Short: "Generate a RSA keypair",
Run: generateRSAKeypair,
fmt.Printf("RSA Public Key written to %s\n", certPath)
}

View File

@ -1,40 +1,66 @@
package commands
import (
"fmt"
"log"
"os"
"github.com/spf13/cobra"
"github.com/authelia/authelia/internal/configuration"
"github.com/authelia/authelia/internal/configuration/schema"
"github.com/authelia/authelia/internal/configuration/validator"
"github.com/authelia/authelia/internal/logging"
)
// ValidateConfigCmd uses the internal configuration reader to validate the configuration.
var ValidateConfigCmd = &cobra.Command{
func newValidateConfigCmd() (cmd *cobra.Command) {
cmd = &cobra.Command{
Use: "validate-config [yaml]",
Short: "Check a configuration against the internal configuration validation mechanisms.",
Run: func(cobraCmd *cobra.Command, args []string) {
configPath := args[0]
if _, err := os.Stat(configPath); err != nil {
log.Fatalf("Error Loading Configuration: %s\n", err)
Short: "Check a configuration against the internal configuration validation mechanisms",
Args: cobra.MinimumNArgs(1),
Run: cmdValidateConfigRun,
}
// TODO: Actually use the configuration to validate some providers like Notifier
_, errs := configuration.Read(configPath)
if len(errs) != 0 {
str := "Errors"
if len(errs) == 1 {
str = "Error"
return cmd
}
errors := ""
for _, err := range errs {
errors += fmt.Sprintf("\t%s\n", err.Error())
func cmdValidateConfigRun(_ *cobra.Command, args []string) {
logger := logging.Logger()
configPath := args[0]
if _, err := os.Stat(configPath); err != nil {
logger.Fatalf("Error Loading Configuration: %v\n", err)
}
log.Fatalf("%s occurred parsing configuration:\n%s", str, errors)
} else {
val := schema.NewStructValidator()
keys, conf, err := configuration.Load(val, configuration.NewYAMLFileSource(configPath))
if err != nil {
logger.Fatalf("Error occurred loading configuration: %v", err)
}
validator.ValidateKeys(keys, configuration.DefaultEnvPrefix, val)
validator.ValidateConfiguration(conf, val)
warnings := val.Warnings()
errors := val.Errors()
if len(warnings) != 0 {
logger.Warn("Warnings occurred while loading the configuration:")
for _, warn := range warnings {
logger.Warnf(" %+v", warn)
}
}
if len(errors) != 0 {
logger.Error("Errors occurred while loading the configuration:")
for _, err := range errors {
logger.Errorf(" %+v", err)
}
logger.Fatal("Can't continue due to errors")
}
log.Println("Configuration parsed successfully without errors.")
}
},
Args: cobra.MinimumNArgs(1),
}

View File

@ -29,21 +29,16 @@ default_redirection_url: https://home.example.com/
## Server Configuration
##
server:
## The address to listen on.
host: 0.0.0.0
## The port to listen on.
port: 9091
## Authelia by default doesn't accept TLS communication on the server port. This section overrides this behaviour.
tls:
## The path to the DER base64/PEM format private key.
key: ""
# key: /config/ssl/key.pem
## The path to the DER base64/PEM format public certificate.
certificate: ""
# certificate: /config/ssl/cert.pem
## Set the single level path Authelia listens on.
## Must be alphanumeric chars and should not contain any slashes.
path: ""
## Buffers usually should be configured to be the same value.
## Explanation at https://www.authelia.com/docs/configuration/server.html
@ -52,16 +47,23 @@ server:
read_buffer_size: 4096
write_buffer_size: 4096
## Set the single level path Authelia listens on.
## Must be alphanumeric chars and should not contain any slashes.
path: ""
## Enables the pprof endpoint.
enable_pprof: false
## Enables the expvars endpoint.
enable_expvars: false
## Authelia by default doesn't accept TLS communication on the server port. This section overrides this behaviour.
tls:
## The path to the DER base64/PEM format private key.
key: ""
## The path to the DER base64/PEM format public certificate.
certificate: ""
##
## Log Configuration
##
log:
## Level of verbosity for logs: info, debug, trace.
level: debug

View File

@ -0,0 +1,19 @@
package configuration
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/authelia/authelia/internal/utils"
)
func TestShouldHaveSameChecksumForBothTemplates(t *testing.T) {
sumRoot, err := utils.HashSHA256FromPath("../../config.template.yml")
assert.NoError(t, err)
sumInternal, err := utils.HashSHA256FromPath("./config.template.yml")
assert.NoError(t, err)
assert.Equal(t, sumRoot, sumInternal, "Ensure both ./config.template.yml and ./internal/configuration/config.template.yml are exactly the same.")
}

View File

@ -1,3 +1,30 @@
package configuration
const windows = "windows"
import (
"errors"
)
// DefaultEnvPrefix is the default environment prefix.
const DefaultEnvPrefix = "AUTHELIA_"
// DefaultEnvDelimiter is the default environment delimiter.
const DefaultEnvDelimiter = "_"
const (
constSecretSuffix = "_FILE"
constDelimiter = "."
constWindows = "windows"
)
const (
errFmtSecretAlreadyDefined = "secrets: error loading secret into key '%s': it's already defined in other " +
"configuration sources"
errFmtSecretIOIssue = "secrets: error loading secret path %s into key '%s': %v"
errFmtGenerateConfiguration = "error occurred generating configuration: %+v"
)
var secretSuffixes = []string{"key", "secret", "password", "token"}
var errNoSources = errors.New("no sources provided")
var errNoValidator = errors.New("no validator provided")

View File

@ -0,0 +1,55 @@
package configuration
import (
"io/ioutil"
"strings"
"github.com/authelia/authelia/internal/utils"
)
func getEnvConfigMap(keys []string, prefix, delimiter string) (keyMap map[string]string, ignoredKeys []string) {
keyMap = make(map[string]string)
for _, key := range keys {
if strings.Contains(key, delimiter) {
originalKey := prefix + strings.ToUpper(strings.ReplaceAll(key, constDelimiter, delimiter))
keyMap[originalKey] = key
}
// Secret envs should be ignored by the env parser.
if isSecretKey(key) {
originalKey := strings.ToUpper(strings.ReplaceAll(key, constDelimiter, delimiter)) + constSecretSuffix
ignoredKeys = append(ignoredKeys, prefix+originalKey)
}
}
return keyMap, ignoredKeys
}
func getSecretConfigMap(keys []string, prefix, delimiter string) (keyMap map[string]string) {
keyMap = make(map[string]string)
for _, key := range keys {
if isSecretKey(key) {
originalKey := strings.ToUpper(strings.ReplaceAll(key, constDelimiter, delimiter)) + constSecretSuffix
keyMap[prefix+originalKey] = key
}
}
return keyMap
}
func isSecretKey(key string) (isSecretKey bool) {
return utils.IsStringInSliceSuffix(key, secretSuffixes)
}
func loadSecret(path string) (value string, err error) {
content, err := ioutil.ReadFile(path)
if err != nil {
return "", err
}
return strings.TrimRight(string(content), "\n"), err
}

View File

@ -0,0 +1,85 @@
package configuration
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestIsSecretKey(t *testing.T) {
assert.True(t, isSecretKey("my_fake_token"))
assert.False(t, isSecretKey("my_fake_tokenz"))
assert.True(t, isSecretKey("my_.fake.secret"))
assert.True(t, isSecretKey("my.password"))
assert.False(t, isSecretKey("my.passwords"))
assert.False(t, isSecretKey("my.passwords"))
}
func TestGetEnvConfigMaps(t *testing.T) {
var (
key string
ok bool
)
input := []string{
"my.non_secret.config_item",
"myother.configkey",
"mysecret.password",
"mysecret.user_password",
}
keys, ignoredKeys := getEnvConfigMap(input, DefaultEnvPrefix, DefaultEnvDelimiter)
key, ok = keys[DefaultEnvPrefix+"MY_NON_SECRET_CONFIG_ITEM"]
assert.True(t, ok)
assert.Equal(t, key, "my.non_secret.config_item")
key, ok = keys[DefaultEnvPrefix+"MYSECRET_USER_PASSWORD"]
assert.True(t, ok)
assert.Equal(t, key, "mysecret.user_password")
key, ok = keys[DefaultEnvPrefix+"MYOTHER_CONFIGKEY"]
assert.False(t, ok)
assert.Equal(t, key, "")
key, ok = keys[DefaultEnvPrefix+"MYSECRET_PASSWORD"]
assert.False(t, ok)
assert.Equal(t, key, "")
assert.Len(t, ignoredKeys, 3)
assert.Contains(t, ignoredKeys, DefaultEnvPrefix+"MYOTHER_CONFIGKEY_FILE")
assert.Contains(t, ignoredKeys, DefaultEnvPrefix+"MYSECRET_PASSWORD_FILE")
assert.Contains(t, ignoredKeys, DefaultEnvPrefix+"MYSECRET_USER_PASSWORD_FILE")
}
func TestGetSecretConfigMap(t *testing.T) {
var (
key string
ok bool
)
input := []string{
"my.non_secret.config_item",
"myother.configkey",
"mysecret.password",
"mysecret.user_password",
}
keys := getSecretConfigMap(input, DefaultEnvPrefix, DefaultEnvDelimiter)
key, ok = keys[DefaultEnvPrefix+"MY_NON_SECRET_CONFIG_ITEM_FILE"]
assert.False(t, ok)
assert.Equal(t, key, "")
key, ok = keys[DefaultEnvPrefix+"MYOTHER_CONFIGKEY_FILE"]
assert.True(t, ok)
assert.Equal(t, key, "myother.configkey")
key, ok = keys[DefaultEnvPrefix+"MYSECRET_PASSWORD_FILE"]
assert.True(t, ok)
assert.Equal(t, key, "mysecret.password")
key, ok = keys[DefaultEnvPrefix+"MYSECRET_USER_PASSWORD_FILE"]
assert.True(t, ok)
assert.Equal(t, key, "mysecret.user_password")
}

View File

@ -0,0 +1,50 @@
package configuration
import (
"fmt"
"strings"
"github.com/authelia/authelia/internal/configuration/schema"
"github.com/authelia/authelia/internal/configuration/validator"
"github.com/authelia/authelia/internal/utils"
)
// koanfEnvironmentCallback returns a koanf callback to map the environment vars to Configuration keys.
func koanfEnvironmentCallback(keyMap map[string]string, ignoredKeys []string, prefix, delimiter string) func(key, value string) (finalKey string, finalValue interface{}) {
return func(key, value string) (finalKey string, finalValue interface{}) {
if k, ok := keyMap[key]; ok {
return k, value
}
if utils.IsStringInSlice(key, ignoredKeys) {
return "", nil
}
formattedKey := strings.TrimPrefix(key, prefix)
formattedKey = strings.ReplaceAll(strings.ToLower(formattedKey), delimiter, constDelimiter)
if utils.IsStringInSlice(formattedKey, validator.ValidKeys) {
return formattedKey, value
}
return key, value
}
}
// koanfEnvironmentSecretsCallback returns a koanf callback to map the environment vars to Configuration keys.
func koanfEnvironmentSecretsCallback(keyMap map[string]string, validator *schema.StructValidator) func(key, value string) (finalKey string, finalValue interface{}) {
return func(key, value string) (finalKey string, finalValue interface{}) {
k, ok := keyMap[key]
if !ok {
return "", nil
}
v, err := loadSecret(value)
if err != nil {
validator.Push(fmt.Errorf(errFmtSecretIOIssue, value, k, err))
return k, ""
}
return k, v
}
}

View File

@ -0,0 +1,129 @@
package configuration
import (
"fmt"
"io/ioutil"
"path/filepath"
"runtime"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/authelia/authelia/internal/configuration/schema"
)
func TestKoanfEnvironmentCallback(t *testing.T) {
var (
key string
value interface{}
)
keyMap := map[string]string{
DefaultEnvPrefix + "KEY_EXAMPLE_UNDERSCORE": "key.example_underscore",
}
ignoredKeys := []string{DefaultEnvPrefix + "SOME_SECRET"}
callback := koanfEnvironmentCallback(keyMap, ignoredKeys, DefaultEnvPrefix, DefaultEnvDelimiter)
key, value = callback(DefaultEnvPrefix+"KEY_EXAMPLE_UNDERSCORE", "value")
assert.Equal(t, "key.example_underscore", key)
assert.Equal(t, "value", value)
key, value = callback(DefaultEnvPrefix+"KEY_EXAMPLE", "value")
assert.Equal(t, DefaultEnvPrefix+"KEY_EXAMPLE", key)
assert.Equal(t, "value", value)
key, value = callback(DefaultEnvPrefix+"THEME", "value")
assert.Equal(t, "theme", key)
assert.Equal(t, "value", value)
key, value = callback(DefaultEnvPrefix+"SOME_SECRET", "value")
assert.Equal(t, "", key)
assert.Nil(t, value)
}
func TestKoanfSecretCallbackWithValidSecrets(t *testing.T) {
var (
key string
value interface{}
)
keyMap := map[string]string{
"AUTHELIA__JWT_SECRET": "jwt_secret",
"AUTHELIA_JWT_SECRET": "jwt_secret",
"AUTHELIA_FAKE_KEY": "fake_key",
"AUTHELIA__FAKE_KEY": "fake_key",
"AUTHELIA_STORAGE_MYSQL_FAKE_PASSWORD": "storage.mysql.fake_password",
"AUTHELIA__STORAGE_MYSQL_FAKE_PASSWORD": "storage.mysql.fake_password",
}
dir, err := ioutil.TempDir("", "authelia-test-callbacks")
assert.NoError(t, err)
secretOne := filepath.Join(dir, "secert_one")
secretTwo := filepath.Join(dir, "secret_two")
assert.NoError(t, testCreateFile(secretOne, "value one", 0600))
assert.NoError(t, testCreateFile(secretTwo, "value two", 0600))
val := schema.NewStructValidator()
callback := koanfEnvironmentSecretsCallback(keyMap, val)
key, value = callback("AUTHELIA_FAKE_KEY", secretOne)
assert.Equal(t, "fake_key", key)
assert.Equal(t, "value one", value)
key, value = callback("AUTHELIA__STORAGE_MYSQL_FAKE_PASSWORD", secretTwo)
assert.Equal(t, "storage.mysql.fake_password", key)
assert.Equal(t, "value two", value)
}
func TestKoanfSecretCallbackShouldIgnoreUndetectedSecrets(t *testing.T) {
keyMap := map[string]string{
"AUTHELIA__JWT_SECRET": "jwt_secret",
"AUTHELIA_JWT_SECRET": "jwt_secret",
}
val := schema.NewStructValidator()
callback := koanfEnvironmentSecretsCallback(keyMap, val)
key, value := callback("AUTHELIA__SESSION_DOMAIN", "/tmp/not-a-path")
assert.Equal(t, "", key)
assert.Nil(t, value)
assert.Len(t, val.Errors(), 0)
assert.Len(t, val.Warnings(), 0)
}
func TestKoanfSecretCallbackShouldErrorOnFSError(t *testing.T) {
if runtime.GOOS == constWindows {
t.Skip("skipping test due to being on windows")
}
keyMap := map[string]string{
"AUTHELIA__THEME": "theme",
"AUTHELIA_THEME": "theme",
}
dir, err := ioutil.TempDir("", "authelia-test-callbacks")
assert.NoError(t, err)
secret := filepath.Join(dir, "inaccessible")
assert.NoError(t, testCreateFile(secret, "secret", 0000))
val := schema.NewStructValidator()
callback := koanfEnvironmentSecretsCallback(keyMap, val)
key, value := callback("AUTHELIA_THEME", secret)
assert.Equal(t, "theme", key)
assert.Equal(t, "", value)
require.Len(t, val.Errors(), 1)
assert.Len(t, val.Warnings(), 0)
assert.EqualError(t, val.Errors()[0], fmt.Sprintf(errFmtSecretIOIssue, secret, "theme", fmt.Sprintf("open %s: permission denied", secret)))
}

View File

@ -0,0 +1,75 @@
package configuration
import (
"fmt"
"github.com/knadh/koanf"
"github.com/mitchellh/mapstructure"
"github.com/authelia/authelia/internal/configuration/schema"
)
// Load the configuration given the provided options and sources.
func Load(val *schema.StructValidator, sources ...Source) (keys []string, configuration *schema.Configuration, err error) {
if val == nil {
return keys, configuration, errNoValidator
}
ko := koanf.NewWithConf(koanf.Conf{
Delim: constDelimiter,
StrictMerge: false,
})
err = loadSources(ko, val, sources...)
if err != nil {
return ko.Keys(), configuration, err
}
configuration = &schema.Configuration{}
unmarshal(ko, val, "", configuration)
return ko.Keys(), configuration, nil
}
func unmarshal(ko *koanf.Koanf, val *schema.StructValidator, path string, o interface{}) {
c := koanf.UnmarshalConf{
DecoderConfig: &mapstructure.DecoderConfig{
DecodeHook: mapstructure.ComposeDecodeHookFunc(
mapstructure.StringToTimeDurationHookFunc(),
mapstructure.StringToSliceHookFunc(","),
),
Metadata: nil,
Result: o,
WeaklyTypedInput: true,
},
}
if err := ko.UnmarshalWithConf(path, o, c); err != nil {
val.Push(fmt.Errorf("error occurred during unmarshalling configuration: %w", err))
}
}
func loadSources(ko *koanf.Koanf, val *schema.StructValidator, sources ...Source) (err error) {
if len(sources) == 0 {
return errNoSources
}
for _, source := range sources {
err := source.Load(val)
if err != nil {
val.Push(fmt.Errorf("failed to load configuration from %s source: %+v", source.Name(), err))
continue
}
err = source.Merge(ko, val)
if err != nil {
val.Push(fmt.Errorf("failed to merge configuration from %s source: %+v", source.Name(), err))
continue
}
}
return nil
}

View File

@ -0,0 +1,287 @@
package configuration
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"runtime"
"sort"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/authelia/authelia/internal/configuration/schema"
"github.com/authelia/authelia/internal/configuration/validator"
"github.com/authelia/authelia/internal/utils"
)
func TestShouldErrorSecretNotExist(t *testing.T) {
testReset()
dir, err := ioutil.TempDir("", "authelia-test-secret-not-exist")
assert.NoError(t, err)
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"JWT_SECRET_FILE", filepath.Join(dir, "jwt")))
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"DUO_API_SECRET_KEY_FILE", filepath.Join(dir, "duo")))
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"SESSION_SECRET_FILE", filepath.Join(dir, "session")))
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"AUTHENTICATION_BACKEND_LDAP_PASSWORD_FILE", filepath.Join(dir, "authentication")))
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"NOTIFIER_SMTP_PASSWORD_FILE", filepath.Join(dir, "notifier")))
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"SESSION_REDIS_PASSWORD_FILE", filepath.Join(dir, "redis")))
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"SESSION_REDIS_HIGH_AVAILABILITY_SENTINEL_PASSWORD_FILE", filepath.Join(dir, "redis-sentinel")))
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"STORAGE_MYSQL_PASSWORD_FILE", filepath.Join(dir, "mysql")))
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"STORAGE_POSTGRES_PASSWORD_FILE", filepath.Join(dir, "postgres")))
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"TLS_KEY_FILE", filepath.Join(dir, "tls")))
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"IDENTITY_PROVIDERS_OIDC_ISSUER_PRIVATE_KEY_FILE", filepath.Join(dir, "oidc-key")))
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"IDENTITY_PROVIDERS_OIDC_HMAC_SECRET_FILE", filepath.Join(dir, "oidc-hmac")))
val := schema.NewStructValidator()
_, _, err = Load(val, NewEnvironmentSource(DefaultEnvPrefix, DefaultEnvDelimiter), NewSecretsSource(DefaultEnvPrefix, DefaultEnvDelimiter))
assert.NoError(t, err)
assert.Len(t, val.Warnings(), 0)
errs := val.Errors()
require.Len(t, errs, 12)
sort.Sort(utils.ErrSliceSortAlphabetical(errs))
errFmt := utils.GetExpectedErrTxt("filenotfound")
// ignore the errors before this as they are checked by the valdator.
assert.EqualError(t, errs[0], fmt.Sprintf(errFmtSecretIOIssue, filepath.Join(dir, "authentication"), "authentication_backend.ldap.password", fmt.Sprintf(errFmt, filepath.Join(dir, "authentication"))))
assert.EqualError(t, errs[1], fmt.Sprintf(errFmtSecretIOIssue, filepath.Join(dir, "duo"), "duo_api.secret_key", fmt.Sprintf(errFmt, filepath.Join(dir, "duo"))))
assert.EqualError(t, errs[2], fmt.Sprintf(errFmtSecretIOIssue, filepath.Join(dir, "jwt"), "jwt_secret", fmt.Sprintf(errFmt, filepath.Join(dir, "jwt"))))
assert.EqualError(t, errs[3], fmt.Sprintf(errFmtSecretIOIssue, filepath.Join(dir, "mysql"), "storage.mysql.password", fmt.Sprintf(errFmt, filepath.Join(dir, "mysql"))))
assert.EqualError(t, errs[4], fmt.Sprintf(errFmtSecretIOIssue, filepath.Join(dir, "notifier"), "notifier.smtp.password", fmt.Sprintf(errFmt, filepath.Join(dir, "notifier"))))
assert.EqualError(t, errs[5], fmt.Sprintf(errFmtSecretIOIssue, filepath.Join(dir, "oidc-hmac"), "identity_providers.oidc.hmac_secret", fmt.Sprintf(errFmt, filepath.Join(dir, "oidc-hmac"))))
assert.EqualError(t, errs[6], fmt.Sprintf(errFmtSecretIOIssue, filepath.Join(dir, "oidc-key"), "identity_providers.oidc.issuer_private_key", fmt.Sprintf(errFmt, filepath.Join(dir, "oidc-key"))))
assert.EqualError(t, errs[7], fmt.Sprintf(errFmtSecretIOIssue, filepath.Join(dir, "postgres"), "storage.postgres.password", fmt.Sprintf(errFmt, filepath.Join(dir, "postgres"))))
assert.EqualError(t, errs[8], fmt.Sprintf(errFmtSecretIOIssue, filepath.Join(dir, "redis"), "session.redis.password", fmt.Sprintf(errFmt, filepath.Join(dir, "redis"))))
assert.EqualError(t, errs[9], fmt.Sprintf(errFmtSecretIOIssue, filepath.Join(dir, "redis-sentinel"), "session.redis.high_availability.sentinel_password", fmt.Sprintf(errFmt, filepath.Join(dir, "redis-sentinel"))))
assert.EqualError(t, errs[10], fmt.Sprintf(errFmtSecretIOIssue, filepath.Join(dir, "session"), "session.secret", fmt.Sprintf(errFmt, filepath.Join(dir, "session"))))
assert.EqualError(t, errs[11], fmt.Sprintf(errFmtSecretIOIssue, filepath.Join(dir, "tls"), "tls_key", fmt.Sprintf(errFmt, filepath.Join(dir, "tls"))))
}
func TestLoadShouldReturnErrWithoutValidator(t *testing.T) {
_, _, err := Load(nil, NewEnvironmentSource(DefaultEnvPrefix, DefaultEnvDelimiter))
assert.EqualError(t, err, "no validator provided")
}
func TestLoadShouldReturnErrWithoutSources(t *testing.T) {
_, _, err := Load(schema.NewStructValidator())
assert.EqualError(t, err, "no sources provided")
}
func TestShouldHaveNotifier(t *testing.T) {
testReset()
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"SESSION_SECRET", "abc"))
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"STORAGE_MYSQL_PASSWORD", "abc"))
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"JWT_SECRET", "abc"))
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"AUTHENTICATION_BACKEND_LDAP_PASSWORD", "abc"))
val := schema.NewStructValidator()
_, config, err := Load(val, NewDefaultSources([]string{"./test_resources/config.yml"}, DefaultEnvPrefix, DefaultEnvDelimiter)...)
assert.NoError(t, err)
assert.Len(t, val.Errors(), 0)
assert.Len(t, val.Warnings(), 0)
assert.NotNil(t, config.Notifier)
}
func TestShouldValidateConfigurationWithEnv(t *testing.T) {
testReset()
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"SESSION_SECRET", "abc"))
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"STORAGE_MYSQL_PASSWORD", "abc"))
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"JWT_SECRET", "abc"))
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"AUTHENTICATION_BACKEND_LDAP_PASSWORD", "abc"))
val := schema.NewStructValidator()
_, _, err := Load(val, NewDefaultSources([]string{"./test_resources/config.yml"}, DefaultEnvPrefix, DefaultEnvDelimiter)...)
assert.NoError(t, err)
assert.Len(t, val.Errors(), 0)
assert.Len(t, val.Warnings(), 0)
}
func TestShouldNotIgnoreInvalidEnvs(t *testing.T) {
testReset()
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"SESSION_SECRET", "an env session secret"))
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"STORAGE_MYSQL_PASSWORD", "an env storage mysql password"))
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"STORAGE_MYSQL", "a bad env"))
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"JWT_SECRET", "an env jwt secret"))
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"AUTHENTICATION_BACKEND_LDAP_PASSWORD", "an env authentication backend ldap password"))
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"AUTHENTICATION_BACKEND_LDAP_URL", "an env authentication backend ldap password"))
val := schema.NewStructValidator()
keys, _, err := Load(val, NewDefaultSources([]string{"./test_resources/config.yml"}, DefaultEnvPrefix, DefaultEnvDelimiter)...)
assert.NoError(t, err)
validator.ValidateKeys(keys, DefaultEnvPrefix, val)
require.Len(t, val.Warnings(), 1)
assert.Len(t, val.Errors(), 0)
assert.EqualError(t, val.Warnings()[0], fmt.Sprintf("configuration environment variable not expected: %sSTORAGE_MYSQL", DefaultEnvPrefix))
}
func TestShouldValidateAndRaiseErrorsOnNormalConfigurationAndSecret(t *testing.T) {
testReset()
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"SESSION_SECRET", "an env session secret"))
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"SESSION_SECRET_FILE", "./test_resources/example_secret"))
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"STORAGE_MYSQL_PASSWORD", "an env storage mysql password"))
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"JWT_SECRET_FILE", "./test_resources/example_secret"))
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"AUTHENTICATION_BACKEND_LDAP_PASSWORD", "an env authentication backend ldap password"))
val := schema.NewStructValidator()
_, config, err := Load(val, NewDefaultSources([]string{"./test_resources/config.yml"}, DefaultEnvPrefix, DefaultEnvDelimiter)...)
assert.NoError(t, err)
require.Len(t, val.Errors(), 1)
assert.Len(t, val.Warnings(), 0)
assert.EqualError(t, val.Errors()[0], "secrets: error loading secret into key 'session.secret': it's already defined in other configuration sources")
assert.Equal(t, "example_secret value", config.JWTSecret)
assert.Equal(t, "example_secret value", config.Session.Secret)
assert.Equal(t, "an env storage mysql password", config.Storage.MySQL.Password)
assert.Equal(t, "an env authentication backend ldap password", config.AuthenticationBackend.LDAP.Password)
}
func TestShouldRaiseIOErrOnUnreadableFile(t *testing.T) {
if runtime.GOOS == constWindows {
t.Skip("skipping test due to being on windows")
}
testReset()
dir, err := ioutil.TempDir("", "authelia-conf")
assert.NoError(t, err)
assert.NoError(t, os.WriteFile(filepath.Join(dir, "myconf.yml"), []byte("server:\n port: 9091\n"), 0000))
cfg := filepath.Join(dir, "myconf.yml")
val := schema.NewStructValidator()
_, _, err = Load(val, NewYAMLFileSource(cfg))
assert.NoError(t, err)
require.Len(t, val.Errors(), 1)
assert.Len(t, val.Warnings(), 0)
assert.EqualError(t, val.Errors()[0], fmt.Sprintf("failed to load configuration from yaml file(%s) source: open %s: permission denied", cfg, cfg))
}
func TestShouldValidateConfigurationWithEnvSecrets(t *testing.T) {
testReset()
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"SESSION_SECRET_FILE", "./test_resources/example_secret"))
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"STORAGE_MYSQL_PASSWORD_FILE", "./test_resources/example_secret"))
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"JWT_SECRET_FILE", "./test_resources/example_secret"))
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"AUTHENTICATION_BACKEND_LDAP_PASSWORD_FILE", "./test_resources/example_secret"))
val := schema.NewStructValidator()
_, config, err := Load(val, NewDefaultSources([]string{"./test_resources/config.yml"}, DefaultEnvPrefix, DefaultEnvDelimiter)...)
assert.NoError(t, err)
assert.Len(t, val.Errors(), 0)
assert.Len(t, val.Warnings(), 0)
assert.Equal(t, "example_secret value", config.JWTSecret)
assert.Equal(t, "example_secret value", config.Session.Secret)
assert.Equal(t, "example_secret value", config.AuthenticationBackend.LDAP.Password)
assert.Equal(t, "example_secret value", config.Storage.MySQL.Password)
}
func TestShouldValidateAndRaiseErrorsOnBadConfiguration(t *testing.T) {
testReset()
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"SESSION_SECRET", "abc"))
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"STORAGE_MYSQL_PASSWORD", "abc"))
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"JWT_SECRET", "abc"))
assert.NoError(t, os.Setenv(DefaultEnvPrefix+"AUTHENTICATION_BACKEND_LDAP_PASSWORD", "abc"))
val := schema.NewStructValidator()
keys, _, err := Load(val, NewDefaultSources([]string{"./test_resources/config_bad_keys.yml"}, DefaultEnvPrefix, DefaultEnvDelimiter)...)
assert.NoError(t, err)
validator.ValidateKeys(keys, DefaultEnvPrefix, val)
require.Len(t, val.Errors(), 2)
assert.Len(t, val.Warnings(), 0)
assert.EqualError(t, val.Errors()[0], "configuration key not expected: loggy_file")
assert.EqualError(t, val.Errors()[1], "invalid configuration key 'logs_level' was replaced by 'log.level'")
}
func TestShouldNotReadConfigurationOnFSAccessDenied(t *testing.T) {
if runtime.GOOS == constWindows {
t.Skip("skipping test due to being on windows")
}
testReset()
dir, err := ioutil.TempDir("", "authelia-config")
assert.NoError(t, err)
cfg := filepath.Join(dir, "config.yml")
assert.NoError(t, testCreateFile(filepath.Join(dir, "config.yml"), "port: 9091\n", 0000))
val := schema.NewStructValidator()
_, _, err = Load(val, NewYAMLFileSource(cfg))
assert.NoError(t, err)
require.Len(t, val.Errors(), 1)
assert.EqualError(t, val.Errors()[0], fmt.Sprintf("failed to load configuration from yaml file(%s) source: open %s: permission denied", cfg, cfg))
}
func TestShouldNotLoadDirectoryConfiguration(t *testing.T) {
testReset()
dir, err := ioutil.TempDir("", "authelia-config")
assert.NoError(t, err)
val := schema.NewStructValidator()
_, _, err = Load(val, NewYAMLFileSource(dir))
assert.NoError(t, err)
require.Len(t, val.Errors(), 1)
assert.Len(t, val.Warnings(), 0)
expectedErr := fmt.Sprintf(utils.GetExpectedErrTxt("yamlisdir"), dir)
assert.EqualError(t, val.Errors()[0], fmt.Sprintf("failed to load configuration from yaml file(%s) source: %s", dir, expectedErr))
}
func testReset() {
testUnsetEnvName("STORAGE_MYSQL")
testUnsetEnvName("JWT_SECRET")
testUnsetEnvName("DUO_API_SECRET_KEY")
testUnsetEnvName("SESSION_SECRET")
testUnsetEnvName("AUTHENTICATION_BACKEND_LDAP_PASSWORD")
testUnsetEnvName("AUTHENTICATION_BACKEND_LDAP_URL")
testUnsetEnvName("NOTIFIER_SMTP_PASSWORD")
testUnsetEnvName("SESSION_REDIS_PASSWORD")
testUnsetEnvName("SESSION_REDIS_HIGH_AVAILABILITY_SENTINEL_PASSWORD")
testUnsetEnvName("STORAGE_MYSQL_PASSWORD")
testUnsetEnvName("STORAGE_POSTGRES_PASSWORD")
testUnsetEnvName("TLS_KEY")
testUnsetEnvName("PORT")
testUnsetEnvName("IDENTITY_PROVIDERS_OIDC_ISSUER_PRIVATE_KEY")
testUnsetEnvName("IDENTITY_PROVIDERS_OIDC_HMAC_SECRET")
}
func testUnsetEnvName(name string) {
_ = os.Unsetenv(DefaultEnvPrefix + name)
_ = os.Unsetenv(DefaultEnvPrefix + name + constSecretSuffix)
}
func testCreateFile(path, value string, perm os.FileMode) (err error) {
return os.WriteFile(path, []byte(value), perm)
}

View File

@ -1,99 +0,0 @@
package configuration
import (
_ "embed" // Embed config.template.yml.
"errors"
"fmt"
"io/ioutil"
"os"
"strings"
"github.com/spf13/viper"
"gopkg.in/yaml.v2"
"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.
func Read(configPath string) (*schema.Configuration, []error) {
logger := logging.Logger()
if configPath == "" {
return nil, []error{errors.New("No config file path provided")}
}
_, err := os.Stat(configPath)
if err != nil {
errs := []error{
fmt.Errorf("Unable to find config file: %v", configPath),
fmt.Errorf("Generating config file: %v", configPath),
}
err = generateConfigFromTemplate(configPath)
if err != nil {
errs = append(errs, err)
} else {
errs = append(errs, fmt.Errorf("Generated configuration at: %v", configPath))
}
return nil, errs
}
file, err := ioutil.ReadFile(configPath)
if err != nil {
return nil, []error{fmt.Errorf("Failed to %v", err)}
}
var data interface{}
err = yaml.Unmarshal(file, &data)
if err != nil {
return nil, []error{fmt.Errorf("Error malformed %v", err)}
}
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
// Dynamically load the secret env names from the SecretNames map.
for _, secretName := range validator.SecretNames {
_ = viper.BindEnv(validator.SecretNameToEnvName(secretName))
}
viper.SetConfigFile(configPath)
_ = viper.ReadInConfig()
var configuration schema.Configuration
viper.Unmarshal(&configuration) //nolint:errcheck // TODO: Legacy code, consider refactoring time permitting.
val := schema.NewStructValidator()
validator.ValidateSecrets(&configuration, val, viper.GetViper())
validator.ValidateConfiguration(&configuration, val)
validator.ValidateKeys(val, viper.AllKeys())
if val.HasErrors() {
return nil, val.Errors()
}
if val.HasWarnings() {
for _, warn := range val.Warnings() {
logger.Warnf(warn.Error())
}
}
return &configuration, nil
}
//go:embed config.template.yml
var cfg []byte
func generateConfigFromTemplate(configPath string) error {
err := ioutil.WriteFile(configPath, cfg, 0600)
if err != nil {
return fmt.Errorf("Unable to generate %v: %v", configPath, err)
}
return nil
}

View File

@ -1,282 +0,0 @@
package configuration
import (
"io/ioutil"
"os"
"path"
"runtime"
"sort"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/authelia/authelia/internal/authentication"
"github.com/authelia/authelia/internal/utils"
)
func createTestingTempFile(t *testing.T, dir, name, content string) {
err := ioutil.WriteFile(path.Join(dir, name), []byte(content), 0600)
require.NoError(t, err)
}
func resetEnv() {
_ = os.Unsetenv("AUTHELIA_JWT_SECRET_FILE")
_ = os.Unsetenv("AUTHELIA_DUO_API_SECRET_KEY_FILE")
_ = os.Unsetenv("AUTHELIA_SESSION_SECRET_FILE")
_ = os.Unsetenv("AUTHELIA_SESSION_SECRET_FILE")
_ = os.Unsetenv("AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PASSWORD_FILE")
_ = os.Unsetenv("AUTHELIA_NOTIFIER_SMTP_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_POSTGRES_PASSWORD_FILE")
}
func setupEnv(t *testing.T) string {
resetEnv()
dirEnv := os.Getenv("AUTHELIA_TESTING_DIR")
if dirEnv != "" {
return dirEnv
}
dir := "/tmp/authelia" + utils.RandomString(10, authentication.HashingPossibleSaltCharacters) + "/"
err := os.MkdirAll(dir, 0700)
require.NoError(t, err)
createTestingTempFile(t, dir, "jwt", "secret_from_env")
createTestingTempFile(t, dir, "duo", "duo_secret_from_env")
createTestingTempFile(t, dir, "session", "session_secret_from_env")
createTestingTempFile(t, dir, "authentication", "ldap_secret_from_env")
createTestingTempFile(t, dir, "notifier", "smtp_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, "postgres", "postgres_secret_from_env")
require.NoError(t, os.Setenv("AUTHELIA_TESTING_DIR", dir))
return dir
}
func TestShouldErrorNoConfigPath(t *testing.T) {
_, errors := Read("")
require.Len(t, errors, 1)
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) {
if runtime.GOOS == windows {
t.Skip("skipping test due to being on windows")
}
resetEnv()
_ = os.Mkdir("/tmp/noperms/", 0000)
_, errors := Read("/tmp/noperms/configuration.yml")
require.Len(t, errors, 3)
require.EqualError(t, errors[0], "Unable to find config file: /tmp/noperms/configuration.yml")
require.EqualError(t, errors[1], "Generating config file: /tmp/noperms/configuration.yml")
require.EqualError(t, errors[2], "Unable to generate /tmp/noperms/configuration.yml: open /tmp/noperms/configuration.yml: permission denied")
}
func TestShouldErrorAndGenerateConfigFile(t *testing.T) {
_, errors := Read("./nonexistent.yml")
_ = os.Remove("./nonexistent.yml")
require.Len(t, errors, 3)
require.EqualError(t, errors[0], "Unable to find config file: ./nonexistent.yml")
require.EqualError(t, errors[1], "Generating config file: ./nonexistent.yml")
require.EqualError(t, errors[2], "Generated configuration at: ./nonexistent.yml")
}
func TestShouldErrorPermissionsConfigFile(t *testing.T) {
resetEnv()
_ = ioutil.WriteFile("/tmp/authelia/permissions.yml", []byte{}, 0000) // nolint:gosec
_, errors := Read("/tmp/authelia/permissions.yml")
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)
assert.EqualError(t, errors[0], "Failed to open /tmp/authelia/permissions.yml: permission denied")
}
}
func TestShouldErrorParseBadConfigFile(t *testing.T) {
_, errors := Read("./test_resources/config_bad_quoting.yml")
require.Len(t, errors, 1)
require.EqualError(t, errors[0], "Error malformed yaml: line 27: did not find expected alphabetic or numeric character")
}
func TestShouldParseConfigFile(t *testing.T) {
dir := setupEnv(t)
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"))
config, errors := Read("./test_resources/config.yml")
require.Len(t, errors, 0)
assert.Equal(t, 9091, config.Server.Port)
assert.Equal(t, "debug", config.Logging.Level)
assert.Equal(t, "https://home.example.com:8080/", config.DefaultRedirectionURL)
assert.Equal(t, "authelia.com", config.TOTP.Issuer)
assert.Equal(t, "secret_from_env", config.JWTSecret)
assert.Equal(t, "api-123456789.example.com", config.DuoAPI.Hostname)
assert.Equal(t, "ABCDEF", config.DuoAPI.IntegrationKey)
assert.Equal(t, "duo_secret_from_env", config.DuoAPI.SecretKey)
assert.Equal(t, "session_secret_from_env", config.Session.Secret)
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, "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, "deny", config.AccessControl.DefaultPolicy)
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) {
dir := setupEnv(t)
require.NoError(t, os.Setenv("AUTHELIA_STORAGE_POSTGRES_PASSWORD_FILE", dir+"postgres"))
require.NoError(t, os.Setenv("AUTHELIA_AUTHENTICATION_BACKEND_LDAP_PASSWORD_FILE", dir+"authentication"))
require.NoError(t, os.Setenv("AUTHELIA_JWT_SECRET_FILE", dir+"jwt"))
require.NoError(t, os.Setenv("AUTHELIA_SESSION_SECRET_FILE", dir+"session"))
config, errors := Read("./test_resources/config_alt.yml")
require.Len(t, errors, 0)
assert.Equal(t, 9091, config.Server.Port)
assert.Equal(t, "debug", config.Logging.Level)
assert.Equal(t, "https://home.example.com:8080/", config.DefaultRedirectionURL)
assert.Equal(t, "authelia.com", config.TOTP.Issuer)
assert.Equal(t, "secret_from_env", config.JWTSecret)
assert.Equal(t, "api-123456789.example.com", config.DuoAPI.Hostname)
assert.Equal(t, "ABCDEF", config.DuoAPI.IntegrationKey)
assert.Equal(t, "postgres_secret_from_env", config.Storage.PostgreSQL.Password)
assert.Equal(t, "deny", config.AccessControl.DefaultPolicy)
assert.Len(t, config.AccessControl.Rules, 12)
}
func TestShouldNotParseConfigFileWithOldOrUnexpectedKeys(t *testing.T) {
dir := setupEnv(t)
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_STORAGE_MYSQL_PASSWORD_FILE", dir+"mysql"))
_, errors := Read("./test_resources/config_bad_keys.yml")
require.Len(t, errors, 2)
// Sort error slice to prevent shenanigans that somehow occur
sort.Slice(errors, func(i, j int) bool {
return errors[i].Error() < errors[j].Error()
})
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'")
}
func TestShouldValidateConfigurationTemplate(t *testing.T) {
resetEnv()
_, errors := Read("../../config.template.yml")
assert.Len(t, errors, 0)
}
func TestShouldOnlyAllowEnvOrConfig(t *testing.T) {
dir := setupEnv(t)
resetEnv()
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_STORAGE_MYSQL_PASSWORD_FILE", dir+"mysql"))
_, errors := Read("./test_resources/config_with_secret.yml")
require.Len(t, errors, 1)
require.EqualError(t, errors[0], "error loading secret (jwt_secret): it's already defined in the config file")
}

View File

@ -2,25 +2,25 @@ package schema
// AccessControlConfiguration represents the configuration related to ACLs.
type AccessControlConfiguration struct {
DefaultPolicy string `mapstructure:"default_policy"`
Networks []ACLNetwork `mapstructure:"networks"`
Rules []ACLRule `mapstructure:"rules"`
DefaultPolicy string `koanf:"default_policy"`
Networks []ACLNetwork `koanf:"networks"`
Rules []ACLRule `koanf:"rules"`
}
// ACLNetwork represents one ACL network group entry; "weak" coerces a single value into slice.
type ACLNetwork struct {
Name string `mapstructure:"name"`
Networks []string `mapstructure:"networks"`
Name string `koanf:"name"`
Networks []string `koanf:"networks"`
}
// ACLRule represents one ACL rule entry; "weak" coerces a single value into slice.
type ACLRule struct {
Domains []string `mapstructure:"domain,weak"`
Policy string `mapstructure:"policy"`
Subjects [][]string `mapstructure:"subject,weak"`
Networks []string `mapstructure:"networks"`
Resources []string `mapstructure:"resources"`
Methods []string `mapstructure:"methods"`
Domains []string `koanf:"domain"`
Policy string `koanf:"policy"`
Subjects [][]string `koanf:"subject"`
Networks []string `koanf:"networks"`
Resources []string `koanf:"resources"`
Methods []string `koanf:"methods"`
}
// DefaultACLNetwork represents the default configuration related to access control network group configuration.

View File

@ -2,45 +2,45 @@ package schema
// LDAPAuthenticationBackendConfiguration represents the configuration related to LDAP server.
type LDAPAuthenticationBackendConfiguration struct {
Implementation string `mapstructure:"implementation"`
URL string `mapstructure:"url"`
BaseDN string `mapstructure:"base_dn"`
AdditionalUsersDN string `mapstructure:"additional_users_dn"`
UsersFilter string `mapstructure:"users_filter"`
AdditionalGroupsDN string `mapstructure:"additional_groups_dn"`
GroupsFilter string `mapstructure:"groups_filter"`
GroupNameAttribute string `mapstructure:"group_name_attribute"`
UsernameAttribute string `mapstructure:"username_attribute"`
MailAttribute string `mapstructure:"mail_attribute"`
DisplayNameAttribute string `mapstructure:"display_name_attribute"`
User string `mapstructure:"user"`
Password string `mapstructure:"password"`
StartTLS bool `mapstructure:"start_tls"`
TLS *TLSConfig `mapstructure:"tls"`
Implementation string `koanf:"implementation"`
URL string `koanf:"url"`
BaseDN string `koanf:"base_dn"`
AdditionalUsersDN string `koanf:"additional_users_dn"`
UsersFilter string `koanf:"users_filter"`
AdditionalGroupsDN string `koanf:"additional_groups_dn"`
GroupsFilter string `koanf:"groups_filter"`
GroupNameAttribute string `koanf:"group_name_attribute"`
UsernameAttribute string `koanf:"username_attribute"`
MailAttribute string `koanf:"mail_attribute"`
DisplayNameAttribute string `koanf:"display_name_attribute"`
User string `koanf:"user"`
Password string `koanf:"password"`
StartTLS bool `koanf:"start_tls"`
TLS *TLSConfig `koanf:"tls"`
}
// FileAuthenticationBackendConfiguration represents the configuration related to file-based backend.
type FileAuthenticationBackendConfiguration struct {
Path string `mapstructure:"path"`
Password *PasswordConfiguration `mapstructure:"password"`
Path string `koanf:"path"`
Password *PasswordConfiguration `koanf:"password"`
}
// PasswordConfiguration represents the configuration related to password hashing.
type PasswordConfiguration struct {
Iterations int `mapstructure:"iterations"`
KeyLength int `mapstructure:"key_length"`
SaltLength int `mapstructure:"salt_length"`
Iterations int `koanf:"iterations"`
KeyLength int `koanf:"key_length"`
SaltLength int `koanf:"salt_length"`
Algorithm string `mapstrucutre:"algorithm"`
Memory int `mapstructure:"memory"`
Parallelism int `mapstructure:"parallelism"`
Memory int `koanf:"memory"`
Parallelism int `koanf:"parallelism"`
}
// AuthenticationBackendConfiguration represents the configuration related to the authentication backend.
type AuthenticationBackendConfiguration struct {
DisableResetPassword bool `mapstructure:"disable_reset_password"`
RefreshInterval string `mapstructure:"refresh_interval"`
LDAP *LDAPAuthenticationBackendConfiguration `mapstructure:"ldap"`
File *FileAuthenticationBackendConfiguration `mapstructure:"file"`
DisableResetPassword bool `koanf:"disable_reset_password"`
RefreshInterval string `koanf:"refresh_interval"`
LDAP *LDAPAuthenticationBackendConfiguration `koanf:"ldap"`
File *FileAuthenticationBackendConfiguration `koanf:"file"`
}
// DefaultPasswordConfiguration represents the default configuration related to Argon2id hashing.

View File

@ -2,10 +2,10 @@ package schema
// Configuration object extracted from YAML configuration file.
type Configuration struct {
Theme string `mapstructure:"theme"`
CertificatesDirectory string `mapstructure:"certificates_directory"`
JWTSecret string `mapstructure:"jwt_secret"`
DefaultRedirectionURL string `mapstructure:"default_redirection_url"`
Theme string `koanf:"theme"`
CertificatesDirectory string `koanf:"certificates_directory"`
JWTSecret string `koanf:"jwt_secret"`
DefaultRedirectionURL string `koanf:"default_redirection_url"`
Host string `koanf:"host"` // Deprecated: To be Removed. TODO: Remove in 4.33.0.
Port int `koanf:"port"` // Deprecated: To be Removed. TODO: Remove in 4.33.0.
@ -15,15 +15,15 @@ type Configuration struct {
LogFormat string `koanf:"log_format"` // Deprecated: To be Removed. TODO: Remove in 4.33.0.
LogFilePath string `koanf:"log_file_path"` // Deprecated: To be Removed. TODO: Remove in 4.33.0.
Logging LogConfiguration `mapstructure:"log"`
IdentityProviders IdentityProvidersConfiguration `mapstructure:"identity_providers"`
AuthenticationBackend AuthenticationBackendConfiguration `mapstructure:"authentication_backend"`
Session SessionConfiguration `mapstructure:"session"`
TOTP *TOTPConfiguration `mapstructure:"totp"`
DuoAPI *DuoAPIConfiguration `mapstructure:"duo_api"`
AccessControl AccessControlConfiguration `mapstructure:"access_control"`
Regulation *RegulationConfiguration `mapstructure:"regulation"`
Storage StorageConfiguration `mapstructure:"storage"`
Notifier *NotifierConfiguration `mapstructure:"notifier"`
Server ServerConfiguration `mapstructure:"server"`
Log LogConfiguration `koanf:"log"`
IdentityProviders IdentityProvidersConfiguration `koanf:"identity_providers"`
AuthenticationBackend AuthenticationBackendConfiguration `koanf:"authentication_backend"`
Session SessionConfiguration `koanf:"session"`
TOTP *TOTPConfiguration `koanf:"totp"`
DuoAPI *DuoAPIConfiguration `koanf:"duo_api"`
AccessControl AccessControlConfiguration `koanf:"access_control"`
Regulation *RegulationConfiguration `koanf:"regulation"`
Storage StorageConfiguration `koanf:"storage"`
Notifier *NotifierConfiguration `koanf:"notifier"`
Server ServerConfiguration `koanf:"server"`
}

View File

@ -2,7 +2,7 @@ package schema
// DuoAPIConfiguration represents the configuration related to Duo API.
type DuoAPIConfiguration struct {
Hostname string `mapstructure:"hostname"`
IntegrationKey string `mapstructure:"integration_key"`
SecretKey string `mapstructure:"secret_key"`
Hostname string `koanf:"hostname"`
IntegrationKey string `koanf:"integration_key"`
SecretKey string `koanf:"secret_key"`
}

View File

@ -4,42 +4,43 @@ import "time"
// IdentityProvidersConfiguration represents the IdentityProviders 2.0 configuration for Authelia.
type IdentityProvidersConfiguration struct {
OIDC *OpenIDConnectConfiguration `mapstructure:"oidc"`
OIDC *OpenIDConnectConfiguration `koanf:"oidc"`
}
// OpenIDConnectConfiguration configuration for OpenID Connect.
type OpenIDConnectConfiguration struct {
// This secret must be 32 bytes long
HMACSecret string `mapstructure:"hmac_secret"`
IssuerPrivateKey string `mapstructure:"issuer_private_key"`
HMACSecret string `koanf:"hmac_secret"`
IssuerPrivateKey string `koanf:"issuer_private_key"`
AccessTokenLifespan time.Duration `mapstructure:"access_token_lifespan"`
AuthorizeCodeLifespan time.Duration `mapstructure:"authorize_code_lifespan"`
IDTokenLifespan time.Duration `mapstructure:"id_token_lifespan"`
RefreshTokenLifespan time.Duration `mapstructure:"refresh_token_lifespan"`
EnableClientDebugMessages bool `mapstructure:"enable_client_debug_messages"`
MinimumParameterEntropy int `mapstructure:"minimum_parameter_entropy"`
AccessTokenLifespan time.Duration `koanf:"access_token_lifespan"`
AuthorizeCodeLifespan time.Duration `koanf:"authorize_code_lifespan"`
IDTokenLifespan time.Duration `koanf:"id_token_lifespan"`
RefreshTokenLifespan time.Duration `koanf:"refresh_token_lifespan"`
Clients []OpenIDConnectClientConfiguration `mapstructure:"clients"`
EnableClientDebugMessages bool `koanf:"enable_client_debug_messages"`
MinimumParameterEntropy int `koanf:"minimum_parameter_entropy"`
Clients []OpenIDConnectClientConfiguration `koanf:"clients"`
}
// OpenIDConnectClientConfiguration configuration for an OpenID Connect client.
type OpenIDConnectClientConfiguration struct {
ID string `mapstructure:"id"`
Description string `mapstructure:"description"`
Secret string `mapstructure:"secret"`
Public bool `mapstructure:"public"`
ID string `koanf:"id"`
Description string `koanf:"description"`
Secret string `koanf:"secret"`
Public bool `koanf:"public"`
Policy string `mapstructure:"authorization_policy"`
Policy string `koanf:"authorization_policy"`
Audience []string `mapstructure:"audience"`
Scopes []string `mapstructure:"scopes"`
RedirectURIs []string `mapstructure:"redirect_uris"`
GrantTypes []string `mapstructure:"grant_types"`
ResponseTypes []string `mapstructure:"response_types"`
ResponseModes []string `mapstructure:"response_modes"`
Audience []string `koanf:"audience"`
Scopes []string `koanf:"scopes"`
RedirectURIs []string `koanf:"redirect_uris"`
GrantTypes []string `koanf:"grant_types"`
ResponseTypes []string `koanf:"response_types"`
ResponseModes []string `koanf:"response_modes"`
UserinfoSigningAlgorithm string `mapstructure:"userinfo_signing_algorithm"`
UserinfoSigningAlgorithm string `koanf:"userinfo_signing_algorithm"`
}
// DefaultOpenIDConnectConfiguration contains defaults for OIDC.

View File

@ -2,10 +2,10 @@ package schema
// LogConfiguration represents the logging configuration.
type LogConfiguration struct {
Level string `mapstructure:"level"`
Format string `mapstructure:"format"`
FilePath string `mapstructure:"file_path"`
KeepStdout bool `mapstructure:"keep_stdout"`
Level string `koanf:"level"`
Format string `koanf:"format"`
FilePath string `koanf:"file_path"`
KeepStdout bool `koanf:"keep_stdout"`
}
// DefaultLoggingConfiguration is the default logging configuration.

View File

@ -2,29 +2,29 @@ package schema
// FileSystemNotifierConfiguration represents the configuration of the notifier writing emails in a file.
type FileSystemNotifierConfiguration struct {
Filename string `mapstructure:"filename"`
Filename string `koanf:"filename"`
}
// SMTPNotifierConfiguration represents the configuration of the SMTP server to send emails with.
type SMTPNotifierConfiguration struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
Username string `mapstructure:"username"`
Password string `mapstructure:"password"`
Identifier string `mapstructure:"identifier"`
Sender string `mapstructure:"sender"`
Subject string `mapstructure:"subject"`
StartupCheckAddress string `mapstructure:"startup_check_address"`
DisableRequireTLS bool `mapstructure:"disable_require_tls"`
DisableHTMLEmails bool `mapstructure:"disable_html_emails"`
TLS *TLSConfig `mapstructure:"tls"`
Host string `koanf:"host"`
Port int `koanf:"port"`
Username string `koanf:"username"`
Password string `koanf:"password"`
Identifier string `koanf:"identifier"`
Sender string `koanf:"sender"`
Subject string `koanf:"subject"`
StartupCheckAddress string `koanf:"startup_check_address"`
DisableRequireTLS bool `koanf:"disable_require_tls"`
DisableHTMLEmails bool `koanf:"disable_html_emails"`
TLS *TLSConfig `koanf:"tls"`
}
// NotifierConfiguration represents the configuration of the notifier to use when sending notifications to users.
type NotifierConfiguration struct {
DisableStartupCheck bool `mapstructure:"disable_startup_check"`
FileSystem *FileSystemNotifierConfiguration `mapstructure:"filesystem"`
SMTP *SMTPNotifierConfiguration `mapstructure:"smtp"`
DisableStartupCheck bool `koanf:"disable_startup_check"`
FileSystem *FileSystemNotifierConfiguration `koanf:"filesystem"`
SMTP *SMTPNotifierConfiguration `koanf:"smtp"`
}
// DefaultSMTPNotifierConfiguration represents default configuration parameters for the SMTP notifier.

View File

@ -2,9 +2,9 @@ package schema
// RegulationConfiguration represents the configuration related to regulation.
type RegulationConfiguration struct {
MaxRetries int `mapstructure:"max_retries"`
FindTime string `mapstructure:"find_time"`
BanTime string `mapstructure:"ban_time"`
MaxRetries int `koanf:"max_retries"`
FindTime string `koanf:"find_time,weak"`
BanTime string `koanf:"ban_time,weak"`
}
// DefaultRegulationConfiguration represents default configuration parameters for the regulator.

View File

@ -2,21 +2,21 @@ package schema
// ServerConfiguration represents the configuration of the http server.
type ServerConfiguration struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
Path string `mapstructure:"path"`
ReadBufferSize int `mapstructure:"read_buffer_size"`
WriteBufferSize int `mapstructure:"write_buffer_size"`
EnablePprof bool `mapstructure:"enable_endpoint_pprof"`
EnableExpvars bool `mapstructure:"enable_endpoint_expvars"`
Host string `koanf:"host"`
Port int `koanf:"port"`
Path string `koanf:"path"`
ReadBufferSize int `koanf:"read_buffer_size"`
WriteBufferSize int `koanf:"write_buffer_size"`
EnablePprof bool `koanf:"enable_endpoint_pprof"`
EnableExpvars bool `koanf:"enable_endpoint_expvars"`
TLS ServerTLSConfiguration `mapstructure:"tls"`
TLS ServerTLSConfiguration `koanf:"tls"`
}
// ServerTLSConfiguration represents the configuration of the http servers TLS options.
type ServerTLSConfiguration struct {
Certificate string `mapstructure:"certificate"`
Key string `mapstructure:"key"`
Certificate string `koanf:"certificate"`
Key string `koanf:"key"`
}
// DefaultServerConfiguration represents the default values of the ServerConfiguration.

View File

@ -2,42 +2,42 @@ package schema
// RedisNode Represents a Node.
type RedisNode struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
Host string `koanf:"host"`
Port int `koanf:"port"`
}
// RedisHighAvailabilityConfiguration holds configuration variables for Redis Cluster/Sentinel.
type RedisHighAvailabilityConfiguration struct {
SentinelName string `mapstructure:"sentinel_name"`
SentinelPassword string `mapstructure:"sentinel_password"`
Nodes []RedisNode `mapstructure:"nodes"`
RouteByLatency bool `mapstructure:"route_by_latency"`
RouteRandomly bool `mapstructure:"route_randomly"`
SentinelName string `koanf:"sentinel_name"`
SentinelPassword string `koanf:"sentinel_password"`
Nodes []RedisNode `koanf:"nodes"`
RouteByLatency bool `koanf:"route_by_latency"`
RouteRandomly bool `koanf:"route_randomly"`
}
// RedisSessionConfiguration represents the configuration related to redis session store.
type RedisSessionConfiguration struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
Username string `mapstructure:"username"`
Password string `mapstructure:"password"`
DatabaseIndex int `mapstructure:"database_index"`
MaximumActiveConnections int `mapstructure:"maximum_active_connections"`
MinimumIdleConnections int `mapstructure:"minimum_idle_connections"`
TLS *TLSConfig `mapstructure:"tls"`
HighAvailability *RedisHighAvailabilityConfiguration `mapstructure:"high_availability"`
Host string `koanf:"host"`
Port int `koanf:"port"`
Username string `koanf:"username"`
Password string `koanf:"password"`
DatabaseIndex int `koanf:"database_index"`
MaximumActiveConnections int `koanf:"maximum_active_connections"`
MinimumIdleConnections int `koanf:"minimum_idle_connections"`
TLS *TLSConfig `koanf:"tls"`
HighAvailability *RedisHighAvailabilityConfiguration `koanf:"high_availability"`
}
// SessionConfiguration represents the configuration related to user sessions.
type SessionConfiguration struct {
Name string `mapstructure:"name"`
Domain string `mapstructure:"domain"`
SameSite string `mapstructure:"same_site"`
Secret string `mapstructure:"secret"`
Expiration string `mapstructure:"expiration"`
Inactivity string `mapstructure:"inactivity"`
RememberMeDuration string `mapstructure:"remember_me_duration"`
Redis *RedisSessionConfiguration `mapstructure:"redis"`
Name string `koanf:"name"`
Domain string `koanf:"domain"`
SameSite string `koanf:"same_site"`
Secret string `koanf:"secret"`
Expiration string `koanf:"expiration"`
Inactivity string `koanf:"inactivity"`
RememberMeDuration string `koanf:"remember_me_duration"`
Redis *RedisSessionConfiguration `koanf:"redis"`
}
// DefaultSessionConfiguration is the default session configuration.

View File

@ -2,7 +2,7 @@ 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"`
MinimumVersion string `koanf:"minimum_version"`
SkipVerify bool `koanf:"skip_verify"`
ServerName string `koanf:"server_name"`
}

View File

@ -2,32 +2,32 @@ package schema
// LocalStorageConfiguration represents the configuration when using local storage.
type LocalStorageConfiguration struct {
Path string `mapstructure:"path"`
Path string `koanf:"path"`
}
// SQLStorageConfiguration represents the configuration of the SQL database.
type SQLStorageConfiguration struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
Database string `mapstructure:"database"`
Username string `mapstructure:"username"`
Password string `mapstructure:"password"`
Host string `koanf:"host"`
Port int `koanf:"port"`
Database string `koanf:"database"`
Username string `koanf:"username"`
Password string `koanf:"password"`
}
// MySQLStorageConfiguration represents the configuration of a MySQL database.
type MySQLStorageConfiguration struct {
SQLStorageConfiguration `mapstructure:",squash"`
SQLStorageConfiguration `koanf:",squash"`
}
// PostgreSQLStorageConfiguration represents the configuration of a Postgres database.
type PostgreSQLStorageConfiguration struct {
SQLStorageConfiguration `mapstructure:",squash"`
SSLMode string `mapstructure:"sslmode"`
SQLStorageConfiguration `koanf:",squash"`
SSLMode string `koanf:"sslmode"`
}
// StorageConfiguration represents the configuration of the storage backend.
type StorageConfiguration struct {
Local *LocalStorageConfiguration `mapstructure:"local"`
MySQL *MySQLStorageConfiguration `mapstructure:"mysql"`
PostgreSQL *PostgreSQLStorageConfiguration `mapstructure:"postgres"`
Local *LocalStorageConfiguration `koanf:"local"`
MySQL *MySQLStorageConfiguration `koanf:"mysql"`
PostgreSQL *PostgreSQLStorageConfiguration `koanf:"postgres"`
}

View File

@ -2,9 +2,9 @@ package schema
// TOTPConfiguration represents the configuration related to TOTP options.
type TOTPConfiguration struct {
Issuer string `mapstructure:"issuer"`
Period int `mapstructure:"period"`
Skew *int `mapstructure:"skew"`
Issuer string `koanf:"issuer"`
Period int `koanf:"period"`
Skew *int `koanf:"skew"`
}
var defaultOtpSkew = 1

View File

@ -0,0 +1,126 @@
package configuration
import (
"errors"
"fmt"
"github.com/knadh/koanf"
"github.com/knadh/koanf/parsers/yaml"
"github.com/knadh/koanf/providers/env"
"github.com/knadh/koanf/providers/file"
"github.com/authelia/authelia/internal/configuration/schema"
"github.com/authelia/authelia/internal/configuration/validator"
)
// NewYAMLFileSource returns a Source configured to load from a specified YAML path. If there is an issue accessing this
// path it also returns an error.
func NewYAMLFileSource(path string) (source *YAMLFileSource) {
return &YAMLFileSource{
koanf: koanf.New(constDelimiter),
path: path,
}
}
// NewYAMLFileSources returns a slice of Source configured to load from specified YAML files.
func NewYAMLFileSources(paths []string) (sources []*YAMLFileSource) {
for _, path := range paths {
source := NewYAMLFileSource(path)
sources = append(sources, source)
}
return sources
}
// Name of the Source.
func (s YAMLFileSource) Name() (name string) {
return fmt.Sprintf("yaml file(%s)", s.path)
}
// Merge the YAMLFileSource koanf.Koanf into the provided one.
func (s *YAMLFileSource) Merge(ko *koanf.Koanf, _ *schema.StructValidator) (err error) {
return ko.Merge(s.koanf)
}
// Load the Source into the YAMLFileSource koanf.Koanf.
func (s *YAMLFileSource) Load(_ *schema.StructValidator) (err error) {
if s.path == "" {
return errors.New("invalid yaml path source configuration")
}
return s.koanf.Load(file.Provider(s.path), yaml.Parser())
}
// NewEnvironmentSource returns a Source configured to load from environment variables.
func NewEnvironmentSource(prefix, delimiter string) (source *EnvironmentSource) {
return &EnvironmentSource{
koanf: koanf.New(constDelimiter),
prefix: prefix,
delimiter: delimiter,
}
}
// Name of the Source.
func (s EnvironmentSource) Name() (name string) {
return "environment"
}
// Merge the EnvironmentSource koanf.Koanf into the provided one.
func (s *EnvironmentSource) Merge(ko *koanf.Koanf, _ *schema.StructValidator) (err error) {
return ko.Merge(s.koanf)
}
// Load the Source into the EnvironmentSource koanf.Koanf.
func (s *EnvironmentSource) Load(_ *schema.StructValidator) (err error) {
keyMap, ignoredKeys := getEnvConfigMap(validator.ValidKeys, s.prefix, s.delimiter)
return s.koanf.Load(env.ProviderWithValue(s.prefix, constDelimiter, koanfEnvironmentCallback(keyMap, ignoredKeys, s.prefix, s.delimiter)), nil)
}
// NewSecretsSource returns a Source configured to load from secrets.
func NewSecretsSource(prefix, delimiter string) (source *SecretsSource) {
return &SecretsSource{
koanf: koanf.New(constDelimiter),
prefix: prefix,
delimiter: delimiter,
}
}
// Name of the Source.
func (s SecretsSource) Name() (name string) {
return "secrets"
}
// Merge the SecretsSource koanf.Koanf into the provided one.
func (s *SecretsSource) Merge(ko *koanf.Koanf, val *schema.StructValidator) (err error) {
for _, key := range s.koanf.Keys() {
value, ok := ko.Get(key).(string)
if ok && value != "" {
val.Push(fmt.Errorf(errFmtSecretAlreadyDefined, key))
}
}
return ko.Merge(s.koanf)
}
// Load the Source into the SecretsSource koanf.Koanf.
func (s *SecretsSource) Load(val *schema.StructValidator) (err error) {
keyMap := getSecretConfigMap(validator.ValidKeys, s.prefix, s.delimiter)
return s.koanf.Load(env.ProviderWithValue(s.prefix, constDelimiter, koanfEnvironmentSecretsCallback(keyMap, val)), nil)
}
// NewDefaultSources returns a slice of Source configured to load from specified YAML files.
func NewDefaultSources(filePaths []string, prefix, delimiter string) (sources []Source) {
fileSources := NewYAMLFileSources(filePaths)
for _, source := range fileSources {
sources = append(sources, source)
}
sources = append(sources, NewEnvironmentSource(prefix, delimiter))
sources = append(sources, NewSecretsSource(prefix, delimiter))
return sources
}

View File

@ -0,0 +1,31 @@
package configuration
import (
_ "embed"
"fmt"
"io/ioutil"
"os"
)
//go:embed config.template.yml
var template []byte
// EnsureConfigurationExists is an auxiliary function to the main Configuration tools that ensures the Configuration
// template is created if it doesn't already exist.
func EnsureConfigurationExists(path string) (created bool, err error) {
_, err = os.Stat(path)
if err != nil {
if os.IsNotExist(err) {
err := ioutil.WriteFile(path, template, 0600)
if err != nil {
return false, fmt.Errorf(errFmtGenerateConfiguration, err)
}
return true, nil
}
return false, fmt.Errorf(errFmtGenerateConfiguration, err)
}
return false, nil
}

View File

@ -0,0 +1,59 @@
package configuration
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"runtime"
"testing"
"github.com/stretchr/testify/assert"
"github.com/authelia/authelia/internal/utils"
)
func TestShouldGenerateConfiguration(t *testing.T) {
dir, err := ioutil.TempDir("", "authelia-config")
assert.NoError(t, err)
cfg := filepath.Join(dir, "config.yml")
created, err := EnsureConfigurationExists(cfg)
assert.NoError(t, err)
assert.True(t, created)
_, err = os.Stat(cfg)
assert.NoError(t, err)
}
func TestShouldNotGenerateConfigurationOnFSAccessDenied(t *testing.T) {
if runtime.GOOS == constWindows {
t.Skip("skipping test due to being on windows")
}
dir, err := ioutil.TempDir("", "authelia-config")
assert.NoError(t, err)
assert.NoError(t, os.Mkdir(filepath.Join(dir, "zero"), 0000))
cfg := filepath.Join(dir, "zero", "config.yml")
created, err := EnsureConfigurationExists(cfg)
assert.EqualError(t, err, fmt.Sprintf("error occurred generating configuration: stat %s: permission denied", cfg))
assert.False(t, created)
}
func TestShouldNotGenerateConfiguration(t *testing.T) {
dir, err := ioutil.TempDir("", "authelia-config")
assert.NoError(t, err)
cfg := filepath.Join(dir, "..", "not-a-dir", "config.yml")
created, err := EnsureConfigurationExists(cfg)
expectedErr := fmt.Sprintf(utils.GetExpectedErrTxt("pathnotfound"), cfg)
assert.EqualError(t, err, fmt.Sprintf(errFmtGenerateConfiguration, expectedErr))
assert.False(t, created)
}

View File

@ -0,0 +1 @@
example_secret value

View File

@ -0,0 +1,34 @@
package configuration
import (
"github.com/knadh/koanf"
"github.com/authelia/authelia/internal/configuration/schema"
)
// Source is an abstract representation of a configuration Source implementation.
type Source interface {
Name() (name string)
Merge(ko *koanf.Koanf, val *schema.StructValidator) (err error)
Load(val *schema.StructValidator) (err error)
}
// YAMLFileSource is a configuration Source with a YAML File.
type YAMLFileSource struct {
koanf *koanf.Koanf
path string
}
// EnvironmentSource is a configuration Source which loads values from the environment.
type EnvironmentSource struct {
koanf *koanf.Koanf
prefix string
delimiter string
}
// SecretsSource loads environment variables that have a value pointing to a file.
type SecretsSource struct {
koanf *koanf.Koanf
prefix string
delimiter string
}

View File

@ -12,7 +12,7 @@ import (
// IsPolicyValid check if policy is valid.
func IsPolicyValid(policy string) (isValid bool) {
return policy == denyPolicy || policy == oneFactorPolicy || policy == twoFactorPolicy || policy == bypassPolicy
return policy == policyDeny || policy == policyOneFactor || policy == policyTwoFactor || policy == policyBypass
}
// IsResourceValid check if a resource is valid.
@ -52,7 +52,7 @@ func IsNetworkValid(network string) (isValid bool) {
// ValidateAccessControl validates access control configuration.
func ValidateAccessControl(configuration *schema.AccessControlConfiguration, validator *schema.StructValidator) {
if configuration.DefaultPolicy == "" {
configuration.DefaultPolicy = denyPolicy
configuration.DefaultPolicy = policyDeny
}
if !IsPolicyValid(configuration.DefaultPolicy) {
@ -73,7 +73,7 @@ func ValidateAccessControl(configuration *schema.AccessControlConfiguration, val
// ValidateRules validates an ACL Rule configuration.
func ValidateRules(configuration schema.AccessControlConfiguration, validator *schema.StructValidator) {
if configuration.Rules == nil || len(configuration.Rules) == 0 {
if configuration.DefaultPolicy != oneFactorPolicy && configuration.DefaultPolicy != twoFactorPolicy {
if configuration.DefaultPolicy != policyOneFactor && configuration.DefaultPolicy != policyTwoFactor {
validator.Push(fmt.Errorf("Default Policy [%s] is invalid, access control rules must be provided or a policy must either be 'one_factor' or 'two_factor'", configuration.DefaultPolicy))
return
@ -103,7 +103,7 @@ func ValidateRules(configuration schema.AccessControlConfiguration, validator *s
validateMethods(rulePosition, rule, validator)
if rule.Policy == bypassPolicy && len(rule.Subjects) != 0 {
if rule.Policy == policyBypass && len(rule.Subjects) != 0 {
validator.Push(fmt.Errorf(errAccessControlInvalidPolicyWithSubjects, rulePosition, rule.Domains, rule.Subjects))
}
}

View File

@ -18,7 +18,7 @@ type AccessControl struct {
func (suite *AccessControl) SetupTest() {
suite.validator = schema.NewStructValidator()
suite.configuration.DefaultPolicy = denyPolicy
suite.configuration.DefaultPolicy = policyDeny
suite.configuration.Networks = schema.DefaultACLNetwork
suite.configuration.Rules = schema.DefaultACLRule
}
@ -71,7 +71,7 @@ func (suite *AccessControl) TestShouldRaiseErrorWithNoRulesDefined() {
func (suite *AccessControl) TestShouldRaiseWarningWithNoRulesDefined() {
suite.configuration.Rules = []schema.ACLRule{}
suite.configuration.DefaultPolicy = twoFactorPolicy
suite.configuration.DefaultPolicy = policyTwoFactor
ValidateRules(suite.configuration, suite.validator)

View File

@ -10,7 +10,7 @@ import (
"github.com/authelia/authelia/internal/utils"
)
// ValidateAuthenticationBackend validates and update authentication backend configuration.
// ValidateAuthenticationBackend validates and updates the authentication backend configuration.
func ValidateAuthenticationBackend(configuration *schema.AuthenticationBackendConfiguration, validator *schema.StructValidator) {
if configuration.LDAP == nil && configuration.File == nil {
validator.Push(errors.New("Please provide `ldap` or `file` object in `authentication_backend`"))
@ -36,7 +36,7 @@ func ValidateAuthenticationBackend(configuration *schema.AuthenticationBackendCo
}
}
//nolint:gocyclo // TODO: Consider refactoring/simplifying, time permitting.
// validateFileAuthenticationBackend validates and updates the file authentication backend configuration.
func validateFileAuthenticationBackend(configuration *schema.FileAuthenticationBackendConfiguration, validator *schema.StructValidator) {
if configuration.Path == "" {
validator.Push(errors.New("Please provide a `path` for the users database in `authentication_backend`"))
@ -45,26 +45,6 @@ func validateFileAuthenticationBackend(configuration *schema.FileAuthenticationB
if configuration.Password == nil {
configuration.Password = &schema.DefaultPasswordConfiguration
} else {
if configuration.Password.Algorithm == "" {
configuration.Password.Algorithm = schema.DefaultPasswordConfiguration.Algorithm
} else {
configuration.Password.Algorithm = strings.ToLower(configuration.Password.Algorithm)
if configuration.Password.Algorithm != argon2id && configuration.Password.Algorithm != sha512 {
validator.Push(fmt.Errorf("Unknown hashing algorithm supplied, valid values are argon2id and sha512, you configured '%s'", configuration.Password.Algorithm))
}
}
// Iterations (time)
if configuration.Password.Iterations == 0 {
if configuration.Password.Algorithm == argon2id {
configuration.Password.Iterations = schema.DefaultPasswordConfiguration.Iterations
} else {
configuration.Password.Iterations = schema.DefaultPasswordSHA512Configuration.Iterations
}
} else if configuration.Password.Iterations < 1 {
validator.Push(fmt.Errorf("The number of iterations specified is invalid, must be 1 or more, you configured %d", configuration.Password.Iterations))
}
// Salt Length
switch {
case configuration.Password.SaltLength == 0:
@ -73,7 +53,36 @@ func validateFileAuthenticationBackend(configuration *schema.FileAuthenticationB
validator.Push(fmt.Errorf("The salt length must be 2 or more, you configured %d", configuration.Password.SaltLength))
}
if configuration.Password.Algorithm == argon2id {
switch configuration.Password.Algorithm {
case "":
configuration.Password.Algorithm = schema.DefaultPasswordConfiguration.Algorithm
fallthrough
case hashArgon2id:
validateFileAuthenticationBackendArgon2id(configuration, validator)
case hashSHA512:
validateFileAuthenticationBackendSHA512(configuration)
default:
validator.Push(fmt.Errorf("Unknown hashing algorithm supplied, valid values are argon2id and sha512, you configured '%s'", configuration.Password.Algorithm))
}
if configuration.Password.Iterations < 1 {
validator.Push(fmt.Errorf("The number of iterations specified is invalid, must be 1 or more, you configured %d", configuration.Password.Iterations))
}
}
}
func validateFileAuthenticationBackendSHA512(configuration *schema.FileAuthenticationBackendConfiguration) {
// Iterations (time)
if configuration.Password.Iterations == 0 {
configuration.Password.Iterations = schema.DefaultPasswordSHA512Configuration.Iterations
}
}
func validateFileAuthenticationBackendArgon2id(configuration *schema.FileAuthenticationBackendConfiguration, validator *schema.StructValidator) {
// Iterations (time)
if configuration.Password.Iterations == 0 {
configuration.Password.Iterations = schema.DefaultPasswordConfiguration.Iterations
}
// Parallelism
if configuration.Password.Parallelism == 0 {
configuration.Password.Parallelism = schema.DefaultPasswordConfiguration.Parallelism
@ -95,8 +104,6 @@ func validateFileAuthenticationBackend(configuration *schema.FileAuthenticationB
validator.Push(fmt.Errorf("Key length for argon2id must be 16, you configured %d", configuration.Password.KeyLength))
}
}
}
}
func validateLDAPAuthenticationBackend(configuration *schema.LDAPAuthenticationBackendConfiguration, validator *schema.StructValidator) {
if configuration.Implementation == "" {

View File

@ -97,7 +97,7 @@ func (suite *FileBasedAuthenticationBackend) TestShouldSetDefaultConfigurationWh
ValidateAuthenticationBackend(&suite.configuration, suite.validator)
suite.Assert().False(suite.validator.HasWarnings())
suite.Assert().False(suite.validator.HasErrors())
suite.Assert().Len(suite.validator.Errors(), 0)
suite.Assert().Equal(schema.DefaultPasswordConfiguration.KeyLength, suite.configuration.File.Password.KeyLength)
suite.Assert().Equal(schema.DefaultPasswordConfiguration.Iterations, suite.configuration.File.Password.Iterations)
@ -115,7 +115,7 @@ func (suite *FileBasedAuthenticationBackend) TestShouldSetDefaultConfigurationWh
ValidateAuthenticationBackend(&suite.configuration, suite.validator)
suite.Assert().False(suite.validator.HasWarnings())
suite.Assert().False(suite.validator.HasErrors())
suite.Assert().Len(suite.validator.Errors(), 0)
suite.Assert().Equal(schema.DefaultPasswordSHA512Configuration.KeyLength, suite.configuration.File.Password.KeyLength)
suite.Assert().Equal(schema.DefaultPasswordSHA512Configuration.Iterations, suite.configuration.File.Password.Iterations)

View File

@ -14,8 +14,8 @@ func newDefaultConfig() schema.Configuration {
config := schema.Configuration{}
config.Server.Host = loopback
config.Server.Port = 9090
config.Logging.Level = "info"
config.Logging.Format = "text"
config.Log.Level = "info"
config.Log.Format = "text"
config.JWTSecret = testJWTSecret
config.AuthenticationBackend.File = &schema.FileAuthenticationBackendConfiguration{
Path: "/a/path",
@ -81,7 +81,10 @@ func TestShouldRaiseErrorWithUndefinedJWTSecretKey(t *testing.T) {
ValidateConfiguration(&config, validator)
require.Len(t, validator.Errors(), 1)
require.Len(t, validator.Warnings(), 1)
assert.EqualError(t, validator.Errors()[0], "Provide a JWT secret using \"jwt_secret\" key")
assert.EqualError(t, validator.Warnings()[0], "No access control rules have been defined so the default policy two_factor will be applied to all requests")
}
func TestShouldRaiseErrorWithBadDefaultRedirectionURL(t *testing.T) {
@ -91,16 +94,24 @@ func TestShouldRaiseErrorWithBadDefaultRedirectionURL(t *testing.T) {
ValidateConfiguration(&config, validator)
require.Len(t, validator.Errors(), 1)
require.Len(t, validator.Warnings(), 1)
assert.EqualError(t, validator.Errors()[0], "Value for \"default_redirection_url\" is invalid: the url 'bad_default_redirection_url' is not absolute because it doesn't start with a scheme like 'http://' or 'https://'")
assert.EqualError(t, validator.Warnings()[0], "No access control rules have been defined so the default policy two_factor will be applied to all requests")
}
func TestShouldNotOverrideCertificatesDirectoryAndShouldPassWhenBlank(t *testing.T) {
validator := schema.NewStructValidator()
config := newDefaultConfig()
ValidateConfiguration(&config, validator)
require.Len(t, validator.Errors(), 0)
assert.Len(t, validator.Errors(), 0)
require.Len(t, validator.Warnings(), 1)
require.Equal(t, "", config.CertificatesDirectory)
assert.EqualError(t, validator.Warnings()[0], "No access control rules have been defined so the default policy two_factor will be applied to all requests")
}
func TestShouldRaiseErrorOnInvalidCertificatesDirectory(t *testing.T) {
@ -111,6 +122,7 @@ func TestShouldRaiseErrorOnInvalidCertificatesDirectory(t *testing.T) {
ValidateConfiguration(&config, validator)
require.Len(t, validator.Errors(), 1)
require.Len(t, validator.Warnings(), 1)
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.")
@ -118,12 +130,18 @@ func TestShouldRaiseErrorOnInvalidCertificatesDirectory(t *testing.T) {
assert.EqualError(t, validator.Errors()[0], "Error checking certificate directory: stat not-a-real-file.go: no such file or directory")
}
assert.EqualError(t, validator.Warnings()[0], "No access control rules have been defined so the default policy two_factor will be applied to all requests")
validator = schema.NewStructValidator()
config.CertificatesDirectory = "const.go"
ValidateConfiguration(&config, validator)
require.Len(t, validator.Errors(), 1)
require.Len(t, validator.Warnings(), 1)
assert.EqualError(t, validator.Errors()[0], "The path const.go specified for certificate_directory is not a directory")
assert.EqualError(t, validator.Warnings()[0], "No access control rules have been defined so the default policy two_factor will be applied to all requests")
}
func TestShouldNotRaiseErrorOnValidCertificatesDirectory(t *testing.T) {
@ -133,5 +151,8 @@ func TestShouldNotRaiseErrorOnValidCertificatesDirectory(t *testing.T) {
ValidateConfiguration(&config, validator)
require.Len(t, validator.Errors(), 0)
assert.Len(t, validator.Errors(), 0)
require.Len(t, validator.Warnings(), 1)
assert.EqualError(t, validator.Warnings()[0], "No access control rules have been defined so the default policy two_factor will be applied to all requests")
}

View File

@ -1,65 +1,36 @@
package validator
import "regexp"
const (
loopback = "127.0.0.1"
oauth2InstalledApp = "urn:ietf:wg:oauth:2.0:oob"
)
// Policy constants.
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'"
policyBypass = "bypass"
policyOneFactor = "one_factor"
policyTwoFactor = "two_factor"
policyDeny = "deny"
)
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"
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"
errFmtOIDCServerClientRedirectURI = "OIDC client with ID '%s' redirect URI %s has an invalid scheme '%s', " +
"should be http or https"
errFmtOIDCClientRedirectURIPublic = "openid connect provider: client with ID '%s' redirect URI '%s' is " +
"only valid for the public client type, not the confidential client type"
errFmtOIDCClientRedirectURIAbsolute = "openid connect provider: client with ID '%s' redirect URI '%s' is invalid " +
"because it has no scheme when it should be http or https"
errFmtOIDCServerClientRedirectURICantBeParsed = "OIDC client with ID '%s' has an invalid redirect URI '%s' " +
"could not be parsed: %v"
errFmtOIDCServerClientInvalidPolicy = "OIDC client with ID '%s' has an invalid policy '%s', " +
"should be either 'one_factor' or 'two_factor'"
errFmtOIDCServerClientInvalidSecret = "OIDC client with ID '%s' has an empty secret" //nolint:gosec
errFmtOIDCClientPublicInvalidSecret = "openid connect provider: client with ID '%s' is public but does not have an empty secret" //nolint:gosec
errFmtOIDCServerClientInvalidScope = "OIDC client with ID '%s' has an invalid scope '%s', " +
"must be one of: '%s'"
errFmtOIDCServerClientInvalidGrantType = "OIDC client with ID '%s' has an invalid grant type '%s', " +
"must be one of: '%s'"
errFmtOIDCServerClientInvalidResponseMode = "OIDC client with ID '%s' has an invalid response mode '%s', " +
"must be one of: '%s'"
errFmtOIDCServerClientInvalidUserinfoAlgorithm = "OIDC client with ID '%s' has an invalid userinfo signing " +
"algorithm '%s', must be one of: '%s'"
errFmtOIDCServerInsecureParameterEntropy = "SECURITY ISSUE: OIDC minimum parameter entropy is configured to an " +
"unsafe value, it should be above 8 but it's configured to %d."
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"
bypassPolicy = "bypass"
oneFactorPolicy = "one_factor"
twoFactorPolicy = "two_factor"
denyPolicy = "deny"
argon2id = "argon2id"
sha512 = "sha512"
// Hashing constants.
const (
hashArgon2id = "argon2id"
hashSHA512 = "sha512"
)
// Scheme constants.
const (
schemeLDAP = "ldap"
schemeLDAPS = "ldaps"
schemeHTTP = "http"
schemeHTTPS = "https"
)
// Test constants.
const (
testBadTimer = "-1"
testInvalidPolicy = "invalid"
testJWTSecret = "a_secret"
@ -70,9 +41,58 @@ const (
testModeDisabled = "disable"
testTLSCert = "/tmp/cert.pem"
testTLSKey = "/tmp/key.pem"
)
errAccessControlInvalidPolicyWithSubjects = "Policy [bypass] for rule #%d domain %s with subjects %s is invalid. " +
"It is not supported to configure both policy bypass and subjects. For more information see: " +
// OpenID Error constants.
const (
errFmtOIDCClientsDuplicateID = "openid connect provider: one or more clients have the same ID"
errFmtOIDCClientsWithEmptyID = "openid connect provider: one or more clients have been configured with an empty ID"
errFmtOIDCNoClientsConfigured = "openid connect provider: no clients are configured"
errFmtOIDCNoPrivateKey = "openid connect provider: issuer private key must be provided"
errFmtOIDCClientInvalidSecret = "openid connect provider: client with ID '%s' has an empty secret"
errFmtOIDCClientPublicInvalidSecret = "openid connect provider: client with ID '%s' is public but does not have " +
"an empty secret"
errFmtOIDCClientRedirectURI = "openid connect provider: client with ID '%s' redirect URI %s has an " +
"invalid scheme %s, should be http or https"
errFmtOIDCClientRedirectURICantBeParsed = "openid connect provider: client with ID '%s' has an invalid redirect " +
"URI '%s' could not be parsed: %v"
errFmtOIDCClientRedirectURIPublic = "openid connect provider: client with ID '%s' redirect URI '%s' is " +
"only valid for the public client type, not the confidential client type"
errFmtOIDCClientRedirectURIAbsolute = "openid connect provider: client with ID '%s' redirect URI '%s' is invalid " +
"because it has no scheme when it should be http or https"
errFmtOIDCClientInvalidPolicy = "openid connect provider: client with ID '%s' has an invalid policy " +
"'%s', should be either 'one_factor' or 'two_factor'"
errFmtOIDCClientInvalidScope = "openid connect provider: client with ID '%s' has an invalid scope " +
"'%s', must be one of: '%s'"
errFmtOIDCClientInvalidGrantType = "openid connect provider: client with ID '%s' has an invalid grant type " +
"'%s', must be one of: '%s'"
errFmtOIDCClientInvalidResponseMode = "openid connect provider: client with ID '%s' has an invalid response mode " +
"'%s', must be one of: '%s'"
errFmtOIDCClientInvalidUserinfoAlgorithm = "openid connect provider: client with ID '%s' has an invalid userinfo signing " +
"algorithm '%s', must be one of: '%s'"
errFmtOIDCServerInsecureParameterEntropy = "openid connect provider: SECURITY ISSUE - minimum parameter entropy is " +
"configured to an unsafe value, it should be above 8 but it's configured to %d"
)
// Error constants.
const (
errFmtDeprecatedConfigurationKey = "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"
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"
errAccessControlInvalidPolicyWithSubjects = "policy [bypass] for rule #%d 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"
)
@ -84,28 +104,16 @@ var validOIDCGrantTypes = []string{"implicit", "refresh_token", "authorization_c
var validOIDCResponseModes = []string{"form_post", "query", "fragment"}
var validOIDCUserinfoAlgorithms = []string{"none", "RS256"}
// 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",
"OpenIDConnectHMACSecret": "identity_providers.oidc.hmac_secret",
"OpenIDConnectIssuerPrivateKey": "identity_providers.oidc.issuer_private_key",
}
var reKeyReplacer = regexp.MustCompile(`\[\d+]`)
// validKeys is a list of valid keys that are not secret names. For the sake of consistency please place any secret in
// 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.
"certificates_directory",
"theme",
"default_redirection_url",
"jwt_secret",
// Log keys.
"log.level",
@ -139,14 +147,26 @@ var validKeys = []string{
"totp.period",
"totp.skew",
// DUO API Keys.
"duo_api.hostname",
"duo_api.secret_key",
"duo_api.integration_key",
// Access Control Keys.
"access_control.rules",
"access_control.default_policy",
"access_control.networks",
"access_control.rules",
"access_control.rules[].domain",
"access_control.rules[].methods",
"access_control.rules[].networks",
"access_control.rules[].subject",
"access_control.rules[].policy",
"access_control.rules[].resources",
// Session Keys.
"session.name",
"session.domain",
"session.secret",
"session.same_site",
"session.expiration",
"session.inactivity",
@ -156,6 +176,7 @@ var validKeys = []string{
"session.redis.host",
"session.redis.port",
"session.redis.username",
"session.redis.password",
"session.redis.database_index",
"session.redis.maximum_active_connections",
"session.redis.minimum_idle_connections",
@ -163,6 +184,7 @@ var validKeys = []string{
"session.redis.tls.skip_verify",
"session.redis.tls.server_name",
"session.redis.high_availability.sentinel_name",
"session.redis.high_availability.sentinel_password",
"session.redis.high_availability.nodes",
"session.redis.high_availability.route_by_latency",
"session.redis.high_availability.route_randomly",
@ -180,12 +202,14 @@ var validKeys = []string{
"storage.mysql.port",
"storage.mysql.database",
"storage.mysql.username",
"storage.mysql.password",
// PostgreSQL Storage Keys.
"storage.postgres.host",
"storage.postgres.port",
"storage.postgres.database",
"storage.postgres.username",
"storage.postgres.password",
"storage.postgres.sslmode",
// FileSystem Notifier Keys.
@ -193,9 +217,10 @@ var validKeys = []string{
"notifier.disable_startup_check",
// SMTP Notifier Keys.
"notifier.smtp.username",
"notifier.smtp.host",
"notifier.smtp.port",
"notifier.smtp.username",
"notifier.smtp.password",
"notifier.smtp.identifier",
"notifier.smtp.sender",
"notifier.smtp.subject",
@ -211,10 +236,6 @@ var validKeys = []string{
"regulation.find_time",
"regulation.ban_time",
// DUO API Keys.
"duo_api.hostname",
"duo_api.integration_key",
// Authentication Backend Keys.
"authentication_backend.disable_reset_password",
"authentication_backend.refresh_interval",
@ -232,6 +253,7 @@ var validKeys = []string{
"authentication_backend.ldap.mail_attribute",
"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",
@ -247,12 +269,22 @@ var validKeys = []string{
"authentication_backend.file.password.parallelism",
// Identity Provider Keys.
"identity_providers.oidc.clients",
"identity_providers.oidc.hmac_secret",
"identity_providers.oidc.issuer_private_key",
"identity_providers.oidc.id_token_lifespan",
"identity_providers.oidc.access_token_lifespan",
"identity_providers.oidc.refresh_token_lifespan",
"identity_providers.oidc.authorize_code_lifespan",
"identity_providers.oidc.enable_client_debug_messages",
"identity_providers.oidc.clients",
"identity_providers.oidc.clients[].id",
"identity_providers.oidc.clients[].description",
"identity_providers.oidc.clients[].secret",
"identity_providers.oidc.clients[].redirect_uris",
"identity_providers.oidc.clients[].authorization_policy",
"identity_providers.oidc.clients[].scopes",
"identity_providers.oidc.clients[].grant_types",
"identity_providers.oidc.clients[].response_types",
}
var replacedKeys = map[string]string{
@ -265,8 +297,8 @@ var replacedKeys = map[string]string{
var specificErrorKeys = map[string]string{
"google_analytics": "config key removed: google_analytics - this functionality has been deprecated",
"notifier.smtp.trusted_cert": "invalid configuration key `notifier.smtp.trusted_cert` it has been removed, " +
"option has been replaced by the global option `certificates_directory`",
"notifier.smtp.trusted_cert": "invalid configuration key 'notifier.smtp.trusted_cert' it has been removed, " +
"option has been replaced by the global option 'certificates_directory'",
"authentication_backend.file.password_options.algorithm": errFilePOptions,
"authentication_backend.file.password_options.iterations": errFilePOptions,

View File

@ -18,7 +18,7 @@ func ValidateIdentityProviders(configuration *schema.IdentityProvidersConfigurat
func validateOIDC(configuration *schema.OpenIDConnectConfiguration, validator *schema.StructValidator) {
if configuration != nil {
if configuration.IssuerPrivateKey == "" {
validator.Push(fmt.Errorf("OIDC Server issuer private key must be provided"))
validator.Push(fmt.Errorf(errFmtOIDCNoPrivateKey))
}
if configuration.AccessTokenLifespan == time.Duration(0) {
@ -44,7 +44,7 @@ func validateOIDC(configuration *schema.OpenIDConnectConfiguration, validator *s
validateOIDCClients(configuration, validator)
if len(configuration.Clients) == 0 {
validator.Push(fmt.Errorf("OIDC Server has no clients defined"))
validator.Push(fmt.Errorf(errFmtOIDCNoClientsConfigured))
}
}
}
@ -74,14 +74,14 @@ func validateOIDCClients(configuration *schema.OpenIDConnectConfiguration, valid
}
} else {
if client.Secret == "" {
validator.Push(fmt.Errorf(errFmtOIDCServerClientInvalidSecret, client.ID))
validator.Push(fmt.Errorf(errFmtOIDCClientInvalidSecret, client.ID))
}
}
if client.Policy == "" {
configuration.Clients[c].Policy = schema.DefaultOpenIDConnectClientConfiguration.Policy
} else if client.Policy != oneFactorPolicy && client.Policy != twoFactorPolicy {
validator.Push(fmt.Errorf(errFmtOIDCServerClientInvalidPolicy, client.ID, client.Policy))
} else if client.Policy != policyOneFactor && client.Policy != policyTwoFactor {
validator.Push(fmt.Errorf(errFmtOIDCClientInvalidPolicy, client.ID, client.Policy))
}
validateOIDCClientScopes(c, configuration, validator)
@ -94,11 +94,11 @@ func validateOIDCClients(configuration *schema.OpenIDConnectConfiguration, valid
}
if invalidID {
validator.Push(fmt.Errorf("OIDC Server has one or more clients with an empty ID"))
validator.Push(fmt.Errorf(errFmtOIDCClientsWithEmptyID))
}
if duplicateIDs {
validator.Push(fmt.Errorf("OIDC Server has clients with duplicate ID's"))
validator.Push(fmt.Errorf(errFmtOIDCClientsDuplicateID))
}
}
@ -115,7 +115,7 @@ func validateOIDCClientScopes(c int, configuration *schema.OpenIDConnectConfigur
for _, scope := range configuration.Clients[c].Scopes {
if !utils.IsStringInSlice(scope, validOIDCScopes) {
validator.Push(fmt.Errorf(
errFmtOIDCServerClientInvalidScope,
errFmtOIDCClientInvalidScope,
configuration.Clients[c].ID, scope, strings.Join(validOIDCScopes, "', '")))
}
}
@ -130,7 +130,7 @@ func validateOIDCClientGrantTypes(c int, configuration *schema.OpenIDConnectConf
for _, grantType := range configuration.Clients[c].GrantTypes {
if !utils.IsStringInSlice(grantType, validOIDCGrantTypes) {
validator.Push(fmt.Errorf(
errFmtOIDCServerClientInvalidGrantType,
errFmtOIDCClientInvalidGrantType,
configuration.Clients[c].ID, grantType, strings.Join(validOIDCGrantTypes, "', '")))
}
}
@ -152,7 +152,7 @@ func validateOIDCClientResponseModes(c int, configuration *schema.OpenIDConnectC
for _, responseMode := range configuration.Clients[c].ResponseModes {
if !utils.IsStringInSlice(responseMode, validOIDCResponseModes) {
validator.Push(fmt.Errorf(
errFmtOIDCServerClientInvalidResponseMode,
errFmtOIDCClientInvalidResponseMode,
configuration.Clients[c].ID, responseMode, strings.Join(validOIDCResponseModes, "', '")))
}
}
@ -162,7 +162,7 @@ func validateOIDDClientUserinfoAlgorithm(c int, configuration *schema.OpenIDConn
if configuration.Clients[c].UserinfoSigningAlgorithm == "" {
configuration.Clients[c].UserinfoSigningAlgorithm = schema.DefaultOpenIDConnectClientConfiguration.UserinfoSigningAlgorithm
} else if !utils.IsStringInSlice(configuration.Clients[c].UserinfoSigningAlgorithm, validOIDCUserinfoAlgorithms) {
validator.Push(fmt.Errorf(errFmtOIDCServerClientInvalidUserinfoAlgorithm,
validator.Push(fmt.Errorf(errFmtOIDCClientInvalidUserinfoAlgorithm,
configuration.Clients[c].ID, configuration.Clients[c].UserinfoSigningAlgorithm, strings.Join(validOIDCUserinfoAlgorithms, ", ")))
}
}
@ -181,7 +181,7 @@ func validateOIDCClientRedirectURIs(client schema.OpenIDConnectClientConfigurati
parsedURL, err := url.Parse(redirectURI)
if err != nil {
validator.Push(fmt.Errorf(errFmtOIDCServerClientRedirectURICantBeParsed, client.ID, redirectURI, err))
validator.Push(fmt.Errorf(errFmtOIDCClientRedirectURICantBeParsed, client.ID, redirectURI, err))
continue
}
@ -191,7 +191,7 @@ func validateOIDCClientRedirectURIs(client schema.OpenIDConnectClientConfigurati
}
if parsedURL.Scheme != schemeHTTPS && parsedURL.Scheme != schemeHTTP {
validator.Push(fmt.Errorf(errFmtOIDCServerClientRedirectURI, client.ID, redirectURI, parsedURL.Scheme))
validator.Push(fmt.Errorf(errFmtOIDCClientRedirectURI, client.ID, redirectURI, parsedURL.Scheme))
}
}
}

View File

@ -25,8 +25,8 @@ func TestShouldRaiseErrorWhenInvalidOIDCServerConfiguration(t *testing.T) {
require.Len(t, validator.Errors(), 2)
assert.EqualError(t, validator.Errors()[0], "OIDC Server issuer private key must be provided")
assert.EqualError(t, validator.Errors()[1], "OIDC Server has no clients defined")
assert.EqualError(t, validator.Errors()[0], errFmtOIDCNoPrivateKey)
assert.EqualError(t, validator.Errors()[1], errFmtOIDCNoClientsConfigured)
}
func TestShouldRaiseErrorWhenOIDCServerIssuerPrivateKeyPathInvalid(t *testing.T) {
@ -42,7 +42,7 @@ func TestShouldRaiseErrorWhenOIDCServerIssuerPrivateKeyPathInvalid(t *testing.T)
require.Len(t, validator.Errors(), 1)
assert.EqualError(t, validator.Errors()[0], "OIDC Server has no clients defined")
assert.EqualError(t, validator.Errors()[0], errFmtOIDCNoClientsConfigured)
}
func TestShouldRaiseErrorWhenOIDCServerClientBadValues(t *testing.T) {
@ -79,7 +79,7 @@ func TestShouldRaiseErrorWhenOIDCServerClientBadValues(t *testing.T) {
{
ID: "client-check-uri-parse",
Secret: "a-secret",
Policy: twoFactorPolicy,
Policy: policyTwoFactor,
RedirectURIs: []string{
"http://abc@%two",
},
@ -87,7 +87,7 @@ func TestShouldRaiseErrorWhenOIDCServerClientBadValues(t *testing.T) {
{
ID: "client-check-uri-abs",
Secret: "a-secret",
Policy: twoFactorPolicy,
Policy: policyTwoFactor,
RedirectURIs: []string{
"google.com",
},
@ -101,14 +101,14 @@ func TestShouldRaiseErrorWhenOIDCServerClientBadValues(t *testing.T) {
require.Len(t, validator.Errors(), 8)
assert.Equal(t, schema.DefaultOpenIDConnectClientConfiguration.Policy, config.OIDC.Clients[0].Policy)
assert.EqualError(t, validator.Errors()[0], fmt.Sprintf(errFmtOIDCServerClientInvalidSecret, ""))
assert.EqualError(t, validator.Errors()[1], fmt.Sprintf(errFmtOIDCServerClientRedirectURI, "", "tcp://google.com", "tcp"))
assert.EqualError(t, validator.Errors()[2], fmt.Sprintf(errFmtOIDCServerClientInvalidPolicy, "a-client", "a-policy"))
assert.EqualError(t, validator.Errors()[3], fmt.Sprintf(errFmtOIDCServerClientInvalidPolicy, "a-client", "a-policy"))
assert.EqualError(t, validator.Errors()[4], fmt.Sprintf(errFmtOIDCServerClientRedirectURICantBeParsed, "client-check-uri-parse", "http://abc@%two", errors.New("parse \"http://abc@%two\": invalid URL escape \"%tw\"")))
assert.EqualError(t, validator.Errors()[0], fmt.Sprintf(errFmtOIDCClientInvalidSecret, ""))
assert.EqualError(t, validator.Errors()[1], fmt.Sprintf(errFmtOIDCClientRedirectURI, "", "tcp://google.com", "tcp"))
assert.EqualError(t, validator.Errors()[2], fmt.Sprintf(errFmtOIDCClientInvalidPolicy, "a-client", "a-policy"))
assert.EqualError(t, validator.Errors()[3], fmt.Sprintf(errFmtOIDCClientInvalidPolicy, "a-client", "a-policy"))
assert.EqualError(t, validator.Errors()[4], fmt.Sprintf(errFmtOIDCClientRedirectURICantBeParsed, "client-check-uri-parse", "http://abc@%two", errors.New("parse \"http://abc@%two\": invalid URL escape \"%tw\"")))
assert.EqualError(t, validator.Errors()[5], fmt.Sprintf(errFmtOIDCClientRedirectURIAbsolute, "client-check-uri-abs", "google.com"))
assert.EqualError(t, validator.Errors()[6], "OIDC Server has one or more clients with an empty ID")
assert.EqualError(t, validator.Errors()[7], "OIDC Server has clients with duplicate ID's")
assert.EqualError(t, validator.Errors()[6], errFmtOIDCClientsWithEmptyID)
assert.EqualError(t, validator.Errors()[7], errFmtOIDCClientsDuplicateID)
}
func TestShouldRaiseErrorWhenOIDCClientConfiguredWithBadScopes(t *testing.T) {
@ -134,7 +134,7 @@ func TestShouldRaiseErrorWhenOIDCClientConfiguredWithBadScopes(t *testing.T) {
ValidateIdentityProviders(config, validator)
require.Len(t, validator.Errors(), 1)
assert.EqualError(t, validator.Errors()[0], "OIDC client with ID 'good_id' has an invalid scope "+
assert.EqualError(t, validator.Errors()[0], "openid connect provider: client with ID 'good_id' has an invalid scope "+
"'bad_scope', must be one of: 'openid', 'email', 'profile', 'groups', 'offline_access'")
}
@ -161,7 +161,7 @@ func TestShouldRaiseErrorWhenOIDCClientConfiguredWithBadGrantTypes(t *testing.T)
ValidateIdentityProviders(config, validator)
require.Len(t, validator.Errors(), 1)
assert.EqualError(t, validator.Errors()[0], "OIDC client with ID 'good_id' has an invalid grant type "+
assert.EqualError(t, validator.Errors()[0], "openid connect provider: client with ID 'good_id' has an invalid grant type "+
"'bad_grant_type', must be one of: 'implicit', 'refresh_token', 'authorization_code', "+
"'password', 'client_credentials'")
}
@ -189,7 +189,7 @@ func TestShouldRaiseErrorWhenOIDCClientConfiguredWithBadResponseModes(t *testing
ValidateIdentityProviders(config, validator)
require.Len(t, validator.Errors(), 1)
assert.EqualError(t, validator.Errors()[0], "OIDC client with ID 'good_id' has an invalid response mode "+
assert.EqualError(t, validator.Errors()[0], "openid connect provider: client with ID 'good_id' has an invalid response mode "+
"'bad_responsemode', must be one of: 'form_post', 'query', 'fragment'")
}
@ -216,7 +216,7 @@ func TestShouldRaiseErrorWhenOIDCClientConfiguredWithBadUserinfoAlg(t *testing.T
ValidateIdentityProviders(config, validator)
require.Len(t, validator.Errors(), 1)
assert.EqualError(t, validator.Errors()[0], "OIDC client with ID 'good_id' has an invalid userinfo "+
assert.EqualError(t, validator.Errors()[0], "openid connect provider: client with ID 'good_id' has an invalid userinfo "+
"signing algorithm 'rs256', must be one of: 'none, RS256'")
}
@ -245,7 +245,7 @@ func TestValidateIdentityProvidersShouldRaiseWarningOnSecurityIssue(t *testing.T
assert.Len(t, validator.Errors(), 0)
require.Len(t, validator.Warnings(), 1)
assert.EqualError(t, validator.Warnings()[0], "SECURITY ISSUE: OIDC minimum parameter entropy is configured to an unsafe value, it should be above 8 but it's configured to 1.")
assert.EqualError(t, validator.Warnings()[0], "openid connect provider: SECURITY ISSUE - minimum parameter entropy is configured to an unsafe value, it should be above 8 but it's configured to 1")
}
func TestValidateIdentityProvidersShouldRaiseErrorsOnInvalidClientTypes(t *testing.T) {
@ -345,7 +345,7 @@ func TestValidateIdentityProvidersShouldSetDefaultValues(t *testing.T) {
ID: "b-client",
Description: "Normal Description",
Secret: "b-client-secret",
Policy: oneFactorPolicy,
Policy: policyOneFactor,
UserinfoSigningAlgorithm: "RS256",
RedirectURIs: []string{
"https://google.com",
@ -375,11 +375,11 @@ func TestValidateIdentityProvidersShouldSetDefaultValues(t *testing.T) {
assert.Len(t, validator.Errors(), 0)
// Assert Clients[0] Policy is set to the default, and the default doesn't override Clients[1]'s Policy.
assert.Equal(t, config.OIDC.Clients[0].Policy, twoFactorPolicy)
assert.Equal(t, config.OIDC.Clients[1].Policy, oneFactorPolicy)
assert.Equal(t, policyTwoFactor, config.OIDC.Clients[0].Policy)
assert.Equal(t, policyOneFactor, config.OIDC.Clients[1].Policy)
assert.Equal(t, config.OIDC.Clients[0].UserinfoSigningAlgorithm, "none")
assert.Equal(t, config.OIDC.Clients[1].UserinfoSigningAlgorithm, "RS256")
assert.Equal(t, "none", config.OIDC.Clients[0].UserinfoSigningAlgorithm)
assert.Equal(t, "RS256", config.OIDC.Clients[1].UserinfoSigningAlgorithm)
// Assert Clients[0] Description is set to the Clients[0] ID, and Clients[1]'s Description is not overridden.
assert.Equal(t, config.OIDC.Clients[0].ID, config.OIDC.Clients[0].Description)

View File

@ -3,35 +3,38 @@ package validator
import (
"errors"
"fmt"
"strings"
"github.com/authelia/authelia/internal/configuration/schema"
"github.com/authelia/authelia/internal/utils"
)
// ValidateKeys determines if a provided key is valid.
func ValidateKeys(validator *schema.StructValidator, keys []string) {
// ValidateKeys determines if all provided keys are valid.
func ValidateKeys(keys []string, prefix string, validator *schema.StructValidator) {
var errStrings []string
for _, key := range keys {
if utils.IsStringInSlice(key, validKeys) {
expectedKey := reKeyReplacer.ReplaceAllString(key, "[]")
if utils.IsStringInSlice(expectedKey, ValidKeys) {
continue
}
if isSecretKey(key) {
continue
}
if newKey, ok := replacedKeys[key]; ok {
if newKey, ok := replacedKeys[expectedKey]; ok {
validator.Push(fmt.Errorf(errFmtReplacedConfigurationKey, key, newKey))
continue
}
if err, ok := specificErrorKeys[key]; ok {
if err, ok := specificErrorKeys[expectedKey]; ok {
if !utils.IsStringInSlice(err, errStrings) {
errStrings = append(errStrings, err)
}
} else {
validator.Push(fmt.Errorf("config key not expected: %s", key))
if strings.HasPrefix(key, prefix) {
validator.PushWarning(fmt.Errorf("configuration environment variable not expected: %s", key))
} else {
validator.Push(fmt.Errorf("configuration key not expected: %s", key))
}
}
}

View File

@ -12,25 +12,41 @@ import (
)
func TestShouldValidateGoodKeys(t *testing.T) {
configKeys := validKeys
configKeys := ValidKeys
val := schema.NewStructValidator()
ValidateKeys(val, configKeys)
ValidateKeys(configKeys, "AUTHELIA_", val)
require.Len(t, val.Errors(), 0)
}
func TestShouldNotValidateBadKeys(t *testing.T) {
configKeys := validKeys
configKeys := ValidKeys
configKeys = append(configKeys, "bad_key")
configKeys = append(configKeys, "totp.skewy")
val := schema.NewStructValidator()
ValidateKeys(val, configKeys)
ValidateKeys(configKeys, "AUTHELIA_", val)
errs := val.Errors()
require.Len(t, errs, 2)
assert.EqualError(t, errs[0], "config key not expected: bad_key")
assert.EqualError(t, errs[1], "config key not expected: totp.skewy")
assert.EqualError(t, errs[0], "configuration key not expected: bad_key")
assert.EqualError(t, errs[1], "configuration key not expected: totp.skewy")
}
func TestShouldNotValidateBadEnvKeys(t *testing.T) {
configKeys := ValidKeys
configKeys = append(configKeys, "AUTHELIA__BAD_ENV_KEY")
configKeys = append(configKeys, "AUTHELIA_BAD_ENV_KEY")
val := schema.NewStructValidator()
ValidateKeys(configKeys, "AUTHELIA_", val)
warns := val.Warnings()
assert.Len(t, val.Errors(), 0)
require.Len(t, warns, 2)
assert.EqualError(t, warns[0], "configuration environment variable not expected: AUTHELIA__BAD_ENV_KEY")
assert.EqualError(t, warns[1], "configuration environment variable not expected: AUTHELIA_BAD_ENV_KEY")
}
func TestAllSpecificErrorKeys(t *testing.T) {
@ -48,7 +64,7 @@ func TestAllSpecificErrorKeys(t *testing.T) {
}
val := schema.NewStructValidator()
ValidateKeys(val, configKeys)
ValidateKeys(configKeys, "AUTHELIA_", val)
errs := val.Errors()
@ -72,7 +88,7 @@ func TestSpecificErrorKeys(t *testing.T) {
}
val := schema.NewStructValidator()
ValidateKeys(val, configKeys)
ValidateKeys(configKeys, "AUTHELIA_", val)
errs := val.Errors()
@ -95,7 +111,7 @@ func TestReplacedErrors(t *testing.T) {
}
val := schema.NewStructValidator()
ValidateKeys(val, configKeys)
ValidateKeys(configKeys, "AUTHELIA_", val)
warns := val.Warnings()
errs := val.Errors()
@ -109,18 +125,3 @@ func TestReplacedErrors(t *testing.T) {
assert.EqualError(t, errs[3], fmt.Sprintf(errFmtReplacedConfigurationKey, "logs_file_path", "log.file_path"))
assert.EqualError(t, errs[4], fmt.Sprintf(errFmtReplacedConfigurationKey, "logs_level", "log.level"))
}
func TestSecretKeysDontRaiseErrors(t *testing.T) {
configKeys := []string{}
for _, key := range SecretNames {
configKeys = append(configKeys, SecretNameToEnvName(key))
configKeys = append(configKeys, key)
}
val := schema.NewStructValidator()
ValidateKeys(val, configKeys)
assert.Len(t, val.Warnings(), 0)
assert.Len(t, val.Errors(), 0)
}

View File

@ -12,16 +12,16 @@ import (
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.Log.Level == "" {
configuration.Log.Level = schema.DefaultLoggingConfiguration.Level
}
if configuration.Logging.Format == "" {
configuration.Logging.Format = schema.DefaultLoggingConfiguration.Format
if configuration.Log.Format == "" {
configuration.Log.Format = schema.DefaultLoggingConfiguration.Format
}
if !utils.IsStringInSlice(configuration.Logging.Level, validLoggingLevels) {
validator.Push(fmt.Errorf(errFmtLoggingLevelInvalid, configuration.Logging.Level, strings.Join(validLoggingLevels, ", ")))
if !utils.IsStringInSlice(configuration.Log.Level, validLoggingLevels) {
validator.Push(fmt.Errorf(errFmtLoggingLevelInvalid, configuration.Log.Level, strings.Join(validLoggingLevels, ", ")))
}
}
@ -30,24 +30,24 @@ func applyDeprecatedLoggingConfiguration(configuration *schema.Configuration, va
if configuration.LogLevel != "" {
validator.PushWarning(fmt.Errorf(errFmtDeprecatedConfigurationKey, "log_level", "4.33.0", "log.level"))
if configuration.Logging.Level == "" {
configuration.Logging.Level = configuration.LogLevel
if configuration.Log.Level == "" {
configuration.Log.Level = configuration.LogLevel
}
}
if configuration.LogFormat != "" {
validator.PushWarning(fmt.Errorf(errFmtDeprecatedConfigurationKey, "log_format", "4.33.0", "log.format"))
if configuration.Logging.Format == "" {
configuration.Logging.Format = configuration.LogFormat
if configuration.Log.Format == "" {
configuration.Log.Format = configuration.LogFormat
}
}
if configuration.LogFilePath != "" {
validator.PushWarning(fmt.Errorf(errFmtDeprecatedConfigurationKey, "log_file_path", "4.33.0", "log.file_path"))
if configuration.Logging.FilePath == "" {
configuration.Logging.FilePath = configuration.LogFilePath
if configuration.Log.FilePath == "" {
configuration.Log.FilePath = configuration.LogFilePath
}
}
}

View File

@ -20,20 +20,20 @@ func TestShouldSetDefaultLoggingValues(t *testing.T) {
assert.Len(t, validator.Warnings(), 0)
assert.Len(t, validator.Errors(), 0)
require.NotNil(t, config.Logging.KeepStdout)
require.NotNil(t, config.Log.KeepStdout)
assert.Equal(t, "", config.LogLevel)
assert.Equal(t, "", config.LogFormat)
assert.Equal(t, "", config.LogFilePath)
assert.Equal(t, "", config.LogLevel) // TODO: DEPRECATED TEST. Remove in 4.33.0.
assert.Equal(t, "", config.LogFormat) // TODO: DEPRECATED TEST. Remove in 4.33.0.
assert.Equal(t, "", config.LogFilePath) // TODO: DEPRECATED TEST. Remove in 4.33.0.
assert.Equal(t, "info", config.Logging.Level)
assert.Equal(t, "text", config.Logging.Format)
assert.Equal(t, "", config.Logging.FilePath)
assert.Equal(t, "info", config.Log.Level)
assert.Equal(t, "text", config.Log.Format)
assert.Equal(t, "", config.Log.FilePath)
}
func TestShouldRaiseErrorOnInvalidLoggingLevel(t *testing.T) {
config := &schema.Configuration{
Logging: schema.LogConfiguration{
Log: schema.LogConfiguration{
Level: "TRACE",
},
}
@ -63,15 +63,15 @@ func TestShouldMigrateDeprecatedLoggingConfig(t *testing.T) {
assert.Len(t, validator.Errors(), 0)
require.Len(t, validator.Warnings(), 3)
require.NotNil(t, config.Logging.KeepStdout)
require.NotNil(t, config.Log.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.Equal(t, "trace", config.Log.Level)
assert.Equal(t, "json", config.Log.Format)
assert.Equal(t, "/a/b/c", config.Log.FilePath)
assert.EqualError(t, validator.Warnings()[0], fmt.Sprintf(errFmtDeprecatedConfigurationKey, "log_level", "4.33.0", "log.level"))
assert.EqualError(t, validator.Warnings()[1], fmt.Sprintf(errFmtDeprecatedConfigurationKey, "log_format", "4.33.0", "log.format"))
@ -80,7 +80,7 @@ func TestShouldMigrateDeprecatedLoggingConfig(t *testing.T) {
func TestShouldRaiseErrorsAndNotOverwriteConfigurationWhenUsingDeprecatedLoggingConfig(t *testing.T) {
config := &schema.Configuration{
Logging: schema.LogConfiguration{
Log: schema.LogConfiguration{
Level: "info",
Format: "text",
FilePath: "/x/y/z",
@ -95,12 +95,12 @@ func TestShouldRaiseErrorsAndNotOverwriteConfigurationWhenUsingDeprecatedLogging
ValidateLogging(config, validator)
require.NotNil(t, config.Logging.KeepStdout)
require.NotNil(t, config.Log.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.Equal(t, "info", config.Log.Level)
assert.Equal(t, "text", config.Log.Format)
assert.True(t, config.Log.KeepStdout)
assert.Equal(t, "/x/y/z", config.Log.FilePath)
assert.Len(t, validator.Errors(), 0)
require.Len(t, validator.Warnings(), 3)

View File

@ -1,89 +0,0 @@
package validator
import (
"fmt"
"io/ioutil"
"strings"
"github.com/spf13/viper"
"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.
func ValidateSecrets(configuration *schema.Configuration, validator *schema.StructValidator, viper *viper.Viper) {
configuration.JWTSecret = getSecretValue(SecretNames["JWTSecret"], validator, viper)
configuration.Session.Secret = getSecretValue(SecretNames["SessionSecret"], validator, viper)
if configuration.DuoAPI != nil {
configuration.DuoAPI.SecretKey = getSecretValue(SecretNames["DUOSecretKey"], validator, viper)
}
if configuration.Session.Redis != nil {
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 {
configuration.AuthenticationBackend.LDAP.Password = getSecretValue(SecretNames["LDAPPassword"], validator, viper)
}
if configuration.Notifier != nil && configuration.Notifier.SMTP != nil {
configuration.Notifier.SMTP.Password = getSecretValue(SecretNames["SMTPPassword"], validator, viper)
}
if configuration.Storage.MySQL != nil {
configuration.Storage.MySQL.Password = getSecretValue(SecretNames["MySQLPassword"], validator, viper)
}
if configuration.Storage.PostgreSQL != nil {
configuration.Storage.PostgreSQL.Password = getSecretValue(SecretNames["PostgreSQLPassword"], validator, viper)
}
if configuration.IdentityProviders.OIDC != nil {
configuration.IdentityProviders.OIDC.HMACSecret = getSecretValue(SecretNames["OpenIDConnectHMACSecret"], validator, viper)
configuration.IdentityProviders.OIDC.IssuerPrivateKey = getSecretValue(SecretNames["OpenIDConnectIssuerPrivateKey"], validator, viper)
}
}
func getSecretValue(name string, validator *schema.StructValidator, viper *viper.Viper) string {
configValue := viper.GetString(name)
fileEnvValue := viper.GetString(SecretNameToEnvName(name))
// Error Checking.
if fileEnvValue != "" && configValue != "" {
validator.Push(fmt.Errorf("error loading secret (%s): it's already defined in the config file", name))
}
// Derive Secret.
if fileEnvValue != "" {
content, err := ioutil.ReadFile(fileEnvValue)
if err != nil {
validator.Push(fmt.Errorf("error loading secret file (%s): %s", name, err))
} else {
// TODO: Test this functionality.
return strings.TrimRight(string(content), "\n")
}
}
return configValue
}

View File

@ -1,18 +0,0 @@
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

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

View File

@ -6,45 +6,44 @@ import (
logrus_stack "github.com/Gurpartap/logrus-stack"
"github.com/sirupsen/logrus"
"github.com/authelia/authelia/internal/configuration/schema"
)
// Logger return the standard logrus logger.
// Logger returns the standard logrus logger.
func Logger() *logrus.Logger {
return logrus.StandardLogger()
}
// SetLevel set the level of the logger.
func SetLevel(level logrus.Level) {
logrus.SetLevel(level)
}
// InitializeLogger configures the default loggers stack levels, formatting, and the output destinations.
func InitializeLogger(config schema.LogConfiguration, log bool) error {
setLevelStr(config.Level, log)
// InitializeLogger initialize logger.
func InitializeLogger(format, filename string, stdout bool) error {
callerLevels := []logrus.Level{}
stackLevels := []logrus.Level{logrus.PanicLevel, logrus.FatalLevel, logrus.ErrorLevel}
logrus.AddHook(logrus_stack.NewHook(callerLevels, stackLevels))
if format == logFormatJSON {
if config.Format == logFormatJSON {
logrus.SetFormatter(&logrus.JSONFormatter{})
} else {
logrus.SetFormatter(&logrus.TextFormatter{})
}
if filename != "" {
f, err := os.OpenFile(filename, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600)
if config.FilePath != "" {
f, err := os.OpenFile(config.FilePath, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600)
if err != nil {
return err
}
if format != logFormatJSON {
if config.Format != logFormatJSON {
logrus.SetFormatter(&logrus.TextFormatter{
DisableColors: true,
FullTimestamp: true,
})
}
if stdout {
if config.KeepStdout {
logLocations := io.MultiWriter(os.Stdout, f)
logrus.SetOutput(logLocations)
} else {
@ -54,3 +53,26 @@ func InitializeLogger(format, filename string, stdout bool) error {
return nil
}
func setLevelStr(level string, log bool) {
switch level {
case "error":
logrus.SetLevel(logrus.ErrorLevel)
case "warn":
logrus.SetLevel(logrus.WarnLevel)
case "info":
logrus.SetLevel(logrus.InfoLevel)
case "debug":
logrus.SetLevel(logrus.DebugLevel)
case "trace":
logrus.SetLevel(logrus.TraceLevel)
default:
level = "info (default)"
logrus.SetLevel(logrus.InfoLevel)
}
if log {
logrus.Infof("Log severity set to %s", level)
}
}

View File

@ -9,6 +9,8 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/authelia/authelia/internal/configuration/schema"
)
func TestShouldWriteLogsToFile(t *testing.T) {
@ -20,7 +22,7 @@ func TestShouldWriteLogsToFile(t *testing.T) {
defer os.RemoveAll(dir)
path := fmt.Sprintf("%s/authelia.log", dir)
err = InitializeLogger("text", path, false)
err = InitializeLogger(schema.LogConfiguration{Format: "text", FilePath: path, KeepStdout: false}, false)
require.NoError(t, err)
Logger().Info("This is a test")
@ -43,7 +45,7 @@ func TestShouldWriteLogsToFileAndStdout(t *testing.T) {
defer os.RemoveAll(dir)
path := fmt.Sprintf("%s/authelia.log", dir)
err = InitializeLogger("text", path, true)
err = InitializeLogger(schema.LogConfiguration{Format: "text", FilePath: path, KeepStdout: true}, false)
require.NoError(t, err)
Logger().Info("This is a test")
@ -66,7 +68,7 @@ func TestShouldFormatLogsAsJSON(t *testing.T) {
defer os.RemoveAll(dir)
path := fmt.Sprintf("%s/authelia.log", dir)
err = InitializeLogger("json", path, false)
err = InitializeLogger(schema.LogConfiguration{Format: "json", FilePath: path, KeepStdout: false}, false)
require.NoError(t, err)
Logger().Info("This is a test")

View File

@ -153,8 +153,8 @@ func registerRoutes(configuration schema.Configuration, providers middlewares.Pr
return handler
}
// StartServer start Authelia server with the given configuration and providers.
func StartServer(configuration schema.Configuration, providers middlewares.Providers) {
// Start Authelia's internal webserver with the given configuration and providers.
func Start(configuration schema.Configuration, providers middlewares.Providers) {
logger := logging.Logger()
handler := registerRoutes(configuration, providers)
@ -192,10 +192,10 @@ func StartServer(configuration schema.Configuration, providers middlewares.Provi
}
if configuration.Server.TLS.Certificate != "" && configuration.Server.TLS.Key != "" {
logger.Infof("Authelia is listening for TLS connections on %s%s", addrPattern, configuration.Server.Path)
logger.Infof("Listening for TLS connections on %s%s", addrPattern, configuration.Server.Path)
logger.Fatal(server.ServeTLS(listener, configuration.Server.TLS.Certificate, configuration.Server.TLS.Key))
} else {
logger.Infof("Authelia is listening for non-TLS connections on %s%s", addrPattern, configuration.Server.Path)
logger.Infof("Listening for non-TLS connections on %s%s", addrPattern, configuration.Server.Path)
logger.Fatal(server.Serve(listener))
}
}

View File

@ -46,7 +46,7 @@ func waitUntilAutheliaBackendIsReady(dockerEnvironment *DockerEnvironment) error
90*time.Second,
dockerEnvironment,
"authelia-backend",
[]string{"Authelia is listening for"})
[]string{"Listening for"})
}
func waitUntilAutheliaFrontendIsReady(dockerEnvironment *DockerEnvironment) error {

View File

@ -39,7 +39,7 @@ func (s *CLISuite) SetupTest() {
}
func (s *CLISuite) TestShouldPrintBuildInformation() {
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "build"})
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "build-info"})
s.Assert().Nil(err)
s.Assert().Contains(output, "Last Tag: ")
s.Assert().Contains(output, "State: ")
@ -86,76 +86,76 @@ func (s *CLISuite) TestShouldHashPasswordSHA512() {
func (s *CLISuite) TestShouldGenerateCertificateRSA() {
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "certificates", "generate", "--host", "*.example.com", "--dir", "/tmp/"})
s.Assert().Nil(err)
s.Assert().Contains(output, "wrote /tmp/cert.pem")
s.Assert().Contains(output, "wrote /tmp/key.pem")
s.Assert().Contains(output, "Certificate Public Key written to /tmp/cert.pem")
s.Assert().Contains(output, "Certificate Private Key written to /tmp/key.pem")
}
func (s *CLISuite) TestShouldGenerateCertificateRSAWithIPAddress() {
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "certificates", "generate", "--host", "127.0.0.1", "--dir", "/tmp/"})
s.Assert().Nil(err)
s.Assert().Contains(output, "wrote /tmp/cert.pem")
s.Assert().Contains(output, "wrote /tmp/key.pem")
s.Assert().Contains(output, "Certificate Public Key written to /tmp/cert.pem")
s.Assert().Contains(output, "Certificate Private Key written to /tmp/key.pem")
}
func (s *CLISuite) TestShouldGenerateCertificateRSAWithStartDate() {
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "certificates", "generate", "--host", "*.example.com", "--dir", "/tmp/", "--start-date", "'Jan 1 15:04:05 2011'"})
s.Assert().Nil(err)
s.Assert().Contains(output, "wrote /tmp/cert.pem")
s.Assert().Contains(output, "wrote /tmp/key.pem")
s.Assert().Contains(output, "Certificate Public Key written to /tmp/cert.pem")
s.Assert().Contains(output, "Certificate Private Key written to /tmp/key.pem")
}
func (s *CLISuite) TestShouldFailGenerateCertificateRSAWithStartDate() {
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "certificates", "generate", "--host", "*.example.com", "--dir", "/tmp/", "--start-date", "Jan"})
s.Assert().NotNil(err)
s.Assert().Contains(output, "Failed to parse creation date: parsing time \"Jan\" as \"Jan 2 15:04:05 2006\": cannot parse \"\" as \"2\"")
s.Assert().Contains(output, "Failed to parse start date: parsing time \"Jan\" as \"Jan 2 15:04:05 2006\": cannot parse \"\" as \"2\"")
}
func (s *CLISuite) TestShouldGenerateCertificateCA() {
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "certificates", "generate", "--host", "*.example.com", "--dir", "/tmp/", "--ca"})
s.Assert().Nil(err)
s.Assert().Contains(output, "wrote /tmp/cert.pem")
s.Assert().Contains(output, "wrote /tmp/key.pem")
s.Assert().Contains(output, "Certificate Public Key written to /tmp/cert.pem")
s.Assert().Contains(output, "Certificate Private Key written to /tmp/key.pem")
}
func (s *CLISuite) TestShouldGenerateCertificateEd25519() {
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "certificates", "generate", "--host", "*.example.com", "--dir", "/tmp/", "--ed25519"})
s.Assert().Nil(err)
s.Assert().Contains(output, "wrote /tmp/cert.pem")
s.Assert().Contains(output, "wrote /tmp/key.pem")
s.Assert().Contains(output, "Certificate Public Key written to /tmp/cert.pem")
s.Assert().Contains(output, "Certificate Private Key written to /tmp/key.pem")
}
func (s *CLISuite) TestShouldFailGenerateCertificateECDSA() {
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "certificates", "generate", "--host", "*.example.com", "--dir", "/tmp/", "--ecdsa-curve", "invalid"})
s.Assert().NotNil(err)
s.Assert().Contains(output, "Unrecognized elliptic curve: \"invalid\"")
s.Assert().Contains(output, "Failed to generate private key: unrecognized elliptic curve: \"invalid\"")
}
func (s *CLISuite) TestShouldGenerateCertificateECDSAP224() {
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "certificates", "generate", "--host", "*.example.com", "--dir", "/tmp/", "--ecdsa-curve", "P224"})
s.Assert().Nil(err)
s.Assert().Contains(output, "wrote /tmp/cert.pem")
s.Assert().Contains(output, "wrote /tmp/key.pem")
s.Assert().Contains(output, "Certificate Public Key written to /tmp/cert.pem")
s.Assert().Contains(output, "Certificate Private Key written to /tmp/key.pem")
}
func (s *CLISuite) TestShouldGenerateCertificateECDSAP256() {
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "certificates", "generate", "--host", "*.example.com", "--dir", "/tmp/", "--ecdsa-curve", "P256"})
s.Assert().Nil(err)
s.Assert().Contains(output, "wrote /tmp/cert.pem")
s.Assert().Contains(output, "wrote /tmp/key.pem")
s.Assert().Contains(output, "Certificate Public Key written to /tmp/cert.pem")
s.Assert().Contains(output, "Certificate Private Key written to /tmp/key.pem")
}
func (s *CLISuite) TestShouldGenerateCertificateECDSAP384() {
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "certificates", "generate", "--host", "*.example.com", "--dir", "/tmp/", "--ecdsa-curve", "P384"})
s.Assert().Nil(err)
s.Assert().Contains(output, "wrote /tmp/cert.pem")
s.Assert().Contains(output, "wrote /tmp/key.pem")
s.Assert().Contains(output, "Certificate Public Key written to /tmp/cert.pem")
s.Assert().Contains(output, "Certificate Private Key written to /tmp/key.pem")
}
func (s *CLISuite) TestShouldGenerateCertificateECDSAP521() {
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "certificates", "generate", "--host", "*.example.com", "--dir", "/tmp/", "--ecdsa-curve", "P521"})
s.Assert().Nil(err)
s.Assert().Contains(output, "wrote /tmp/cert.pem")
s.Assert().Contains(output, "wrote /tmp/key.pem")
s.Assert().Contains(output, "Certificate Public Key written to /tmp/cert.pem")
s.Assert().Contains(output, "Certificate Private Key written to /tmp/key.pem")
}
func TestCLISuite(t *testing.T) {

View File

@ -5,7 +5,7 @@ import (
"crypto/x509"
"fmt"
"io/ioutil"
"path"
"path/filepath"
"strings"
"github.com/authelia/authelia/internal/configuration/schema"
@ -28,10 +28,10 @@ func NewTLSConfig(config *schema.TLSConfig, defaultMinVersion uint16, certPool *
}
// NewX509CertPool generates a x509.CertPool from the system PKI and the directory specified.
func NewX509CertPool(directory string) (certPool *x509.CertPool, errors []error, nonFatalErrors []error) {
func NewX509CertPool(directory string) (certPool *x509.CertPool, warnings []error, errors []error) {
certPool, err := x509.SystemCertPool()
if err != nil {
nonFatalErrors = append(nonFatalErrors, fmt.Errorf("could not load system certificate pool which may result in untrusted certificate issues: %v", err))
warnings = append(warnings, fmt.Errorf("could not load system certificate pool which may result in untrusted certificate issues: %v", err))
certPool = x509.NewCertPool()
}
@ -48,7 +48,7 @@ func NewX509CertPool(directory string) (certPool *x509.CertPool, errors []error,
nameLower := strings.ToLower(certFileInfo.Name())
if !certFileInfo.IsDir() && (strings.HasSuffix(nameLower, ".cer") || strings.HasSuffix(nameLower, ".crt") || strings.HasSuffix(nameLower, ".pem")) {
certPath := path.Join(directory, certFileInfo.Name())
certPath := filepath.Join(directory, certFileInfo.Name())
logger.Tracef("Found possible cert %s, attempting to add it to the pool", certPath)
@ -65,7 +65,7 @@ func NewX509CertPool(directory string) (certPool *x509.CertPool, errors []error,
logger.Tracef("Finished scan of directory %s for certificates", directory)
return certPool, errors, nonFatalErrors
return certPool, warnings, errors
}
// TLSStringToTLSConfigVersion returns a go crypto/tls version for a tls.Config based on string input.

View File

@ -77,50 +77,50 @@ func TestShouldReturnZeroAndErrorOnInvalidTLSVersions(t *testing.T) {
}
func TestShouldReturnErrWhenX509DirectoryNotExist(t *testing.T) {
pool, errs, nonFatalErrs := NewX509CertPool("/tmp/asdfzyxabc123/not/a/real/dir")
pool, warnings, errors := NewX509CertPool("/tmp/asdfzyxabc123/not/a/real/dir")
assert.NotNil(t, pool)
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")
require.Len(t, warnings, 1)
assert.EqualError(t, warnings[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, warnings, 0)
}
require.Len(t, errs, 1)
require.Len(t, errors, 1)
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.")
assert.EqualError(t, errors[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")
assert.EqualError(t, errors[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")
pool, warnings, errors := NewX509CertPool("/tmp")
assert.NotNil(t, pool)
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")
require.Len(t, warnings, 1)
assert.EqualError(t, warnings[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, warnings, 0)
}
assert.Len(t, errs, 0)
assert.Len(t, errors, 0)
}
func TestShouldReadCertsFromDirectoryButNotKeys(t *testing.T) {
pool, errs, nonFatalErrs := NewX509CertPool("../suites/common/ssl/")
pool, warnings, errors := NewX509CertPool("../suites/common/ssl/")
assert.NotNil(t, pool)
require.Len(t, errs, 1)
require.Len(t, errors, 1)
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")
require.Len(t, warnings, 1)
assert.EqualError(t, warnings[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, warnings, 0)
}
assert.EqualError(t, errs[0], "could not import certificate key.pem")
assert.EqualError(t, errors[0], "could not import certificate key.pem")
}

View File

@ -44,6 +44,8 @@ const (
clean = "clean"
tagged = "tagged"
unknown = "unknown"
errFmtLinuxNotFound = "open %s: no such file or directory"
)
var (

View File

@ -0,0 +1,42 @@
package utils
import "runtime"
// ErrSliceSortAlphabetical is a helper type that can be used with sort.Sort to sort a slice of errors in alphabetical
// order. Usage is simple just do sort.Sort(ErrSliceSortAlphabetical([]error{})).
type ErrSliceSortAlphabetical []error
func (s ErrSliceSortAlphabetical) Len() int { return len(s) }
func (s ErrSliceSortAlphabetical) Less(i, j int) bool { return s[i].Error() < s[j].Error() }
func (s ErrSliceSortAlphabetical) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
// GetExpectedErrTxt returns error text for expected errs.
func GetExpectedErrTxt(err string) string {
switch err {
case "pathnotfound":
switch runtime.GOOS {
case windows:
return "open %s: The system cannot find the path specified."
default:
return errFmtLinuxNotFound
}
case "filenotfound":
switch runtime.GOOS {
case windows:
return "open %s: The system cannot find the file specified."
default:
return errFmtLinuxNotFound
}
case "yamlisdir":
switch runtime.GOOS {
case windows:
return "read %s: The handle is invalid."
default:
return "read %s: is a directory"
}
}
return ""
}

View File

@ -2,12 +2,34 @@ package utils
import (
"crypto/sha256"
"fmt"
"encoding/hex"
"io"
"os"
)
// HashSHA256FromString takes an input string and calculates the SHA256 checksum returning it as a base16 hash string.
func HashSHA256FromString(input string) (output string) {
sum := sha256.Sum256([]byte(input))
hash := sha256.New()
return fmt.Sprintf("%x", sum)
hash.Write([]byte(input))
return hex.EncodeToString(hash.Sum(nil))
}
// HashSHA256FromPath takes a path string and calculates the SHA256 checksum of the file at the path returning it as a base16 hash string.
func HashSHA256FromPath(path string) (output string, err error) {
file, err := os.Open(path)
if err != nil {
return "", err
}
defer file.Close()
hash := sha256.New()
if _, err := io.Copy(hash, file); err != nil {
return "", err
}
return hex.EncodeToString(hash.Sum(nil)), nil
}

View File

@ -0,0 +1,67 @@
package utils
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
)
func TestShouldHashString(t *testing.T) {
input := "input"
anotherInput := "another"
sum := HashSHA256FromString(input)
assert.Equal(t, "c96c6d5be8d08a12e7b5cdc1b207fa6b2430974c86803d8891675e76fd992c20", sum)
anotherSum := HashSHA256FromString(anotherInput)
assert.Equal(t, "ae448ac86c4e8e4dec645729708ef41873ae79c6dff84eff73360989487f08e5", anotherSum)
assert.NotEqual(t, sum, anotherSum)
randomInput := RandomString(40, AlphaNumericCharacters)
randomSum := HashSHA256FromString(randomInput)
assert.NotEqual(t, randomSum, sum)
assert.NotEqual(t, randomSum, anotherSum)
}
func TestShouldHashPath(t *testing.T) {
dir, err := ioutil.TempDir("", "authelia-hashing")
assert.NoError(t, err)
err = os.WriteFile(filepath.Join(dir, "myfile"), []byte("output\n"), 0600)
assert.NoError(t, err)
err = os.WriteFile(filepath.Join(dir, "anotherfile"), []byte("another\n"), 0600)
assert.NoError(t, err)
err = os.WriteFile(filepath.Join(dir, "randomfile"), []byte(RandomString(40, AlphaNumericCharacters)+"\n"), 0600)
assert.NoError(t, err)
sum, err := HashSHA256FromPath(filepath.Join(dir, "myfile"))
assert.NoError(t, err)
assert.Equal(t, "9aff6ba4b042b9d09991a9fbf8c80ddbd2a9c433638339cd831bed955e39f106", sum)
anotherSum, err := HashSHA256FromPath(filepath.Join(dir, "anotherfile"))
assert.NoError(t, err)
assert.Equal(t, "33a7b215065f2ee8635efb72620bc269a1efb889ba3026560334da7366742374", anotherSum)
randomSum, err := HashSHA256FromPath(filepath.Join(dir, "randomfile"))
assert.NoError(t, err)
assert.NotEqual(t, randomSum, sum)
assert.NotEqual(t, randomSum, anotherSum)
sum, err = HashSHA256FromPath(filepath.Join(dir, "notafile"))
assert.Equal(t, "", sum)
errTxt := GetExpectedErrTxt("filenotfound")
assert.EqualError(t, err, fmt.Sprintf(errTxt, filepath.Join(dir, "notafile")))
}

View File

@ -46,6 +46,17 @@ func IsStringInSlice(needle string, haystack []string) (inSlice bool) {
return false
}
// IsStringInSliceSuffix checks if the needle string has one of the suffixes in the haystack.
func IsStringInSliceSuffix(needle string, haystack []string) (hasSuffix bool) {
for _, straw := range haystack {
if strings.HasSuffix(needle, straw) {
return true
}
}
return false
}
// IsStringInSliceFold checks if a single string is in a slice of strings but uses strings.EqualFold to compare them.
func IsStringInSliceFold(needle string, haystack []string) (inSlice bool) {
for _, b := range haystack {

View File

@ -131,3 +131,14 @@ func TestShouldNotFindStringInSliceFold(t *testing.T) {
assert.False(t, IsStringInSliceFold(a, slice))
assert.False(t, IsStringInSliceFold(b, slice))
}
func TestIsStringInSliceSuffix(t *testing.T) {
suffixes := []string{"apple", "banana"}
assert.True(t, IsStringInSliceSuffix("apple.banana", suffixes))
assert.True(t, IsStringInSliceSuffix("a.banana", suffixes))
assert.True(t, IsStringInSliceSuffix("a_banana", suffixes))
assert.True(t, IsStringInSliceSuffix("an.apple", suffixes))
assert.False(t, IsStringInSliceSuffix("an.orange", suffixes))
assert.False(t, IsStringInSliceSuffix("an.apple.orange", suffixes))
}