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

Make AWS credential types more explicit #4360

Merged
merged 8 commits into from
Aug 16, 2018

Conversation

joelthompson
Copy link
Contributor

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

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.
@jefferai jefferai modified the milestones: 0.10.1, 0.10.2 Apr 16, 2018
Copy link
Contributor

@tyrannosaurus-becks tyrannosaurus-becks left a 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) {
Copy link
Contributor

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?

Copy link
Contributor Author

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.

Copy link
Contributor

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
Copy link
Contributor

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?

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. 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! :)

Copy link
Contributor

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"),
Copy link
Contributor

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!

Copy link
Contributor Author

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,
Copy link
Contributor

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?

Copy link
Contributor Author

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!

Copy link
Contributor

Choose a reason for hiding this comment

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

👍

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! 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,
Copy link
Contributor Author

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"),
Copy link
Contributor Author

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) {
Copy link
Contributor Author

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
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. 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! :)

Copy link
Contributor

@chrishoffman chrishoffman left a 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()
Copy link
Contributor

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.

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

Copy link
Contributor

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.

Copy link
Contributor Author

@joelthompson joelthompson Aug 4, 2018

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:

  1. Request 1 comes in to change the policy_document paramemter of an existing, already-upgraded role
  2. Request 2 comes in at the same time to change the role_arns of the same role.
  3. Request 1 and Request 2 both read the same, old role data
  4. Request 1 and Request 2 both modify the same role in different ways
  5. 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?

Copy link
Member

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.

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>.+)$`)
Copy link
Contributor

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?

https://docs.aws.amazon.com/sdk-for-go/api/aws/arn/#Parse

Copy link
Contributor Author

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

return roleEntry, nil
}

b.roleMutex.Lock()
Copy link
Contributor

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.

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 catch. I really wish golang had a native concept of upgradable locks :/

},

"policy_document": &framework.FieldSchema{
Type: framework.TypeString, // TODO: Investigate adding a TypeJSON
Copy link
Contributor

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.

Copy link
Contributor Author

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.

}

func nonLockedSetAwsRole(ctx context.Context, s logical.Storage, roleName string, roleEntry *awsRoleEntry) error {
Copy link
Contributor

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.

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 don't have strong feelings either way. I mostly did it to maintain some symmetry with the nonLockedRoleRead/lockedRoleRead methods.

case iamUserCred:
return b.secretAccessKeysCreate(ctx, req.Storage, req.DisplayName, roleName, role)
case assumedRoleCred:
if ttl == 0 {
Copy link
Contributor

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.

Copy link
Contributor Author

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?

}
return b.assumeRole(ctx, req.Storage, req.DisplayName, roleName, roleArn, role.PolicyDocument, ttl)
case federationTokenCred:
if ttl == 0 {
Copy link
Contributor

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.

return logical.ErrorResponse("cannot supply deprecated role or policy parameters with policy_document"), nil
}
var compacted string
if policyDocumentRaw.(string) == "" {
Copy link
Contributor

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks! Done.

roleEntry.ProhibitFlexibleCredPath = false
}

if len(roleEntry.CredentialTypes) < 1 {
Copy link
Contributor

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?

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, done.


func (b *backend) lockedRoleRead(ctx context.Context, s logical.Storage, roleName string) (*awsRoleEntry, error) {
b.roleMutex.RLock()
roleEntry, err := nonLockedRoleRead(ctx, s, roleName)
Copy link
Contributor

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.

@jefferai jefferai modified the milestones: 0.10.2, 0.10.3 May 22, 2018
@jefferai jefferai modified the milestones: 0.10.3, 0.10.4 Jun 12, 2018
@jefferai
Copy link
Member

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.
@joelthompson
Copy link
Contributor Author

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

joelthompson added a commit to joelthompson/vault that referenced this pull request Aug 4, 2018
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.
briankassouf pushed a commit that referenced this pull request Aug 6, 2018
This adds the AWS ARN parser into the vendor as suggested by
#4360 (comment)
@jefferai jefferai modified the milestones: 0.10.5 , 0.11 Aug 13, 2018
assumeRoleInput := &sts.AssumeRoleInput{
RoleSessionName: aws.String(username),
RoleArn: aws.String(roleArn),
DurationSeconds: &lifeTimeInSeconds,
Copy link
Member

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"

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.

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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants