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)
|
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.auditPaths()...)
|
||||||
b.Backend.Paths = append(b.Backend.Paths, b.mountPaths()...)
|
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.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.leasePaths()...)
|
||||||
b.Backend.Paths = append(b.Backend.Paths, b.policyPaths()...)
|
b.Backend.Paths = append(b.Backend.Paths, b.policyPaths()...)
|
||||||
b.Backend.Paths = append(b.Backend.Paths, b.wrappingPaths()...)
|
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
|
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
|
// 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) {
|
func (b *SystemBackend) handleLeaseLookup(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
||||||
leaseID := data.Get("lease_id").(string)
|
leaseID := data.Get("lease_id").(string)
|
||||||
|
@ -5307,6 +5359,27 @@ the auth path.`,
|
||||||
the mount.`,
|
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": {
|
||||||
"Renew a lease on a secret",
|
"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