mirror of
				https://github.com/optim-enterprises-bv/vault.git
				synced 2025-10-30 18:17:55 +00:00 
			
		
		
		
	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:
		
							
								
								
									
										4
									
								
								changelog/18675.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								changelog/18675.txt
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | // TestIdentityStore_UnlockUserTest tests the user is | ||||||
| // unlocked if locked  using | // 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) { | func TestIdentityStore_UnlockUserTest(t *testing.T) { | ||||||
| 	coreConfig := &vault.CoreConfig{ | 	coreConfig := &vault.CoreConfig{ | ||||||
| 		CredentialBackends: map[string]logical.Factory{ | 		CredentialBackends: map[string]logical.Factory{ | ||||||
| @@ -393,7 +393,7 @@ func TestIdentityStore_UnlockUserTest(t *testing.T) { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// unlock user | 	// 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) | 		t.Fatal(err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| @@ -405,7 +405,7 @@ func TestIdentityStore_UnlockUserTest(t *testing.T) { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// unlock unlocked user | 	// 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) | 		t.Fatal(err) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|   | |||||||
| @@ -2208,6 +2208,27 @@ func (b *SystemBackend) handleTuneWriteCommon(ctx context.Context, path string, | |||||||
| 	return resp, nil | 	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 | // 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) { | func (b *SystemBackend) handleUnlockUser(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { | ||||||
| 	mountAccessor := data.Get("mount_accessor").(string) | 	mountAccessor := data.Get("mount_accessor").(string) | ||||||
| @@ -2232,33 +2253,6 @@ func (b *SystemBackend) handleUnlockUser(ctx context.Context, req *logical.Reque | |||||||
| 	return nil, nil | 	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) | ||||||
| @@ -5395,7 +5389,7 @@ the mount.`, | |||||||
| 		"Unlock the locked user with given mount_accessor and alias_identifier.", | 		"Unlock the locked user with given mount_accessor and alias_identifier.", | ||||||
| 		` | 		` | ||||||
| This path responds to the following HTTP methods. | 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 | 		Unlocks the user with given mount_accessor and alias_identifier | ||||||
| 		if locked.`, | 		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": { | 	"alias_identifier": { | ||||||
| 		`It is the name of the alias (user). For example, if the alias belongs to userpass backend,  | 		`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 | 	   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 { | func (b *SystemBackend) lockedUserPaths() []*framework.Path { | ||||||
| 	return []*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{ | 			Fields: map[string]*framework.FieldSchema{ | ||||||
| 				"mount_accessor": { | 				"mount_accessor": { | ||||||
| 					Type:        framework.TypeString, | 					Type:        framework.TypeString, | ||||||
| @@ -2089,5 +2089,22 @@ func (b *SystemBackend) lockedUserPaths() []*framework.Path { | |||||||
| 			HelpSynopsis:    strings.TrimSpace(sysHelp["unlock_user"][0]), | 			HelpSynopsis:    strings.TrimSpace(sysHelp["unlock_user"][0]), | ||||||
| 			HelpDescription: strings.TrimSpace(sysHelp["unlock_user"][1]), | 			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]), | ||||||
|  | 		}, | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										195
									
								
								vault/logical_system_user_lockout.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										195
									
								
								vault/logical_system_user_lockout.go
									
									
									
									
									
										Normal file
									
								
							| @@ -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) | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user
	 akshya96
					akshya96