Add bound cidrs to tokens in AppRole (#4680)

This commit is contained in:
Becca Petrin
2018-06-19 19:57:11 -07:00
committed by Jeff Mitchell
parent bd99c43e9c
commit b3a711d717
7 changed files with 493 additions and 62 deletions

View File

@@ -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{

View File

@@ -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.",

View File

@@ -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,

View File

@@ -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) {

View File

@@ -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")
}
}

View File

@@ -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

View File

@@ -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