Prevent Brute Forcing: Create api endpoint to unlock users (#18279)
* code changes for unlock * add test * adding sys help * adding sys help * updating unlock user function * edit test * add changelog * syshelp * adding open api response definition * removing response fields * change path name
This commit is contained in:
parent
3ccbddab0e
commit
4126060d88
|
@ -0,0 +1,4 @@
|
|||
```release-note:improvement
|
||||
core: Added sys/lockedusers/[mount_accessor]/unlock/[alias_identifier] endpoint to unlock an user
|
||||
with given mount_accessor and alias_identifier if locked
|
||||
```
|
|
@ -324,3 +324,88 @@ func TestIdentityStore_LockoutCounterResetTest(t *testing.T) {
|
|||
t.Fatalf("expected to see invalid username or password error as user is not locked out, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestIdentityStore_UnlockUserTest tests the user is
|
||||
// unlocked if locked using
|
||||
// sys/lockedusers/[mount_accessor]/unlock/[alias-identifier]
|
||||
func TestIdentityStore_UnlockUserTest(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
|
||||
|
||||
// enable userpass auth method on path userpass
|
||||
if err := active.Sys().EnableAuthWithOptions("userpass", &api.EnableAuthOptions{
|
||||
Type: "userpass",
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// get mount accessor for userpass mount
|
||||
secret, err := standby.Logical().Read("sys/auth/userpass")
|
||||
if err != nil || secret == nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
mountAccessor := secret.Data["accessor"].(string)
|
||||
|
||||
// tune auth mount
|
||||
userlockoutConfig := &api.UserLockoutConfigInput{
|
||||
LockoutThreshold: "2",
|
||||
LockoutDuration: "5m",
|
||||
}
|
||||
if err = active.Sys().TuneMount("auth/userpass", api.MountConfigInput{
|
||||
UserLockoutConfig: userlockoutConfig,
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// create a user for userpass
|
||||
if _, err = standby.Logical().Write("auth/userpass/users/bsmith", map[string]interface{}{
|
||||
"password": "training",
|
||||
}); 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 : permission denied as user locked out
|
||||
if _, err = standby.Logical().Write("auth/userpass/login/bsmith", map[string]interface{}{
|
||||
"password": "training",
|
||||
}); 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)
|
||||
}
|
||||
|
||||
// unlock user
|
||||
if _, err = standby.Logical().Write("sys/lockedusers/"+mountAccessor+"/unlock/bsmith", nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// login: should be successful as user unlocked
|
||||
if _, err = standby.Logical().Write("auth/userpass/login/bsmith", map[string]interface{}{
|
||||
"password": "training",
|
||||
}); err != nil {
|
||||
t.Fatal("expected login to succeed as user is unlocked")
|
||||
}
|
||||
|
||||
// unlock unlocked user
|
||||
if _, err = active.Logical().Write("sys/lockedusers/mountAccessor/unlock/bsmith", nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -176,6 +176,7 @@ func NewSystemBackend(core *Core, logger log.Logger) *SystemBackend {
|
|||
b.Backend.Paths = append(b.Backend.Paths, b.auditPaths()...)
|
||||
b.Backend.Paths = append(b.Backend.Paths, b.mountPaths()...)
|
||||
b.Backend.Paths = append(b.Backend.Paths, b.authPaths()...)
|
||||
b.Backend.Paths = append(b.Backend.Paths, b.lockedUserPaths()...)
|
||||
b.Backend.Paths = append(b.Backend.Paths, b.leasePaths()...)
|
||||
b.Backend.Paths = append(b.Backend.Paths, b.policyPaths()...)
|
||||
b.Backend.Paths = append(b.Backend.Paths, b.wrappingPaths()...)
|
||||
|
@ -2204,6 +2205,57 @@ func (b *SystemBackend) handleTuneWriteCommon(ctx context.Context, path string,
|
|||
return resp, nil
|
||||
}
|
||||
|
||||
// handleUnlockUser is used to unlock user with given mount_accessor and alias_identifier if locked
|
||||
func (b *SystemBackend) handleUnlockUser(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
||||
mountAccessor := data.Get("mount_accessor").(string)
|
||||
if mountAccessor == "" {
|
||||
return logical.ErrorResponse(
|
||||
"missing mount_accessor"),
|
||||
logical.ErrInvalidRequest
|
||||
}
|
||||
|
||||
aliasName := data.Get("alias_identifier").(string)
|
||||
if aliasName == "" {
|
||||
return logical.ErrorResponse(
|
||||
"missing alias_identifier"),
|
||||
logical.ErrInvalidRequest
|
||||
}
|
||||
|
||||
if err := unlockUser(ctx, b.Core, mountAccessor, aliasName); err != nil {
|
||||
b.Backend.Logger().Error("unlock user failed", "mount accessor", mountAccessor, "alias identifier", aliasName, "error", err)
|
||||
return handleError(err)
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// unlockUser deletes the entry for locked user from storage and userFailedLoginInfo map
|
||||
func unlockUser(ctx context.Context, core *Core, mountAccessor string, aliasName string) error {
|
||||
ns, err := namespace.FromContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
lockedUserStoragePath := coreLockedUsersPath + ns.ID + "/" + mountAccessor + "/" + aliasName
|
||||
|
||||
// remove entry for locked user from storage
|
||||
if err := core.barrier.Delete(ctx, lockedUserStoragePath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
loginUserInfoKey := FailedLoginUser{
|
||||
aliasName: aliasName,
|
||||
mountAccessor: mountAccessor,
|
||||
}
|
||||
|
||||
// remove entry for locked user from userFailedLoginInfo map
|
||||
if err := updateUserFailedLoginInfo(ctx, core, loginUserInfoKey, nil, true); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleLease is use to view the metadata for a given LeaseID
|
||||
func (b *SystemBackend) handleLeaseLookup(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
||||
leaseID := data.Get("lease_id").(string)
|
||||
|
@ -5307,6 +5359,27 @@ the auth path.`,
|
|||
the mount.`,
|
||||
},
|
||||
|
||||
"unlock_user": {
|
||||
"Unlock the locked user with given mount_accessor and alias_identifier.",
|
||||
`
|
||||
This path responds to the following HTTP methods.
|
||||
POST sys/lockedusers/:mount_accessor/unlock/:alias_identifier
|
||||
Unlocks the user with given mount_accessor and alias_identifier
|
||||
if locked.`,
|
||||
},
|
||||
|
||||
"mount_accessor": {
|
||||
"MountAccessor is the identifier of the mount entry to which the user belongs",
|
||||
"",
|
||||
},
|
||||
|
||||
"alias_identifier": {
|
||||
`It is the name of the alias (user). For example, if the alias belongs to userpass backend,
|
||||
the name should be a valid username within userpass auth method. If the alias belongs
|
||||
to an approle auth method, the name should be a valid RoleID`,
|
||||
"",
|
||||
},
|
||||
|
||||
"renew": {
|
||||
"Renew a lease on a secret",
|
||||
`
|
||||
|
|
|
@ -2057,3 +2057,29 @@ func (b *SystemBackend) mountPaths() []*framework.Path {
|
|||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (b *SystemBackend) lockedUserPaths() []*framework.Path {
|
||||
return []*framework.Path{
|
||||
{
|
||||
Pattern: "lockedusers/(?P<mount_accessor>.+?)/unlock/(?P<alias_identifier>.+)",
|
||||
Fields: map[string]*framework.FieldSchema{
|
||||
"mount_accessor": {
|
||||
Type: framework.TypeString,
|
||||
Description: strings.TrimSpace(sysHelp["mount_accessor"][0]),
|
||||
},
|
||||
"alias_identifier": {
|
||||
Type: framework.TypeString,
|
||||
Description: strings.TrimSpace(sysHelp["alias_identifier"][0]),
|
||||
},
|
||||
},
|
||||
Operations: map[logical.Operation]framework.OperationHandler{
|
||||
logical.UpdateOperation: &framework.PathOperation{
|
||||
Callback: b.handleUnlockUser,
|
||||
Summary: "Unlocks the user with given mount_accessor and alias_identifier",
|
||||
},
|
||||
},
|
||||
HelpSynopsis: strings.TrimSpace(sysHelp["unlock_user"][0]),
|
||||
HelpDescription: strings.TrimSpace(sysHelp["unlock_user"][1]),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue