feat(commands): add access-control check-policy command (#2871)

This adds an access-control command that checks the policy enforcement for a given criteria using a configuration file and refactors the configuration validation command to include all configuration sources.
pull/2927/head
James Elliott 2022-02-28 14:15:01 +11:00 committed by GitHub
parent d87a56fa1a
commit 3c81e75d79
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
48 changed files with 1657 additions and 995 deletions

View File

@ -95,7 +95,7 @@ upgrading to prevent configuration changes from impacting downtime in an upgrade
integrations, it only checks that your configuration syntax is valid. integrations, it only checks that your configuration syntax is valid.
```console ```console
$ authelia validate-config configuration.yml $ authelia validate-config --config configuration.yml
``` ```
# Duration Notation Format # Duration Notation Format

View File

@ -124,12 +124,23 @@ func isMatchForNetworks(subject Subject, acl *AccessControlRule) (match bool) {
return false return false
} }
// Same as isExactMatchForSubjects except it theoretically matches if subject is anonymous since they'd need to authenticate.
func isMatchForSubjects(subject Subject, acl *AccessControlRule) (match bool) { func isMatchForSubjects(subject Subject, acl *AccessControlRule) (match bool) {
// If there are no subjects in this rule then the subject condition is a match. if subject.IsAnonymous() {
if len(acl.Subjects) == 0 || subject.IsAnonymous() {
return true return true
} }
return isExactMatchForSubjects(subject, acl)
}
func isExactMatchForSubjects(subject Subject, acl *AccessControlRule) (match bool) {
// If there are no subjects in this rule then the subject condition is a match.
if len(acl.Subjects) == 0 {
return true
} else if subject.IsAnonymous() {
return false
}
// Iterate over the subjects until we find a match (return true) or until we exit the loop (return false). // Iterate over the subjects until we find a match (return true) or until we exit the loop (return false).
for _, subjectRule := range acl.Subjects { for _, subjectRule := range acl.Subjects {
if subjectRule.IsMatch(subject) { if subjectRule.IsMatch(subject) {

View File

@ -66,3 +66,28 @@ func (p Authorizer) GetRequiredLevel(subject Subject, object Object) Level {
return p.defaultPolicy return p.defaultPolicy
} }
// GetRuleMatchResults iterates through the rules and produces a list of RuleMatchResult provided a subject and object.
func (p Authorizer) GetRuleMatchResults(subject Subject, object Object) (results []RuleMatchResult) {
skipped := false
results = make([]RuleMatchResult, len(p.rules))
for i, rule := range p.rules {
results[i] = RuleMatchResult{
Rule: rule,
Skipped: skipped,
MatchDomain: isMatchForDomains(subject, object, rule),
MatchResources: isMatchForResources(object, rule),
MatchMethods: isMatchForMethods(object, rule),
MatchNetworks: isMatchForNetworks(subject, rule),
MatchSubjects: isMatchForSubjects(subject, rule),
MatchSubjectsExact: isExactMatchForSubjects(subject, rule),
}
skipped = skipped || results[i].IsMatch()
}
return results
}

View File

@ -31,20 +31,23 @@ func NewAuthorizerTester(config schema.AccessControlConfiguration) *AuthorizerTe
} }
func (s *AuthorizerTester) CheckAuthorizations(t *testing.T, subject Subject, requestURI, method string, expectedLevel Level) { func (s *AuthorizerTester) CheckAuthorizations(t *testing.T, subject Subject, requestURI, method string, expectedLevel Level) {
url, _ := url.ParseRequestURI(requestURI) targetURL, _ := url.ParseRequestURI(requestURI)
object := Object{ object := NewObject(targetURL, method)
Scheme: url.Scheme,
Domain: url.Hostname(),
Path: url.Path,
Method: method,
}
level := s.GetRequiredLevel(subject, object) level := s.GetRequiredLevel(subject, object)
assert.Equal(t, expectedLevel, level) assert.Equal(t, expectedLevel, level)
} }
func (s *AuthorizerTester) GetRuleMatchResults(subject Subject, requestURI, method string) (results []RuleMatchResult) {
targetURL, _ := url.ParseRequestURI(requestURI)
object := NewObject(targetURL, method)
return s.Authorizer.GetRuleMatchResults(subject, object)
}
type AuthorizerTesterBuilder struct { type AuthorizerTesterBuilder struct {
config schema.AccessControlConfiguration config schema.AccessControlConfiguration
} }
@ -481,6 +484,59 @@ func (s *AuthorizerSuite) TestShouldMatchResourceWithSubjectRules() {
tester.CheckAuthorizations(s.T(), John, "https://private.example.com", "GET", TwoFactor) tester.CheckAuthorizations(s.T(), John, "https://private.example.com", "GET", TwoFactor)
tester.CheckAuthorizations(s.T(), Bob, "https://private.example.com", "GET", Denied) tester.CheckAuthorizations(s.T(), Bob, "https://private.example.com", "GET", Denied)
tester.CheckAuthorizations(s.T(), AnonymousUser, "https://private.example.com", "GET", TwoFactor) tester.CheckAuthorizations(s.T(), AnonymousUser, "https://private.example.com", "GET", TwoFactor)
results := tester.GetRuleMatchResults(John, "https://private.example.com", "GET")
require.Len(s.T(), results, 7)
assert.False(s.T(), results[0].IsMatch())
assert.False(s.T(), results[0].MatchDomain)
assert.False(s.T(), results[0].MatchResources)
assert.True(s.T(), results[0].MatchSubjects)
assert.True(s.T(), results[0].MatchNetworks)
assert.True(s.T(), results[0].MatchMethods)
assert.False(s.T(), results[1].IsMatch())
assert.False(s.T(), results[1].MatchDomain)
assert.False(s.T(), results[1].MatchResources)
assert.True(s.T(), results[1].MatchSubjects)
assert.True(s.T(), results[1].MatchNetworks)
assert.True(s.T(), results[1].MatchMethods)
assert.False(s.T(), results[2].IsMatch())
assert.False(s.T(), results[2].MatchDomain)
assert.True(s.T(), results[2].MatchResources)
assert.True(s.T(), results[2].MatchSubjects)
assert.True(s.T(), results[2].MatchNetworks)
assert.True(s.T(), results[2].MatchMethods)
assert.False(s.T(), results[3].IsMatch())
assert.False(s.T(), results[3].MatchDomain)
assert.False(s.T(), results[3].MatchResources)
assert.True(s.T(), results[3].MatchSubjects)
assert.True(s.T(), results[3].MatchNetworks)
assert.True(s.T(), results[3].MatchMethods)
assert.False(s.T(), results[4].IsMatch())
assert.False(s.T(), results[4].MatchDomain)
assert.False(s.T(), results[4].MatchResources)
assert.True(s.T(), results[4].MatchSubjects)
assert.True(s.T(), results[4].MatchNetworks)
assert.True(s.T(), results[4].MatchMethods)
assert.False(s.T(), results[5].IsMatch())
assert.False(s.T(), results[5].MatchDomain)
assert.True(s.T(), results[5].MatchResources)
assert.True(s.T(), results[5].MatchSubjects)
assert.True(s.T(), results[5].MatchNetworks)
assert.True(s.T(), results[5].MatchMethods)
assert.True(s.T(), results[6].IsMatch())
assert.True(s.T(), results[6].MatchDomain)
assert.True(s.T(), results[6].MatchResources)
assert.True(s.T(), results[6].MatchSubjects)
assert.True(s.T(), results[6].MatchNetworks)
assert.True(s.T(), results[6].MatchMethods)
} }
func (s *AuthorizerSuite) TestPolicyToLevel() { func (s *AuthorizerSuite) TestPolicyToLevel() {

View File

@ -58,3 +58,27 @@ func NewObject(targetURL *url.URL, method string) (object Object) {
return object return object
} }
// RuleMatchResult describes how well a rule matched a subject/object combo.
type RuleMatchResult struct {
Rule *AccessControlRule
Skipped bool
MatchDomain bool
MatchResources bool
MatchMethods bool
MatchNetworks bool
MatchSubjects bool
MatchSubjectsExact bool
}
// IsMatch returns true if all the criteria matched.
func (r RuleMatchResult) IsMatch() (match bool) {
return r.MatchDomain && r.MatchResources && r.MatchMethods && r.MatchNetworks && r.MatchSubjectsExact
}
// IsPotentialMatch returns true if the rule is potentially a match.
func (r RuleMatchResult) IsPotentialMatch() (match bool) {
return r.MatchDomain && r.MatchResources && r.MatchMethods && r.MatchNetworks && r.MatchSubjects && !r.MatchSubjectsExact
}

View File

@ -25,6 +25,22 @@ func PolicyToLevel(policy string) Level {
return Denied return Denied
} }
// LevelToPolicy converts a int authorization level to string policy.
func LevelToPolicy(level Level) (policy string) {
switch level {
case Bypass:
return bypass
case OneFactor:
return oneFactor
case TwoFactor:
return twoFactor
case Denied:
return deny
}
return deny
}
func schemaSubjectToACLSubject(subjectRule string) (subject AccessControlSubject) { func schemaSubjectToACLSubject(subjectRule string) (subject AccessControlSubject) {
if strings.HasPrefix(subjectRule, userPrefix) { if strings.HasPrefix(subjectRule, userPrefix) {
user := strings.Trim(subjectRule[len(userPrefix):], " ") user := strings.Trim(subjectRule[len(userPrefix):], " ")

View File

@ -0,0 +1,242 @@
package commands
import (
"errors"
"fmt"
"net"
"net/url"
"strings"
"github.com/spf13/cobra"
"github.com/authelia/authelia/v4/internal/authorization"
"github.com/authelia/authelia/v4/internal/configuration"
"github.com/authelia/authelia/v4/internal/configuration/schema"
"github.com/authelia/authelia/v4/internal/configuration/validator"
)
func newAccessControlCommand() (cmd *cobra.Command) {
cmd = &cobra.Command{
Use: "access-control",
Short: "Helpers for the access control system",
}
cmd.AddCommand(
newAccessControlCheckCommand(),
)
return cmd
}
func newAccessControlCheckCommand() (cmd *cobra.Command) {
cmd = &cobra.Command{
Use: "check-policy",
Short: "Checks a request against the access control rules to determine what policy would be applied",
Long: accessControlPolicyCheckLong,
RunE: accessControlCheckRunE,
}
cmdWithConfigFlags(cmd, false, []string{"config.yml"})
cmd.Flags().String("url", "", "the url of the object")
cmd.Flags().String("method", "GET", "the HTTP method of the object")
cmd.Flags().String("username", "", "the username of the subject")
cmd.Flags().StringSlice("groups", nil, "the groups of the subject")
cmd.Flags().String("ip", "", "the ip of the subject")
cmd.Flags().Bool("verbose", false, "enables verbose output")
return cmd
}
func accessControlCheckRunE(cmd *cobra.Command, _ []string) (err error) {
configs, err := cmd.Flags().GetStringSlice("config")
if err != nil {
return err
}
sources := make([]configuration.Source, len(configs)+2)
for i, path := range configs {
sources[i] = configuration.NewYAMLFileSource(path)
}
sources[0+len(configs)] = configuration.NewEnvironmentSource(configuration.DefaultEnvPrefix, configuration.DefaultEnvDelimiter)
sources[1+len(configs)] = configuration.NewSecretsSource(configuration.DefaultEnvPrefix, configuration.DefaultEnvDelimiter)
val := schema.NewStructValidator()
accessControlConfig := &schema.Configuration{}
if _, err = configuration.LoadAdvanced(val, "access_control", &accessControlConfig.AccessControl, sources...); err != nil {
return err
}
v := schema.NewStructValidator()
validator.ValidateAccessControl(accessControlConfig, v)
if v.HasErrors() || v.HasWarnings() {
return errors.New("your configuration has errors")
}
authorizer := authorization.NewAuthorizer(accessControlConfig)
subject, object, err := getSubjectAndObjectFromFlags(cmd)
if err != nil {
return err
}
results := authorizer.GetRuleMatchResults(subject, object)
if len(results) == 0 {
fmt.Printf("\nThe default policy '%s' will be applied to ALL requests as no rules are configured.\n\n", accessControlConfig.AccessControl.DefaultPolicy)
return nil
}
verbose, err := cmd.Flags().GetBool("verbose")
if err != nil {
return err
}
accessControlCheckWriteOutput(object, subject, results, accessControlConfig.AccessControl.DefaultPolicy, verbose)
return nil
}
func accessControlCheckWriteObjectSubject(object authorization.Object, subject authorization.Subject) {
output := strings.Builder{}
output.WriteString(fmt.Sprintf("Performing policy check for request to '%s'", object.String()))
if object.Method != "" {
output.WriteString(fmt.Sprintf(" method '%s'", object.Method))
}
if subject.Username != "" {
output.WriteString(fmt.Sprintf(" username '%s'", subject.Username))
}
if len(subject.Groups) != 0 {
output.WriteString(fmt.Sprintf(" groups '%s'", strings.Join(subject.Groups, ",")))
}
if subject.IP != nil {
output.WriteString(fmt.Sprintf(" from IP '%s'", subject.IP.String()))
}
output.WriteString(".\n")
fmt.Println(output.String())
}
func accessControlCheckWriteOutput(object authorization.Object, subject authorization.Subject, results []authorization.RuleMatchResult, defaultPolicy string, verbose bool) {
accessControlCheckWriteObjectSubject(object, subject)
fmt.Printf(" #\tDomain\tResource\tMethod\tNetwork\tSubject\n")
var (
appliedPos int
applied authorization.RuleMatchResult
potentialPos int
potential authorization.RuleMatchResult
)
for i, result := range results {
if result.Skipped && !verbose {
break
}
switch {
case result.IsMatch() && !result.Skipped:
appliedPos, applied = i+1, result
fmt.Printf("* %d\t%s\t%s\t\t%s\t%s\t%s\n", i+1, hitMissMay(result.MatchDomain), hitMissMay(result.MatchResources), hitMissMay(result.MatchMethods), hitMissMay(result.MatchNetworks), hitMissMay(result.MatchSubjects, result.MatchSubjectsExact))
case result.IsPotentialMatch() && !result.Skipped:
if potentialPos == 0 {
potentialPos, potential = i+1, result
}
fmt.Printf("~ %d\t%s\t%s\t\t%s\t%s\t%s\n", i+1, hitMissMay(result.MatchDomain), hitMissMay(result.MatchResources), hitMissMay(result.MatchMethods), hitMissMay(result.MatchNetworks), hitMissMay(result.MatchSubjects, result.MatchSubjectsExact))
default:
fmt.Printf(" %d\t%s\t%s\t\t%s\t%s\t%s\n", i+1, hitMissMay(result.MatchDomain), hitMissMay(result.MatchResources), hitMissMay(result.MatchMethods), hitMissMay(result.MatchNetworks), hitMissMay(result.MatchSubjects, result.MatchSubjectsExact))
}
}
switch {
case appliedPos != 0 && (potentialPos == 0 || (potentialPos > appliedPos)):
fmt.Printf("\nThe policy '%s' from rule #%d will be applied to this request.\n\n", authorization.LevelToPolicy(applied.Rule.Policy), appliedPos)
case potentialPos != 0 && appliedPos != 0:
fmt.Printf("\nThe policy '%s' from rule #%d will potentially be applied to this request. If not policy '%s' from rule #%d will be.\n\n", authorization.LevelToPolicy(potential.Rule.Policy), potentialPos, authorization.LevelToPolicy(applied.Rule.Policy), appliedPos)
case potentialPos != 0:
fmt.Printf("\nThe policy '%s' from rule #%d will potentially be applied to this request. Otherwise the policy '%s' from the default policy will be.\n\n", authorization.LevelToPolicy(potential.Rule.Policy), potentialPos, defaultPolicy)
default:
fmt.Printf("\nThe policy '%s' from the default policy will be applied to this request as no rules matched the request.\n\n", defaultPolicy)
}
}
func hitMissMay(in ...bool) (out string) {
var hit, miss bool
for _, x := range in {
if x {
hit = true
} else {
miss = true
}
}
switch {
case hit && miss:
return "may"
case hit:
return "hit"
default:
return "miss"
}
}
func getSubjectAndObjectFromFlags(cmd *cobra.Command) (subject authorization.Subject, object authorization.Object, err error) {
requestURL, err := cmd.Flags().GetString("url")
if err != nil {
return subject, object, err
}
parsedURL, err := url.Parse(requestURL)
if err != nil {
return subject, object, err
}
method, err := cmd.Flags().GetString("method")
if err != nil {
return subject, object, err
}
username, err := cmd.Flags().GetString("username")
if err != nil {
return subject, object, err
}
groups, err := cmd.Flags().GetStringSlice("groups")
if err != nil {
return subject, object, err
}
remoteIP, err := cmd.Flags().GetString("ip")
if err != nil {
return subject, object, err
}
parsedIP := net.ParseIP(remoteIP)
subject = authorization.Subject{
Username: username,
Groups: groups,
IP: parsedIP,
}
object = authorization.NewObject(parsedURL, method)
return subject, object, nil
}

View File

@ -3,6 +3,7 @@ package commands
import ( import (
"os" "os"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/authelia/authelia/v4/internal/configuration" "github.com/authelia/authelia/v4/internal/configuration"
@ -12,18 +13,27 @@ import (
) )
// cmdWithConfigFlags is used for commands which require access to the configuration to add the flag to the command. // cmdWithConfigFlags is used for commands which require access to the configuration to add the flag to the command.
func cmdWithConfigFlags(cmd *cobra.Command) { func cmdWithConfigFlags(cmd *cobra.Command, persistent bool, configs []string) {
cmd.Flags().StringSliceP("config", "c", []string{}, "Configuration files") if persistent {
cmd.PersistentFlags().StringSliceP("config", "c", configs, "configuration files to load")
} else {
cmd.Flags().StringSliceP("config", "c", configs, "configuration files to load")
}
} }
var config *schema.Configuration var config *schema.Configuration
func newCmdWithConfigPreRun(ensureConfigExists, validateKeys, validateConfiguration bool) func(cmd *cobra.Command, args []string) { func newCmdWithConfigPreRun(ensureConfigExists, validateKeys, validateConfiguration bool) func(cmd *cobra.Command, args []string) {
return func(cmd *cobra.Command, _ []string) { return func(cmd *cobra.Command, _ []string) {
logger := logging.Logger() var (
logger *logrus.Logger
configs []string
err error
)
configs, err := cmd.Root().Flags().GetStringSlice("config") logger = logging.Logger()
if err != nil {
if configs, err = cmd.Flags().GetStringSlice("config"); err != nil {
logger.Fatalf("Error reading flags: %v", err) logger.Fatalf("Error reading flags: %v", err)
} }
@ -39,23 +49,15 @@ func newCmdWithConfigPreRun(ensureConfigExists, validateKeys, validateConfigurat
} }
} }
var keys []string var (
val *schema.StructValidator
)
val := schema.NewStructValidator() config, val, err = loadConfig(configs, validateKeys, validateConfiguration)
keys, config, err = configuration.Load(val, configuration.NewDefaultSources(configs, configuration.DefaultEnvPrefix, configuration.DefaultEnvDelimiter)...)
if err != nil { if err != nil {
logger.Fatalf("Error occurred loading configuration: %v", err) 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() warnings := val.Warnings()
if len(warnings) != 0 { if len(warnings) != 0 {
for _, warning := range warnings { for _, warning := range warnings {
@ -73,3 +75,27 @@ func newCmdWithConfigPreRun(ensureConfigExists, validateKeys, validateConfigurat
} }
} }
} }
func loadConfig(configs []string, validateKeys, validateConfiguration bool) (c *schema.Configuration, val *schema.StructValidator, err error) {
var keys []string
val = schema.NewStructValidator()
if keys, c, err = configuration.Load(val,
configuration.NewDefaultSources(
configs,
configuration.DefaultEnvPrefix,
configuration.DefaultEnvDelimiter)...); err != nil {
return nil, nil, err
}
if validateKeys {
validator.ValidateKeys(keys, configuration.DefaultEnvPrefix, val)
}
if validateConfiguration {
validator.ValidateConfiguration(c, val)
}
return c, val, nil
}

View File

@ -80,6 +80,23 @@ PowerShell:
# and source this file from your PowerShell profile. # and source this file from your PowerShell profile.
` `
const accessControlPolicyCheckLong = `
Checks a request against the access control rules to determine what policy would be applied.
Legend:
# The rule position in the configuration.
* The first fully matched rule.
~ Potential match i.e. if the user was authenticated they may match this rule.
hit The criteria in this column is a match to the request.
miss The criteria in this column is not match to the request.
may The criteria in this column is potentially a match to the request.
Notes:
A rule that potentially matches a request will cause a redirection to occur in order to perform one-factor
authentication. This is so Authelia can adequately determine if the rule actually matches.
`
const ( const (
storageMigrateDirectionUp = "up" storageMigrateDirectionUp = "up"
storageMigrateDirectionDown = "down" storageMigrateDirectionDown = "down"

View File

@ -31,7 +31,7 @@ func NewRootCmd() (cmd *cobra.Command) {
Run: cmdRootRun, Run: cmdRootRun,
} }
cmdWithConfigFlags(cmd) cmdWithConfigFlags(cmd, false, []string{})
cmd.AddCommand( cmd.AddCommand(
newBuildInfoCmd(), newBuildInfoCmd(),
@ -41,6 +41,7 @@ func NewRootCmd() (cmd *cobra.Command) {
NewRSACmd(), NewRSACmd(),
NewStorageCmd(), NewStorageCmd(),
newValidateConfigCmd(), newValidateConfigCmd(),
newAccessControlCommand(),
) )
return cmd return cmd

View File

@ -13,7 +13,7 @@ func NewStorageCmd() (cmd *cobra.Command) {
PersistentPreRunE: storagePersistentPreRunE, PersistentPreRunE: storagePersistentPreRunE,
} }
cmd.PersistentFlags().StringSliceP("config", "c", []string{"config.yml"}, "configuration file to load for the storage migration") cmdWithConfigFlags(cmd, true, []string{"config.yml"})
cmd.PersistentFlags().String("encryption-key", "", "the storage encryption key to use") cmd.PersistentFlags().String("encryption-key", "", "the storage encryption key to use")

View File

@ -1,66 +1,70 @@
package commands package commands
import ( import (
"log" "fmt"
"os"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/authelia/authelia/v4/internal/configuration"
"github.com/authelia/authelia/v4/internal/configuration/schema" "github.com/authelia/authelia/v4/internal/configuration/schema"
"github.com/authelia/authelia/v4/internal/configuration/validator"
"github.com/authelia/authelia/v4/internal/logging"
) )
func newValidateConfigCmd() (cmd *cobra.Command) { func newValidateConfigCmd() (cmd *cobra.Command) {
cmd = &cobra.Command{ cmd = &cobra.Command{
Use: "validate-config [yaml]", Use: "validate-config",
Short: "Check a configuration against the internal configuration validation mechanisms", Short: "Check a configuration against the internal configuration validation mechanisms",
Args: cobra.MinimumNArgs(1), Args: cobra.NoArgs,
Run: cmdValidateConfigRun, RunE: cmdValidateConfigRunE,
} }
cmdWithConfigFlags(cmd, false, []string{"config.yml"})
return cmd return cmd
} }
func cmdValidateConfigRun(_ *cobra.Command, args []string) { func cmdValidateConfigRunE(cmd *cobra.Command, _ []string) (err error) {
logger := logging.Logger() var (
configs []string
val *schema.StructValidator
)
configPath := args[0] if configs, err = cmd.Flags().GetStringSlice("config"); err != nil {
if _, err := os.Stat(configPath); err != nil { return err
logger.Fatalf("Error Loading Configuration: %v\n", err)
} }
val := schema.NewStructValidator() config, val, err = loadConfig(configs, true, true)
keys, conf, err := configuration.Load(val, configuration.NewYAMLFileSource(configPath))
if err != nil { if err != nil {
logger.Fatalf("Error occurred loading configuration: %v", err) return fmt.Errorf("error occurred loading configuration: %v", err)
} }
validator.ValidateKeys(keys, configuration.DefaultEnvPrefix, val) switch {
validator.ValidateConfiguration(conf, val) case val.HasErrors():
fmt.Println("Configuration parsed and loaded with errors:")
fmt.Println("")
warnings := val.Warnings() for _, err = range val.Errors() {
errors := val.Errors() fmt.Printf("\t - %v\n", err)
if len(warnings) != 0 {
logger.Warn("Warnings occurred while loading the configuration:")
for _, warn := range warnings {
logger.Warnf(" %+v", warn)
}
} }
if len(errors) != 0 { fmt.Println("")
logger.Error("Errors occurred while loading the configuration:")
for _, err := range errors { if !val.HasWarnings() {
logger.Errorf(" %+v", err) break
} }
logger.Fatal("Can't continue due to errors") fallthrough
case val.HasWarnings():
fmt.Println("Configuration parsed and loaded with warnings:")
fmt.Println("")
for _, err = range val.Warnings() {
fmt.Printf("\t - %v\n", err)
} }
log.Println("Configuration parsed successfully without errors.") fmt.Println("")
default:
fmt.Println("Configuration parsed and loaded successfully without errors.")
fmt.Println("")
}
return nil
} }

View File

@ -260,7 +260,7 @@ func TestShouldHandleErrInvalidatorWhenSMTPSenderBlank(t *testing.T) {
require.Len(t, val.Errors(), 1) require.Len(t, val.Errors(), 1)
assert.Len(t, val.Warnings(), 0) assert.Len(t, val.Warnings(), 0)
assert.EqualError(t, val.Errors()[0], "smtp notifier: the 'sender' must be configured") assert.EqualError(t, val.Errors()[0], "notifier: smtp: option 'sender' is required")
} }
func TestShouldDecodeSMTPSenderWithoutName(t *testing.T) { func TestShouldDecodeSMTPSenderWithoutName(t *testing.T) {

View File

@ -12,7 +12,7 @@ import (
// IsPolicyValid check if policy is valid. // IsPolicyValid check if policy is valid.
func IsPolicyValid(policy string) (isValid bool) { func IsPolicyValid(policy string) (isValid bool) {
return policy == policyDeny || policy == policyOneFactor || policy == policyTwoFactor || policy == policyBypass return utils.IsStringInSlice(policy, validACLRulePolicies)
} }
// IsResourceValid check if a resource is valid. // IsResourceValid check if a resource is valid.
@ -27,8 +27,8 @@ func IsSubjectValid(subject string) (isValid bool) {
} }
// IsNetworkGroupValid check if a network group is valid. // IsNetworkGroupValid check if a network group is valid.
func IsNetworkGroupValid(configuration schema.AccessControlConfiguration, network string) bool { func IsNetworkGroupValid(config schema.AccessControlConfiguration, network string) bool {
for _, networks := range configuration.Networks { for _, networks := range config.Networks {
if network != networks.Name { if network != networks.Name {
continue continue
} else { } else {
@ -49,21 +49,29 @@ func IsNetworkValid(network string) (isValid bool) {
return true return true
} }
func ruleDescriptor(position int, rule schema.ACLRule) string {
if len(rule.Domains) == 0 {
return fmt.Sprintf("#%d", position)
}
return fmt.Sprintf("#%d (domain '%s')", position, strings.Join(rule.Domains, ","))
}
// ValidateAccessControl validates access control configuration. // ValidateAccessControl validates access control configuration.
func ValidateAccessControl(configuration *schema.AccessControlConfiguration, validator *schema.StructValidator) { func ValidateAccessControl(config *schema.Configuration, validator *schema.StructValidator) {
if configuration.DefaultPolicy == "" { if config.AccessControl.DefaultPolicy == "" {
configuration.DefaultPolicy = policyDeny config.AccessControl.DefaultPolicy = policyDeny
} }
if !IsPolicyValid(configuration.DefaultPolicy) { if !IsPolicyValid(config.AccessControl.DefaultPolicy) {
validator.Push(fmt.Errorf("'default_policy' must either be 'deny', 'two_factor', 'one_factor' or 'bypass'")) validator.Push(fmt.Errorf(errFmtAccessControlDefaultPolicyValue, strings.Join(validACLRulePolicies, "', '"), config.AccessControl.DefaultPolicy))
} }
if configuration.Networks != nil { if config.AccessControl.Networks != nil {
for _, n := range configuration.Networks { for _, n := range config.AccessControl.Networks {
for _, networks := range n.Networks { for _, networks := range n.Networks {
if !IsNetworkValid(networks) { if !IsNetworkValid(networks) {
validator.Push(fmt.Errorf("Network %s from network group: %s must be a valid IP or CIDR", n.Networks, n.Name)) validator.Push(fmt.Errorf(errFmtAccessControlNetworkGroupIPCIDRInvalid, n.Name, networks))
} }
} }
} }
@ -71,31 +79,31 @@ func ValidateAccessControl(configuration *schema.AccessControlConfiguration, val
} }
// ValidateRules validates an ACL Rule configuration. // ValidateRules validates an ACL Rule configuration.
func ValidateRules(configuration schema.AccessControlConfiguration, validator *schema.StructValidator) { func ValidateRules(config *schema.Configuration, validator *schema.StructValidator) {
if configuration.Rules == nil || len(configuration.Rules) == 0 { if config.AccessControl.Rules == nil || len(config.AccessControl.Rules) == 0 {
if configuration.DefaultPolicy != policyOneFactor && configuration.DefaultPolicy != policyTwoFactor { if config.AccessControl.DefaultPolicy != policyOneFactor && config.AccessControl.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)) validator.Push(fmt.Errorf(errFmtAccessControlDefaultPolicyWithoutRules, config.AccessControl.DefaultPolicy))
return return
} }
validator.PushWarning(fmt.Errorf("No access control rules have been defined so the default policy %s will be applied to all requests", configuration.DefaultPolicy)) validator.PushWarning(fmt.Errorf(errFmtAccessControlWarnNoRulesDefaultPolicy, config.AccessControl.DefaultPolicy))
return return
} }
for i, rule := range configuration.Rules { for i, rule := range config.AccessControl.Rules {
rulePosition := i + 1 rulePosition := i + 1
if len(rule.Domains) == 0 { if len(rule.Domains) == 0 {
validator.Push(fmt.Errorf("Rule #%d is invalid, a policy must have one or more domains", rulePosition)) validator.Push(fmt.Errorf(errFmtAccessControlRuleNoDomains, ruleDescriptor(rulePosition, rule)))
} }
if !IsPolicyValid(rule.Policy) { if !IsPolicyValid(rule.Policy) {
validator.Push(fmt.Errorf("Policy [%s] for rule #%d domain: %s is invalid, a policy must either be 'deny', 'two_factor', 'one_factor' or 'bypass'", rule.Policy, rulePosition, rule.Domains)) validator.Push(fmt.Errorf(errFmtAccessControlRuleInvalidPolicy, ruleDescriptor(rulePosition, rule), rule.Policy))
} }
validateNetworks(rulePosition, rule, configuration, validator) validateNetworks(rulePosition, rule, config.AccessControl, validator)
validateResources(rulePosition, rule, validator) validateResources(rulePosition, rule, validator)
@ -104,16 +112,16 @@ func ValidateRules(configuration schema.AccessControlConfiguration, validator *s
validateMethods(rulePosition, rule, validator) validateMethods(rulePosition, rule, validator)
if rule.Policy == policyBypass && len(rule.Subjects) != 0 { if rule.Policy == policyBypass && len(rule.Subjects) != 0 {
validator.Push(fmt.Errorf(errAccessControlInvalidPolicyWithSubjects, rulePosition, rule.Domains, rule.Subjects)) validator.Push(fmt.Errorf(errAccessControlRuleBypassPolicyInvalidWithSubjects, ruleDescriptor(rulePosition, rule)))
} }
} }
} }
func validateNetworks(rulePosition int, rule schema.ACLRule, configuration schema.AccessControlConfiguration, validator *schema.StructValidator) { func validateNetworks(rulePosition int, rule schema.ACLRule, config schema.AccessControlConfiguration, validator *schema.StructValidator) {
for _, network := range rule.Networks { for _, network := range rule.Networks {
if !IsNetworkValid(network) { if !IsNetworkValid(network) {
if !IsNetworkGroupValid(configuration, network) { if !IsNetworkGroupValid(config, network) {
validator.Push(fmt.Errorf("Network %s for rule #%d domain: %s is not a valid network or network group", rule.Networks, rulePosition, rule.Domains)) validator.Push(fmt.Errorf(errFmtAccessControlRuleNetworksInvalid, ruleDescriptor(rulePosition, rule), network))
} }
} }
} }
@ -122,7 +130,7 @@ func validateNetworks(rulePosition int, rule schema.ACLRule, configuration schem
func validateResources(rulePosition int, rule schema.ACLRule, validator *schema.StructValidator) { func validateResources(rulePosition int, rule schema.ACLRule, validator *schema.StructValidator) {
for _, resource := range rule.Resources { for _, resource := range rule.Resources {
if err := IsResourceValid(resource); err != nil { if err := IsResourceValid(resource); err != nil {
validator.Push(fmt.Errorf("Resource %s for rule #%d domain: %s is invalid, %s", rule.Resources, rulePosition, rule.Domains, err)) validator.Push(fmt.Errorf(errFmtAccessControlRuleResourceInvalid, ruleDescriptor(rulePosition, rule), resource, err))
} }
} }
} }
@ -131,7 +139,7 @@ func validateSubjects(rulePosition int, rule schema.ACLRule, validator *schema.S
for _, subjectRule := range rule.Subjects { for _, subjectRule := range rule.Subjects {
for _, subject := range subjectRule { for _, subject := range subjectRule {
if !IsSubjectValid(subject) { if !IsSubjectValid(subject) {
validator.Push(fmt.Errorf("Subject %s for rule #%d domain: %s is invalid, must start with 'user:' or 'group:'", subjectRule, rulePosition, rule.Domains)) validator.Push(fmt.Errorf(errFmtAccessControlRuleSubjectInvalid, ruleDescriptor(rulePosition, rule), subject))
} }
} }
} }
@ -139,8 +147,8 @@ func validateSubjects(rulePosition int, rule schema.ACLRule, validator *schema.S
func validateMethods(rulePosition int, rule schema.ACLRule, validator *schema.StructValidator) { func validateMethods(rulePosition int, rule schema.ACLRule, validator *schema.StructValidator) {
for _, method := range rule.Methods { for _, method := range rule.Methods {
if !utils.IsStringInSliceFold(method, validHTTPRequestMethods) { if !utils.IsStringInSliceFold(method, validACLRuleMethods) {
validator.Push(fmt.Errorf("Method %s for rule #%d domain: %s is invalid, must be one of the following methods: %s", method, rulePosition, rule.Domains, strings.Join(validHTTPRequestMethods, ", "))) validator.Push(fmt.Errorf(errFmtAccessControlRuleMethodInvalid, ruleDescriptor(rulePosition, rule), method, strings.Join(validACLRuleMethods, "', '")))
} }
} }
} }

View File

@ -12,107 +12,117 @@ import (
type AccessControl struct { type AccessControl struct {
suite.Suite suite.Suite
configuration schema.AccessControlConfiguration config *schema.Configuration
validator *schema.StructValidator validator *schema.StructValidator
} }
func (suite *AccessControl) SetupTest() { func (suite *AccessControl) SetupTest() {
suite.validator = schema.NewStructValidator() suite.validator = schema.NewStructValidator()
suite.configuration.DefaultPolicy = policyDeny suite.config = &schema.Configuration{
suite.configuration.Networks = schema.DefaultACLNetwork AccessControl: schema.AccessControlConfiguration{
suite.configuration.Rules = schema.DefaultACLRule DefaultPolicy: policyDeny,
Networks: schema.DefaultACLNetwork,
Rules: schema.DefaultACLRule,
},
}
} }
func (suite *AccessControl) TestShouldValidateCompleteConfiguration() { func (suite *AccessControl) TestShouldValidateCompleteConfiguration() {
ValidateAccessControl(&suite.configuration, suite.validator) ValidateAccessControl(suite.config, suite.validator)
suite.Assert().False(suite.validator.HasWarnings()) suite.Assert().False(suite.validator.HasWarnings())
suite.Assert().False(suite.validator.HasErrors()) suite.Assert().False(suite.validator.HasErrors())
} }
func (suite *AccessControl) TestShouldRaiseErrorInvalidDefaultPolicy() { func (suite *AccessControl) TestShouldRaiseErrorInvalidDefaultPolicy() {
suite.configuration.DefaultPolicy = testInvalidPolicy suite.config.AccessControl.DefaultPolicy = testInvalidPolicy
ValidateAccessControl(&suite.configuration, suite.validator) ValidateAccessControl(suite.config, suite.validator)
suite.Assert().False(suite.validator.HasWarnings()) suite.Assert().False(suite.validator.HasWarnings())
suite.Require().Len(suite.validator.Errors(), 1) suite.Require().Len(suite.validator.Errors(), 1)
suite.Assert().EqualError(suite.validator.Errors()[0], "'default_policy' must either be 'deny', 'two_factor', 'one_factor' or 'bypass'") suite.Assert().EqualError(suite.validator.Errors()[0], "access control: option 'default_policy' must be one of 'bypass', 'one_factor', 'two_factor', 'deny' but it is configured as 'invalid'")
} }
func (suite *AccessControl) TestShouldRaiseErrorInvalidNetworkGroupNetwork() { func (suite *AccessControl) TestShouldRaiseErrorInvalidNetworkGroupNetwork() {
suite.configuration.Networks = []schema.ACLNetwork{ suite.config.AccessControl.Networks = []schema.ACLNetwork{
{ {
Name: "internal", Name: "internal",
Networks: []string{"abc.def.ghi.jkl"}, Networks: []string{"abc.def.ghi.jkl"},
}, },
} }
ValidateAccessControl(&suite.configuration, suite.validator) ValidateAccessControl(suite.config, suite.validator)
suite.Assert().False(suite.validator.HasWarnings()) suite.Assert().False(suite.validator.HasWarnings())
suite.Require().Len(suite.validator.Errors(), 1) suite.Require().Len(suite.validator.Errors(), 1)
suite.Assert().EqualError(suite.validator.Errors()[0], "Network [abc.def.ghi.jkl] from network group: internal must be a valid IP or CIDR") suite.Assert().EqualError(suite.validator.Errors()[0], "access control: networks: network group 'internal' is invalid: the network 'abc.def.ghi.jkl' is not a valid IP or CIDR notation")
} }
func (suite *AccessControl) TestShouldRaiseErrorWithNoRulesDefined() { func (suite *AccessControl) TestShouldRaiseErrorWithNoRulesDefined() {
suite.configuration.Rules = []schema.ACLRule{} suite.config.AccessControl.Rules = []schema.ACLRule{}
ValidateRules(suite.configuration, suite.validator) ValidateRules(suite.config, suite.validator)
suite.Assert().False(suite.validator.HasWarnings()) suite.Assert().False(suite.validator.HasWarnings())
suite.Require().Len(suite.validator.Errors(), 1) suite.Require().Len(suite.validator.Errors(), 1)
suite.Assert().EqualError(suite.validator.Errors()[0], "Default Policy [deny] is invalid, access control rules must be provided or a policy must either be 'one_factor' or 'two_factor'") suite.Assert().EqualError(suite.validator.Errors()[0], "access control: 'default_policy' option 'deny' is invalid: when no rules are specified it must be 'two_factor' or 'one_factor'")
} }
func (suite *AccessControl) TestShouldRaiseWarningWithNoRulesDefined() { func (suite *AccessControl) TestShouldRaiseWarningWithNoRulesDefined() {
suite.configuration.Rules = []schema.ACLRule{} suite.config.AccessControl.Rules = []schema.ACLRule{}
suite.configuration.DefaultPolicy = policyTwoFactor suite.config.AccessControl.DefaultPolicy = policyTwoFactor
ValidateRules(suite.configuration, suite.validator) ValidateRules(suite.config, suite.validator)
suite.Assert().False(suite.validator.HasErrors()) suite.Assert().False(suite.validator.HasErrors())
suite.Require().Len(suite.validator.Warnings(), 1) suite.Require().Len(suite.validator.Warnings(), 1)
suite.Assert().EqualError(suite.validator.Warnings()[0], "No access control rules have been defined so the default policy two_factor will be applied to all requests") suite.Assert().EqualError(suite.validator.Warnings()[0], "access control: no rules have been specified so the 'default_policy' of 'two_factor' is going to be applied to all requests")
} }
func (suite *AccessControl) TestShouldRaiseErrorsWithEmptyRules() { func (suite *AccessControl) TestShouldRaiseErrorsWithEmptyRules() {
suite.configuration.Rules = []schema.ACLRule{{}, {}} suite.config.AccessControl.Rules = []schema.ACLRule{
{},
{
Policy: "wrong",
},
}
ValidateRules(suite.configuration, suite.validator) ValidateRules(suite.config, suite.validator)
suite.Assert().False(suite.validator.HasWarnings()) suite.Assert().False(suite.validator.HasWarnings())
suite.Require().Len(suite.validator.Errors(), 4) suite.Require().Len(suite.validator.Errors(), 4)
suite.Assert().EqualError(suite.validator.Errors()[0], "Rule #1 is invalid, a policy must have one or more domains") suite.Assert().EqualError(suite.validator.Errors()[0], "access control: rule #1: rule is invalid: must have the option 'domain' configured")
suite.Assert().EqualError(suite.validator.Errors()[1], "Policy [] for rule #1 domain: [] is invalid, a policy must either be 'deny', 'two_factor', 'one_factor' or 'bypass'") suite.Assert().EqualError(suite.validator.Errors()[1], "access control: rule #1: rule 'policy' option '' is invalid: must be one of 'deny', 'two_factor', 'one_factor' or 'bypass'")
suite.Assert().EqualError(suite.validator.Errors()[2], "Rule #2 is invalid, a policy must have one or more domains") suite.Assert().EqualError(suite.validator.Errors()[2], "access control: rule #2: rule is invalid: must have the option 'domain' configured")
suite.Assert().EqualError(suite.validator.Errors()[3], "Policy [] for rule #2 domain: [] is invalid, a policy must either be 'deny', 'two_factor', 'one_factor' or 'bypass'") suite.Assert().EqualError(suite.validator.Errors()[3], "access control: rule #2: rule 'policy' option 'wrong' is invalid: must be one of 'deny', 'two_factor', 'one_factor' or 'bypass'")
} }
func (suite *AccessControl) TestShouldRaiseErrorInvalidPolicy() { func (suite *AccessControl) TestShouldRaiseErrorInvalidPolicy() {
suite.configuration.Rules = []schema.ACLRule{ suite.config.AccessControl.Rules = []schema.ACLRule{
{ {
Domains: []string{"public.example.com"}, Domains: []string{"public.example.com"},
Policy: testInvalidPolicy, Policy: testInvalidPolicy,
}, },
} }
ValidateRules(suite.configuration, suite.validator) ValidateRules(suite.config, suite.validator)
suite.Assert().False(suite.validator.HasWarnings()) suite.Assert().False(suite.validator.HasWarnings())
suite.Require().Len(suite.validator.Errors(), 1) suite.Require().Len(suite.validator.Errors(), 1)
suite.Assert().EqualError(suite.validator.Errors()[0], "Policy [invalid] for rule #1 domain: [public.example.com] is invalid, a policy must either be 'deny', 'two_factor', 'one_factor' or 'bypass'") suite.Assert().EqualError(suite.validator.Errors()[0], "access control: rule #1 (domain 'public.example.com'): rule 'policy' option 'invalid' is invalid: must be one of 'deny', 'two_factor', 'one_factor' or 'bypass'")
} }
func (suite *AccessControl) TestShouldRaiseErrorInvalidNetwork() { func (suite *AccessControl) TestShouldRaiseErrorInvalidNetwork() {
suite.configuration.Rules = []schema.ACLRule{ suite.config.AccessControl.Rules = []schema.ACLRule{
{ {
Domains: []string{"public.example.com"}, Domains: []string{"public.example.com"},
Policy: "bypass", Policy: "bypass",
@ -120,16 +130,16 @@ func (suite *AccessControl) TestShouldRaiseErrorInvalidNetwork() {
}, },
} }
ValidateRules(suite.configuration, suite.validator) ValidateRules(suite.config, suite.validator)
suite.Assert().False(suite.validator.HasWarnings()) suite.Assert().False(suite.validator.HasWarnings())
suite.Require().Len(suite.validator.Errors(), 1) suite.Require().Len(suite.validator.Errors(), 1)
suite.Assert().EqualError(suite.validator.Errors()[0], "Network [abc.def.ghi.jkl/32] for rule #1 domain: [public.example.com] is not a valid network or network group") suite.Assert().EqualError(suite.validator.Errors()[0], "access control: rule #1 (domain 'public.example.com'): the network 'abc.def.ghi.jkl/32' is not a valid Group Name, IP, or CIDR notation")
} }
func (suite *AccessControl) TestShouldRaiseErrorInvalidMethod() { func (suite *AccessControl) TestShouldRaiseErrorInvalidMethod() {
suite.configuration.Rules = []schema.ACLRule{ suite.config.AccessControl.Rules = []schema.ACLRule{
{ {
Domains: []string{"public.example.com"}, Domains: []string{"public.example.com"},
Policy: "bypass", Policy: "bypass",
@ -137,16 +147,16 @@ func (suite *AccessControl) TestShouldRaiseErrorInvalidMethod() {
}, },
} }
ValidateRules(suite.configuration, suite.validator) ValidateRules(suite.config, suite.validator)
suite.Assert().False(suite.validator.HasWarnings()) suite.Assert().False(suite.validator.HasWarnings())
suite.Require().Len(suite.validator.Errors(), 1) suite.Require().Len(suite.validator.Errors(), 1)
suite.Assert().EqualError(suite.validator.Errors()[0], "Method HOP for rule #1 domain: [public.example.com] is invalid, must be one of the following methods: GET, HEAD, POST, PUT, PATCH, DELETE, TRACE, CONNECT, OPTIONS") suite.Assert().EqualError(suite.validator.Errors()[0], "access control: rule #1 (domain 'public.example.com'): 'methods' option 'HOP' is invalid: must be one of 'GET', 'HEAD', 'POST', 'PUT', 'PATCH', 'DELETE', 'TRACE', 'CONNECT', 'OPTIONS'")
} }
func (suite *AccessControl) TestShouldRaiseErrorInvalidResource() { func (suite *AccessControl) TestShouldRaiseErrorInvalidResource() {
suite.configuration.Rules = []schema.ACLRule{ suite.config.AccessControl.Rules = []schema.ACLRule{
{ {
Domains: []string{"public.example.com"}, Domains: []string{"public.example.com"},
Policy: "bypass", Policy: "bypass",
@ -154,18 +164,18 @@ func (suite *AccessControl) TestShouldRaiseErrorInvalidResource() {
}, },
} }
ValidateRules(suite.configuration, suite.validator) ValidateRules(suite.config, suite.validator)
suite.Assert().False(suite.validator.HasWarnings()) suite.Assert().False(suite.validator.HasWarnings())
suite.Require().Len(suite.validator.Errors(), 1) suite.Require().Len(suite.validator.Errors(), 1)
suite.Assert().EqualError(suite.validator.Errors()[0], "Resource [^/(api.*] for rule #1 domain: [public.example.com] is invalid, error parsing regexp: missing closing ): `^/(api.*`") suite.Assert().EqualError(suite.validator.Errors()[0], "access control: rule #1 (domain 'public.example.com'): 'resources' option '^/(api.*' is invalid: error parsing regexp: missing closing ): `^/(api.*`")
} }
func (suite *AccessControl) TestShouldRaiseErrorInvalidSubject() { func (suite *AccessControl) TestShouldRaiseErrorInvalidSubject() {
domains := []string{"public.example.com"} domains := []string{"public.example.com"}
subjects := [][]string{{"invalid"}} subjects := [][]string{{"invalid"}}
suite.configuration.Rules = []schema.ACLRule{ suite.config.AccessControl.Rules = []schema.ACLRule{
{ {
Domains: domains, Domains: domains,
Policy: "bypass", Policy: "bypass",
@ -173,13 +183,13 @@ func (suite *AccessControl) TestShouldRaiseErrorInvalidSubject() {
}, },
} }
ValidateRules(suite.configuration, suite.validator) ValidateRules(suite.config, suite.validator)
suite.Require().Len(suite.validator.Warnings(), 0) suite.Require().Len(suite.validator.Warnings(), 0)
suite.Require().Len(suite.validator.Errors(), 2) suite.Require().Len(suite.validator.Errors(), 2)
suite.Assert().EqualError(suite.validator.Errors()[0], "Subject [invalid] for rule #1 domain: [public.example.com] is invalid, must start with 'user:' or 'group:'") suite.Assert().EqualError(suite.validator.Errors()[0], "access control: rule #1 (domain 'public.example.com'): 'subject' option 'invalid' is invalid: must start with 'user:' or 'group:'")
suite.Assert().EqualError(suite.validator.Errors()[1], fmt.Sprintf(errAccessControlInvalidPolicyWithSubjects, 1, domains, subjects)) suite.Assert().EqualError(suite.validator.Errors()[1], fmt.Sprintf(errAccessControlRuleBypassPolicyInvalidWithSubjects, ruleDescriptor(1, suite.config.AccessControl.Rules[0])))
} }
func TestAccessControl(t *testing.T) { func TestAccessControl(t *testing.T) {

View File

@ -1,7 +1,6 @@
package validator package validator
import ( import (
"errors"
"fmt" "fmt"
"net/url" "net/url"
"strings" "strings"
@ -11,260 +10,254 @@ import (
) )
// ValidateAuthenticationBackend validates and updates the authentication backend configuration. // ValidateAuthenticationBackend validates and updates the authentication backend configuration.
func ValidateAuthenticationBackend(configuration *schema.AuthenticationBackendConfiguration, validator *schema.StructValidator) { func ValidateAuthenticationBackend(config *schema.AuthenticationBackendConfiguration, validator *schema.StructValidator) {
if configuration.LDAP == nil && configuration.File == nil { if config.LDAP == nil && config.File == nil {
validator.Push(errors.New("Please provide `ldap` or `file` object in `authentication_backend`")) validator.Push(fmt.Errorf(errFmtAuthBackendNotConfigured))
} }
if configuration.LDAP != nil && configuration.File != nil { if config.LDAP != nil && config.File != nil {
validator.Push(errors.New("You cannot provide both `ldap` and `file` objects in `authentication_backend`")) validator.Push(fmt.Errorf(errFmtAuthBackendMultipleConfigured))
} }
if configuration.File != nil { if config.File != nil {
validateFileAuthenticationBackend(configuration.File, validator) validateFileAuthenticationBackend(config.File, validator)
} else if configuration.LDAP != nil { } else if config.LDAP != nil {
validateLDAPAuthenticationBackend(configuration.LDAP, validator) validateLDAPAuthenticationBackend(config.LDAP, validator)
} }
if configuration.RefreshInterval == "" { if config.RefreshInterval == "" {
configuration.RefreshInterval = schema.RefreshIntervalDefault config.RefreshInterval = schema.RefreshIntervalDefault
} else { } else {
_, err := utils.ParseDurationString(configuration.RefreshInterval) _, err := utils.ParseDurationString(config.RefreshInterval)
if err != nil && configuration.RefreshInterval != schema.ProfileRefreshDisabled && configuration.RefreshInterval != schema.ProfileRefreshAlways { if err != nil && config.RefreshInterval != schema.ProfileRefreshDisabled && config.RefreshInterval != schema.ProfileRefreshAlways {
validator.Push(fmt.Errorf("Auth Backend `refresh_interval` is configured to '%s' but it must be either a duration notation or one of 'disable', or 'always'. Error from parser: %s", configuration.RefreshInterval, err)) validator.Push(fmt.Errorf(errFmtAuthBackendRefreshInterval, config.RefreshInterval, err))
} }
} }
} }
// validateFileAuthenticationBackend validates and updates the file authentication backend configuration. // validateFileAuthenticationBackend validates and updates the file authentication backend configuration.
func validateFileAuthenticationBackend(configuration *schema.FileAuthenticationBackendConfiguration, validator *schema.StructValidator) { func validateFileAuthenticationBackend(config *schema.FileAuthenticationBackendConfiguration, validator *schema.StructValidator) {
if configuration.Path == "" { if config.Path == "" {
validator.Push(errors.New("Please provide a `path` for the users database in `authentication_backend`")) validator.Push(fmt.Errorf(errFmtFileAuthBackendPathNotConfigured))
} }
if configuration.Password == nil { if config.Password == nil {
configuration.Password = &schema.DefaultPasswordConfiguration config.Password = &schema.DefaultPasswordConfiguration
} else { } else {
// Salt Length. // Salt Length.
switch { switch {
case configuration.Password.SaltLength == 0: case config.Password.SaltLength == 0:
configuration.Password.SaltLength = schema.DefaultPasswordConfiguration.SaltLength config.Password.SaltLength = schema.DefaultPasswordConfiguration.SaltLength
case configuration.Password.SaltLength < 8: case config.Password.SaltLength < 8:
validator.Push(fmt.Errorf("The salt length must be 2 or more, you configured %d", configuration.Password.SaltLength)) validator.Push(fmt.Errorf(errFmtFileAuthBackendPasswordSaltLength, config.Password.SaltLength))
} }
switch configuration.Password.Algorithm { switch config.Password.Algorithm {
case "": case "":
configuration.Password.Algorithm = schema.DefaultPasswordConfiguration.Algorithm config.Password.Algorithm = schema.DefaultPasswordConfiguration.Algorithm
fallthrough fallthrough
case hashArgon2id: case hashArgon2id:
validateFileAuthenticationBackendArgon2id(configuration, validator) validateFileAuthenticationBackendArgon2id(config, validator)
case hashSHA512: case hashSHA512:
validateFileAuthenticationBackendSHA512(configuration) validateFileAuthenticationBackendSHA512(config)
default: default:
validator.Push(fmt.Errorf("Unknown hashing algorithm supplied, valid values are argon2id and sha512, you configured '%s'", configuration.Password.Algorithm)) validator.Push(fmt.Errorf(errFmtFileAuthBackendPasswordUnknownAlg, config.Password.Algorithm))
} }
if configuration.Password.Iterations < 1 { if config.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)) validator.Push(fmt.Errorf(errFmtFileAuthBackendPasswordInvalidIterations, config.Password.Iterations))
} }
} }
} }
func validateFileAuthenticationBackendSHA512(configuration *schema.FileAuthenticationBackendConfiguration) { func validateFileAuthenticationBackendSHA512(config *schema.FileAuthenticationBackendConfiguration) {
// Iterations (time). // Iterations (time).
if configuration.Password.Iterations == 0 { if config.Password.Iterations == 0 {
configuration.Password.Iterations = schema.DefaultPasswordSHA512Configuration.Iterations config.Password.Iterations = schema.DefaultPasswordSHA512Configuration.Iterations
} }
} }
func validateFileAuthenticationBackendArgon2id(configuration *schema.FileAuthenticationBackendConfiguration, validator *schema.StructValidator) { func validateFileAuthenticationBackendArgon2id(config *schema.FileAuthenticationBackendConfiguration, validator *schema.StructValidator) {
// Iterations (time). // Iterations (time).
if configuration.Password.Iterations == 0 { if config.Password.Iterations == 0 {
configuration.Password.Iterations = schema.DefaultPasswordConfiguration.Iterations config.Password.Iterations = schema.DefaultPasswordConfiguration.Iterations
} }
// Parallelism. // Parallelism.
if configuration.Password.Parallelism == 0 { if config.Password.Parallelism == 0 {
configuration.Password.Parallelism = schema.DefaultPasswordConfiguration.Parallelism config.Password.Parallelism = schema.DefaultPasswordConfiguration.Parallelism
} else if configuration.Password.Parallelism < 1 { } else if config.Password.Parallelism < 1 {
validator.Push(fmt.Errorf("Parallelism for argon2id must be 1 or more, you configured %d", configuration.Password.Parallelism)) validator.Push(fmt.Errorf(errFmtFileAuthBackendPasswordArgon2idInvalidParallelism, config.Password.Parallelism))
} }
// Memory. // Memory.
if configuration.Password.Memory == 0 { if config.Password.Memory == 0 {
configuration.Password.Memory = schema.DefaultPasswordConfiguration.Memory config.Password.Memory = schema.DefaultPasswordConfiguration.Memory
} else if configuration.Password.Memory < configuration.Password.Parallelism*8 { } else if config.Password.Memory < config.Password.Parallelism*8 {
validator.Push(fmt.Errorf("Memory for argon2id must be %d or more (parallelism * 8), you configured memory as %d and parallelism as %d", configuration.Password.Parallelism*8, configuration.Password.Memory, configuration.Password.Parallelism)) validator.Push(fmt.Errorf(errFmtFileAuthBackendPasswordArgon2idInvalidMemory, config.Password.Parallelism, config.Password.Parallelism*8, config.Password.Memory))
} }
// Key Length. // Key Length.
if configuration.Password.KeyLength == 0 { if config.Password.KeyLength == 0 {
configuration.Password.KeyLength = schema.DefaultPasswordConfiguration.KeyLength config.Password.KeyLength = schema.DefaultPasswordConfiguration.KeyLength
} else if configuration.Password.KeyLength < 16 { } else if config.Password.KeyLength < 16 {
validator.Push(fmt.Errorf("Key length for argon2id must be 16, you configured %d", configuration.Password.KeyLength)) validator.Push(fmt.Errorf(errFmtFileAuthBackendPasswordArgon2idInvalidKeyLength, config.Password.KeyLength))
} }
} }
func validateLDAPAuthenticationBackend(configuration *schema.LDAPAuthenticationBackendConfiguration, validator *schema.StructValidator) { func validateLDAPAuthenticationBackend(config *schema.LDAPAuthenticationBackendConfiguration, validator *schema.StructValidator) {
if configuration.Timeout == 0 { if config.Timeout == 0 {
configuration.Timeout = schema.DefaultLDAPAuthenticationBackendConfiguration.Timeout config.Timeout = schema.DefaultLDAPAuthenticationBackendConfiguration.Timeout
} }
if configuration.Implementation == "" { if config.Implementation == "" {
configuration.Implementation = schema.DefaultLDAPAuthenticationBackendConfiguration.Implementation config.Implementation = schema.DefaultLDAPAuthenticationBackendConfiguration.Implementation
} }
if configuration.TLS == nil { if config.TLS == nil {
configuration.TLS = schema.DefaultLDAPAuthenticationBackendConfiguration.TLS config.TLS = schema.DefaultLDAPAuthenticationBackendConfiguration.TLS
} }
if configuration.TLS.MinimumVersion == "" { if config.TLS.MinimumVersion == "" {
configuration.TLS.MinimumVersion = schema.DefaultLDAPAuthenticationBackendConfiguration.TLS.MinimumVersion config.TLS.MinimumVersion = schema.DefaultLDAPAuthenticationBackendConfiguration.TLS.MinimumVersion
} }
if _, err := utils.TLSStringToTLSConfigVersion(configuration.TLS.MinimumVersion); err != nil { if _, err := utils.TLSStringToTLSConfigVersion(config.TLS.MinimumVersion); err != nil {
validator.Push(fmt.Errorf("error occurred validating the LDAP minimum_tls_version key with value %s: %v", configuration.TLS.MinimumVersion, err)) validator.Push(fmt.Errorf(errFmtLDAPAuthBackendTLSMinVersion, config.TLS.MinimumVersion, err))
} }
switch configuration.Implementation { switch config.Implementation {
case schema.LDAPImplementationCustom: case schema.LDAPImplementationCustom:
setDefaultImplementationCustomLDAPAuthenticationBackend(configuration) setDefaultImplementationCustomLDAPAuthenticationBackend(config)
case schema.LDAPImplementationActiveDirectory: case schema.LDAPImplementationActiveDirectory:
setDefaultImplementationActiveDirectoryLDAPAuthenticationBackend(configuration) setDefaultImplementationActiveDirectoryLDAPAuthenticationBackend(config)
default: default:
validator.Push(fmt.Errorf("authentication backend ldap implementation must be blank or one of the following values `%s`, `%s`", schema.LDAPImplementationCustom, schema.LDAPImplementationActiveDirectory)) validator.Push(fmt.Errorf(errFmtLDAPAuthBackendImplementation, config.Implementation, strings.Join([]string{schema.LDAPImplementationCustom, schema.LDAPImplementationActiveDirectory}, "', '")))
} }
if strings.Contains(configuration.UsersFilter, "{0}") { if strings.Contains(config.UsersFilter, "{0}") {
validator.Push(fmt.Errorf("authentication backend ldap users filter must not contain removed placeholders" + validator.Push(fmt.Errorf(errFmtLDAPAuthBackendFilterReplacedPlaceholders, "users_filter", "{0}", "{input}"))
", {0} has been replaced with {input}"))
} }
if strings.Contains(configuration.GroupsFilter, "{0}") || if strings.Contains(config.GroupsFilter, "{0}") {
strings.Contains(configuration.GroupsFilter, "{1}") { validator.Push(fmt.Errorf(errFmtLDAPAuthBackendFilterReplacedPlaceholders, "groups_filter", "{0}", "{input}"))
validator.Push(fmt.Errorf("authentication backend ldap groups filter must not contain removed " +
"placeholders, {0} has been replaced with {input} and {1} has been replaced with {username}"))
} }
if configuration.URL == "" { if strings.Contains(config.GroupsFilter, "{1}") {
validator.Push(errors.New("Please provide a URL to the LDAP server")) validator.Push(fmt.Errorf(errFmtLDAPAuthBackendFilterReplacedPlaceholders, "groups_filter", "{1}", "{username}"))
}
if config.URL == "" {
validator.Push(fmt.Errorf(errFmtLDAPAuthBackendMissingOption, "url"))
} else { } else {
ldapURL, serverName := validateLDAPURL(configuration.URL, validator) validateLDAPAuthenticationBackendURL(config, validator)
}
configuration.URL = ldapURL validateLDAPRequiredParameters(config, validator)
}
if configuration.TLS.ServerName == "" { func validateLDAPAuthenticationBackendURL(config *schema.LDAPAuthenticationBackendConfiguration, validator *schema.StructValidator) {
configuration.TLS.ServerName = serverName var (
parsedURL *url.URL
err error
)
if parsedURL, err = url.Parse(config.URL); err != nil {
validator.Push(fmt.Errorf(errFmtLDAPAuthBackendURLNotParsable, err))
return
}
if parsedURL.Scheme != schemeLDAP && parsedURL.Scheme != schemeLDAPS {
validator.Push(fmt.Errorf(errFmtLDAPAuthBackendURLInvalidScheme, parsedURL.Scheme))
return
}
config.URL = parsedURL.String()
if config.TLS.ServerName == "" {
config.TLS.ServerName = parsedURL.Hostname()
} }
} }
validateLDAPRequiredParameters(configuration, validator) func validateLDAPRequiredParameters(config *schema.LDAPAuthenticationBackendConfiguration, validator *schema.StructValidator) {
}
// Wrapper for test purposes to exclude the hostname from the return.
func validateLDAPURLSimple(ldapURL string, validator *schema.StructValidator) (finalURL string) {
finalURL, _ = validateLDAPURL(ldapURL, validator)
return finalURL
}
func validateLDAPURL(ldapURL string, validator *schema.StructValidator) (finalURL string, hostname string) {
parsedURL, err := url.Parse(ldapURL)
if err != nil {
validator.Push(errors.New("Unable to parse URL to ldap server. The scheme is probably missing: ldap:// or ldaps://"))
return "", ""
}
if !(parsedURL.Scheme == schemeLDAP || parsedURL.Scheme == schemeLDAPS) {
validator.Push(errors.New("Unknown scheme for ldap url, should be ldap:// or ldaps://"))
return "", ""
}
return parsedURL.String(), parsedURL.Hostname()
}
func validateLDAPRequiredParameters(configuration *schema.LDAPAuthenticationBackendConfiguration, validator *schema.StructValidator) {
// TODO: see if it's possible to disable this check if disable_reset_password is set and when anonymous/user binding is supported (#101 and #387). // TODO: see if it's possible to disable this check if disable_reset_password is set and when anonymous/user binding is supported (#101 and #387).
if configuration.User == "" { if config.User == "" {
validator.Push(errors.New("Please provide a user name to connect to the LDAP server")) validator.Push(fmt.Errorf(errFmtLDAPAuthBackendMissingOption, "user"))
} }
// TODO: see if it's possible to disable this check if disable_reset_password is set and when anonymous/user binding is supported (#101 and #387). // TODO: see if it's possible to disable this check if disable_reset_password is set and when anonymous/user binding is supported (#101 and #387).
if configuration.Password == "" { if config.Password == "" {
validator.Push(errors.New("Please provide a password to connect to the LDAP server")) validator.Push(fmt.Errorf(errFmtLDAPAuthBackendMissingOption, "password"))
} }
if configuration.BaseDN == "" { if config.BaseDN == "" {
validator.Push(errors.New("Please provide a base DN to connect to the LDAP server")) validator.Push(fmt.Errorf(errFmtLDAPAuthBackendMissingOption, "base_dn"))
} }
if configuration.UsersFilter == "" { if config.UsersFilter == "" {
validator.Push(errors.New("Please provide a users filter with `users_filter` attribute")) validator.Push(fmt.Errorf(errFmtLDAPAuthBackendMissingOption, "users_filter"))
} else { } else {
if !strings.HasPrefix(configuration.UsersFilter, "(") || !strings.HasSuffix(configuration.UsersFilter, ")") { if !strings.HasPrefix(config.UsersFilter, "(") || !strings.HasSuffix(config.UsersFilter, ")") {
validator.Push(errors.New("The users filter should contain enclosing parenthesis. For instance {username_attribute}={input} should be ({username_attribute}={input})")) validator.Push(fmt.Errorf(errFmtLDAPAuthBackendFilterEnclosingParenthesis, "users_filter", config.UsersFilter, config.UsersFilter))
} }
if !strings.Contains(configuration.UsersFilter, "{username_attribute}") { if !strings.Contains(config.UsersFilter, "{username_attribute}") {
validator.Push(errors.New("Unable to detect {username_attribute} placeholder in users_filter, your configuration is broken. " + validator.Push(fmt.Errorf(errFmtLDAPAuthBackendFilterMissingPlaceholder, "users_filter", "username_attribute"))
"Please review configuration options listed at https://www.authelia.com/docs/configuration/authentication/ldap.html"))
} }
// This test helps the user know that users_filter is broken after the breaking change induced by this commit. // This test helps the user know that users_filter is broken after the breaking change induced by this commit.
if !strings.Contains(configuration.UsersFilter, "{0}") && !strings.Contains(configuration.UsersFilter, "{input}") { if !strings.Contains(config.UsersFilter, "{input}") {
validator.Push(errors.New("Unable to detect {input} placeholder in users_filter, your configuration might be broken. " + validator.Push(fmt.Errorf(errFmtLDAPAuthBackendFilterMissingPlaceholder, "users_filter", "input"))
"Please review configuration options listed at https://www.authelia.com/docs/configuration/authentication/ldap.html"))
} }
} }
if configuration.GroupsFilter == "" { if config.GroupsFilter == "" {
validator.Push(errors.New("Please provide a groups filter with `groups_filter` attribute")) validator.Push(fmt.Errorf(errFmtLDAPAuthBackendMissingOption, "groups_filter"))
} else if !strings.HasPrefix(configuration.GroupsFilter, "(") || !strings.HasSuffix(configuration.GroupsFilter, ")") { } else if !strings.HasPrefix(config.GroupsFilter, "(") || !strings.HasSuffix(config.GroupsFilter, ")") {
validator.Push(errors.New("The groups filter should contain enclosing parenthesis. For instance cn={input} should be (cn={input})")) validator.Push(fmt.Errorf(errFmtLDAPAuthBackendFilterEnclosingParenthesis, "groups_filter", config.GroupsFilter, config.GroupsFilter))
} }
} }
func setDefaultImplementationActiveDirectoryLDAPAuthenticationBackend(configuration *schema.LDAPAuthenticationBackendConfiguration) { func setDefaultImplementationActiveDirectoryLDAPAuthenticationBackend(config *schema.LDAPAuthenticationBackendConfiguration) {
if configuration.UsersFilter == "" { if config.UsersFilter == "" {
configuration.UsersFilter = schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.UsersFilter config.UsersFilter = schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.UsersFilter
} }
if configuration.UsernameAttribute == "" { if config.UsernameAttribute == "" {
configuration.UsernameAttribute = schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.UsernameAttribute config.UsernameAttribute = schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.UsernameAttribute
} }
if configuration.DisplayNameAttribute == "" { if config.DisplayNameAttribute == "" {
configuration.DisplayNameAttribute = schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.DisplayNameAttribute config.DisplayNameAttribute = schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.DisplayNameAttribute
} }
if configuration.MailAttribute == "" { if config.MailAttribute == "" {
configuration.MailAttribute = schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.MailAttribute config.MailAttribute = schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.MailAttribute
} }
if configuration.GroupsFilter == "" { if config.GroupsFilter == "" {
configuration.GroupsFilter = schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.GroupsFilter config.GroupsFilter = schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.GroupsFilter
} }
if configuration.GroupNameAttribute == "" { if config.GroupNameAttribute == "" {
configuration.GroupNameAttribute = schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.GroupNameAttribute config.GroupNameAttribute = schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.GroupNameAttribute
} }
} }
func setDefaultImplementationCustomLDAPAuthenticationBackend(configuration *schema.LDAPAuthenticationBackendConfiguration) { func setDefaultImplementationCustomLDAPAuthenticationBackend(config *schema.LDAPAuthenticationBackendConfiguration) {
if configuration.UsernameAttribute == "" { if config.UsernameAttribute == "" {
configuration.UsernameAttribute = schema.DefaultLDAPAuthenticationBackendConfiguration.UsernameAttribute config.UsernameAttribute = schema.DefaultLDAPAuthenticationBackendConfiguration.UsernameAttribute
} }
if configuration.GroupNameAttribute == "" { if config.GroupNameAttribute == "" {
configuration.GroupNameAttribute = schema.DefaultLDAPAuthenticationBackendConfiguration.GroupNameAttribute config.GroupNameAttribute = schema.DefaultLDAPAuthenticationBackendConfiguration.GroupNameAttribute
} }
if configuration.MailAttribute == "" { if config.MailAttribute == "" {
configuration.MailAttribute = schema.DefaultLDAPAuthenticationBackendConfiguration.MailAttribute config.MailAttribute = schema.DefaultLDAPAuthenticationBackendConfiguration.MailAttribute
} }
if configuration.DisplayNameAttribute == "" { if config.DisplayNameAttribute == "" {
configuration.DisplayNameAttribute = schema.DefaultLDAPAuthenticationBackendConfiguration.DisplayNameAttribute config.DisplayNameAttribute = schema.DefaultLDAPAuthenticationBackendConfiguration.DisplayNameAttribute
} }
} }

View File

@ -23,7 +23,7 @@ func TestShouldRaiseErrorWhenBothBackendsProvided(t *testing.T) {
ValidateAuthenticationBackend(&backendConfig, validator) ValidateAuthenticationBackend(&backendConfig, validator)
require.Len(t, validator.Errors(), 1) require.Len(t, validator.Errors(), 1)
assert.EqualError(t, validator.Errors()[0], "You cannot provide both `ldap` and `file` objects in `authentication_backend`") assert.EqualError(t, validator.Errors()[0], "authentication_backend: please ensure only one of the 'file' or 'ldap' backend is configured")
} }
func TestShouldRaiseErrorWhenNoBackendProvided(t *testing.T) { func TestShouldRaiseErrorWhenNoBackendProvided(t *testing.T) {
@ -33,19 +33,19 @@ func TestShouldRaiseErrorWhenNoBackendProvided(t *testing.T) {
ValidateAuthenticationBackend(&backendConfig, validator) ValidateAuthenticationBackend(&backendConfig, validator)
require.Len(t, validator.Errors(), 1) require.Len(t, validator.Errors(), 1)
assert.EqualError(t, validator.Errors()[0], "Please provide `ldap` or `file` object in `authentication_backend`") assert.EqualError(t, validator.Errors()[0], "authentication_backend: you must ensure either the 'file' or 'ldap' authentication backend is configured")
} }
type FileBasedAuthenticationBackend struct { type FileBasedAuthenticationBackend struct {
suite.Suite suite.Suite
configuration schema.AuthenticationBackendConfiguration config schema.AuthenticationBackendConfiguration
validator *schema.StructValidator validator *schema.StructValidator
} }
func (suite *FileBasedAuthenticationBackend) SetupTest() { func (suite *FileBasedAuthenticationBackend) SetupTest() {
suite.validator = schema.NewStructValidator() suite.validator = schema.NewStructValidator()
suite.configuration = schema.AuthenticationBackendConfiguration{} suite.config = schema.AuthenticationBackendConfiguration{}
suite.configuration.File = &schema.FileAuthenticationBackendConfiguration{Path: "/a/path", Password: &schema.PasswordConfiguration{ suite.config.File = &schema.FileAuthenticationBackendConfiguration{Path: "/a/path", Password: &schema.PasswordConfiguration{
Algorithm: schema.DefaultPasswordConfiguration.Algorithm, Algorithm: schema.DefaultPasswordConfiguration.Algorithm,
Iterations: schema.DefaultPasswordConfiguration.Iterations, Iterations: schema.DefaultPasswordConfiguration.Iterations,
Parallelism: schema.DefaultPasswordConfiguration.Parallelism, Parallelism: schema.DefaultPasswordConfiguration.Parallelism,
@ -53,150 +53,150 @@ func (suite *FileBasedAuthenticationBackend) SetupTest() {
KeyLength: schema.DefaultPasswordConfiguration.KeyLength, KeyLength: schema.DefaultPasswordConfiguration.KeyLength,
SaltLength: schema.DefaultPasswordConfiguration.SaltLength, SaltLength: schema.DefaultPasswordConfiguration.SaltLength,
}} }}
suite.configuration.File.Password.Algorithm = schema.DefaultPasswordConfiguration.Algorithm suite.config.File.Password.Algorithm = schema.DefaultPasswordConfiguration.Algorithm
} }
func (suite *FileBasedAuthenticationBackend) TestShouldValidateCompleteConfiguration() { func (suite *FileBasedAuthenticationBackend) TestShouldValidateCompleteConfiguration() {
ValidateAuthenticationBackend(&suite.configuration, suite.validator) ValidateAuthenticationBackend(&suite.config, suite.validator)
suite.Assert().False(suite.validator.HasWarnings()) suite.Assert().False(suite.validator.HasWarnings())
suite.Assert().False(suite.validator.HasErrors()) suite.Assert().False(suite.validator.HasErrors())
} }
func (suite *FileBasedAuthenticationBackend) TestShouldRaiseErrorWhenNoPathProvided() { func (suite *FileBasedAuthenticationBackend) TestShouldRaiseErrorWhenNoPathProvided() {
suite.configuration.File.Path = "" suite.config.File.Path = ""
ValidateAuthenticationBackend(&suite.configuration, suite.validator) ValidateAuthenticationBackend(&suite.config, suite.validator)
suite.Assert().False(suite.validator.HasWarnings()) suite.Assert().False(suite.validator.HasWarnings())
suite.Require().Len(suite.validator.Errors(), 1) suite.Require().Len(suite.validator.Errors(), 1)
suite.Assert().EqualError(suite.validator.Errors()[0], "Please provide a `path` for the users database in `authentication_backend`") suite.Assert().EqualError(suite.validator.Errors()[0], "authentication_backend: file: option 'path' is required")
} }
func (suite *FileBasedAuthenticationBackend) TestShouldRaiseErrorWhenMemoryNotMoreThanEightTimesParallelism() { func (suite *FileBasedAuthenticationBackend) TestShouldRaiseErrorWhenMemoryNotMoreThanEightTimesParallelism() {
suite.configuration.File.Password.Memory = 8 suite.config.File.Password.Memory = 8
suite.configuration.File.Password.Parallelism = 2 suite.config.File.Password.Parallelism = 2
ValidateAuthenticationBackend(&suite.configuration, suite.validator) ValidateAuthenticationBackend(&suite.config, suite.validator)
suite.Assert().False(suite.validator.HasWarnings()) suite.Assert().False(suite.validator.HasWarnings())
suite.Require().Len(suite.validator.Errors(), 1) suite.Require().Len(suite.validator.Errors(), 1)
suite.Assert().EqualError(suite.validator.Errors()[0], "Memory for argon2id must be 16 or more (parallelism * 8), you configured memory as 8 and parallelism as 2") suite.Assert().EqualError(suite.validator.Errors()[0], "authentication_backend: file: password: option 'memory' must at least be parallelism multiplied by 8 when using algorithm 'argon2id' with parallelism 2 it should be at least 16 but it is configured as '8'")
} }
func (suite *FileBasedAuthenticationBackend) TestShouldSetDefaultConfigurationWhenBlank() { func (suite *FileBasedAuthenticationBackend) TestShouldSetDefaultConfigurationWhenBlank() {
suite.configuration.File.Password = &schema.PasswordConfiguration{} suite.config.File.Password = &schema.PasswordConfiguration{}
suite.Assert().Equal(0, suite.configuration.File.Password.KeyLength) suite.Assert().Equal(0, suite.config.File.Password.KeyLength)
suite.Assert().Equal(0, suite.configuration.File.Password.Iterations) suite.Assert().Equal(0, suite.config.File.Password.Iterations)
suite.Assert().Equal(0, suite.configuration.File.Password.SaltLength) suite.Assert().Equal(0, suite.config.File.Password.SaltLength)
suite.Assert().Equal("", suite.configuration.File.Password.Algorithm) suite.Assert().Equal("", suite.config.File.Password.Algorithm)
suite.Assert().Equal(0, suite.configuration.File.Password.Memory) suite.Assert().Equal(0, suite.config.File.Password.Memory)
suite.Assert().Equal(0, suite.configuration.File.Password.Parallelism) suite.Assert().Equal(0, suite.config.File.Password.Parallelism)
ValidateAuthenticationBackend(&suite.configuration, suite.validator) ValidateAuthenticationBackend(&suite.config, suite.validator)
suite.Assert().False(suite.validator.HasWarnings()) suite.Assert().False(suite.validator.HasWarnings())
suite.Assert().Len(suite.validator.Errors(), 0) suite.Assert().Len(suite.validator.Errors(), 0)
suite.Assert().Equal(schema.DefaultPasswordConfiguration.KeyLength, suite.configuration.File.Password.KeyLength) suite.Assert().Equal(schema.DefaultPasswordConfiguration.KeyLength, suite.config.File.Password.KeyLength)
suite.Assert().Equal(schema.DefaultPasswordConfiguration.Iterations, suite.configuration.File.Password.Iterations) suite.Assert().Equal(schema.DefaultPasswordConfiguration.Iterations, suite.config.File.Password.Iterations)
suite.Assert().Equal(schema.DefaultPasswordConfiguration.SaltLength, suite.configuration.File.Password.SaltLength) suite.Assert().Equal(schema.DefaultPasswordConfiguration.SaltLength, suite.config.File.Password.SaltLength)
suite.Assert().Equal(schema.DefaultPasswordConfiguration.Algorithm, suite.configuration.File.Password.Algorithm) suite.Assert().Equal(schema.DefaultPasswordConfiguration.Algorithm, suite.config.File.Password.Algorithm)
suite.Assert().Equal(schema.DefaultPasswordConfiguration.Memory, suite.configuration.File.Password.Memory) suite.Assert().Equal(schema.DefaultPasswordConfiguration.Memory, suite.config.File.Password.Memory)
suite.Assert().Equal(schema.DefaultPasswordConfiguration.Parallelism, suite.configuration.File.Password.Parallelism) suite.Assert().Equal(schema.DefaultPasswordConfiguration.Parallelism, suite.config.File.Password.Parallelism)
} }
func (suite *FileBasedAuthenticationBackend) TestShouldSetDefaultConfigurationWhenOnlySHA512Set() { func (suite *FileBasedAuthenticationBackend) TestShouldSetDefaultConfigurationWhenOnlySHA512Set() {
suite.configuration.File.Password = &schema.PasswordConfiguration{} suite.config.File.Password = &schema.PasswordConfiguration{}
suite.Assert().Equal("", suite.configuration.File.Password.Algorithm) suite.Assert().Equal("", suite.config.File.Password.Algorithm)
suite.configuration.File.Password.Algorithm = "sha512" suite.config.File.Password.Algorithm = "sha512"
ValidateAuthenticationBackend(&suite.configuration, suite.validator) ValidateAuthenticationBackend(&suite.config, suite.validator)
suite.Assert().False(suite.validator.HasWarnings()) suite.Assert().False(suite.validator.HasWarnings())
suite.Assert().Len(suite.validator.Errors(), 0) suite.Assert().Len(suite.validator.Errors(), 0)
suite.Assert().Equal(schema.DefaultPasswordSHA512Configuration.KeyLength, suite.configuration.File.Password.KeyLength) suite.Assert().Equal(schema.DefaultPasswordSHA512Configuration.KeyLength, suite.config.File.Password.KeyLength)
suite.Assert().Equal(schema.DefaultPasswordSHA512Configuration.Iterations, suite.configuration.File.Password.Iterations) suite.Assert().Equal(schema.DefaultPasswordSHA512Configuration.Iterations, suite.config.File.Password.Iterations)
suite.Assert().Equal(schema.DefaultPasswordSHA512Configuration.SaltLength, suite.configuration.File.Password.SaltLength) suite.Assert().Equal(schema.DefaultPasswordSHA512Configuration.SaltLength, suite.config.File.Password.SaltLength)
suite.Assert().Equal(schema.DefaultPasswordSHA512Configuration.Algorithm, suite.configuration.File.Password.Algorithm) suite.Assert().Equal(schema.DefaultPasswordSHA512Configuration.Algorithm, suite.config.File.Password.Algorithm)
suite.Assert().Equal(schema.DefaultPasswordSHA512Configuration.Memory, suite.configuration.File.Password.Memory) suite.Assert().Equal(schema.DefaultPasswordSHA512Configuration.Memory, suite.config.File.Password.Memory)
suite.Assert().Equal(schema.DefaultPasswordSHA512Configuration.Parallelism, suite.configuration.File.Password.Parallelism) suite.Assert().Equal(schema.DefaultPasswordSHA512Configuration.Parallelism, suite.config.File.Password.Parallelism)
} }
func (suite *FileBasedAuthenticationBackend) TestShouldRaiseErrorWhenKeyLengthTooLow() { func (suite *FileBasedAuthenticationBackend) TestShouldRaiseErrorWhenKeyLengthTooLow() {
suite.configuration.File.Password.KeyLength = 1 suite.config.File.Password.KeyLength = 1
ValidateAuthenticationBackend(&suite.configuration, suite.validator) ValidateAuthenticationBackend(&suite.config, suite.validator)
suite.Assert().False(suite.validator.HasWarnings()) suite.Assert().False(suite.validator.HasWarnings())
suite.Require().Len(suite.validator.Errors(), 1) suite.Require().Len(suite.validator.Errors(), 1)
suite.Assert().EqualError(suite.validator.Errors()[0], "Key length for argon2id must be 16, you configured 1") suite.Assert().EqualError(suite.validator.Errors()[0], "authentication_backend: file: password: option 'key_length' must be 16 or more when using algorithm 'argon2id' but it is configured as '1'")
} }
func (suite *FileBasedAuthenticationBackend) TestShouldRaiseErrorWhenSaltLengthTooLow() { func (suite *FileBasedAuthenticationBackend) TestShouldRaiseErrorWhenSaltLengthTooLow() {
suite.configuration.File.Password.SaltLength = -1 suite.config.File.Password.SaltLength = -1
ValidateAuthenticationBackend(&suite.configuration, suite.validator) ValidateAuthenticationBackend(&suite.config, suite.validator)
suite.Assert().False(suite.validator.HasWarnings()) suite.Assert().False(suite.validator.HasWarnings())
suite.Require().Len(suite.validator.Errors(), 1) suite.Require().Len(suite.validator.Errors(), 1)
suite.Assert().EqualError(suite.validator.Errors()[0], "The salt length must be 2 or more, you configured -1") suite.Assert().EqualError(suite.validator.Errors()[0], "authentication_backend: file: password: option 'salt_length' must be 2 or more but it is configured a '-1'")
} }
func (suite *FileBasedAuthenticationBackend) TestShouldRaiseErrorWhenBadAlgorithmDefined() { func (suite *FileBasedAuthenticationBackend) TestShouldRaiseErrorWhenBadAlgorithmDefined() {
suite.configuration.File.Password.Algorithm = "bogus" suite.config.File.Password.Algorithm = "bogus"
ValidateAuthenticationBackend(&suite.configuration, suite.validator) ValidateAuthenticationBackend(&suite.config, suite.validator)
suite.Assert().False(suite.validator.HasWarnings()) suite.Assert().False(suite.validator.HasWarnings())
suite.Require().Len(suite.validator.Errors(), 1) suite.Require().Len(suite.validator.Errors(), 1)
suite.Assert().EqualError(suite.validator.Errors()[0], "Unknown hashing algorithm supplied, valid values are argon2id and sha512, you configured 'bogus'") suite.Assert().EqualError(suite.validator.Errors()[0], "authentication_backend: file: password: option 'algorithm' must be either 'argon2id' or 'sha512' but it is configured as 'bogus'")
} }
func (suite *FileBasedAuthenticationBackend) TestShouldRaiseErrorWhenIterationsTooLow() { func (suite *FileBasedAuthenticationBackend) TestShouldRaiseErrorWhenIterationsTooLow() {
suite.configuration.File.Password.Iterations = -1 suite.config.File.Password.Iterations = -1
ValidateAuthenticationBackend(&suite.configuration, suite.validator) ValidateAuthenticationBackend(&suite.config, suite.validator)
suite.Assert().False(suite.validator.HasWarnings()) suite.Assert().False(suite.validator.HasWarnings())
suite.Require().Len(suite.validator.Errors(), 1) suite.Require().Len(suite.validator.Errors(), 1)
suite.Assert().EqualError(suite.validator.Errors()[0], "The number of iterations specified is invalid, must be 1 or more, you configured -1") suite.Assert().EqualError(suite.validator.Errors()[0], "authentication_backend: file: password: option 'iterations' must be 1 or more but it is configured as '-1'")
} }
func (suite *FileBasedAuthenticationBackend) TestShouldRaiseErrorWhenParallelismTooLow() { func (suite *FileBasedAuthenticationBackend) TestShouldRaiseErrorWhenParallelismTooLow() {
suite.configuration.File.Password.Parallelism = -1 suite.config.File.Password.Parallelism = -1
ValidateAuthenticationBackend(&suite.configuration, suite.validator) ValidateAuthenticationBackend(&suite.config, suite.validator)
suite.Assert().False(suite.validator.HasWarnings()) suite.Assert().False(suite.validator.HasWarnings())
suite.Require().Len(suite.validator.Errors(), 1) suite.Require().Len(suite.validator.Errors(), 1)
suite.Assert().EqualError(suite.validator.Errors()[0], "Parallelism for argon2id must be 1 or more, you configured -1") suite.Assert().EqualError(suite.validator.Errors()[0], "authentication_backend: file: password: option 'parallelism' must be 1 or more when using algorithm 'argon2id' but it is configured as '-1'")
} }
func (suite *FileBasedAuthenticationBackend) TestShouldSetDefaultValues() { func (suite *FileBasedAuthenticationBackend) TestShouldSetDefaultValues() {
suite.configuration.File.Password.Algorithm = "" suite.config.File.Password.Algorithm = ""
suite.configuration.File.Password.Iterations = 0 suite.config.File.Password.Iterations = 0
suite.configuration.File.Password.SaltLength = 0 suite.config.File.Password.SaltLength = 0
suite.configuration.File.Password.Memory = 0 suite.config.File.Password.Memory = 0
suite.configuration.File.Password.Parallelism = 0 suite.config.File.Password.Parallelism = 0
ValidateAuthenticationBackend(&suite.configuration, suite.validator) ValidateAuthenticationBackend(&suite.config, suite.validator)
suite.Assert().False(suite.validator.HasWarnings()) suite.Assert().False(suite.validator.HasWarnings())
suite.Assert().False(suite.validator.HasErrors()) suite.Assert().False(suite.validator.HasErrors())
suite.Assert().Equal(schema.DefaultPasswordConfiguration.Algorithm, suite.configuration.File.Password.Algorithm) suite.Assert().Equal(schema.DefaultPasswordConfiguration.Algorithm, suite.config.File.Password.Algorithm)
suite.Assert().Equal(schema.DefaultPasswordConfiguration.Iterations, suite.configuration.File.Password.Iterations) suite.Assert().Equal(schema.DefaultPasswordConfiguration.Iterations, suite.config.File.Password.Iterations)
suite.Assert().Equal(schema.DefaultPasswordConfiguration.SaltLength, suite.configuration.File.Password.SaltLength) suite.Assert().Equal(schema.DefaultPasswordConfiguration.SaltLength, suite.config.File.Password.SaltLength)
suite.Assert().Equal(schema.DefaultPasswordConfiguration.Memory, suite.configuration.File.Password.Memory) suite.Assert().Equal(schema.DefaultPasswordConfiguration.Memory, suite.config.File.Password.Memory)
suite.Assert().Equal(schema.DefaultPasswordConfiguration.Parallelism, suite.configuration.File.Password.Parallelism) suite.Assert().Equal(schema.DefaultPasswordConfiguration.Parallelism, suite.config.File.Password.Parallelism)
} }
func TestFileBasedAuthenticationBackend(t *testing.T) { func TestFileBasedAuthenticationBackend(t *testing.T) {
@ -205,289 +205,265 @@ func TestFileBasedAuthenticationBackend(t *testing.T) {
type LDAPAuthenticationBackendSuite struct { type LDAPAuthenticationBackendSuite struct {
suite.Suite suite.Suite
configuration schema.AuthenticationBackendConfiguration config schema.AuthenticationBackendConfiguration
validator *schema.StructValidator validator *schema.StructValidator
} }
func (suite *LDAPAuthenticationBackendSuite) SetupTest() { func (suite *LDAPAuthenticationBackendSuite) SetupTest() {
suite.validator = schema.NewStructValidator() suite.validator = schema.NewStructValidator()
suite.configuration = schema.AuthenticationBackendConfiguration{} suite.config = schema.AuthenticationBackendConfiguration{}
suite.configuration.LDAP = &schema.LDAPAuthenticationBackendConfiguration{} suite.config.LDAP = &schema.LDAPAuthenticationBackendConfiguration{}
suite.configuration.LDAP.Implementation = schema.LDAPImplementationCustom suite.config.LDAP.Implementation = schema.LDAPImplementationCustom
suite.configuration.LDAP.URL = testLDAPURL suite.config.LDAP.URL = testLDAPURL
suite.configuration.LDAP.User = testLDAPUser suite.config.LDAP.User = testLDAPUser
suite.configuration.LDAP.Password = testLDAPPassword suite.config.LDAP.Password = testLDAPPassword
suite.configuration.LDAP.BaseDN = testLDAPBaseDN suite.config.LDAP.BaseDN = testLDAPBaseDN
suite.configuration.LDAP.UsernameAttribute = "uid" suite.config.LDAP.UsernameAttribute = "uid"
suite.configuration.LDAP.UsersFilter = "({username_attribute}={input})" suite.config.LDAP.UsersFilter = "({username_attribute}={input})"
suite.configuration.LDAP.GroupsFilter = "(cn={input})" suite.config.LDAP.GroupsFilter = "(cn={input})"
} }
func (suite *LDAPAuthenticationBackendSuite) TestShouldValidateCompleteConfiguration() { func (suite *LDAPAuthenticationBackendSuite) TestShouldValidateCompleteConfiguration() {
ValidateAuthenticationBackend(&suite.configuration, suite.validator) ValidateAuthenticationBackend(&suite.config, suite.validator)
suite.Assert().False(suite.validator.HasWarnings()) suite.Assert().False(suite.validator.HasWarnings())
suite.Assert().False(suite.validator.HasErrors()) suite.Assert().False(suite.validator.HasErrors())
} }
func (suite *LDAPAuthenticationBackendSuite) TestShouldValidateDefaultImplementationAndUsernameAttribute() { func (suite *LDAPAuthenticationBackendSuite) TestShouldValidateDefaultImplementationAndUsernameAttribute() {
suite.configuration.LDAP.Implementation = "" suite.config.LDAP.Implementation = ""
suite.configuration.LDAP.UsernameAttribute = "" suite.config.LDAP.UsernameAttribute = ""
ValidateAuthenticationBackend(&suite.configuration, suite.validator) ValidateAuthenticationBackend(&suite.config, suite.validator)
suite.Assert().Equal(schema.LDAPImplementationCustom, suite.configuration.LDAP.Implementation) suite.Assert().Equal(schema.LDAPImplementationCustom, suite.config.LDAP.Implementation)
suite.Assert().Equal(suite.configuration.LDAP.UsernameAttribute, schema.DefaultLDAPAuthenticationBackendConfiguration.UsernameAttribute) suite.Assert().Equal(suite.config.LDAP.UsernameAttribute, schema.DefaultLDAPAuthenticationBackendConfiguration.UsernameAttribute)
suite.Assert().False(suite.validator.HasWarnings()) suite.Assert().False(suite.validator.HasWarnings())
suite.Assert().False(suite.validator.HasErrors()) suite.Assert().False(suite.validator.HasErrors())
} }
func (suite *LDAPAuthenticationBackendSuite) TestShouldRaiseErrorWhenImplementationIsInvalidMSAD() { func (suite *LDAPAuthenticationBackendSuite) TestShouldRaiseErrorWhenImplementationIsInvalidMSAD() {
suite.configuration.LDAP.Implementation = "masd" suite.config.LDAP.Implementation = "masd"
ValidateAuthenticationBackend(&suite.configuration, suite.validator) ValidateAuthenticationBackend(&suite.config, suite.validator)
suite.Assert().False(suite.validator.HasWarnings()) suite.Assert().False(suite.validator.HasWarnings())
suite.Require().Len(suite.validator.Errors(), 1) suite.Require().Len(suite.validator.Errors(), 1)
suite.Assert().EqualError(suite.validator.Errors()[0], "authentication backend ldap implementation must be blank or one of the following values `custom`, `activedirectory`") suite.Assert().EqualError(suite.validator.Errors()[0], "authentication_backend: ldap: option 'implementation' is configured as 'masd' but must be one of the following values: 'custom', 'activedirectory'")
} }
func (suite *LDAPAuthenticationBackendSuite) TestShouldRaiseErrorWhenURLNotProvided() { func (suite *LDAPAuthenticationBackendSuite) TestShouldRaiseErrorWhenURLNotProvided() {
suite.configuration.LDAP.URL = "" suite.config.LDAP.URL = ""
ValidateAuthenticationBackend(&suite.configuration, suite.validator) ValidateAuthenticationBackend(&suite.config, suite.validator)
suite.Assert().False(suite.validator.HasWarnings()) suite.Assert().False(suite.validator.HasWarnings())
suite.Require().Len(suite.validator.Errors(), 1) suite.Require().Len(suite.validator.Errors(), 1)
suite.Assert().EqualError(suite.validator.Errors()[0], "Please provide a URL to the LDAP server") suite.Assert().EqualError(suite.validator.Errors()[0], "authentication_backend: ldap: option 'url' is required")
} }
func (suite *LDAPAuthenticationBackendSuite) TestShouldRaiseErrorWhenUserNotProvided() { func (suite *LDAPAuthenticationBackendSuite) TestShouldRaiseErrorWhenUserNotProvided() {
suite.configuration.LDAP.User = "" suite.config.LDAP.User = ""
ValidateAuthenticationBackend(&suite.configuration, suite.validator) ValidateAuthenticationBackend(&suite.config, suite.validator)
suite.Assert().False(suite.validator.HasWarnings()) suite.Assert().False(suite.validator.HasWarnings())
suite.Require().Len(suite.validator.Errors(), 1) suite.Require().Len(suite.validator.Errors(), 1)
suite.Assert().EqualError(suite.validator.Errors()[0], "Please provide a user name to connect to the LDAP server") suite.Assert().EqualError(suite.validator.Errors()[0], "authentication_backend: ldap: option 'user' is required")
} }
func (suite *LDAPAuthenticationBackendSuite) TestShouldRaiseErrorWhenPasswordNotProvided() { func (suite *LDAPAuthenticationBackendSuite) TestShouldRaiseErrorWhenPasswordNotProvided() {
suite.configuration.LDAP.Password = "" suite.config.LDAP.Password = ""
ValidateAuthenticationBackend(&suite.configuration, suite.validator) ValidateAuthenticationBackend(&suite.config, suite.validator)
suite.Assert().False(suite.validator.HasWarnings()) suite.Assert().False(suite.validator.HasWarnings())
suite.Require().Len(suite.validator.Errors(), 1) suite.Require().Len(suite.validator.Errors(), 1)
suite.Assert().EqualError(suite.validator.Errors()[0], "Please provide a password to connect to the LDAP server") suite.Assert().EqualError(suite.validator.Errors()[0], "authentication_backend: ldap: option 'password' is required")
} }
func (suite *LDAPAuthenticationBackendSuite) TestShouldRaiseErrorWhenBaseDNNotProvided() { func (suite *LDAPAuthenticationBackendSuite) TestShouldRaiseErrorWhenBaseDNNotProvided() {
suite.configuration.LDAP.BaseDN = "" suite.config.LDAP.BaseDN = ""
ValidateAuthenticationBackend(&suite.configuration, suite.validator) ValidateAuthenticationBackend(&suite.config, suite.validator)
suite.Assert().False(suite.validator.HasWarnings()) suite.Assert().False(suite.validator.HasWarnings())
suite.Assert().Len(suite.validator.Errors(), 1) suite.Assert().Len(suite.validator.Errors(), 1)
suite.Assert().EqualError(suite.validator.Errors()[0], "Please provide a base DN to connect to the LDAP server") suite.Assert().EqualError(suite.validator.Errors()[0], "authentication_backend: ldap: option 'base_dn' is required")
} }
func (suite *LDAPAuthenticationBackendSuite) TestShouldRaiseOnEmptyGroupsFilter() { func (suite *LDAPAuthenticationBackendSuite) TestShouldRaiseOnEmptyGroupsFilter() {
suite.configuration.LDAP.GroupsFilter = "" suite.config.LDAP.GroupsFilter = ""
ValidateAuthenticationBackend(&suite.configuration, suite.validator) ValidateAuthenticationBackend(&suite.config, suite.validator)
suite.Assert().False(suite.validator.HasWarnings()) suite.Assert().False(suite.validator.HasWarnings())
suite.Require().Len(suite.validator.Errors(), 1) suite.Require().Len(suite.validator.Errors(), 1)
suite.Assert().EqualError(suite.validator.Errors()[0], "Please provide a groups filter with `groups_filter` attribute") suite.Assert().EqualError(suite.validator.Errors()[0], "authentication_backend: ldap: option 'groups_filter' is required")
} }
func (suite *LDAPAuthenticationBackendSuite) TestShouldRaiseOnEmptyUsersFilter() { func (suite *LDAPAuthenticationBackendSuite) TestShouldRaiseOnEmptyUsersFilter() {
suite.configuration.LDAP.UsersFilter = "" suite.config.LDAP.UsersFilter = ""
ValidateAuthenticationBackend(&suite.configuration, suite.validator) ValidateAuthenticationBackend(&suite.config, suite.validator)
suite.Assert().False(suite.validator.HasWarnings()) suite.Assert().False(suite.validator.HasWarnings())
suite.Require().Len(suite.validator.Errors(), 1) suite.Require().Len(suite.validator.Errors(), 1)
suite.Assert().EqualError(suite.validator.Errors()[0], "Please provide a users filter with `users_filter` attribute") suite.Assert().EqualError(suite.validator.Errors()[0], "authentication_backend: ldap: option 'users_filter' is required")
} }
func (suite *LDAPAuthenticationBackendSuite) TestShouldNotRaiseOnEmptyUsernameAttribute() { func (suite *LDAPAuthenticationBackendSuite) TestShouldNotRaiseOnEmptyUsernameAttribute() {
suite.configuration.LDAP.UsernameAttribute = "" suite.config.LDAP.UsernameAttribute = ""
ValidateAuthenticationBackend(&suite.configuration, suite.validator) ValidateAuthenticationBackend(&suite.config, suite.validator)
suite.Assert().False(suite.validator.HasWarnings()) suite.Assert().False(suite.validator.HasWarnings())
suite.Assert().False(suite.validator.HasErrors()) suite.Assert().False(suite.validator.HasErrors())
} }
func (suite *LDAPAuthenticationBackendSuite) TestShouldRaiseOnBadRefreshInterval() { func (suite *LDAPAuthenticationBackendSuite) TestShouldRaiseOnBadRefreshInterval() {
suite.configuration.RefreshInterval = "blah" suite.config.RefreshInterval = "blah"
ValidateAuthenticationBackend(&suite.configuration, suite.validator) ValidateAuthenticationBackend(&suite.config, suite.validator)
suite.Assert().False(suite.validator.HasWarnings()) suite.Assert().False(suite.validator.HasWarnings())
suite.Require().Len(suite.validator.Errors(), 1) suite.Require().Len(suite.validator.Errors(), 1)
suite.Assert().EqualError(suite.validator.Errors()[0], "Auth Backend `refresh_interval` is configured to 'blah' but it must be either a duration notation or one of 'disable', or 'always'. Error from parser: could not convert the input string of blah into a duration") suite.Assert().EqualError(suite.validator.Errors()[0], "authentication_backend: option 'refresh_interval' is configured to 'blah' but it must be either a duration notation or one of 'disable', or 'always': could not parse 'blah' as a duration")
} }
func (suite *LDAPAuthenticationBackendSuite) TestShouldSetDefaultImplementation() { func (suite *LDAPAuthenticationBackendSuite) TestShouldSetDefaultImplementation() {
ValidateAuthenticationBackend(&suite.configuration, suite.validator) ValidateAuthenticationBackend(&suite.config, suite.validator)
suite.Assert().False(suite.validator.HasWarnings()) suite.Assert().False(suite.validator.HasWarnings())
suite.Assert().False(suite.validator.HasErrors()) suite.Assert().False(suite.validator.HasErrors())
suite.Assert().Equal(schema.LDAPImplementationCustom, suite.configuration.LDAP.Implementation) suite.Assert().Equal(schema.LDAPImplementationCustom, suite.config.LDAP.Implementation)
} }
func (suite *LDAPAuthenticationBackendSuite) TestShouldRaiseErrorOnBadFilterPlaceholders() { func (suite *LDAPAuthenticationBackendSuite) TestShouldRaiseErrorOnBadFilterPlaceholders() {
suite.configuration.LDAP.UsersFilter = "(&({username_attribute}={0})(objectCategory=person)(objectClass=user)(!userAccountControl:1.2.840.113556.1.4.803:=2))" suite.config.LDAP.UsersFilter = "(&({username_attribute}={0})(objectCategory=person)(objectClass=user)(!userAccountControl:1.2.840.113556.1.4.803:=2))"
suite.configuration.LDAP.GroupsFilter = "(&(member={0})(objectClass=group)(objectCategory=group))" suite.config.LDAP.GroupsFilter = "(&({username_attribute}={1})(member={0})(objectClass=group)(objectCategory=group))"
ValidateAuthenticationBackend(&suite.configuration, suite.validator) ValidateAuthenticationBackend(&suite.config, suite.validator)
suite.Assert().False(suite.validator.HasWarnings()) suite.Assert().False(suite.validator.HasWarnings())
suite.Assert().True(suite.validator.HasErrors()) suite.Assert().True(suite.validator.HasErrors())
suite.Require().Len(suite.validator.Errors(), 2) suite.Require().Len(suite.validator.Errors(), 4)
suite.Assert().EqualError(suite.validator.Errors()[0], "authentication backend ldap users filter must "+ suite.Assert().EqualError(suite.validator.Errors()[0], "authentication_backend: ldap: option 'users_filter' has an invalid placeholder: '{0}' has been removed, please use '{input}' instead")
"not contain removed placeholders, {0} has been replaced with {input}") suite.Assert().EqualError(suite.validator.Errors()[1], "authentication_backend: ldap: option 'groups_filter' has an invalid placeholder: '{0}' has been removed, please use '{input}' instead")
suite.Assert().EqualError(suite.validator.Errors()[1], "authentication backend ldap groups filter must "+ suite.Assert().EqualError(suite.validator.Errors()[2], "authentication_backend: ldap: option 'groups_filter' has an invalid placeholder: '{1}' has been removed, please use '{username}' instead")
"not contain removed placeholders, "+ suite.Assert().EqualError(suite.validator.Errors()[3], "authentication_backend: ldap: option 'users_filter' must contain the placeholder '{input}' but it is required")
"{0} has been replaced with {input} and {1} has been replaced with {username}")
} }
func (suite *LDAPAuthenticationBackendSuite) TestShouldSetDefaultGroupNameAttribute() { func (suite *LDAPAuthenticationBackendSuite) TestShouldSetDefaultGroupNameAttribute() {
ValidateAuthenticationBackend(&suite.configuration, suite.validator) ValidateAuthenticationBackend(&suite.config, suite.validator)
suite.Assert().False(suite.validator.HasWarnings()) suite.Assert().False(suite.validator.HasWarnings())
suite.Assert().False(suite.validator.HasErrors()) suite.Assert().False(suite.validator.HasErrors())
suite.Assert().Equal("cn", suite.configuration.LDAP.GroupNameAttribute) suite.Assert().Equal("cn", suite.config.LDAP.GroupNameAttribute)
} }
func (suite *LDAPAuthenticationBackendSuite) TestShouldSetDefaultMailAttribute() { func (suite *LDAPAuthenticationBackendSuite) TestShouldSetDefaultMailAttribute() {
ValidateAuthenticationBackend(&suite.configuration, suite.validator) ValidateAuthenticationBackend(&suite.config, suite.validator)
suite.Assert().False(suite.validator.HasWarnings()) suite.Assert().False(suite.validator.HasWarnings())
suite.Assert().False(suite.validator.HasErrors()) suite.Assert().False(suite.validator.HasErrors())
suite.Assert().Equal("mail", suite.configuration.LDAP.MailAttribute) suite.Assert().Equal("mail", suite.config.LDAP.MailAttribute)
} }
func (suite *LDAPAuthenticationBackendSuite) TestShouldSetDefaultDisplayNameAttribute() { func (suite *LDAPAuthenticationBackendSuite) TestShouldSetDefaultDisplayNameAttribute() {
ValidateAuthenticationBackend(&suite.configuration, suite.validator) ValidateAuthenticationBackend(&suite.config, suite.validator)
suite.Assert().False(suite.validator.HasWarnings()) suite.Assert().False(suite.validator.HasWarnings())
suite.Assert().False(suite.validator.HasErrors()) suite.Assert().False(suite.validator.HasErrors())
suite.Assert().Equal("displayName", suite.configuration.LDAP.DisplayNameAttribute) suite.Assert().Equal("displayName", suite.config.LDAP.DisplayNameAttribute)
} }
func (suite *LDAPAuthenticationBackendSuite) TestShouldSetDefaultRefreshInterval() { func (suite *LDAPAuthenticationBackendSuite) TestShouldSetDefaultRefreshInterval() {
ValidateAuthenticationBackend(&suite.configuration, suite.validator) ValidateAuthenticationBackend(&suite.config, suite.validator)
suite.Assert().False(suite.validator.HasWarnings()) suite.Assert().False(suite.validator.HasWarnings())
suite.Assert().False(suite.validator.HasErrors()) suite.Assert().False(suite.validator.HasErrors())
suite.Assert().Equal("5m", suite.configuration.RefreshInterval) suite.Assert().Equal("5m", suite.config.RefreshInterval)
} }
func (suite *LDAPAuthenticationBackendSuite) TestShouldRaiseWhenUsersFilterDoesNotContainEnclosingParenthesis() { func (suite *LDAPAuthenticationBackendSuite) TestShouldRaiseWhenUsersFilterDoesNotContainEnclosingParenthesis() {
suite.configuration.LDAP.UsersFilter = "{username_attribute}={input}" suite.config.LDAP.UsersFilter = "{username_attribute}={input}"
ValidateAuthenticationBackend(&suite.configuration, suite.validator) ValidateAuthenticationBackend(&suite.config, suite.validator)
suite.Assert().False(suite.validator.HasWarnings()) suite.Assert().False(suite.validator.HasWarnings())
suite.Require().Len(suite.validator.Errors(), 1) suite.Require().Len(suite.validator.Errors(), 1)
suite.Assert().EqualError(suite.validator.Errors()[0], "The users filter should contain enclosing parenthesis. For instance {username_attribute}={input} should be ({username_attribute}={input})") suite.Assert().EqualError(suite.validator.Errors()[0], "authentication_backend: ldap: option 'users_filter' must contain enclosing parenthesis: '{username_attribute}={input}' should probably be '({username_attribute}={input})'")
} }
func (suite *LDAPAuthenticationBackendSuite) TestShouldRaiseWhenGroupsFilterDoesNotContainEnclosingParenthesis() { func (suite *LDAPAuthenticationBackendSuite) TestShouldRaiseWhenGroupsFilterDoesNotContainEnclosingParenthesis() {
suite.configuration.LDAP.GroupsFilter = "cn={input}" suite.config.LDAP.GroupsFilter = "cn={input}"
ValidateAuthenticationBackend(&suite.configuration, suite.validator) ValidateAuthenticationBackend(&suite.config, suite.validator)
suite.Assert().False(suite.validator.HasWarnings()) suite.Assert().False(suite.validator.HasWarnings())
suite.Require().Len(suite.validator.Errors(), 1) suite.Require().Len(suite.validator.Errors(), 1)
suite.Assert().EqualError(suite.validator.Errors()[0], "The groups filter should contain enclosing parenthesis. For instance cn={input} should be (cn={input})") suite.Assert().EqualError(suite.validator.Errors()[0], "authentication_backend: ldap: option 'groups_filter' must contain enclosing parenthesis: 'cn={input}' should probably be '(cn={input})'")
} }
func (suite *LDAPAuthenticationBackendSuite) TestShouldRaiseWhenUsersFilterDoesNotContainUsernameAttribute() { func (suite *LDAPAuthenticationBackendSuite) TestShouldRaiseWhenUsersFilterDoesNotContainUsernameAttribute() {
suite.configuration.LDAP.UsersFilter = "(&({mail_attribute}={input})(objectClass=person))" suite.config.LDAP.UsersFilter = "(&({mail_attribute}={input})(objectClass=person))"
ValidateAuthenticationBackend(&suite.configuration, suite.validator) ValidateAuthenticationBackend(&suite.config, suite.validator)
suite.Assert().False(suite.validator.HasWarnings()) suite.Assert().False(suite.validator.HasWarnings())
suite.Require().Len(suite.validator.Errors(), 1) suite.Require().Len(suite.validator.Errors(), 1)
suite.Assert().EqualError(suite.validator.Errors()[0], "Unable to detect {username_attribute} placeholder in users_filter, your configuration is broken. Please review configuration options listed at https://www.authelia.com/docs/configuration/authentication/ldap.html") suite.Assert().EqualError(suite.validator.Errors()[0], "authentication_backend: ldap: option 'users_filter' must contain the placeholder '{username_attribute}' but it is required")
} }
func (suite *LDAPAuthenticationBackendSuite) TestShouldHelpDetectNoInputPlaceholder() { func (suite *LDAPAuthenticationBackendSuite) TestShouldHelpDetectNoInputPlaceholder() {
suite.configuration.LDAP.UsersFilter = "(&({username_attribute}={mail_attribute})(objectClass=person))" suite.config.LDAP.UsersFilter = "(&({username_attribute}={mail_attribute})(objectClass=person))"
ValidateAuthenticationBackend(&suite.configuration, suite.validator) ValidateAuthenticationBackend(&suite.config, suite.validator)
suite.Assert().False(suite.validator.HasWarnings()) suite.Assert().False(suite.validator.HasWarnings())
suite.Require().Len(suite.validator.Errors(), 1) suite.Require().Len(suite.validator.Errors(), 1)
suite.Assert().EqualError(suite.validator.Errors()[0], "Unable to detect {input} placeholder in users_filter, your configuration might be broken. Please review configuration options listed at https://www.authelia.com/docs/configuration/authentication/ldap.html") suite.Assert().EqualError(suite.validator.Errors()[0], "authentication_backend: ldap: option 'users_filter' must contain the placeholder '{input}' but it is required")
}
func (suite *LDAPAuthenticationBackendSuite) TestShouldAdaptLDAPURL() {
suite.Assert().Equal("", validateLDAPURLSimple(loopback, suite.validator))
suite.Assert().False(suite.validator.HasWarnings())
suite.Require().Len(suite.validator.Errors(), 1)
suite.Assert().EqualError(suite.validator.Errors()[0], "Unknown scheme for ldap url, should be ldap:// or ldaps://")
suite.Assert().Equal("", validateLDAPURLSimple("127.0.0.1:636", suite.validator))
suite.Assert().False(suite.validator.HasWarnings())
suite.Require().Len(suite.validator.Errors(), 2)
suite.Assert().EqualError(suite.validator.Errors()[1], "Unable to parse URL to ldap server. The scheme is probably missing: ldap:// or ldaps://")
suite.Assert().Equal("ldap://127.0.0.1", validateLDAPURLSimple("ldap://127.0.0.1", suite.validator))
suite.Assert().Equal("ldap://127.0.0.1:390", validateLDAPURLSimple("ldap://127.0.0.1:390", suite.validator))
suite.Assert().Equal("ldap://127.0.0.1/abc", validateLDAPURLSimple("ldap://127.0.0.1/abc", suite.validator))
suite.Assert().Equal("ldap://127.0.0.1/abc?test=abc&x=y", validateLDAPURLSimple("ldap://127.0.0.1/abc?test=abc&x=y", suite.validator))
suite.Assert().Equal("ldaps://127.0.0.1:390", validateLDAPURLSimple("ldaps://127.0.0.1:390", suite.validator))
suite.Assert().Equal("ldaps://127.0.0.1", validateLDAPURLSimple("ldaps://127.0.0.1", suite.validator))
} }
func (suite *LDAPAuthenticationBackendSuite) TestShouldSetDefaultTLSMinimumVersion() { func (suite *LDAPAuthenticationBackendSuite) TestShouldSetDefaultTLSMinimumVersion() {
suite.configuration.LDAP.TLS = &schema.TLSConfig{MinimumVersion: ""} suite.config.LDAP.TLS = &schema.TLSConfig{MinimumVersion: ""}
ValidateAuthenticationBackend(&suite.configuration, suite.validator) ValidateAuthenticationBackend(&suite.config, suite.validator)
suite.Assert().False(suite.validator.HasWarnings()) suite.Assert().False(suite.validator.HasWarnings())
suite.Assert().False(suite.validator.HasErrors()) suite.Assert().False(suite.validator.HasErrors())
suite.Assert().Equal(schema.DefaultLDAPAuthenticationBackendConfiguration.TLS.MinimumVersion, suite.configuration.LDAP.TLS.MinimumVersion) suite.Assert().Equal(schema.DefaultLDAPAuthenticationBackendConfiguration.TLS.MinimumVersion, suite.config.LDAP.TLS.MinimumVersion)
} }
func (suite *LDAPAuthenticationBackendSuite) TestShouldNotAllowInvalidTLSValue() { func (suite *LDAPAuthenticationBackendSuite) TestShouldNotAllowInvalidTLSValue() {
suite.configuration.LDAP.TLS = &schema.TLSConfig{ suite.config.LDAP.TLS = &schema.TLSConfig{
MinimumVersion: "SSL2.0", MinimumVersion: "SSL2.0",
} }
ValidateAuthenticationBackend(&suite.configuration, suite.validator) ValidateAuthenticationBackend(&suite.config, suite.validator)
suite.Assert().False(suite.validator.HasWarnings()) suite.Assert().False(suite.validator.HasWarnings())
suite.Require().Len(suite.validator.Errors(), 1) suite.Require().Len(suite.validator.Errors(), 1)
suite.Assert().EqualError(suite.validator.Errors()[0], "error occurred validating the LDAP minimum_tls_version key with value SSL2.0: supplied TLS version isn't supported") suite.Assert().EqualError(suite.validator.Errors()[0], "authentication_backend: ldap: tls: option 'minimum_tls_version' is invalid: SSL2.0: supplied tls version isn't supported")
} }
func TestLdapAuthenticationBackend(t *testing.T) { func TestLdapAuthenticationBackend(t *testing.T) {
@ -496,83 +472,101 @@ func TestLdapAuthenticationBackend(t *testing.T) {
type ActiveDirectoryAuthenticationBackendSuite struct { type ActiveDirectoryAuthenticationBackendSuite struct {
suite.Suite suite.Suite
configuration schema.AuthenticationBackendConfiguration config schema.AuthenticationBackendConfiguration
validator *schema.StructValidator validator *schema.StructValidator
} }
func (suite *ActiveDirectoryAuthenticationBackendSuite) SetupTest() { func (suite *ActiveDirectoryAuthenticationBackendSuite) SetupTest() {
suite.validator = schema.NewStructValidator() suite.validator = schema.NewStructValidator()
suite.configuration = schema.AuthenticationBackendConfiguration{} suite.config = schema.AuthenticationBackendConfiguration{}
suite.configuration.LDAP = &schema.LDAPAuthenticationBackendConfiguration{} suite.config.LDAP = &schema.LDAPAuthenticationBackendConfiguration{}
suite.configuration.LDAP.Implementation = schema.LDAPImplementationActiveDirectory suite.config.LDAP.Implementation = schema.LDAPImplementationActiveDirectory
suite.configuration.LDAP.URL = testLDAPURL suite.config.LDAP.URL = testLDAPURL
suite.configuration.LDAP.User = testLDAPUser suite.config.LDAP.User = testLDAPUser
suite.configuration.LDAP.Password = testLDAPPassword suite.config.LDAP.Password = testLDAPPassword
suite.configuration.LDAP.BaseDN = testLDAPBaseDN suite.config.LDAP.BaseDN = testLDAPBaseDN
suite.configuration.LDAP.TLS = schema.DefaultLDAPAuthenticationBackendConfiguration.TLS suite.config.LDAP.TLS = schema.DefaultLDAPAuthenticationBackendConfiguration.TLS
} }
func (suite *ActiveDirectoryAuthenticationBackendSuite) TestShouldSetActiveDirectoryDefaults() { func (suite *ActiveDirectoryAuthenticationBackendSuite) TestShouldSetActiveDirectoryDefaults() {
ValidateAuthenticationBackend(&suite.configuration, suite.validator) ValidateAuthenticationBackend(&suite.config, suite.validator)
suite.Assert().False(suite.validator.HasWarnings()) suite.Assert().False(suite.validator.HasWarnings())
suite.Assert().False(suite.validator.HasErrors()) suite.Assert().False(suite.validator.HasErrors())
suite.Assert().Equal( suite.Assert().Equal(
schema.DefaultLDAPAuthenticationBackendConfiguration.Timeout, schema.DefaultLDAPAuthenticationBackendConfiguration.Timeout,
suite.configuration.LDAP.Timeout) suite.config.LDAP.Timeout)
suite.Assert().Equal( suite.Assert().Equal(
schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.UsersFilter, schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.UsersFilter,
suite.configuration.LDAP.UsersFilter) suite.config.LDAP.UsersFilter)
suite.Assert().Equal( suite.Assert().Equal(
schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.UsernameAttribute, schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.UsernameAttribute,
suite.configuration.LDAP.UsernameAttribute) suite.config.LDAP.UsernameAttribute)
suite.Assert().Equal( suite.Assert().Equal(
schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.DisplayNameAttribute, schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.DisplayNameAttribute,
suite.configuration.LDAP.DisplayNameAttribute) suite.config.LDAP.DisplayNameAttribute)
suite.Assert().Equal( suite.Assert().Equal(
schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.MailAttribute, schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.MailAttribute,
suite.configuration.LDAP.MailAttribute) suite.config.LDAP.MailAttribute)
suite.Assert().Equal( suite.Assert().Equal(
schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.GroupsFilter, schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.GroupsFilter,
suite.configuration.LDAP.GroupsFilter) suite.config.LDAP.GroupsFilter)
suite.Assert().Equal( suite.Assert().Equal(
schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.GroupNameAttribute, schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.GroupNameAttribute,
suite.configuration.LDAP.GroupNameAttribute) suite.config.LDAP.GroupNameAttribute)
} }
func (suite *ActiveDirectoryAuthenticationBackendSuite) TestShouldOnlySetDefaultsIfNotManuallyConfigured() { func (suite *ActiveDirectoryAuthenticationBackendSuite) TestShouldOnlySetDefaultsIfNotManuallyConfigured() {
suite.configuration.LDAP.Timeout = time.Second * 2 suite.config.LDAP.Timeout = time.Second * 2
suite.configuration.LDAP.UsersFilter = "(&({username_attribute}={input})(objectCategory=person)(objectClass=user)(!userAccountControl:1.2.840.113556.1.4.803:=2))" suite.config.LDAP.UsersFilter = "(&({username_attribute}={input})(objectCategory=person)(objectClass=user)(!userAccountControl:1.2.840.113556.1.4.803:=2))"
suite.configuration.LDAP.UsernameAttribute = "cn" suite.config.LDAP.UsernameAttribute = "cn"
suite.configuration.LDAP.MailAttribute = "userPrincipalName" suite.config.LDAP.MailAttribute = "userPrincipalName"
suite.configuration.LDAP.DisplayNameAttribute = "name" suite.config.LDAP.DisplayNameAttribute = "name"
suite.configuration.LDAP.GroupsFilter = "(&(member={dn})(objectClass=group)(objectCategory=group))" suite.config.LDAP.GroupsFilter = "(&(member={dn})(objectClass=group)(objectCategory=group))"
suite.configuration.LDAP.GroupNameAttribute = "distinguishedName" suite.config.LDAP.GroupNameAttribute = "distinguishedName"
ValidateAuthenticationBackend(&suite.configuration, suite.validator) ValidateAuthenticationBackend(&suite.config, suite.validator)
suite.Assert().NotEqual( suite.Assert().NotEqual(
schema.DefaultLDAPAuthenticationBackendConfiguration.Timeout, schema.DefaultLDAPAuthenticationBackendConfiguration.Timeout,
suite.configuration.LDAP.Timeout) suite.config.LDAP.Timeout)
suite.Assert().NotEqual( suite.Assert().NotEqual(
schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.UsersFilter, schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.UsersFilter,
suite.configuration.LDAP.UsersFilter) suite.config.LDAP.UsersFilter)
suite.Assert().NotEqual( suite.Assert().NotEqual(
schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.UsernameAttribute, schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.UsernameAttribute,
suite.configuration.LDAP.UsernameAttribute) suite.config.LDAP.UsernameAttribute)
suite.Assert().NotEqual( suite.Assert().NotEqual(
schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.DisplayNameAttribute, schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.DisplayNameAttribute,
suite.configuration.LDAP.DisplayNameAttribute) suite.config.LDAP.DisplayNameAttribute)
suite.Assert().NotEqual( suite.Assert().NotEqual(
schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.MailAttribute, schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.MailAttribute,
suite.configuration.LDAP.MailAttribute) suite.config.LDAP.MailAttribute)
suite.Assert().NotEqual( suite.Assert().NotEqual(
schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.GroupsFilter, schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.GroupsFilter,
suite.configuration.LDAP.GroupsFilter) suite.config.LDAP.GroupsFilter)
suite.Assert().NotEqual( suite.Assert().NotEqual(
schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.GroupNameAttribute, schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.GroupNameAttribute,
suite.configuration.LDAP.GroupNameAttribute) suite.config.LDAP.GroupNameAttribute)
}
func (suite *ActiveDirectoryAuthenticationBackendSuite) TestShouldRaiseErrorOnInvalidURLWithHTTP() {
suite.config.LDAP.URL = "http://dc1:389"
validateLDAPAuthenticationBackendURL(suite.config.LDAP, suite.validator)
suite.Require().Len(suite.validator.Errors(), 1)
suite.Assert().EqualError(suite.validator.Errors()[0], "authentication_backend: ldap: option 'url' must have either the 'ldap' or 'ldaps' scheme but it is configured as 'http'")
}
func (suite *ActiveDirectoryAuthenticationBackendSuite) TestShouldRaiseErrorOnInvalidURLWithBadCharacters() {
suite.config.LDAP.URL = "ldap://dc1:abc"
validateLDAPAuthenticationBackendURL(suite.config.LDAP, suite.validator)
suite.Require().Len(suite.validator.Errors(), 1)
suite.Assert().EqualError(suite.validator.Errors()[0], "authentication_backend: ldap: option 'url' could not be parsed: parse \"ldap://dc1:abc\": invalid port \":abc\" after host")
} }
func TestActiveDirectoryAuthenticationBackend(t *testing.T) { func TestActiveDirectoryAuthenticationBackend(t *testing.T) {

View File

@ -3,68 +3,59 @@ package validator
import ( import (
"fmt" "fmt"
"os" "os"
"strings"
"github.com/authelia/authelia/v4/internal/configuration/schema" "github.com/authelia/authelia/v4/internal/configuration/schema"
"github.com/authelia/authelia/v4/internal/utils" "github.com/authelia/authelia/v4/internal/utils"
) )
// ValidateConfiguration and adapt the configuration read from file. // ValidateConfiguration and adapt the configuration read from file.
func ValidateConfiguration(configuration *schema.Configuration, validator *schema.StructValidator) { func ValidateConfiguration(config *schema.Configuration, validator *schema.StructValidator) {
if configuration.CertificatesDirectory != "" { var err error
info, err := os.Stat(configuration.CertificatesDirectory)
if err != nil { if config.CertificatesDirectory != "" {
validator.Push(fmt.Errorf("Error checking certificate directory: %v", err)) var info os.FileInfo
if info, err = os.Stat(config.CertificatesDirectory); err != nil {
validator.Push(fmt.Errorf("the location 'certificates_directory' could not be inspected: %w", err))
} else if !info.IsDir() { } else if !info.IsDir() {
validator.Push(fmt.Errorf("The path %s specified for certificate_directory is not a directory", configuration.CertificatesDirectory)) validator.Push(fmt.Errorf("the location 'certificates_directory' refers to '%s' is not a directory", config.CertificatesDirectory))
} }
} }
if configuration.JWTSecret == "" { if config.JWTSecret == "" {
validator.Push(fmt.Errorf("Provide a JWT secret using \"jwt_secret\" key")) validator.Push(fmt.Errorf("option 'jwt_secret' is required"))
} }
if configuration.DefaultRedirectionURL != "" { if config.DefaultRedirectionURL != "" {
err := utils.IsStringAbsURL(configuration.DefaultRedirectionURL) if err = utils.IsStringAbsURL(config.DefaultRedirectionURL); err != nil {
if err != nil { validator.Push(fmt.Errorf("option 'default_redirection_url' is invalid: %s", strings.ReplaceAll(err.Error(), "like 'http://' or 'https://'", "like 'ldap://' or 'ldaps://'")))
validator.Push(fmt.Errorf("Value for \"default_redirection_url\" is invalid: %+v", err))
} }
} }
ValidateTheme(configuration, validator) ValidateTheme(config, validator)
ValidateLogging(configuration, validator) ValidateLog(config, validator)
ValidateTOTP(configuration, validator) ValidateTOTP(config, validator)
ValidateAuthenticationBackend(&configuration.AuthenticationBackend, validator) ValidateAuthenticationBackend(&config.AuthenticationBackend, validator)
ValidateAccessControl(&configuration.AccessControl, validator) ValidateAccessControl(config, validator)
ValidateRules(configuration.AccessControl, validator) ValidateRules(config, validator)
ValidateSession(&configuration.Session, validator) ValidateSession(&config.Session, validator)
if configuration.Regulation == nil { ValidateRegulation(config, validator)
configuration.Regulation = &schema.DefaultRegulationConfiguration
} ValidateServer(config, validator)
ValidateRegulation(configuration.Regulation, validator) ValidateStorage(config.Storage, validator)
ValidateServer(configuration, validator) ValidateNotifier(config.Notifier, validator)
ValidateStorage(configuration.Storage, validator) ValidateIdentityProviders(&config.IdentityProviders, validator)
if configuration.Notifier == nil { ValidateNTP(config, validator)
validator.Push(fmt.Errorf("A notifier configuration must be provided"))
} else {
ValidateNotifier(configuration.Notifier, validator)
}
ValidateIdentityProviders(&configuration.IdentityProviders, validator)
if configuration.NTP == nil {
configuration.NTP = &schema.DefaultNTPConfiguration
}
ValidateNTP(configuration.NTP, validator)
} }

View File

@ -52,7 +52,7 @@ func TestShouldEnsureNotifierConfigIsProvided(t *testing.T) {
ValidateConfiguration(&config, validator) ValidateConfiguration(&config, validator)
require.Len(t, validator.Errors(), 1) require.Len(t, validator.Errors(), 1)
assert.EqualError(t, validator.Errors()[0], "A notifier configuration must be provided") assert.EqualError(t, validator.Errors()[0], "notifier: you must ensure either the 'smtp' or 'filesystem' notifier is configured")
} }
func TestShouldAddDefaultAccessControl(t *testing.T) { func TestShouldAddDefaultAccessControl(t *testing.T) {
@ -84,8 +84,8 @@ func TestShouldRaiseErrorWithUndefinedJWTSecretKey(t *testing.T) {
require.Len(t, validator.Errors(), 1) require.Len(t, validator.Errors(), 1)
require.Len(t, validator.Warnings(), 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.Errors()[0], "option 'jwt_secret' is required")
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") assert.EqualError(t, validator.Warnings()[0], "access control: no rules have been specified so the 'default_policy' of 'two_factor' is going to be applied to all requests")
} }
func TestShouldRaiseErrorWithBadDefaultRedirectionURL(t *testing.T) { func TestShouldRaiseErrorWithBadDefaultRedirectionURL(t *testing.T) {
@ -97,8 +97,8 @@ func TestShouldRaiseErrorWithBadDefaultRedirectionURL(t *testing.T) {
require.Len(t, validator.Errors(), 1) require.Len(t, validator.Errors(), 1)
require.Len(t, validator.Warnings(), 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.Errors()[0], "option 'default_redirection_url' is invalid: the url 'bad_default_redirection_url' is not absolute because it doesn't start with a scheme like 'ldap://' or 'ldaps://'")
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") assert.EqualError(t, validator.Warnings()[0], "access control: no rules have been specified so the 'default_policy' of 'two_factor' is going to be applied to all requests")
} }
func TestShouldNotOverrideCertificatesDirectoryAndShouldPassWhenBlank(t *testing.T) { func TestShouldNotOverrideCertificatesDirectoryAndShouldPassWhenBlank(t *testing.T) {
@ -112,7 +112,7 @@ func TestShouldNotOverrideCertificatesDirectoryAndShouldPassWhenBlank(t *testing
require.Equal(t, "", config.CertificatesDirectory) 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") assert.EqualError(t, validator.Warnings()[0], "access control: no rules have been specified so the 'default_policy' of 'two_factor' is going to be applied to all requests")
} }
func TestShouldRaiseErrorOnInvalidCertificatesDirectory(t *testing.T) { func TestShouldRaiseErrorOnInvalidCertificatesDirectory(t *testing.T) {
@ -126,12 +126,12 @@ func TestShouldRaiseErrorOnInvalidCertificatesDirectory(t *testing.T) {
require.Len(t, validator.Warnings(), 1) require.Len(t, validator.Warnings(), 1)
if runtime.GOOS == "windows" { 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.") assert.EqualError(t, validator.Errors()[0], "the location 'certificates_directory' could not be inspected: CreateFile not-a-real-file.go: The system cannot find the file specified.")
} else { } else {
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.Errors()[0], "the location 'certificates_directory' could not be inspected: 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") assert.EqualError(t, validator.Warnings()[0], "access control: no rules have been specified so the 'default_policy' of 'two_factor' is going to be applied to all requests")
validator = schema.NewStructValidator() validator = schema.NewStructValidator()
config.CertificatesDirectory = "const.go" config.CertificatesDirectory = "const.go"
@ -141,8 +141,8 @@ func TestShouldRaiseErrorOnInvalidCertificatesDirectory(t *testing.T) {
require.Len(t, validator.Errors(), 1) require.Len(t, validator.Errors(), 1)
require.Len(t, validator.Warnings(), 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.Errors()[0], "the location 'certificates_directory' refers to 'const.go' 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") assert.EqualError(t, validator.Warnings()[0], "access control: no rules have been specified so the 'default_policy' of 'two_factor' is going to be applied to all requests")
} }
func TestShouldNotRaiseErrorOnValidCertificatesDirectory(t *testing.T) { func TestShouldNotRaiseErrorOnValidCertificatesDirectory(t *testing.T) {
@ -155,5 +155,5 @@ func TestShouldNotRaiseErrorOnValidCertificatesDirectory(t *testing.T) {
assert.Len(t, validator.Errors(), 0) assert.Len(t, validator.Errors(), 0)
require.Len(t, validator.Warnings(), 1) 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") assert.EqualError(t, validator.Warnings()[0], "access control: no rules have been specified so the 'default_policy' of 'two_factor' is going to be applied to all requests")
} }

View File

@ -1,6 +1,10 @@
package validator package validator
import "regexp" import (
"regexp"
"github.com/authelia/authelia/v4/internal/oidc"
)
const ( const (
loopback = "127.0.0.1" loopback = "127.0.0.1"
@ -46,64 +50,173 @@ const (
// Notifier Error constants. // Notifier Error constants.
const ( const (
errFmtNotifierMultipleConfigured = "notifier: you can't configure more than one notifier, please ensure " + errFmtNotifierMultipleConfigured = "notifier: please ensure only one of the 'smtp' or 'filesystem' notifier is configured"
"only 'smtp' or 'filesystem' is configured"
errFmtNotifierNotConfigured = "notifier: you must ensure either the 'smtp' or 'filesystem' notifier " + errFmtNotifierNotConfigured = "notifier: you must ensure either the 'smtp' or 'filesystem' notifier " +
"is configured" "is configured"
errFmtNotifierFileSystemFileNameNotConfigured = "filesystem notifier: the 'filename' must be configured" errFmtNotifierFileSystemFileNameNotConfigured = "notifier: filesystem: option 'filename' is required "
errFmtNotifierSMTPNotConfigured = "smtp notifier: the '%s' must be configured" errFmtNotifierSMTPNotConfigured = "notifier: smtp: option '%s' is required"
)
// Authentication Backend Error constants.
const (
errFmtAuthBackendNotConfigured = "authentication_backend: you must ensure either the 'file' or 'ldap' " +
"authentication backend is configured"
errFmtAuthBackendMultipleConfigured = "authentication_backend: please ensure only one of the 'file' or 'ldap' " +
"backend is configured"
errFmtAuthBackendRefreshInterval = "authentication_backend: option 'refresh_interval' is configured to '%s' but " +
"it must be either a duration notation or one of 'disable', or 'always': %w"
errFmtFileAuthBackendPathNotConfigured = "authentication_backend: file: option 'path' is required"
errFmtFileAuthBackendPasswordSaltLength = "authentication_backend: file: password: option 'salt_length' " +
"must be 2 or more but it is configured a '%d'"
errFmtFileAuthBackendPasswordUnknownAlg = "authentication_backend: file: password: option 'algorithm' " +
"must be either 'argon2id' or 'sha512' but it is configured as '%s'"
errFmtFileAuthBackendPasswordInvalidIterations = "authentication_backend: file: password: option " +
"'iterations' must be 1 or more but it is configured as '%d'"
errFmtFileAuthBackendPasswordArgon2idInvalidKeyLength = "authentication_backend: file: password: option " +
"'key_length' must be 16 or more when using algorithm 'argon2id' but it is configured as '%d'"
errFmtFileAuthBackendPasswordArgon2idInvalidParallelism = "authentication_backend: file: password: option " +
"'parallelism' must be 1 or more when using algorithm 'argon2id' but it is configured as '%d'"
errFmtFileAuthBackendPasswordArgon2idInvalidMemory = "authentication_backend: file: password: option 'memory' " +
"must at least be parallelism multiplied by 8 when using algorithm 'argon2id' " +
"with parallelism %d it should be at least %d but it is configured as '%d'"
errFmtLDAPAuthBackendMissingOption = "authentication_backend: ldap: option '%s' is required"
errFmtLDAPAuthBackendTLSMinVersion = "authentication_backend: ldap: tls: option " +
"'minimum_tls_version' is invalid: %s: %w"
errFmtLDAPAuthBackendImplementation = "authentication_backend: ldap: option 'implementation' " +
"is configured as '%s' but must be one of the following values: '%s'"
errFmtLDAPAuthBackendFilterReplacedPlaceholders = "authentication_backend: ldap: option " +
"'%s' has an invalid placeholder: '%s' has been removed, please use '%s' instead"
errFmtLDAPAuthBackendURLNotParsable = "authentication_backend: ldap: option " +
"'url' could not be parsed: %w"
errFmtLDAPAuthBackendURLInvalidScheme = "authentication_backend: ldap: option " +
"'url' must have either the 'ldap' or 'ldaps' scheme but it is configured as '%s'"
errFmtLDAPAuthBackendFilterEnclosingParenthesis = "authentication_backend: ldap: option " +
"'%s' must contain enclosing parenthesis: '%s' should probably be '(%s)'"
errFmtLDAPAuthBackendFilterMissingPlaceholder = "authentication_backend: ldap: option " +
"'%s' must contain the placeholder '{%s}' but it is required"
) )
// TOTP Error constants. // TOTP Error constants.
const ( const (
errFmtTOTPInvalidAlgorithm = "totp: algorithm '%s' is invalid: must be one of %s" errFmtTOTPInvalidAlgorithm = "totp: option 'algorithm' must be one of '%s' but it is configured as '%s'"
errFmtTOTPInvalidPeriod = "totp: period '%d' is invalid: must be 15 or more" errFmtTOTPInvalidPeriod = "totp: option 'period' option must be 15 or more but it is configured as '%d'"
errFmtTOTPInvalidDigits = "totp: digits '%d' is invalid: must be 6 or 8" errFmtTOTPInvalidDigits = "totp: option 'digits' must be 6 or 8 but it is configured as '%d'"
) )
// Storage Error constants. // Storage Error constants.
const ( const (
errStrStorage = "storage: configuration for a 'local', 'mysql' or 'postgres' database must be provided" errStrStorage = "storage: configuration for a 'local', 'mysql' or 'postgres' database must be provided"
errStrStorageEncryptionKeyMustBeProvided = "storage: 'encryption_key' configuration option must be provided" errStrStorageEncryptionKeyMustBeProvided = "storage: option 'encryption_key' must is required"
errStrStorageEncryptionKeyTooShort = "storage: 'encryption_key' configuration option must be 20 characters or longer" errStrStorageEncryptionKeyTooShort = "storage: option 'encryption_key' must be 20 characters or longer"
errFmtStorageUserPassMustBeProvided = "storage: %s: 'username' and 'password' configuration options must be provided" //nolint: gosec errFmtStorageUserPassMustBeProvided = "storage: %s: option 'username' and 'password' are required" //nolint: gosec
errFmtStorageOptionMustBeProvided = "storage: %s: '%s' configuration option must be provided" errFmtStorageOptionMustBeProvided = "storage: %s: option '%s' is required"
errFmtStoragePostgreSQLInvalidSSLMode = "storage: postgres: ssl: 'mode' configuration option '%s' is invalid: must be one of '%s'" errFmtStoragePostgreSQLInvalidSSLMode = "storage: postgres: ssl: option 'mode' must be one of '%s' but it is configured as '%s'"
) )
var storagePostgreSQLValidSSLModes = []string{testModeDisabled, "require", "verify-ca", "verify-full"}
// OpenID Error constants. // OpenID Error constants.
const ( const (
errFmtOIDCClientsDuplicateID = "openid connect provider: one or more clients have the same ID" errFmtOIDCNoClientsConfigured = "identity_providers: oidc: option 'clients' must have one or " +
errFmtOIDCClientsWithEmptyID = "openid connect provider: one or more clients have been configured with an empty ID" "more clients configured"
errFmtOIDCNoClientsConfigured = "openid connect provider: no clients are configured" errFmtOIDCNoPrivateKey = "identity_providers: oidc: option 'issuer_private_key' is required"
errFmtOIDCNoPrivateKey = "openid connect provider: issuer private key must be provided"
errFmtOIDCClientInvalidSecret = "openid connect provider: client with ID '%s' has an empty secret" errFmtOIDCClientsDuplicateID = "identity_providers: oidc: one or more clients have the same id but all client" +
errFmtOIDCClientPublicInvalidSecret = "openid connect provider: client with ID '%s' is public but does not have " + "id's must be unique"
"an empty secret" errFmtOIDCClientsWithEmptyID = "identity_providers: oidc: one or more clients have been configured with " +
errFmtOIDCClientRedirectURI = "openid connect provider: client with ID '%s' redirect URI %s has an " + "an empty id"
"invalid scheme %s, should be http or https"
errFmtOIDCClientRedirectURICantBeParsed = "openid connect provider: client with ID '%s' has an invalid redirect " + errFmtOIDCClientInvalidSecret = "identity_providers: oidc: client '%s': option 'secret' is required"
"URI '%s' could not be parsed: %v" errFmtOIDCClientPublicInvalidSecret = "identity_providers: oidc: client '%s': option 'secret' is " +
errFmtOIDCClientRedirectURIPublic = "openid connect provider: client with ID '%s' redirect URI '%s' is " + "required to be empty when option 'public' is true"
"only valid for the public client type, not the confidential client type" errFmtOIDCClientRedirectURI = "identity_providers: oidc: client '%s': option 'redirect_uris' has an " +
errFmtOIDCClientRedirectURIAbsolute = "openid connect provider: client with ID '%s' redirect URI '%s' is invalid " + "invalid value: redirect uri '%s' must have a scheme of 'http' or 'https' but '%s' is configured"
"because it has no scheme when it should be http or https" errFmtOIDCClientRedirectURICantBeParsed = "identity_providers: oidc: client '%s': option 'redirect_uris' has an " +
errFmtOIDCClientInvalidPolicy = "openid connect provider: client with ID '%s' has an invalid policy " + "invalid value: redirect uri '%s' could not be parsed: %v"
"'%s', should be either 'one_factor' or 'two_factor'" errFmtOIDCClientRedirectURIPublic = "identity_providers: oidc: client '%s': option 'redirect_uris' has the" +
errFmtOIDCClientInvalidScope = "openid connect provider: client with ID '%s' has an invalid scope " + "redirect uri '%s' when option 'public' is false but this is invalid as this uri is not valid " +
"'%s', must be one of: '%s'" "for the openid connect confidential client type"
errFmtOIDCClientInvalidGrantType = "openid connect provider: client with ID '%s' has an invalid grant type " + errFmtOIDCClientRedirectURIAbsolute = "identity_providers: oidc: client '%s': option 'redirect_uris' has an " +
"'%s', must be one of: '%s'" "invalid value: redirect uri '%s' must have the scheme 'http' or 'https' but it has no scheme"
errFmtOIDCClientInvalidResponseMode = "openid connect provider: client with ID '%s' has an invalid response mode " + errFmtOIDCClientInvalidPolicy = "identity_providers: oidc: client '%s': option 'policy' must be 'one_factor' " +
"'%s', must be one of: '%s'" "or 'two_factor' but it is configured as '%s'"
errFmtOIDCClientInvalidUserinfoAlgorithm = "openid connect provider: client with ID '%s' has an invalid userinfo signing " + errFmtOIDCClientInvalidEntry = "identity_providers: oidc: client '%s': option '%s' must only have the values " +
"algorithm '%s', must be one of: '%s'" "'%s' but one option is configured as '%s'"
errFmtOIDCClientInvalidUserinfoAlgorithm = "identity_providers: oidc: client '%s': option " +
"'userinfo_signing_algorithm' must be one of '%s' but it is configured as '%s'"
errFmtOIDCServerInsecureParameterEntropy = "openid connect provider: SECURITY ISSUE - minimum parameter entropy is " + 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" "configured to an unsafe value, it should be above 8 but it's configured to %d"
) )
// Access Control error constants.
const (
errFmtAccessControlDefaultPolicyValue = "access control: option 'default_policy' must be one of '%s' but it is " +
"configured as '%s'"
errFmtAccessControlDefaultPolicyWithoutRules = "access control: 'default_policy' option '%s' is invalid: when " +
"no rules are specified it must be 'two_factor' or 'one_factor'"
errFmtAccessControlNetworkGroupIPCIDRInvalid = "access control: networks: network group '%s' is invalid: the " +
"network '%s' is not a valid IP or CIDR notation"
errFmtAccessControlWarnNoRulesDefaultPolicy = "access control: no rules have been specified so the " +
"'default_policy' of '%s' is going to be applied to all requests"
errFmtAccessControlRuleNoDomains = "access control: rule %s: rule is invalid: must have the option " +
"'domain' configured"
errFmtAccessControlRuleInvalidPolicy = "access control: rule %s: rule 'policy' option '%s' " +
"is invalid: must be one of 'deny', 'two_factor', 'one_factor' or 'bypass'"
errAccessControlRuleBypassPolicyInvalidWithSubjects = "access control: rule %s: 'policy' option 'bypass' is " +
"not supported when 'subject' option is configured: see " +
"https://www.authelia.com/docs/configuration/access-control.html#bypass"
errFmtAccessControlRuleNetworksInvalid = "access control: rule %s: the network '%s' is not a " +
"valid Group Name, IP, or CIDR notation"
errFmtAccessControlRuleResourceInvalid = "access control: rule %s: 'resources' option '%s' is " +
"invalid: %w"
errFmtAccessControlRuleSubjectInvalid = "access control: rule %s: 'subject' option '%s' is " +
"invalid: must start with 'user:' or 'group:'"
errFmtAccessControlRuleMethodInvalid = "access control: rule %s: 'methods' option '%s' is " +
"invalid: must be one of '%s'"
)
// Theme Error constants.
const (
errFmtThemeName = "option 'theme' must be one of '%s' but it is configured as '%s'"
)
// NTP Error constants.
const (
errFmtNTPVersion = "ntp: option 'version' must be either 3 or 4 but it is configured as '%d'"
errFmtNTPMaxDesync = "ntp: option 'max_desync' can't be parsed: %w"
)
// Session error constants.
const (
errFmtSessionCouldNotParseDuration = "session: option '%s' could not be parsed: %w"
errFmtSessionOptionRequired = "session: option '%s' is required"
errFmtSessionDomainMustBeRoot = "session: option 'domain' must be the domain you wish to protect not a wildcard domain but it is configured as '%s'"
errFmtSessionSameSite = "session: option 'same_site' must be one of '%s' but is configured as '%s'"
errFmtSessionSecretRequired = "session: option 'secret' is required when using the '%s' provider"
errFmtSessionRedisPortRange = "session: redis: option 'port' must be between 1 and 65535 but is configured as '%d'"
errFmtSessionRedisHostRequired = "session: redis: option 'host' is required"
errFmtSessionRedisHostOrNodesRequired = "session: redis: option 'host' or the 'high_availability' option 'nodes' is required"
errFmtSessionRedisSentinelMissingName = "session: redis: high_availability: option 'sentinel_name' is required"
errFmtSessionRedisSentinelNodeHostMissing = "session: redis: high_availability: option 'nodes': option 'host' is required for each node but one or more nodes are missing this"
)
// Regulation Error Consts.
const (
errFmtRegulationParseDuration = "regulation: option '%s' could not be parsed: %w"
errFmtRegulationFindTimeGreaterThanBanTime = "regulation: option 'find_time' must be less than or equal to option 'ban_time'"
)
// Server Error constants.
const (
errFmtServerTLSCert = "server: tls: option 'key' must also be accompanied by option 'certificate'"
errFmtServerTLSKey = "server: tls: option 'certificate' must also be accompanied by option 'key'"
errFmtServerPathNoForwardSlashes = "server: option 'path' must not contain any forward slashes"
errFmtServerPathAlphaNum = "server: option 'path' must only contain alpha numeric characters"
errFmtServerBufferSize = "server: option '%s_buffer_size' must be above 0 but it is configured as '%d'"
)
// Error constants. // Error constants.
const ( const (
/* /*
@ -118,26 +231,25 @@ const (
errFmtReplacedConfigurationKey = "invalid configuration key '%s' was replaced by '%s'" errFmtReplacedConfigurationKey = "invalid configuration key '%s' was replaced by '%s'"
errFmtLoggingLevelInvalid = "the log level '%s' is invalid, must be one of: %s" errFmtLoggingLevelInvalid = "log: option 'level' must be one of '%s' but it is configured as '%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" 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" 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" 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"
) )
var validLoggingLevels = []string{"trace", "debug", "info", "warn", "error"} var validStoragePostgreSQLSSLModes = []string{testModeDisabled, "require", "verify-ca", "verify-full"}
var validHTTPRequestMethods = []string{"GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "TRACE", "CONNECT", "OPTIONS"}
var validOIDCScopes = []string{"openid", "email", "profile", "groups", "offline_access"} var validThemeNames = []string{"light", "dark", "grey", "auto"}
var validSessionSameSiteValues = []string{"none", "lax", "strict"}
var validLoLevels = []string{"trace", "debug", "info", "warn", "error"}
var validACLRuleMethods = []string{"GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "TRACE", "CONNECT", "OPTIONS"}
var validACLRulePolicies = []string{policyBypass, policyOneFactor, policyTwoFactor, policyDeny}
var validOIDCScopes = []string{oidc.ScopeOpenID, oidc.ScopeEmail, oidc.ScopeProfile, oidc.ScopeGroups, "offline_access"}
var validOIDCGrantTypes = []string{"implicit", "refresh_token", "authorization_code", "password", "client_credentials"} var validOIDCGrantTypes = []string{"implicit", "refresh_token", "authorization_code", "password", "client_credentials"}
var validOIDCResponseModes = []string{"form_post", "query", "fragment"} var validOIDCResponseModes = []string{"form_post", "query", "fragment"}
var validOIDCUserinfoAlgorithms = []string{"none", "RS256"} var validOIDCUserinfoAlgorithms = []string{"none", "RS256"}

View File

@ -11,55 +11,55 @@ import (
) )
// ValidateIdentityProviders validates and update IdentityProviders configuration. // ValidateIdentityProviders validates and update IdentityProviders configuration.
func ValidateIdentityProviders(configuration *schema.IdentityProvidersConfiguration, validator *schema.StructValidator) { func ValidateIdentityProviders(config *schema.IdentityProvidersConfiguration, validator *schema.StructValidator) {
validateOIDC(configuration.OIDC, validator) validateOIDC(config.OIDC, validator)
} }
func validateOIDC(configuration *schema.OpenIDConnectConfiguration, validator *schema.StructValidator) { func validateOIDC(config *schema.OpenIDConnectConfiguration, validator *schema.StructValidator) {
if configuration != nil { if config != nil {
if configuration.IssuerPrivateKey == "" { if config.IssuerPrivateKey == "" {
validator.Push(fmt.Errorf(errFmtOIDCNoPrivateKey)) validator.Push(fmt.Errorf(errFmtOIDCNoPrivateKey))
} }
if configuration.AccessTokenLifespan == time.Duration(0) { if config.AccessTokenLifespan == time.Duration(0) {
configuration.AccessTokenLifespan = schema.DefaultOpenIDConnectConfiguration.AccessTokenLifespan config.AccessTokenLifespan = schema.DefaultOpenIDConnectConfiguration.AccessTokenLifespan
} }
if configuration.AuthorizeCodeLifespan == time.Duration(0) { if config.AuthorizeCodeLifespan == time.Duration(0) {
configuration.AuthorizeCodeLifespan = schema.DefaultOpenIDConnectConfiguration.AuthorizeCodeLifespan config.AuthorizeCodeLifespan = schema.DefaultOpenIDConnectConfiguration.AuthorizeCodeLifespan
} }
if configuration.IDTokenLifespan == time.Duration(0) { if config.IDTokenLifespan == time.Duration(0) {
configuration.IDTokenLifespan = schema.DefaultOpenIDConnectConfiguration.IDTokenLifespan config.IDTokenLifespan = schema.DefaultOpenIDConnectConfiguration.IDTokenLifespan
} }
if configuration.RefreshTokenLifespan == time.Duration(0) { if config.RefreshTokenLifespan == time.Duration(0) {
configuration.RefreshTokenLifespan = schema.DefaultOpenIDConnectConfiguration.RefreshTokenLifespan config.RefreshTokenLifespan = schema.DefaultOpenIDConnectConfiguration.RefreshTokenLifespan
} }
if configuration.MinimumParameterEntropy != 0 && configuration.MinimumParameterEntropy < 8 { if config.MinimumParameterEntropy != 0 && config.MinimumParameterEntropy < 8 {
validator.PushWarning(fmt.Errorf(errFmtOIDCServerInsecureParameterEntropy, configuration.MinimumParameterEntropy)) validator.PushWarning(fmt.Errorf(errFmtOIDCServerInsecureParameterEntropy, config.MinimumParameterEntropy))
} }
validateOIDCClients(configuration, validator) validateOIDCClients(config, validator)
if len(configuration.Clients) == 0 { if len(config.Clients) == 0 {
validator.Push(fmt.Errorf(errFmtOIDCNoClientsConfigured)) validator.Push(fmt.Errorf(errFmtOIDCNoClientsConfigured))
} }
} }
} }
func validateOIDCClients(configuration *schema.OpenIDConnectConfiguration, validator *schema.StructValidator) { func validateOIDCClients(config *schema.OpenIDConnectConfiguration, validator *schema.StructValidator) {
invalidID, duplicateIDs := false, false invalidID, duplicateIDs := false, false
var ids []string var ids []string
for c, client := range configuration.Clients { for c, client := range config.Clients {
if client.ID == "" { if client.ID == "" {
invalidID = true invalidID = true
} else { } else {
if client.Description == "" { if client.Description == "" {
configuration.Clients[c].Description = client.ID config.Clients[c].Description = client.ID
} }
if utils.IsStringInSliceFold(client.ID, ids) { if utils.IsStringInSliceFold(client.ID, ids) {
@ -79,16 +79,16 @@ func validateOIDCClients(configuration *schema.OpenIDConnectConfiguration, valid
} }
if client.Policy == "" { if client.Policy == "" {
configuration.Clients[c].Policy = schema.DefaultOpenIDConnectClientConfiguration.Policy config.Clients[c].Policy = schema.DefaultOpenIDConnectClientConfiguration.Policy
} else if client.Policy != policyOneFactor && client.Policy != policyTwoFactor { } else if client.Policy != policyOneFactor && client.Policy != policyTwoFactor {
validator.Push(fmt.Errorf(errFmtOIDCClientInvalidPolicy, client.ID, client.Policy)) validator.Push(fmt.Errorf(errFmtOIDCClientInvalidPolicy, client.ID, client.Policy))
} }
validateOIDCClientScopes(c, configuration, validator) validateOIDCClientScopes(c, config, validator)
validateOIDCClientGrantTypes(c, configuration, validator) validateOIDCClientGrantTypes(c, config, validator)
validateOIDCClientResponseTypes(c, configuration, validator) validateOIDCClientResponseTypes(c, config, validator)
validateOIDCClientResponseModes(c, configuration, validator) validateOIDCClientResponseModes(c, config, validator)
validateOIDDClientUserinfoAlgorithm(c, configuration, validator) validateOIDDClientUserinfoAlgorithm(c, config, validator)
validateOIDCClientRedirectURIs(client, validator) validateOIDCClientRedirectURIs(client, validator)
} }
@ -115,8 +115,8 @@ func validateOIDCClientScopes(c int, configuration *schema.OpenIDConnectConfigur
for _, scope := range configuration.Clients[c].Scopes { for _, scope := range configuration.Clients[c].Scopes {
if !utils.IsStringInSlice(scope, validOIDCScopes) { if !utils.IsStringInSlice(scope, validOIDCScopes) {
validator.Push(fmt.Errorf( validator.Push(fmt.Errorf(
errFmtOIDCClientInvalidScope, errFmtOIDCClientInvalidEntry,
configuration.Clients[c].ID, scope, strings.Join(validOIDCScopes, "', '"))) configuration.Clients[c].ID, "scopes", strings.Join(validOIDCScopes, "', '"), scope))
} }
} }
} }
@ -130,8 +130,8 @@ func validateOIDCClientGrantTypes(c int, configuration *schema.OpenIDConnectConf
for _, grantType := range configuration.Clients[c].GrantTypes { for _, grantType := range configuration.Clients[c].GrantTypes {
if !utils.IsStringInSlice(grantType, validOIDCGrantTypes) { if !utils.IsStringInSlice(grantType, validOIDCGrantTypes) {
validator.Push(fmt.Errorf( validator.Push(fmt.Errorf(
errFmtOIDCClientInvalidGrantType, errFmtOIDCClientInvalidEntry,
configuration.Clients[c].ID, grantType, strings.Join(validOIDCGrantTypes, "', '"))) configuration.Clients[c].ID, "grant_types", strings.Join(validOIDCGrantTypes, "', '"), grantType))
} }
} }
} }
@ -152,8 +152,8 @@ func validateOIDCClientResponseModes(c int, configuration *schema.OpenIDConnectC
for _, responseMode := range configuration.Clients[c].ResponseModes { for _, responseMode := range configuration.Clients[c].ResponseModes {
if !utils.IsStringInSlice(responseMode, validOIDCResponseModes) { if !utils.IsStringInSlice(responseMode, validOIDCResponseModes) {
validator.Push(fmt.Errorf( validator.Push(fmt.Errorf(
errFmtOIDCClientInvalidResponseMode, errFmtOIDCClientInvalidEntry,
configuration.Clients[c].ID, responseMode, strings.Join(validOIDCResponseModes, "', '"))) configuration.Clients[c].ID, "response_modes", strings.Join(validOIDCResponseModes, "', '"), responseMode))
} }
} }
} }
@ -163,7 +163,7 @@ func validateOIDDClientUserinfoAlgorithm(c int, configuration *schema.OpenIDConn
configuration.Clients[c].UserinfoSigningAlgorithm = schema.DefaultOpenIDConnectClientConfiguration.UserinfoSigningAlgorithm configuration.Clients[c].UserinfoSigningAlgorithm = schema.DefaultOpenIDConnectClientConfiguration.UserinfoSigningAlgorithm
} else if !utils.IsStringInSlice(configuration.Clients[c].UserinfoSigningAlgorithm, validOIDCUserinfoAlgorithms) { } else if !utils.IsStringInSlice(configuration.Clients[c].UserinfoSigningAlgorithm, validOIDCUserinfoAlgorithms) {
validator.Push(fmt.Errorf(errFmtOIDCClientInvalidUserinfoAlgorithm, validator.Push(fmt.Errorf(errFmtOIDCClientInvalidUserinfoAlgorithm,
configuration.Clients[c].ID, configuration.Clients[c].UserinfoSigningAlgorithm, strings.Join(validOIDCUserinfoAlgorithms, ", "))) configuration.Clients[c].ID, strings.Join(validOIDCUserinfoAlgorithms, ", "), configuration.Clients[c].UserinfoSigningAlgorithm))
} }
} }
@ -174,7 +174,7 @@ func validateOIDCClientRedirectURIs(client schema.OpenIDConnectClientConfigurati
continue continue
} }
validator.Push(fmt.Errorf(errFmtOIDCClientRedirectURIPublic, client.ID, redirectURI)) validator.Push(fmt.Errorf(errFmtOIDCClientRedirectURIPublic, client.ID, oauth2InstalledApp))
continue continue
} }

View File

@ -173,8 +173,7 @@ func TestShouldRaiseErrorWhenOIDCClientConfiguredWithBadScopes(t *testing.T) {
ValidateIdentityProviders(config, validator) ValidateIdentityProviders(config, validator)
require.Len(t, validator.Errors(), 1) require.Len(t, validator.Errors(), 1)
assert.EqualError(t, validator.Errors()[0], "openid connect provider: client with ID 'good_id' has an invalid scope "+ assert.EqualError(t, validator.Errors()[0], "identity_providers: oidc: client 'good_id': option 'scopes' must only have the values 'openid', 'email', 'profile', 'groups', 'offline_access' but one option is configured as 'bad_scope'")
"'bad_scope', must be one of: 'openid', 'email', 'profile', 'groups', 'offline_access'")
} }
func TestShouldRaiseErrorWhenOIDCClientConfiguredWithBadGrantTypes(t *testing.T) { func TestShouldRaiseErrorWhenOIDCClientConfiguredWithBadGrantTypes(t *testing.T) {
@ -200,9 +199,7 @@ func TestShouldRaiseErrorWhenOIDCClientConfiguredWithBadGrantTypes(t *testing.T)
ValidateIdentityProviders(config, validator) ValidateIdentityProviders(config, validator)
require.Len(t, validator.Errors(), 1) require.Len(t, validator.Errors(), 1)
assert.EqualError(t, validator.Errors()[0], "openid connect provider: client with ID 'good_id' has an invalid grant type "+ assert.EqualError(t, validator.Errors()[0], "identity_providers: oidc: client 'good_id': option 'grant_types' must only have the values 'implicit', 'refresh_token', 'authorization_code', 'password', 'client_credentials' but one option is configured as 'bad_grant_type'")
"'bad_grant_type', must be one of: 'implicit', 'refresh_token', 'authorization_code', "+
"'password', 'client_credentials'")
} }
func TestShouldRaiseErrorWhenOIDCClientConfiguredWithBadResponseModes(t *testing.T) { func TestShouldRaiseErrorWhenOIDCClientConfiguredWithBadResponseModes(t *testing.T) {
@ -228,8 +225,7 @@ func TestShouldRaiseErrorWhenOIDCClientConfiguredWithBadResponseModes(t *testing
ValidateIdentityProviders(config, validator) ValidateIdentityProviders(config, validator)
require.Len(t, validator.Errors(), 1) require.Len(t, validator.Errors(), 1)
assert.EqualError(t, validator.Errors()[0], "openid connect provider: client with ID 'good_id' has an invalid response mode "+ assert.EqualError(t, validator.Errors()[0], "identity_providers: oidc: client 'good_id': option 'response_modes' must only have the values 'form_post', 'query', 'fragment' but one option is configured as 'bad_responsemode'")
"'bad_responsemode', must be one of: 'form_post', 'query', 'fragment'")
} }
func TestShouldRaiseErrorWhenOIDCClientConfiguredWithBadUserinfoAlg(t *testing.T) { func TestShouldRaiseErrorWhenOIDCClientConfiguredWithBadUserinfoAlg(t *testing.T) {
@ -255,8 +251,7 @@ func TestShouldRaiseErrorWhenOIDCClientConfiguredWithBadUserinfoAlg(t *testing.T
ValidateIdentityProviders(config, validator) ValidateIdentityProviders(config, validator)
require.Len(t, validator.Errors(), 1) require.Len(t, validator.Errors(), 1)
assert.EqualError(t, validator.Errors()[0], "openid connect provider: client with ID 'good_id' has an invalid userinfo "+ assert.EqualError(t, validator.Errors()[0], "identity_providers: oidc: client 'good_id': option 'userinfo_signing_algorithm' must be one of 'none, RS256' but it is configured as 'rs256'")
"signing algorithm 'rs256', must be one of: 'none, RS256'")
} }
func TestValidateIdentityProvidersShouldRaiseWarningOnSecurityIssue(t *testing.T) { func TestValidateIdentityProvidersShouldRaiseWarningOnSecurityIssue(t *testing.T) {
@ -502,8 +497,8 @@ func TestValidateOIDCClientRedirectURIsSupportingPrivateUseURISchemes(t *testing
assert.Len(t, validator.Warnings(), 0) assert.Len(t, validator.Warnings(), 0)
assert.Len(t, validator.Errors(), 2) assert.Len(t, validator.Errors(), 2)
assert.ElementsMatch(t, validator.Errors(), []error{ assert.ElementsMatch(t, validator.Errors(), []error{
errors.New("openid connect provider: client with ID 'owncloud' redirect URI oc://ios.owncloud.com has an invalid scheme oc, should be http or https"), errors.New("identity_providers: oidc: client 'owncloud': option 'redirect_uris' has an invalid value: redirect uri 'oc://ios.owncloud.com' must have a scheme of 'http' or 'https' but 'oc' is configured"),
errors.New("openid connect provider: client with ID 'owncloud' redirect URI com.example.app:/oauth2redirect/example-provider has an invalid scheme com.example.app, should be http or https"), errors.New("identity_providers: oidc: client 'owncloud': option 'redirect_uris' has an invalid value: redirect uri 'com.example.app:/oauth2redirect/example-provider' must have a scheme of 'http' or 'https' but 'com.example.app' is configured"),
}) })
}) })
} }

View File

@ -0,0 +1,24 @@
package validator
import (
"fmt"
"strings"
"github.com/authelia/authelia/v4/internal/configuration/schema"
"github.com/authelia/authelia/v4/internal/utils"
)
// ValidateLog validates the logging configuration.
func ValidateLog(config *schema.Configuration, validator *schema.StructValidator) {
if config.Log.Level == "" {
config.Log.Level = schema.DefaultLoggingConfiguration.Level
}
if config.Log.Format == "" {
config.Log.Format = schema.DefaultLoggingConfiguration.Format
}
if !utils.IsStringInSlice(config.Log.Level, validLoLevels) {
validator.Push(fmt.Errorf(errFmtLoggingLevelInvalid, strings.Join(validLoLevels, "', '"), config.Log.Level))
}
}

View File

@ -14,7 +14,7 @@ func TestShouldSetDefaultLoggingValues(t *testing.T) {
validator := schema.NewStructValidator() validator := schema.NewStructValidator()
ValidateLogging(config, validator) ValidateLog(config, validator)
assert.Len(t, validator.Warnings(), 0) assert.Len(t, validator.Warnings(), 0)
assert.Len(t, validator.Errors(), 0) assert.Len(t, validator.Errors(), 0)
@ -35,10 +35,10 @@ func TestShouldRaiseErrorOnInvalidLoggingLevel(t *testing.T) {
validator := schema.NewStructValidator() validator := schema.NewStructValidator()
ValidateLogging(config, validator) ValidateLog(config, validator)
assert.Len(t, validator.Warnings(), 0) assert.Len(t, validator.Warnings(), 0)
require.Len(t, validator.Errors(), 1) require.Len(t, validator.Errors(), 1)
assert.EqualError(t, validator.Errors()[0], "the log level 'TRACE' is invalid, must be one of: trace, debug, info, warn, error") assert.EqualError(t, validator.Errors()[0], "log: option 'level' must be one of 'trace', 'debug', 'info', 'warn', 'error' but it is configured as 'TRACE'")
} }

View File

@ -1,24 +0,0 @@
package validator
import (
"fmt"
"strings"
"github.com/authelia/authelia/v4/internal/configuration/schema"
"github.com/authelia/authelia/v4/internal/utils"
)
// ValidateLogging validates the logging configuration.
func ValidateLogging(configuration *schema.Configuration, validator *schema.StructValidator) {
if configuration.Log.Level == "" {
configuration.Log.Level = schema.DefaultLoggingConfiguration.Level
}
if configuration.Log.Format == "" {
configuration.Log.Format = schema.DefaultLoggingConfiguration.Format
}
if !utils.IsStringInSlice(configuration.Log.Level, validLoggingLevels) {
validator.Push(fmt.Errorf(errFmtLoggingLevelInvalid, configuration.Log.Level, strings.Join(validLoggingLevels, ", ")))
}
}

View File

@ -7,62 +7,62 @@ import (
) )
// ValidateNotifier validates and update notifier configuration. // ValidateNotifier validates and update notifier configuration.
func ValidateNotifier(configuration *schema.NotifierConfiguration, validator *schema.StructValidator) { func ValidateNotifier(config *schema.NotifierConfiguration, validator *schema.StructValidator) {
if configuration.SMTP == nil && configuration.FileSystem == nil { if config == nil || (config.SMTP == nil && config.FileSystem == nil) {
validator.Push(fmt.Errorf(errFmtNotifierNotConfigured)) validator.Push(fmt.Errorf(errFmtNotifierNotConfigured))
return return
} else if configuration.SMTP != nil && configuration.FileSystem != nil { } else if config.SMTP != nil && config.FileSystem != nil {
validator.Push(fmt.Errorf(errFmtNotifierMultipleConfigured)) validator.Push(fmt.Errorf(errFmtNotifierMultipleConfigured))
return return
} }
if configuration.FileSystem != nil { if config.FileSystem != nil {
if configuration.FileSystem.Filename == "" { if config.FileSystem.Filename == "" {
validator.Push(fmt.Errorf(errFmtNotifierFileSystemFileNameNotConfigured)) validator.Push(fmt.Errorf(errFmtNotifierFileSystemFileNameNotConfigured))
} }
return return
} }
validateSMTPNotifier(configuration.SMTP, validator) validateSMTPNotifier(config.SMTP, validator)
} }
func validateSMTPNotifier(configuration *schema.SMTPNotifierConfiguration, validator *schema.StructValidator) { func validateSMTPNotifier(config *schema.SMTPNotifierConfiguration, validator *schema.StructValidator) {
if configuration.StartupCheckAddress == "" { if config.StartupCheckAddress == "" {
configuration.StartupCheckAddress = schema.DefaultSMTPNotifierConfiguration.StartupCheckAddress config.StartupCheckAddress = schema.DefaultSMTPNotifierConfiguration.StartupCheckAddress
} }
if configuration.Host == "" { if config.Host == "" {
validator.Push(fmt.Errorf(errFmtNotifierSMTPNotConfigured, "host")) validator.Push(fmt.Errorf(errFmtNotifierSMTPNotConfigured, "host"))
} }
if configuration.Port == 0 { if config.Port == 0 {
validator.Push(fmt.Errorf(errFmtNotifierSMTPNotConfigured, "port")) validator.Push(fmt.Errorf(errFmtNotifierSMTPNotConfigured, "port"))
} }
if configuration.Timeout == 0 { if config.Timeout == 0 {
configuration.Timeout = schema.DefaultSMTPNotifierConfiguration.Timeout config.Timeout = schema.DefaultSMTPNotifierConfiguration.Timeout
} }
if configuration.Sender.Address == "" { if config.Sender.Address == "" {
validator.Push(fmt.Errorf(errFmtNotifierSMTPNotConfigured, "sender")) validator.Push(fmt.Errorf(errFmtNotifierSMTPNotConfigured, "sender"))
} }
if configuration.Subject == "" { if config.Subject == "" {
configuration.Subject = schema.DefaultSMTPNotifierConfiguration.Subject config.Subject = schema.DefaultSMTPNotifierConfiguration.Subject
} }
if configuration.Identifier == "" { if config.Identifier == "" {
configuration.Identifier = schema.DefaultSMTPNotifierConfiguration.Identifier config.Identifier = schema.DefaultSMTPNotifierConfiguration.Identifier
} }
if configuration.TLS == nil { if config.TLS == nil {
configuration.TLS = schema.DefaultSMTPNotifierConfiguration.TLS config.TLS = schema.DefaultSMTPNotifierConfiguration.TLS
} }
if configuration.TLS.ServerName == "" { if config.TLS.ServerName == "" {
configuration.TLS.ServerName = configuration.Host config.TLS.ServerName = config.Host
} }
} }

View File

@ -12,34 +12,34 @@ import (
type NotifierSuite struct { type NotifierSuite struct {
suite.Suite suite.Suite
configuration schema.NotifierConfiguration config schema.NotifierConfiguration
validator *schema.StructValidator validator *schema.StructValidator
} }
func (suite *NotifierSuite) SetupTest() { func (suite *NotifierSuite) SetupTest() {
suite.validator = schema.NewStructValidator() suite.validator = schema.NewStructValidator()
suite.configuration.SMTP = &schema.SMTPNotifierConfiguration{ suite.config.SMTP = &schema.SMTPNotifierConfiguration{
Username: "john", Username: "john",
Password: "password", Password: "password",
Sender: mail.Address{Name: "Authelia", Address: "authelia@example.com"}, Sender: mail.Address{Name: "Authelia", Address: "authelia@example.com"},
Host: "example.com", Host: "example.com",
Port: 25, Port: 25,
} }
suite.configuration.FileSystem = nil suite.config.FileSystem = nil
} }
/* /*
Common Tests. Common Tests.
*/ */
func (suite *NotifierSuite) TestShouldEnsureAtLeastSMTPOrFilesystemIsProvided() { func (suite *NotifierSuite) TestShouldEnsureAtLeastSMTPOrFilesystemIsProvided() {
ValidateNotifier(&suite.configuration, suite.validator) ValidateNotifier(&suite.config, suite.validator)
suite.Assert().False(suite.validator.HasWarnings()) suite.Assert().False(suite.validator.HasWarnings())
suite.Assert().False(suite.validator.HasErrors()) suite.Assert().False(suite.validator.HasErrors())
suite.configuration.SMTP = nil suite.config.SMTP = nil
ValidateNotifier(&suite.configuration, suite.validator) ValidateNotifier(&suite.config, suite.validator)
suite.Assert().False(suite.validator.HasWarnings()) suite.Assert().False(suite.validator.HasWarnings())
suite.Require().True(suite.validator.HasErrors()) suite.Require().True(suite.validator.HasErrors())
@ -50,15 +50,15 @@ func (suite *NotifierSuite) TestShouldEnsureAtLeastSMTPOrFilesystemIsProvided()
} }
func (suite *NotifierSuite) TestShouldEnsureEitherSMTPOrFilesystemIsProvided() { func (suite *NotifierSuite) TestShouldEnsureEitherSMTPOrFilesystemIsProvided() {
ValidateNotifier(&suite.configuration, suite.validator) ValidateNotifier(&suite.config, suite.validator)
suite.Assert().False(suite.validator.HasErrors()) suite.Assert().False(suite.validator.HasErrors())
suite.configuration.FileSystem = &schema.FileSystemNotifierConfiguration{ suite.config.FileSystem = &schema.FileSystemNotifierConfiguration{
Filename: "test", Filename: "test",
} }
ValidateNotifier(&suite.configuration, suite.validator) ValidateNotifier(&suite.config, suite.validator)
suite.Assert().False(suite.validator.HasWarnings()) suite.Assert().False(suite.validator.HasWarnings())
suite.Require().True(suite.validator.HasErrors()) suite.Require().True(suite.validator.HasErrors())
@ -72,43 +72,43 @@ func (suite *NotifierSuite) TestShouldEnsureEitherSMTPOrFilesystemIsProvided() {
SMTP Tests. SMTP Tests.
*/ */
func (suite *NotifierSuite) TestSMTPShouldSetTLSDefaults() { func (suite *NotifierSuite) TestSMTPShouldSetTLSDefaults() {
ValidateNotifier(&suite.configuration, suite.validator) ValidateNotifier(&suite.config, suite.validator)
suite.Assert().False(suite.validator.HasWarnings()) suite.Assert().False(suite.validator.HasWarnings())
suite.Assert().False(suite.validator.HasErrors()) suite.Assert().False(suite.validator.HasErrors())
suite.Assert().Equal("example.com", suite.configuration.SMTP.TLS.ServerName) suite.Assert().Equal("example.com", suite.config.SMTP.TLS.ServerName)
suite.Assert().Equal("TLS1.2", suite.configuration.SMTP.TLS.MinimumVersion) suite.Assert().Equal("TLS1.2", suite.config.SMTP.TLS.MinimumVersion)
suite.Assert().False(suite.configuration.SMTP.TLS.SkipVerify) suite.Assert().False(suite.config.SMTP.TLS.SkipVerify)
} }
func (suite *NotifierSuite) TestSMTPShouldDefaultTLSServerNameToHost() { func (suite *NotifierSuite) TestSMTPShouldDefaultTLSServerNameToHost() {
suite.configuration.SMTP.Host = "google.com" suite.config.SMTP.Host = "google.com"
suite.configuration.SMTP.TLS = &schema.TLSConfig{ suite.config.SMTP.TLS = &schema.TLSConfig{
MinimumVersion: "TLS1.1", MinimumVersion: "TLS1.1",
} }
ValidateNotifier(&suite.configuration, suite.validator) ValidateNotifier(&suite.config, suite.validator)
suite.Assert().False(suite.validator.HasWarnings()) suite.Assert().False(suite.validator.HasWarnings())
suite.Assert().False(suite.validator.HasErrors()) suite.Assert().False(suite.validator.HasErrors())
suite.Assert().Equal("google.com", suite.configuration.SMTP.TLS.ServerName) suite.Assert().Equal("google.com", suite.config.SMTP.TLS.ServerName)
suite.Assert().Equal("TLS1.1", suite.configuration.SMTP.TLS.MinimumVersion) suite.Assert().Equal("TLS1.1", suite.config.SMTP.TLS.MinimumVersion)
suite.Assert().False(suite.configuration.SMTP.TLS.SkipVerify) suite.Assert().False(suite.config.SMTP.TLS.SkipVerify)
} }
func (suite *NotifierSuite) TestSMTPShouldEnsureHostAndPortAreProvided() { func (suite *NotifierSuite) TestSMTPShouldEnsureHostAndPortAreProvided() {
suite.configuration.FileSystem = nil suite.config.FileSystem = nil
ValidateNotifier(&suite.configuration, suite.validator) ValidateNotifier(&suite.config, suite.validator)
suite.Assert().False(suite.validator.HasWarnings()) suite.Assert().False(suite.validator.HasWarnings())
suite.Assert().False(suite.validator.HasErrors()) suite.Assert().False(suite.validator.HasErrors())
suite.configuration.SMTP.Host = "" suite.config.SMTP.Host = ""
suite.configuration.SMTP.Port = 0 suite.config.SMTP.Port = 0
ValidateNotifier(&suite.configuration, suite.validator) ValidateNotifier(&suite.config, suite.validator)
suite.Assert().False(suite.validator.HasWarnings()) suite.Assert().False(suite.validator.HasWarnings())
suite.Assert().True(suite.validator.HasErrors()) suite.Assert().True(suite.validator.HasErrors())
@ -122,9 +122,9 @@ func (suite *NotifierSuite) TestSMTPShouldEnsureHostAndPortAreProvided() {
} }
func (suite *NotifierSuite) TestSMTPShouldEnsureSenderIsProvided() { func (suite *NotifierSuite) TestSMTPShouldEnsureSenderIsProvided() {
suite.configuration.SMTP.Sender = mail.Address{} suite.config.SMTP.Sender = mail.Address{}
ValidateNotifier(&suite.configuration, suite.validator) ValidateNotifier(&suite.config, suite.validator)
suite.Assert().False(suite.validator.HasWarnings()) suite.Assert().False(suite.validator.HasWarnings())
suite.Require().True(suite.validator.HasErrors()) suite.Require().True(suite.validator.HasErrors())
@ -138,18 +138,18 @@ func (suite *NotifierSuite) TestSMTPShouldEnsureSenderIsProvided() {
File Tests. File Tests.
*/ */
func (suite *NotifierSuite) TestFileShouldEnsureFilenameIsProvided() { func (suite *NotifierSuite) TestFileShouldEnsureFilenameIsProvided() {
suite.configuration.SMTP = nil suite.config.SMTP = nil
suite.configuration.FileSystem = &schema.FileSystemNotifierConfiguration{ suite.config.FileSystem = &schema.FileSystemNotifierConfiguration{
Filename: "test", Filename: "test",
} }
ValidateNotifier(&suite.configuration, suite.validator) ValidateNotifier(&suite.config, suite.validator)
suite.Assert().False(suite.validator.HasWarnings()) suite.Assert().False(suite.validator.HasWarnings())
suite.Assert().False(suite.validator.HasErrors()) suite.Assert().False(suite.validator.HasErrors())
suite.configuration.FileSystem.Filename = "" suite.config.FileSystem.Filename = ""
ValidateNotifier(&suite.configuration, suite.validator) ValidateNotifier(&suite.config, suite.validator)
suite.Assert().False(suite.validator.HasWarnings()) suite.Assert().False(suite.validator.HasWarnings())
suite.Require().True(suite.validator.HasErrors()) suite.Require().True(suite.validator.HasErrors())

View File

@ -8,23 +8,29 @@ import (
) )
// ValidateNTP validates and update NTP configuration. // ValidateNTP validates and update NTP configuration.
func ValidateNTP(configuration *schema.NTPConfiguration, validator *schema.StructValidator) { func ValidateNTP(config *schema.Configuration, validator *schema.StructValidator) {
if configuration.Address == "" { if config.NTP == nil {
configuration.Address = schema.DefaultNTPConfiguration.Address config.NTP = &schema.DefaultNTPConfiguration
return
} }
if configuration.Version == 0 { if config.NTP.Address == "" {
configuration.Version = schema.DefaultNTPConfiguration.Version config.NTP.Address = schema.DefaultNTPConfiguration.Address
} else if configuration.Version < 3 || configuration.Version > 4 {
validator.Push(fmt.Errorf("ntp: version must be either 3 or 4"))
} }
if configuration.MaximumDesync == "" { if config.NTP.Version == 0 {
configuration.MaximumDesync = schema.DefaultNTPConfiguration.MaximumDesync config.NTP.Version = schema.DefaultNTPConfiguration.Version
} else if config.NTP.Version < 3 || config.NTP.Version > 4 {
validator.Push(fmt.Errorf(errFmtNTPVersion, config.NTP.Version))
} }
_, err := utils.ParseDurationString(configuration.MaximumDesync) if config.NTP.MaximumDesync == "" {
config.NTP.MaximumDesync = schema.DefaultNTPConfiguration.MaximumDesync
}
_, err := utils.ParseDurationString(config.NTP.MaximumDesync)
if err != nil { if err != nil {
validator.Push(fmt.Errorf("ntp: error occurred parsing NTP max_desync string: %s", err)) validator.Push(fmt.Errorf(errFmtNTPMaxDesync, err))
} }
} }

View File

@ -4,13 +4,15 @@ import (
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/authelia/authelia/v4/internal/configuration/schema" "github.com/authelia/authelia/v4/internal/configuration/schema"
) )
func newDefaultNTPConfig() schema.NTPConfiguration { func newDefaultNTPConfig() schema.Configuration {
config := schema.NTPConfiguration{} return schema.Configuration{
return config NTP: &schema.NTPConfiguration{},
}
} }
func TestShouldSetDefaultNtpAddress(t *testing.T) { func TestShouldSetDefaultNtpAddress(t *testing.T) {
@ -20,7 +22,7 @@ func TestShouldSetDefaultNtpAddress(t *testing.T) {
ValidateNTP(&config, validator) ValidateNTP(&config, validator)
assert.Len(t, validator.Errors(), 0) assert.Len(t, validator.Errors(), 0)
assert.Equal(t, schema.DefaultNTPConfiguration.Address, config.Address) assert.Equal(t, schema.DefaultNTPConfiguration.Address, config.NTP.Address)
} }
func TestShouldSetDefaultNtpVersion(t *testing.T) { func TestShouldSetDefaultNtpVersion(t *testing.T) {
@ -30,7 +32,7 @@ func TestShouldSetDefaultNtpVersion(t *testing.T) {
ValidateNTP(&config, validator) ValidateNTP(&config, validator)
assert.Len(t, validator.Errors(), 0) assert.Len(t, validator.Errors(), 0)
assert.Equal(t, schema.DefaultNTPConfiguration.Version, config.Version) assert.Equal(t, schema.DefaultNTPConfiguration.Version, config.NTP.Version)
} }
func TestShouldSetDefaultNtpMaximumDesync(t *testing.T) { func TestShouldSetDefaultNtpMaximumDesync(t *testing.T) {
@ -40,7 +42,7 @@ func TestShouldSetDefaultNtpMaximumDesync(t *testing.T) {
ValidateNTP(&config, validator) ValidateNTP(&config, validator)
assert.Len(t, validator.Errors(), 0) assert.Len(t, validator.Errors(), 0)
assert.Equal(t, schema.DefaultNTPConfiguration.MaximumDesync, config.MaximumDesync) assert.Equal(t, schema.DefaultNTPConfiguration.MaximumDesync, config.NTP.MaximumDesync)
} }
func TestShouldSetDefaultNtpDisableStartupCheck(t *testing.T) { func TestShouldSetDefaultNtpDisableStartupCheck(t *testing.T) {
@ -50,16 +52,29 @@ func TestShouldSetDefaultNtpDisableStartupCheck(t *testing.T) {
ValidateNTP(&config, validator) ValidateNTP(&config, validator)
assert.Len(t, validator.Errors(), 0) assert.Len(t, validator.Errors(), 0)
assert.Equal(t, schema.DefaultNTPConfiguration.DisableStartupCheck, config.DisableStartupCheck) assert.Equal(t, schema.DefaultNTPConfiguration.DisableStartupCheck, config.NTP.DisableStartupCheck)
} }
func TestShouldRaiseErrorOnMaximumDesyncString(t *testing.T) { func TestShouldRaiseErrorOnMaximumDesyncString(t *testing.T) {
validator := schema.NewStructValidator() validator := schema.NewStructValidator()
config := newDefaultNTPConfig() config := newDefaultNTPConfig()
config.MaximumDesync = "a second" config.NTP.MaximumDesync = "a second"
ValidateNTP(&config, validator) ValidateNTP(&config, validator)
assert.Len(t, validator.Errors(), 1) require.Len(t, validator.Errors(), 1)
assert.EqualError(t, validator.Errors()[0], "ntp: error occurred parsing NTP max_desync string: could not convert the input string of a second into a duration")
assert.EqualError(t, validator.Errors()[0], "ntp: option 'max_desync' can't be parsed: could not parse 'a second' as a duration")
}
func TestShouldRaiseErrorOnInvalidNTPVersion(t *testing.T) {
validator := schema.NewStructValidator()
config := newDefaultNTPConfig()
config.NTP.Version = 1
ValidateNTP(&config, validator)
require.Len(t, validator.Errors(), 1)
assert.EqualError(t, validator.Errors()[0], "ntp: option 'version' must be either 3 or 4 but it is configured as '1'")
} }

View File

@ -8,26 +8,32 @@ import (
) )
// ValidateRegulation validates and update regulator configuration. // ValidateRegulation validates and update regulator configuration.
func ValidateRegulation(configuration *schema.RegulationConfiguration, validator *schema.StructValidator) { func ValidateRegulation(config *schema.Configuration, validator *schema.StructValidator) {
if configuration.FindTime == "" { if config.Regulation == nil {
configuration.FindTime = schema.DefaultRegulationConfiguration.FindTime // 2 min. config.Regulation = &schema.DefaultRegulationConfiguration
return
} }
if configuration.BanTime == "" { if config.Regulation.FindTime == "" {
configuration.BanTime = schema.DefaultRegulationConfiguration.BanTime // 5 min. config.Regulation.FindTime = schema.DefaultRegulationConfiguration.FindTime // 2 min.
} }
findTime, err := utils.ParseDurationString(configuration.FindTime) if config.Regulation.BanTime == "" {
config.Regulation.BanTime = schema.DefaultRegulationConfiguration.BanTime // 5 min.
}
findTime, err := utils.ParseDurationString(config.Regulation.FindTime)
if err != nil { if err != nil {
validator.Push(fmt.Errorf("Error occurred parsing regulation find_time string: %s", err)) validator.Push(fmt.Errorf(errFmtRegulationParseDuration, "find_time", err))
} }
banTime, err := utils.ParseDurationString(configuration.BanTime) banTime, err := utils.ParseDurationString(config.Regulation.BanTime)
if err != nil { if err != nil {
validator.Push(fmt.Errorf("Error occurred parsing regulation ban_time string: %s", err)) validator.Push(fmt.Errorf(errFmtRegulationParseDuration, "ban_time", err))
} }
if findTime > banTime { if findTime > banTime {
validator.Push(fmt.Errorf("find_time cannot be greater than ban_time")) validator.Push(fmt.Errorf(errFmtRegulationFindTimeGreaterThanBanTime))
} }
} }

View File

@ -8,8 +8,11 @@ import (
"github.com/authelia/authelia/v4/internal/configuration/schema" "github.com/authelia/authelia/v4/internal/configuration/schema"
) )
func newDefaultRegulationConfig() schema.RegulationConfiguration { func newDefaultRegulationConfig() schema.Configuration {
config := schema.RegulationConfiguration{} config := schema.Configuration{
Regulation: &schema.RegulationConfiguration{},
}
return config return config
} }
@ -20,7 +23,7 @@ func TestShouldSetDefaultRegulationBanTime(t *testing.T) {
ValidateRegulation(&config, validator) ValidateRegulation(&config, validator)
assert.Len(t, validator.Errors(), 0) assert.Len(t, validator.Errors(), 0)
assert.Equal(t, schema.DefaultRegulationConfiguration.BanTime, config.BanTime) assert.Equal(t, schema.DefaultRegulationConfiguration.BanTime, config.Regulation.BanTime)
} }
func TestShouldSetDefaultRegulationFindTime(t *testing.T) { func TestShouldSetDefaultRegulationFindTime(t *testing.T) {
@ -30,30 +33,30 @@ func TestShouldSetDefaultRegulationFindTime(t *testing.T) {
ValidateRegulation(&config, validator) ValidateRegulation(&config, validator)
assert.Len(t, validator.Errors(), 0) assert.Len(t, validator.Errors(), 0)
assert.Equal(t, schema.DefaultRegulationConfiguration.FindTime, config.FindTime) assert.Equal(t, schema.DefaultRegulationConfiguration.FindTime, config.Regulation.FindTime)
} }
func TestShouldRaiseErrorWhenFindTimeLessThanBanTime(t *testing.T) { func TestShouldRaiseErrorWhenFindTimeLessThanBanTime(t *testing.T) {
validator := schema.NewStructValidator() validator := schema.NewStructValidator()
config := newDefaultRegulationConfig() config := newDefaultRegulationConfig()
config.FindTime = "1m" config.Regulation.FindTime = "1m"
config.BanTime = "10s" config.Regulation.BanTime = "10s"
ValidateRegulation(&config, validator) ValidateRegulation(&config, validator)
assert.Len(t, validator.Errors(), 1) assert.Len(t, validator.Errors(), 1)
assert.EqualError(t, validator.Errors()[0], "find_time cannot be greater than ban_time") assert.EqualError(t, validator.Errors()[0], "regulation: option 'find_time' must be less than or equal to option 'ban_time'")
} }
func TestShouldRaiseErrorOnBadDurationStrings(t *testing.T) { func TestShouldRaiseErrorOnBadDurationStrings(t *testing.T) {
validator := schema.NewStructValidator() validator := schema.NewStructValidator()
config := newDefaultRegulationConfig() config := newDefaultRegulationConfig()
config.FindTime = "a year" config.Regulation.FindTime = "a year"
config.BanTime = "forever" config.Regulation.BanTime = "forever"
ValidateRegulation(&config, validator) ValidateRegulation(&config, validator)
assert.Len(t, validator.Errors(), 2) assert.Len(t, validator.Errors(), 2)
assert.EqualError(t, validator.Errors()[0], "Error occurred parsing regulation find_time string: could not convert the input string of a year into a duration") assert.EqualError(t, validator.Errors()[0], "regulation: option 'find_time' could not be parsed: could not parse 'a year' as a duration")
assert.EqualError(t, validator.Errors()[1], "Error occurred parsing regulation ban_time string: could not convert the input string of forever into a duration") assert.EqualError(t, validator.Errors()[1], "regulation: option 'ban_time' could not be parsed: could not parse 'forever' as a duration")
} }

View File

@ -10,40 +10,41 @@ import (
) )
// ValidateServer checks a server configuration is correct. // ValidateServer checks a server configuration is correct.
func ValidateServer(configuration *schema.Configuration, validator *schema.StructValidator) { func ValidateServer(config *schema.Configuration, validator *schema.StructValidator) {
if configuration.Server.Host == "" { if config.Server.Host == "" {
configuration.Server.Host = schema.DefaultServerConfiguration.Host config.Server.Host = schema.DefaultServerConfiguration.Host
} }
if configuration.Server.Port == 0 { if config.Server.Port == 0 {
configuration.Server.Port = schema.DefaultServerConfiguration.Port config.Server.Port = schema.DefaultServerConfiguration.Port
} }
if configuration.Server.TLS.Key != "" && configuration.Server.TLS.Certificate == "" { if config.Server.TLS.Key != "" && config.Server.TLS.Certificate == "" {
validator.Push(fmt.Errorf("server: no TLS certificate provided to accompany the TLS key, please configure the 'server.tls.certificate' option")) validator.Push(fmt.Errorf(errFmtServerTLSCert))
} else if configuration.Server.TLS.Key == "" && configuration.Server.TLS.Certificate != "" { } else if config.Server.TLS.Key == "" && config.Server.TLS.Certificate != "" {
validator.Push(fmt.Errorf("server: no TLS key provided to accompany the TLS certificate, please configure the 'server.tls.key' option")) validator.Push(fmt.Errorf(errFmtServerTLSKey))
} }
switch { switch {
case strings.Contains(configuration.Server.Path, "/"): case strings.Contains(config.Server.Path, "/"):
validator.Push(fmt.Errorf("server path must not contain any forward slashes")) validator.Push(fmt.Errorf(errFmtServerPathNoForwardSlashes))
case !utils.IsStringAlphaNumeric(configuration.Server.Path): case !utils.IsStringAlphaNumeric(config.Server.Path):
validator.Push(fmt.Errorf("server path must only be alpha numeric characters")) validator.Push(fmt.Errorf(errFmtServerPathAlphaNum))
case configuration.Server.Path == "": // Don't do anything if it's blank. case config.Server.Path == "": // Don't do anything if it's blank.
break
default: default:
configuration.Server.Path = path.Clean("/" + configuration.Server.Path) config.Server.Path = path.Clean("/" + config.Server.Path)
} }
if configuration.Server.ReadBufferSize == 0 { if config.Server.ReadBufferSize == 0 {
configuration.Server.ReadBufferSize = schema.DefaultServerConfiguration.ReadBufferSize config.Server.ReadBufferSize = schema.DefaultServerConfiguration.ReadBufferSize
} else if configuration.Server.ReadBufferSize < 0 { } else if config.Server.ReadBufferSize < 0 {
validator.Push(fmt.Errorf("server read buffer size must be above 0")) validator.Push(fmt.Errorf(errFmtServerBufferSize, "read", config.Server.ReadBufferSize))
} }
if configuration.Server.WriteBufferSize == 0 { if config.Server.WriteBufferSize == 0 {
configuration.Server.WriteBufferSize = schema.DefaultServerConfiguration.WriteBufferSize config.Server.WriteBufferSize = schema.DefaultServerConfiguration.WriteBufferSize
} else if configuration.Server.WriteBufferSize < 0 { } else if config.Server.WriteBufferSize < 0 {
validator.Push(fmt.Errorf("server write buffer size must be above 0")) validator.Push(fmt.Errorf(errFmtServerBufferSize, "write", config.Server.WriteBufferSize))
} }
} }

View File

@ -71,8 +71,8 @@ func TestShouldRaiseOnNegativeValues(t *testing.T) {
require.Len(t, validator.Errors(), 2) require.Len(t, validator.Errors(), 2)
assert.EqualError(t, validator.Errors()[0], "server read buffer size must be above 0") assert.EqualError(t, validator.Errors()[0], "server: option 'read_buffer_size' must be above 0 but it is configured as '-1'")
assert.EqualError(t, validator.Errors()[1], "server write buffer size must be above 0") assert.EqualError(t, validator.Errors()[1], "server: option 'write_buffer_size' must be above 0 but it is configured as '-1'")
} }
func TestShouldRaiseOnNonAlphanumericCharsInPath(t *testing.T) { func TestShouldRaiseOnNonAlphanumericCharsInPath(t *testing.T) {
@ -123,7 +123,7 @@ func TestShouldRaiseErrorWhenTLSCertWithoutKeyIsProvided(t *testing.T) {
ValidateServer(&config, validator) ValidateServer(&config, validator)
require.Len(t, validator.Errors(), 1) require.Len(t, validator.Errors(), 1)
assert.EqualError(t, validator.Errors()[0], "server: no TLS key provided to accompany the TLS certificate, please configure the 'server.tls.key' option") assert.EqualError(t, validator.Errors()[0], "server: tls: option 'certificate' must also be accompanied by option 'key'")
} }
func TestShouldRaiseErrorWhenTLSKeyWithoutCertIsProvided(t *testing.T) { func TestShouldRaiseErrorWhenTLSKeyWithoutCertIsProvided(t *testing.T) {
@ -133,7 +133,7 @@ func TestShouldRaiseErrorWhenTLSKeyWithoutCertIsProvided(t *testing.T) {
ValidateServer(&config, validator) ValidateServer(&config, validator)
require.Len(t, validator.Errors(), 1) require.Len(t, validator.Errors(), 1)
assert.EqualError(t, validator.Errors()[0], "server: no TLS certificate provided to accompany the TLS key, please configure the 'server.tls.certificate' option") assert.EqualError(t, validator.Errors()[0], "server: tls: option 'key' must also be accompanied by option 'certificate'")
} }
func TestShouldNotRaiseErrorWhenBothTLSCertificateAndKeyAreProvided(t *testing.T) { func TestShouldNotRaiseErrorWhenBothTLSCertificateAndKeyAreProvided(t *testing.T) {

View File

@ -10,109 +10,110 @@ import (
) )
// ValidateSession validates and update session configuration. // ValidateSession validates and update session configuration.
func ValidateSession(configuration *schema.SessionConfiguration, validator *schema.StructValidator) { func ValidateSession(config *schema.SessionConfiguration, validator *schema.StructValidator) {
if configuration.Name == "" { if config.Name == "" {
configuration.Name = schema.DefaultSessionConfiguration.Name config.Name = schema.DefaultSessionConfiguration.Name
} }
if configuration.Redis != nil { if config.Redis != nil {
if configuration.Redis.HighAvailability != nil { if config.Redis.HighAvailability != nil {
if configuration.Redis.HighAvailability.SentinelName != "" { validateRedisSentinel(config, validator)
validateRedisSentinel(configuration, validator)
} else { } else {
validator.Push(fmt.Errorf("Session provider redis is configured for high availability but doesn't have a sentinel_name which is required")) validateRedis(config, validator)
}
} else {
validateRedis(configuration, validator)
} }
} }
validateSession(configuration, validator) validateSession(config, validator)
} }
func validateSession(configuration *schema.SessionConfiguration, validator *schema.StructValidator) { func validateSession(config *schema.SessionConfiguration, validator *schema.StructValidator) {
if configuration.Expiration == "" { if config.Expiration == "" {
configuration.Expiration = schema.DefaultSessionConfiguration.Expiration // 1 hour. config.Expiration = schema.DefaultSessionConfiguration.Expiration // 1 hour.
} else if _, err := utils.ParseDurationString(configuration.Expiration); err != nil { } else if _, err := utils.ParseDurationString(config.Expiration); err != nil {
validator.Push(fmt.Errorf("Error occurred parsing session expiration string: %s", err)) validator.Push(fmt.Errorf(errFmtSessionCouldNotParseDuration, "expiriation", err))
} }
if configuration.Inactivity == "" { if config.Inactivity == "" {
configuration.Inactivity = schema.DefaultSessionConfiguration.Inactivity // 5 min. config.Inactivity = schema.DefaultSessionConfiguration.Inactivity // 5 min.
} else if _, err := utils.ParseDurationString(configuration.Inactivity); err != nil { } else if _, err := utils.ParseDurationString(config.Inactivity); err != nil {
validator.Push(fmt.Errorf("Error occurred parsing session inactivity string: %s", err)) validator.Push(fmt.Errorf(errFmtSessionCouldNotParseDuration, "inactivity", err))
} }
if configuration.RememberMeDuration == "" { if config.RememberMeDuration == "" {
configuration.RememberMeDuration = schema.DefaultSessionConfiguration.RememberMeDuration // 1 month. config.RememberMeDuration = schema.DefaultSessionConfiguration.RememberMeDuration // 1 month.
} else if _, err := utils.ParseDurationString(configuration.RememberMeDuration); err != nil { } else if _, err := utils.ParseDurationString(config.RememberMeDuration); err != nil {
validator.Push(fmt.Errorf("Error occurred parsing session remember_me_duration string: %s", err)) validator.Push(fmt.Errorf(errFmtSessionCouldNotParseDuration, "remember_me_duration", err))
} }
if configuration.Domain == "" { if config.Domain == "" {
validator.Push(errors.New("Set domain of the session object")) validator.Push(fmt.Errorf(errFmtSessionOptionRequired, "domain"))
} }
if strings.Contains(configuration.Domain, "*") { if strings.HasPrefix(config.Domain, "*.") {
validator.Push(errors.New("The domain of the session must be the root domain you're protecting instead of a wildcard domain")) validator.Push(fmt.Errorf(errFmtSessionDomainMustBeRoot, config.Domain))
} }
if configuration.SameSite == "" { if config.SameSite == "" {
configuration.SameSite = schema.DefaultSessionConfiguration.SameSite config.SameSite = schema.DefaultSessionConfiguration.SameSite
} else if configuration.SameSite != "none" && configuration.SameSite != "lax" && configuration.SameSite != "strict" { } else if !utils.IsStringInSlice(config.SameSite, validSessionSameSiteValues) {
validator.Push(errors.New("session same_site is configured incorrectly, must be one of 'none', 'lax', or 'strict'")) validator.Push(fmt.Errorf(errFmtSessionSameSite, strings.Join(validSessionSameSiteValues, "', '"), config.SameSite))
} }
} }
func validateRedis(configuration *schema.SessionConfiguration, validator *schema.StructValidator) { func validateRedisCommon(config *schema.SessionConfiguration, validator *schema.StructValidator) {
if configuration.Redis.Host == "" { if config.Secret == "" {
validator.Push(fmt.Errorf(errFmtSessionRedisHostRequired, "redis")) validator.Push(fmt.Errorf(errFmtSessionSecretRequired, "redis"))
}
} }
if configuration.Secret == "" { func validateRedis(config *schema.SessionConfiguration, validator *schema.StructValidator) {
validator.Push(fmt.Errorf(errFmtSessionSecretRedisProvider, "redis")) if config.Redis.Host == "" {
validator.Push(fmt.Errorf(errFmtSessionRedisHostRequired))
} }
if !strings.HasPrefix(configuration.Redis.Host, "/") && configuration.Redis.Port == 0 { validateRedisCommon(config, validator)
if !strings.HasPrefix(config.Redis.Host, "/") && config.Redis.Port == 0 {
validator.Push(errors.New("A redis port different than 0 must be provided")) validator.Push(errors.New("A redis port different than 0 must be provided"))
} else if configuration.Redis.Port < 0 || configuration.Redis.Port > 65535 { } else if config.Redis.Port < 0 || config.Redis.Port > 65535 {
validator.Push(fmt.Errorf(errFmtSessionRedisPortRange, "redis")) validator.Push(fmt.Errorf(errFmtSessionRedisPortRange, config.Redis.Port))
} }
if configuration.Redis.MaximumActiveConnections <= 0 { if config.Redis.MaximumActiveConnections <= 0 {
configuration.Redis.MaximumActiveConnections = 8 config.Redis.MaximumActiveConnections = 8
} }
} }
func validateRedisSentinel(configuration *schema.SessionConfiguration, validator *schema.StructValidator) { func validateRedisSentinel(config *schema.SessionConfiguration, validator *schema.StructValidator) {
if configuration.Redis.Port == 0 { if config.Redis.HighAvailability.SentinelName == "" {
configuration.Redis.Port = 26379 validator.Push(fmt.Errorf(errFmtSessionRedisSentinelMissingName))
} else if configuration.Redis.Port < 0 || configuration.Redis.Port > 65535 {
validator.Push(fmt.Errorf(errFmtSessionRedisPortRange, "redis sentinel"))
} }
validateHighAvailability(configuration, validator, "redis sentinel") if config.Redis.Port == 0 {
config.Redis.Port = 26379
} else if config.Redis.Port < 0 || config.Redis.Port > 65535 {
validator.Push(fmt.Errorf(errFmtSessionRedisPortRange, config.Redis.Port))
} }
func validateHighAvailability(configuration *schema.SessionConfiguration, validator *schema.StructValidator, provider string) { if config.Redis.Host == "" && len(config.Redis.HighAvailability.Nodes) == 0 {
if configuration.Redis.Host == "" && len(configuration.Redis.HighAvailability.Nodes) == 0 { validator.Push(fmt.Errorf(errFmtSessionRedisHostOrNodesRequired))
validator.Push(fmt.Errorf(errFmtSessionRedisHostOrNodesRequired, provider))
} }
if configuration.Secret == "" { validateRedisCommon(config, validator)
validator.Push(fmt.Errorf(errFmtSessionSecretRedisProvider, provider))
}
for i, node := range configuration.Redis.HighAvailability.Nodes { hostMissing := false
for i, node := range config.Redis.HighAvailability.Nodes {
if node.Host == "" { if node.Host == "" {
validator.Push(fmt.Errorf("The %s nodes require a host set but you have not set the host for one or more nodes", provider)) hostMissing = true
break
} }
if node.Port == 0 { if node.Port == 0 {
if provider == "redis sentinel" { config.Redis.HighAvailability.Nodes[i].Port = 26379
configuration.Redis.HighAvailability.Nodes[i].Port = 26379
} }
} }
if hostMissing {
validator.Push(fmt.Errorf(errFmtSessionRedisSentinelNodeHostMissing))
} }
} }

View File

@ -100,7 +100,7 @@ func TestShouldRaiseErrorWithInvalidRedisPortLow(t *testing.T) {
assert.False(t, validator.HasWarnings()) assert.False(t, validator.HasWarnings())
require.Len(t, validator.Errors(), 1) require.Len(t, validator.Errors(), 1)
assert.EqualError(t, validator.Errors()[0], fmt.Sprintf(errFmtSessionRedisPortRange, "redis")) assert.EqualError(t, validator.Errors()[0], fmt.Sprintf(errFmtSessionRedisPortRange, -1))
} }
func TestShouldRaiseErrorWithInvalidRedisPortHigh(t *testing.T) { func TestShouldRaiseErrorWithInvalidRedisPortHigh(t *testing.T) {
@ -117,7 +117,7 @@ func TestShouldRaiseErrorWithInvalidRedisPortHigh(t *testing.T) {
assert.False(t, validator.HasWarnings()) assert.False(t, validator.HasWarnings())
require.Len(t, validator.Errors(), 1) require.Len(t, validator.Errors(), 1)
assert.EqualError(t, validator.Errors()[0], fmt.Sprintf(errFmtSessionRedisPortRange, "redis")) assert.EqualError(t, validator.Errors()[0], fmt.Sprintf(errFmtSessionRedisPortRange, 65536))
} }
func TestShouldRaiseErrorWhenRedisIsUsedAndSecretNotSet(t *testing.T) { func TestShouldRaiseErrorWhenRedisIsUsedAndSecretNotSet(t *testing.T) {
@ -140,7 +140,7 @@ func TestShouldRaiseErrorWhenRedisIsUsedAndSecretNotSet(t *testing.T) {
assert.False(t, validator.HasWarnings()) assert.False(t, validator.HasWarnings())
assert.Len(t, validator.Errors(), 1) assert.Len(t, validator.Errors(), 1)
assert.EqualError(t, validator.Errors()[0], fmt.Sprintf(errFmtSessionSecretRedisProvider, "redis")) assert.EqualError(t, validator.Errors()[0], fmt.Sprintf(errFmtSessionSecretRequired, "redis"))
} }
func TestShouldRaiseErrorWhenRedisHasHostnameButNoPort(t *testing.T) { func TestShouldRaiseErrorWhenRedisHasHostnameButNoPort(t *testing.T) {
@ -193,7 +193,7 @@ func TestShouldRaiseOneErrorWhenRedisHighAvailabilityHasNodesWithNoHost(t *testi
assert.False(t, validator.HasWarnings()) assert.False(t, validator.HasWarnings())
require.Len(t, errors, 1) require.Len(t, errors, 1)
assert.EqualError(t, errors[0], "The redis sentinel nodes require a host set but you have not set the host for one or more nodes") assert.EqualError(t, errors[0], "session: redis: high_availability: option 'nodes': option 'host' is required for each node but one or more nodes are missing this")
} }
func TestShouldRaiseOneErrorWhenRedisHighAvailabilityDoesNotHaveSentinelName(t *testing.T) { func TestShouldRaiseOneErrorWhenRedisHighAvailabilityDoesNotHaveSentinelName(t *testing.T) {
@ -215,7 +215,7 @@ func TestShouldRaiseOneErrorWhenRedisHighAvailabilityDoesNotHaveSentinelName(t *
assert.False(t, validator.HasWarnings()) assert.False(t, validator.HasWarnings())
require.Len(t, errors, 1) require.Len(t, errors, 1)
assert.EqualError(t, errors[0], "Session provider redis is configured for high availability but doesn't have a sentinel_name which is required") assert.EqualError(t, errors[0], "session: redis: high_availability: option 'sentinel_name' is required")
} }
func TestShouldUpdateDefaultPortWhenRedisSentinelHasNodes(t *testing.T) { func TestShouldUpdateDefaultPortWhenRedisSentinelHasNodes(t *testing.T) {
@ -281,8 +281,8 @@ func TestShouldRaiseErrorsWhenRedisSentinelOptionsIncorrectlyConfigured(t *testi
assert.False(t, validator.HasWarnings()) assert.False(t, validator.HasWarnings())
require.Len(t, errors, 2) require.Len(t, errors, 2)
assert.EqualError(t, errors[0], fmt.Sprintf(errFmtSessionRedisPortRange, "redis sentinel")) assert.EqualError(t, errors[0], fmt.Sprintf(errFmtSessionRedisPortRange, 65536))
assert.EqualError(t, errors[1], fmt.Sprintf(errFmtSessionSecretRedisProvider, "redis sentinel")) assert.EqualError(t, errors[1], fmt.Sprintf(errFmtSessionSecretRequired, "redis"))
validator.Clear() validator.Clear()
@ -295,8 +295,8 @@ func TestShouldRaiseErrorsWhenRedisSentinelOptionsIncorrectlyConfigured(t *testi
assert.False(t, validator.HasWarnings()) assert.False(t, validator.HasWarnings())
require.Len(t, errors, 2) require.Len(t, errors, 2)
assert.EqualError(t, errors[0], fmt.Sprintf(errFmtSessionRedisPortRange, "redis sentinel")) assert.EqualError(t, errors[0], fmt.Sprintf(errFmtSessionRedisPortRange, -1))
assert.EqualError(t, errors[1], fmt.Sprintf(errFmtSessionSecretRedisProvider, "redis sentinel")) assert.EqualError(t, errors[1], fmt.Sprintf(errFmtSessionSecretRequired, "redis"))
} }
func TestShouldNotRaiseErrorsAndSetDefaultPortWhenRedisSentinelPortBlank(t *testing.T) { func TestShouldNotRaiseErrorsAndSetDefaultPortWhenRedisSentinelPortBlank(t *testing.T) {
@ -347,7 +347,7 @@ func TestShouldRaiseErrorWhenRedisHostAndHighAvailabilityNodesEmpty(t *testing.T
assert.False(t, validator.HasWarnings()) assert.False(t, validator.HasWarnings())
require.Len(t, validator.Errors(), 1) require.Len(t, validator.Errors(), 1)
assert.EqualError(t, validator.Errors()[0], fmt.Sprintf(errFmtSessionRedisHostOrNodesRequired, "redis sentinel")) assert.EqualError(t, validator.Errors()[0], errFmtSessionRedisHostOrNodesRequired)
} }
func TestShouldRaiseErrorsWhenRedisHostNotSet(t *testing.T) { func TestShouldRaiseErrorsWhenRedisHostNotSet(t *testing.T) {
@ -365,7 +365,7 @@ func TestShouldRaiseErrorsWhenRedisHostNotSet(t *testing.T) {
assert.False(t, validator.HasWarnings()) assert.False(t, validator.HasWarnings())
require.Len(t, errors, 1) require.Len(t, errors, 1)
assert.EqualError(t, errors[0], fmt.Sprintf(errFmtSessionRedisHostRequired, "redis")) assert.EqualError(t, errors[0], errFmtSessionRedisHostRequired)
} }
func TestShouldRaiseErrorWhenDomainNotSet(t *testing.T) { func TestShouldRaiseErrorWhenDomainNotSet(t *testing.T) {
@ -377,7 +377,7 @@ func TestShouldRaiseErrorWhenDomainNotSet(t *testing.T) {
assert.False(t, validator.HasWarnings()) assert.False(t, validator.HasWarnings())
assert.Len(t, validator.Errors(), 1) assert.Len(t, validator.Errors(), 1)
assert.EqualError(t, validator.Errors()[0], "Set domain of the session object") assert.EqualError(t, validator.Errors()[0], "session: option 'domain' is required")
} }
func TestShouldRaiseErrorWhenDomainIsWildcard(t *testing.T) { func TestShouldRaiseErrorWhenDomainIsWildcard(t *testing.T) {
@ -389,7 +389,7 @@ func TestShouldRaiseErrorWhenDomainIsWildcard(t *testing.T) {
assert.False(t, validator.HasWarnings()) assert.False(t, validator.HasWarnings())
assert.Len(t, validator.Errors(), 1) assert.Len(t, validator.Errors(), 1)
assert.EqualError(t, validator.Errors()[0], "The domain of the session must be the root domain you're protecting instead of a wildcard domain") assert.EqualError(t, validator.Errors()[0], "session: option 'domain' must be the domain you wish to protect not a wildcard domain but it is configured as '*.example.com'")
} }
func TestShouldRaiseErrorWhenSameSiteSetIncorrectly(t *testing.T) { func TestShouldRaiseErrorWhenSameSiteSetIncorrectly(t *testing.T) {
@ -401,7 +401,7 @@ func TestShouldRaiseErrorWhenSameSiteSetIncorrectly(t *testing.T) {
assert.False(t, validator.HasWarnings()) assert.False(t, validator.HasWarnings())
assert.Len(t, validator.Errors(), 1) assert.Len(t, validator.Errors(), 1)
assert.EqualError(t, validator.Errors()[0], "session same_site is configured incorrectly, must be one of 'none', 'lax', or 'strict'") assert.EqualError(t, validator.Errors()[0], "session: option 'same_site' must be one of 'none', 'lax', 'strict' but is configured as 'NOne'")
} }
func TestShouldNotRaiseErrorWhenSameSiteSetCorrectly(t *testing.T) { func TestShouldNotRaiseErrorWhenSameSiteSetCorrectly(t *testing.T) {
@ -430,8 +430,8 @@ func TestShouldRaiseErrorWhenBadInactivityAndExpirationSet(t *testing.T) {
assert.False(t, validator.HasWarnings()) assert.False(t, validator.HasWarnings())
assert.Len(t, validator.Errors(), 2) assert.Len(t, validator.Errors(), 2)
assert.EqualError(t, validator.Errors()[0], "Error occurred parsing session expiration string: could not convert the input string of -1 into a duration") assert.EqualError(t, validator.Errors()[0], "session: option 'expiriation' could not be parsed: could not parse '-1' as a duration")
assert.EqualError(t, validator.Errors()[1], "Error occurred parsing session inactivity string: could not convert the input string of -1 into a duration") assert.EqualError(t, validator.Errors()[1], "session: option 'inactivity' could not be parsed: could not parse '-1' as a duration")
} }
func TestShouldRaiseErrorWhenBadRememberMeDurationSet(t *testing.T) { func TestShouldRaiseErrorWhenBadRememberMeDurationSet(t *testing.T) {
@ -443,7 +443,7 @@ func TestShouldRaiseErrorWhenBadRememberMeDurationSet(t *testing.T) {
assert.False(t, validator.HasWarnings()) assert.False(t, validator.HasWarnings())
assert.Len(t, validator.Errors(), 1) assert.Len(t, validator.Errors(), 1)
assert.EqualError(t, validator.Errors()[0], "Error occurred parsing session remember_me_duration string: could not convert the input string of 1 year into a duration") assert.EqualError(t, validator.Errors()[0], "session: option 'remember_me_duration' could not be parsed: could not parse '1 year' as a duration")
} }
func TestShouldSetDefaultRememberMeDuration(t *testing.T) { func TestShouldSetDefaultRememberMeDuration(t *testing.T) {

View File

@ -10,66 +10,66 @@ import (
) )
// ValidateStorage validates storage configuration. // ValidateStorage validates storage configuration.
func ValidateStorage(configuration schema.StorageConfiguration, validator *schema.StructValidator) { func ValidateStorage(config schema.StorageConfiguration, validator *schema.StructValidator) {
if configuration.Local == nil && configuration.MySQL == nil && configuration.PostgreSQL == nil { if config.Local == nil && config.MySQL == nil && config.PostgreSQL == nil {
validator.Push(errors.New(errStrStorage)) validator.Push(errors.New(errStrStorage))
} }
switch { switch {
case configuration.MySQL != nil: case config.MySQL != nil:
validateSQLConfiguration(&configuration.MySQL.SQLStorageConfiguration, validator, "mysql") validateSQLConfiguration(&config.MySQL.SQLStorageConfiguration, validator, "mysql")
case configuration.PostgreSQL != nil: case config.PostgreSQL != nil:
validatePostgreSQLConfiguration(configuration.PostgreSQL, validator) validatePostgreSQLConfiguration(config.PostgreSQL, validator)
case configuration.Local != nil: case config.Local != nil:
validateLocalStorageConfiguration(configuration.Local, validator) validateLocalStorageConfiguration(config.Local, validator)
} }
if configuration.EncryptionKey == "" { if config.EncryptionKey == "" {
validator.Push(errors.New(errStrStorageEncryptionKeyMustBeProvided)) validator.Push(errors.New(errStrStorageEncryptionKeyMustBeProvided))
} else if len(configuration.EncryptionKey) < 20 { } else if len(config.EncryptionKey) < 20 {
validator.Push(errors.New(errStrStorageEncryptionKeyTooShort)) validator.Push(errors.New(errStrStorageEncryptionKeyTooShort))
} }
} }
func validateSQLConfiguration(configuration *schema.SQLStorageConfiguration, validator *schema.StructValidator, provider string) { func validateSQLConfiguration(config *schema.SQLStorageConfiguration, validator *schema.StructValidator, provider string) {
if configuration.Timeout == 0 { if config.Timeout == 0 {
configuration.Timeout = schema.DefaultSQLStorageConfiguration.Timeout config.Timeout = schema.DefaultSQLStorageConfiguration.Timeout
} }
if configuration.Host == "" { if config.Host == "" {
validator.Push(fmt.Errorf(errFmtStorageOptionMustBeProvided, provider, "host")) validator.Push(fmt.Errorf(errFmtStorageOptionMustBeProvided, provider, "host"))
} }
if configuration.Username == "" || configuration.Password == "" { if config.Username == "" || config.Password == "" {
validator.Push(fmt.Errorf(errFmtStorageUserPassMustBeProvided, provider)) validator.Push(fmt.Errorf(errFmtStorageUserPassMustBeProvided, provider))
} }
if configuration.Database == "" { if config.Database == "" {
validator.Push(fmt.Errorf(errFmtStorageOptionMustBeProvided, provider, "database")) validator.Push(fmt.Errorf(errFmtStorageOptionMustBeProvided, provider, "database"))
} }
} }
func validatePostgreSQLConfiguration(configuration *schema.PostgreSQLStorageConfiguration, validator *schema.StructValidator) { func validatePostgreSQLConfiguration(config *schema.PostgreSQLStorageConfiguration, validator *schema.StructValidator) {
validateSQLConfiguration(&configuration.SQLStorageConfiguration, validator, "postgres") validateSQLConfiguration(&config.SQLStorageConfiguration, validator, "postgres")
if configuration.Schema == "" { if config.Schema == "" {
configuration.Schema = schema.DefaultPostgreSQLStorageConfiguration.Schema config.Schema = schema.DefaultPostgreSQLStorageConfiguration.Schema
} }
// Deprecated. TODO: Remove in v4.36.0. // Deprecated. TODO: Remove in v4.36.0.
if configuration.SSLMode != "" && configuration.SSL.Mode == "" { if config.SSLMode != "" && config.SSL.Mode == "" {
configuration.SSL.Mode = configuration.SSLMode config.SSL.Mode = config.SSLMode
} }
if configuration.SSL.Mode == "" { if config.SSL.Mode == "" {
configuration.SSL.Mode = schema.DefaultPostgreSQLStorageConfiguration.SSL.Mode config.SSL.Mode = schema.DefaultPostgreSQLStorageConfiguration.SSL.Mode
} else if !utils.IsStringInSlice(configuration.SSL.Mode, storagePostgreSQLValidSSLModes) { } else if !utils.IsStringInSlice(config.SSL.Mode, validStoragePostgreSQLSSLModes) {
validator.Push(fmt.Errorf(errFmtStoragePostgreSQLInvalidSSLMode, configuration.SSL.Mode, strings.Join(storagePostgreSQLValidSSLModes, "', '"))) validator.Push(fmt.Errorf(errFmtStoragePostgreSQLInvalidSSLMode, strings.Join(validStoragePostgreSQLSSLModes, "', '"), config.SSL.Mode))
} }
} }
func validateLocalStorageConfiguration(configuration *schema.LocalStorageConfiguration, validator *schema.StructValidator) { func validateLocalStorageConfiguration(config *schema.LocalStorageConfiguration, validator *schema.StructValidator) {
if configuration.Path == "" { if config.Path == "" {
validator.Push(fmt.Errorf(errFmtStorageOptionMustBeProvided, "local", "path")) validator.Push(fmt.Errorf(errFmtStorageOptionMustBeProvided, "local", "path"))
} }
} }

View File

@ -10,24 +10,24 @@ import (
type StorageSuite struct { type StorageSuite struct {
suite.Suite suite.Suite
configuration schema.StorageConfiguration config schema.StorageConfiguration
validator *schema.StructValidator validator *schema.StructValidator
} }
func (suite *StorageSuite) SetupTest() { func (suite *StorageSuite) SetupTest() {
suite.validator = schema.NewStructValidator() suite.validator = schema.NewStructValidator()
suite.configuration.EncryptionKey = testEncryptionKey suite.config.EncryptionKey = testEncryptionKey
suite.configuration.Local = nil suite.config.Local = nil
suite.configuration.PostgreSQL = nil suite.config.PostgreSQL = nil
suite.configuration.MySQL = nil suite.config.MySQL = nil
} }
func (suite *StorageSuite) TestShouldValidateOneStorageIsConfigured() { func (suite *StorageSuite) TestShouldValidateOneStorageIsConfigured() {
suite.configuration.Local = nil suite.config.Local = nil
suite.configuration.PostgreSQL = nil suite.config.PostgreSQL = nil
suite.configuration.MySQL = nil suite.config.MySQL = nil
ValidateStorage(suite.configuration, suite.validator) ValidateStorage(suite.config, suite.validator)
suite.Require().Len(suite.validator.Warnings(), 0) suite.Require().Len(suite.validator.Warnings(), 0)
suite.Require().Len(suite.validator.Errors(), 1) suite.Require().Len(suite.validator.Errors(), 1)
@ -35,37 +35,37 @@ func (suite *StorageSuite) TestShouldValidateOneStorageIsConfigured() {
} }
func (suite *StorageSuite) TestShouldValidateLocalPathIsProvided() { func (suite *StorageSuite) TestShouldValidateLocalPathIsProvided() {
suite.configuration.Local = &schema.LocalStorageConfiguration{ suite.config.Local = &schema.LocalStorageConfiguration{
Path: "", Path: "",
} }
ValidateStorage(suite.configuration, suite.validator) ValidateStorage(suite.config, suite.validator)
suite.Require().Len(suite.validator.Warnings(), 0) suite.Require().Len(suite.validator.Warnings(), 0)
suite.Require().Len(suite.validator.Errors(), 1) suite.Require().Len(suite.validator.Errors(), 1)
suite.Assert().EqualError(suite.validator.Errors()[0], "storage: local: 'path' configuration option must be provided") suite.Assert().EqualError(suite.validator.Errors()[0], "storage: local: option 'path' is required")
suite.validator.Clear() suite.validator.Clear()
suite.configuration.Local.Path = "/myapth" suite.config.Local.Path = "/myapth"
ValidateStorage(suite.configuration, suite.validator) ValidateStorage(suite.config, suite.validator)
suite.Require().Len(suite.validator.Warnings(), 0) suite.Require().Len(suite.validator.Warnings(), 0)
suite.Require().Len(suite.validator.Errors(), 0) suite.Require().Len(suite.validator.Errors(), 0)
} }
func (suite *StorageSuite) TestShouldValidateMySQLHostUsernamePasswordAndDatabaseAreProvided() { func (suite *StorageSuite) TestShouldValidateMySQLHostUsernamePasswordAndDatabaseAreProvided() {
suite.configuration.MySQL = &schema.MySQLStorageConfiguration{} suite.config.MySQL = &schema.MySQLStorageConfiguration{}
ValidateStorage(suite.configuration, suite.validator) ValidateStorage(suite.config, suite.validator)
suite.Require().Len(suite.validator.Errors(), 3) suite.Require().Len(suite.validator.Errors(), 3)
suite.Assert().EqualError(suite.validator.Errors()[0], "storage: mysql: 'host' configuration option must be provided") suite.Assert().EqualError(suite.validator.Errors()[0], "storage: mysql: option 'host' is required")
suite.Assert().EqualError(suite.validator.Errors()[1], "storage: mysql: 'username' and 'password' configuration options must be provided") suite.Assert().EqualError(suite.validator.Errors()[1], "storage: mysql: option 'username' and 'password' are required")
suite.Assert().EqualError(suite.validator.Errors()[2], "storage: mysql: 'database' configuration option must be provided") suite.Assert().EqualError(suite.validator.Errors()[2], "storage: mysql: option 'database' is required")
suite.validator.Clear() suite.validator.Clear()
suite.configuration.MySQL = &schema.MySQLStorageConfiguration{ suite.config.MySQL = &schema.MySQLStorageConfiguration{
SQLStorageConfiguration: schema.SQLStorageConfiguration{ SQLStorageConfiguration: schema.SQLStorageConfiguration{
Host: "localhost", Host: "localhost",
Username: "myuser", Username: "myuser",
@ -73,24 +73,24 @@ func (suite *StorageSuite) TestShouldValidateMySQLHostUsernamePasswordAndDatabas
Database: "database", Database: "database",
}, },
} }
ValidateStorage(suite.configuration, suite.validator) ValidateStorage(suite.config, suite.validator)
suite.Require().Len(suite.validator.Warnings(), 0) suite.Require().Len(suite.validator.Warnings(), 0)
suite.Require().Len(suite.validator.Errors(), 0) suite.Require().Len(suite.validator.Errors(), 0)
} }
func (suite *StorageSuite) TestShouldValidatePostgreSQLHostUsernamePasswordAndDatabaseAreProvided() { func (suite *StorageSuite) TestShouldValidatePostgreSQLHostUsernamePasswordAndDatabaseAreProvided() {
suite.configuration.PostgreSQL = &schema.PostgreSQLStorageConfiguration{} suite.config.PostgreSQL = &schema.PostgreSQLStorageConfiguration{}
suite.configuration.MySQL = nil suite.config.MySQL = nil
ValidateStorage(suite.configuration, suite.validator) ValidateStorage(suite.config, suite.validator)
suite.Require().Len(suite.validator.Errors(), 3) suite.Require().Len(suite.validator.Errors(), 3)
suite.Assert().EqualError(suite.validator.Errors()[0], "storage: postgres: 'host' configuration option must be provided") suite.Assert().EqualError(suite.validator.Errors()[0], "storage: postgres: option 'host' is required")
suite.Assert().EqualError(suite.validator.Errors()[1], "storage: postgres: 'username' and 'password' configuration options must be provided") suite.Assert().EqualError(suite.validator.Errors()[1], "storage: postgres: option 'username' and 'password' are required")
suite.Assert().EqualError(suite.validator.Errors()[2], "storage: postgres: 'database' configuration option must be provided") suite.Assert().EqualError(suite.validator.Errors()[2], "storage: postgres: option 'database' is required")
suite.validator.Clear() suite.validator.Clear()
suite.configuration.PostgreSQL = &schema.PostgreSQLStorageConfiguration{ suite.config.PostgreSQL = &schema.PostgreSQLStorageConfiguration{
SQLStorageConfiguration: schema.SQLStorageConfiguration{ SQLStorageConfiguration: schema.SQLStorageConfiguration{
Host: "postgre", Host: "postgre",
Username: "myuser", Username: "myuser",
@ -98,14 +98,14 @@ func (suite *StorageSuite) TestShouldValidatePostgreSQLHostUsernamePasswordAndDa
Database: "database", Database: "database",
}, },
} }
ValidateStorage(suite.configuration, suite.validator) ValidateStorage(suite.config, suite.validator)
suite.Assert().Len(suite.validator.Warnings(), 0) suite.Assert().Len(suite.validator.Warnings(), 0)
suite.Assert().Len(suite.validator.Errors(), 0) suite.Assert().Len(suite.validator.Errors(), 0)
} }
func (suite *StorageSuite) TestShouldValidatePostgresSSLModeAndSchemaDefaults() { func (suite *StorageSuite) TestShouldValidatePostgresSSLModeAndSchemaDefaults() {
suite.configuration.PostgreSQL = &schema.PostgreSQLStorageConfiguration{ suite.config.PostgreSQL = &schema.PostgreSQLStorageConfiguration{
SQLStorageConfiguration: schema.SQLStorageConfiguration{ SQLStorageConfiguration: schema.SQLStorageConfiguration{
Host: "db1", Host: "db1",
Username: "myuser", Username: "myuser",
@ -114,17 +114,17 @@ func (suite *StorageSuite) TestShouldValidatePostgresSSLModeAndSchemaDefaults()
}, },
} }
ValidateStorage(suite.configuration, suite.validator) ValidateStorage(suite.config, suite.validator)
suite.Assert().Len(suite.validator.Warnings(), 0) suite.Assert().Len(suite.validator.Warnings(), 0)
suite.Assert().Len(suite.validator.Errors(), 0) suite.Assert().Len(suite.validator.Errors(), 0)
suite.Assert().Equal("disable", suite.configuration.PostgreSQL.SSL.Mode) suite.Assert().Equal("disable", suite.config.PostgreSQL.SSL.Mode)
suite.Assert().Equal("public", suite.configuration.PostgreSQL.Schema) suite.Assert().Equal("public", suite.config.PostgreSQL.Schema)
} }
func (suite *StorageSuite) TestShouldValidatePostgresDefaultsDontOverrideConfiguration() { func (suite *StorageSuite) TestShouldValidatePostgresDefaultsDontOverrideConfiguration() {
suite.configuration.PostgreSQL = &schema.PostgreSQLStorageConfiguration{ suite.config.PostgreSQL = &schema.PostgreSQLStorageConfiguration{
SQLStorageConfiguration: schema.SQLStorageConfiguration{ SQLStorageConfiguration: schema.SQLStorageConfiguration{
Host: "db1", Host: "db1",
Username: "myuser", Username: "myuser",
@ -137,17 +137,17 @@ func (suite *StorageSuite) TestShouldValidatePostgresDefaultsDontOverrideConfigu
}, },
} }
ValidateStorage(suite.configuration, suite.validator) ValidateStorage(suite.config, suite.validator)
suite.Assert().Len(suite.validator.Warnings(), 0) suite.Assert().Len(suite.validator.Warnings(), 0)
suite.Assert().Len(suite.validator.Errors(), 0) suite.Assert().Len(suite.validator.Errors(), 0)
suite.Assert().Equal("require", suite.configuration.PostgreSQL.SSL.Mode) suite.Assert().Equal("require", suite.config.PostgreSQL.SSL.Mode)
suite.Assert().Equal("authelia", suite.configuration.PostgreSQL.Schema) suite.Assert().Equal("authelia", suite.config.PostgreSQL.Schema)
} }
func (suite *StorageSuite) TestShouldValidatePostgresSSLModeMustBeValid() { func (suite *StorageSuite) TestShouldValidatePostgresSSLModeMustBeValid() {
suite.configuration.PostgreSQL = &schema.PostgreSQLStorageConfiguration{ suite.config.PostgreSQL = &schema.PostgreSQLStorageConfiguration{
SQLStorageConfiguration: schema.SQLStorageConfiguration{ SQLStorageConfiguration: schema.SQLStorageConfiguration{
Host: "db2", Host: "db2",
Username: "myuser", Username: "myuser",
@ -159,16 +159,16 @@ func (suite *StorageSuite) TestShouldValidatePostgresSSLModeMustBeValid() {
}, },
} }
ValidateStorage(suite.configuration, suite.validator) ValidateStorage(suite.config, suite.validator)
suite.Assert().Len(suite.validator.Warnings(), 0) suite.Assert().Len(suite.validator.Warnings(), 0)
suite.Require().Len(suite.validator.Errors(), 1) suite.Require().Len(suite.validator.Errors(), 1)
suite.Assert().EqualError(suite.validator.Errors()[0], "storage: postgres: ssl: 'mode' configuration option 'unknown' is invalid: must be one of 'disable', 'require', 'verify-ca', 'verify-full'") suite.Assert().EqualError(suite.validator.Errors()[0], "storage: postgres: ssl: option 'mode' must be one of 'disable', 'require', 'verify-ca', 'verify-full' but it is configured as 'unknown'")
} }
// Deprecated. TODO: Remove in v4.36.0. // Deprecated. TODO: Remove in v4.36.0.
func (suite *StorageSuite) TestShouldValidatePostgresSSLModeMustBeMappedForDeprecations() { func (suite *StorageSuite) TestShouldValidatePostgresSSLModeMustBeMappedForDeprecations() {
suite.configuration.PostgreSQL = &schema.PostgreSQLStorageConfiguration{ suite.config.PostgreSQL = &schema.PostgreSQLStorageConfiguration{
SQLStorageConfiguration: schema.SQLStorageConfiguration{ SQLStorageConfiguration: schema.SQLStorageConfiguration{
Host: "pg", Host: "pg",
Username: "myuser", Username: "myuser",
@ -178,38 +178,38 @@ func (suite *StorageSuite) TestShouldValidatePostgresSSLModeMustBeMappedForDepre
SSLMode: "require", SSLMode: "require",
} }
ValidateStorage(suite.configuration, suite.validator) ValidateStorage(suite.config, suite.validator)
suite.Assert().Len(suite.validator.Warnings(), 0) suite.Assert().Len(suite.validator.Warnings(), 0)
suite.Assert().Len(suite.validator.Errors(), 0) suite.Assert().Len(suite.validator.Errors(), 0)
suite.Assert().Equal(suite.configuration.PostgreSQL.SSL.Mode, "require") suite.Assert().Equal(suite.config.PostgreSQL.SSL.Mode, "require")
} }
func (suite *StorageSuite) TestShouldRaiseErrorOnNoEncryptionKey() { func (suite *StorageSuite) TestShouldRaiseErrorOnNoEncryptionKey() {
suite.configuration.EncryptionKey = "" suite.config.EncryptionKey = ""
suite.configuration.Local = &schema.LocalStorageConfiguration{ suite.config.Local = &schema.LocalStorageConfiguration{
Path: "/this/is/a/path", Path: "/this/is/a/path",
} }
ValidateStorage(suite.configuration, suite.validator) ValidateStorage(suite.config, suite.validator)
suite.Require().Len(suite.validator.Warnings(), 0) suite.Require().Len(suite.validator.Warnings(), 0)
suite.Require().Len(suite.validator.Errors(), 1) suite.Require().Len(suite.validator.Errors(), 1)
suite.Assert().EqualError(suite.validator.Errors()[0], "storage: 'encryption_key' configuration option must be provided") suite.Assert().EqualError(suite.validator.Errors()[0], "storage: option 'encryption_key' must is required")
} }
func (suite *StorageSuite) TestShouldRaiseErrorOnShortEncryptionKey() { func (suite *StorageSuite) TestShouldRaiseErrorOnShortEncryptionKey() {
suite.configuration.EncryptionKey = "abc" suite.config.EncryptionKey = "abc"
suite.configuration.Local = &schema.LocalStorageConfiguration{ suite.config.Local = &schema.LocalStorageConfiguration{
Path: "/this/is/a/path", Path: "/this/is/a/path",
} }
ValidateStorage(suite.configuration, suite.validator) ValidateStorage(suite.config, suite.validator)
suite.Require().Len(suite.validator.Warnings(), 0) suite.Require().Len(suite.validator.Warnings(), 0)
suite.Require().Len(suite.validator.Errors(), 1) suite.Require().Len(suite.validator.Errors(), 1)
suite.Assert().EqualError(suite.validator.Errors()[0], "storage: 'encryption_key' configuration option must be 20 characters or longer") suite.Assert().EqualError(suite.validator.Errors()[0], "storage: option 'encryption_key' must be 20 characters or longer")
} }
func TestShouldRunStorageSuite(t *testing.T) { func TestShouldRunStorageSuite(t *testing.T) {

View File

@ -2,20 +2,19 @@ package validator
import ( import (
"fmt" "fmt"
"regexp" "strings"
"github.com/authelia/authelia/v4/internal/configuration/schema" "github.com/authelia/authelia/v4/internal/configuration/schema"
"github.com/authelia/authelia/v4/internal/utils"
) )
// ValidateTheme validates and update Theme configuration. // ValidateTheme validates and update Theme configuration.
func ValidateTheme(configuration *schema.Configuration, validator *schema.StructValidator) { func ValidateTheme(config *schema.Configuration, validator *schema.StructValidator) {
if configuration.Theme == "" { if config.Theme == "" {
configuration.Theme = "light" config.Theme = "light"
} }
validThemes := regexp.MustCompile("light|dark|grey|auto") if !utils.IsStringInSlice(config.Theme, validThemeNames) {
validator.Push(fmt.Errorf(errFmtThemeName, strings.Join(validThemeNames, "', '"), config.Theme))
if !validThemes.MatchString(configuration.Theme) {
validator.Push(fmt.Errorf("Theme: %s is not valid, valid themes are: \"light\", \"dark\", \"grey\" or \"auto\"", configuration.Theme))
} }
} }

View File

@ -10,33 +10,33 @@ import (
type Theme struct { type Theme struct {
suite.Suite suite.Suite
configuration *schema.Configuration config *schema.Configuration
validator *schema.StructValidator validator *schema.StructValidator
} }
func (suite *Theme) SetupTest() { func (suite *Theme) SetupTest() {
suite.validator = schema.NewStructValidator() suite.validator = schema.NewStructValidator()
suite.configuration = &schema.Configuration{ suite.config = &schema.Configuration{
Theme: "light", Theme: "light",
} }
} }
func (suite *Theme) TestShouldValidateCompleteConfiguration() { func (suite *Theme) TestShouldValidateCompleteConfiguration() {
ValidateTheme(suite.configuration, suite.validator) ValidateTheme(suite.config, suite.validator)
suite.Assert().False(suite.validator.HasWarnings()) suite.Assert().False(suite.validator.HasWarnings())
suite.Assert().False(suite.validator.HasErrors()) suite.Assert().False(suite.validator.HasErrors())
} }
func (suite *Theme) TestShouldRaiseErrorWhenInvalidThemeProvided() { func (suite *Theme) TestShouldRaiseErrorWhenInvalidThemeProvided() {
suite.configuration.Theme = "invalid" suite.config.Theme = "invalid"
ValidateTheme(suite.configuration, suite.validator) ValidateTheme(suite.config, suite.validator)
suite.Assert().False(suite.validator.HasWarnings()) suite.Assert().False(suite.validator.HasWarnings())
suite.Require().Len(suite.validator.Errors(), 1) suite.Require().Len(suite.validator.Errors(), 1)
suite.Assert().EqualError(suite.validator.Errors()[0], "Theme: invalid is not valid, valid themes are: \"light\", \"dark\", \"grey\" or \"auto\"") suite.Assert().EqualError(suite.validator.Errors()[0], "option 'theme' must be one of 'light', 'dark', 'grey', 'auto' but it is configured as 'invalid'")
} }
func TestThemes(t *testing.T) { func TestThemes(t *testing.T) {

View File

@ -9,40 +9,40 @@ import (
) )
// ValidateTOTP validates and update TOTP configuration. // ValidateTOTP validates and update TOTP configuration.
func ValidateTOTP(configuration *schema.Configuration, validator *schema.StructValidator) { func ValidateTOTP(config *schema.Configuration, validator *schema.StructValidator) {
if configuration.TOTP == nil { if config.TOTP == nil {
configuration.TOTP = &schema.DefaultTOTPConfiguration config.TOTP = &schema.DefaultTOTPConfiguration
return return
} }
if configuration.TOTP.Issuer == "" { if config.TOTP.Issuer == "" {
configuration.TOTP.Issuer = schema.DefaultTOTPConfiguration.Issuer config.TOTP.Issuer = schema.DefaultTOTPConfiguration.Issuer
} }
if configuration.TOTP.Algorithm == "" { if config.TOTP.Algorithm == "" {
configuration.TOTP.Algorithm = schema.DefaultTOTPConfiguration.Algorithm config.TOTP.Algorithm = schema.DefaultTOTPConfiguration.Algorithm
} else { } else {
configuration.TOTP.Algorithm = strings.ToUpper(configuration.TOTP.Algorithm) config.TOTP.Algorithm = strings.ToUpper(config.TOTP.Algorithm)
if !utils.IsStringInSlice(configuration.TOTP.Algorithm, schema.TOTPPossibleAlgorithms) { if !utils.IsStringInSlice(config.TOTP.Algorithm, schema.TOTPPossibleAlgorithms) {
validator.Push(fmt.Errorf(errFmtTOTPInvalidAlgorithm, configuration.TOTP.Algorithm, strings.Join(schema.TOTPPossibleAlgorithms, ", "))) validator.Push(fmt.Errorf(errFmtTOTPInvalidAlgorithm, strings.Join(schema.TOTPPossibleAlgorithms, "', '"), config.TOTP.Algorithm))
} }
} }
if configuration.TOTP.Period == 0 { if config.TOTP.Period == 0 {
configuration.TOTP.Period = schema.DefaultTOTPConfiguration.Period config.TOTP.Period = schema.DefaultTOTPConfiguration.Period
} else if configuration.TOTP.Period < 15 { } else if config.TOTP.Period < 15 {
validator.Push(fmt.Errorf(errFmtTOTPInvalidPeriod, configuration.TOTP.Period)) validator.Push(fmt.Errorf(errFmtTOTPInvalidPeriod, config.TOTP.Period))
} }
if configuration.TOTP.Digits == 0 { if config.TOTP.Digits == 0 {
configuration.TOTP.Digits = schema.DefaultTOTPConfiguration.Digits config.TOTP.Digits = schema.DefaultTOTPConfiguration.Digits
} else if configuration.TOTP.Digits != 6 && configuration.TOTP.Digits != 8 { } else if config.TOTP.Digits != 6 && config.TOTP.Digits != 8 {
validator.Push(fmt.Errorf(errFmtTOTPInvalidDigits, configuration.TOTP.Digits)) validator.Push(fmt.Errorf(errFmtTOTPInvalidDigits, config.TOTP.Digits))
} }
if configuration.TOTP.Skew == nil { if config.TOTP.Skew == nil {
configuration.TOTP.Skew = schema.DefaultTOTPConfiguration.Skew config.TOTP.Skew = schema.DefaultTOTPConfiguration.Skew
} }
} }

View File

@ -53,7 +53,7 @@ func TestShouldRaiseErrorWhenInvalidTOTPAlgorithm(t *testing.T) {
ValidateTOTP(config, validator) ValidateTOTP(config, validator)
require.Len(t, validator.Errors(), 1) require.Len(t, validator.Errors(), 1)
assert.EqualError(t, validator.Errors()[0], fmt.Sprintf(errFmtTOTPInvalidAlgorithm, "SHA3", strings.Join(schema.TOTPPossibleAlgorithms, ", "))) assert.EqualError(t, validator.Errors()[0], fmt.Sprintf(errFmtTOTPInvalidAlgorithm, strings.Join(schema.TOTPPossibleAlgorithms, "', '"), "SHA3"))
} }
func TestShouldRaiseErrorWhenInvalidTOTPValues(t *testing.T) { func TestShouldRaiseErrorWhenInvalidTOTPValues(t *testing.T) {

View File

@ -10,16 +10,19 @@ import (
) )
func TestShouldCheckNTP(t *testing.T) { func TestShouldCheckNTP(t *testing.T) {
config := schema.NTPConfiguration{ config := &schema.Configuration{
NTP: &schema.NTPConfiguration{
Address: "time.cloudflare.com:123", Address: "time.cloudflare.com:123",
Version: 4, Version: 4,
MaximumDesync: "3s", MaximumDesync: "3s",
DisableStartupCheck: false, DisableStartupCheck: false,
},
} }
sv := schema.NewStructValidator()
validator.ValidateNTP(&config, sv)
ntp := NewProvider(&config) sv := schema.NewStructValidator()
validator.ValidateNTP(config, sv)
ntp := NewProvider(config.NTP)
assert.NoError(t, ntp.StartupCheck()) assert.NoError(t, ntp.StartupCheck())
} }

View File

@ -41,6 +41,21 @@ access_control:
policy: two_factor policy: two_factor
- domain: "singlefactor.example.com" - domain: "singlefactor.example.com"
policy: one_factor policy: one_factor
- domain: "resources.example.com"
policy: one_factor
resources: ["^/resources"]
- domain: "method.example.com"
policy: one_factor
methods: ["POST"]
- domain: "network.example.com"
policy: one_factor
networks: ["192.168.1.0/24"]
- domain: "group.example.com"
policy: one_factor
subject: ["group:basic"]
- domain: "user.example.com"
policy: one_factor
subject: ["user:john"]
notifier: notifier:
filesystem: filesystem:

View File

@ -66,15 +66,15 @@ func (s *CLISuite) TestShouldPrintVersion() {
} }
func (s *CLISuite) TestShouldValidateConfig() { func (s *CLISuite) TestShouldValidateConfig() {
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "validate-config", "/config/configuration.yml"}) output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "validate-config", "--config", "/config/configuration.yml"})
s.Assert().NoError(err) s.Assert().NoError(err)
s.Assert().Contains(output, "Configuration parsed successfully without errors") s.Assert().Contains(output, "Configuration parsed and loaded successfully without errors.")
} }
func (s *CLISuite) TestShouldFailValidateConfig() { func (s *CLISuite) TestShouldFailValidateConfig() {
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "validate-config", "/config/invalid.yml"}) output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "validate-config", "--config", "/config/invalid.yml"})
s.Assert().NotNil(err) s.Assert().NoError(err)
s.Assert().Contains(output, "Error Loading Configuration: stat /config/invalid.yml: no such file or directory") s.Assert().Contains(output, "failed to load configuration from yaml file(/config/invalid.yml) source: open /config/invalid.yml: no such file or directory")
} }
func (s *CLISuite) TestShouldHashPasswordArgon2id() { func (s *CLISuite) TestShouldHashPasswordArgon2id() {
@ -168,12 +168,12 @@ func (s *CLISuite) TestStorageShouldShowErrWithoutConfig() {
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "schema-info"}) output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "schema-info"})
s.Assert().EqualError(err, "exit status 1") s.Assert().EqualError(err, "exit status 1")
s.Assert().Contains(output, "Error: storage: configuration for a 'local', 'mysql' or 'postgres' database must be provided, storage: 'encryption_key' configuration option must be provided\n") s.Assert().Contains(output, "Error: storage: configuration for a 'local', 'mysql' or 'postgres' database must be provided, storage: option 'encryption_key' must is required\n")
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "migrate", "history"}) output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "migrate", "history"})
s.Assert().EqualError(err, "exit status 1") s.Assert().EqualError(err, "exit status 1")
s.Assert().Contains(output, "Error: storage: configuration for a 'local', 'mysql' or 'postgres' database must be provided, storage: 'encryption_key' configuration option must be provided\n") s.Assert().Contains(output, "Error: storage: configuration for a 'local', 'mysql' or 'postgres' database must be provided, storage: option 'encryption_key' must is required\n")
} }
func (s *CLISuite) TestStorage00ShouldShowCorrectPreInitInformation() { func (s *CLISuite) TestStorage00ShouldShowCorrectPreInitInformation() {
@ -382,6 +382,94 @@ func (s *CLISuite) TestStorage05ShouldMigrateDown() {
s.Regexp(pattern1, output) s.Regexp(pattern1, output)
} }
func (s *CLISuite) TestACLPolicyCheckVerbose() {
output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "access-control", "check-policy", "--url=https://public.example.com", "--verbose", "--config", "/config/configuration.yml"})
s.Assert().NoError(err)
// This is an example of `authelia access-control check-policy --config .\internal\suites\CLI\configuration.yml --url=https://public.example.com --verbose`.
s.Contains(output, "Performing policy check for request to 'https://public.example.com' method 'GET'.\n\n")
s.Contains(output, " #\tDomain\tResource\tMethod\tNetwork\tSubject\n")
s.Contains(output, "* 1\thit\thit\t\thit\thit\thit\n")
s.Contains(output, " 2\tmiss\thit\t\thit\thit\thit\n")
s.Contains(output, " 3\tmiss\thit\t\thit\thit\thit\n")
s.Contains(output, " 4\tmiss\thit\t\thit\thit\thit\n")
s.Contains(output, " 5\tmiss\tmiss\t\thit\thit\thit\n")
s.Contains(output, " 6\tmiss\thit\t\tmiss\thit\thit\n")
s.Contains(output, " 7\tmiss\thit\t\thit\tmiss\thit\n")
s.Contains(output, " 8\tmiss\thit\t\thit\thit\tmay\n")
s.Contains(output, " 9\tmiss\thit\t\thit\thit\tmay\n")
s.Contains(output, "The policy 'bypass' from rule #1 will be applied to this request.")
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "access-control", "check-policy", "--url=https://admin.example.com", "--method=HEAD", "--username=tom", "--groups=basic,test", "--ip=192.168.2.3", "--verbose", "--config", "/config/configuration.yml"})
s.Assert().NoError(err)
// This is an example of `authelia access-control check-policy --config .\internal\suites\CLI\configuration.yml --url=https://admin.example.com --method=HEAD --username=tom --groups=basic,test --ip=192.168.2.3 --verbose`.
s.Contains(output, "Performing policy check for request to 'https://admin.example.com' method 'HEAD' username 'tom' groups 'basic,test' from IP '192.168.2.3'.\n\n")
s.Contains(output, " #\tDomain\tResource\tMethod\tNetwork\tSubject\n")
s.Contains(output, " #\tDomain\tResource\tMethod\tNetwork\tSubject\n")
s.Contains(output, " 1\tmiss\thit\t\thit\thit\thit\n")
s.Contains(output, "* 2\thit\thit\t\thit\thit\thit\n")
s.Contains(output, " 3\tmiss\thit\t\thit\thit\thit\n")
s.Contains(output, " 4\tmiss\thit\t\thit\thit\thit\n")
s.Contains(output, " 5\tmiss\tmiss\t\thit\thit\thit\n")
s.Contains(output, " 6\tmiss\thit\t\tmiss\thit\thit\n")
s.Contains(output, " 7\tmiss\thit\t\thit\tmiss\thit\n")
s.Contains(output, " 8\tmiss\thit\t\thit\thit\thit\n")
s.Contains(output, " 9\tmiss\thit\t\thit\thit\tmiss\n")
s.Contains(output, "The policy 'two_factor' from rule #2 will be applied to this request.")
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "access-control", "check-policy", "--url=https://resources.example.com/resources/test", "--method=POST", "--username=john", "--groups=admin,test", "--ip=192.168.1.3", "--verbose", "--config", "/config/configuration.yml"})
s.Assert().NoError(err)
// This is an example of `authelia access-control check-policy --config .\internal\suites\CLI\configuration.yml --url=https://resources.example.com/resources/test --method=POST --username=john --groups=admin,test --ip=192.168.1.3 --verbose`.
s.Contains(output, "Performing policy check for request to 'https://resources.example.com/resources/test' method 'POST' username 'john' groups 'admin,test' from IP '192.168.1.3'.\n\n")
s.Contains(output, " #\tDomain\tResource\tMethod\tNetwork\tSubject\n")
s.Contains(output, " 1\tmiss\thit\t\thit\thit\thit\n")
s.Contains(output, " 2\tmiss\thit\t\thit\thit\thit\n")
s.Contains(output, " 3\tmiss\thit\t\thit\thit\thit\n")
s.Contains(output, " 4\tmiss\thit\t\thit\thit\thit\n")
s.Contains(output, "* 5\thit\thit\t\thit\thit\thit\n")
s.Contains(output, " 6\tmiss\thit\t\thit\thit\thit\n")
s.Contains(output, " 7\tmiss\thit\t\thit\thit\thit\n")
s.Contains(output, " 8\tmiss\thit\t\thit\thit\tmiss\n")
s.Contains(output, " 9\tmiss\thit\t\thit\thit\thit\n")
s.Contains(output, "The policy 'one_factor' from rule #5 will be applied to this request.")
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "access-control", "check-policy", "--url=https://user.example.com/resources/test", "--method=HEAD", "--username=john", "--groups=admin,test", "--ip=192.168.1.3", "--verbose", "--config", "/config/configuration.yml"})
s.Assert().NoError(err)
// This is an example of `access-control check-policy --config .\internal\suites\CLI\configuration.yml --url=https://user.example.com --method=HEAD --username=john --groups=admin,test --ip=192.168.1.3 --verbose`.
s.Contains(output, "Performing policy check for request to 'https://user.example.com/resources/test' method 'HEAD' username 'john' groups 'admin,test' from IP '192.168.1.3'.\n\n")
s.Contains(output, " #\tDomain\tResource\tMethod\tNetwork\tSubject\n")
s.Contains(output, " 1\tmiss\thit\t\thit\thit\thit\n")
s.Contains(output, " 2\tmiss\thit\t\thit\thit\thit\n")
s.Contains(output, " 3\tmiss\thit\t\thit\thit\thit\n")
s.Contains(output, " 4\tmiss\thit\t\thit\thit\thit\n")
s.Contains(output, " 5\tmiss\thit\t\thit\thit\thit\n")
s.Contains(output, " 6\tmiss\thit\t\tmiss\thit\thit\n")
s.Contains(output, " 7\tmiss\thit\t\thit\thit\thit\n")
s.Contains(output, " 8\tmiss\thit\t\thit\thit\tmiss\n")
s.Contains(output, "* 9\thit\thit\t\thit\thit\thit\n")
s.Contains(output, "The policy 'one_factor' from rule #9 will be applied to this request.")
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "access-control", "check-policy", "--url=https://user.example.com", "--method=HEAD", "--ip=192.168.1.3", "--verbose", "--config", "/config/configuration.yml"})
s.Assert().NoError(err)
// This is an example of `authelia access-control check-policy --config .\internal\suites\CLI\configuration.yml --url=https://user.example.com --method=HEAD --ip=192.168.1.3 --verbose`.
s.Contains(output, "Performing policy check for request to 'https://user.example.com' method 'HEAD' from IP '192.168.1.3'.\n\n")
s.Contains(output, " #\tDomain\tResource\tMethod\tNetwork\tSubject\n")
s.Contains(output, " 1\tmiss\thit\t\thit\thit\thit\n")
s.Contains(output, " 2\tmiss\thit\t\thit\thit\thit\n")
s.Contains(output, " 3\tmiss\thit\t\thit\thit\thit\n")
s.Contains(output, " 4\tmiss\thit\t\thit\thit\thit\n")
s.Contains(output, " 5\tmiss\tmiss\t\thit\thit\thit\n")
s.Contains(output, " 6\tmiss\thit\t\tmiss\thit\thit\n")
s.Contains(output, " 7\tmiss\thit\t\thit\thit\thit\n")
s.Contains(output, " 8\tmiss\thit\t\thit\thit\tmay\n")
s.Contains(output, "~ 9\thit\thit\t\thit\thit\tmay\n")
s.Contains(output, "The policy 'one_factor' from rule #9 will potentially be applied to this request. Otherwise the policy 'bypass' from the default policy will be.")
}
func TestCLISuite(t *testing.T) { func TestCLISuite(t *testing.T) {
if testing.Short() { if testing.Short() {
t.Skip("skipping suite test in short mode") t.Skip("skipping suite test in short mode")

View File

@ -68,12 +68,12 @@ func TestShouldReturnZeroAndErrorOnInvalidTLSVersions(t *testing.T) {
version, err := TLSStringToTLSConfigVersion("TLS1.4") version, err := TLSStringToTLSConfigVersion("TLS1.4")
assert.Error(t, err) assert.Error(t, err)
assert.Equal(t, uint16(0), version) assert.Equal(t, uint16(0), version)
assert.EqualError(t, err, "supplied TLS version isn't supported") assert.EqualError(t, err, "supplied tls version isn't supported")
version, err = TLSStringToTLSConfigVersion("SSL3.0") version, err = TLSStringToTLSConfigVersion("SSL3.0")
assert.Error(t, err) assert.Error(t, err)
assert.Equal(t, uint16(0), version) assert.Equal(t, uint16(0), version)
assert.EqualError(t, err, "supplied TLS version isn't supported") assert.EqualError(t, err, "supplied tls version isn't supported")
} }
func TestShouldReturnErrWhenX509DirectoryNotExist(t *testing.T) { func TestShouldReturnErrWhenX509DirectoryNotExist(t *testing.T) {

View File

@ -73,4 +73,4 @@ var htmlEscaper = strings.NewReplacer(
var ErrTimeoutReached = errors.New("timeout reached") var ErrTimeoutReached = errors.New("timeout reached")
// ErrTLSVersionNotSupported returned when an unknown TLS version supplied. // ErrTLSVersionNotSupported returned when an unknown TLS version supplied.
var ErrTLSVersionNotSupported = errors.New("supplied TLS version isn't supported") var ErrTLSVersionNotSupported = errors.New("supplied tls version isn't supported")

View File

@ -38,13 +38,13 @@ func ParseDurationString(input string) (time.Duration, error) {
case input == "0" || len(matches) == 3: case input == "0" || len(matches) == 3:
seconds, err := strconv.Atoi(input) seconds, err := strconv.Atoi(input)
if err != nil { if err != nil {
return 0, fmt.Errorf("could not convert the input string of %s into a duration: %s", input, err) return 0, fmt.Errorf("could not parse '%s' as a duration: %w", input, err)
} }
duration = time.Duration(seconds) * time.Second duration = time.Duration(seconds) * time.Second
case input != "": case input != "":
// Throw this error if input is anything other than a blank string, blank string will default to a duration of nothing. // Throw this error if input is anything other than a blank string, blank string will default to a duration of nothing.
return 0, fmt.Errorf("could not convert the input string of %s into a duration", input) return 0, fmt.Errorf("could not parse '%s' as a duration", input)
} }
return duration, nil return duration, nil

View File

@ -47,25 +47,25 @@ func TestShouldParseSecondsString(t *testing.T) {
func TestShouldNotParseDurationStringWithOutOfOrderQuantitiesAndUnits(t *testing.T) { func TestShouldNotParseDurationStringWithOutOfOrderQuantitiesAndUnits(t *testing.T) {
duration, err := ParseDurationString("h1") duration, err := ParseDurationString("h1")
assert.EqualError(t, err, "could not convert the input string of h1 into a duration") assert.EqualError(t, err, "could not parse 'h1' as a duration")
assert.Equal(t, time.Duration(0), duration) assert.Equal(t, time.Duration(0), duration)
} }
func TestShouldNotParseBadDurationString(t *testing.T) { func TestShouldNotParseBadDurationString(t *testing.T) {
duration, err := ParseDurationString("10x") duration, err := ParseDurationString("10x")
assert.EqualError(t, err, "could not convert the input string of 10x into a duration") assert.EqualError(t, err, "could not parse '10x' as a duration")
assert.Equal(t, time.Duration(0), duration) assert.Equal(t, time.Duration(0), duration)
} }
func TestShouldNotParseDurationStringWithMultiValueUnits(t *testing.T) { func TestShouldNotParseDurationStringWithMultiValueUnits(t *testing.T) {
duration, err := ParseDurationString("10ms") duration, err := ParseDurationString("10ms")
assert.EqualError(t, err, "could not convert the input string of 10ms into a duration") assert.EqualError(t, err, "could not parse '10ms' as a duration")
assert.Equal(t, time.Duration(0), duration) assert.Equal(t, time.Duration(0), duration)
} }
func TestShouldNotParseDurationStringWithLeadingZero(t *testing.T) { func TestShouldNotParseDurationStringWithLeadingZero(t *testing.T) {
duration, err := ParseDurationString("005h") duration, err := ParseDurationString("005h")
assert.EqualError(t, err, "could not convert the input string of 005h into a duration") assert.EqualError(t, err, "could not parse '005h' as a duration")
assert.Equal(t, time.Duration(0), duration) assert.Equal(t, time.Duration(0), duration)
} }