// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package configutil import ( "errors" "fmt" "strconv" "strings" "time" "github.com/hashicorp/go-multierror" "github.com/hashicorp/go-secure-stdlib/parseutil" "github.com/hashicorp/hcl" "github.com/hashicorp/hcl/hcl/ast" ) const ( UserLockoutThresholdDefault = 5 UserLockoutDurationDefault = 15 * time.Minute UserLockoutCounterResetDefault = 15 * time.Minute DisableUserLockoutDefault = false ) type UserLockout struct { Type string LockoutThreshold uint64 `hcl:"-"` LockoutThresholdRaw interface{} `hcl:"lockout_threshold"` LockoutDuration time.Duration `hcl:"-"` LockoutDurationRaw interface{} `hcl:"lockout_duration"` LockoutCounterReset time.Duration `hcl:"-"` LockoutCounterResetRaw interface{} `hcl:"lockout_counter_reset"` DisableLockout bool `hcl:"-"` DisableLockoutRaw interface{} `hcl:"disable_lockout"` } func GetSupportedUserLockoutsAuthMethods() []string { return []string{"userpass", "approle", "ldap"} } func ParseUserLockouts(result *SharedConfig, list *ast.ObjectList) error { var err error result.UserLockouts = make([]*UserLockout, 0, len(list.Items)) userLockoutsMap := make(map[string]*UserLockout) for i, item := range list.Items { var userLockoutConfig UserLockout if err := hcl.DecodeObject(&userLockoutConfig, item.Val); err != nil { return multierror.Prefix(err, fmt.Sprintf("userLockouts.%d:", i)) } // Base values { switch { case userLockoutConfig.Type != "": case len(item.Keys) == 1: userLockoutConfig.Type = strings.ToLower(item.Keys[0].Token.Value().(string)) default: return multierror.Prefix(errors.New("auth type for user lockout must be specified, if it applies to all auth methods specify \"all\" "), fmt.Sprintf("user_lockouts.%d:", i)) } userLockoutConfig.Type = strings.ToLower(userLockoutConfig.Type) // Supported auth methods for user lockout configuration: ldap, approle, userpass // "all" is used to apply the configuration to all supported auth methods switch userLockoutConfig.Type { case "all", "ldap", "approle", "userpass": result.found(userLockoutConfig.Type, userLockoutConfig.Type) default: return multierror.Prefix(fmt.Errorf("unsupported auth type %q", userLockoutConfig.Type), fmt.Sprintf("user_lockouts.%d:", i)) } } // Lockout Parameters // Not setting raw entries to nil here as soon as they are parsed // as they are used to set the missing user lockout configuration values later. { if userLockoutConfig.LockoutThresholdRaw != nil { userLockoutThresholdString := fmt.Sprintf("%v", userLockoutConfig.LockoutThresholdRaw) if userLockoutConfig.LockoutThreshold, err = strconv.ParseUint(userLockoutThresholdString, 10, 64); err != nil { return multierror.Prefix(fmt.Errorf("error parsing lockout_threshold: %w", err), fmt.Sprintf("user_lockouts.%d", i)) } } if userLockoutConfig.LockoutDurationRaw != nil { if userLockoutConfig.LockoutDuration, err = parseutil.ParseDurationSecond(userLockoutConfig.LockoutDurationRaw); err != nil { return multierror.Prefix(fmt.Errorf("error parsing lockout_duration: %w", err), fmt.Sprintf("user_lockouts.%d", i)) } if userLockoutConfig.LockoutDuration < 0 { return multierror.Prefix(errors.New("lockout_duration cannot be negative"), fmt.Sprintf("user_lockouts.%d", i)) } } if userLockoutConfig.LockoutCounterResetRaw != nil { if userLockoutConfig.LockoutCounterReset, err = parseutil.ParseDurationSecond(userLockoutConfig.LockoutCounterResetRaw); err != nil { return multierror.Prefix(fmt.Errorf("error parsing lockout_counter_reset: %w", err), fmt.Sprintf("user_lockouts.%d", i)) } if userLockoutConfig.LockoutCounterReset < 0 { return multierror.Prefix(errors.New("lockout_counter_reset cannot be negative"), fmt.Sprintf("user_lockouts.%d", i)) } } if userLockoutConfig.DisableLockoutRaw != nil { if userLockoutConfig.DisableLockout, err = parseutil.ParseBool(userLockoutConfig.DisableLockoutRaw); err != nil { return multierror.Prefix(fmt.Errorf("invalid value for disable_lockout: %w", err), fmt.Sprintf("user_lockouts.%d", i)) } } } userLockoutsMap[userLockoutConfig.Type] = &userLockoutConfig } // Use raw entries to set values for user lockout configurations fields // that were not configured using config file. // The raw entries would mean that the entry was configured by the user using the config file. // If any of these fields are not configured using the config file (missing fields), // we set values for these fields with defaults // The issue with not being able to use non-raw entries is because of fields lockout threshold // and disable lockout. We cannot differentiate using non-raw entries if the user configured these fields // with values (0 and false) or if the the user did not configure these values in config file at all. // The raw fields are set to nil after setting missing values in setNilValuesForRawUserLockoutFields function userLockoutsMap = setMissingUserLockoutValuesInMap(userLockoutsMap) for _, userLockoutValues := range userLockoutsMap { result.UserLockouts = append(result.UserLockouts, userLockoutValues) } return nil } // setUserLockoutValueAllInMap sets default user lockout values for key "all" (all auth methods) // for user lockout fields that are not configured using config file func setUserLockoutValueAllInMap(userLockoutAll *UserLockout) *UserLockout { if userLockoutAll.Type == "" { userLockoutAll.Type = "all" } if userLockoutAll.LockoutThresholdRaw == nil { userLockoutAll.LockoutThreshold = UserLockoutThresholdDefault } if userLockoutAll.LockoutDurationRaw == nil { userLockoutAll.LockoutDuration = UserLockoutDurationDefault } if userLockoutAll.LockoutCounterResetRaw == nil { userLockoutAll.LockoutCounterReset = UserLockoutCounterResetDefault } if userLockoutAll.DisableLockoutRaw == nil { userLockoutAll.DisableLockout = DisableUserLockoutDefault } return setNilValuesForRawUserLockoutFields(userLockoutAll) } // setDefaultUserLockoutValuesInMap sets missing user lockout fields for auth methods // with default values (from key "all") that are not configured using config file func setMissingUserLockoutValuesInMap(userLockoutsMap map[string]*UserLockout) map[string]*UserLockout { // set values for "all" key with default values for "all" user lockout fields that are not configured // the "all" key values will be used as default values for other auth methods userLockoutAll, ok := userLockoutsMap["all"] switch ok { case true: userLockoutsMap["all"] = setUserLockoutValueAllInMap(userLockoutAll) default: userLockoutsMap["all"] = setUserLockoutValueAllInMap(&UserLockout{}) } for _, userLockoutAuth := range userLockoutsMap { if userLockoutAuth.Type == "all" { continue } // set missing values if userLockoutAuth.LockoutThresholdRaw == nil { userLockoutAuth.LockoutThreshold = userLockoutsMap["all"].LockoutThreshold } if userLockoutAuth.LockoutDurationRaw == nil { userLockoutAuth.LockoutDuration = userLockoutsMap["all"].LockoutDuration } if userLockoutAuth.LockoutCounterResetRaw == nil { userLockoutAuth.LockoutCounterReset = userLockoutsMap["all"].LockoutCounterReset } if userLockoutAuth.DisableLockoutRaw == nil { userLockoutAuth.DisableLockout = userLockoutsMap["all"].DisableLockout } userLockoutAuth = setNilValuesForRawUserLockoutFields(userLockoutAuth) userLockoutsMap[userLockoutAuth.Type] = userLockoutAuth } return userLockoutsMap } // setNilValuesForRawUserLockoutFields sets nil values for user lockout Raw fields func setNilValuesForRawUserLockoutFields(userLockout *UserLockout) *UserLockout { userLockout.LockoutThresholdRaw = nil userLockout.LockoutDurationRaw = nil userLockout.LockoutCounterResetRaw = nil userLockout.DisableLockoutRaw = nil return userLockout }