Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

auth/aws: Allow lists in binds #3907

Merged
merged 20 commits into from
Mar 2, 2018

Conversation

joelthompson
Copy link
Contributor

@joelthompson joelthompson commented Feb 4, 2018

In the aws auth method, allow a number of binds to take in lists
instead of a single string value. The intended semantic is that, for
each bind type set, clients must match at least one of each of the bind
types set in order to authenticate.

There are still some gaps, but I think it's close enough that I'd like to get feedback from the Vault team about my overall direction before continuing. Of note, this does have a minor backwards incompatibility in that the bind_* properties that now can be lists, and so they will be lists when the role is read from clients, rather than a string, so clients need to be updated to expect a list return value.

Things remaining for me to do once my above questions are figured out:

  • Improve tests to cover various use cases
  • Update docs

Fixes #3330
Fixes #1975

In the aws auth method, allow a number of binds to take in lists
instead of a single string value. The intended semantic is that, for
each bind type set, clients must match at least one of each of the bind
types set in order to authenticate.
@joelthompson
Copy link
Contributor Author

@jefferai -- RE: the comment about guarding the role upgrading code in #3816 (comment) I only added the guard in the lockedAWSRole method and not also in the pathRoleCreateUpdate method as I (naively) assume creating/updating roles would already be protected in secondary mode. Let me know if that's incorrect.

