-
Notifications
You must be signed in to change notification settings - Fork 4.3k
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
Conversation
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.
@jefferai -- RE: the comment about guarding the role upgrading code in #3816 (comment) I only added the guard in the |
if !strutil.GlobbedStringsMatch(roleEntry.BoundIamPrincipalARN, fullArn) { | ||
matchedWildcardBind := false | ||
for _, principalARN := range roleEntry.BoundIamPrincipalARNs { | ||
if strings.HasSuffix(principalARN, "*") && strutil.GlobbedStringsMatch(principalARN, fullArn) { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same question here.
builtin/credential/aws/path_role.go
Outdated
@@ -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, |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 = "" |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
builtin/credential/aws/path_role.go
Outdated
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"` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Needs to be exported.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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") |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
:)
There was a problem hiding this 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!
There was a problem hiding this 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) { |
There was a problem hiding this comment.
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 :)
builtin/credential/aws/path_role.go
Outdated
@@ -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, |
There was a problem hiding this comment.
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 = "" |
There was a problem hiding this comment.
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.
builtin/credential/aws/path_role.go
Outdated
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"` |
There was a problem hiding this comment.
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") |
There was a problem hiding this comment.
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.
builtin/credential/aws/path_role.go
Outdated
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"` |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good call out :) done
builtin/credential/aws/path_role.go
Outdated
roleEntry.BoundIamPrincipalID = principalId | ||
upgraded = true | ||
roleEntry.Version = 1 | ||
fallthrough |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sounds good.
builtin/credential/aws/path_role.go
Outdated
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 { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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, |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
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.
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.
Looks like these changes were dropped from prior commit
@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 |
@@ -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 |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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).
There was a problem hiding this 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!
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:
Fixes #3330
Fixes #1975