2023-03-15 19:00:52 +03:00
|
|
|
// Copyright (c) HashiCorp, Inc.
|
2024-01-02 21:36:20 +03:00
|
|
|
// SPDX-License-Identifier: BUSL-1.1
|
2023-03-15 19:00:52 +03:00
|
|
|
|
2023-01-18 01:25:56 +03:00
|
|
|
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
|
2023-05-31 20:51:20 +03:00
|
|
|
// if read only error, the error is handled by handleError in logical_system.go
|
|
|
|
// this will be forwarded to the active node
|
2023-01-18 01:25:56 +03:00
|
|
|
if err := core.barrier.Delete(ctx, lockedUserStoragePath); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
loginUserInfoKey := FailedLoginUser{
|
|
|
|
aliasName: aliasName,
|
|
|
|
mountAccessor: mountAccessor,
|
|
|
|
}
|
|
|
|
|
2023-05-31 20:51:20 +03:00
|
|
|
// remove entry for locked user from userFailedLoginInfo map and storage
|
2023-01-18 01:25:56 +03:00
|
|
|
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)
|
|
|
|
}
|