mirror of
				https://github.com/optim-enterprises-bv/vault.git
				synced 2025-10-31 02:28:09 +00:00 
			
		
		
		
	Add bound cidrs to tokens in AppRole (#4680)
This commit is contained in:
		 Becca Petrin
					Becca Petrin
				
			
				
					committed by
					
						 Jeff Mitchell
						Jeff Mitchell
					
				
			
			
				
	
			
			
			 Jeff Mitchell
						Jeff Mitchell
					
				
			
						parent
						
							bd99c43e9c
						
					
				
				
					commit
					b3a711d717
				
			| @@ -8,6 +8,7 @@ import ( | ||||
|  | ||||
| 	"github.com/hashicorp/errwrap" | ||||
| 	"github.com/hashicorp/vault/helper/cidrutil" | ||||
| 	"github.com/hashicorp/vault/helper/parseutil" | ||||
| 	"github.com/hashicorp/vault/logical" | ||||
| 	"github.com/hashicorp/vault/logical/framework" | ||||
| ) | ||||
| @@ -160,7 +161,7 @@ func (b *backend) pathLoginUpdate(ctx context.Context, req *logical.Request, dat | ||||
|  | ||||
| 			// Ensure that the CIDRs on the secret ID are still a subset of that of | ||||
| 			// role's | ||||
| 			err = verifyCIDRRoleSecretIDSubset(entry.CIDRList, role.BoundCIDRList) | ||||
| 			err = verifyCIDRRoleSecretIDSubset(entry.CIDRList, role.SecretIDBoundCIDRs) | ||||
| 			if err != nil { | ||||
| 				return nil, err | ||||
| 			} | ||||
| @@ -228,7 +229,7 @@ func (b *backend) pathLoginUpdate(ctx context.Context, req *logical.Request, dat | ||||
|  | ||||
| 			// Ensure that the CIDRs on the secret ID are still a subset of that of | ||||
| 			// role's | ||||
| 			err = verifyCIDRRoleSecretIDSubset(entry.CIDRList, role.BoundCIDRList) | ||||
| 			err = verifyCIDRRoleSecretIDSubset(entry.CIDRList, role.SecretIDBoundCIDRs) | ||||
| 			if err != nil { | ||||
| 				return nil, err | ||||
| 			} | ||||
| @@ -250,17 +251,22 @@ func (b *backend) pathLoginUpdate(ctx context.Context, req *logical.Request, dat | ||||
| 		metadata = entry.Metadata | ||||
| 	} | ||||
|  | ||||
| 	if len(role.BoundCIDRList) != 0 { | ||||
| 	if len(role.SecretIDBoundCIDRs) != 0 { | ||||
| 		if req.Connection == nil || req.Connection.RemoteAddr == "" { | ||||
| 			return nil, fmt.Errorf("failed to get connection information") | ||||
| 		} | ||||
|  | ||||
| 		belongs, err := cidrutil.IPBelongsToCIDRBlocksSlice(req.Connection.RemoteAddr, role.BoundCIDRList) | ||||
| 		belongs, err := cidrutil.IPBelongsToCIDRBlocksSlice(req.Connection.RemoteAddr, role.SecretIDBoundCIDRs) | ||||
| 		if err != nil || !belongs { | ||||
| 			return logical.ErrorResponse(errwrap.Wrapf(fmt.Sprintf("source address %q unauthorized by CIDR restrictions on the role: {{err}}", req.Connection.RemoteAddr), err).Error()), nil | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// Parse the CIDRs we should be binding the token to. | ||||
| 	tokenBoundCIDRs, err := parseutil.ParseAddrs(role.TokenBoundCIDRs) | ||||
| 	if err != nil { | ||||
| 		return logical.ErrorResponse(err.Error()), nil | ||||
| 	} | ||||
|  | ||||
| 	// For some reason, if metadata was set to nil while processing secret ID | ||||
| 	// binding, ensure that it is initialized again to avoid a panic. | ||||
| 	if metadata == nil { | ||||
| @@ -286,6 +292,7 @@ func (b *backend) pathLoginUpdate(ctx context.Context, req *logical.Request, dat | ||||
| 		Alias: &logical.Alias{ | ||||
| 			Name: role.RoleID, | ||||
| 		}, | ||||
| 		BoundCIDRs: tokenBoundCIDRs, | ||||
| 	} | ||||
|  | ||||
| 	return &logical.Response{ | ||||
|   | ||||
| @@ -2,6 +2,7 @@ package approle | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"strings" | ||||
| 	"time" | ||||
| @@ -55,12 +56,20 @@ type roleStorageEntry struct { | ||||
| 	// A constraint, if set, requires 'secret_id' credential to be presented during login | ||||
| 	BindSecretID bool `json:"bind_secret_id" mapstructure:"bind_secret_id"` | ||||
|  | ||||
| 	// A constraint, if set, specifies the CIDR blocks from which logins should be allowed | ||||
| 	// Deprecated: A constraint, if set, specifies the CIDR blocks from which logins should be allowed, | ||||
| 	// please use SecretIDBoundCIDRs instead. | ||||
| 	BoundCIDRListOld string `json:"bound_cidr_list,omitempty"` | ||||
|  | ||||
| 	// A constraint, if set, specifies the CIDR blocks from which logins should be allowed | ||||
| 	// Deprecated: A constraint, if set, specifies the CIDR blocks from which logins should be allowed, | ||||
| 	// please use SecretIDBoundCIDRs instead. | ||||
| 	BoundCIDRList []string `json:"bound_cidr_list_list" mapstructure:"bound_cidr_list"` | ||||
|  | ||||
| 	// A constraint, if set, specifies the CIDR blocks from which logins should be allowed | ||||
| 	SecretIDBoundCIDRs []string `json:"secret_id_bound_cidrs" mapstructure:"secret_id_bound_cidrs"` | ||||
|  | ||||
| 	// A constraint, if set, specifies the CIDR blocks from which token use should be allowed | ||||
| 	TokenBoundCIDRs []string `json:"token_bound_cidrs" mapstructure:"token_bound_cidrs"` | ||||
|  | ||||
| 	// Period, if set, indicates that the token generated using this role | ||||
| 	// should never expire. The token should be renewed within the duration | ||||
| 	// specified by this value. The renewal duration will be fixed if the | ||||
| @@ -125,10 +134,21 @@ func rolePaths(b *backend) []*framework.Path { | ||||
| 					Default:     true, | ||||
| 					Description: "Impose secret_id to be presented when logging in using this role. Defaults to 'true'.", | ||||
| 				}, | ||||
| 				// Deprecated | ||||
| 				"bound_cidr_list": &framework.FieldSchema{ | ||||
| 					Type: framework.TypeCommaStringSlice, | ||||
| 					Description: `Deprecated: Please use "secret_id_bound_cidrs" instead. Comma separated string or list  | ||||
| of CIDR blocks. If set, specifies the blocks of IP addresses which can perform the login operation.`, | ||||
| 				}, | ||||
| 				"secret_id_bound_cidrs": &framework.FieldSchema{ | ||||
| 					Type: framework.TypeCommaStringSlice, | ||||
| 					Description: `Comma separated string or list of CIDR blocks. If set, specifies the blocks of | ||||
| IP addresses which can perform the login operation.`, | ||||
| 				}, | ||||
| 				"token_bound_cidrs": &framework.FieldSchema{ | ||||
| 					Type: framework.TypeCommaStringSlice, | ||||
| 					Description: `Comma separated string or list of CIDR blocks. If set, specifies the blocks of | ||||
| IP addresses which can use the returned token.`, | ||||
| 				}, | ||||
| 				"policies": &framework.FieldSchema{ | ||||
| 					Type:        framework.TypeCommaStringSlice, | ||||
| @@ -231,18 +251,60 @@ can only be set during role creation and once set, it can't be reset later.`, | ||||
| 				}, | ||||
| 				"bound_cidr_list": &framework.FieldSchema{ | ||||
| 					Type: framework.TypeCommaStringSlice, | ||||
| 					Description: `Comma separated string or list of CIDR blocks. If set, specifies the blocks of | ||||
| IP addresses which can perform the login operation.`, | ||||
| 					Description: `Deprecated: Please use "secret_id_bound_cidrs" instead. Comma separated string or list  | ||||
| of CIDR blocks. If set, specifies the blocks of IP addresses which can perform the login operation.`, | ||||
| 				}, | ||||
| 			}, | ||||
| 			Callbacks: map[logical.Operation]framework.OperationFunc{ | ||||
| 				logical.UpdateOperation: b.pathRoleBoundCIDRListUpdate, | ||||
| 				logical.UpdateOperation: b.pathRoleBoundCIDRUpdate, | ||||
| 				logical.ReadOperation:   b.pathRoleBoundCIDRListRead, | ||||
| 				logical.DeleteOperation: b.pathRoleBoundCIDRListDelete, | ||||
| 			}, | ||||
| 			HelpSynopsis:    strings.TrimSpace(roleHelp["role-bound-cidr-list"][0]), | ||||
| 			HelpDescription: strings.TrimSpace(roleHelp["role-bound-cidr-list"][1]), | ||||
| 		}, | ||||
| 		&framework.Path{ | ||||
| 			Pattern: "role/" + framework.GenericNameRegex("role_name") + "/secret-id-bound-cidrs$", | ||||
| 			Fields: map[string]*framework.FieldSchema{ | ||||
| 				"role_name": &framework.FieldSchema{ | ||||
| 					Type:        framework.TypeString, | ||||
| 					Description: "Name of the role.", | ||||
| 				}, | ||||
| 				"secret_id_bound_cidrs": &framework.FieldSchema{ | ||||
| 					Type: framework.TypeCommaStringSlice, | ||||
| 					Description: `Comma separated string or list of CIDR blocks. If set, specifies the blocks of | ||||
| IP addresses which can perform the login operation.`, | ||||
| 				}, | ||||
| 			}, | ||||
| 			Callbacks: map[logical.Operation]framework.OperationFunc{ | ||||
| 				logical.UpdateOperation: b.pathRoleBoundCIDRUpdate, | ||||
| 				logical.ReadOperation:   b.pathRoleSecretIDBoundCIDRRead, | ||||
| 				logical.DeleteOperation: b.pathRoleSecretIDBoundCIDRDelete, | ||||
| 			}, | ||||
| 			HelpSynopsis:    strings.TrimSpace(roleHelp["secret-id-bound-cidrs"][0]), | ||||
| 			HelpDescription: strings.TrimSpace(roleHelp["secret-id-bound-cidrs"][1]), | ||||
| 		}, | ||||
| 		&framework.Path{ | ||||
| 			Pattern: "role/" + framework.GenericNameRegex("role_name") + "/token-bound-cidrs$", | ||||
| 			Fields: map[string]*framework.FieldSchema{ | ||||
| 				"role_name": &framework.FieldSchema{ | ||||
| 					Type:        framework.TypeString, | ||||
| 					Description: "Name of the role.", | ||||
| 				}, | ||||
| 				"token_bound_cidrs": &framework.FieldSchema{ | ||||
| 					Type: framework.TypeCommaStringSlice, | ||||
| 					Description: `Comma separated string or list of CIDR blocks. If set, specifies the blocks of | ||||
| IP addresses which can use the returned token.`, | ||||
| 				}, | ||||
| 			}, | ||||
| 			Callbacks: map[logical.Operation]framework.OperationFunc{ | ||||
| 				logical.UpdateOperation: b.pathRoleBoundCIDRUpdate, | ||||
| 				logical.ReadOperation:   b.pathRoleTokenBoundCIDRRead, | ||||
| 				logical.DeleteOperation: b.pathRoleTokenBoundCIDRDelete, | ||||
| 			}, | ||||
| 			HelpSynopsis:    strings.TrimSpace(roleHelp["token-bound-cidrs"][0]), | ||||
| 			HelpDescription: strings.TrimSpace(roleHelp["token-bound-cidrs"][1]), | ||||
| 		}, | ||||
| 		&framework.Path{ | ||||
| 			Pattern: "role/" + framework.GenericNameRegex("role_name") + "/bind-secret-id$", | ||||
| 			Fields: map[string]*framework.FieldSchema{ | ||||
| @@ -662,6 +724,8 @@ func validateRoleConstraints(role *roleStorageEntry) error { | ||||
| 	switch { | ||||
| 	case role.BindSecretID: | ||||
| 	case len(role.BoundCIDRList) != 0: | ||||
| 	case len(role.SecretIDBoundCIDRs) != 0: | ||||
| 	case len(role.TokenBoundCIDRs) != 0: | ||||
| 	default: | ||||
| 		return fmt.Errorf("at least one constraint should be enabled on the role") | ||||
| 	} | ||||
| @@ -749,11 +813,17 @@ func (b *backend) roleEntry(ctx context.Context, s logical.Storage, roleName str | ||||
| 	needsUpgrade := false | ||||
|  | ||||
| 	if role.BoundCIDRListOld != "" { | ||||
| 		role.BoundCIDRList = strings.Split(role.BoundCIDRListOld, ",") | ||||
| 		role.SecretIDBoundCIDRs = strutil.ParseDedupAndSortStrings(role.BoundCIDRListOld, ",") | ||||
| 		role.BoundCIDRListOld = "" | ||||
| 		needsUpgrade = true | ||||
| 	} | ||||
|  | ||||
| 	if len(role.BoundCIDRList) != 0 { | ||||
| 		role.SecretIDBoundCIDRs = role.BoundCIDRList | ||||
| 		role.BoundCIDRList = nil | ||||
| 		needsUpgrade = true | ||||
| 	} | ||||
|  | ||||
| 	if role.SecretIDPrefix == "" { | ||||
| 		role.SecretIDPrefix = secretIDPrefix | ||||
| 		needsUpgrade = true | ||||
| @@ -847,14 +917,26 @@ func (b *backend) pathRoleCreateUpdate(ctx context.Context, req *logical.Request | ||||
| 		role.BindSecretID = data.Get("bind_secret_id").(bool) | ||||
| 	} | ||||
|  | ||||
| 	if boundCIDRListRaw, ok := data.GetOk("bound_cidr_list"); ok { | ||||
| 		role.BoundCIDRList = boundCIDRListRaw.([]string) | ||||
| 	} else if req.Operation == logical.CreateOperation { | ||||
| 		role.BoundCIDRList = data.Get("bound_cidr_list").([]string) | ||||
| 	if boundCIDRListRaw, ok := data.GetFirst("secret_id_bound_cidrs", "bound_cidr_list"); ok { | ||||
| 		role.SecretIDBoundCIDRs = boundCIDRListRaw.([]string) | ||||
| 	} | ||||
|  | ||||
| 	if len(role.BoundCIDRList) != 0 { | ||||
| 		valid, err := cidrutil.ValidateCIDRListSlice(role.BoundCIDRList) | ||||
| 	if len(role.SecretIDBoundCIDRs) != 0 { | ||||
| 		valid, err := cidrutil.ValidateCIDRListSlice(role.SecretIDBoundCIDRs) | ||||
| 		if err != nil { | ||||
| 			return nil, errwrap.Wrapf("failed to validate CIDR blocks: {{err}}", err) | ||||
| 		} | ||||
| 		if !valid { | ||||
| 			return logical.ErrorResponse("invalid CIDR blocks"), nil | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	if boundCIDRListRaw, ok := data.GetOk("token_bound_cidrs"); ok { | ||||
| 		role.TokenBoundCIDRs = boundCIDRListRaw.([]string) | ||||
| 	} | ||||
|  | ||||
| 	if len(role.TokenBoundCIDRs) != 0 { | ||||
| 		valid, err := cidrutil.ValidateCIDRListSlice(role.TokenBoundCIDRs) | ||||
| 		if err != nil { | ||||
| 			return nil, errwrap.Wrapf("failed to validate CIDR blocks: {{err}}", err) | ||||
| 		} | ||||
| @@ -955,16 +1037,20 @@ func (b *backend) pathRoleRead(ctx context.Context, req *logical.Request, data * | ||||
| 	} | ||||
|  | ||||
| 	respData := map[string]interface{}{ | ||||
| 		"bind_secret_id":     role.BindSecretID, | ||||
| 		"bound_cidr_list":    role.BoundCIDRList, | ||||
| 		"period":             role.Period / time.Second, | ||||
| 		"policies":           role.Policies, | ||||
| 		"secret_id_num_uses": role.SecretIDNumUses, | ||||
| 		"secret_id_ttl":      role.SecretIDTTL / time.Second, | ||||
| 		"token_max_ttl":      role.TokenMaxTTL / time.Second, | ||||
| 		"token_num_uses":     role.TokenNumUses, | ||||
| 		"token_ttl":          role.TokenTTL / time.Second, | ||||
| 		"local_secret_ids":   false, | ||||
| 		"bind_secret_id": role.BindSecretID, | ||||
| 		// TODO - remove this deprecated field in future versions, | ||||
| 		// and its associated warning below. | ||||
| 		"bound_cidr_list":       role.SecretIDBoundCIDRs, | ||||
| 		"secret_id_bound_cidrs": role.SecretIDBoundCIDRs, | ||||
| 		"token_bound_cidrs":     role.TokenBoundCIDRs, | ||||
| 		"period":                role.Period / time.Second, | ||||
| 		"policies":              role.Policies, | ||||
| 		"secret_id_num_uses":    role.SecretIDNumUses, | ||||
| 		"secret_id_ttl":         role.SecretIDTTL / time.Second, | ||||
| 		"token_max_ttl":         role.TokenMaxTTL / time.Second, | ||||
| 		"token_num_uses":        role.TokenNumUses, | ||||
| 		"token_ttl":             role.TokenTTL / time.Second, | ||||
| 		"local_secret_ids":      false, | ||||
| 	} | ||||
|  | ||||
| 	if role.SecretIDPrefix == secretIDLocalPrefix { | ||||
| @@ -978,6 +1064,7 @@ func (b *backend) pathRoleRead(ctx context.Context, req *logical.Request, data * | ||||
| 	if err := validateRoleConstraints(role); err != nil { | ||||
| 		resp.AddWarning("Role does not have any constraints set on it. Updates to this role will require a constraint to be set") | ||||
| 	} | ||||
| 	resp.AddWarning(`The "bound_cidr_list" parameter is deprecated and will be removed in favor of "secret_id_bound_cidrs".`) | ||||
|  | ||||
| 	// For sanity, verify that the index still exists. If the index is missing, | ||||
| 	// add one and return a warning so it can be reported. | ||||
| @@ -1312,7 +1399,7 @@ func (b *backend) pathRoleSecretIDAccessorDestroyUpdateDelete(ctx context.Contex | ||||
| 	return nil, nil | ||||
| } | ||||
|  | ||||
| func (b *backend) pathRoleBoundCIDRListUpdate(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { | ||||
| func (b *backend) pathRoleBoundCIDRUpdate(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { | ||||
| 	roleName := data.Get("role_name").(string) | ||||
| 	if roleName == "" { | ||||
| 		return logical.ErrorResponse("missing role_name"), nil | ||||
| @@ -1331,12 +1418,18 @@ func (b *backend) pathRoleBoundCIDRListUpdate(ctx context.Context, req *logical. | ||||
| 		return nil, logical.ErrUnsupportedPath | ||||
| 	} | ||||
|  | ||||
| 	role.BoundCIDRList = data.Get("bound_cidr_list").([]string) | ||||
| 	if len(role.BoundCIDRList) == 0 { | ||||
| 	var cidrs []string | ||||
| 	if cidrsIfc, ok := data.GetFirst("secret_id_bound_cidrs", "bound_cidr_list"); ok { | ||||
| 		cidrs = cidrsIfc.([]string) | ||||
| 		role.SecretIDBoundCIDRs = cidrs | ||||
| 	} else if cidrsIfc, ok := data.GetOk("token_bound_cidrs"); ok { | ||||
| 		cidrs = cidrsIfc.([]string) | ||||
| 		role.TokenBoundCIDRs = cidrs | ||||
| 	} | ||||
| 	if len(cidrs) == 0 { | ||||
| 		return logical.ErrorResponse("missing bound_cidr_list"), nil | ||||
| 	} | ||||
|  | ||||
| 	valid, err := cidrutil.ValidateCIDRListSlice(role.BoundCIDRList) | ||||
| 	valid, err := cidrutil.ValidateCIDRListSlice(cidrs) | ||||
| 	if err != nil { | ||||
| 		return nil, errwrap.Wrapf("failed to validate CIDR blocks: {{err}}", err) | ||||
| 	} | ||||
| @@ -1347,7 +1440,19 @@ func (b *backend) pathRoleBoundCIDRListUpdate(ctx context.Context, req *logical. | ||||
| 	return nil, b.setRoleEntry(ctx, req.Storage, role.name, role, "") | ||||
| } | ||||
|  | ||||
| func (b *backend) pathRoleSecretIDBoundCIDRRead(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { | ||||
| 	return b.pathRoleFieldRead(ctx, req, data, "secret_id_bound_cidrs") | ||||
| } | ||||
|  | ||||
| func (b *backend) pathRoleTokenBoundCIDRRead(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { | ||||
| 	return b.pathRoleFieldRead(ctx, req, data, "token_bound_cidrs") | ||||
| } | ||||
|  | ||||
| func (b *backend) pathRoleBoundCIDRListRead(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { | ||||
| 	return b.pathRoleFieldRead(ctx, req, data, "bound_cidr_list") | ||||
| } | ||||
|  | ||||
| func (b *backend) pathRoleFieldRead(ctx context.Context, req *logical.Request, data *framework.FieldData, fieldName string) (*logical.Response, error) { | ||||
| 	roleName := data.Get("role_name").(string) | ||||
| 	if roleName == "" { | ||||
| 		return logical.ErrorResponse("missing role_name"), nil | ||||
| @@ -1363,16 +1468,36 @@ func (b *backend) pathRoleBoundCIDRListRead(ctx context.Context, req *logical.Re | ||||
| 	} | ||||
| 	if role == nil { | ||||
| 		return nil, nil | ||||
| 	} else { | ||||
| 		switch fieldName { | ||||
| 		case "secret_id_bound_cidrs": | ||||
| 			return &logical.Response{ | ||||
| 				Data: map[string]interface{}{ | ||||
| 					"secret_id_bound_cidrs": role.SecretIDBoundCIDRs, | ||||
| 				}, | ||||
| 			}, nil | ||||
| 		case "token_bound_cidrs": | ||||
| 			return &logical.Response{ | ||||
| 				Data: map[string]interface{}{ | ||||
| 					"token_bound_cidrs": role.TokenBoundCIDRs, | ||||
| 				}, | ||||
| 			}, nil | ||||
| 		case "bound_cidr_list": | ||||
| 			resp := &logical.Response{ | ||||
| 				Data: map[string]interface{}{ | ||||
| 					"bound_cidr_list": role.BoundCIDRList, | ||||
| 				}, | ||||
| 			} | ||||
| 			resp.AddWarning(`The "bound_cidr_list" parameter is deprecated and will be removed. Please use "secret_id_bound_cidrs" instead.`) | ||||
| 			return resp, nil | ||||
| 		default: | ||||
| 			// shouldn't occur IRL | ||||
| 			return nil, errors.New("unrecognized field provided: " + fieldName) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	return &logical.Response{ | ||||
| 		Data: map[string]interface{}{ | ||||
| 			"bound_cidr_list": role.BoundCIDRList, | ||||
| 		}, | ||||
| 	}, nil | ||||
| } | ||||
|  | ||||
| func (b *backend) pathRoleBoundCIDRListDelete(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { | ||||
| func (b *backend) pathRoleBoundCIDRDelete(ctx context.Context, req *logical.Request, data *framework.FieldData, fieldName string) (*logical.Response, error) { | ||||
| 	roleName := data.Get("role_name").(string) | ||||
| 	if roleName == "" { | ||||
| 		return logical.ErrorResponse("missing role_name"), nil | ||||
| @@ -1391,9 +1516,27 @@ func (b *backend) pathRoleBoundCIDRListDelete(ctx context.Context, req *logical. | ||||
| 	} | ||||
|  | ||||
| 	// Deleting a field implies setting the value to it's default value. | ||||
| 	role.BoundCIDRList = data.GetDefaultOrZero("bound_cidr_list").([]string) | ||||
| 	switch fieldName { | ||||
| 	case "bound_cidr_list": | ||||
| 		role.BoundCIDRList = data.GetDefaultOrZero("bound_cidr_list").([]string) | ||||
| 	case "secret_id_bound_cidrs": | ||||
| 		role.SecretIDBoundCIDRs = data.GetDefaultOrZero("secret_id_bound_cidrs").([]string) | ||||
| 	case "token_bound_cidrs": | ||||
| 		role.TokenBoundCIDRs = data.GetDefaultOrZero("token_bound_cidrs").([]string) | ||||
| 	} | ||||
| 	return nil, b.setRoleEntry(ctx, req.Storage, roleName, role, "") | ||||
| } | ||||
|  | ||||
| 	return nil, b.setRoleEntry(ctx, req.Storage, role.name, role, "") | ||||
| func (b *backend) pathRoleBoundCIDRListDelete(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { | ||||
| 	return b.pathRoleBoundCIDRDelete(ctx, req, data, "bound_cidr_list") | ||||
| } | ||||
|  | ||||
| func (b *backend) pathRoleSecretIDBoundCIDRDelete(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { | ||||
| 	return b.pathRoleBoundCIDRDelete(ctx, req, data, "secret_id_bound_cidrs") | ||||
| } | ||||
|  | ||||
| func (b *backend) pathRoleTokenBoundCIDRDelete(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { | ||||
| 	return b.pathRoleBoundCIDRDelete(ctx, req, data, "token_bound_cidrs") | ||||
| } | ||||
|  | ||||
| func (b *backend) pathRoleBindSecretIDUpdate(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { | ||||
| @@ -2137,7 +2280,7 @@ func (b *backend) handleRoleSecretIDCommon(ctx context.Context, req *logical.Req | ||||
| 	} | ||||
|  | ||||
| 	// Ensure that the CIDRs on the secret ID are a subset of that of role's | ||||
| 	if err := verifyCIDRRoleSecretIDSubset(secretIDCIDRs, role.BoundCIDRList); err != nil { | ||||
| 	if err := verifyCIDRRoleSecretIDSubset(secretIDCIDRs, role.SecretIDBoundCIDRs); err != nil { | ||||
| 		return nil, err | ||||
| 	} | ||||
|  | ||||
| @@ -2266,11 +2409,25 @@ configured using the parameters of this endpoint.`, | ||||
| The value of 'secret_id' can be retrieved using 'role/<role_name>/secret-id' endpoint.`, | ||||
| 	}, | ||||
| 	"role-bound-cidr-list": { | ||||
| 		`Deprecated: Comma separated list of CIDR blocks, if set, specifies blocks of IP | ||||
| addresses which can perform the login operation`, | ||||
| 		`During login, the IP address of the client will be checked to see if it | ||||
| belongs to the CIDR blocks specified. If CIDR blocks were set and if the | ||||
| IP is not encompassed by it, login fails`, | ||||
| 	}, | ||||
| 	"secret-id-bound-cidrs": { | ||||
| 		`Comma separated list of CIDR blocks, if set, specifies blocks of IP | ||||
| addresses which can perform the login operation`, | ||||
| 		`During login, the IP address of the client will be checked to see if it | ||||
| belongs to the CIDR blocks specified. If CIDR blocks were set and if the | ||||
| IP is not encompassed by it, login fails`, | ||||
| 	}, | ||||
| 	"token-bound-cidrs": { | ||||
| 		`Comma separated string or list of CIDR blocks. If set, specifies the blocks of | ||||
| IP addresses which can use the returned token.`, | ||||
| 		`During use of the returned token, the IP address of the client will be checked to see if it | ||||
| belongs to the CIDR blocks specified. If CIDR blocks were set and if the | ||||
| IP is not encompassed by it, token use fails`, | ||||
| 	}, | ||||
| 	"role-policies": { | ||||
| 		"Policies of the role.", | ||||
|   | ||||
| @@ -243,10 +243,10 @@ func TestAppRole_UpgradeBoundCIDRList(t *testing.T) { | ||||
| 	} | ||||
|  | ||||
| 	expected := []string{"127.0.0.1/18", "192.178.1.2/24"} | ||||
| 	actual := resp.Data["bound_cidr_list"].([]string) | ||||
| 	actual := resp.Data["secret_id_bound_cidrs"].([]string) | ||||
|  | ||||
| 	if !reflect.DeepEqual(expected, actual) { | ||||
| 		t.Fatalf("bad: bound_cidr_list; expected: %#v\nactual: %#v\n", expected, actual) | ||||
| 		t.Fatalf("bad: secret_id_bound_cidrs; expected: %#v\nactual: %#v\n", expected, actual) | ||||
| 	} | ||||
|  | ||||
| 	// Modify the storage entry of the role to hold the old style string typed bound_cidr_list | ||||
| @@ -519,7 +519,7 @@ func TestAppRole_RoleReadSetIndex(t *testing.T) { | ||||
| 	} | ||||
|  | ||||
| 	// Check if the warning is being returned | ||||
| 	if !strings.Contains(resp.Warnings[0], "Role identifier was missing an index back to role name.") { | ||||
| 	if !strings.Contains(resp.Warnings[1], "Role identifier was missing an index back to role name.") { | ||||
| 		t.Fatalf("bad: expected a warning in the response") | ||||
| 	} | ||||
|  | ||||
| @@ -1122,13 +1122,13 @@ func TestAppRole_RoleCRUD(t *testing.T) { | ||||
| 	b, storage := createBackendWithStorage(t) | ||||
|  | ||||
| 	roleData := map[string]interface{}{ | ||||
| 		"policies":           "p,q,r,s", | ||||
| 		"secret_id_num_uses": 10, | ||||
| 		"secret_id_ttl":      300, | ||||
| 		"token_ttl":          400, | ||||
| 		"token_max_ttl":      500, | ||||
| 		"token_num_uses":     600, | ||||
| 		"bound_cidr_list":    "127.0.0.1/32,127.0.0.1/16", | ||||
| 		"policies":              "p,q,r,s", | ||||
| 		"secret_id_num_uses":    10, | ||||
| 		"secret_id_ttl":         300, | ||||
| 		"token_ttl":             400, | ||||
| 		"token_max_ttl":         500, | ||||
| 		"token_num_uses":        600, | ||||
| 		"secret_id_bound_cidrs": "127.0.0.1/32,127.0.0.1/16", | ||||
| 	} | ||||
| 	roleReq := &logical.Request{ | ||||
| 		Operation: logical.CreateOperation, | ||||
| @@ -1149,14 +1149,16 @@ func TestAppRole_RoleCRUD(t *testing.T) { | ||||
| 	} | ||||
|  | ||||
| 	expected := map[string]interface{}{ | ||||
| 		"bind_secret_id":     true, | ||||
| 		"policies":           []string{"p", "q", "r", "s"}, | ||||
| 		"secret_id_num_uses": 10, | ||||
| 		"secret_id_ttl":      300, | ||||
| 		"token_ttl":          400, | ||||
| 		"token_max_ttl":      500, | ||||
| 		"token_num_uses":     600, | ||||
| 		"bound_cidr_list":    []string{"127.0.0.1/32", "127.0.0.1/16"}, | ||||
| 		"bind_secret_id":        true, | ||||
| 		"policies":              []string{"p", "q", "r", "s"}, | ||||
| 		"secret_id_num_uses":    10, | ||||
| 		"secret_id_ttl":         300, | ||||
| 		"token_ttl":             400, | ||||
| 		"token_max_ttl":         500, | ||||
| 		"token_num_uses":        600, | ||||
| 		"secret_id_bound_cidrs": []string{"127.0.0.1/32", "127.0.0.1/16"}, | ||||
| 		"bound_cidr_list":       []string{"127.0.0.1/32", "127.0.0.1/16"}, // returned for backwards compatibility | ||||
| 		"token_bound_cidrs":     []string{}, | ||||
| 	} | ||||
|  | ||||
| 	var expectedStruct roleStorageEntry | ||||
| @@ -1591,6 +1593,221 @@ func TestAppRole_RoleCRUD(t *testing.T) { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestAppRole_RoleWithTokenBoundCIDRsCRUD(t *testing.T) { | ||||
| 	var resp *logical.Response | ||||
| 	var err error | ||||
| 	b, storage := createBackendWithStorage(t) | ||||
|  | ||||
| 	roleData := map[string]interface{}{ | ||||
| 		"policies":              "p,q,r,s", | ||||
| 		"secret_id_num_uses":    10, | ||||
| 		"secret_id_ttl":         300, | ||||
| 		"token_ttl":             400, | ||||
| 		"token_max_ttl":         500, | ||||
| 		"token_num_uses":        600, | ||||
| 		"secret_id_bound_cidrs": "127.0.0.1/32,127.0.0.1/16", | ||||
| 		"token_bound_cidrs":     "127.0.0.1/32,127.0.0.1/16", | ||||
| 	} | ||||
| 	roleReq := &logical.Request{ | ||||
| 		Operation: logical.CreateOperation, | ||||
| 		Path:      "role/role1", | ||||
| 		Storage:   storage, | ||||
| 		Data:      roleData, | ||||
| 	} | ||||
|  | ||||
| 	resp, err = b.HandleRequest(context.Background(), roleReq) | ||||
| 	if err != nil || (resp != nil && resp.IsError()) { | ||||
| 		t.Fatalf("err:%v resp:%#v", err, resp) | ||||
| 	} | ||||
|  | ||||
| 	roleReq.Operation = logical.ReadOperation | ||||
| 	resp, err = b.HandleRequest(context.Background(), roleReq) | ||||
| 	if err != nil || (resp != nil && resp.IsError()) { | ||||
| 		t.Fatalf("err:%v resp:%#v", err, resp) | ||||
| 	} | ||||
|  | ||||
| 	expected := map[string]interface{}{ | ||||
| 		"bind_secret_id":        true, | ||||
| 		"policies":              []string{"p", "q", "r", "s"}, | ||||
| 		"secret_id_num_uses":    10, | ||||
| 		"secret_id_ttl":         300, | ||||
| 		"token_ttl":             400, | ||||
| 		"token_max_ttl":         500, | ||||
| 		"token_num_uses":        600, | ||||
| 		"token_bound_cidrs":     []string{"127.0.0.1/32", "127.0.0.1/16"}, | ||||
| 		"secret_id_bound_cidrs": []string{"127.0.0.1/32", "127.0.0.1/16"}, | ||||
| 		"bound_cidr_list":       []string{"127.0.0.1/32", "127.0.0.1/16"}, // provided for backwards compatibility | ||||
| 	} | ||||
|  | ||||
| 	var expectedStruct roleStorageEntry | ||||
| 	err = mapstructure.Decode(expected, &expectedStruct) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
|  | ||||
| 	var actualStruct roleStorageEntry | ||||
| 	err = mapstructure.Decode(resp.Data, &actualStruct) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
|  | ||||
| 	expectedStruct.RoleID = actualStruct.RoleID | ||||
| 	if !reflect.DeepEqual(expectedStruct, actualStruct) { | ||||
| 		t.Fatalf("bad:\nexpected:%#v\nactual:%#v\n", expectedStruct, actualStruct) | ||||
| 	} | ||||
|  | ||||
| 	roleData = map[string]interface{}{ | ||||
| 		"role_id":            "test_role_id", | ||||
| 		"policies":           "a,b,c,d", | ||||
| 		"secret_id_num_uses": 100, | ||||
| 		"secret_id_ttl":      3000, | ||||
| 		"token_ttl":          4000, | ||||
| 		"token_max_ttl":      5000, | ||||
| 	} | ||||
| 	roleReq.Data = roleData | ||||
| 	roleReq.Operation = logical.UpdateOperation | ||||
|  | ||||
| 	resp, err = b.HandleRequest(context.Background(), roleReq) | ||||
| 	if err != nil || (resp != nil && resp.IsError()) { | ||||
| 		t.Fatalf("err:%v resp:%#v", err, resp) | ||||
| 	} | ||||
|  | ||||
| 	roleReq.Operation = logical.ReadOperation | ||||
| 	resp, err = b.HandleRequest(context.Background(), roleReq) | ||||
| 	if err != nil || (resp != nil && resp.IsError()) { | ||||
| 		t.Fatalf("err:%v resp:%#v", err, resp) | ||||
| 	} | ||||
|  | ||||
| 	expected = map[string]interface{}{ | ||||
| 		"policies":           []string{"a", "b", "c", "d"}, | ||||
| 		"secret_id_num_uses": 100, | ||||
| 		"secret_id_ttl":      3000, | ||||
| 		"token_ttl":          4000, | ||||
| 		"token_max_ttl":      5000, | ||||
| 	} | ||||
| 	err = mapstructure.Decode(expected, &expectedStruct) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
|  | ||||
| 	err = mapstructure.Decode(resp.Data, &actualStruct) | ||||
| 	if err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
|  | ||||
| 	if !reflect.DeepEqual(expectedStruct, actualStruct) { | ||||
| 		t.Fatalf("bad:\nexpected:%#v\nactual:%#v\n", expectedStruct, actualStruct) | ||||
| 	} | ||||
|  | ||||
| 	// RUD for secret-id-bound-cidrs field | ||||
| 	roleReq.Path = "role/role1/secret-id-bound-cidrs" | ||||
| 	roleReq.Operation = logical.ReadOperation | ||||
| 	resp, err = b.HandleRequest(context.Background(), roleReq) | ||||
| 	if err != nil || (resp != nil && resp.IsError()) { | ||||
| 		t.Fatalf("err:%v resp:%#v", err, resp) | ||||
| 	} | ||||
| 	if resp.Data["secret_id_bound_cidrs"].([]string)[0] != "127.0.0.1/32" || | ||||
| 		resp.Data["secret_id_bound_cidrs"].([]string)[1] != "127.0.0.1/16" { | ||||
| 		t.Fatalf("bad: secret_id_bound_cidrs: expected:127.0.0.1/32,127.0.0.1/16 actual:%d\n", resp.Data["secret_id_bound_cidrs"].(int)) | ||||
| 	} | ||||
|  | ||||
| 	roleReq.Data = map[string]interface{}{"secret_id_bound_cidrs": []string{"127.0.0.1/20"}} | ||||
| 	roleReq.Operation = logical.UpdateOperation | ||||
| 	resp, err = b.HandleRequest(context.Background(), roleReq) | ||||
| 	if err != nil || (resp != nil && resp.IsError()) { | ||||
| 		t.Fatalf("err:%v resp:%#v", err, resp) | ||||
| 	} | ||||
|  | ||||
| 	roleReq.Operation = logical.ReadOperation | ||||
| 	resp, err = b.HandleRequest(context.Background(), roleReq) | ||||
| 	if err != nil || (resp != nil && resp.IsError()) { | ||||
| 		t.Fatalf("err:%v resp:%#v", err, resp) | ||||
| 	} | ||||
|  | ||||
| 	if resp.Data["secret_id_bound_cidrs"].([]string)[0] != "127.0.0.1/20" { | ||||
| 		t.Fatalf("bad: secret_id_bound_cidrs: expected:127.0.0.1/20 actual:%s\n", resp.Data["secret_id_bound_cidrs"].([]string)[0]) | ||||
| 	} | ||||
|  | ||||
| 	roleReq.Operation = logical.DeleteOperation | ||||
| 	resp, err = b.HandleRequest(context.Background(), roleReq) | ||||
| 	if err != nil || (resp != nil && resp.IsError()) { | ||||
| 		t.Fatalf("err:%v resp:%#v", err, resp) | ||||
| 	} | ||||
|  | ||||
| 	roleReq.Operation = logical.ReadOperation | ||||
| 	resp, err = b.HandleRequest(context.Background(), roleReq) | ||||
| 	if err != nil || (resp != nil && resp.IsError()) { | ||||
| 		t.Fatalf("err:%v resp:%#v", err, resp) | ||||
| 	} | ||||
|  | ||||
| 	if len(resp.Data["secret_id_bound_cidrs"].([]string)) != 0 { | ||||
| 		t.Fatalf("expected value to be reset") | ||||
| 	} | ||||
|  | ||||
| 	// RUD for token-bound-cidrs field | ||||
| 	roleReq.Path = "role/role1/token-bound-cidrs" | ||||
| 	roleReq.Operation = logical.ReadOperation | ||||
| 	resp, err = b.HandleRequest(context.Background(), roleReq) | ||||
| 	if err != nil || (resp != nil && resp.IsError()) { | ||||
| 		t.Fatalf("err:%v resp:%#v", err, resp) | ||||
| 	} | ||||
| 	if resp.Data["token_bound_cidrs"].([]string)[0] != "127.0.0.1/32" || | ||||
| 		resp.Data["token_bound_cidrs"].([]string)[1] != "127.0.0.1/16" { | ||||
| 		t.Fatalf("bad: token_bound_cidrs: expected:127.0.0.1/32,127.0.0.1/16 actual:%d\n", resp.Data["token_bound_cidrs"].(int)) | ||||
| 	} | ||||
|  | ||||
| 	roleReq.Data = map[string]interface{}{"token_bound_cidrs": []string{"127.0.0.1/20"}} | ||||
| 	roleReq.Operation = logical.UpdateOperation | ||||
| 	resp, err = b.HandleRequest(context.Background(), roleReq) | ||||
| 	if err != nil || (resp != nil && resp.IsError()) { | ||||
| 		t.Fatalf("err:%v resp:%#v", err, resp) | ||||
| 	} | ||||
|  | ||||
| 	roleReq.Operation = logical.ReadOperation | ||||
| 	resp, err = b.HandleRequest(context.Background(), roleReq) | ||||
| 	if err != nil || (resp != nil && resp.IsError()) { | ||||
| 		t.Fatalf("err:%v resp:%#v", err, resp) | ||||
| 	} | ||||
|  | ||||
| 	if resp.Data["token_bound_cidrs"].([]string)[0] != "127.0.0.1/20" { | ||||
| 		t.Fatalf("bad: token_bound_cidrs: expected:127.0.0.1/20 actual:%s\n", resp.Data["token_bound_cidrs"].([]string)[0]) | ||||
| 	} | ||||
|  | ||||
| 	roleReq.Operation = logical.DeleteOperation | ||||
| 	resp, err = b.HandleRequest(context.Background(), roleReq) | ||||
| 	if err != nil || (resp != nil && resp.IsError()) { | ||||
| 		t.Fatalf("err:%v resp:%#v", err, resp) | ||||
| 	} | ||||
|  | ||||
| 	roleReq.Operation = logical.ReadOperation | ||||
| 	resp, err = b.HandleRequest(context.Background(), roleReq) | ||||
| 	if err != nil || (resp != nil && resp.IsError()) { | ||||
| 		t.Fatalf("err:%v resp:%#v", err, resp) | ||||
| 	} | ||||
|  | ||||
| 	if len(resp.Data["token_bound_cidrs"].([]string)) != 0 { | ||||
| 		t.Fatalf("expected value to be reset") | ||||
| 	} | ||||
|  | ||||
| 	// Delete test for role | ||||
| 	roleReq.Path = "role/role1" | ||||
| 	roleReq.Operation = logical.DeleteOperation | ||||
| 	resp, err = b.HandleRequest(context.Background(), roleReq) | ||||
| 	if err != nil || (resp != nil && resp.IsError()) { | ||||
| 		t.Fatalf("err:%v resp:%#v", err, resp) | ||||
| 	} | ||||
|  | ||||
| 	roleReq.Operation = logical.ReadOperation | ||||
| 	resp, err = b.HandleRequest(context.Background(), roleReq) | ||||
| 	if err != nil || (resp != nil && resp.IsError()) { | ||||
| 		t.Fatalf("err:%v resp:%#v", err, resp) | ||||
| 	} | ||||
|  | ||||
| 	if resp != nil { | ||||
| 		t.Fatalf("expected a nil response") | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func createRole(t *testing.T, b *backend, s logical.Storage, roleName, policies string) { | ||||
| 	roleData := map[string]interface{}{ | ||||
| 		"policies":           policies, | ||||
|   | ||||
| @@ -80,6 +80,19 @@ func (d *FieldData) GetDefaultOrZero(k string) interface{} { | ||||
| 	return schema.DefaultOrZero() | ||||
| } | ||||
|  | ||||
| // GetFirst gets the value for the given field names, in order from first | ||||
| // to last. This can be useful for fields with a current name, and one or | ||||
| // more deprecated names. The second return value will be false if the keys | ||||
| // are invalid or the keys are not set at all. | ||||
| func (d *FieldData) GetFirst(k ...string) (interface{}, bool) { | ||||
| 	for _, v := range k { | ||||
| 		if result, ok := d.GetOk(v); ok { | ||||
| 			return result, ok | ||||
| 		} | ||||
| 	} | ||||
| 	return nil, false | ||||
| } | ||||
|  | ||||
| // GetOk gets the value for the given field. The second return value | ||||
| // will be false if the key is invalid or the key is not set at all. | ||||
| func (d *FieldData) GetOk(k string) (interface{}, bool) { | ||||
|   | ||||
| @@ -642,3 +642,37 @@ func TestFieldDataGet_Error(t *testing.T) { | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func TestFieldDataGetFirst(t *testing.T) { | ||||
| 	data := &FieldData{ | ||||
| 		Raw: map[string]interface{}{ | ||||
| 			"foo":  "bar", | ||||
| 			"fizz": "buzz", | ||||
| 		}, | ||||
| 		Schema: map[string]*FieldSchema{ | ||||
| 			"foo":  {Type: TypeNameString}, | ||||
| 			"fizz": {Type: TypeNameString}, | ||||
| 		}, | ||||
| 	} | ||||
|  | ||||
| 	result, ok := data.GetFirst("foo", "fizz") | ||||
| 	if !ok { | ||||
| 		t.Fatal("should have found value for foo") | ||||
| 	} | ||||
| 	if result.(string) != "bar" { | ||||
| 		t.Fatal("should have gotten bar for foo") | ||||
| 	} | ||||
|  | ||||
| 	result, ok = data.GetFirst("fizz", "foo") | ||||
| 	if !ok { | ||||
| 		t.Fatal("should have found value for fizz") | ||||
| 	} | ||||
| 	if result.(string) != "buzz" { | ||||
| 		t.Fatal("should have gotten buzz for fizz") | ||||
| 	} | ||||
|  | ||||
| 	result, ok = data.GetFirst("cats") | ||||
| 	if ok { | ||||
| 		t.Fatal("shouldn't have gotten anything for cats") | ||||
| 	} | ||||
| } | ||||
|   | ||||
| @@ -69,9 +69,12 @@ enabled while creating or updating a role. | ||||
| - `role_name` `(string: <required>)` - Name of the AppRole. | ||||
| - `bind_secret_id` `(bool: true)` - Require `secret_id` to be presented when | ||||
|   logging in using this AppRole. | ||||
| - `bound_cidr_list` `(array: [])` - Comma-separated string or list of CIDR | ||||
| - `secret_id_bound_cidrs` `(array: [])` - Comma-separated string or list of CIDR | ||||
|   blocks; if set, specifies blocks of IP addresses which can perform the login | ||||
|   operation. | ||||
| - `token_bound_cidrs` `(array: [])` - Comma-separated string or list of CIDR | ||||
|   blocks; if set, specifies blocks of IP addresses which can use the auth tokens | ||||
|   generated by this role. | ||||
| - `policies` `(array: [])` - Comma-separated list of policies set on tokens | ||||
|   issued via this AppRole. | ||||
| - `secret_id_num_uses` `(integer: 0)` - Number of times any particular SecretID | ||||
|   | ||||
| @@ -226,7 +226,7 @@ credentials for login. The `bind_secret_id` constraint requires `secret_id` to | ||||
| be presented at the login endpoint.  Going forward, this auth method can support | ||||
| more constraint parameters to support varied set of Apps. Some constraints will | ||||
| not require a credential, but still enforce constraints for login.  For | ||||
| example, `bound_cidr_list` will only allow requests coming from IP addresses | ||||
| example, `secret_id_bound_cidrs` will only allow logins coming from IP addresses | ||||
| belonging to configured CIDR blocks on the AppRole. | ||||
|  | ||||
| ## API | ||||
|   | ||||
		Reference in New Issue
	
	Block a user