Prevent Brute Forcing: Create an api endpoint to list locked users OSS changes (#18675)
* api to list lockedusers oss changes * add changelog
This commit is contained in:
parent
c9763996d4
commit
b2276a369a
|
@ -0,0 +1,4 @@
|
|||
```release-note:improvement
|
||||
core: Added sys/locked-users endpoint to list locked users. Changed api endpoint from
|
||||
sys/lockedusers/[mount_accessor]/unlock/[alias_identifier] to sys/locked-users/[mount_accessor]/unlock/[alias_identifier].
|
||||
```
|
|
@ -327,7 +327,7 @@ func TestIdentityStore_LockoutCounterResetTest(t *testing.T) {
|
|||
|
||||
// TestIdentityStore_UnlockUserTest tests the user is
|
||||
// unlocked if locked using
|
||||
// sys/lockedusers/[mount_accessor]/unlock/[alias-identifier]
|
||||
// sys/locked-users/[mount_accessor]/unlock/[alias-identifier]
|
||||
func TestIdentityStore_UnlockUserTest(t *testing.T) {
|
||||
coreConfig := &vault.CoreConfig{
|
||||
CredentialBackends: map[string]logical.Factory{
|
||||
|
@ -393,7 +393,7 @@ func TestIdentityStore_UnlockUserTest(t *testing.T) {
|
|||
}
|
||||
|
||||
// unlock user
|
||||
if _, err = standby.Logical().Write("sys/lockedusers/"+mountAccessor+"/unlock/bsmith", nil); err != nil {
|
||||
if _, err = standby.Logical().Write("sys/locked-users/"+mountAccessor+"/unlock/bsmith", nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
|
@ -405,7 +405,7 @@ func TestIdentityStore_UnlockUserTest(t *testing.T) {
|
|||
}
|
||||
|
||||
// unlock unlocked user
|
||||
if _, err = active.Logical().Write("sys/lockedusers/mountAccessor/unlock/bsmith", nil); err != nil {
|
||||
if _, err = active.Logical().Write("sys/locked-users/mountAccessor/unlock/bsmith", nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2208,6 +2208,27 @@ func (b *SystemBackend) handleTuneWriteCommon(ctx context.Context, path string,
|
|||
return resp, nil
|
||||
}
|
||||
|
||||
// handleLockedUsersMetricQuery reports the locked user count metrics for this namespace and all child namespaces
|
||||
// if mount_accessor in request, returns the locked user metrics for that mount accessor for namespace in ctx
|
||||
func (b *SystemBackend) handleLockedUsersMetricQuery(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
||||
var mountAccessor string
|
||||
if mountAccessorRaw, ok := d.GetOk("mount_accessor"); ok {
|
||||
mountAccessor = mountAccessorRaw.(string)
|
||||
}
|
||||
|
||||
results, err := b.handleLockedUsersQuery(ctx, mountAccessor)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if results == nil {
|
||||
return logical.RespondWithStatusCode(nil, req, http.StatusNoContent)
|
||||
}
|
||||
|
||||
return &logical.Response{
|
||||
Data: results,
|
||||
}, 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)
|
||||
|
@ -2232,33 +2253,6 @@ func (b *SystemBackend) handleUnlockUser(ctx context.Context, req *logical.Reque
|
|||
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)
|
||||
|
@ -5395,7 +5389,7 @@ the mount.`,
|
|||
"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
|
||||
POST sys/locked-users/:mount_accessor/unlock/:alias_identifier
|
||||
Unlocks the user with given mount_accessor and alias_identifier
|
||||
if locked.`,
|
||||
},
|
||||
|
@ -5405,6 +5399,14 @@ This path responds to the following HTTP methods.
|
|||
"",
|
||||
},
|
||||
|
||||
"locked_users": {
|
||||
"Report the locked user count metrics",
|
||||
`
|
||||
This path responds to the following HTTP methods.
|
||||
GET sys/locked-users
|
||||
Report the locked user count metrics, for current namespace and all child namespaces.`,
|
||||
},
|
||||
|
||||
"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
|
||||
|
|
|
@ -2069,7 +2069,7 @@ func (b *SystemBackend) experimentPaths() []*framework.Path {
|
|||
func (b *SystemBackend) lockedUserPaths() []*framework.Path {
|
||||
return []*framework.Path{
|
||||
{
|
||||
Pattern: "lockedusers/(?P<mount_accessor>.+?)/unlock/(?P<alias_identifier>.+)",
|
||||
Pattern: "locked-users/(?P<mount_accessor>.+?)/unlock/(?P<alias_identifier>.+)",
|
||||
Fields: map[string]*framework.FieldSchema{
|
||||
"mount_accessor": {
|
||||
Type: framework.TypeString,
|
||||
|
@ -2089,5 +2089,22 @@ func (b *SystemBackend) lockedUserPaths() []*framework.Path {
|
|||
HelpSynopsis: strings.TrimSpace(sysHelp["unlock_user"][0]),
|
||||
HelpDescription: strings.TrimSpace(sysHelp["unlock_user"][1]),
|
||||
},
|
||||
{
|
||||
Pattern: "locked-users",
|
||||
Fields: map[string]*framework.FieldSchema{
|
||||
"mount_accessor": {
|
||||
Type: framework.TypeString,
|
||||
Description: strings.TrimSpace(sysHelp["mount_accessor"][0]),
|
||||
},
|
||||
},
|
||||
Operations: map[logical.Operation]framework.OperationHandler{
|
||||
logical.ReadOperation: &framework.PathOperation{
|
||||
Callback: b.handleLockedUsersMetricQuery,
|
||||
Summary: "Report the locked user count metrics, for this namespace and all child namespaces.",
|
||||
},
|
||||
},
|
||||
HelpSynopsis: strings.TrimSpace(sysHelp["locked_users"][0]),
|
||||
HelpDescription: strings.TrimSpace(sysHelp["locked_users"][1]),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,195 @@
|
|||
package vault
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/vault/helper/namespace"
|
||||
)
|
||||
|
||||
type LockedUsersResponse struct {
|
||||
NamespaceID string `json:"namespace_id" mapstructure:"namespace_id"`
|
||||
NamespacePath string `json:"namespace_path" mapstructure:"namespace_path"`
|
||||
Counts int `json:"counts" mapstructure:"counts"`
|
||||
MountAccessors []*ResponseMountAccessors `json:"mount_accessors" mapstructure:"mount_accessors"`
|
||||
}
|
||||
|
||||
type ResponseMountAccessors struct {
|
||||
MountAccessor string `json:"mount_accessor" mapstructure:"mount_accessor"`
|
||||
Counts int `json:"counts" mapstructure:"counts"`
|
||||
AliasIdentifiers []string `json:"alias_identifiers" mapstructure:"alias_identifiers"`
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// handleLockedUsersQuery reports the locked user metrics by namespace in the decreasing order
|
||||
// of locked users
|
||||
func (b *SystemBackend) handleLockedUsersQuery(ctx context.Context, mountAccessor string) (map[string]interface{}, error) {
|
||||
// Calculate the namespace response breakdowns of locked users for query namespace and child namespaces (if needed)
|
||||
totalCount, byNamespaceResponse, err := b.getLockedUsersResponses(ctx, mountAccessor)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Now populate the response based on breakdowns.
|
||||
responseData := make(map[string]interface{})
|
||||
responseData["by_namespace"] = byNamespaceResponse
|
||||
responseData["total"] = totalCount
|
||||
return responseData, nil
|
||||
}
|
||||
|
||||
// getLockedUsersResponses returns the locked users
|
||||
// for a particular mount_accessor if provided in request
|
||||
// else returns it for the current namespace and all the child namespaces that has locked users
|
||||
// they are sorted in the decreasing order of locked users count
|
||||
func (b *SystemBackend) getLockedUsersResponses(ctx context.Context, mountAccessor string) (int, []*LockedUsersResponse, error) {
|
||||
lockedUsersResponse := make([]*LockedUsersResponse, 0)
|
||||
totalCounts := 0
|
||||
|
||||
queryNS, err := namespace.FromContext(ctx)
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
|
||||
if mountAccessor != "" {
|
||||
// get the locked user response for mount_accessor, here for mount_accessor in request
|
||||
totalCountForNSID, mountAccessorsResponse, err := b.getMountAccessorsLockedUsers(ctx, []string{mountAccessor + "/"},
|
||||
coreLockedUsersPath+queryNS.ID+"/")
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
|
||||
totalCounts += totalCountForNSID
|
||||
lockedUsersResponse = append(lockedUsersResponse, &LockedUsersResponse{
|
||||
NamespaceID: queryNS.ID,
|
||||
NamespacePath: queryNS.Path,
|
||||
Counts: totalCountForNSID,
|
||||
MountAccessors: mountAccessorsResponse,
|
||||
})
|
||||
return totalCounts, lockedUsersResponse, nil
|
||||
}
|
||||
|
||||
// no mount_accessor is provided in request, get information for current namespace and its child namespaces
|
||||
|
||||
// get all the namespaces of locked users
|
||||
nsIDs, err := b.Core.barrier.List(ctx, coreLockedUsersPath)
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
|
||||
// identify if the namespaces must be included in response and get counts
|
||||
for _, nsID := range nsIDs {
|
||||
nsID = strings.TrimSuffix(nsID, "/")
|
||||
ns, err := NamespaceByID(ctx, nsID, b.Core)
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
|
||||
if b.includeNSInLockedUsersResponse(queryNS, ns) {
|
||||
var displayPath string
|
||||
if ns == nil {
|
||||
// deleted namespace
|
||||
displayPath = fmt.Sprintf("deleted namespace %q", nsID)
|
||||
} else {
|
||||
displayPath = ns.Path
|
||||
}
|
||||
|
||||
// get mount accessors of locked users for this namespace
|
||||
mountAccessors, err := b.Core.barrier.List(ctx, coreLockedUsersPath+nsID+"/")
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
|
||||
// get the locked user response for mount_accessor list
|
||||
totalCountForNSID, mountAccessorsResponse, err := b.getMountAccessorsLockedUsers(ctx, mountAccessors, coreLockedUsersPath+nsID+"/")
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
|
||||
totalCounts += totalCountForNSID
|
||||
lockedUsersResponse = append(lockedUsersResponse, &LockedUsersResponse{
|
||||
NamespaceID: strings.TrimSuffix(nsID, "/"),
|
||||
NamespacePath: displayPath,
|
||||
Counts: totalCountForNSID,
|
||||
MountAccessors: mountAccessorsResponse,
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// sort namespaces in response by decreasing order of counts
|
||||
sort.Slice(lockedUsersResponse, func(i, j int) bool {
|
||||
return lockedUsersResponse[i].Counts > lockedUsersResponse[j].Counts
|
||||
})
|
||||
|
||||
return totalCounts, lockedUsersResponse, nil
|
||||
}
|
||||
|
||||
// getMountAccessorsLockedUsers returns the locked users for all the mount_accessors of locked users for a namespace
|
||||
// they are sorted in the decreasing order of locked users
|
||||
// returns the total locked users for the namespace and locked users response for every mount_accessor for a namespace that has locked users
|
||||
func (b *SystemBackend) getMountAccessorsLockedUsers(ctx context.Context, mountAccessors []string, lockedUsersPath string) (int, []*ResponseMountAccessors, error) {
|
||||
byMountAccessorsResponse := make([]*ResponseMountAccessors, 0)
|
||||
totalCountForMountAccessors := 0
|
||||
|
||||
for _, mountAccessor := range mountAccessors {
|
||||
// get the list of aliases of locked users for a mount accessor
|
||||
aliasIdentifiers, err := b.Core.barrier.List(ctx, lockedUsersPath+mountAccessor)
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
|
||||
totalCountForMountAccessors += len(aliasIdentifiers)
|
||||
byMountAccessorsResponse = append(byMountAccessorsResponse, &ResponseMountAccessors{
|
||||
MountAccessor: strings.TrimSuffix(mountAccessor, "/"),
|
||||
Counts: len(aliasIdentifiers),
|
||||
AliasIdentifiers: aliasIdentifiers,
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
// sort mount Accessors in response by decreasing order of counts
|
||||
sort.Slice(byMountAccessorsResponse, func(i, j int) bool {
|
||||
return byMountAccessorsResponse[i].Counts > byMountAccessorsResponse[j].Counts
|
||||
})
|
||||
|
||||
return totalCountForMountAccessors, byMountAccessorsResponse, nil
|
||||
}
|
||||
|
||||
// includeNSInLockedUsersResponse checks if the namespace is the child namespace of namespace in query
|
||||
// if child namespace, it can be included in response
|
||||
// locked users from deleted namespaces are listed under root namespace
|
||||
func (b *SystemBackend) includeNSInLockedUsersResponse(query *namespace.Namespace, record *namespace.Namespace) bool {
|
||||
if record == nil {
|
||||
// Deleted namespace, only include in root queries
|
||||
return query.ID == namespace.RootNamespaceID
|
||||
}
|
||||
return record.HasParent(query)
|
||||
}
|
Loading…
Reference in New Issue