From 3c81e75d7904866029ad68e325ab19e6593d4826 Mon Sep 17 00:00:00 2001 From: James Elliott Date: Mon, 28 Feb 2022 14:15:01 +1100 Subject: [PATCH] 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. --- docs/configuration/index.md | 2 +- internal/authorization/access_control_rule.go | 15 +- internal/authorization/authorizer.go | 25 ++ internal/authorization/authorizer_test.go | 70 ++- internal/authorization/types.go | 24 ++ internal/authorization/util.go | 16 + internal/commands/acl.go | 242 +++++++++++ internal/commands/configuration.go | 60 ++- internal/commands/const.go | 17 + internal/commands/root.go | 3 +- internal/commands/storage.go | 2 +- internal/commands/validate.go | 78 ++-- internal/configuration/provider_test.go | 2 +- .../configuration/validator/access_control.go | 64 +-- .../validator/access_control_test.go | 92 ++-- .../configuration/validator/authentication.go | 271 ++++++------ .../validator/authentication_test.go | 398 +++++++++--------- .../configuration/validator/configuration.go | 65 ++- .../validator/configuration_test.go | 24 +- internal/configuration/validator/const.go | 220 +++++++--- .../validator/identity_providers.go | 68 +-- .../validator/identity_providers_test.go | 17 +- internal/configuration/validator/log.go | 24 ++ .../{logging_test.go => log_test.go} | 6 +- internal/configuration/validator/logging.go | 24 -- internal/configuration/validator/notifier.go | 44 +- .../configuration/validator/notifier_test.go | 64 +-- internal/configuration/validator/ntp.go | 28 +- internal/configuration/validator/ntp_test.go | 35 +- .../configuration/validator/regulation.go | 26 +- .../validator/regulation_test.go | 25 +- internal/configuration/validator/server.go | 47 ++- .../configuration/validator/server_test.go | 8 +- internal/configuration/validator/session.go | 127 +++--- .../configuration/validator/session_test.go | 34 +- internal/configuration/validator/storage.go | 56 +-- .../configuration/validator/storage_test.go | 104 ++--- internal/configuration/validator/theme.go | 15 +- .../configuration/validator/theme_test.go | 14 +- internal/configuration/validator/totp.go | 40 +- internal/configuration/validator/totp_test.go | 2 +- internal/ntp/ntp_test.go | 19 +- internal/suites/CLI/configuration.yml | 15 + internal/suites/suite_cli_test.go | 102 ++++- internal/utils/certificates_test.go | 4 +- internal/utils/const.go | 2 +- internal/utils/time.go | 4 +- internal/utils/time_test.go | 8 +- 48 files changed, 1657 insertions(+), 995 deletions(-) create mode 100644 internal/commands/acl.go create mode 100644 internal/configuration/validator/log.go rename internal/configuration/validator/{logging_test.go => log_test.go} (79%) delete mode 100644 internal/configuration/validator/logging.go diff --git a/docs/configuration/index.md b/docs/configuration/index.md index 7e346c438..1fb0d7138 100644 --- a/docs/configuration/index.md +++ b/docs/configuration/index.md @@ -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. ```console -$ authelia validate-config configuration.yml +$ authelia validate-config --config configuration.yml ``` # Duration Notation Format diff --git a/internal/authorization/access_control_rule.go b/internal/authorization/access_control_rule.go index 81c8f0326..37c984661 100644 --- a/internal/authorization/access_control_rule.go +++ b/internal/authorization/access_control_rule.go @@ -124,12 +124,23 @@ func isMatchForNetworks(subject Subject, acl *AccessControlRule) (match bool) { 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) { - // If there are no subjects in this rule then the subject condition is a match. - if len(acl.Subjects) == 0 || subject.IsAnonymous() { + if subject.IsAnonymous() { 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). for _, subjectRule := range acl.Subjects { if subjectRule.IsMatch(subject) { diff --git a/internal/authorization/authorizer.go b/internal/authorization/authorizer.go index 4ee601503..51bb49114 100644 --- a/internal/authorization/authorizer.go +++ b/internal/authorization/authorizer.go @@ -66,3 +66,28 @@ func (p Authorizer) GetRequiredLevel(subject Subject, object Object) Level { 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 +} diff --git a/internal/authorization/authorizer_test.go b/internal/authorization/authorizer_test.go index c8858e542..805025c4b 100644 --- a/internal/authorization/authorizer_test.go +++ b/internal/authorization/authorizer_test.go @@ -31,20 +31,23 @@ func NewAuthorizerTester(config schema.AccessControlConfiguration) *AuthorizerTe } func (s *AuthorizerTester) CheckAuthorizations(t *testing.T, subject Subject, requestURI, method string, expectedLevel Level) { - url, _ := url.ParseRequestURI(requestURI) + targetURL, _ := url.ParseRequestURI(requestURI) - object := Object{ - Scheme: url.Scheme, - Domain: url.Hostname(), - Path: url.Path, - Method: method, - } + object := NewObject(targetURL, method) level := s.GetRequiredLevel(subject, object) 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 { 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(), Bob, "https://private.example.com", "GET", Denied) 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() { diff --git a/internal/authorization/types.go b/internal/authorization/types.go index 880254eb1..7dcc3a131 100644 --- a/internal/authorization/types.go +++ b/internal/authorization/types.go @@ -58,3 +58,27 @@ func NewObject(targetURL *url.URL, method string) (object 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 +} diff --git a/internal/authorization/util.go b/internal/authorization/util.go index 64aac911b..ded829ab6 100644 --- a/internal/authorization/util.go +++ b/internal/authorization/util.go @@ -25,6 +25,22 @@ func PolicyToLevel(policy string) Level { 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) { if strings.HasPrefix(subjectRule, userPrefix) { user := strings.Trim(subjectRule[len(userPrefix):], " ") diff --git a/internal/commands/acl.go b/internal/commands/acl.go new file mode 100644 index 000000000..e0bc6c95a --- /dev/null +++ b/internal/commands/acl.go @@ -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 +} diff --git a/internal/commands/configuration.go b/internal/commands/configuration.go index 3f3a28125..fb65fea69 100644 --- a/internal/commands/configuration.go +++ b/internal/commands/configuration.go @@ -3,6 +3,7 @@ package commands import ( "os" + "github.com/sirupsen/logrus" "github.com/spf13/cobra" "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. -func cmdWithConfigFlags(cmd *cobra.Command) { - cmd.Flags().StringSliceP("config", "c", []string{}, "Configuration files") +func cmdWithConfigFlags(cmd *cobra.Command, persistent bool, configs []string) { + 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 func newCmdWithConfigPreRun(ensureConfigExists, validateKeys, validateConfiguration bool) func(cmd *cobra.Command, args []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") - if err != nil { + logger = logging.Logger() + + if configs, err = cmd.Flags().GetStringSlice("config"); err != nil { 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() - - keys, config, err = configuration.Load(val, configuration.NewDefaultSources(configs, configuration.DefaultEnvPrefix, configuration.DefaultEnvDelimiter)...) + config, val, err = loadConfig(configs, validateKeys, validateConfiguration) if err != nil { logger.Fatalf("Error occurred loading configuration: %v", err) } - if validateKeys { - validator.ValidateKeys(keys, configuration.DefaultEnvPrefix, val) - } - - if validateConfiguration { - validator.ValidateConfiguration(config, val) - } - warnings := val.Warnings() if len(warnings) != 0 { for _, warning := range warnings { @@ -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 +} diff --git a/internal/commands/const.go b/internal/commands/const.go index 5a5317e69..40edb6a8f 100644 --- a/internal/commands/const.go +++ b/internal/commands/const.go @@ -80,6 +80,23 @@ PowerShell: # 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 ( storageMigrateDirectionUp = "up" storageMigrateDirectionDown = "down" diff --git a/internal/commands/root.go b/internal/commands/root.go index 58951f8d3..37c9cc3ce 100644 --- a/internal/commands/root.go +++ b/internal/commands/root.go @@ -31,7 +31,7 @@ func NewRootCmd() (cmd *cobra.Command) { Run: cmdRootRun, } - cmdWithConfigFlags(cmd) + cmdWithConfigFlags(cmd, false, []string{}) cmd.AddCommand( newBuildInfoCmd(), @@ -41,6 +41,7 @@ func NewRootCmd() (cmd *cobra.Command) { NewRSACmd(), NewStorageCmd(), newValidateConfigCmd(), + newAccessControlCommand(), ) return cmd diff --git a/internal/commands/storage.go b/internal/commands/storage.go index 1ac7c1317..02c6e22fa 100644 --- a/internal/commands/storage.go +++ b/internal/commands/storage.go @@ -13,7 +13,7 @@ func NewStorageCmd() (cmd *cobra.Command) { 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") diff --git a/internal/commands/validate.go b/internal/commands/validate.go index 4c8a73c3a..642cbdce5 100644 --- a/internal/commands/validate.go +++ b/internal/commands/validate.go @@ -1,66 +1,70 @@ package commands import ( - "log" - "os" + "fmt" "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/validator" - "github.com/authelia/authelia/v4/internal/logging" ) func newValidateConfigCmd() (cmd *cobra.Command) { cmd = &cobra.Command{ - Use: "validate-config [yaml]", + Use: "validate-config", Short: "Check a configuration against the internal configuration validation mechanisms", - Args: cobra.MinimumNArgs(1), - Run: cmdValidateConfigRun, + Args: cobra.NoArgs, + RunE: cmdValidateConfigRunE, } + cmdWithConfigFlags(cmd, false, []string{"config.yml"}) + return cmd } -func cmdValidateConfigRun(_ *cobra.Command, args []string) { - logger := logging.Logger() +func cmdValidateConfigRunE(cmd *cobra.Command, _ []string) (err error) { + var ( + configs []string + val *schema.StructValidator + ) - configPath := args[0] - if _, err := os.Stat(configPath); err != nil { - logger.Fatalf("Error Loading Configuration: %v\n", err) + if configs, err = cmd.Flags().GetStringSlice("config"); err != nil { + return err } - val := schema.NewStructValidator() - - keys, conf, err := configuration.Load(val, configuration.NewYAMLFileSource(configPath)) + config, val, err = loadConfig(configs, true, true) 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) - validator.ValidateConfiguration(conf, val) + switch { + case val.HasErrors(): + fmt.Println("Configuration parsed and loaded with errors:") + fmt.Println("") - warnings := val.Warnings() - errors := val.Errors() - - if len(warnings) != 0 { - logger.Warn("Warnings occurred while loading the configuration:") - - for _, warn := range warnings { - logger.Warnf(" %+v", warn) - } - } - - if len(errors) != 0 { - logger.Error("Errors occurred while loading the configuration:") - - for _, err := range errors { - logger.Errorf(" %+v", err) + for _, err = range val.Errors() { + fmt.Printf("\t - %v\n", err) } - logger.Fatal("Can't continue due to errors") + fmt.Println("") + + if !val.HasWarnings() { + break + } + + 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) + } + + fmt.Println("") + default: + fmt.Println("Configuration parsed and loaded successfully without errors.") + fmt.Println("") } - log.Println("Configuration parsed successfully without errors.") + return nil } diff --git a/internal/configuration/provider_test.go b/internal/configuration/provider_test.go index 961b15d41..84690aa08 100644 --- a/internal/configuration/provider_test.go +++ b/internal/configuration/provider_test.go @@ -260,7 +260,7 @@ func TestShouldHandleErrInvalidatorWhenSMTPSenderBlank(t *testing.T) { require.Len(t, val.Errors(), 1) 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) { diff --git a/internal/configuration/validator/access_control.go b/internal/configuration/validator/access_control.go index 19d271f50..1b70be0df 100644 --- a/internal/configuration/validator/access_control.go +++ b/internal/configuration/validator/access_control.go @@ -12,7 +12,7 @@ import ( // IsPolicyValid check if policy is valid. 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. @@ -27,8 +27,8 @@ func IsSubjectValid(subject string) (isValid bool) { } // IsNetworkGroupValid check if a network group is valid. -func IsNetworkGroupValid(configuration schema.AccessControlConfiguration, network string) bool { - for _, networks := range configuration.Networks { +func IsNetworkGroupValid(config schema.AccessControlConfiguration, network string) bool { + for _, networks := range config.Networks { if network != networks.Name { continue } else { @@ -49,21 +49,29 @@ func IsNetworkValid(network string) (isValid bool) { 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. -func ValidateAccessControl(configuration *schema.AccessControlConfiguration, validator *schema.StructValidator) { - if configuration.DefaultPolicy == "" { - configuration.DefaultPolicy = policyDeny +func ValidateAccessControl(config *schema.Configuration, validator *schema.StructValidator) { + if config.AccessControl.DefaultPolicy == "" { + config.AccessControl.DefaultPolicy = policyDeny } - if !IsPolicyValid(configuration.DefaultPolicy) { - validator.Push(fmt.Errorf("'default_policy' must either be 'deny', 'two_factor', 'one_factor' or 'bypass'")) + if !IsPolicyValid(config.AccessControl.DefaultPolicy) { + validator.Push(fmt.Errorf(errFmtAccessControlDefaultPolicyValue, strings.Join(validACLRulePolicies, "', '"), config.AccessControl.DefaultPolicy)) } - if configuration.Networks != nil { - for _, n := range configuration.Networks { + if config.AccessControl.Networks != nil { + for _, n := range config.AccessControl.Networks { for _, networks := range n.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. -func ValidateRules(configuration schema.AccessControlConfiguration, validator *schema.StructValidator) { - if configuration.Rules == nil || len(configuration.Rules) == 0 { - if configuration.DefaultPolicy != policyOneFactor && configuration.DefaultPolicy != policyTwoFactor { - validator.Push(fmt.Errorf("Default Policy [%s] is invalid, access control rules must be provided or a policy must either be 'one_factor' or 'two_factor'", configuration.DefaultPolicy)) +func ValidateRules(config *schema.Configuration, validator *schema.StructValidator) { + if config.AccessControl.Rules == nil || len(config.AccessControl.Rules) == 0 { + if config.AccessControl.DefaultPolicy != policyOneFactor && config.AccessControl.DefaultPolicy != policyTwoFactor { + validator.Push(fmt.Errorf(errFmtAccessControlDefaultPolicyWithoutRules, config.AccessControl.DefaultPolicy)) 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 } - for i, rule := range configuration.Rules { + for i, rule := range config.AccessControl.Rules { rulePosition := i + 1 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) { - 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) @@ -104,16 +112,16 @@ func ValidateRules(configuration schema.AccessControlConfiguration, validator *s validateMethods(rulePosition, rule, validator) 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 { if !IsNetworkValid(network) { - if !IsNetworkGroupValid(configuration, 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)) + if !IsNetworkGroupValid(config, network) { + 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) { for _, resource := range rule.Resources { 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 _, subject := range subjectRule { 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) { for _, method := range rule.Methods { - if !utils.IsStringInSliceFold(method, validHTTPRequestMethods) { - 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, ", "))) + if !utils.IsStringInSliceFold(method, validACLRuleMethods) { + validator.Push(fmt.Errorf(errFmtAccessControlRuleMethodInvalid, ruleDescriptor(rulePosition, rule), method, strings.Join(validACLRuleMethods, "', '"))) } } } diff --git a/internal/configuration/validator/access_control_test.go b/internal/configuration/validator/access_control_test.go index 9a72ffe7b..05c95e4e9 100644 --- a/internal/configuration/validator/access_control_test.go +++ b/internal/configuration/validator/access_control_test.go @@ -12,107 +12,117 @@ import ( type AccessControl struct { suite.Suite - configuration schema.AccessControlConfiguration - validator *schema.StructValidator + config *schema.Configuration + validator *schema.StructValidator } func (suite *AccessControl) SetupTest() { suite.validator = schema.NewStructValidator() - suite.configuration.DefaultPolicy = policyDeny - suite.configuration.Networks = schema.DefaultACLNetwork - suite.configuration.Rules = schema.DefaultACLRule + suite.config = &schema.Configuration{ + AccessControl: schema.AccessControlConfiguration{ + DefaultPolicy: policyDeny, + + Networks: schema.DefaultACLNetwork, + Rules: schema.DefaultACLRule, + }, + } } 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.HasErrors()) } 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.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() { - suite.configuration.Networks = []schema.ACLNetwork{ + suite.config.AccessControl.Networks = []schema.ACLNetwork{ { Name: "internal", Networks: []string{"abc.def.ghi.jkl"}, }, } - ValidateAccessControl(&suite.configuration, suite.validator) + ValidateAccessControl(suite.config, suite.validator) suite.Assert().False(suite.validator.HasWarnings()) 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() { - 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.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() { - 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.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() { - 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.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()[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()[2], "Rule #2 is invalid, a policy must have one or more domains") - 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()[0], "access control: rule #1: rule is invalid: must have the option 'domain' configured") + 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], "access control: rule #2: rule is invalid: must have the option 'domain' configured") + 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() { - suite.configuration.Rules = []schema.ACLRule{ + suite.config.AccessControl.Rules = []schema.ACLRule{ { Domains: []string{"public.example.com"}, Policy: testInvalidPolicy, }, } - ValidateRules(suite.configuration, suite.validator) + ValidateRules(suite.config, suite.validator) suite.Assert().False(suite.validator.HasWarnings()) 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() { - suite.configuration.Rules = []schema.ACLRule{ + suite.config.AccessControl.Rules = []schema.ACLRule{ { Domains: []string{"public.example.com"}, 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.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() { - suite.configuration.Rules = []schema.ACLRule{ + suite.config.AccessControl.Rules = []schema.ACLRule{ { Domains: []string{"public.example.com"}, 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.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() { - suite.configuration.Rules = []schema.ACLRule{ + suite.config.AccessControl.Rules = []schema.ACLRule{ { Domains: []string{"public.example.com"}, 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.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() { domains := []string{"public.example.com"} subjects := [][]string{{"invalid"}} - suite.configuration.Rules = []schema.ACLRule{ + suite.config.AccessControl.Rules = []schema.ACLRule{ { Domains: domains, 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.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()[1], fmt.Sprintf(errAccessControlInvalidPolicyWithSubjects, 1, domains, subjects)) + 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(errAccessControlRuleBypassPolicyInvalidWithSubjects, ruleDescriptor(1, suite.config.AccessControl.Rules[0]))) } func TestAccessControl(t *testing.T) { diff --git a/internal/configuration/validator/authentication.go b/internal/configuration/validator/authentication.go index e7547f32a..b0aae8413 100644 --- a/internal/configuration/validator/authentication.go +++ b/internal/configuration/validator/authentication.go @@ -1,7 +1,6 @@ package validator import ( - "errors" "fmt" "net/url" "strings" @@ -11,260 +10,254 @@ import ( ) // ValidateAuthenticationBackend validates and updates the authentication backend configuration. -func ValidateAuthenticationBackend(configuration *schema.AuthenticationBackendConfiguration, validator *schema.StructValidator) { - if configuration.LDAP == nil && configuration.File == nil { - validator.Push(errors.New("Please provide `ldap` or `file` object in `authentication_backend`")) +func ValidateAuthenticationBackend(config *schema.AuthenticationBackendConfiguration, validator *schema.StructValidator) { + if config.LDAP == nil && config.File == nil { + validator.Push(fmt.Errorf(errFmtAuthBackendNotConfigured)) } - if configuration.LDAP != nil && configuration.File != nil { - validator.Push(errors.New("You cannot provide both `ldap` and `file` objects in `authentication_backend`")) + if config.LDAP != nil && config.File != nil { + validator.Push(fmt.Errorf(errFmtAuthBackendMultipleConfigured)) } - if configuration.File != nil { - validateFileAuthenticationBackend(configuration.File, validator) - } else if configuration.LDAP != nil { - validateLDAPAuthenticationBackend(configuration.LDAP, validator) + if config.File != nil { + validateFileAuthenticationBackend(config.File, validator) + } else if config.LDAP != nil { + validateLDAPAuthenticationBackend(config.LDAP, validator) } - if configuration.RefreshInterval == "" { - configuration.RefreshInterval = schema.RefreshIntervalDefault + if config.RefreshInterval == "" { + config.RefreshInterval = schema.RefreshIntervalDefault } else { - _, err := utils.ParseDurationString(configuration.RefreshInterval) - if err != nil && configuration.RefreshInterval != schema.ProfileRefreshDisabled && configuration.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)) + _, err := utils.ParseDurationString(config.RefreshInterval) + if err != nil && config.RefreshInterval != schema.ProfileRefreshDisabled && config.RefreshInterval != schema.ProfileRefreshAlways { + validator.Push(fmt.Errorf(errFmtAuthBackendRefreshInterval, config.RefreshInterval, err)) } } } // validateFileAuthenticationBackend validates and updates the file authentication backend configuration. -func validateFileAuthenticationBackend(configuration *schema.FileAuthenticationBackendConfiguration, validator *schema.StructValidator) { - if configuration.Path == "" { - validator.Push(errors.New("Please provide a `path` for the users database in `authentication_backend`")) +func validateFileAuthenticationBackend(config *schema.FileAuthenticationBackendConfiguration, validator *schema.StructValidator) { + if config.Path == "" { + validator.Push(fmt.Errorf(errFmtFileAuthBackendPathNotConfigured)) } - if configuration.Password == nil { - configuration.Password = &schema.DefaultPasswordConfiguration + if config.Password == nil { + config.Password = &schema.DefaultPasswordConfiguration } else { // Salt Length. switch { - case configuration.Password.SaltLength == 0: - configuration.Password.SaltLength = schema.DefaultPasswordConfiguration.SaltLength - case configuration.Password.SaltLength < 8: - validator.Push(fmt.Errorf("The salt length must be 2 or more, you configured %d", configuration.Password.SaltLength)) + case config.Password.SaltLength == 0: + config.Password.SaltLength = schema.DefaultPasswordConfiguration.SaltLength + case config.Password.SaltLength < 8: + validator.Push(fmt.Errorf(errFmtFileAuthBackendPasswordSaltLength, config.Password.SaltLength)) } - switch configuration.Password.Algorithm { + switch config.Password.Algorithm { case "": - configuration.Password.Algorithm = schema.DefaultPasswordConfiguration.Algorithm + config.Password.Algorithm = schema.DefaultPasswordConfiguration.Algorithm fallthrough case hashArgon2id: - validateFileAuthenticationBackendArgon2id(configuration, validator) + validateFileAuthenticationBackendArgon2id(config, validator) case hashSHA512: - validateFileAuthenticationBackendSHA512(configuration) + validateFileAuthenticationBackendSHA512(config) 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 { - validator.Push(fmt.Errorf("The number of iterations specified is invalid, must be 1 or more, you configured %d", configuration.Password.Iterations)) + if config.Password.Iterations < 1 { + validator.Push(fmt.Errorf(errFmtFileAuthBackendPasswordInvalidIterations, config.Password.Iterations)) } } } -func validateFileAuthenticationBackendSHA512(configuration *schema.FileAuthenticationBackendConfiguration) { +func validateFileAuthenticationBackendSHA512(config *schema.FileAuthenticationBackendConfiguration) { // Iterations (time). - if configuration.Password.Iterations == 0 { - configuration.Password.Iterations = schema.DefaultPasswordSHA512Configuration.Iterations + if config.Password.Iterations == 0 { + config.Password.Iterations = schema.DefaultPasswordSHA512Configuration.Iterations } } -func validateFileAuthenticationBackendArgon2id(configuration *schema.FileAuthenticationBackendConfiguration, validator *schema.StructValidator) { +func validateFileAuthenticationBackendArgon2id(config *schema.FileAuthenticationBackendConfiguration, validator *schema.StructValidator) { // Iterations (time). - if configuration.Password.Iterations == 0 { - configuration.Password.Iterations = schema.DefaultPasswordConfiguration.Iterations + if config.Password.Iterations == 0 { + config.Password.Iterations = schema.DefaultPasswordConfiguration.Iterations } // Parallelism. - if configuration.Password.Parallelism == 0 { - configuration.Password.Parallelism = schema.DefaultPasswordConfiguration.Parallelism - } else if configuration.Password.Parallelism < 1 { - validator.Push(fmt.Errorf("Parallelism for argon2id must be 1 or more, you configured %d", configuration.Password.Parallelism)) + if config.Password.Parallelism == 0 { + config.Password.Parallelism = schema.DefaultPasswordConfiguration.Parallelism + } else if config.Password.Parallelism < 1 { + validator.Push(fmt.Errorf(errFmtFileAuthBackendPasswordArgon2idInvalidParallelism, config.Password.Parallelism)) } // Memory. - if configuration.Password.Memory == 0 { - configuration.Password.Memory = schema.DefaultPasswordConfiguration.Memory - } else if configuration.Password.Memory < configuration.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)) + if config.Password.Memory == 0 { + config.Password.Memory = schema.DefaultPasswordConfiguration.Memory + } else if config.Password.Memory < config.Password.Parallelism*8 { + validator.Push(fmt.Errorf(errFmtFileAuthBackendPasswordArgon2idInvalidMemory, config.Password.Parallelism, config.Password.Parallelism*8, config.Password.Memory)) } // Key Length. - if configuration.Password.KeyLength == 0 { - configuration.Password.KeyLength = schema.DefaultPasswordConfiguration.KeyLength - } else if configuration.Password.KeyLength < 16 { - validator.Push(fmt.Errorf("Key length for argon2id must be 16, you configured %d", configuration.Password.KeyLength)) + if config.Password.KeyLength == 0 { + config.Password.KeyLength = schema.DefaultPasswordConfiguration.KeyLength + } else if config.Password.KeyLength < 16 { + validator.Push(fmt.Errorf(errFmtFileAuthBackendPasswordArgon2idInvalidKeyLength, config.Password.KeyLength)) } } -func validateLDAPAuthenticationBackend(configuration *schema.LDAPAuthenticationBackendConfiguration, validator *schema.StructValidator) { - if configuration.Timeout == 0 { - configuration.Timeout = schema.DefaultLDAPAuthenticationBackendConfiguration.Timeout +func validateLDAPAuthenticationBackend(config *schema.LDAPAuthenticationBackendConfiguration, validator *schema.StructValidator) { + if config.Timeout == 0 { + config.Timeout = schema.DefaultLDAPAuthenticationBackendConfiguration.Timeout } - if configuration.Implementation == "" { - configuration.Implementation = schema.DefaultLDAPAuthenticationBackendConfiguration.Implementation + if config.Implementation == "" { + config.Implementation = schema.DefaultLDAPAuthenticationBackendConfiguration.Implementation } - if configuration.TLS == nil { - configuration.TLS = schema.DefaultLDAPAuthenticationBackendConfiguration.TLS + if config.TLS == nil { + config.TLS = schema.DefaultLDAPAuthenticationBackendConfiguration.TLS } - if configuration.TLS.MinimumVersion == "" { - configuration.TLS.MinimumVersion = schema.DefaultLDAPAuthenticationBackendConfiguration.TLS.MinimumVersion + if config.TLS.MinimumVersion == "" { + config.TLS.MinimumVersion = schema.DefaultLDAPAuthenticationBackendConfiguration.TLS.MinimumVersion } - if _, err := utils.TLSStringToTLSConfigVersion(configuration.TLS.MinimumVersion); err != nil { - validator.Push(fmt.Errorf("error occurred validating the LDAP minimum_tls_version key with value %s: %v", configuration.TLS.MinimumVersion, err)) + if _, err := utils.TLSStringToTLSConfigVersion(config.TLS.MinimumVersion); err != nil { + validator.Push(fmt.Errorf(errFmtLDAPAuthBackendTLSMinVersion, config.TLS.MinimumVersion, err)) } - switch configuration.Implementation { + switch config.Implementation { case schema.LDAPImplementationCustom: - setDefaultImplementationCustomLDAPAuthenticationBackend(configuration) + setDefaultImplementationCustomLDAPAuthenticationBackend(config) case schema.LDAPImplementationActiveDirectory: - setDefaultImplementationActiveDirectoryLDAPAuthenticationBackend(configuration) + setDefaultImplementationActiveDirectoryLDAPAuthenticationBackend(config) 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}") { - validator.Push(fmt.Errorf("authentication backend ldap users filter must not contain removed placeholders" + - ", {0} has been replaced with {input}")) + if strings.Contains(config.UsersFilter, "{0}") { + validator.Push(fmt.Errorf(errFmtLDAPAuthBackendFilterReplacedPlaceholders, "users_filter", "{0}", "{input}")) } - if strings.Contains(configuration.GroupsFilter, "{0}") || - strings.Contains(configuration.GroupsFilter, "{1}") { - 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 strings.Contains(config.GroupsFilter, "{0}") { + validator.Push(fmt.Errorf(errFmtLDAPAuthBackendFilterReplacedPlaceholders, "groups_filter", "{0}", "{input}")) } - if configuration.URL == "" { - validator.Push(errors.New("Please provide a URL to the LDAP server")) + if strings.Contains(config.GroupsFilter, "{1}") { + validator.Push(fmt.Errorf(errFmtLDAPAuthBackendFilterReplacedPlaceholders, "groups_filter", "{1}", "{username}")) + } + + if config.URL == "" { + validator.Push(fmt.Errorf(errFmtLDAPAuthBackendMissingOption, "url")) } else { - ldapURL, serverName := validateLDAPURL(configuration.URL, validator) - - configuration.URL = ldapURL - - if configuration.TLS.ServerName == "" { - configuration.TLS.ServerName = serverName - } + validateLDAPAuthenticationBackendURL(config, validator) } - validateLDAPRequiredParameters(configuration, validator) + validateLDAPRequiredParameters(config, validator) } -// Wrapper for test purposes to exclude the hostname from the return. -func validateLDAPURLSimple(ldapURL string, validator *schema.StructValidator) (finalURL string) { - finalURL, _ = validateLDAPURL(ldapURL, validator) +func validateLDAPAuthenticationBackendURL(config *schema.LDAPAuthenticationBackendConfiguration, validator *schema.StructValidator) { + var ( + parsedURL *url.URL + err error + ) - return finalURL -} + if parsedURL, err = url.Parse(config.URL); err != nil { + validator.Push(fmt.Errorf(errFmtLDAPAuthBackendURLNotParsable, err)) -func validateLDAPURL(ldapURL string, validator *schema.StructValidator) (finalURL string, hostname string) { - parsedURL, err := url.Parse(ldapURL) - - if err != nil { - validator.Push(errors.New("Unable to parse URL to ldap server. The scheme is probably missing: ldap:// or ldaps://")) - return "", "" + return } - if !(parsedURL.Scheme == schemeLDAP || parsedURL.Scheme == schemeLDAPS) { - validator.Push(errors.New("Unknown scheme for ldap url, should be ldap:// or ldaps://")) - return "", "" + if parsedURL.Scheme != schemeLDAP && parsedURL.Scheme != schemeLDAPS { + validator.Push(fmt.Errorf(errFmtLDAPAuthBackendURLInvalidScheme, parsedURL.Scheme)) + + return } - return parsedURL.String(), parsedURL.Hostname() + config.URL = parsedURL.String() + if config.TLS.ServerName == "" { + config.TLS.ServerName = parsedURL.Hostname() + } } -func validateLDAPRequiredParameters(configuration *schema.LDAPAuthenticationBackendConfiguration, validator *schema.StructValidator) { +func validateLDAPRequiredParameters(config *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). - if configuration.User == "" { - validator.Push(errors.New("Please provide a user name to connect to the LDAP server")) + if config.User == "" { + 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). - if configuration.Password == "" { - validator.Push(errors.New("Please provide a password to connect to the LDAP server")) + if config.Password == "" { + validator.Push(fmt.Errorf(errFmtLDAPAuthBackendMissingOption, "password")) } - if configuration.BaseDN == "" { - validator.Push(errors.New("Please provide a base DN to connect to the LDAP server")) + if config.BaseDN == "" { + validator.Push(fmt.Errorf(errFmtLDAPAuthBackendMissingOption, "base_dn")) } - if configuration.UsersFilter == "" { - validator.Push(errors.New("Please provide a users filter with `users_filter` attribute")) + if config.UsersFilter == "" { + validator.Push(fmt.Errorf(errFmtLDAPAuthBackendMissingOption, "users_filter")) } else { - if !strings.HasPrefix(configuration.UsersFilter, "(") || !strings.HasSuffix(configuration.UsersFilter, ")") { - validator.Push(errors.New("The users filter should contain enclosing parenthesis. For instance {username_attribute}={input} should be ({username_attribute}={input})")) + if !strings.HasPrefix(config.UsersFilter, "(") || !strings.HasSuffix(config.UsersFilter, ")") { + validator.Push(fmt.Errorf(errFmtLDAPAuthBackendFilterEnclosingParenthesis, "users_filter", config.UsersFilter, config.UsersFilter)) } - if !strings.Contains(configuration.UsersFilter, "{username_attribute}") { - validator.Push(errors.New("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")) + if !strings.Contains(config.UsersFilter, "{username_attribute}") { + validator.Push(fmt.Errorf(errFmtLDAPAuthBackendFilterMissingPlaceholder, "users_filter", "username_attribute")) } // 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}") { - validator.Push(errors.New("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")) + if !strings.Contains(config.UsersFilter, "{input}") { + validator.Push(fmt.Errorf(errFmtLDAPAuthBackendFilterMissingPlaceholder, "users_filter", "input")) } } - if configuration.GroupsFilter == "" { - validator.Push(errors.New("Please provide a groups filter with `groups_filter` attribute")) - } else if !strings.HasPrefix(configuration.GroupsFilter, "(") || !strings.HasSuffix(configuration.GroupsFilter, ")") { - validator.Push(errors.New("The groups filter should contain enclosing parenthesis. For instance cn={input} should be (cn={input})")) + if config.GroupsFilter == "" { + validator.Push(fmt.Errorf(errFmtLDAPAuthBackendMissingOption, "groups_filter")) + } else if !strings.HasPrefix(config.GroupsFilter, "(") || !strings.HasSuffix(config.GroupsFilter, ")") { + validator.Push(fmt.Errorf(errFmtLDAPAuthBackendFilterEnclosingParenthesis, "groups_filter", config.GroupsFilter, config.GroupsFilter)) } } -func setDefaultImplementationActiveDirectoryLDAPAuthenticationBackend(configuration *schema.LDAPAuthenticationBackendConfiguration) { - if configuration.UsersFilter == "" { - configuration.UsersFilter = schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.UsersFilter +func setDefaultImplementationActiveDirectoryLDAPAuthenticationBackend(config *schema.LDAPAuthenticationBackendConfiguration) { + if config.UsersFilter == "" { + config.UsersFilter = schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.UsersFilter } - if configuration.UsernameAttribute == "" { - configuration.UsernameAttribute = schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.UsernameAttribute + if config.UsernameAttribute == "" { + config.UsernameAttribute = schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.UsernameAttribute } - if configuration.DisplayNameAttribute == "" { - configuration.DisplayNameAttribute = schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.DisplayNameAttribute + if config.DisplayNameAttribute == "" { + config.DisplayNameAttribute = schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.DisplayNameAttribute } - if configuration.MailAttribute == "" { - configuration.MailAttribute = schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.MailAttribute + if config.MailAttribute == "" { + config.MailAttribute = schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.MailAttribute } - if configuration.GroupsFilter == "" { - configuration.GroupsFilter = schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.GroupsFilter + if config.GroupsFilter == "" { + config.GroupsFilter = schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.GroupsFilter } - if configuration.GroupNameAttribute == "" { - configuration.GroupNameAttribute = schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.GroupNameAttribute + if config.GroupNameAttribute == "" { + config.GroupNameAttribute = schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.GroupNameAttribute } } -func setDefaultImplementationCustomLDAPAuthenticationBackend(configuration *schema.LDAPAuthenticationBackendConfiguration) { - if configuration.UsernameAttribute == "" { - configuration.UsernameAttribute = schema.DefaultLDAPAuthenticationBackendConfiguration.UsernameAttribute +func setDefaultImplementationCustomLDAPAuthenticationBackend(config *schema.LDAPAuthenticationBackendConfiguration) { + if config.UsernameAttribute == "" { + config.UsernameAttribute = schema.DefaultLDAPAuthenticationBackendConfiguration.UsernameAttribute } - if configuration.GroupNameAttribute == "" { - configuration.GroupNameAttribute = schema.DefaultLDAPAuthenticationBackendConfiguration.GroupNameAttribute + if config.GroupNameAttribute == "" { + config.GroupNameAttribute = schema.DefaultLDAPAuthenticationBackendConfiguration.GroupNameAttribute } - if configuration.MailAttribute == "" { - configuration.MailAttribute = schema.DefaultLDAPAuthenticationBackendConfiguration.MailAttribute + if config.MailAttribute == "" { + config.MailAttribute = schema.DefaultLDAPAuthenticationBackendConfiguration.MailAttribute } - if configuration.DisplayNameAttribute == "" { - configuration.DisplayNameAttribute = schema.DefaultLDAPAuthenticationBackendConfiguration.DisplayNameAttribute + if config.DisplayNameAttribute == "" { + config.DisplayNameAttribute = schema.DefaultLDAPAuthenticationBackendConfiguration.DisplayNameAttribute } } diff --git a/internal/configuration/validator/authentication_test.go b/internal/configuration/validator/authentication_test.go index aadc71659..c1d1aa2a0 100644 --- a/internal/configuration/validator/authentication_test.go +++ b/internal/configuration/validator/authentication_test.go @@ -23,7 +23,7 @@ func TestShouldRaiseErrorWhenBothBackendsProvided(t *testing.T) { ValidateAuthenticationBackend(&backendConfig, validator) require.Len(t, validator.Errors(), 1) - assert.EqualError(t, validator.Errors()[0], "You cannot provide both `ldap` and `file` objects in `authentication_backend`") + 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) { @@ -33,19 +33,19 @@ func TestShouldRaiseErrorWhenNoBackendProvided(t *testing.T) { ValidateAuthenticationBackend(&backendConfig, validator) 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 { suite.Suite - configuration schema.AuthenticationBackendConfiguration - validator *schema.StructValidator + config schema.AuthenticationBackendConfiguration + validator *schema.StructValidator } func (suite *FileBasedAuthenticationBackend) SetupTest() { suite.validator = schema.NewStructValidator() - suite.configuration = schema.AuthenticationBackendConfiguration{} - suite.configuration.File = &schema.FileAuthenticationBackendConfiguration{Path: "/a/path", Password: &schema.PasswordConfiguration{ + suite.config = schema.AuthenticationBackendConfiguration{} + suite.config.File = &schema.FileAuthenticationBackendConfiguration{Path: "/a/path", Password: &schema.PasswordConfiguration{ Algorithm: schema.DefaultPasswordConfiguration.Algorithm, Iterations: schema.DefaultPasswordConfiguration.Iterations, Parallelism: schema.DefaultPasswordConfiguration.Parallelism, @@ -53,150 +53,150 @@ func (suite *FileBasedAuthenticationBackend) SetupTest() { KeyLength: schema.DefaultPasswordConfiguration.KeyLength, SaltLength: schema.DefaultPasswordConfiguration.SaltLength, }} - suite.configuration.File.Password.Algorithm = schema.DefaultPasswordConfiguration.Algorithm + suite.config.File.Password.Algorithm = schema.DefaultPasswordConfiguration.Algorithm } 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.HasErrors()) } 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.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() { - suite.configuration.File.Password.Memory = 8 - suite.configuration.File.Password.Parallelism = 2 + suite.config.File.Password.Memory = 8 + suite.config.File.Password.Parallelism = 2 - ValidateAuthenticationBackend(&suite.configuration, suite.validator) + ValidateAuthenticationBackend(&suite.config, suite.validator) suite.Assert().False(suite.validator.HasWarnings()) suite.Require().Len(suite.validator.Errors(), 1) - suite.Assert().EqualError(suite.validator.Errors()[0], "Memory for argon2id must be 16 or more (parallelism * 8), you configured memory as 8 and parallelism as 2") + 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() { - 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.configuration.File.Password.Iterations) - suite.Assert().Equal(0, suite.configuration.File.Password.SaltLength) - suite.Assert().Equal("", suite.configuration.File.Password.Algorithm) - suite.Assert().Equal(0, suite.configuration.File.Password.Memory) - suite.Assert().Equal(0, suite.configuration.File.Password.Parallelism) + suite.Assert().Equal(0, suite.config.File.Password.KeyLength) + suite.Assert().Equal(0, suite.config.File.Password.Iterations) + suite.Assert().Equal(0, suite.config.File.Password.SaltLength) + suite.Assert().Equal("", suite.config.File.Password.Algorithm) + suite.Assert().Equal(0, suite.config.File.Password.Memory) + 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().Len(suite.validator.Errors(), 0) - suite.Assert().Equal(schema.DefaultPasswordConfiguration.KeyLength, suite.configuration.File.Password.KeyLength) - suite.Assert().Equal(schema.DefaultPasswordConfiguration.Iterations, suite.configuration.File.Password.Iterations) - suite.Assert().Equal(schema.DefaultPasswordConfiguration.SaltLength, suite.configuration.File.Password.SaltLength) - suite.Assert().Equal(schema.DefaultPasswordConfiguration.Algorithm, suite.configuration.File.Password.Algorithm) - suite.Assert().Equal(schema.DefaultPasswordConfiguration.Memory, suite.configuration.File.Password.Memory) - suite.Assert().Equal(schema.DefaultPasswordConfiguration.Parallelism, suite.configuration.File.Password.Parallelism) + suite.Assert().Equal(schema.DefaultPasswordConfiguration.KeyLength, suite.config.File.Password.KeyLength) + suite.Assert().Equal(schema.DefaultPasswordConfiguration.Iterations, suite.config.File.Password.Iterations) + suite.Assert().Equal(schema.DefaultPasswordConfiguration.SaltLength, suite.config.File.Password.SaltLength) + suite.Assert().Equal(schema.DefaultPasswordConfiguration.Algorithm, suite.config.File.Password.Algorithm) + suite.Assert().Equal(schema.DefaultPasswordConfiguration.Memory, suite.config.File.Password.Memory) + suite.Assert().Equal(schema.DefaultPasswordConfiguration.Parallelism, suite.config.File.Password.Parallelism) } func (suite *FileBasedAuthenticationBackend) TestShouldSetDefaultConfigurationWhenOnlySHA512Set() { - suite.configuration.File.Password = &schema.PasswordConfiguration{} - suite.Assert().Equal("", suite.configuration.File.Password.Algorithm) - suite.configuration.File.Password.Algorithm = "sha512" + suite.config.File.Password = &schema.PasswordConfiguration{} + suite.Assert().Equal("", suite.config.File.Password.Algorithm) + suite.config.File.Password.Algorithm = "sha512" - ValidateAuthenticationBackend(&suite.configuration, suite.validator) + ValidateAuthenticationBackend(&suite.config, suite.validator) suite.Assert().False(suite.validator.HasWarnings()) suite.Assert().Len(suite.validator.Errors(), 0) - suite.Assert().Equal(schema.DefaultPasswordSHA512Configuration.KeyLength, suite.configuration.File.Password.KeyLength) - suite.Assert().Equal(schema.DefaultPasswordSHA512Configuration.Iterations, suite.configuration.File.Password.Iterations) - suite.Assert().Equal(schema.DefaultPasswordSHA512Configuration.SaltLength, suite.configuration.File.Password.SaltLength) - suite.Assert().Equal(schema.DefaultPasswordSHA512Configuration.Algorithm, suite.configuration.File.Password.Algorithm) - suite.Assert().Equal(schema.DefaultPasswordSHA512Configuration.Memory, suite.configuration.File.Password.Memory) - suite.Assert().Equal(schema.DefaultPasswordSHA512Configuration.Parallelism, suite.configuration.File.Password.Parallelism) + suite.Assert().Equal(schema.DefaultPasswordSHA512Configuration.KeyLength, suite.config.File.Password.KeyLength) + suite.Assert().Equal(schema.DefaultPasswordSHA512Configuration.Iterations, suite.config.File.Password.Iterations) + suite.Assert().Equal(schema.DefaultPasswordSHA512Configuration.SaltLength, suite.config.File.Password.SaltLength) + suite.Assert().Equal(schema.DefaultPasswordSHA512Configuration.Algorithm, suite.config.File.Password.Algorithm) + suite.Assert().Equal(schema.DefaultPasswordSHA512Configuration.Memory, suite.config.File.Password.Memory) + suite.Assert().Equal(schema.DefaultPasswordSHA512Configuration.Parallelism, suite.config.File.Password.Parallelism) } 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.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() { - 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.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() { - 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.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() { - 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.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() { - 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.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() { - suite.configuration.File.Password.Algorithm = "" - suite.configuration.File.Password.Iterations = 0 - suite.configuration.File.Password.SaltLength = 0 - suite.configuration.File.Password.Memory = 0 - suite.configuration.File.Password.Parallelism = 0 + suite.config.File.Password.Algorithm = "" + suite.config.File.Password.Iterations = 0 + suite.config.File.Password.SaltLength = 0 + suite.config.File.Password.Memory = 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.HasErrors()) - suite.Assert().Equal(schema.DefaultPasswordConfiguration.Algorithm, suite.configuration.File.Password.Algorithm) - suite.Assert().Equal(schema.DefaultPasswordConfiguration.Iterations, suite.configuration.File.Password.Iterations) - suite.Assert().Equal(schema.DefaultPasswordConfiguration.SaltLength, suite.configuration.File.Password.SaltLength) - suite.Assert().Equal(schema.DefaultPasswordConfiguration.Memory, suite.configuration.File.Password.Memory) - suite.Assert().Equal(schema.DefaultPasswordConfiguration.Parallelism, suite.configuration.File.Password.Parallelism) + suite.Assert().Equal(schema.DefaultPasswordConfiguration.Algorithm, suite.config.File.Password.Algorithm) + suite.Assert().Equal(schema.DefaultPasswordConfiguration.Iterations, suite.config.File.Password.Iterations) + suite.Assert().Equal(schema.DefaultPasswordConfiguration.SaltLength, suite.config.File.Password.SaltLength) + suite.Assert().Equal(schema.DefaultPasswordConfiguration.Memory, suite.config.File.Password.Memory) + suite.Assert().Equal(schema.DefaultPasswordConfiguration.Parallelism, suite.config.File.Password.Parallelism) } func TestFileBasedAuthenticationBackend(t *testing.T) { @@ -205,289 +205,265 @@ func TestFileBasedAuthenticationBackend(t *testing.T) { type LDAPAuthenticationBackendSuite struct { suite.Suite - configuration schema.AuthenticationBackendConfiguration - validator *schema.StructValidator + config schema.AuthenticationBackendConfiguration + validator *schema.StructValidator } func (suite *LDAPAuthenticationBackendSuite) SetupTest() { suite.validator = schema.NewStructValidator() - suite.configuration = schema.AuthenticationBackendConfiguration{} - suite.configuration.LDAP = &schema.LDAPAuthenticationBackendConfiguration{} - suite.configuration.LDAP.Implementation = schema.LDAPImplementationCustom - suite.configuration.LDAP.URL = testLDAPURL - suite.configuration.LDAP.User = testLDAPUser - suite.configuration.LDAP.Password = testLDAPPassword - suite.configuration.LDAP.BaseDN = testLDAPBaseDN - suite.configuration.LDAP.UsernameAttribute = "uid" - suite.configuration.LDAP.UsersFilter = "({username_attribute}={input})" - suite.configuration.LDAP.GroupsFilter = "(cn={input})" + suite.config = schema.AuthenticationBackendConfiguration{} + suite.config.LDAP = &schema.LDAPAuthenticationBackendConfiguration{} + suite.config.LDAP.Implementation = schema.LDAPImplementationCustom + suite.config.LDAP.URL = testLDAPURL + suite.config.LDAP.User = testLDAPUser + suite.config.LDAP.Password = testLDAPPassword + suite.config.LDAP.BaseDN = testLDAPBaseDN + suite.config.LDAP.UsernameAttribute = "uid" + suite.config.LDAP.UsersFilter = "({username_attribute}={input})" + suite.config.LDAP.GroupsFilter = "(cn={input})" } 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.HasErrors()) } func (suite *LDAPAuthenticationBackendSuite) TestShouldValidateDefaultImplementationAndUsernameAttribute() { - suite.configuration.LDAP.Implementation = "" - suite.configuration.LDAP.UsernameAttribute = "" - ValidateAuthenticationBackend(&suite.configuration, suite.validator) + suite.config.LDAP.Implementation = "" + suite.config.LDAP.UsernameAttribute = "" + 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.HasErrors()) } 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.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() { - suite.configuration.LDAP.URL = "" - ValidateAuthenticationBackend(&suite.configuration, suite.validator) + suite.config.LDAP.URL = "" + ValidateAuthenticationBackend(&suite.config, suite.validator) suite.Assert().False(suite.validator.HasWarnings()) suite.Require().Len(suite.validator.Errors(), 1) - suite.Assert().EqualError(suite.validator.Errors()[0], "Please provide a URL to the LDAP server") + suite.Assert().EqualError(suite.validator.Errors()[0], "authentication_backend: ldap: option 'url' is required") } 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.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() { - 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.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() { - 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().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() { - 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.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() { - 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.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() { - 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.HasErrors()) } 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.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() { - ValidateAuthenticationBackend(&suite.configuration, suite.validator) + ValidateAuthenticationBackend(&suite.config, suite.validator) suite.Assert().False(suite.validator.HasWarnings()) 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() { - suite.configuration.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.UsersFilter = "(&({username_attribute}={0})(objectCategory=person)(objectClass=user)(!userAccountControl:1.2.840.113556.1.4.803:=2))" + 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().True(suite.validator.HasErrors()) - suite.Require().Len(suite.validator.Errors(), 2) - suite.Assert().EqualError(suite.validator.Errors()[0], "authentication backend ldap users filter must "+ - "not contain removed placeholders, {0} has been replaced with {input}") - suite.Assert().EqualError(suite.validator.Errors()[1], "authentication backend ldap groups filter must "+ - "not contain removed placeholders, "+ - "{0} has been replaced with {input} and {1} has been replaced with {username}") + suite.Require().Len(suite.validator.Errors(), 4) + 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") + 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()[2], "authentication_backend: ldap: option 'groups_filter' has an invalid placeholder: '{1}' has been removed, please use '{username}' instead") + suite.Assert().EqualError(suite.validator.Errors()[3], "authentication_backend: ldap: option 'users_filter' must contain the placeholder '{input}' but it is required") } 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.HasErrors()) - suite.Assert().Equal("cn", suite.configuration.LDAP.GroupNameAttribute) + suite.Assert().Equal("cn", suite.config.LDAP.GroupNameAttribute) } 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.HasErrors()) - suite.Assert().Equal("mail", suite.configuration.LDAP.MailAttribute) + suite.Assert().Equal("mail", suite.config.LDAP.MailAttribute) } 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.HasErrors()) - suite.Assert().Equal("displayName", suite.configuration.LDAP.DisplayNameAttribute) + suite.Assert().Equal("displayName", suite.config.LDAP.DisplayNameAttribute) } 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.HasErrors()) - suite.Assert().Equal("5m", suite.configuration.RefreshInterval) + suite.Assert().Equal("5m", suite.config.RefreshInterval) } 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.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() { - 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.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() { - suite.configuration.LDAP.UsersFilter = "(&({mail_attribute}={input})(objectClass=person))" - ValidateAuthenticationBackend(&suite.configuration, suite.validator) + suite.config.LDAP.UsersFilter = "(&({mail_attribute}={input})(objectClass=person))" + ValidateAuthenticationBackend(&suite.config, suite.validator) suite.Assert().False(suite.validator.HasWarnings()) suite.Require().Len(suite.validator.Errors(), 1) - suite.Assert().EqualError(suite.validator.Errors()[0], "Unable to detect {username_attribute} placeholder in users_filter, your configuration is broken. Please review configuration options listed at https://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() { - 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.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") -} - -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)) + 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) 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.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() { - suite.configuration.LDAP.TLS = &schema.TLSConfig{ + suite.config.LDAP.TLS = &schema.TLSConfig{ MinimumVersion: "SSL2.0", } - ValidateAuthenticationBackend(&suite.configuration, suite.validator) + ValidateAuthenticationBackend(&suite.config, suite.validator) suite.Assert().False(suite.validator.HasWarnings()) suite.Require().Len(suite.validator.Errors(), 1) - suite.Assert().EqualError(suite.validator.Errors()[0], "error occurred validating the LDAP minimum_tls_version key with value SSL2.0: supplied TLS version isn't supported") + 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) { @@ -496,83 +472,101 @@ func TestLdapAuthenticationBackend(t *testing.T) { type ActiveDirectoryAuthenticationBackendSuite struct { suite.Suite - configuration schema.AuthenticationBackendConfiguration - validator *schema.StructValidator + config schema.AuthenticationBackendConfiguration + validator *schema.StructValidator } func (suite *ActiveDirectoryAuthenticationBackendSuite) SetupTest() { suite.validator = schema.NewStructValidator() - suite.configuration = schema.AuthenticationBackendConfiguration{} - suite.configuration.LDAP = &schema.LDAPAuthenticationBackendConfiguration{} - suite.configuration.LDAP.Implementation = schema.LDAPImplementationActiveDirectory - suite.configuration.LDAP.URL = testLDAPURL - suite.configuration.LDAP.User = testLDAPUser - suite.configuration.LDAP.Password = testLDAPPassword - suite.configuration.LDAP.BaseDN = testLDAPBaseDN - suite.configuration.LDAP.TLS = schema.DefaultLDAPAuthenticationBackendConfiguration.TLS + suite.config = schema.AuthenticationBackendConfiguration{} + suite.config.LDAP = &schema.LDAPAuthenticationBackendConfiguration{} + suite.config.LDAP.Implementation = schema.LDAPImplementationActiveDirectory + suite.config.LDAP.URL = testLDAPURL + suite.config.LDAP.User = testLDAPUser + suite.config.LDAP.Password = testLDAPPassword + suite.config.LDAP.BaseDN = testLDAPBaseDN + suite.config.LDAP.TLS = schema.DefaultLDAPAuthenticationBackendConfiguration.TLS } 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.HasErrors()) suite.Assert().Equal( schema.DefaultLDAPAuthenticationBackendConfiguration.Timeout, - suite.configuration.LDAP.Timeout) + suite.config.LDAP.Timeout) suite.Assert().Equal( schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.UsersFilter, - suite.configuration.LDAP.UsersFilter) + suite.config.LDAP.UsersFilter) suite.Assert().Equal( schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.UsernameAttribute, - suite.configuration.LDAP.UsernameAttribute) + suite.config.LDAP.UsernameAttribute) suite.Assert().Equal( schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.DisplayNameAttribute, - suite.configuration.LDAP.DisplayNameAttribute) + suite.config.LDAP.DisplayNameAttribute) suite.Assert().Equal( schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.MailAttribute, - suite.configuration.LDAP.MailAttribute) + suite.config.LDAP.MailAttribute) suite.Assert().Equal( schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.GroupsFilter, - suite.configuration.LDAP.GroupsFilter) + suite.config.LDAP.GroupsFilter) suite.Assert().Equal( schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.GroupNameAttribute, - suite.configuration.LDAP.GroupNameAttribute) + suite.config.LDAP.GroupNameAttribute) } func (suite *ActiveDirectoryAuthenticationBackendSuite) TestShouldOnlySetDefaultsIfNotManuallyConfigured() { - suite.configuration.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.configuration.LDAP.UsernameAttribute = "cn" - suite.configuration.LDAP.MailAttribute = "userPrincipalName" - suite.configuration.LDAP.DisplayNameAttribute = "name" - suite.configuration.LDAP.GroupsFilter = "(&(member={dn})(objectClass=group)(objectCategory=group))" - suite.configuration.LDAP.GroupNameAttribute = "distinguishedName" + suite.config.LDAP.Timeout = time.Second * 2 + suite.config.LDAP.UsersFilter = "(&({username_attribute}={input})(objectCategory=person)(objectClass=user)(!userAccountControl:1.2.840.113556.1.4.803:=2))" + suite.config.LDAP.UsernameAttribute = "cn" + suite.config.LDAP.MailAttribute = "userPrincipalName" + suite.config.LDAP.DisplayNameAttribute = "name" + suite.config.LDAP.GroupsFilter = "(&(member={dn})(objectClass=group)(objectCategory=group))" + suite.config.LDAP.GroupNameAttribute = "distinguishedName" - ValidateAuthenticationBackend(&suite.configuration, suite.validator) + ValidateAuthenticationBackend(&suite.config, suite.validator) suite.Assert().NotEqual( schema.DefaultLDAPAuthenticationBackendConfiguration.Timeout, - suite.configuration.LDAP.Timeout) + suite.config.LDAP.Timeout) suite.Assert().NotEqual( schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.UsersFilter, - suite.configuration.LDAP.UsersFilter) + suite.config.LDAP.UsersFilter) suite.Assert().NotEqual( schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.UsernameAttribute, - suite.configuration.LDAP.UsernameAttribute) + suite.config.LDAP.UsernameAttribute) suite.Assert().NotEqual( schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.DisplayNameAttribute, - suite.configuration.LDAP.DisplayNameAttribute) + suite.config.LDAP.DisplayNameAttribute) suite.Assert().NotEqual( schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.MailAttribute, - suite.configuration.LDAP.MailAttribute) + suite.config.LDAP.MailAttribute) suite.Assert().NotEqual( schema.DefaultLDAPAuthenticationBackendImplementationActiveDirectoryConfiguration.GroupsFilter, - suite.configuration.LDAP.GroupsFilter) + suite.config.LDAP.GroupsFilter) suite.Assert().NotEqual( 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) { diff --git a/internal/configuration/validator/configuration.go b/internal/configuration/validator/configuration.go index 12bba0e64..b23182cab 100644 --- a/internal/configuration/validator/configuration.go +++ b/internal/configuration/validator/configuration.go @@ -3,68 +3,59 @@ package validator import ( "fmt" "os" + "strings" "github.com/authelia/authelia/v4/internal/configuration/schema" "github.com/authelia/authelia/v4/internal/utils" ) // ValidateConfiguration and adapt the configuration read from file. -func ValidateConfiguration(configuration *schema.Configuration, validator *schema.StructValidator) { - if configuration.CertificatesDirectory != "" { - info, err := os.Stat(configuration.CertificatesDirectory) - if err != nil { - validator.Push(fmt.Errorf("Error checking certificate directory: %v", err)) +func ValidateConfiguration(config *schema.Configuration, validator *schema.StructValidator) { + var err error + + if config.CertificatesDirectory != "" { + 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() { - 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 == "" { - validator.Push(fmt.Errorf("Provide a JWT secret using \"jwt_secret\" key")) + if config.JWTSecret == "" { + validator.Push(fmt.Errorf("option 'jwt_secret' is required")) } - if configuration.DefaultRedirectionURL != "" { - err := utils.IsStringAbsURL(configuration.DefaultRedirectionURL) - if err != nil { - validator.Push(fmt.Errorf("Value for \"default_redirection_url\" is invalid: %+v", err)) + if config.DefaultRedirectionURL != "" { + if err = utils.IsStringAbsURL(config.DefaultRedirectionURL); 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://'"))) } } - 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 { - configuration.Regulation = &schema.DefaultRegulationConfiguration - } + ValidateRegulation(config, validator) - ValidateRegulation(configuration.Regulation, validator) + ValidateServer(config, validator) - ValidateServer(configuration, validator) + ValidateStorage(config.Storage, validator) - ValidateStorage(configuration.Storage, validator) + ValidateNotifier(config.Notifier, validator) - if configuration.Notifier == nil { - validator.Push(fmt.Errorf("A notifier configuration must be provided")) - } else { - ValidateNotifier(configuration.Notifier, validator) - } + ValidateIdentityProviders(&config.IdentityProviders, validator) - ValidateIdentityProviders(&configuration.IdentityProviders, validator) - - if configuration.NTP == nil { - configuration.NTP = &schema.DefaultNTPConfiguration - } - - ValidateNTP(configuration.NTP, validator) + ValidateNTP(config, validator) } diff --git a/internal/configuration/validator/configuration_test.go b/internal/configuration/validator/configuration_test.go index 6db192c69..8cff7dc52 100644 --- a/internal/configuration/validator/configuration_test.go +++ b/internal/configuration/validator/configuration_test.go @@ -52,7 +52,7 @@ func TestShouldEnsureNotifierConfigIsProvided(t *testing.T) { ValidateConfiguration(&config, validator) 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) { @@ -84,8 +84,8 @@ func TestShouldRaiseErrorWithUndefinedJWTSecretKey(t *testing.T) { require.Len(t, validator.Errors(), 1) require.Len(t, validator.Warnings(), 1) - assert.EqualError(t, validator.Errors()[0], "Provide a JWT secret using \"jwt_secret\" key") - assert.EqualError(t, validator.Warnings()[0], "No access control rules have been defined so the default policy two_factor will be applied to all requests") + assert.EqualError(t, validator.Errors()[0], "option 'jwt_secret' is required") + 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) { @@ -97,8 +97,8 @@ func TestShouldRaiseErrorWithBadDefaultRedirectionURL(t *testing.T) { require.Len(t, validator.Errors(), 1) require.Len(t, validator.Warnings(), 1) - assert.EqualError(t, validator.Errors()[0], "Value for \"default_redirection_url\" is invalid: the url 'bad_default_redirection_url' is not absolute because it doesn't start with a scheme like 'http://' or 'https://'") - assert.EqualError(t, validator.Warnings()[0], "No access control rules have been defined so the default policy two_factor will be applied to all requests") + 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], "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) { @@ -112,7 +112,7 @@ func TestShouldNotOverrideCertificatesDirectoryAndShouldPassWhenBlank(t *testing 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) { @@ -126,12 +126,12 @@ func TestShouldRaiseErrorOnInvalidCertificatesDirectory(t *testing.T) { require.Len(t, validator.Warnings(), 1) if runtime.GOOS == "windows" { - assert.EqualError(t, validator.Errors()[0], "Error checking certificate directory: CreateFile not-a-real-file.go: The system cannot find the file specified.") + 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 { - 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() config.CertificatesDirectory = "const.go" @@ -141,8 +141,8 @@ func TestShouldRaiseErrorOnInvalidCertificatesDirectory(t *testing.T) { require.Len(t, validator.Errors(), 1) require.Len(t, validator.Warnings(), 1) - assert.EqualError(t, validator.Errors()[0], "The path const.go specified for certificate_directory is not a directory") - assert.EqualError(t, validator.Warnings()[0], "No access control rules have been defined so the default policy two_factor will be applied to all requests") + assert.EqualError(t, validator.Errors()[0], "the location 'certificates_directory' refers to 'const.go' is not a directory") + 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) { @@ -155,5 +155,5 @@ func TestShouldNotRaiseErrorOnValidCertificatesDirectory(t *testing.T) { assert.Len(t, validator.Errors(), 0) require.Len(t, validator.Warnings(), 1) - assert.EqualError(t, validator.Warnings()[0], "No access control rules have been defined so the default policy two_factor will be applied to all requests") + 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") } diff --git a/internal/configuration/validator/const.go b/internal/configuration/validator/const.go index 487c58e44..2754d2363 100644 --- a/internal/configuration/validator/const.go +++ b/internal/configuration/validator/const.go @@ -1,6 +1,10 @@ package validator -import "regexp" +import ( + "regexp" + + "github.com/authelia/authelia/v4/internal/oidc" +) const ( loopback = "127.0.0.1" @@ -46,64 +50,173 @@ const ( // Notifier Error constants. const ( - errFmtNotifierMultipleConfigured = "notifier: you can't configure more than one notifier, please ensure " + - "only 'smtp' or 'filesystem' is configured" - errFmtNotifierNotConfigured = "notifier: you must ensure either the 'smtp' or 'filesystem' notifier " + + errFmtNotifierMultipleConfigured = "notifier: please ensure only one of the 'smtp' or 'filesystem' notifier is configured" + errFmtNotifierNotConfigured = "notifier: you must ensure either the 'smtp' or 'filesystem' notifier " + "is configured" - errFmtNotifierFileSystemFileNameNotConfigured = "filesystem notifier: the 'filename' must be configured" - errFmtNotifierSMTPNotConfigured = "smtp notifier: the '%s' must be configured" + errFmtNotifierFileSystemFileNameNotConfigured = "notifier: filesystem: option 'filename' is required " + 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. const ( - errFmtTOTPInvalidAlgorithm = "totp: algorithm '%s' is invalid: must be one of %s" - errFmtTOTPInvalidPeriod = "totp: period '%d' is invalid: must be 15 or more" - errFmtTOTPInvalidDigits = "totp: digits '%d' is invalid: must be 6 or 8" + errFmtTOTPInvalidAlgorithm = "totp: option 'algorithm' must be one of '%s' but it is configured as '%s'" + errFmtTOTPInvalidPeriod = "totp: option 'period' option must be 15 or more but it is configured as '%d'" + errFmtTOTPInvalidDigits = "totp: option 'digits' must be 6 or 8 but it is configured as '%d'" ) // Storage Error constants. const ( errStrStorage = "storage: configuration for a 'local', 'mysql' or 'postgres' database must be provided" - errStrStorageEncryptionKeyMustBeProvided = "storage: 'encryption_key' configuration option must be provided" - errStrStorageEncryptionKeyTooShort = "storage: 'encryption_key' configuration option must be 20 characters or longer" - errFmtStorageUserPassMustBeProvided = "storage: %s: 'username' and 'password' configuration options must be provided" //nolint: gosec - errFmtStorageOptionMustBeProvided = "storage: %s: '%s' configuration option must be provided" - errFmtStoragePostgreSQLInvalidSSLMode = "storage: postgres: ssl: 'mode' configuration option '%s' is invalid: must be one of '%s'" + errStrStorageEncryptionKeyMustBeProvided = "storage: option 'encryption_key' must is required" + errStrStorageEncryptionKeyTooShort = "storage: option 'encryption_key' must be 20 characters or longer" + errFmtStorageUserPassMustBeProvided = "storage: %s: option 'username' and 'password' are required" //nolint: gosec + errFmtStorageOptionMustBeProvided = "storage: %s: option '%s' is required" + 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. const ( - errFmtOIDCClientsDuplicateID = "openid connect provider: one or more clients have the same ID" - errFmtOIDCClientsWithEmptyID = "openid connect provider: one or more clients have been configured with an empty ID" - errFmtOIDCNoClientsConfigured = "openid connect provider: no clients are configured" - errFmtOIDCNoPrivateKey = "openid connect provider: issuer private key must be provided" - errFmtOIDCClientInvalidSecret = "openid connect provider: client with ID '%s' has an empty secret" - errFmtOIDCClientPublicInvalidSecret = "openid connect provider: client with ID '%s' is public but does not have " + - "an empty secret" - errFmtOIDCClientRedirectURI = "openid connect provider: client with ID '%s' redirect URI %s has an " + - "invalid scheme %s, should be http or https" - errFmtOIDCClientRedirectURICantBeParsed = "openid connect provider: client with ID '%s' has an invalid redirect " + - "URI '%s' could not be parsed: %v" - errFmtOIDCClientRedirectURIPublic = "openid connect provider: client with ID '%s' redirect URI '%s' is " + - "only valid for the public client type, not the confidential client type" - errFmtOIDCClientRedirectURIAbsolute = "openid connect provider: client with ID '%s' redirect URI '%s' is invalid " + - "because it has no scheme when it should be http or https" - errFmtOIDCClientInvalidPolicy = "openid connect provider: client with ID '%s' has an invalid policy " + - "'%s', should be either 'one_factor' or 'two_factor'" - errFmtOIDCClientInvalidScope = "openid connect provider: client with ID '%s' has an invalid scope " + - "'%s', must be one of: '%s'" - errFmtOIDCClientInvalidGrantType = "openid connect provider: client with ID '%s' has an invalid grant type " + - "'%s', must be one of: '%s'" - errFmtOIDCClientInvalidResponseMode = "openid connect provider: client with ID '%s' has an invalid response mode " + - "'%s', must be one of: '%s'" - errFmtOIDCClientInvalidUserinfoAlgorithm = "openid connect provider: client with ID '%s' has an invalid userinfo signing " + - "algorithm '%s', must be one of: '%s'" + errFmtOIDCNoClientsConfigured = "identity_providers: oidc: option 'clients' must have one or " + + "more clients configured" + errFmtOIDCNoPrivateKey = "identity_providers: oidc: option 'issuer_private_key' is required" + + errFmtOIDCClientsDuplicateID = "identity_providers: oidc: one or more clients have the same id but all client" + + "id's must be unique" + errFmtOIDCClientsWithEmptyID = "identity_providers: oidc: one or more clients have been configured with " + + "an empty id" + + errFmtOIDCClientInvalidSecret = "identity_providers: oidc: client '%s': option 'secret' is required" + errFmtOIDCClientPublicInvalidSecret = "identity_providers: oidc: client '%s': option 'secret' is " + + "required to be empty when option 'public' is true" + errFmtOIDCClientRedirectURI = "identity_providers: oidc: client '%s': option 'redirect_uris' has an " + + "invalid value: redirect uri '%s' must have a scheme of 'http' or 'https' but '%s' is configured" + errFmtOIDCClientRedirectURICantBeParsed = "identity_providers: oidc: client '%s': option 'redirect_uris' has an " + + "invalid value: redirect uri '%s' could not be parsed: %v" + errFmtOIDCClientRedirectURIPublic = "identity_providers: oidc: client '%s': option 'redirect_uris' has the" + + "redirect uri '%s' when option 'public' is false but this is invalid as this uri is not valid " + + "for the openid connect confidential client type" + errFmtOIDCClientRedirectURIAbsolute = "identity_providers: oidc: client '%s': option 'redirect_uris' has an " + + "invalid value: redirect uri '%s' must have the scheme 'http' or 'https' but it has no scheme" + errFmtOIDCClientInvalidPolicy = "identity_providers: oidc: client '%s': option 'policy' must be 'one_factor' " + + "or 'two_factor' but it is configured as '%s'" + errFmtOIDCClientInvalidEntry = "identity_providers: oidc: client '%s': option '%s' must only have the values " + + "'%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 " + "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. const ( /* @@ -118,26 +231,25 @@ const ( errFmtReplacedConfigurationKey = "invalid configuration key '%s' was replaced by '%s'" - errFmtLoggingLevelInvalid = "the log level '%s' is invalid, must be one of: %s" - - errFmtSessionSecretRedisProvider = "the session secret must be set when using the %s session provider" - errFmtSessionRedisPortRange = "the port must be between 1 and 65535 for the %s session provider" - errFmtSessionRedisHostRequired = "the host must be provided when using the %s session provider" - errFmtSessionRedisHostOrNodesRequired = "either the host or a node must be provided when using the %s session provider" + errFmtLoggingLevelInvalid = "log: option 'level' must be one of '%s' but it is configured as '%s'" errFileHashing = "config key incorrect: authentication_backend.file.hashing should be authentication_backend.file.password" errFilePHashing = "config key incorrect: authentication_backend.file.password_hashing should be authentication_backend.file.password" errFilePOptions = "config key incorrect: authentication_backend.file.password_options should be authentication_backend.file.password" - - errAccessControlInvalidPolicyWithSubjects = "policy [bypass] for rule #%d domain %s with subjects %s is invalid. It is " + - "not supported to configure both policy bypass and subjects. For more information see: " + - "https://www.authelia.com/docs/configuration/access-control.html#combining-subjects-and-the-bypass-policy" ) -var validLoggingLevels = []string{"trace", "debug", "info", "warn", "error"} -var validHTTPRequestMethods = []string{"GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "TRACE", "CONNECT", "OPTIONS"} +var validStoragePostgreSQLSSLModes = []string{testModeDisabled, "require", "verify-ca", "verify-full"} -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 validOIDCResponseModes = []string{"form_post", "query", "fragment"} var validOIDCUserinfoAlgorithms = []string{"none", "RS256"} diff --git a/internal/configuration/validator/identity_providers.go b/internal/configuration/validator/identity_providers.go index b119e1643..525aef418 100644 --- a/internal/configuration/validator/identity_providers.go +++ b/internal/configuration/validator/identity_providers.go @@ -11,55 +11,55 @@ import ( ) // ValidateIdentityProviders validates and update IdentityProviders configuration. -func ValidateIdentityProviders(configuration *schema.IdentityProvidersConfiguration, validator *schema.StructValidator) { - validateOIDC(configuration.OIDC, validator) +func ValidateIdentityProviders(config *schema.IdentityProvidersConfiguration, validator *schema.StructValidator) { + validateOIDC(config.OIDC, validator) } -func validateOIDC(configuration *schema.OpenIDConnectConfiguration, validator *schema.StructValidator) { - if configuration != nil { - if configuration.IssuerPrivateKey == "" { +func validateOIDC(config *schema.OpenIDConnectConfiguration, validator *schema.StructValidator) { + if config != nil { + if config.IssuerPrivateKey == "" { validator.Push(fmt.Errorf(errFmtOIDCNoPrivateKey)) } - if configuration.AccessTokenLifespan == time.Duration(0) { - configuration.AccessTokenLifespan = schema.DefaultOpenIDConnectConfiguration.AccessTokenLifespan + if config.AccessTokenLifespan == time.Duration(0) { + config.AccessTokenLifespan = schema.DefaultOpenIDConnectConfiguration.AccessTokenLifespan } - if configuration.AuthorizeCodeLifespan == time.Duration(0) { - configuration.AuthorizeCodeLifespan = schema.DefaultOpenIDConnectConfiguration.AuthorizeCodeLifespan + if config.AuthorizeCodeLifespan == time.Duration(0) { + config.AuthorizeCodeLifespan = schema.DefaultOpenIDConnectConfiguration.AuthorizeCodeLifespan } - if configuration.IDTokenLifespan == time.Duration(0) { - configuration.IDTokenLifespan = schema.DefaultOpenIDConnectConfiguration.IDTokenLifespan + if config.IDTokenLifespan == time.Duration(0) { + config.IDTokenLifespan = schema.DefaultOpenIDConnectConfiguration.IDTokenLifespan } - if configuration.RefreshTokenLifespan == time.Duration(0) { - configuration.RefreshTokenLifespan = schema.DefaultOpenIDConnectConfiguration.RefreshTokenLifespan + if config.RefreshTokenLifespan == time.Duration(0) { + config.RefreshTokenLifespan = schema.DefaultOpenIDConnectConfiguration.RefreshTokenLifespan } - if configuration.MinimumParameterEntropy != 0 && configuration.MinimumParameterEntropy < 8 { - validator.PushWarning(fmt.Errorf(errFmtOIDCServerInsecureParameterEntropy, configuration.MinimumParameterEntropy)) + if config.MinimumParameterEntropy != 0 && config.MinimumParameterEntropy < 8 { + 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)) } } } -func validateOIDCClients(configuration *schema.OpenIDConnectConfiguration, validator *schema.StructValidator) { +func validateOIDCClients(config *schema.OpenIDConnectConfiguration, validator *schema.StructValidator) { invalidID, duplicateIDs := false, false var ids []string - for c, client := range configuration.Clients { + for c, client := range config.Clients { if client.ID == "" { invalidID = true } else { if client.Description == "" { - configuration.Clients[c].Description = client.ID + config.Clients[c].Description = client.ID } if utils.IsStringInSliceFold(client.ID, ids) { @@ -79,16 +79,16 @@ func validateOIDCClients(configuration *schema.OpenIDConnectConfiguration, valid } 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 { validator.Push(fmt.Errorf(errFmtOIDCClientInvalidPolicy, client.ID, client.Policy)) } - validateOIDCClientScopes(c, configuration, validator) - validateOIDCClientGrantTypes(c, configuration, validator) - validateOIDCClientResponseTypes(c, configuration, validator) - validateOIDCClientResponseModes(c, configuration, validator) - validateOIDDClientUserinfoAlgorithm(c, configuration, validator) + validateOIDCClientScopes(c, config, validator) + validateOIDCClientGrantTypes(c, config, validator) + validateOIDCClientResponseTypes(c, config, validator) + validateOIDCClientResponseModes(c, config, validator) + validateOIDDClientUserinfoAlgorithm(c, config, validator) validateOIDCClientRedirectURIs(client, validator) } @@ -115,8 +115,8 @@ func validateOIDCClientScopes(c int, configuration *schema.OpenIDConnectConfigur for _, scope := range configuration.Clients[c].Scopes { if !utils.IsStringInSlice(scope, validOIDCScopes) { validator.Push(fmt.Errorf( - errFmtOIDCClientInvalidScope, - configuration.Clients[c].ID, scope, strings.Join(validOIDCScopes, "', '"))) + errFmtOIDCClientInvalidEntry, + 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 { if !utils.IsStringInSlice(grantType, validOIDCGrantTypes) { validator.Push(fmt.Errorf( - errFmtOIDCClientInvalidGrantType, - configuration.Clients[c].ID, grantType, strings.Join(validOIDCGrantTypes, "', '"))) + errFmtOIDCClientInvalidEntry, + 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 { if !utils.IsStringInSlice(responseMode, validOIDCResponseModes) { validator.Push(fmt.Errorf( - errFmtOIDCClientInvalidResponseMode, - configuration.Clients[c].ID, responseMode, strings.Join(validOIDCResponseModes, "', '"))) + errFmtOIDCClientInvalidEntry, + 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 } else if !utils.IsStringInSlice(configuration.Clients[c].UserinfoSigningAlgorithm, validOIDCUserinfoAlgorithms) { 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 } - validator.Push(fmt.Errorf(errFmtOIDCClientRedirectURIPublic, client.ID, redirectURI)) + validator.Push(fmt.Errorf(errFmtOIDCClientRedirectURIPublic, client.ID, oauth2InstalledApp)) continue } diff --git a/internal/configuration/validator/identity_providers_test.go b/internal/configuration/validator/identity_providers_test.go index 49f9de11f..00dcb7bfc 100644 --- a/internal/configuration/validator/identity_providers_test.go +++ b/internal/configuration/validator/identity_providers_test.go @@ -173,8 +173,7 @@ func TestShouldRaiseErrorWhenOIDCClientConfiguredWithBadScopes(t *testing.T) { ValidateIdentityProviders(config, validator) require.Len(t, validator.Errors(), 1) - assert.EqualError(t, validator.Errors()[0], "openid connect provider: client with ID 'good_id' has an invalid scope "+ - "'bad_scope', must be one of: 'openid', 'email', 'profile', 'groups', 'offline_access'") + 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'") } func TestShouldRaiseErrorWhenOIDCClientConfiguredWithBadGrantTypes(t *testing.T) { @@ -200,9 +199,7 @@ func TestShouldRaiseErrorWhenOIDCClientConfiguredWithBadGrantTypes(t *testing.T) ValidateIdentityProviders(config, validator) 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 "+ - "'bad_grant_type', must be one of: 'implicit', 'refresh_token', 'authorization_code', "+ - "'password', 'client_credentials'") + 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'") } func TestShouldRaiseErrorWhenOIDCClientConfiguredWithBadResponseModes(t *testing.T) { @@ -228,8 +225,7 @@ func TestShouldRaiseErrorWhenOIDCClientConfiguredWithBadResponseModes(t *testing ValidateIdentityProviders(config, validator) 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 "+ - "'bad_responsemode', must be one of: 'form_post', 'query', 'fragment'") + 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'") } func TestShouldRaiseErrorWhenOIDCClientConfiguredWithBadUserinfoAlg(t *testing.T) { @@ -255,8 +251,7 @@ func TestShouldRaiseErrorWhenOIDCClientConfiguredWithBadUserinfoAlg(t *testing.T ValidateIdentityProviders(config, validator) require.Len(t, validator.Errors(), 1) - assert.EqualError(t, validator.Errors()[0], "openid connect provider: client with ID 'good_id' has an invalid userinfo "+ - "signing algorithm 'rs256', must be one of: 'none, RS256'") + 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'") } func TestValidateIdentityProvidersShouldRaiseWarningOnSecurityIssue(t *testing.T) { @@ -502,8 +497,8 @@ func TestValidateOIDCClientRedirectURIsSupportingPrivateUseURISchemes(t *testing assert.Len(t, validator.Warnings(), 0) assert.Len(t, validator.Errors(), 2) 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("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 'oc://ios.owncloud.com' must have a scheme of 'http' or 'https' but 'oc' is configured"), + 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"), }) }) } diff --git a/internal/configuration/validator/log.go b/internal/configuration/validator/log.go new file mode 100644 index 000000000..1522f6804 --- /dev/null +++ b/internal/configuration/validator/log.go @@ -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)) + } +} diff --git a/internal/configuration/validator/logging_test.go b/internal/configuration/validator/log_test.go similarity index 79% rename from internal/configuration/validator/logging_test.go rename to internal/configuration/validator/log_test.go index 329a0e54e..56cff19de 100644 --- a/internal/configuration/validator/logging_test.go +++ b/internal/configuration/validator/log_test.go @@ -14,7 +14,7 @@ func TestShouldSetDefaultLoggingValues(t *testing.T) { validator := schema.NewStructValidator() - ValidateLogging(config, validator) + ValidateLog(config, validator) assert.Len(t, validator.Warnings(), 0) assert.Len(t, validator.Errors(), 0) @@ -35,10 +35,10 @@ func TestShouldRaiseErrorOnInvalidLoggingLevel(t *testing.T) { validator := schema.NewStructValidator() - ValidateLogging(config, validator) + ValidateLog(config, validator) assert.Len(t, validator.Warnings(), 0) require.Len(t, validator.Errors(), 1) - assert.EqualError(t, validator.Errors()[0], "the log level 'TRACE' is invalid, must be one of: trace, debug, info, warn, error") + assert.EqualError(t, validator.Errors()[0], "log: option 'level' must be one of 'trace', 'debug', 'info', 'warn', 'error' but it is configured as 'TRACE'") } diff --git a/internal/configuration/validator/logging.go b/internal/configuration/validator/logging.go deleted file mode 100644 index 00c1a7562..000000000 --- a/internal/configuration/validator/logging.go +++ /dev/null @@ -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, ", "))) - } -} diff --git a/internal/configuration/validator/notifier.go b/internal/configuration/validator/notifier.go index 024defc28..fbb21a18c 100644 --- a/internal/configuration/validator/notifier.go +++ b/internal/configuration/validator/notifier.go @@ -7,62 +7,62 @@ import ( ) // ValidateNotifier validates and update notifier configuration. -func ValidateNotifier(configuration *schema.NotifierConfiguration, validator *schema.StructValidator) { - if configuration.SMTP == nil && configuration.FileSystem == nil { +func ValidateNotifier(config *schema.NotifierConfiguration, validator *schema.StructValidator) { + if config == nil || (config.SMTP == nil && config.FileSystem == nil) { validator.Push(fmt.Errorf(errFmtNotifierNotConfigured)) return - } else if configuration.SMTP != nil && configuration.FileSystem != nil { + } else if config.SMTP != nil && config.FileSystem != nil { validator.Push(fmt.Errorf(errFmtNotifierMultipleConfigured)) return } - if configuration.FileSystem != nil { - if configuration.FileSystem.Filename == "" { + if config.FileSystem != nil { + if config.FileSystem.Filename == "" { validator.Push(fmt.Errorf(errFmtNotifierFileSystemFileNameNotConfigured)) } return } - validateSMTPNotifier(configuration.SMTP, validator) + validateSMTPNotifier(config.SMTP, validator) } -func validateSMTPNotifier(configuration *schema.SMTPNotifierConfiguration, validator *schema.StructValidator) { - if configuration.StartupCheckAddress == "" { - configuration.StartupCheckAddress = schema.DefaultSMTPNotifierConfiguration.StartupCheckAddress +func validateSMTPNotifier(config *schema.SMTPNotifierConfiguration, validator *schema.StructValidator) { + if config.StartupCheckAddress == "" { + config.StartupCheckAddress = schema.DefaultSMTPNotifierConfiguration.StartupCheckAddress } - if configuration.Host == "" { + if config.Host == "" { validator.Push(fmt.Errorf(errFmtNotifierSMTPNotConfigured, "host")) } - if configuration.Port == 0 { + if config.Port == 0 { validator.Push(fmt.Errorf(errFmtNotifierSMTPNotConfigured, "port")) } - if configuration.Timeout == 0 { - configuration.Timeout = schema.DefaultSMTPNotifierConfiguration.Timeout + if config.Timeout == 0 { + config.Timeout = schema.DefaultSMTPNotifierConfiguration.Timeout } - if configuration.Sender.Address == "" { + if config.Sender.Address == "" { validator.Push(fmt.Errorf(errFmtNotifierSMTPNotConfigured, "sender")) } - if configuration.Subject == "" { - configuration.Subject = schema.DefaultSMTPNotifierConfiguration.Subject + if config.Subject == "" { + config.Subject = schema.DefaultSMTPNotifierConfiguration.Subject } - if configuration.Identifier == "" { - configuration.Identifier = schema.DefaultSMTPNotifierConfiguration.Identifier + if config.Identifier == "" { + config.Identifier = schema.DefaultSMTPNotifierConfiguration.Identifier } - if configuration.TLS == nil { - configuration.TLS = schema.DefaultSMTPNotifierConfiguration.TLS + if config.TLS == nil { + config.TLS = schema.DefaultSMTPNotifierConfiguration.TLS } - if configuration.TLS.ServerName == "" { - configuration.TLS.ServerName = configuration.Host + if config.TLS.ServerName == "" { + config.TLS.ServerName = config.Host } } diff --git a/internal/configuration/validator/notifier_test.go b/internal/configuration/validator/notifier_test.go index 1af7219a8..7e7eb8d94 100644 --- a/internal/configuration/validator/notifier_test.go +++ b/internal/configuration/validator/notifier_test.go @@ -12,34 +12,34 @@ import ( type NotifierSuite struct { suite.Suite - configuration schema.NotifierConfiguration - validator *schema.StructValidator + config schema.NotifierConfiguration + validator *schema.StructValidator } func (suite *NotifierSuite) SetupTest() { suite.validator = schema.NewStructValidator() - suite.configuration.SMTP = &schema.SMTPNotifierConfiguration{ + suite.config.SMTP = &schema.SMTPNotifierConfiguration{ Username: "john", Password: "password", Sender: mail.Address{Name: "Authelia", Address: "authelia@example.com"}, Host: "example.com", Port: 25, } - suite.configuration.FileSystem = nil + suite.config.FileSystem = nil } /* Common Tests. */ 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.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.Require().True(suite.validator.HasErrors()) @@ -50,15 +50,15 @@ func (suite *NotifierSuite) TestShouldEnsureAtLeastSMTPOrFilesystemIsProvided() } func (suite *NotifierSuite) TestShouldEnsureEitherSMTPOrFilesystemIsProvided() { - ValidateNotifier(&suite.configuration, suite.validator) + ValidateNotifier(&suite.config, suite.validator) suite.Assert().False(suite.validator.HasErrors()) - suite.configuration.FileSystem = &schema.FileSystemNotifierConfiguration{ + suite.config.FileSystem = &schema.FileSystemNotifierConfiguration{ Filename: "test", } - ValidateNotifier(&suite.configuration, suite.validator) + ValidateNotifier(&suite.config, suite.validator) suite.Assert().False(suite.validator.HasWarnings()) suite.Require().True(suite.validator.HasErrors()) @@ -72,43 +72,43 @@ func (suite *NotifierSuite) TestShouldEnsureEitherSMTPOrFilesystemIsProvided() { SMTP Tests. */ 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.HasErrors()) - suite.Assert().Equal("example.com", suite.configuration.SMTP.TLS.ServerName) - suite.Assert().Equal("TLS1.2", suite.configuration.SMTP.TLS.MinimumVersion) - suite.Assert().False(suite.configuration.SMTP.TLS.SkipVerify) + suite.Assert().Equal("example.com", suite.config.SMTP.TLS.ServerName) + suite.Assert().Equal("TLS1.2", suite.config.SMTP.TLS.MinimumVersion) + suite.Assert().False(suite.config.SMTP.TLS.SkipVerify) } func (suite *NotifierSuite) TestSMTPShouldDefaultTLSServerNameToHost() { - suite.configuration.SMTP.Host = "google.com" - suite.configuration.SMTP.TLS = &schema.TLSConfig{ + suite.config.SMTP.Host = "google.com" + suite.config.SMTP.TLS = &schema.TLSConfig{ MinimumVersion: "TLS1.1", } - ValidateNotifier(&suite.configuration, suite.validator) + ValidateNotifier(&suite.config, suite.validator) suite.Assert().False(suite.validator.HasWarnings()) suite.Assert().False(suite.validator.HasErrors()) - suite.Assert().Equal("google.com", suite.configuration.SMTP.TLS.ServerName) - suite.Assert().Equal("TLS1.1", suite.configuration.SMTP.TLS.MinimumVersion) - suite.Assert().False(suite.configuration.SMTP.TLS.SkipVerify) + suite.Assert().Equal("google.com", suite.config.SMTP.TLS.ServerName) + suite.Assert().Equal("TLS1.1", suite.config.SMTP.TLS.MinimumVersion) + suite.Assert().False(suite.config.SMTP.TLS.SkipVerify) } func (suite *NotifierSuite) TestSMTPShouldEnsureHostAndPortAreProvided() { - suite.configuration.FileSystem = nil - ValidateNotifier(&suite.configuration, suite.validator) + suite.config.FileSystem = nil + ValidateNotifier(&suite.config, suite.validator) suite.Assert().False(suite.validator.HasWarnings()) suite.Assert().False(suite.validator.HasErrors()) - suite.configuration.SMTP.Host = "" - suite.configuration.SMTP.Port = 0 + suite.config.SMTP.Host = "" + suite.config.SMTP.Port = 0 - ValidateNotifier(&suite.configuration, suite.validator) + ValidateNotifier(&suite.config, suite.validator) suite.Assert().False(suite.validator.HasWarnings()) suite.Assert().True(suite.validator.HasErrors()) @@ -122,9 +122,9 @@ func (suite *NotifierSuite) TestSMTPShouldEnsureHostAndPortAreProvided() { } 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.Require().True(suite.validator.HasErrors()) @@ -138,18 +138,18 @@ func (suite *NotifierSuite) TestSMTPShouldEnsureSenderIsProvided() { File Tests. */ func (suite *NotifierSuite) TestFileShouldEnsureFilenameIsProvided() { - suite.configuration.SMTP = nil - suite.configuration.FileSystem = &schema.FileSystemNotifierConfiguration{ + suite.config.SMTP = nil + suite.config.FileSystem = &schema.FileSystemNotifierConfiguration{ Filename: "test", } - ValidateNotifier(&suite.configuration, suite.validator) + ValidateNotifier(&suite.config, suite.validator) suite.Assert().False(suite.validator.HasWarnings()) 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.Require().True(suite.validator.HasErrors()) diff --git a/internal/configuration/validator/ntp.go b/internal/configuration/validator/ntp.go index 01cdd913b..a99f9e9ec 100644 --- a/internal/configuration/validator/ntp.go +++ b/internal/configuration/validator/ntp.go @@ -8,23 +8,29 @@ import ( ) // ValidateNTP validates and update NTP configuration. -func ValidateNTP(configuration *schema.NTPConfiguration, validator *schema.StructValidator) { - if configuration.Address == "" { - configuration.Address = schema.DefaultNTPConfiguration.Address +func ValidateNTP(config *schema.Configuration, validator *schema.StructValidator) { + if config.NTP == nil { + config.NTP = &schema.DefaultNTPConfiguration + + return } - if configuration.Version == 0 { - configuration.Version = schema.DefaultNTPConfiguration.Version - } else if configuration.Version < 3 || configuration.Version > 4 { - validator.Push(fmt.Errorf("ntp: version must be either 3 or 4")) + if config.NTP.Address == "" { + config.NTP.Address = schema.DefaultNTPConfiguration.Address } - if configuration.MaximumDesync == "" { - configuration.MaximumDesync = schema.DefaultNTPConfiguration.MaximumDesync + if config.NTP.Version == 0 { + 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 { - validator.Push(fmt.Errorf("ntp: error occurred parsing NTP max_desync string: %s", err)) + validator.Push(fmt.Errorf(errFmtNTPMaxDesync, err)) } } diff --git a/internal/configuration/validator/ntp_test.go b/internal/configuration/validator/ntp_test.go index f2bc29c9b..34fcae229 100644 --- a/internal/configuration/validator/ntp_test.go +++ b/internal/configuration/validator/ntp_test.go @@ -4,13 +4,15 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/authelia/authelia/v4/internal/configuration/schema" ) -func newDefaultNTPConfig() schema.NTPConfiguration { - config := schema.NTPConfiguration{} - return config +func newDefaultNTPConfig() schema.Configuration { + return schema.Configuration{ + NTP: &schema.NTPConfiguration{}, + } } func TestShouldSetDefaultNtpAddress(t *testing.T) { @@ -20,7 +22,7 @@ func TestShouldSetDefaultNtpAddress(t *testing.T) { ValidateNTP(&config, validator) 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) { @@ -30,7 +32,7 @@ func TestShouldSetDefaultNtpVersion(t *testing.T) { ValidateNTP(&config, validator) 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) { @@ -40,7 +42,7 @@ func TestShouldSetDefaultNtpMaximumDesync(t *testing.T) { ValidateNTP(&config, validator) 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) { @@ -50,16 +52,29 @@ func TestShouldSetDefaultNtpDisableStartupCheck(t *testing.T) { ValidateNTP(&config, validator) 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) { validator := schema.NewStructValidator() config := newDefaultNTPConfig() - config.MaximumDesync = "a second" + config.NTP.MaximumDesync = "a second" ValidateNTP(&config, validator) - assert.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") + require.Len(t, validator.Errors(), 1) + + 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'") } diff --git a/internal/configuration/validator/regulation.go b/internal/configuration/validator/regulation.go index 5e11ac5cf..4f25a6079 100644 --- a/internal/configuration/validator/regulation.go +++ b/internal/configuration/validator/regulation.go @@ -8,26 +8,32 @@ import ( ) // ValidateRegulation validates and update regulator configuration. -func ValidateRegulation(configuration *schema.RegulationConfiguration, validator *schema.StructValidator) { - if configuration.FindTime == "" { - configuration.FindTime = schema.DefaultRegulationConfiguration.FindTime // 2 min. +func ValidateRegulation(config *schema.Configuration, validator *schema.StructValidator) { + if config.Regulation == nil { + config.Regulation = &schema.DefaultRegulationConfiguration + + return } - if configuration.BanTime == "" { - configuration.BanTime = schema.DefaultRegulationConfiguration.BanTime // 5 min. + if config.Regulation.FindTime == "" { + 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 { - 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 { - validator.Push(fmt.Errorf("Error occurred parsing regulation ban_time string: %s", err)) + validator.Push(fmt.Errorf(errFmtRegulationParseDuration, "ban_time", err)) } if findTime > banTime { - validator.Push(fmt.Errorf("find_time cannot be greater than ban_time")) + validator.Push(fmt.Errorf(errFmtRegulationFindTimeGreaterThanBanTime)) } } diff --git a/internal/configuration/validator/regulation_test.go b/internal/configuration/validator/regulation_test.go index a2feb710f..81893c953 100644 --- a/internal/configuration/validator/regulation_test.go +++ b/internal/configuration/validator/regulation_test.go @@ -8,8 +8,11 @@ import ( "github.com/authelia/authelia/v4/internal/configuration/schema" ) -func newDefaultRegulationConfig() schema.RegulationConfiguration { - config := schema.RegulationConfiguration{} +func newDefaultRegulationConfig() schema.Configuration { + config := schema.Configuration{ + Regulation: &schema.RegulationConfiguration{}, + } + return config } @@ -20,7 +23,7 @@ func TestShouldSetDefaultRegulationBanTime(t *testing.T) { ValidateRegulation(&config, validator) 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) { @@ -30,30 +33,30 @@ func TestShouldSetDefaultRegulationFindTime(t *testing.T) { ValidateRegulation(&config, validator) 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) { validator := schema.NewStructValidator() config := newDefaultRegulationConfig() - config.FindTime = "1m" - config.BanTime = "10s" + config.Regulation.FindTime = "1m" + config.Regulation.BanTime = "10s" ValidateRegulation(&config, validator) 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) { validator := schema.NewStructValidator() config := newDefaultRegulationConfig() - config.FindTime = "a year" - config.BanTime = "forever" + config.Regulation.FindTime = "a year" + config.Regulation.BanTime = "forever" ValidateRegulation(&config, validator) 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()[1], "Error occurred parsing regulation ban_time string: could not convert the input string of forever 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], "regulation: option 'ban_time' could not be parsed: could not parse 'forever' as a duration") } diff --git a/internal/configuration/validator/server.go b/internal/configuration/validator/server.go index 3a49af52f..a958f0e13 100644 --- a/internal/configuration/validator/server.go +++ b/internal/configuration/validator/server.go @@ -10,40 +10,41 @@ import ( ) // ValidateServer checks a server configuration is correct. -func ValidateServer(configuration *schema.Configuration, validator *schema.StructValidator) { - if configuration.Server.Host == "" { - configuration.Server.Host = schema.DefaultServerConfiguration.Host +func ValidateServer(config *schema.Configuration, validator *schema.StructValidator) { + if config.Server.Host == "" { + config.Server.Host = schema.DefaultServerConfiguration.Host } - if configuration.Server.Port == 0 { - configuration.Server.Port = schema.DefaultServerConfiguration.Port + if config.Server.Port == 0 { + config.Server.Port = schema.DefaultServerConfiguration.Port } - if configuration.Server.TLS.Key != "" && configuration.Server.TLS.Certificate == "" { - validator.Push(fmt.Errorf("server: no TLS certificate provided to accompany the TLS key, please configure the 'server.tls.certificate' option")) - } else if configuration.Server.TLS.Key == "" && configuration.Server.TLS.Certificate != "" { - validator.Push(fmt.Errorf("server: no TLS key provided to accompany the TLS certificate, please configure the 'server.tls.key' option")) + if config.Server.TLS.Key != "" && config.Server.TLS.Certificate == "" { + validator.Push(fmt.Errorf(errFmtServerTLSCert)) + } else if config.Server.TLS.Key == "" && config.Server.TLS.Certificate != "" { + validator.Push(fmt.Errorf(errFmtServerTLSKey)) } switch { - case strings.Contains(configuration.Server.Path, "/"): - validator.Push(fmt.Errorf("server path must not contain any forward slashes")) - case !utils.IsStringAlphaNumeric(configuration.Server.Path): - validator.Push(fmt.Errorf("server path must only be alpha numeric characters")) - case configuration.Server.Path == "": // Don't do anything if it's blank. + case strings.Contains(config.Server.Path, "/"): + validator.Push(fmt.Errorf(errFmtServerPathNoForwardSlashes)) + case !utils.IsStringAlphaNumeric(config.Server.Path): + validator.Push(fmt.Errorf(errFmtServerPathAlphaNum)) + case config.Server.Path == "": // Don't do anything if it's blank. + break default: - configuration.Server.Path = path.Clean("/" + configuration.Server.Path) + config.Server.Path = path.Clean("/" + config.Server.Path) } - if configuration.Server.ReadBufferSize == 0 { - configuration.Server.ReadBufferSize = schema.DefaultServerConfiguration.ReadBufferSize - } else if configuration.Server.ReadBufferSize < 0 { - validator.Push(fmt.Errorf("server read buffer size must be above 0")) + if config.Server.ReadBufferSize == 0 { + config.Server.ReadBufferSize = schema.DefaultServerConfiguration.ReadBufferSize + } else if config.Server.ReadBufferSize < 0 { + validator.Push(fmt.Errorf(errFmtServerBufferSize, "read", config.Server.ReadBufferSize)) } - if configuration.Server.WriteBufferSize == 0 { - configuration.Server.WriteBufferSize = schema.DefaultServerConfiguration.WriteBufferSize - } else if configuration.Server.WriteBufferSize < 0 { - validator.Push(fmt.Errorf("server write buffer size must be above 0")) + if config.Server.WriteBufferSize == 0 { + config.Server.WriteBufferSize = schema.DefaultServerConfiguration.WriteBufferSize + } else if config.Server.WriteBufferSize < 0 { + validator.Push(fmt.Errorf(errFmtServerBufferSize, "write", config.Server.WriteBufferSize)) } } diff --git a/internal/configuration/validator/server_test.go b/internal/configuration/validator/server_test.go index fd6348363..e11e884a2 100644 --- a/internal/configuration/validator/server_test.go +++ b/internal/configuration/validator/server_test.go @@ -71,8 +71,8 @@ func TestShouldRaiseOnNegativeValues(t *testing.T) { 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()[1], "server write 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: option 'write_buffer_size' must be above 0 but it is configured as '-1'") } func TestShouldRaiseOnNonAlphanumericCharsInPath(t *testing.T) { @@ -123,7 +123,7 @@ func TestShouldRaiseErrorWhenTLSCertWithoutKeyIsProvided(t *testing.T) { ValidateServer(&config, validator) 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) { @@ -133,7 +133,7 @@ func TestShouldRaiseErrorWhenTLSKeyWithoutCertIsProvided(t *testing.T) { ValidateServer(&config, validator) 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) { diff --git a/internal/configuration/validator/session.go b/internal/configuration/validator/session.go index 381b7f6ff..83a6c39c1 100644 --- a/internal/configuration/validator/session.go +++ b/internal/configuration/validator/session.go @@ -10,109 +10,110 @@ import ( ) // ValidateSession validates and update session configuration. -func ValidateSession(configuration *schema.SessionConfiguration, validator *schema.StructValidator) { - if configuration.Name == "" { - configuration.Name = schema.DefaultSessionConfiguration.Name +func ValidateSession(config *schema.SessionConfiguration, validator *schema.StructValidator) { + if config.Name == "" { + config.Name = schema.DefaultSessionConfiguration.Name } - if configuration.Redis != nil { - if configuration.Redis.HighAvailability != nil { - if configuration.Redis.HighAvailability.SentinelName != "" { - validateRedisSentinel(configuration, validator) - } else { - validator.Push(fmt.Errorf("Session provider redis is configured for high availability but doesn't have a sentinel_name which is required")) - } + if config.Redis != nil { + if config.Redis.HighAvailability != nil { + validateRedisSentinel(config, validator) } else { - validateRedis(configuration, validator) + validateRedis(config, validator) } } - validateSession(configuration, validator) + validateSession(config, validator) } -func validateSession(configuration *schema.SessionConfiguration, validator *schema.StructValidator) { - if configuration.Expiration == "" { - configuration.Expiration = schema.DefaultSessionConfiguration.Expiration // 1 hour. - } else if _, err := utils.ParseDurationString(configuration.Expiration); err != nil { - validator.Push(fmt.Errorf("Error occurred parsing session expiration string: %s", err)) +func validateSession(config *schema.SessionConfiguration, validator *schema.StructValidator) { + if config.Expiration == "" { + config.Expiration = schema.DefaultSessionConfiguration.Expiration // 1 hour. + } else if _, err := utils.ParseDurationString(config.Expiration); err != nil { + validator.Push(fmt.Errorf(errFmtSessionCouldNotParseDuration, "expiriation", err)) } - if configuration.Inactivity == "" { - configuration.Inactivity = schema.DefaultSessionConfiguration.Inactivity // 5 min. - } else if _, err := utils.ParseDurationString(configuration.Inactivity); err != nil { - validator.Push(fmt.Errorf("Error occurred parsing session inactivity string: %s", err)) + if config.Inactivity == "" { + config.Inactivity = schema.DefaultSessionConfiguration.Inactivity // 5 min. + } else if _, err := utils.ParseDurationString(config.Inactivity); err != nil { + validator.Push(fmt.Errorf(errFmtSessionCouldNotParseDuration, "inactivity", err)) } - if configuration.RememberMeDuration == "" { - configuration.RememberMeDuration = schema.DefaultSessionConfiguration.RememberMeDuration // 1 month. - } else if _, err := utils.ParseDurationString(configuration.RememberMeDuration); err != nil { - validator.Push(fmt.Errorf("Error occurred parsing session remember_me_duration string: %s", err)) + if config.RememberMeDuration == "" { + config.RememberMeDuration = schema.DefaultSessionConfiguration.RememberMeDuration // 1 month. + } else if _, err := utils.ParseDurationString(config.RememberMeDuration); err != nil { + validator.Push(fmt.Errorf(errFmtSessionCouldNotParseDuration, "remember_me_duration", err)) } - if configuration.Domain == "" { - validator.Push(errors.New("Set domain of the session object")) + if config.Domain == "" { + validator.Push(fmt.Errorf(errFmtSessionOptionRequired, "domain")) } - if strings.Contains(configuration.Domain, "*") { - validator.Push(errors.New("The domain of the session must be the root domain you're protecting instead of a wildcard domain")) + if strings.HasPrefix(config.Domain, "*.") { + validator.Push(fmt.Errorf(errFmtSessionDomainMustBeRoot, config.Domain)) } - if configuration.SameSite == "" { - configuration.SameSite = schema.DefaultSessionConfiguration.SameSite - } else if configuration.SameSite != "none" && configuration.SameSite != "lax" && configuration.SameSite != "strict" { - validator.Push(errors.New("session same_site is configured incorrectly, must be one of 'none', 'lax', or 'strict'")) + if config.SameSite == "" { + config.SameSite = schema.DefaultSessionConfiguration.SameSite + } else if !utils.IsStringInSlice(config.SameSite, validSessionSameSiteValues) { + validator.Push(fmt.Errorf(errFmtSessionSameSite, strings.Join(validSessionSameSiteValues, "', '"), config.SameSite)) } } -func validateRedis(configuration *schema.SessionConfiguration, validator *schema.StructValidator) { - if configuration.Redis.Host == "" { - validator.Push(fmt.Errorf(errFmtSessionRedisHostRequired, "redis")) +func validateRedisCommon(config *schema.SessionConfiguration, validator *schema.StructValidator) { + if config.Secret == "" { + validator.Push(fmt.Errorf(errFmtSessionSecretRequired, "redis")) + } +} + +func validateRedis(config *schema.SessionConfiguration, validator *schema.StructValidator) { + if config.Redis.Host == "" { + validator.Push(fmt.Errorf(errFmtSessionRedisHostRequired)) } - if configuration.Secret == "" { - validator.Push(fmt.Errorf(errFmtSessionSecretRedisProvider, "redis")) - } + validateRedisCommon(config, validator) - if !strings.HasPrefix(configuration.Redis.Host, "/") && configuration.Redis.Port == 0 { + if !strings.HasPrefix(config.Redis.Host, "/") && config.Redis.Port == 0 { validator.Push(errors.New("A redis port different than 0 must be provided")) - } else if configuration.Redis.Port < 0 || configuration.Redis.Port > 65535 { - validator.Push(fmt.Errorf(errFmtSessionRedisPortRange, "redis")) + } else if config.Redis.Port < 0 || config.Redis.Port > 65535 { + validator.Push(fmt.Errorf(errFmtSessionRedisPortRange, config.Redis.Port)) } - if configuration.Redis.MaximumActiveConnections <= 0 { - configuration.Redis.MaximumActiveConnections = 8 + if config.Redis.MaximumActiveConnections <= 0 { + config.Redis.MaximumActiveConnections = 8 } } -func validateRedisSentinel(configuration *schema.SessionConfiguration, validator *schema.StructValidator) { - if configuration.Redis.Port == 0 { - configuration.Redis.Port = 26379 - } else if configuration.Redis.Port < 0 || configuration.Redis.Port > 65535 { - validator.Push(fmt.Errorf(errFmtSessionRedisPortRange, "redis sentinel")) +func validateRedisSentinel(config *schema.SessionConfiguration, validator *schema.StructValidator) { + if config.Redis.HighAvailability.SentinelName == "" { + validator.Push(fmt.Errorf(errFmtSessionRedisSentinelMissingName)) } - validateHighAvailability(configuration, validator, "redis sentinel") -} - -func validateHighAvailability(configuration *schema.SessionConfiguration, validator *schema.StructValidator, provider string) { - if configuration.Redis.Host == "" && len(configuration.Redis.HighAvailability.Nodes) == 0 { - validator.Push(fmt.Errorf(errFmtSessionRedisHostOrNodesRequired, provider)) + 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)) } - if configuration.Secret == "" { - validator.Push(fmt.Errorf(errFmtSessionSecretRedisProvider, provider)) + if config.Redis.Host == "" && len(config.Redis.HighAvailability.Nodes) == 0 { + validator.Push(fmt.Errorf(errFmtSessionRedisHostOrNodesRequired)) } - for i, node := range configuration.Redis.HighAvailability.Nodes { + validateRedisCommon(config, validator) + + hostMissing := false + + for i, node := range config.Redis.HighAvailability.Nodes { 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)) - break + hostMissing = true } if node.Port == 0 { - if provider == "redis sentinel" { - configuration.Redis.HighAvailability.Nodes[i].Port = 26379 - } + config.Redis.HighAvailability.Nodes[i].Port = 26379 } } + + if hostMissing { + validator.Push(fmt.Errorf(errFmtSessionRedisSentinelNodeHostMissing)) + } } diff --git a/internal/configuration/validator/session_test.go b/internal/configuration/validator/session_test.go index b3e612a0e..281fbae8a 100644 --- a/internal/configuration/validator/session_test.go +++ b/internal/configuration/validator/session_test.go @@ -100,7 +100,7 @@ func TestShouldRaiseErrorWithInvalidRedisPortLow(t *testing.T) { assert.False(t, validator.HasWarnings()) 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) { @@ -117,7 +117,7 @@ func TestShouldRaiseErrorWithInvalidRedisPortHigh(t *testing.T) { assert.False(t, validator.HasWarnings()) 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) { @@ -140,7 +140,7 @@ func TestShouldRaiseErrorWhenRedisIsUsedAndSecretNotSet(t *testing.T) { assert.False(t, validator.HasWarnings()) 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) { @@ -193,7 +193,7 @@ func TestShouldRaiseOneErrorWhenRedisHighAvailabilityHasNodesWithNoHost(t *testi assert.False(t, validator.HasWarnings()) 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) { @@ -215,7 +215,7 @@ func TestShouldRaiseOneErrorWhenRedisHighAvailabilityDoesNotHaveSentinelName(t * assert.False(t, validator.HasWarnings()) 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) { @@ -281,8 +281,8 @@ func TestShouldRaiseErrorsWhenRedisSentinelOptionsIncorrectlyConfigured(t *testi assert.False(t, validator.HasWarnings()) require.Len(t, errors, 2) - assert.EqualError(t, errors[0], fmt.Sprintf(errFmtSessionRedisPortRange, "redis sentinel")) - assert.EqualError(t, errors[1], fmt.Sprintf(errFmtSessionSecretRedisProvider, "redis sentinel")) + assert.EqualError(t, errors[0], fmt.Sprintf(errFmtSessionRedisPortRange, 65536)) + assert.EqualError(t, errors[1], fmt.Sprintf(errFmtSessionSecretRequired, "redis")) validator.Clear() @@ -295,8 +295,8 @@ func TestShouldRaiseErrorsWhenRedisSentinelOptionsIncorrectlyConfigured(t *testi assert.False(t, validator.HasWarnings()) require.Len(t, errors, 2) - assert.EqualError(t, errors[0], fmt.Sprintf(errFmtSessionRedisPortRange, "redis sentinel")) - assert.EqualError(t, errors[1], fmt.Sprintf(errFmtSessionSecretRedisProvider, "redis sentinel")) + assert.EqualError(t, errors[0], fmt.Sprintf(errFmtSessionRedisPortRange, -1)) + assert.EqualError(t, errors[1], fmt.Sprintf(errFmtSessionSecretRequired, "redis")) } func TestShouldNotRaiseErrorsAndSetDefaultPortWhenRedisSentinelPortBlank(t *testing.T) { @@ -347,7 +347,7 @@ func TestShouldRaiseErrorWhenRedisHostAndHighAvailabilityNodesEmpty(t *testing.T assert.False(t, validator.HasWarnings()) 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) { @@ -365,7 +365,7 @@ func TestShouldRaiseErrorsWhenRedisHostNotSet(t *testing.T) { assert.False(t, validator.HasWarnings()) 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) { @@ -377,7 +377,7 @@ func TestShouldRaiseErrorWhenDomainNotSet(t *testing.T) { assert.False(t, validator.HasWarnings()) 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) { @@ -389,7 +389,7 @@ func TestShouldRaiseErrorWhenDomainIsWildcard(t *testing.T) { assert.False(t, validator.HasWarnings()) 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) { @@ -401,7 +401,7 @@ func TestShouldRaiseErrorWhenSameSiteSetIncorrectly(t *testing.T) { assert.False(t, validator.HasWarnings()) 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) { @@ -430,8 +430,8 @@ func TestShouldRaiseErrorWhenBadInactivityAndExpirationSet(t *testing.T) { assert.False(t, validator.HasWarnings()) 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()[1], "Error occurred parsing session inactivity 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], "session: option 'inactivity' could not be parsed: could not parse '-1' as a duration") } func TestShouldRaiseErrorWhenBadRememberMeDurationSet(t *testing.T) { @@ -443,7 +443,7 @@ func TestShouldRaiseErrorWhenBadRememberMeDurationSet(t *testing.T) { assert.False(t, validator.HasWarnings()) 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) { diff --git a/internal/configuration/validator/storage.go b/internal/configuration/validator/storage.go index bbed3e6ae..0ff673eae 100644 --- a/internal/configuration/validator/storage.go +++ b/internal/configuration/validator/storage.go @@ -10,66 +10,66 @@ import ( ) // ValidateStorage validates storage configuration. -func ValidateStorage(configuration schema.StorageConfiguration, validator *schema.StructValidator) { - if configuration.Local == nil && configuration.MySQL == nil && configuration.PostgreSQL == nil { +func ValidateStorage(config schema.StorageConfiguration, validator *schema.StructValidator) { + if config.Local == nil && config.MySQL == nil && config.PostgreSQL == nil { validator.Push(errors.New(errStrStorage)) } switch { - case configuration.MySQL != nil: - validateSQLConfiguration(&configuration.MySQL.SQLStorageConfiguration, validator, "mysql") - case configuration.PostgreSQL != nil: - validatePostgreSQLConfiguration(configuration.PostgreSQL, validator) - case configuration.Local != nil: - validateLocalStorageConfiguration(configuration.Local, validator) + case config.MySQL != nil: + validateSQLConfiguration(&config.MySQL.SQLStorageConfiguration, validator, "mysql") + case config.PostgreSQL != nil: + validatePostgreSQLConfiguration(config.PostgreSQL, validator) + case config.Local != nil: + validateLocalStorageConfiguration(config.Local, validator) } - if configuration.EncryptionKey == "" { + if config.EncryptionKey == "" { validator.Push(errors.New(errStrStorageEncryptionKeyMustBeProvided)) - } else if len(configuration.EncryptionKey) < 20 { + } else if len(config.EncryptionKey) < 20 { validator.Push(errors.New(errStrStorageEncryptionKeyTooShort)) } } -func validateSQLConfiguration(configuration *schema.SQLStorageConfiguration, validator *schema.StructValidator, provider string) { - if configuration.Timeout == 0 { - configuration.Timeout = schema.DefaultSQLStorageConfiguration.Timeout +func validateSQLConfiguration(config *schema.SQLStorageConfiguration, validator *schema.StructValidator, provider string) { + if config.Timeout == 0 { + config.Timeout = schema.DefaultSQLStorageConfiguration.Timeout } - if configuration.Host == "" { + if config.Host == "" { validator.Push(fmt.Errorf(errFmtStorageOptionMustBeProvided, provider, "host")) } - if configuration.Username == "" || configuration.Password == "" { + if config.Username == "" || config.Password == "" { validator.Push(fmt.Errorf(errFmtStorageUserPassMustBeProvided, provider)) } - if configuration.Database == "" { + if config.Database == "" { validator.Push(fmt.Errorf(errFmtStorageOptionMustBeProvided, provider, "database")) } } -func validatePostgreSQLConfiguration(configuration *schema.PostgreSQLStorageConfiguration, validator *schema.StructValidator) { - validateSQLConfiguration(&configuration.SQLStorageConfiguration, validator, "postgres") +func validatePostgreSQLConfiguration(config *schema.PostgreSQLStorageConfiguration, validator *schema.StructValidator) { + validateSQLConfiguration(&config.SQLStorageConfiguration, validator, "postgres") - if configuration.Schema == "" { - configuration.Schema = schema.DefaultPostgreSQLStorageConfiguration.Schema + if config.Schema == "" { + config.Schema = schema.DefaultPostgreSQLStorageConfiguration.Schema } // Deprecated. TODO: Remove in v4.36.0. - if configuration.SSLMode != "" && configuration.SSL.Mode == "" { - configuration.SSL.Mode = configuration.SSLMode + if config.SSLMode != "" && config.SSL.Mode == "" { + config.SSL.Mode = config.SSLMode } - if configuration.SSL.Mode == "" { - configuration.SSL.Mode = schema.DefaultPostgreSQLStorageConfiguration.SSL.Mode - } else if !utils.IsStringInSlice(configuration.SSL.Mode, storagePostgreSQLValidSSLModes) { - validator.Push(fmt.Errorf(errFmtStoragePostgreSQLInvalidSSLMode, configuration.SSL.Mode, strings.Join(storagePostgreSQLValidSSLModes, "', '"))) + if config.SSL.Mode == "" { + config.SSL.Mode = schema.DefaultPostgreSQLStorageConfiguration.SSL.Mode + } else if !utils.IsStringInSlice(config.SSL.Mode, validStoragePostgreSQLSSLModes) { + validator.Push(fmt.Errorf(errFmtStoragePostgreSQLInvalidSSLMode, strings.Join(validStoragePostgreSQLSSLModes, "', '"), config.SSL.Mode)) } } -func validateLocalStorageConfiguration(configuration *schema.LocalStorageConfiguration, validator *schema.StructValidator) { - if configuration.Path == "" { +func validateLocalStorageConfiguration(config *schema.LocalStorageConfiguration, validator *schema.StructValidator) { + if config.Path == "" { validator.Push(fmt.Errorf(errFmtStorageOptionMustBeProvided, "local", "path")) } } diff --git a/internal/configuration/validator/storage_test.go b/internal/configuration/validator/storage_test.go index 78bbc27db..2d1cd8e50 100644 --- a/internal/configuration/validator/storage_test.go +++ b/internal/configuration/validator/storage_test.go @@ -10,24 +10,24 @@ import ( type StorageSuite struct { suite.Suite - configuration schema.StorageConfiguration - validator *schema.StructValidator + config schema.StorageConfiguration + validator *schema.StructValidator } func (suite *StorageSuite) SetupTest() { suite.validator = schema.NewStructValidator() - suite.configuration.EncryptionKey = testEncryptionKey - suite.configuration.Local = nil - suite.configuration.PostgreSQL = nil - suite.configuration.MySQL = nil + suite.config.EncryptionKey = testEncryptionKey + suite.config.Local = nil + suite.config.PostgreSQL = nil + suite.config.MySQL = nil } func (suite *StorageSuite) TestShouldValidateOneStorageIsConfigured() { - suite.configuration.Local = nil - suite.configuration.PostgreSQL = nil - suite.configuration.MySQL = nil + suite.config.Local = nil + suite.config.PostgreSQL = 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.Errors(), 1) @@ -35,37 +35,37 @@ func (suite *StorageSuite) TestShouldValidateOneStorageIsConfigured() { } func (suite *StorageSuite) TestShouldValidateLocalPathIsProvided() { - suite.configuration.Local = &schema.LocalStorageConfiguration{ + suite.config.Local = &schema.LocalStorageConfiguration{ Path: "", } - ValidateStorage(suite.configuration, suite.validator) + ValidateStorage(suite.config, suite.validator) suite.Require().Len(suite.validator.Warnings(), 0) 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.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.Errors(), 0) } func (suite *StorageSuite) TestShouldValidateMySQLHostUsernamePasswordAndDatabaseAreProvided() { - suite.configuration.MySQL = &schema.MySQLStorageConfiguration{} - ValidateStorage(suite.configuration, suite.validator) + suite.config.MySQL = &schema.MySQLStorageConfiguration{} + ValidateStorage(suite.config, suite.validator) 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()[1], "storage: mysql: 'username' and 'password' configuration options must be provided") - suite.Assert().EqualError(suite.validator.Errors()[2], "storage: mysql: 'database' 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: option 'username' and 'password' are required") + suite.Assert().EqualError(suite.validator.Errors()[2], "storage: mysql: option 'database' is required") suite.validator.Clear() - suite.configuration.MySQL = &schema.MySQLStorageConfiguration{ + suite.config.MySQL = &schema.MySQLStorageConfiguration{ SQLStorageConfiguration: schema.SQLStorageConfiguration{ Host: "localhost", Username: "myuser", @@ -73,24 +73,24 @@ func (suite *StorageSuite) TestShouldValidateMySQLHostUsernamePasswordAndDatabas Database: "database", }, } - ValidateStorage(suite.configuration, suite.validator) + ValidateStorage(suite.config, suite.validator) suite.Require().Len(suite.validator.Warnings(), 0) suite.Require().Len(suite.validator.Errors(), 0) } func (suite *StorageSuite) TestShouldValidatePostgreSQLHostUsernamePasswordAndDatabaseAreProvided() { - suite.configuration.PostgreSQL = &schema.PostgreSQLStorageConfiguration{} - suite.configuration.MySQL = nil - ValidateStorage(suite.configuration, suite.validator) + suite.config.PostgreSQL = &schema.PostgreSQLStorageConfiguration{} + suite.config.MySQL = nil + ValidateStorage(suite.config, suite.validator) 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()[1], "storage: postgres: 'username' and 'password' configuration options must be provided") - suite.Assert().EqualError(suite.validator.Errors()[2], "storage: postgres: 'database' 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: option 'username' and 'password' are required") + suite.Assert().EqualError(suite.validator.Errors()[2], "storage: postgres: option 'database' is required") suite.validator.Clear() - suite.configuration.PostgreSQL = &schema.PostgreSQLStorageConfiguration{ + suite.config.PostgreSQL = &schema.PostgreSQLStorageConfiguration{ SQLStorageConfiguration: schema.SQLStorageConfiguration{ Host: "postgre", Username: "myuser", @@ -98,14 +98,14 @@ func (suite *StorageSuite) TestShouldValidatePostgreSQLHostUsernamePasswordAndDa Database: "database", }, } - ValidateStorage(suite.configuration, suite.validator) + ValidateStorage(suite.config, suite.validator) suite.Assert().Len(suite.validator.Warnings(), 0) suite.Assert().Len(suite.validator.Errors(), 0) } func (suite *StorageSuite) TestShouldValidatePostgresSSLModeAndSchemaDefaults() { - suite.configuration.PostgreSQL = &schema.PostgreSQLStorageConfiguration{ + suite.config.PostgreSQL = &schema.PostgreSQLStorageConfiguration{ SQLStorageConfiguration: schema.SQLStorageConfiguration{ Host: "db1", 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.Errors(), 0) - suite.Assert().Equal("disable", suite.configuration.PostgreSQL.SSL.Mode) - suite.Assert().Equal("public", suite.configuration.PostgreSQL.Schema) + suite.Assert().Equal("disable", suite.config.PostgreSQL.SSL.Mode) + suite.Assert().Equal("public", suite.config.PostgreSQL.Schema) } func (suite *StorageSuite) TestShouldValidatePostgresDefaultsDontOverrideConfiguration() { - suite.configuration.PostgreSQL = &schema.PostgreSQLStorageConfiguration{ + suite.config.PostgreSQL = &schema.PostgreSQLStorageConfiguration{ SQLStorageConfiguration: schema.SQLStorageConfiguration{ Host: "db1", 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.Errors(), 0) - suite.Assert().Equal("require", suite.configuration.PostgreSQL.SSL.Mode) - suite.Assert().Equal("authelia", suite.configuration.PostgreSQL.Schema) + suite.Assert().Equal("require", suite.config.PostgreSQL.SSL.Mode) + suite.Assert().Equal("authelia", suite.config.PostgreSQL.Schema) } func (suite *StorageSuite) TestShouldValidatePostgresSSLModeMustBeValid() { - suite.configuration.PostgreSQL = &schema.PostgreSQLStorageConfiguration{ + suite.config.PostgreSQL = &schema.PostgreSQLStorageConfiguration{ SQLStorageConfiguration: schema.SQLStorageConfiguration{ Host: "db2", 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.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. func (suite *StorageSuite) TestShouldValidatePostgresSSLModeMustBeMappedForDeprecations() { - suite.configuration.PostgreSQL = &schema.PostgreSQLStorageConfiguration{ + suite.config.PostgreSQL = &schema.PostgreSQLStorageConfiguration{ SQLStorageConfiguration: schema.SQLStorageConfiguration{ Host: "pg", Username: "myuser", @@ -178,38 +178,38 @@ func (suite *StorageSuite) TestShouldValidatePostgresSSLModeMustBeMappedForDepre SSLMode: "require", } - ValidateStorage(suite.configuration, suite.validator) + ValidateStorage(suite.config, suite.validator) suite.Assert().Len(suite.validator.Warnings(), 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() { - suite.configuration.EncryptionKey = "" - suite.configuration.Local = &schema.LocalStorageConfiguration{ + suite.config.EncryptionKey = "" + suite.config.Local = &schema.LocalStorageConfiguration{ 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.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() { - suite.configuration.EncryptionKey = "abc" - suite.configuration.Local = &schema.LocalStorageConfiguration{ + suite.config.EncryptionKey = "abc" + suite.config.Local = &schema.LocalStorageConfiguration{ 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.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) { diff --git a/internal/configuration/validator/theme.go b/internal/configuration/validator/theme.go index 758fe4546..f6c1a68d7 100644 --- a/internal/configuration/validator/theme.go +++ b/internal/configuration/validator/theme.go @@ -2,20 +2,19 @@ package validator import ( "fmt" - "regexp" + "strings" "github.com/authelia/authelia/v4/internal/configuration/schema" + "github.com/authelia/authelia/v4/internal/utils" ) // ValidateTheme validates and update Theme configuration. -func ValidateTheme(configuration *schema.Configuration, validator *schema.StructValidator) { - if configuration.Theme == "" { - configuration.Theme = "light" +func ValidateTheme(config *schema.Configuration, validator *schema.StructValidator) { + if config.Theme == "" { + config.Theme = "light" } - validThemes := regexp.MustCompile("light|dark|grey|auto") - - if !validThemes.MatchString(configuration.Theme) { - validator.Push(fmt.Errorf("Theme: %s is not valid, valid themes are: \"light\", \"dark\", \"grey\" or \"auto\"", configuration.Theme)) + if !utils.IsStringInSlice(config.Theme, validThemeNames) { + validator.Push(fmt.Errorf(errFmtThemeName, strings.Join(validThemeNames, "', '"), config.Theme)) } } diff --git a/internal/configuration/validator/theme_test.go b/internal/configuration/validator/theme_test.go index 8ffd89be4..df279cb80 100644 --- a/internal/configuration/validator/theme_test.go +++ b/internal/configuration/validator/theme_test.go @@ -10,33 +10,33 @@ import ( type Theme struct { suite.Suite - configuration *schema.Configuration - validator *schema.StructValidator + config *schema.Configuration + validator *schema.StructValidator } func (suite *Theme) SetupTest() { suite.validator = schema.NewStructValidator() - suite.configuration = &schema.Configuration{ + suite.config = &schema.Configuration{ Theme: "light", } } 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.HasErrors()) } 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.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) { diff --git a/internal/configuration/validator/totp.go b/internal/configuration/validator/totp.go index 1310b6711..e29b3dea2 100644 --- a/internal/configuration/validator/totp.go +++ b/internal/configuration/validator/totp.go @@ -9,40 +9,40 @@ import ( ) // ValidateTOTP validates and update TOTP configuration. -func ValidateTOTP(configuration *schema.Configuration, validator *schema.StructValidator) { - if configuration.TOTP == nil { - configuration.TOTP = &schema.DefaultTOTPConfiguration +func ValidateTOTP(config *schema.Configuration, validator *schema.StructValidator) { + if config.TOTP == nil { + config.TOTP = &schema.DefaultTOTPConfiguration return } - if configuration.TOTP.Issuer == "" { - configuration.TOTP.Issuer = schema.DefaultTOTPConfiguration.Issuer + if config.TOTP.Issuer == "" { + config.TOTP.Issuer = schema.DefaultTOTPConfiguration.Issuer } - if configuration.TOTP.Algorithm == "" { - configuration.TOTP.Algorithm = schema.DefaultTOTPConfiguration.Algorithm + if config.TOTP.Algorithm == "" { + config.TOTP.Algorithm = schema.DefaultTOTPConfiguration.Algorithm } 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) { - validator.Push(fmt.Errorf(errFmtTOTPInvalidAlgorithm, configuration.TOTP.Algorithm, strings.Join(schema.TOTPPossibleAlgorithms, ", "))) + if !utils.IsStringInSlice(config.TOTP.Algorithm, schema.TOTPPossibleAlgorithms) { + validator.Push(fmt.Errorf(errFmtTOTPInvalidAlgorithm, strings.Join(schema.TOTPPossibleAlgorithms, "', '"), config.TOTP.Algorithm)) } } - if configuration.TOTP.Period == 0 { - configuration.TOTP.Period = schema.DefaultTOTPConfiguration.Period - } else if configuration.TOTP.Period < 15 { - validator.Push(fmt.Errorf(errFmtTOTPInvalidPeriod, configuration.TOTP.Period)) + if config.TOTP.Period == 0 { + config.TOTP.Period = schema.DefaultTOTPConfiguration.Period + } else if config.TOTP.Period < 15 { + validator.Push(fmt.Errorf(errFmtTOTPInvalidPeriod, config.TOTP.Period)) } - if configuration.TOTP.Digits == 0 { - configuration.TOTP.Digits = schema.DefaultTOTPConfiguration.Digits - } else if configuration.TOTP.Digits != 6 && configuration.TOTP.Digits != 8 { - validator.Push(fmt.Errorf(errFmtTOTPInvalidDigits, configuration.TOTP.Digits)) + if config.TOTP.Digits == 0 { + config.TOTP.Digits = schema.DefaultTOTPConfiguration.Digits + } else if config.TOTP.Digits != 6 && config.TOTP.Digits != 8 { + validator.Push(fmt.Errorf(errFmtTOTPInvalidDigits, config.TOTP.Digits)) } - if configuration.TOTP.Skew == nil { - configuration.TOTP.Skew = schema.DefaultTOTPConfiguration.Skew + if config.TOTP.Skew == nil { + config.TOTP.Skew = schema.DefaultTOTPConfiguration.Skew } } diff --git a/internal/configuration/validator/totp_test.go b/internal/configuration/validator/totp_test.go index 497284f35..463e30b50 100644 --- a/internal/configuration/validator/totp_test.go +++ b/internal/configuration/validator/totp_test.go @@ -53,7 +53,7 @@ func TestShouldRaiseErrorWhenInvalidTOTPAlgorithm(t *testing.T) { ValidateTOTP(config, validator) 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) { diff --git a/internal/ntp/ntp_test.go b/internal/ntp/ntp_test.go index 756568bdd..624d13b5e 100644 --- a/internal/ntp/ntp_test.go +++ b/internal/ntp/ntp_test.go @@ -10,16 +10,19 @@ import ( ) func TestShouldCheckNTP(t *testing.T) { - config := schema.NTPConfiguration{ - Address: "time.cloudflare.com:123", - Version: 4, - MaximumDesync: "3s", - DisableStartupCheck: false, + config := &schema.Configuration{ + NTP: &schema.NTPConfiguration{ + Address: "time.cloudflare.com:123", + Version: 4, + MaximumDesync: "3s", + 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()) } diff --git a/internal/suites/CLI/configuration.yml b/internal/suites/CLI/configuration.yml index de0f31cc6..9a978c5ae 100644 --- a/internal/suites/CLI/configuration.yml +++ b/internal/suites/CLI/configuration.yml @@ -41,6 +41,21 @@ access_control: policy: two_factor - domain: "singlefactor.example.com" 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: filesystem: diff --git a/internal/suites/suite_cli_test.go b/internal/suites/suite_cli_test.go index 7ba377284..83768345f 100644 --- a/internal/suites/suite_cli_test.go +++ b/internal/suites/suite_cli_test.go @@ -66,15 +66,15 @@ func (s *CLISuite) TestShouldPrintVersion() { } 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().Contains(output, "Configuration parsed successfully without errors") + s.Assert().Contains(output, "Configuration parsed and loaded successfully without errors.") } func (s *CLISuite) TestShouldFailValidateConfig() { - output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "validate-config", "/config/invalid.yml"}) - s.Assert().NotNil(err) - s.Assert().Contains(output, "Error Loading Configuration: stat /config/invalid.yml: no such file or directory") + output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "validate-config", "--config", "/config/invalid.yml"}) + s.Assert().NoError(err) + 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() { @@ -168,12 +168,12 @@ func (s *CLISuite) TestStorageShouldShowErrWithoutConfig() { output, err := s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "schema-info"}) 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"}) 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() { @@ -382,6 +382,94 @@ func (s *CLISuite) TestStorage05ShouldMigrateDown() { 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) { if testing.Short() { t.Skip("skipping suite test in short mode") diff --git a/internal/utils/certificates_test.go b/internal/utils/certificates_test.go index 697f174a5..2cf103076 100644 --- a/internal/utils/certificates_test.go +++ b/internal/utils/certificates_test.go @@ -68,12 +68,12 @@ func TestShouldReturnZeroAndErrorOnInvalidTLSVersions(t *testing.T) { version, err := TLSStringToTLSConfigVersion("TLS1.4") assert.Error(t, err) assert.Equal(t, uint16(0), version) - assert.EqualError(t, err, "supplied TLS version isn't supported") + assert.EqualError(t, err, "supplied tls version isn't supported") version, err = TLSStringToTLSConfigVersion("SSL3.0") assert.Error(t, err) assert.Equal(t, uint16(0), version) - assert.EqualError(t, err, "supplied TLS version isn't supported") + assert.EqualError(t, err, "supplied tls version isn't supported") } func TestShouldReturnErrWhenX509DirectoryNotExist(t *testing.T) { diff --git a/internal/utils/const.go b/internal/utils/const.go index bb649256e..56f5011fa 100644 --- a/internal/utils/const.go +++ b/internal/utils/const.go @@ -73,4 +73,4 @@ var htmlEscaper = strings.NewReplacer( var ErrTimeoutReached = errors.New("timeout reached") // 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") diff --git a/internal/utils/time.go b/internal/utils/time.go index 7b03d5c11..4d1f71778 100644 --- a/internal/utils/time.go +++ b/internal/utils/time.go @@ -38,13 +38,13 @@ func ParseDurationString(input string) (time.Duration, error) { case input == "0" || len(matches) == 3: seconds, err := strconv.Atoi(input) 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 case input != "": // 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 diff --git a/internal/utils/time_test.go b/internal/utils/time_test.go index a4104720b..f2e208c8b 100644 --- a/internal/utils/time_test.go +++ b/internal/utils/time_test.go @@ -47,25 +47,25 @@ func TestShouldParseSecondsString(t *testing.T) { func TestShouldNotParseDurationStringWithOutOfOrderQuantitiesAndUnits(t *testing.T) { 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) } func TestShouldNotParseBadDurationString(t *testing.T) { 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) } func TestShouldNotParseDurationStringWithMultiValueUnits(t *testing.T) { 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) } func TestShouldNotParseDurationStringWithLeadingZero(t *testing.T) { 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) }