Vault 8307 user lockout workflow oss (#17951)

* adding oss file changes

* check disabled and read values from config

* isUserLocked, getUserLockout Configurations, check user lock before login and return error

* remove stale entry from storage during read

* added failed login process workflow

* success workflow updated

* user lockouts external tests

* changing update to support delete

* provide access to alias look ahead function

* adding path alias lookahead

* adding tests

* added changelog

* added comments

* adding changes from ent branch

* adding lock to UpdateUserFailedLoginInfo

* fix return default bug
This commit is contained in:
akshya96 2022-12-06 17:22:46 -08:00 committed by GitHub
parent 94a349c406
commit 1801f09c6a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 720 additions and 10 deletions

3
changelog/17951.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:improvement
core: added changes for user lockout workflow.
```

View File

@ -32,6 +32,10 @@ type UserLockout struct {
DisableLockoutRaw interface{} `hcl:"disable_lockout"` DisableLockoutRaw interface{} `hcl:"disable_lockout"`
} }
func GetSupportedUserLockoutsAuthMethods() []string {
return []string{"userpass", "approle", "ldap"}
}
func ParseUserLockouts(result *SharedConfig, list *ast.ObjectList) error { func ParseUserLockouts(result *SharedConfig, list *ast.ObjectList) error {
var err error var err error
result.UserLockouts = make([]*UserLockout, 0, len(list.Items)) result.UserLockouts = make([]*UserLockout, 0, len(list.Items))

View File

@ -525,6 +525,9 @@ type Core struct {
// and login counter, last failed login time as value // and login counter, last failed login time as value
userFailedLoginInfo map[FailedLoginUser]*FailedLoginInfo userFailedLoginInfo map[FailedLoginUser]*FailedLoginInfo
// userFailedLoginInfoLock controls access to the userFailedLoginInfoMap
userFailedLoginInfoLock sync.RWMutex
enableMlock bool enableMlock bool
// This can be used to trigger operations to stop running when Vault is // This can be used to trigger operations to stop running when Vault is
@ -3455,6 +3458,39 @@ func (c *Core) DetermineRoleFromLoginRequest(mountPoint string, data map[string]
return resp.Data["role"].(string) return resp.Data["role"].(string)
} }
// aliasNameFromLoginRequest will determine the aliasName from the login Request
func (c *Core) aliasNameFromLoginRequest(ctx context.Context, req *logical.Request) (string, error) {
c.authLock.RLock()
defer c.authLock.RUnlock()
ns, err := namespace.FromContext(ctx)
if err != nil {
return "", err
}
// ns path is added while checking matching backend
mountPath := strings.TrimPrefix(req.MountPoint, ns.Path)
matchingBackend := c.router.MatchingBackend(ctx, mountPath)
if matchingBackend == nil || matchingBackend.Type() != logical.TypeCredential {
// pathLoginAliasLookAhead operation does not apply to this request
return "", nil
}
path := strings.ReplaceAll(req.Path, mountPath, "")
resp, err := matchingBackend.HandleRequest(ctx, &logical.Request{
MountPoint: req.MountPoint,
Path: path,
Operation: logical.AliasLookaheadOperation,
Data: req.Data,
Storage: c.router.MatchingStorageByAPIPath(ctx, req.Path),
})
if err != nil || resp.Auth.Alias == nil {
return "", nil
}
return resp.Auth.Alias.Name, nil
}
// ListMounts will provide a slice containing a deep copy each mount entry // ListMounts will provide a slice containing a deep copy each mount entry
func (c *Core) ListMounts() ([]*MountEntry, error) { func (c *Core) ListMounts() ([]*MountEntry, error) {
c.mountsLock.RLock() c.mountsLock.RLock()

View File

@ -0,0 +1,326 @@
package identity
import (
"strings"
"testing"
"time"
"github.com/hashicorp/vault/api"
"github.com/hashicorp/vault/builtin/credential/userpass"
vaulthttp "github.com/hashicorp/vault/http"
"github.com/hashicorp/vault/sdk/logical"
"github.com/hashicorp/vault/vault"
)
// TestIdentityStore_UserLockoutTest tests that the user gets locked after
// more than 1 failed login request than the number specified for
// lockout threshold field in user lockout configuration. It also
// tests that the user gets unlocked after the duration specified
// for lockout duration field has passed
func TestIdentityStore_UserLockoutTest(t *testing.T) {
coreConfig := &vault.CoreConfig{
CredentialBackends: map[string]logical.Factory{
"userpass": userpass.Factory,
},
}
cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{
HandlerFunc: vaulthttp.Handler,
})
cluster.Start()
defer cluster.Cleanup()
active := cluster.Cores[0].Client
standby := cluster.Cores[1].Client
err := active.Sys().EnableAuthWithOptions("userpass", &api.EnableAuthOptions{
Type: "userpass",
})
if err != nil {
t.Fatal(err)
}
// tune auth mount
userlockoutConfig := &api.UserLockoutConfigInput{
LockoutThreshold: "3",
LockoutDuration: "5s",
LockoutCounterResetDuration: "5s",
}
err = active.Sys().TuneMount("auth/userpass", api.MountConfigInput{
UserLockoutConfig: userlockoutConfig,
})
if err != nil {
t.Fatal(err)
}
// create a user for userpass
_, err = standby.Logical().Write("auth/userpass/users/bsmith", map[string]interface{}{
"password": "training",
})
if err != nil {
t.Fatal(err)
}
// login failure count 1
standby.Logical().Write("auth/userpass/login/bsmith", map[string]interface{}{
"password": "wrongPassword",
})
// login failure count 2
standby.Logical().Write("auth/userpass/login/bsmith", map[string]interface{}{
"password": "wrongPassword",
})
// login failure count 3
active.Logical().Write("auth/userpass/login/bsmith", map[string]interface{}{
"password": "wrongPassword",
})
// login : permission denied as user locked out
_, err = standby.Logical().Write("auth/userpass/login/bsmith", map[string]interface{}{
"password": "training",
})
if err == nil {
t.Fatal("expected login to fail as user locked out")
}
if !strings.Contains(err.Error(), logical.ErrPermissionDenied.Error()) {
t.Fatalf("expected to see permission denied error as user locked out, got %v", err)
}
time.Sleep(5 * time.Second)
// login with right password and wait for user to get unlocked
_, err = standby.Logical().Write("auth/userpass/login/bsmith", map[string]interface{}{
"password": "training",
})
if err != nil {
t.Fatal("expected login to succeed as user is unlocked")
}
}
// TestIdentityStore_UserFailedLoginMapResetOnSuccess tests that
// the user lockout feature is reset for a user after one successfull attempt
// after multiple failed login attempts (within lockout threshold)
func TestIdentityStore_UserFailedLoginMapResetOnSuccess(t *testing.T) {
coreConfig := &vault.CoreConfig{
CredentialBackends: map[string]logical.Factory{
"userpass": userpass.Factory,
},
}
cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{
HandlerFunc: vaulthttp.Handler,
})
cluster.Start()
defer cluster.Cleanup()
client := cluster.Cores[0].Client
err := client.Sys().EnableAuthWithOptions("userpass", &api.EnableAuthOptions{
Type: "userpass",
})
if err != nil {
t.Fatal(err)
}
// tune auth mount
userlockoutConfig := &api.UserLockoutConfigInput{
LockoutThreshold: "3",
LockoutDuration: "5s",
LockoutCounterResetDuration: "5s",
}
err = client.Sys().TuneMount("auth/userpass", api.MountConfigInput{
UserLockoutConfig: userlockoutConfig,
})
if err != nil {
t.Fatal(err)
}
// create a user for userpass
_, err = client.Logical().Write("auth/userpass/users/bsmith", map[string]interface{}{
"password": "training",
})
if err != nil {
t.Fatal(err)
}
// login failure count 1
client.Logical().Write("auth/userpass/login/bsmith", map[string]interface{}{
"password": "wrongPassword",
})
// login failure count 2
client.Logical().Write("auth/userpass/login/bsmith", map[string]interface{}{
"password": "wrongPassword",
})
// login with right credentials - successful login
// entry for this user is removed from userFailedLoginInfo map
_, err = client.Logical().Write("auth/userpass/login/bsmith", map[string]interface{}{
"password": "training",
})
if err != nil {
t.Fatal(err)
}
// login failure count 3, is now count 1 after successful login
client.Logical().Write("auth/userpass/login/bsmith", map[string]interface{}{
"password": "wrongPassword",
})
// login failure count 4, is now count 2 after successful login
// error should not be permission denied as user not locked out
_, err = client.Logical().Write("auth/userpass/login/bsmith", map[string]interface{}{
"password": "wrongPassword",
})
if err == nil {
t.Fatal("expected login to fail due to wrong credentials")
}
if !strings.Contains(err.Error(), "invalid username or password") {
t.Fatalf("expected to see invalid username or password error as user is not locked out, got %v", err)
}
}
// TestIdentityStore_DisableUserLockoutTest tests that user login will
// fail when supplied with wrong credentials. If the user is locked,
// it returns permission denied. In this case, it returns invalid user
// credentials error as the user lockout feature is disabled and the
// user did not get locked after multiple failed login attempts
func TestIdentityStore_DisableUserLockoutTest(t *testing.T) {
coreConfig := &vault.CoreConfig{
CredentialBackends: map[string]logical.Factory{
"userpass": userpass.Factory,
},
}
cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{
HandlerFunc: vaulthttp.Handler,
})
cluster.Start()
defer cluster.Cleanup()
active := cluster.Cores[0].Client
standby := cluster.Cores[1].Client
err := active.Sys().EnableAuthWithOptions("userpass", &api.EnableAuthOptions{
Type: "userpass",
})
if err != nil {
t.Fatal(err)
}
// tune auth mount
disableLockout := true
userlockoutConfig := &api.UserLockoutConfigInput{
LockoutThreshold: "3",
DisableLockout: &disableLockout,
}
err = active.Sys().TuneMount("auth/userpass", api.MountConfigInput{
UserLockoutConfig: userlockoutConfig,
})
if err != nil {
t.Fatal(err)
}
// create a userpass user
_, err = standby.Logical().Write("auth/userpass/users/bsmith", map[string]interface{}{
"password": "training",
})
if err != nil {
t.Fatal(err)
}
// login failure count 1
standby.Logical().Write("auth/userpass/login/bsmith", map[string]interface{}{
"password": "wrongPassword",
})
// login failure count 2
standby.Logical().Write("auth/userpass/login/bsmith", map[string]interface{}{
"password": "wrongPassword",
})
// login failure count 3
active.Logical().Write("auth/userpass/login/bsmith", map[string]interface{}{
"password": "wrongPassword",
})
// login failure count 4
_, err = standby.Logical().Write("auth/userpass/login/bsmith", map[string]interface{}{
"password": "wrongPassword",
})
if err == nil {
t.Fatal("expected login to fail due to wrong credentials")
}
if !strings.Contains(err.Error(), "invalid username or password") {
t.Fatalf("expected to see invalid username or password error as user is not locked out, got %v", err)
}
}
// TestIdentityStore_LockoutCounterResetTest tests that the user lockout counter
// for a user is reset after no failed login attempts for a duration
// as specified for lockout counter reset field in user lockout configuration
func TestIdentityStore_LockoutCounterResetTest(t *testing.T) {
coreConfig := &vault.CoreConfig{
CredentialBackends: map[string]logical.Factory{
"userpass": userpass.Factory,
},
}
cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{
HandlerFunc: vaulthttp.Handler,
})
cluster.Start()
defer cluster.Cleanup()
active := cluster.Cores[0].Client
standby := cluster.Cores[1].Client
err := active.Sys().EnableAuthWithOptions("userpass", &api.EnableAuthOptions{
Type: "userpass",
})
if err != nil {
t.Fatal(err)
}
// tune auth mount
userlockoutConfig := &api.UserLockoutConfigInput{
LockoutThreshold: "3",
LockoutCounterResetDuration: "5s",
}
err = active.Sys().TuneMount("auth/userpass", api.MountConfigInput{
UserLockoutConfig: userlockoutConfig,
})
if err != nil {
t.Fatal(err)
}
// create a user for userpass
_, err = standby.Logical().Write("auth/userpass/users/bsmith", map[string]interface{}{
"password": "training",
})
if err != nil {
t.Fatal(err)
}
// login failure count 1
standby.Logical().Write("auth/userpass/login/bsmith", map[string]interface{}{
"password": "wrongPassword",
})
// login failure count 2
standby.Logical().Write("auth/userpass/login/bsmith", map[string]interface{}{
"password": "wrongPassword",
})
// set sleep timer to reset login counter
time.Sleep(5 * time.Second)
// login failure 3, count should be reset, this will be treated as failed count 1
active.Logical().Write("auth/userpass/login/bsmith", map[string]interface{}{
"password": "wrongPassword",
})
// login failure 4, this will be treated as failed count 2
_, err = standby.Logical().Write("auth/userpass/login/bsmith", map[string]interface{}{
"password": "wrongPassword",
})
if err == nil {
t.Fatal("expected login to fail due to wrong credentials")
}
if !strings.Contains(err.Error(), "invalid username or password") {
t.Fatalf("expected to see invalid username or password error as user is not locked out, got %v", err)
}
}

View File

@ -7,6 +7,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"os" "os"
"strconv"
"strings" "strings"
"time" "time"
@ -17,6 +18,7 @@ import (
"github.com/hashicorp/go-secure-stdlib/strutil" "github.com/hashicorp/go-secure-stdlib/strutil"
"github.com/hashicorp/go-sockaddr" "github.com/hashicorp/go-sockaddr"
"github.com/hashicorp/go-uuid" "github.com/hashicorp/go-uuid"
"github.com/hashicorp/vault/command/server"
"github.com/hashicorp/vault/helper/identity" "github.com/hashicorp/vault/helper/identity"
"github.com/hashicorp/vault/helper/identity/mfa" "github.com/hashicorp/vault/helper/identity/mfa"
"github.com/hashicorp/vault/helper/metricsutil" "github.com/hashicorp/vault/helper/metricsutil"
@ -37,6 +39,8 @@ import (
const ( const (
replTimeout = 1 * time.Second replTimeout = 1 * time.Second
EnvVaultDisableLocalAuthMountEntities = "VAULT_DISABLE_LOCAL_AUTH_MOUNT_ENTITIES" EnvVaultDisableLocalAuthMountEntities = "VAULT_DISABLE_LOCAL_AUTH_MOUNT_ENTITIES"
// base path to store locked users
coreLockedUsersPath = "core/login/lockedUsers/"
) )
var ( var (
@ -1325,8 +1329,35 @@ func (c *Core) handleLoginRequest(ctx context.Context, req *logical.Request) (re
return nil, nil, ErrInternalError return nil, nil, ErrInternalError
} }
// check if user lockout feature is disabled
isUserLockoutDisabled, err := c.isUserLockoutDisabled(entry)
if err != nil {
return nil, nil, err
}
// if user lockout feature is not disabled, check if the user is locked
if !isUserLockoutDisabled {
isloginUserLocked, err := c.isUserLocked(ctx, entry, req)
if err != nil {
return nil, nil, err
}
if isloginUserLocked {
return nil, nil, logical.ErrPermissionDenied
}
}
// Route the request // Route the request
resp, routeErr := c.doRouting(ctx, req) resp, routeErr := c.doRouting(ctx, req)
// if routeErr has invalid credentials error, update the userFailedLoginMap
if routeErr != nil && routeErr == logical.ErrInvalidCredentials {
err := c.failedUserLoginProcess(ctx, entry, req)
if err != nil {
return nil, nil, err
}
return nil, nil, resp.Error()
}
if resp != nil { if resp != nil {
// If wrapping is used, use the shortest between the request and response // If wrapping is used, use the shortest between the request and response
var wrapTTL time.Duration var wrapTTL time.Duration
@ -1596,6 +1627,23 @@ func (c *Core) handleLoginRequest(ctx context.Context, req *logical.Request) (re
resp = respTokenCreate resp = respTokenCreate
} }
// Successful login, remove any entry from userFailedLoginInfo map
// if it exists. This is done for batch tokens (for oss & ent)
// For service tokens on oss it is taken care by core RegisterAuth function.
// For service tokens on ent it is taken care by registerAuth RPC calls.
// This update is done as part of registerAuth of RPC calls from standby
// to active node. This is added there to reduce RPC calls
if !isUserLockoutDisabled && (auth.TokenType == logical.TokenTypeBatch) {
loginUserInfoKey := FailedLoginUser{
aliasName: auth.Alias.Name,
mountAccessor: auth.Alias.MountAccessor,
}
err = updateUserFailedLoginInfo(ctx, c, loginUserInfoKey, nil, true)
if err != nil {
return nil, nil, err
}
}
// if we were already going to return some error from this login, do that. // if we were already going to return some error from this login, do that.
// if not, we will then check if the API is locked for the requesting // if not, we will then check if the API is locked for the requesting
// namespace, to avoid leaking locked namespaces to unauthenticated clients. // namespace, to avoid leaking locked namespaces to unauthenticated clients.
@ -1717,6 +1765,273 @@ func (c *Core) LoginCreateToken(ctx context.Context, ns *namespace.Namespace, re
return leaseGenerated, resp, nil return leaseGenerated, resp, nil
} }
// failedUserLoginProcess updates the userFailedLoginMap with login count and last failed
// login time for users with failed login attempt
// If the user gets locked for current login attempt, it updates the storage entry too
func (c *Core) failedUserLoginProcess(ctx context.Context, mountEntry *MountEntry, req *logical.Request) error {
// get the user lockout configuration for the user
userLockoutConfiguration := c.getUserLockoutConfiguration(mountEntry)
// determine the key for userFailedLoginInfo map
loginUserInfoKey, err := c.getLoginUserInfoKey(ctx, mountEntry, req)
if err != nil {
return err
}
// get entry from userFailedLoginInfo map for the key
userFailedLoginInfo, err := getUserFailedLoginInfo(ctx, c, loginUserInfoKey)
if err != nil {
return err
}
// update the last failed login time with current time
failedLoginInfo := FailedLoginInfo{
lastFailedLoginTime: int(time.Now().Unix()),
}
// set the failed login count value for the entry in userFailedLoginInfo map
switch userFailedLoginInfo {
case nil: // entry does not exist in userfailedLoginMap
failedLoginInfo.count = 1
default:
failedLoginInfo.count = userFailedLoginInfo.count + 1
// if counter reset, set the count value to 1 as this gets counted as new entry
lastFailedLoginTime := time.Unix(int64(userFailedLoginInfo.lastFailedLoginTime), 0)
counterResetDuration := userLockoutConfiguration.LockoutCounterReset
if time.Now().After(lastFailedLoginTime.Add(counterResetDuration)) {
failedLoginInfo.count = 1
}
}
// update the userFailedLoginInfo map with the updated/new entry
err = updateUserFailedLoginInfo(ctx, c, loginUserInfoKey, &failedLoginInfo, false)
if err != nil {
return err
}
// if failed login count has reached threshold, create a storage entry as the user got locked
if failedLoginInfo.count >= uint(userLockoutConfiguration.LockoutThreshold) {
// user locked
ns, err := namespace.FromContext(ctx)
if err != nil {
return fmt.Errorf("could not parse namespace from http context: %w", err)
}
storageUserLockoutPath := fmt.Sprintf(coreLockedUsersPath+"%s/%s/%s", ns.ID, loginUserInfoKey.mountAccessor, loginUserInfoKey.aliasName)
compressedBytes, err := jsonutil.EncodeJSONAndCompress(failedLoginInfo.lastFailedLoginTime, nil)
if err != nil {
c.logger.Error("failed to encode or compress failed login user entry", "error", err)
return err
}
// Create an entry
entry := &logical.StorageEntry{
Key: storageUserLockoutPath,
Value: compressedBytes,
}
// Write to the physical backend
if err := c.barrier.Put(ctx, entry); err != nil {
c.logger.Error("failed to persist failed login user entry", "error", err)
return err
}
}
return nil
}
// getLoginUserInfoKey gets failedUserLoginInfo map key for login user
func (c *Core) getLoginUserInfoKey(ctx context.Context, mountEntry *MountEntry, req *logical.Request) (FailedLoginUser, error) {
userInfo := FailedLoginUser{}
aliasName, err := c.aliasNameFromLoginRequest(ctx, req)
if err != nil {
return userInfo, err
}
if aliasName == "" {
return userInfo, errors.New("failed to determine alias name from login request")
}
userInfo.aliasName = aliasName
userInfo.mountAccessor = mountEntry.Accessor
return userInfo, nil
}
// isUserLockoutDisabled checks if user lockout feature to prevent brute forcing is disabled
// Auth types userpass, ldap and approle support this feature
// precedence: environment var setting >> auth tune setting >> config file setting >> default (enabled)
func (c *Core) isUserLockoutDisabled(mountEntry *MountEntry) (bool, error) {
if !strutil.StrListContains(configutil.GetSupportedUserLockoutsAuthMethods(), mountEntry.Type) {
return true, nil
}
// check environment variable
var disableUserLockout bool
if disableUserLockoutEnv := os.Getenv(consts.VaultDisableUserLockout); disableUserLockoutEnv != "" {
var err error
disableUserLockout, err = strconv.ParseBool(disableUserLockoutEnv)
if err != nil {
return false, errors.New("Error parsing the environment variable VAULT_DISABLE_USER_LOCKOUT")
}
}
if disableUserLockout {
return true, nil
}
// read auth tune for mount entry
userLockoutConfigFromMount := mountEntry.Config.UserLockoutConfig
if userLockoutConfigFromMount != nil && userLockoutConfigFromMount.DisableLockout {
return true, nil
}
// read config for auth type from config file
userLockoutConfiguration := c.getUserLockoutFromConfig(mountEntry.Type)
if userLockoutConfiguration.DisableLockout {
return true, nil
}
// default
return false, nil
}
// isUserLocked determines if the login user is locked
func (c *Core) isUserLocked(ctx context.Context, mountEntry *MountEntry, req *logical.Request) (locked bool, err error) {
// get userFailedLoginInfo map key for login user
loginUserInfoKey, err := c.getLoginUserInfoKey(ctx, mountEntry, req)
if err != nil {
return false, err
}
// get entry from userFailedLoginInfo map for the key
userFailedLoginInfo, err := getUserFailedLoginInfo(ctx, c, loginUserInfoKey)
if err != nil {
return false, err
}
userLockoutConfiguration := c.getUserLockoutConfiguration(mountEntry)
switch userFailedLoginInfo {
case nil:
// entry not found in userFailedLoginInfo map, check storage to re-verify
ns, err := namespace.FromContext(ctx)
if err != nil {
return false, fmt.Errorf("could not parse namespace from http context: %w", err)
}
storageUserLockoutPath := fmt.Sprintf(coreLockedUsersPath+"%s/%s/%s", ns.ID, loginUserInfoKey.mountAccessor, loginUserInfoKey.aliasName)
existingEntry, err := c.barrier.Get(ctx, storageUserLockoutPath)
if err != nil {
return false, err
}
var lastLoginTime int
if existingEntry == nil {
// no storage entry found, user is not locked
return false, nil
}
err = jsonutil.DecodeJSON(existingEntry.Value, &lastLoginTime)
if err != nil {
return false, err
}
// if time passed from last login time is within lockout duration, the user is locked
if time.Now().Unix()-int64(lastLoginTime) < int64(userLockoutConfiguration.LockoutDuration.Seconds()) {
// user locked
return true, nil
} else {
// user is not locked. Entry is stale, remove this from storage
if err := c.barrier.Delete(ctx, storageUserLockoutPath); err != nil {
c.logger.Error("failed to cleanup storage entry for user", "path", storageUserLockoutPath, "error", err)
}
}
default:
// entry found in userFailedLoginInfo map, check if the user is locked
isCountOverLockoutThreshold := userFailedLoginInfo.count >= uint(userLockoutConfiguration.LockoutThreshold)
isWithinLockoutDuration := time.Now().Unix()-int64(userFailedLoginInfo.lastFailedLoginTime) < int64(userLockoutConfiguration.LockoutDuration.Seconds())
if isCountOverLockoutThreshold && isWithinLockoutDuration {
// user locked
return true, nil
}
}
return false, nil
}
// getUserLockoutConfiguration gets the user lockout configuration for a mount entry
// it checks the config file and auth tune values
// precedence: auth tune >> config file values for auth type >> config file values for all type
// >> default user lockout values
// getUserLockoutFromConfig call in this function takes care of config file precedence
func (c *Core) getUserLockoutConfiguration(mountEntry *MountEntry) (userLockoutConfig UserLockoutConfig) {
// get user configuration values from config file
userLockoutConfig = c.getUserLockoutFromConfig(mountEntry.Type)
authTuneUserLockoutConfig := mountEntry.Config.UserLockoutConfig
// if user lockout is not configured using auth tune, return values from config file
if authTuneUserLockoutConfig == nil {
return userLockoutConfig
}
// replace values in return with config file configuration
// for fields that are not configured using auth tune
if authTuneUserLockoutConfig.LockoutThreshold != 0 {
userLockoutConfig.LockoutThreshold = authTuneUserLockoutConfig.LockoutThreshold
}
if authTuneUserLockoutConfig.LockoutDuration != 0 {
userLockoutConfig.LockoutDuration = authTuneUserLockoutConfig.LockoutDuration
}
if authTuneUserLockoutConfig.LockoutCounterReset != 0 {
userLockoutConfig.LockoutCounterReset = authTuneUserLockoutConfig.LockoutCounterReset
}
if authTuneUserLockoutConfig.DisableLockout {
userLockoutConfig.DisableLockout = authTuneUserLockoutConfig.DisableLockout
}
return userLockoutConfig
}
// getUserLockoutFromConfig gets the userlockout configuration for given mount type from config file
// it reads the user lockout configuration from server config
// it has values for "all" type and any mountType that is configured using config file
// "all" type values are updated in shared config with default values i.e; if "all" type is
// not configured in config file, it is updated in shared config with default configuration
// If "all" type is configured in config file, any missing fields are updated with default values
// similarly missing values for a given mount type in config file are updated with "all" type
// default values
// If user_lockout configuration is not configured using config file at all, defaults are returned
func (c *Core) getUserLockoutFromConfig(mountType string) UserLockoutConfig {
defaultUserLockoutConfig := UserLockoutConfig{
LockoutThreshold: configutil.UserLockoutThresholdDefault,
LockoutDuration: configutil.UserLockoutDurationDefault,
LockoutCounterReset: configutil.UserLockoutCounterResetDefault,
DisableLockout: configutil.DisableUserLockoutDefault,
}
conf := c.rawConfig.Load()
if conf == nil {
return defaultUserLockoutConfig
}
userlockouts := conf.(*server.Config).UserLockouts
if userlockouts == nil {
return defaultUserLockoutConfig
}
for _, userLockoutConfig := range userlockouts {
switch userLockoutConfig.Type {
case "all":
defaultUserLockoutConfig = UserLockoutConfig{
LockoutThreshold: userLockoutConfig.LockoutThreshold,
LockoutDuration: userLockoutConfig.LockoutDuration,
LockoutCounterReset: userLockoutConfig.LockoutCounterReset,
DisableLockout: userLockoutConfig.DisableLockout,
}
case mountType:
return UserLockoutConfig{
LockoutThreshold: userLockoutConfig.LockoutThreshold,
LockoutDuration: userLockoutConfig.LockoutDuration,
LockoutCounterReset: userLockoutConfig.LockoutCounterReset,
DisableLockout: userLockoutConfig.DisableLockout,
}
}
}
return defaultUserLockoutConfig
}
func (c *Core) buildMfaEnforcementResponse(eConfig *mfa.MFAEnforcementConfig) (*logical.MFAConstraintAny, error) { func (c *Core) buildMfaEnforcementResponse(eConfig *mfa.MFAEnforcementConfig) (*logical.MFAConstraintAny, error) {
mfaAny := &logical.MFAConstraintAny{ mfaAny := &logical.MFAConstraintAny{
Any: []*logical.MFAMethodID{}, Any: []*logical.MFAMethodID{},
@ -1810,23 +2125,49 @@ func (c *Core) RegisterAuth(ctx context.Context, tokenTTL time.Duration, path st
if te.ExternalID != "" { if te.ExternalID != "" {
auth.ClientToken = te.ExternalID auth.ClientToken = te.ExternalID
} }
// Successful login, remove any entry from userFailedLoginInfo map
// if it exists. This is done for service tokens (for oss) here.
// For ent it is taken care by registerAuth RPC calls.
if auth.Alias != nil {
loginUserInfoKey := FailedLoginUser{
aliasName: auth.Alias.Name,
mountAccessor: auth.Alias.MountAccessor,
}
err = c.UpdateUserFailedLoginInfo(ctx, loginUserInfoKey, nil, true)
if err != nil {
return err
}
}
} }
return nil return nil
} }
// GetUserFailedLoginInfo gets the failed login information for a user based on alias name and mountAccessor // GetUserFailedLoginInfo gets the failed login information for a user based on alias name and mountAccessor
func (c *Core) GetUserFailedLoginInfo(ctx context.Context, userKey FailedLoginUser) *FailedLoginInfo { func (c *Core) GetUserFailedLoginInfo(ctx context.Context, userKey FailedLoginUser) *FailedLoginInfo {
return c.userFailedLoginInfo[userKey] c.userFailedLoginInfoLock.Lock()
value, exists := c.userFailedLoginInfo[userKey]
c.userFailedLoginInfoLock.Unlock()
if exists {
return value
}
return nil
} }
// UpdateUserFailedLoginInfo updates the failed login information for a user based on alias name and mountAccessor // UpdateUserFailedLoginInfo updates the failed login information for a user based on alias name and mountAccessor
func (c *Core) UpdateUserFailedLoginInfo(ctx context.Context, userKey FailedLoginUser, failedLoginInfo FailedLoginInfo) error { func (c *Core) UpdateUserFailedLoginInfo(ctx context.Context, userKey FailedLoginUser, failedLoginInfo *FailedLoginInfo, deleteEntry bool) error {
c.userFailedLoginInfo[userKey] = &failedLoginInfo c.userFailedLoginInfoLock.Lock()
switch deleteEntry {
case false:
// create or update entry in the map
c.userFailedLoginInfo[userKey] = failedLoginInfo
default:
// delete the entry from the map
delete(c.userFailedLoginInfo, userKey)
}
c.userFailedLoginInfoLock.Unlock()
// check if the update worked // check if the update worked
failedLoginResp := c.GetUserFailedLoginInfo(ctx, userKey) failedLoginResp := c.GetUserFailedLoginInfo(ctx, userKey)
if failedLoginResp == nil { if (failedLoginResp == nil && !deleteEntry) || (failedLoginResp != nil && deleteEntry) {
return fmt.Errorf("failed to update entry in userFailedLoginInfo map") return fmt.Errorf("failed to update entry in userFailedLoginInfo map")
} }
return nil return nil

View File

@ -50,12 +50,12 @@ func getAuthRegisterFunc(c *Core) (RegisterAuthFunc, error) {
return c.RegisterAuth, nil return c.RegisterAuth, nil
} }
func getUserFailedLoginInfo(ctx context.Context, c *Core, userInfo FailedLoginUser) *FailedLoginInfo { func getUserFailedLoginInfo(ctx context.Context, c *Core, userInfo FailedLoginUser) (*FailedLoginInfo, error) {
return c.GetUserFailedLoginInfo(ctx, userInfo) return c.GetUserFailedLoginInfo(ctx, userInfo), nil
} }
func (c *Core) updateUserFailedLoginInfo(ctx context.Context, userInfo FailedLoginUser, failedLoginInfo FailedLoginInfo) error { func updateUserFailedLoginInfo(ctx context.Context, c *Core, userInfo FailedLoginUser, failedLoginInfo *FailedLoginInfo, deleteEntry bool) error {
return c.UpdateUserFailedLoginInfo(ctx, userInfo, failedLoginInfo) return c.UpdateUserFailedLoginInfo(ctx, userInfo, failedLoginInfo, deleteEntry)
} }
func possiblyForwardAliasCreation(ctx context.Context, c *Core, inErr error, auth *logical.Auth, entity *identity.Entity) (*identity.Entity, bool, error) { func possiblyForwardAliasCreation(ctx context.Context, c *Core, inErr error, auth *logical.Auth, entity *identity.Entity) (*identity.Entity, bool, error) {