if !strutil.GlobbedStringsMatch(roleEntry.BoundIamPrincipalARN, fullArn) {
matchedWildcardBind := false
for _, principalARN := range roleEntry.BoundIamPrincipalARNs {
if strings.HasSuffix(principalARN, "*") && strutil.GlobbedStringsMatch(principalARN, fullArn) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is prefix matching sufficient here, do you think, or should this support globs anywhere in the ARN?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The prior code only supported prefix matching, so I'm just keeping that.

It's a good question as to whether globs anywhere in the ARN should be supported; I haven't seen any requests for it either on the mailing list or in the issue tracker, and if general wildcards should be supported, let's address that in a separate PR :)

// about whether the bound IAM principal ARN is a wildcard or not, and what that wildcard is.
matchedWildcardBind := false
for _, principalARN := range roleEntry.BoundIamPrincipalARNs {
if strings.HasSuffix(principalARN, "*") && strutil.GlobbedStringsMatch(principalARN, fullArn) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same question here.

@@ -436,7 +499,9 @@ func (b *backend) pathRoleCreateUpdate(ctx context.Context, req *logical.Request
return nil, err
}
if roleEntry == nil {
roleEntry = &awsRoleEntry{}
roleEntry = &awsRoleEntry{
version: currentRoleStorageVersion,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This value can't be unexported, because JSON can't marshal/unmarshal if it is. So it will end up always being 0 on read.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call out. I didn't want to return this to clients, and that was before I stole the ToResponseData method, so I've now exported it.

roleEntry.BoundIamPrincipalID = principalID
} else {
// Need to handle the case where we're switching from a non-wildcard principal to a wildcard principal
roleEntry.BoundIamPrincipalID = ""
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this case still being handled? Does it need to be?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe it does the right thing.

Before, the code needed to handle switching from non-widlcard to a wildcard. Now, however, it needs to handle switching from a list containing some widlcard and some non-wildcard ARNs, to a different list containing some (possibly same and possibly different) wildcard ARNs and some (possibly same and possibly different) non-wildcard ARNs. So this is no longer a special case.

It's handled by calling roleEntry.BoundIamPrincipalIDs = []string{} whenever bound_iam_principal_arn is explicitly set and then building up the BoundIamPrincipalIDs for each non-wildcard principal.

DisallowReauthentication bool `json:"disallow_reauthentication" structs:"disallow_reauthentication" mapstructure:"disallow_reauthentication"`
HMACKey string `json:"hmac_key" structs:"hmac_key" mapstructure:"hmac_key"`
Period time.Duration `json:"period" mapstructure:"period" structs:"period"`
version int `json:"version" mapstructure:"version" structs:"versoin"`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs to be exported.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, there is a typo in the structs tag.

Also, if structs isn't being used anymore, which it seems it isn't, and which is my preference, just remove the structs tags :-D

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done, including removing the structs tags entirely from this type.

Honestly, was never 100% sure why structs and mapstructure tags were included and I just kept including them because they had been included. If they're not needed, I'm also 100% in favor of removing them :)

data[field] = []string{}
}
}
convertNilToEmptySlice(responseData, "bound_ami_id")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this really necessary? The JSON marshaling code ought to see that it's a nil slice and do the right thing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my testing, yes (it was a surprise to me as well). https://stackoverflow.com/a/44305910 is the best explanation I've seen of the differences between a nil slice and an empty slice. The former gets marshaled to null while the later gets marshaled to [] which makes sense to me.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

null is in fact the right thing, although it's untyped; or omitempty to just not actually include it at all. But if you think it's useful to return real data, keep it as is.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm all in favor of less code, and it seems likely that this might get forgotten to get updated if/when additional fields are added, so removed and will return null :)

Copy link
Member

@jefferai jefferai left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some minor comments, mostly looking great!

Copy link
Contributor Author

@joelthompson joelthompson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Will try to get to docs and tests tomorrow.

if !strutil.GlobbedStringsMatch(roleEntry.BoundIamPrincipalARN, fullArn) {
matchedWildcardBind := false
for _, principalARN := range roleEntry.BoundIamPrincipalARNs {
if strings.HasSuffix(principalARN, "*") && strutil.GlobbedStringsMatch(principalARN, fullArn) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The prior code only supported prefix matching, so I'm just keeping that.

It's a good question as to whether globs anywhere in the ARN should be supported; I haven't seen any requests for it either on the mailing list or in the issue tracker, and if general wildcards should be supported, let's address that in a separate PR :)

@@ -436,7 +499,9 @@ func (b *backend) pathRoleCreateUpdate(ctx context.Context, req *logical.Request
return nil, err
}
if roleEntry == nil {
roleEntry = &awsRoleEntry{}
roleEntry = &awsRoleEntry{
version: currentRoleStorageVersion,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call out. I didn't want to return this to clients, and that was before I stole the ToResponseData method, so I've now exported it.

roleEntry.BoundIamPrincipalID = principalID
} else {
// Need to handle the case where we're switching from a non-wildcard principal to a wildcard principal
roleEntry.BoundIamPrincipalID = ""
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe it does the right thing.

Before, the code needed to handle switching from non-widlcard to a wildcard. Now, however, it needs to handle switching from a list containing some widlcard and some non-wildcard ARNs, to a different list containing some (possibly same and possibly different) wildcard ARNs and some (possibly same and possibly different) non-wildcard ARNs. So this is no longer a special case.

It's handled by calling roleEntry.BoundIamPrincipalIDs = []string{} whenever bound_iam_principal_arn is explicitly set and then building up the BoundIamPrincipalIDs for each non-wildcard principal.

DisallowReauthentication bool `json:"disallow_reauthentication" structs:"disallow_reauthentication" mapstructure:"disallow_reauthentication"`
HMACKey string `json:"hmac_key" structs:"hmac_key" mapstructure:"hmac_key"`
Period time.Duration `json:"period" mapstructure:"period" structs:"period"`
version int `json:"version" mapstructure:"version" structs:"versoin"`
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done, including removing the structs tags entirely from this type.

Honestly, was never 100% sure why structs and mapstructure tags were included and I just kept including them because they had been included. If they're not needed, I'm also 100% in favor of removing them :)

data[field] = []string{}
}
}
convertNilToEmptySlice(responseData, "bound_ami_id")
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In my testing, yes (it was a surprise to me as well). https://stackoverflow.com/a/44305910 is the best explanation I've seen of the differences between a nil slice and an empty slice. The former gets marshaled to null while the later gets marshaled to [] which makes sense to me.

HMACKey string `json:"hmac_key" structs:"hmac_key" mapstructure:"hmac_key"`
Period time.Duration `json:"period" mapstructure:"period" structs:"period"`
AuthType string `json:"auth_type" mapstructure:"auth_type"`
BoundAmiID string `json:"bound_ami_id,omitempty" mapstructure:"bound_ami_id"`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we segregate the old string fields at the bottom of the struct and have a "// Deprecated" comment on them. This is just to improve readability.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call out :) done

roleEntry.BoundIamPrincipalID = principalId
upgraded = true
roleEntry.Version = 1
fallthrough
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need a fallthrough here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right now, we don't. But, it's more future safe. The idea is that each case would upgrade the role by only a single version for simplicity (I started trying to tear my hair out trying to think about different upgrade scenarios and ordering, which is why I went ahead and implemented this versioning scheme), so, in this case, from 0 to 1. Now, let's say we have a version 2 (which I've conceptually proposed in a separate issue). This case would upgrade the role from version 0 to version 1. We'd still need to upgrade the role to version 2, which is what the fallthrough would achieve. We don't need to rely on whoever adds the case 2 remembering to add the fallthrough to the version 0 case; it's already there.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good.

