-
Notifications
You must be signed in to change notification settings - Fork 4.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Prevent Brute Forcing: Create an api endpoint to list locked users OS…
…S changes (#18675) * api to list lockedusers oss changes * add changelog
- Loading branch information
Showing
5 changed files
with
250 additions
and
32 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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]. | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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) | ||
} |