mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-10-30 02:02:43 +00:00
Vault 8305 Prevent Brute Forcing in Auth methods : Setting user lockout configuration (#17338)
* config file changes * lockout config changes * auth tune r/w and auth tune * removing changes at enable * removing q.Q * go mod tidy * removing comments * changing struct name for config file * fixing mount tune * adding test file for user lockout * fixing comments and add changelog * addressing comments * fixing mount table updates * updating consts in auth_tune * small fixes * adding hcl parse test * fixing config compare * fixing github comments * optimize userlockouts.go * fixing test * minor changes * adding comments * adding sort to flaky test * fix flaky test
This commit is contained in:
@@ -254,20 +254,20 @@ type MountInput struct {
|
||||
}
|
||||
|
||||
type MountConfigInput struct {
|
||||
Options map[string]string `json:"options" mapstructure:"options"`
|
||||
DefaultLeaseTTL string `json:"default_lease_ttl" mapstructure:"default_lease_ttl"`
|
||||
Description *string `json:"description,omitempty" mapstructure:"description"`
|
||||
MaxLeaseTTL string `json:"max_lease_ttl" mapstructure:"max_lease_ttl"`
|
||||
ForceNoCache bool `json:"force_no_cache" mapstructure:"force_no_cache"`
|
||||
AuditNonHMACRequestKeys []string `json:"audit_non_hmac_request_keys,omitempty" mapstructure:"audit_non_hmac_request_keys"`
|
||||
AuditNonHMACResponseKeys []string `json:"audit_non_hmac_response_keys,omitempty" mapstructure:"audit_non_hmac_response_keys"`
|
||||
ListingVisibility string `json:"listing_visibility,omitempty" mapstructure:"listing_visibility"`
|
||||
PassthroughRequestHeaders []string `json:"passthrough_request_headers,omitempty" mapstructure:"passthrough_request_headers"`
|
||||
AllowedResponseHeaders []string `json:"allowed_response_headers,omitempty" mapstructure:"allowed_response_headers"`
|
||||
TokenType string `json:"token_type,omitempty" mapstructure:"token_type"`
|
||||
AllowedManagedKeys []string `json:"allowed_managed_keys,omitempty" mapstructure:"allowed_managed_keys"`
|
||||
PluginVersion string `json:"plugin_version,omitempty"`
|
||||
|
||||
Options map[string]string `json:"options" mapstructure:"options"`
|
||||
DefaultLeaseTTL string `json:"default_lease_ttl" mapstructure:"default_lease_ttl"`
|
||||
Description *string `json:"description,omitempty" mapstructure:"description"`
|
||||
MaxLeaseTTL string `json:"max_lease_ttl" mapstructure:"max_lease_ttl"`
|
||||
ForceNoCache bool `json:"force_no_cache" mapstructure:"force_no_cache"`
|
||||
AuditNonHMACRequestKeys []string `json:"audit_non_hmac_request_keys,omitempty" mapstructure:"audit_non_hmac_request_keys"`
|
||||
AuditNonHMACResponseKeys []string `json:"audit_non_hmac_response_keys,omitempty" mapstructure:"audit_non_hmac_response_keys"`
|
||||
ListingVisibility string `json:"listing_visibility,omitempty" mapstructure:"listing_visibility"`
|
||||
PassthroughRequestHeaders []string `json:"passthrough_request_headers,omitempty" mapstructure:"passthrough_request_headers"`
|
||||
AllowedResponseHeaders []string `json:"allowed_response_headers,omitempty" mapstructure:"allowed_response_headers"`
|
||||
TokenType string `json:"token_type,omitempty" mapstructure:"token_type"`
|
||||
AllowedManagedKeys []string `json:"allowed_managed_keys,omitempty" mapstructure:"allowed_managed_keys"`
|
||||
PluginVersion string `json:"plugin_version,omitempty"`
|
||||
UserLockoutConfig *UserLockoutConfigInput `json:"user_lockout_config,omitempty"`
|
||||
// Deprecated: This field will always be blank for newer server responses.
|
||||
PluginName string `json:"plugin_name,omitempty" mapstructure:"plugin_name"`
|
||||
}
|
||||
@@ -289,21 +289,35 @@ type MountOutput struct {
|
||||
}
|
||||
|
||||
type MountConfigOutput struct {
|
||||
DefaultLeaseTTL int `json:"default_lease_ttl" mapstructure:"default_lease_ttl"`
|
||||
MaxLeaseTTL int `json:"max_lease_ttl" mapstructure:"max_lease_ttl"`
|
||||
ForceNoCache bool `json:"force_no_cache" mapstructure:"force_no_cache"`
|
||||
AuditNonHMACRequestKeys []string `json:"audit_non_hmac_request_keys,omitempty" mapstructure:"audit_non_hmac_request_keys"`
|
||||
AuditNonHMACResponseKeys []string `json:"audit_non_hmac_response_keys,omitempty" mapstructure:"audit_non_hmac_response_keys"`
|
||||
ListingVisibility string `json:"listing_visibility,omitempty" mapstructure:"listing_visibility"`
|
||||
PassthroughRequestHeaders []string `json:"passthrough_request_headers,omitempty" mapstructure:"passthrough_request_headers"`
|
||||
AllowedResponseHeaders []string `json:"allowed_response_headers,omitempty" mapstructure:"allowed_response_headers"`
|
||||
TokenType string `json:"token_type,omitempty" mapstructure:"token_type"`
|
||||
AllowedManagedKeys []string `json:"allowed_managed_keys,omitempty" mapstructure:"allowed_managed_keys"`
|
||||
|
||||
DefaultLeaseTTL int `json:"default_lease_ttl" mapstructure:"default_lease_ttl"`
|
||||
MaxLeaseTTL int `json:"max_lease_ttl" mapstructure:"max_lease_ttl"`
|
||||
ForceNoCache bool `json:"force_no_cache" mapstructure:"force_no_cache"`
|
||||
AuditNonHMACRequestKeys []string `json:"audit_non_hmac_request_keys,omitempty" mapstructure:"audit_non_hmac_request_keys"`
|
||||
AuditNonHMACResponseKeys []string `json:"audit_non_hmac_response_keys,omitempty" mapstructure:"audit_non_hmac_response_keys"`
|
||||
ListingVisibility string `json:"listing_visibility,omitempty" mapstructure:"listing_visibility"`
|
||||
PassthroughRequestHeaders []string `json:"passthrough_request_headers,omitempty" mapstructure:"passthrough_request_headers"`
|
||||
AllowedResponseHeaders []string `json:"allowed_response_headers,omitempty" mapstructure:"allowed_response_headers"`
|
||||
TokenType string `json:"token_type,omitempty" mapstructure:"token_type"`
|
||||
AllowedManagedKeys []string `json:"allowed_managed_keys,omitempty" mapstructure:"allowed_managed_keys"`
|
||||
UserLockoutConfig *UserLockoutConfigOutput `json:"user_lockout_config,omitempty"`
|
||||
// Deprecated: This field will always be blank for newer server responses.
|
||||
PluginName string `json:"plugin_name,omitempty" mapstructure:"plugin_name"`
|
||||
}
|
||||
|
||||
type UserLockoutConfigInput struct {
|
||||
LockoutThreshold string `json:"lockout_threshold,omitempty" structs:"lockout_threshold" mapstructure:"lockout_threshold"`
|
||||
LockoutDuration string `json:"lockout_duration,omitempty" structs:"lockout_duration" mapstructure:"lockout_duration"`
|
||||
LockoutCounterResetDuration string `json:"lockout_counter_reset_duration,omitempty" structs:"lockout_counter_reset_duration" mapstructure:"lockout_counter_reset_duration"`
|
||||
DisableLockout *bool `json:"lockout_disable,omitempty" structs:"lockout_disable" mapstructure:"lockout_disable"`
|
||||
}
|
||||
|
||||
type UserLockoutConfigOutput struct {
|
||||
LockoutThreshold uint `json:"lockout_threshold,omitempty" structs:"lockout_threshold" mapstructure:"lockout_threshold"`
|
||||
LockoutDuration int `json:"lockout_duration,omitempty" structs:"lockout_duration" mapstructure:"lockout_duration"`
|
||||
LockoutCounterReset int `json:"lockout_counter_reset,omitempty" structs:"lockout_counter_reset" mapstructure:"lockout_counter_reset"`
|
||||
DisableLockout *bool `json:"disable_lockout,omitempty" structs:"disable_lockout" mapstructure:"disable_lockout"`
|
||||
}
|
||||
|
||||
type MountMigrationOutput struct {
|
||||
MigrationID string `mapstructure:"migration_id"`
|
||||
}
|
||||
|
||||
3
changelog/17338.txt
Normal file
3
changelog/17338.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
```release-note:feature
|
||||
core: Add user lockout field to config and configuring this for auth mount using auth tune to prevent brute forcing in auth methods
|
||||
```
|
||||
@@ -20,18 +20,22 @@ var (
|
||||
type AuthTuneCommand struct {
|
||||
*BaseCommand
|
||||
|
||||
flagAuditNonHMACRequestKeys []string
|
||||
flagAuditNonHMACResponseKeys []string
|
||||
flagDefaultLeaseTTL time.Duration
|
||||
flagDescription string
|
||||
flagListingVisibility string
|
||||
flagMaxLeaseTTL time.Duration
|
||||
flagPassthroughRequestHeaders []string
|
||||
flagAllowedResponseHeaders []string
|
||||
flagOptions map[string]string
|
||||
flagTokenType string
|
||||
flagVersion int
|
||||
flagPluginVersion string
|
||||
flagAuditNonHMACRequestKeys []string
|
||||
flagAuditNonHMACResponseKeys []string
|
||||
flagDefaultLeaseTTL time.Duration
|
||||
flagDescription string
|
||||
flagListingVisibility string
|
||||
flagMaxLeaseTTL time.Duration
|
||||
flagPassthroughRequestHeaders []string
|
||||
flagAllowedResponseHeaders []string
|
||||
flagOptions map[string]string
|
||||
flagTokenType string
|
||||
flagVersion int
|
||||
flagPluginVersion string
|
||||
flagUserLockoutThreshold uint
|
||||
flagUserLockoutDuration time.Duration
|
||||
flagUserLockoutCounterResetDuration time.Duration
|
||||
flagUserLockoutDisable bool
|
||||
}
|
||||
|
||||
func (c *AuthTuneCommand) Synopsis() string {
|
||||
@@ -145,6 +149,41 @@ func (c *AuthTuneCommand) Flags() *FlagSets {
|
||||
Usage: "Select the version of the auth method to run. Not supported by all auth methods.",
|
||||
})
|
||||
|
||||
f.UintVar(&UintVar{
|
||||
Name: flagNameUserLockoutThreshold,
|
||||
Target: &c.flagUserLockoutThreshold,
|
||||
Usage: "The threshold for user lockout for this auth method. If unspecified, this " +
|
||||
"defaults to the Vault server's globally configured user lockout threshold, " +
|
||||
"or a previously configured value for the auth method.",
|
||||
})
|
||||
|
||||
f.DurationVar(&DurationVar{
|
||||
Name: flagNameUserLockoutDuration,
|
||||
Target: &c.flagUserLockoutDuration,
|
||||
Completion: complete.PredictAnything,
|
||||
Usage: "The user lockout duration for this auth method. If unspecified, this " +
|
||||
"defaults to the Vault server's globally configured user lockout duration, " +
|
||||
"or a previously configured value for the auth method.",
|
||||
})
|
||||
|
||||
f.DurationVar(&DurationVar{
|
||||
Name: flagNameUserLockoutCounterResetDuration,
|
||||
Target: &c.flagUserLockoutCounterResetDuration,
|
||||
Completion: complete.PredictAnything,
|
||||
Usage: "The user lockout counter reset duration for this auth method. If unspecified, this " +
|
||||
"defaults to the Vault server's globally configured user lockout counter reset duration, " +
|
||||
"or a previously configured value for the auth method.",
|
||||
})
|
||||
|
||||
f.BoolVar(&BoolVar{
|
||||
Name: flagNameUserLockoutDisable,
|
||||
Target: &c.flagUserLockoutDisable,
|
||||
Default: false,
|
||||
Usage: "Disable user lockout for this auth method. If unspecified, this " +
|
||||
"defaults to the Vault server's globally configured user lockout disable, " +
|
||||
"or a previously configured value for the auth method.",
|
||||
})
|
||||
|
||||
f.StringVar(&StringVar{
|
||||
Name: flagNamePluginVersion,
|
||||
Target: &c.flagPluginVersion,
|
||||
@@ -230,6 +269,24 @@ func (c *AuthTuneCommand) Run(args []string) int {
|
||||
if fl.Name == flagNameTokenType {
|
||||
mountConfigInput.TokenType = c.flagTokenType
|
||||
}
|
||||
switch fl.Name {
|
||||
case flagNameUserLockoutThreshold, flagNameUserLockoutDuration, flagNameUserLockoutCounterResetDuration, flagNameUserLockoutDisable:
|
||||
if mountConfigInput.UserLockoutConfig == nil {
|
||||
mountConfigInput.UserLockoutConfig = &api.UserLockoutConfigInput{}
|
||||
}
|
||||
}
|
||||
if fl.Name == flagNameUserLockoutThreshold {
|
||||
mountConfigInput.UserLockoutConfig.LockoutThreshold = strconv.FormatUint(uint64(c.flagUserLockoutThreshold), 10)
|
||||
}
|
||||
if fl.Name == flagNameUserLockoutDuration {
|
||||
mountConfigInput.UserLockoutConfig.LockoutDuration = ttlToAPI(c.flagUserLockoutDuration)
|
||||
}
|
||||
if fl.Name == flagNameUserLockoutCounterResetDuration {
|
||||
mountConfigInput.UserLockoutConfig.LockoutCounterResetDuration = ttlToAPI(c.flagUserLockoutCounterResetDuration)
|
||||
}
|
||||
if fl.Name == flagNameUserLockoutDisable {
|
||||
mountConfigInput.UserLockoutConfig.DisableLockout = &c.flagUserLockoutDisable
|
||||
}
|
||||
|
||||
if fl.Name == flagNamePluginVersion {
|
||||
mountConfigInput.PluginVersion = c.flagPluginVersion
|
||||
|
||||
@@ -126,6 +126,14 @@ const (
|
||||
flagNameAllowedManagedKeys = "allowed-managed-keys"
|
||||
// flagNamePluginVersion selects what version of a plugin should be used.
|
||||
flagNamePluginVersion = "plugin-version"
|
||||
// flagNameUserLockoutThreshold is the flag name used for tuning the auth mount lockout threshold parameter
|
||||
flagNameUserLockoutThreshold = "user-lockout-threshold"
|
||||
// flagNameUserLockoutDuration is the flag name used for tuning the auth mount lockout duration parameter
|
||||
flagNameUserLockoutDuration = "user-lockout-duration"
|
||||
// flagNameUserLockoutCounterResetDuration is the flag name used for tuning the auth mount lockout counter reset parameter
|
||||
flagNameUserLockoutCounterResetDuration = "user-lockout-counter-reset-duration"
|
||||
// flagNameUserLockoutDisable is the flag name used for tuning the auth mount disable lockout parameter
|
||||
flagNameUserLockoutDisable = "user-lockout-disable"
|
||||
// flagNameDisableRedirects is used to prevent the client from honoring a single redirect as a response to a request
|
||||
flagNameDisableRedirects = "disable-redirects"
|
||||
)
|
||||
|
||||
@@ -36,6 +36,10 @@ func TestParseListeners(t *testing.T) {
|
||||
testParseListeners(t)
|
||||
}
|
||||
|
||||
func TestParseUserLockouts(t *testing.T) {
|
||||
testParseUserLockouts(t)
|
||||
}
|
||||
|
||||
func TestParseSockaddrTemplate(t *testing.T) {
|
||||
testParseSockaddrTemplate(t)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package server
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -892,6 +893,67 @@ listener "tcp" {
|
||||
}
|
||||
}
|
||||
|
||||
func testParseUserLockouts(t *testing.T) {
|
||||
obj, _ := hcl.Parse(strings.TrimSpace(`
|
||||
user_lockout "all" {
|
||||
lockout_duration = "40m"
|
||||
lockout_counter_reset = "45m"
|
||||
disable_lockout = "false"
|
||||
}
|
||||
user_lockout "userpass" {
|
||||
lockout_threshold = "100"
|
||||
lockout_duration = "20m"
|
||||
}
|
||||
user_lockout "ldap" {
|
||||
disable_lockout = "true"
|
||||
}`))
|
||||
|
||||
config := Config{
|
||||
SharedConfig: &configutil.SharedConfig{},
|
||||
}
|
||||
list, _ := obj.Node.(*ast.ObjectList)
|
||||
objList := list.Filter("user_lockout")
|
||||
configutil.ParseUserLockouts(config.SharedConfig, objList)
|
||||
|
||||
sort.Slice(config.SharedConfig.UserLockouts[:], func(i, j int) bool {
|
||||
return config.SharedConfig.UserLockouts[i].Type < config.SharedConfig.UserLockouts[j].Type
|
||||
})
|
||||
|
||||
expected := &Config{
|
||||
SharedConfig: &configutil.SharedConfig{
|
||||
UserLockouts: []*configutil.UserLockout{
|
||||
{
|
||||
Type: "all",
|
||||
LockoutThreshold: 5,
|
||||
LockoutDuration: 2400000000000,
|
||||
LockoutCounterReset: 2700000000000,
|
||||
DisableLockout: false,
|
||||
},
|
||||
{
|
||||
Type: "userpass",
|
||||
LockoutThreshold: 100,
|
||||
LockoutDuration: 1200000000000,
|
||||
LockoutCounterReset: 2700000000000,
|
||||
DisableLockout: false,
|
||||
},
|
||||
{
|
||||
Type: "ldap",
|
||||
LockoutThreshold: 5,
|
||||
LockoutDuration: 2400000000000,
|
||||
LockoutCounterReset: 2700000000000,
|
||||
DisableLockout: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
sort.Slice(expected.SharedConfig.UserLockouts[:], func(i, j int) bool {
|
||||
return expected.SharedConfig.UserLockouts[i].Type < expected.SharedConfig.UserLockouts[j].Type
|
||||
})
|
||||
config.Prune()
|
||||
require.Equal(t, config, *expected)
|
||||
}
|
||||
|
||||
func testParseSockaddrTemplate(t *testing.T) {
|
||||
config, err := ParseConfig(`
|
||||
api_addr = <<EOF
|
||||
|
||||
@@ -21,6 +21,8 @@ type SharedConfig struct {
|
||||
|
||||
Listeners []*Listener `hcl:"-"`
|
||||
|
||||
UserLockouts []*UserLockout `hcl:"-"`
|
||||
|
||||
Seals []*KMS `hcl:"-"`
|
||||
Entropy *Entropy `hcl:"-"`
|
||||
|
||||
@@ -134,6 +136,13 @@ func ParseConfig(d string) (*SharedConfig, error) {
|
||||
}
|
||||
}
|
||||
|
||||
if o := list.Filter("user_lockout"); len(o.Items) > 0 {
|
||||
result.found("user_lockout", "UserLockout")
|
||||
if err := ParseUserLockouts(&result, o); err != nil {
|
||||
return nil, fmt.Errorf("error parsing 'user_lockout': %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if o := list.Filter("telemetry"); len(o.Items) > 0 {
|
||||
result.found("telemetry", "Telemetry")
|
||||
if err := parseTelemetry(&result, o); err != nil {
|
||||
@@ -194,6 +203,22 @@ func (c *SharedConfig) Sanitized() map[string]interface{} {
|
||||
result["listeners"] = sanitizedListeners
|
||||
}
|
||||
|
||||
// Sanitize user lockout stanza
|
||||
if len(c.UserLockouts) != 0 {
|
||||
var sanitizedUserLockouts []interface{}
|
||||
for _, userlockout := range c.UserLockouts {
|
||||
cleanUserLockout := map[string]interface{}{
|
||||
"type": userlockout.Type,
|
||||
"lockout_threshold": userlockout.LockoutThreshold,
|
||||
"lockout_duration": userlockout.LockoutDuration,
|
||||
"lockout_counter_reset": userlockout.LockoutCounterReset,
|
||||
"disable_lockout": userlockout.DisableLockout,
|
||||
}
|
||||
sanitizedUserLockouts = append(sanitizedUserLockouts, cleanUserLockout)
|
||||
}
|
||||
result["user_lockout_configs"] = sanitizedUserLockouts
|
||||
}
|
||||
|
||||
// Sanitize seals stanza
|
||||
if len(c.Seals) != 0 {
|
||||
var sanitizedSeals []interface{}
|
||||
|
||||
@@ -14,6 +14,13 @@ func (c *SharedConfig) Merge(c2 *SharedConfig) *SharedConfig {
|
||||
result.Listeners = append(result.Listeners, l)
|
||||
}
|
||||
|
||||
for _, userlockout := range c.UserLockouts {
|
||||
result.UserLockouts = append(result.UserLockouts, userlockout)
|
||||
}
|
||||
for _, userlockout := range c2.UserLockouts {
|
||||
result.UserLockouts = append(result.UserLockouts, userlockout)
|
||||
}
|
||||
|
||||
result.HCPLinkConf = c.HCPLinkConf
|
||||
if c2.HCPLinkConf != nil {
|
||||
result.HCPLinkConf = c2.HCPLinkConf
|
||||
|
||||
186
internalshared/configutil/userlockout.go
Normal file
186
internalshared/configutil/userlockout.go
Normal file
@@ -0,0 +1,186 @@
|
||||
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 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
|
||||
}
|
||||
69
internalshared/configutil/userlockout_test.go
Normal file
69
internalshared/configutil/userlockout_test.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package configutil
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestParseUserLockout(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("Missing user lockout block in config file", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
inputConfig := make(map[string]*UserLockout)
|
||||
expectedConfig := make(map[string]*UserLockout)
|
||||
expectedConfigall := &UserLockout{}
|
||||
expectedConfigall.Type = "all"
|
||||
expectedConfigall.LockoutThreshold = UserLockoutThresholdDefault
|
||||
expectedConfigall.LockoutDuration = UserLockoutDurationDefault
|
||||
expectedConfigall.LockoutCounterReset = UserLockoutCounterResetDefault
|
||||
expectedConfigall.DisableLockout = DisableUserLockoutDefault
|
||||
expectedConfig["all"] = expectedConfigall
|
||||
|
||||
outputConfig := setMissingUserLockoutValuesInMap(inputConfig)
|
||||
if !reflect.DeepEqual(expectedConfig["all"], outputConfig["all"]) {
|
||||
t.Errorf("user lockout config: expected %#v\nactual %#v", expectedConfig["all"], outputConfig["all"])
|
||||
}
|
||||
})
|
||||
t.Run("setting default lockout counter reset and lockout duration for userpass in config ", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
// input user lockout in config file
|
||||
inputConfig := make(map[string]*UserLockout)
|
||||
configAll := &UserLockout{}
|
||||
configAll.Type = "all"
|
||||
configAll.LockoutCounterReset = 20 * time.Minute
|
||||
configAll.LockoutCounterResetRaw = "1200000000000"
|
||||
inputConfig["all"] = configAll
|
||||
configUserpass := &UserLockout{}
|
||||
configUserpass.Type = "userpass"
|
||||
configUserpass.LockoutDuration = 10 * time.Minute
|
||||
configUserpass.LockoutDurationRaw = "600000000000"
|
||||
inputConfig["userpass"] = configUserpass
|
||||
|
||||
expectedConfig := make(map[string]*UserLockout)
|
||||
expectedConfigall := &UserLockout{}
|
||||
expectedConfigUserpass := &UserLockout{}
|
||||
// expected default values
|
||||
expectedConfigall.Type = "all"
|
||||
expectedConfigall.LockoutThreshold = UserLockoutThresholdDefault
|
||||
expectedConfigall.LockoutDuration = UserLockoutDurationDefault
|
||||
expectedConfigall.LockoutCounterReset = 20 * time.Minute
|
||||
expectedConfigall.DisableLockout = DisableUserLockoutDefault
|
||||
// expected values for userpass
|
||||
expectedConfigUserpass.Type = "userpass"
|
||||
expectedConfigUserpass.LockoutThreshold = UserLockoutThresholdDefault
|
||||
expectedConfigUserpass.LockoutDuration = 10 * time.Minute
|
||||
expectedConfigUserpass.LockoutCounterReset = 20 * time.Minute
|
||||
expectedConfigUserpass.DisableLockout = DisableUserLockoutDefault
|
||||
expectedConfig["all"] = expectedConfigall
|
||||
expectedConfig["userpass"] = expectedConfigUserpass
|
||||
|
||||
outputConfig := setMissingUserLockoutValuesInMap(inputConfig)
|
||||
if !reflect.DeepEqual(expectedConfig["all"], outputConfig["all"]) {
|
||||
t.Errorf("user lockout config: expected %#v\nactual %#v", expectedConfig["all"], outputConfig["all"])
|
||||
}
|
||||
if !reflect.DeepEqual(expectedConfig["userpass"], outputConfig["userpass"]) {
|
||||
t.Errorf("user lockout config: expected %#v\nactual %#v", expectedConfig["userpass"], outputConfig["userpass"])
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -34,4 +34,6 @@ const (
|
||||
ReplicationResolverALPN = "replication_resolver_v1"
|
||||
|
||||
VaultEnableFilePermissionsCheckEnv = "VAULT_ENABLE_FILE_PERMISSIONS_CHECK"
|
||||
|
||||
VaultDisableUserLockout = "VAULT_DISABLE_USER_LOCKOUT"
|
||||
)
|
||||
|
||||
@@ -944,6 +944,15 @@ func (b *SystemBackend) mountInfo(ctx context.Context, entry *MountEntry) map[st
|
||||
if entry.Table == credentialTableType {
|
||||
entryConfig["token_type"] = entry.Config.TokenType.String()
|
||||
}
|
||||
if entry.Config.UserLockoutConfig != nil {
|
||||
userLockoutConfig := map[string]interface{}{
|
||||
"user_lockout_counter_reset_duration": int64(entry.Config.UserLockoutConfig.LockoutCounterReset.Seconds()),
|
||||
"user_lockout_threshold": entry.Config.UserLockoutConfig.LockoutThreshold,
|
||||
"user_lockout_duration": int64(entry.Config.UserLockoutConfig.LockoutDuration.Seconds()),
|
||||
"user_lockout_disable": entry.Config.UserLockoutConfig.DisableLockout,
|
||||
}
|
||||
entryConfig["user_lockout_config"] = userLockoutConfig
|
||||
}
|
||||
|
||||
// Add deprecation status only if it exists
|
||||
builtinType := b.Core.builtinTypeFromMountEntry(ctx, entry)
|
||||
@@ -1604,6 +1613,13 @@ func (b *SystemBackend) handleTuneReadCommon(ctx context.Context, path string) (
|
||||
resp.Data["allowed_managed_keys"] = rawVal.([]string)
|
||||
}
|
||||
|
||||
if mountEntry.Config.UserLockoutConfig != nil {
|
||||
resp.Data["user_lockout_counter_reset_duration"] = int64(mountEntry.Config.UserLockoutConfig.LockoutCounterReset.Seconds())
|
||||
resp.Data["user_lockout_threshold"] = mountEntry.Config.UserLockoutConfig.LockoutThreshold
|
||||
resp.Data["user_lockout_duration"] = int64(mountEntry.Config.UserLockoutConfig.LockoutDuration.Seconds())
|
||||
resp.Data["user_lockout_disable"] = mountEntry.Config.UserLockoutConfig.DisableLockout
|
||||
}
|
||||
|
||||
if len(mountEntry.Options) > 0 {
|
||||
resp.Data["options"] = mountEntry.Options
|
||||
}
|
||||
@@ -1723,6 +1739,111 @@ func (b *SystemBackend) handleTuneWriteCommon(ctx context.Context, path string,
|
||||
}
|
||||
}
|
||||
|
||||
// user-lockout config
|
||||
{
|
||||
var apiuserLockoutConfig APIUserLockoutConfig
|
||||
|
||||
userLockoutConfigMap := data.Get("user_lockout_config").(map[string]interface{})
|
||||
var err error
|
||||
if userLockoutConfigMap != nil && len(userLockoutConfigMap) != 0 {
|
||||
err := mapstructure.Decode(userLockoutConfigMap, &apiuserLockoutConfig)
|
||||
if err != nil {
|
||||
return logical.ErrorResponse(
|
||||
"unable to convert given user lockout config information"),
|
||||
logical.ErrInvalidRequest
|
||||
}
|
||||
|
||||
// Supported auth methods for user lockout configuration: ldap, approle, userpass
|
||||
switch strings.ToLower(mountEntry.Type) {
|
||||
case "ldap", "approle", "userpass":
|
||||
default:
|
||||
return logical.ErrorResponse("tuning of user lockout configuration for auth type %q not allowed", mountEntry.Type),
|
||||
logical.ErrInvalidRequest
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if len(userLockoutConfigMap) > 0 && mountEntry.Config.UserLockoutConfig == nil {
|
||||
mountEntry.Config.UserLockoutConfig = &UserLockoutConfig{}
|
||||
}
|
||||
|
||||
var oldUserLockoutThreshold uint64
|
||||
var newUserLockoutDuration, oldUserLockoutDuration time.Duration
|
||||
var newUserLockoutCounterReset, oldUserLockoutCounterReset time.Duration
|
||||
var oldUserLockoutDisable bool
|
||||
|
||||
if apiuserLockoutConfig.LockoutThreshold != "" {
|
||||
userLockoutThreshold, err := strconv.ParseUint(apiuserLockoutConfig.LockoutThreshold, 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to parse user lockout threshold: %w", err)
|
||||
}
|
||||
oldUserLockoutThreshold = mountEntry.Config.UserLockoutConfig.LockoutThreshold
|
||||
mountEntry.Config.UserLockoutConfig.LockoutThreshold = userLockoutThreshold
|
||||
}
|
||||
|
||||
if apiuserLockoutConfig.LockoutDuration != "" {
|
||||
oldUserLockoutDuration = mountEntry.Config.UserLockoutConfig.LockoutDuration
|
||||
switch apiuserLockoutConfig.LockoutDuration {
|
||||
case "":
|
||||
newUserLockoutDuration = oldUserLockoutDuration
|
||||
case "system":
|
||||
newUserLockoutDuration = time.Duration(0)
|
||||
default:
|
||||
tmpUserLockoutDuration, err := parseutil.ParseDurationSecond(apiuserLockoutConfig.LockoutDuration)
|
||||
if err != nil {
|
||||
return handleError(err)
|
||||
}
|
||||
newUserLockoutDuration = tmpUserLockoutDuration
|
||||
|
||||
}
|
||||
mountEntry.Config.UserLockoutConfig.LockoutDuration = newUserLockoutDuration
|
||||
}
|
||||
|
||||
if apiuserLockoutConfig.LockoutCounterResetDuration != "" {
|
||||
oldUserLockoutCounterReset = mountEntry.Config.UserLockoutConfig.LockoutCounterReset
|
||||
switch apiuserLockoutConfig.LockoutCounterResetDuration {
|
||||
case "":
|
||||
newUserLockoutCounterReset = oldUserLockoutCounterReset
|
||||
case "system":
|
||||
newUserLockoutCounterReset = time.Duration(0)
|
||||
default:
|
||||
tmpUserLockoutCounterReset, err := parseutil.ParseDurationSecond(apiuserLockoutConfig.LockoutCounterResetDuration)
|
||||
if err != nil {
|
||||
return handleError(err)
|
||||
}
|
||||
newUserLockoutCounterReset = tmpUserLockoutCounterReset
|
||||
}
|
||||
|
||||
mountEntry.Config.UserLockoutConfig.LockoutCounterReset = newUserLockoutCounterReset
|
||||
}
|
||||
|
||||
if apiuserLockoutConfig.DisableLockout != nil {
|
||||
oldUserLockoutDisable = mountEntry.Config.UserLockoutConfig.DisableLockout
|
||||
userLockoutDisable := apiuserLockoutConfig.DisableLockout
|
||||
mountEntry.Config.UserLockoutConfig.DisableLockout = *userLockoutDisable
|
||||
}
|
||||
|
||||
// Update the mount table
|
||||
if len(userLockoutConfigMap) > 0 {
|
||||
switch {
|
||||
case strings.HasPrefix(path, "auth/"):
|
||||
err = b.Core.persistAuth(ctx, b.Core.auth, &mountEntry.Local)
|
||||
default:
|
||||
err = b.Core.persistMounts(ctx, b.Core.mounts, &mountEntry.Local)
|
||||
}
|
||||
if err != nil {
|
||||
mountEntry.Config.UserLockoutConfig.LockoutCounterReset = oldUserLockoutCounterReset
|
||||
mountEntry.Config.UserLockoutConfig.LockoutThreshold = oldUserLockoutThreshold
|
||||
mountEntry.Config.UserLockoutConfig.LockoutDuration = oldUserLockoutDuration
|
||||
mountEntry.Config.UserLockoutConfig.DisableLockout = oldUserLockoutDisable
|
||||
return handleError(err)
|
||||
}
|
||||
if b.Core.logger.IsInfo() {
|
||||
b.Core.logger.Info("tuning of user_lockout_config successful", "path", path)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
if rawVal, ok := data.GetOk("description"); ok {
|
||||
description := rawVal.(string)
|
||||
|
||||
@@ -5021,6 +5142,10 @@ in the plugin catalog.`,
|
||||
`The options to pass into the backend. Should be a json object with string keys and values.`,
|
||||
},
|
||||
|
||||
"tune_user_lockout_config": {
|
||||
`The user lockout configuration to pass into the backend. Should be a json object with string keys and values.`,
|
||||
},
|
||||
|
||||
"remount": {
|
||||
"Move the mount point of an already-mounted backend, within or across namespaces",
|
||||
`
|
||||
|
||||
@@ -1542,6 +1542,10 @@ func (b *SystemBackend) authPaths() []*framework.Path {
|
||||
Type: framework.TypeString,
|
||||
Description: strings.TrimSpace(sysHelp["token_type"][0]),
|
||||
},
|
||||
"user_lockout_config": {
|
||||
Type: framework.TypeMap,
|
||||
Description: strings.TrimSpace(sysHelp["tune_user_lockout_config"][0]),
|
||||
},
|
||||
"plugin_version": {
|
||||
Type: framework.TypeString,
|
||||
Description: strings.TrimSpace(sysHelp["plugin-catalog_version"][0]),
|
||||
@@ -1929,6 +1933,10 @@ func (b *SystemBackend) mountPaths() []*framework.Path {
|
||||
Type: framework.TypeString,
|
||||
Description: strings.TrimSpace(sysHelp["plugin-catalog_version"][0]),
|
||||
},
|
||||
"user_lockout_config": {
|
||||
Type: framework.TypeMap,
|
||||
Description: strings.TrimSpace(sysHelp["tune_user_lockout_config"][0]),
|
||||
},
|
||||
},
|
||||
|
||||
Callbacks: map[logical.Operation]framework.OperationFunc{
|
||||
|
||||
@@ -351,6 +351,7 @@ type MountConfig struct {
|
||||
AllowedResponseHeaders []string `json:"allowed_response_headers,omitempty" structs:"allowed_response_headers" mapstructure:"allowed_response_headers"`
|
||||
TokenType logical.TokenType `json:"token_type,omitempty" structs:"token_type" mapstructure:"token_type"`
|
||||
AllowedManagedKeys []string `json:"allowed_managed_keys,omitempty" mapstructure:"allowed_managed_keys"`
|
||||
UserLockoutConfig *UserLockoutConfig `json:"user_lockout_config,omitempty" mapstructure:"user_lockout_config"`
|
||||
|
||||
// PluginName is the name of the plugin registered in the catalog.
|
||||
//
|
||||
@@ -358,6 +359,20 @@ type MountConfig struct {
|
||||
PluginName string `json:"plugin_name,omitempty" structs:"plugin_name,omitempty" mapstructure:"plugin_name"`
|
||||
}
|
||||
|
||||
type UserLockoutConfig struct {
|
||||
LockoutThreshold uint64 `json:"lockout_threshold,omitempty" structs:"lockout_threshold" mapstructure:"lockout_threshold"`
|
||||
LockoutDuration time.Duration `json:"lockout_duration,omitempty" structs:"lockout_duration" mapstructure:"lockout_duration"`
|
||||
LockoutCounterReset time.Duration `json:"lockout_counter_reset,omitempty" structs:"lockout_counter_reset" mapstructure:"lockout_counter_reset"`
|
||||
DisableLockout bool `json:"disable_lockout,omitempty" structs:"disable_lockout" mapstructure:"disable_lockout"`
|
||||
}
|
||||
|
||||
type APIUserLockoutConfig struct {
|
||||
LockoutThreshold string `json:"lockout_threshold,omitempty" structs:"lockout_threshold" mapstructure:"lockout_threshold"`
|
||||
LockoutDuration string `json:"lockout_duration,omitempty" structs:"lockout_duration" mapstructure:"lockout_duration"`
|
||||
LockoutCounterResetDuration string `json:"lockout_counter_reset_duration,omitempty" structs:"lockout_counter_reset_duration" mapstructure:"lockout_counter_reset_duration"`
|
||||
DisableLockout *bool `json:"lockout_disable,omitempty" structs:"lockout_disable" mapstructure:"lockout_disable"`
|
||||
}
|
||||
|
||||
// APIMountConfig is an embedded struct of api.MountConfigInput
|
||||
type APIMountConfig struct {
|
||||
DefaultLeaseTTL string `json:"default_lease_ttl" structs:"default_lease_ttl" mapstructure:"default_lease_ttl"`
|
||||
@@ -370,6 +385,7 @@ type APIMountConfig struct {
|
||||
AllowedResponseHeaders []string `json:"allowed_response_headers,omitempty" structs:"allowed_response_headers" mapstructure:"allowed_response_headers"`
|
||||
TokenType string `json:"token_type" structs:"token_type" mapstructure:"token_type"`
|
||||
AllowedManagedKeys []string `json:"allowed_managed_keys,omitempty" mapstructure:"allowed_managed_keys"`
|
||||
UserLockoutConfig *UserLockoutConfig `json:"user_lockout_config,omitempty" mapstructure:"user_lockout_config"`
|
||||
PluginVersion string `json:"plugin_version,omitempty" mapstructure:"plugin_version"`
|
||||
|
||||
// PluginName is the name of the plugin registered in the catalog.
|
||||
@@ -1275,8 +1291,8 @@ func (c *Core) runMountUpdates(ctx context.Context, needPersist bool) error {
|
||||
entry.NamespaceID = namespace.RootNamespaceID
|
||||
needPersist = true
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
// Done if we have restored the mount table and we don't need
|
||||
// to persist
|
||||
if !needPersist {
|
||||
|
||||
Reference in New Issue
Block a user