principalID, err := b.resolveArnToUniqueIDFunc(ctx, req.Storage, roleEntry.BoundIamPrincipalARN)
if err != nil {
return logical.ErrorResponse(fmt.Sprintf("unable to resolve ARN %#v to internal ID: %#v", roleEntry.BoundIamPrincipalARN, err)), nil
} else if roleEntry.ResolveAWSUniqueIDs && len(roleEntry.BoundIamPrincipalIDs) == 0 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if this should be an else if block or should it be an independent if. Don't we want to resolve the ARNs in the role anyways?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, realized that I could reduce code duplication with this and the above block by making it an independent if so I've done that :)

func (r *awsRoleEntry) ToResponseData() map[string]interface{} {
responseData := map[string]interface{}{
"auth_type": r.AuthType,
"bound_ami_id": r.BoundAmiIDs,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although I see the reason as to why the keys in the response map to the older fields and the values to the newer, I am feeling a little off about it.

The other option that is bugging me is to return both the singular and slice keys with deprecation notice for the former.

@jefferai Thoughts?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Never mind. Ignore this comment.

@jefferai jefferai added this to the 0.10 milestone Feb 14, 2018
@jefferai jefferai modified the milestones: 0.10, 0.9.5 Feb 21, 2018
Acceptance tests were failing due to hashicorp#4014 so, as a workaround for now,
passing in the identity document and the RSA signature rather than the
PKCS7 document.
@joelthompson
Copy link
Contributor Author

joelthompson commented Feb 22, 2018

I think I've got the code in a good spot and I've started adding some tests, but I want to take some time to re-review the tests and possibly improve the testing. I also haven't updated any of the docs yet.

This reverts commit c342691.

The underlying issue causing the need for the workaround has been fixed,
and additional testing changes have been added in hashicorp#4031 so this is no
longer necessary.
@joelthompson joelthompson changed the title WIP: auth/aws: Allow lists in binds auth/aws: Allow lists in binds Feb 23, 2018
Looks like these changes were dropped from prior commit
@joelthompson
Copy link
Contributor Author

@jefferai @vishalnayak -- I think I've responded to all your feedback thus far and I've got reasonable tests and documentation, so this should be ready for you to review when you get a chance :)

It believe (but am not sure) that Travis failure is unrelated. Best guess, it looks to be unhappy because f54832b didn't revert the changes to helper/certutil/certutil_test.go which added calls to x509.MarshalPKCS8PrivateKey (new in Go 1.10).

@@ -548,52 +550,63 @@ inferencing configuration of that role.
"ec2"). Only those bindings applicable to the auth type chosen will be allowed
to be configured on the role.
- `bound_ami_id` `(string: "")` - If set, defines a constraint on the EC2
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

s/(string: "")/(list: []) for all the fields that are upgraded to TypeStringCommaSlice

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done (FWIW, I just copied what was done for the PKI secret backend, e..g, http://localhost:4567/api/secret/pki/index.html#ou-1).

vishalnayak
vishalnayak previously approved these changes Feb 23, 2018
Copy link
Member

@vishalnayak vishalnayak left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One minor comment on the docs. Other than that this LGTM!

@jefferai jefferai modified the milestones: 0.9.5, 0.9.6 Feb 28, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
3 participants