-
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
Make AWS credential types more explicit #4360
Conversation
The AWS secret engine had a lot of confusing overloading with role paramemters and how they mapped to each of the three credential types supported. This now adds parameters to remove the overloading while maintaining backwards compatibility. With the change, it also becomes easier to add other feature requests. Attaching multiple managed policies to IAM users and adding a policy document to STS AssumedRole credentials is now also supported. Fixes hashicorp#4229 Fixes hashicorp#3751 Fixes hashicorp#2817
This allows unsetting the policy_document by passing in an empty string. Previously, it would fail because the empty string isn't a valid JSON document.
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.
Looks great! None of my comments are blockers.
return logical.ListResponse(append(entries, legacyEntries...)), nil | ||
} | ||
|
||
func (b *backend) pathRolesDelete(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { |
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 notice that some of these framework.OperationFunc
are methods of the back end, and some aren't. Some do need the b
to do stuff, and others don't. Would it sense to have all of them be methods of the back end for consistency? Or to only attach them when they need something off the back end?
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.
Personally, I would make them all be methods of the backend. However, I'm not the most familiar with golang idioms, so I figured I'd leave it the way I found it, and also to not add additional things to this PR. I'd be happy to submit a follow up to make this change, though.
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.
Ah! I didn't realize it was already that way. It's fine the way it is, I think. I don't think there's a "right" way to do it, I've never seen anything like that in the style guides. :-)
if r.InvalidData != "" { | ||
respData["invalid_data"] = r.InvalidData | ||
} | ||
return respData |
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.
Am I right in presuming it's intentional that prohibit_flexible_cred_path
and version
are omitted?
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. version
is purely an implementation detail about how the role data is stored in the internal storage, to make upgrading the way roles are stored easier in the future. prohibit_flexible_cred_path
is also an implementation detail to securely enable backwards compatibility and also unify the /creds
and /sts
path. My thinking is, if clients are using the "old" parameters in defining the role, then they'll use the same /creds
or /sts
path used before to retrieve credentials. And if they're using the "new" parameters introduced in this PR, then it won't matter which of the two endpoints are used.
Of course, this has the potential to add confusion. I think omitting these is the right balance, but I'd love to hear any suggestions on how it could be improved! :)
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.
👍 That sounds good!
"github.com/hashicorp/vault/logical" | ||
"github.com/hashicorp/vault/logical/framework" | ||
"github.com/mitchellh/mapstructure" | ||
) | ||
|
||
func pathUser(b *backend) *framework.Path { | ||
return &framework.Path{ | ||
Pattern: "creds/" + framework.GenericNameRegex("name"), | ||
Pattern: "(creds|sts)/" + framework.GenericNameRegex("name"), |
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.
Clever way to handle those still using sts!
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! :)
@@ -234,7 +236,7 @@ func (b *backend) secretAccessKeysCreate( | |||
"security_token": nil, | |||
}, map[string]interface{}{ | |||
"username": username, | |||
"policy": policy, | |||
"policy": role, |
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.
Should this also be role.PolicyDocument
?
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.
That's a good question. Previously, the policy
variable was overloaded to be either the policy document or the managed policy ARN, and regardless of which one it was, it was getting stuck in the metadata. Now that an IAM user can have both a policy document and managed policy ARNs, it didn't seem right to pick one or the other to put in the metadata, but rather to put both in so as to not lose information. But, I'm open to suggestions about a better way of doing this!
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.
👍
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! I also left an open question in #4229 (comment) about whether the parameter in defining a role should be credential_type
or credential_types
. I did the former, but I'm starting to lean more towards the latter and would like to know if there are other opinions.
@@ -234,7 +236,7 @@ func (b *backend) secretAccessKeysCreate( | |||
"security_token": nil, | |||
}, map[string]interface{}{ | |||
"username": username, | |||
"policy": policy, | |||
"policy": role, |
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.
That's a good question. Previously, the policy
variable was overloaded to be either the policy document or the managed policy ARN, and regardless of which one it was, it was getting stuck in the metadata. Now that an IAM user can have both a policy document and managed policy ARNs, it didn't seem right to pick one or the other to put in the metadata, but rather to put both in so as to not lose information. But, I'm open to suggestions about a better way of doing this!
"github.com/hashicorp/vault/logical" | ||
"github.com/hashicorp/vault/logical/framework" | ||
"github.com/mitchellh/mapstructure" | ||
) | ||
|
||
func pathUser(b *backend) *framework.Path { | ||
return &framework.Path{ | ||
Pattern: "creds/" + framework.GenericNameRegex("name"), | ||
Pattern: "(creds|sts)/" + framework.GenericNameRegex("name"), |
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! :)
return logical.ListResponse(append(entries, legacyEntries...)), nil | ||
} | ||
|
||
func (b *backend) pathRolesDelete(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { |
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.
Personally, I would make them all be methods of the backend. However, I'm not the most familiar with golang idioms, so I figured I'd leave it the way I found it, and also to not add additional things to this PR. I'd be happy to submit a follow up to make this change, though.
if r.InvalidData != "" { | ||
respData["invalid_data"] = r.InvalidData | ||
} | ||
return respData |
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. version
is purely an implementation detail about how the role data is stored in the internal storage, to make upgrading the way roles are stored easier in the future. prohibit_flexible_cred_path
is also an implementation detail to securely enable backwards compatibility and also unify the /creds
and /sts
path. My thinking is, if clients are using the "old" parameters in defining the role, then they'll use the same /creds
or /sts
path used before to retrieve credentials. And if they're using the "new" parameters introduced in this PR, then it won't matter which of the two endpoints are used.
Of course, this has the potential to add confusion. I think omitting these is the right balance, but I'd love to hear any suggestions on how it could be improved! :)
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 is looking really good! I like the simplification of the api.
My only real concern is the upgrade on write scenario for roles. I think this can be simplified quite a bit by just upgrading the entry on read and doing the right thing with the upsert. It is one more storage write but I think that is ok. Even if there is a failure lower, the role will already be upgraded. There are also some scenarios where the policy may get deleted during the upgrade and fail further down meaning the entry is gone.
return logical.ErrorResponse("missing role name"), nil | ||
} | ||
|
||
b.roleMutex.Lock() |
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 think you can remove the mutex on write and just use the read role method with the lock to upgrade the record and return the updated entry. If the entry is nil, you an still default it to and empty role.
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 reason for the mutex is to prevent race conditions when multiple write entries are made at the same time on the HTTP endpoint. Or is this something I don't need to worry about?
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.
We don't guarantee order of requests and the upgrade itself should lock, so it is safe to not worry about locks for the http requests.
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.
Do you mean the upgrade of the role entry from legacy to current should take the lock? I still don't think that's safe and we still need the lock. Imagine the following scenario:
- Request 1 comes in to change the
policy_document
paramemter of an existing, already-upgraded role - Request 2 comes in at the same time to change the
role_arns
of the same role. - Request 1 and Request 2 both read the same, old role data
- Request 1 and Request 2 both modify the same role in different ways
- Request 1 and Request 2 will race to write different role data to the storage backend, and the loser of the race will have the request lost
If the semantics of the write involved writing the entirety of the role at once, it would be fine. But since the semantics also allow modifying only some parameters of the role in place, we need a write lock here.
Or am I missing something else?
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 think it's fine...I don't anticipate that this is going to be called a ton so contention shouldn't be bad.
builtin/logical/aws/path_roles.go
Outdated
func upgradeLegacyPolicyEntry(entry string) *awsRoleEntry { | ||
var newRoleEntry *awsRoleEntry | ||
if strings.HasPrefix(entry, "arn:") { | ||
arnRegex := regexp.MustCompile(`^arn:aws[a-z-]*:iam::(?P<account_num>\d{12}|aws):(?P<entity_type>\w+)/(?P<role_path>.+)$`) |
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.
It looks like the AWS SDK has a ARN parser. Can this be used?
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.
Yes! I hadn't seen that before, looks perfect. It requires adding it to the vendor; I've pulled that into a separate PR to keep this cleaner: #5048
builtin/logical/aws/path_roles.go
Outdated
return roleEntry, nil | ||
} | ||
|
||
b.roleMutex.Lock() |
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.
After obtaining the write lock, you will want to check if the role again to make sure it has not been upgraded.
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 catch. I really wish golang had a native concept of upgradable locks :/
builtin/logical/aws/path_roles.go
Outdated
}, | ||
|
||
"policy_document": &framework.FieldSchema{ | ||
Type: framework.TypeString, // TODO: Investigate adding a TypeJSON |
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 not sure a TypeJSON is going to be possible since you will need to know the struct you are unmarshalling to. I think TypeString is fine here and validation just moves to the path handler.
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.
Yeah, @jefferai had suggested it a little while ago, and it would really make this more user friendly, but it'll probably require a bit of gymnastics to make it work robustly.
builtin/logical/aws/path_roles.go
Outdated
} | ||
|
||
func nonLockedSetAwsRole(ctx context.Context, s logical.Storage, roleName string, roleEntry *awsRoleEntry) error { |
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 don't think you need to express the lock requirement in the function name. This is either going to be locked while upgrading or during a regular write, we don't guarantee order of requests, so it is fine that there is no lock in that case.
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 don't have strong feelings either way. I mostly did it to maintain some symmetry with the nonLockedRoleRead
/lockedRoleRead
methods.
builtin/logical/aws/path_user.go
Outdated
case iamUserCred: | ||
return b.secretAccessKeysCreate(ctx, req.Storage, req.DisplayName, roleName, role) | ||
case assumedRoleCred: | ||
if ttl == 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.
Why a default value here? Core will now handle setting defaults based on the tuned mount setting.
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.
For no good reasons.... :)
The default value of 3600 really only made sense for assumed role or federation token creds and didn't make sense for IAM user creds. I missed the fact that this parameter was being ignored for IAM user creds.
Given that, does it make sense to check and add a warning if a ttl is passed when requesting IAM user creds?
builtin/logical/aws/path_user.go
Outdated
} | ||
return b.assumeRole(ctx, req.Storage, req.DisplayName, roleName, roleArn, role.PolicyDocument, ttl) | ||
case federationTokenCred: | ||
if ttl == 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.
Same question on default value.
builtin/logical/aws/path_roles.go
Outdated
return logical.ErrorResponse("cannot supply deprecated role or policy parameters with policy_document"), nil | ||
} | ||
var compacted string | ||
if policyDocumentRaw.(string) == "" { |
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.
You can simplify this with something like this:
compacted := policyDocumentRaw.(string)
if len(compacted) > 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.
Thanks! Done.
builtin/logical/aws/path_roles.go
Outdated
roleEntry.ProhibitFlexibleCredPath = false | ||
} | ||
|
||
if len(roleEntry.CredentialTypes) < 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.
Can you make this == 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.
Yep, done.
builtin/logical/aws/path_roles.go
Outdated
|
||
func (b *backend) lockedRoleRead(ctx context.Context, s logical.Storage, roleName string) (*awsRoleEntry, error) { | ||
b.roleMutex.RLock() | ||
roleEntry, err := nonLockedRoleRead(ctx, s, roleName) |
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.
Based on some of the other suggestions, the need for the nonLocked method may go away. If so, can you just inline this logic.
Hi @joelthompson, just wanted to see if you were going to be able to get to the feedback soon. |
This gets rid of the duplicated role upgrade code between both role reading and role writing by handling the upgrade all in the role reading.
@chrishoffman I think I largely agree with your sentiment about simplifying the role upgrade code, so take a look at the most recent commit I pushed and let me know if that's what you were thinking. I'll wait for #5048 to get merged, then merge master into my PR, fix the merge conflict in the docs, and use the AWS SDK ARN parser, and hopefully this should (finally!) be good to go. |
This adds the AWS ARN parser into the vendor as suggested by hashicorp#4360 (comment)
The testAccStepReadUser and testAccStepReadSTS were virtually identical, so they are consolidated into a single method with the path passed in.
This adds the AWS ARN parser into the vendor as suggested by #4360 (comment)
assumeRoleInput := &sts.AssumeRoleInput{ | ||
RoleSessionName: aws.String(username), | ||
RoleArn: aws.String(roleArn), | ||
DurationSeconds: &lifeTimeInSeconds, |
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 definitely read this as "a life time in seconds"
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.
Looks awesome! I'm going to merge this, if anyone else on the team wants to review it before beta (or between now and final) there's still time. Thanks, Joel!
The AWS secret engine had a lot of confusing overloading with role
paramemters and how they mapped to each of the three credential types
supported. This now adds parameters to remove the overloading while
maintaining backwards compatibility.
With the change, it also becomes easier to add other feature requests.
Attaching multiple managed policies to IAM users and adding a policy
document to STS AssumedRole credentials is now also supported.
Have not yet added the
TypeJSON
as suggested in #4229. Wanted to get this out and get some feedback first. I also haven't updated the UI; investigating that now.I plan on addressing the validation suggested in #2302 in a separate PR, this is already big enough on its own :)
Fixes #4229
Fixes #3751
Fixes #2817