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:
akshya96 2022-12-19 14:24:42 -08:00 committed by GitHub
parent 3ccbddab0e
commit 4126060d88
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 188 additions and 0 deletions

4
changelog/18279.txt Normal file
View File

@ -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
```

View File

@ -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)
}
}

View File

@ -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",
`

View File

@ -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]),
},
}
}