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

Add Alibaba auth method #8

Merged
merged 26 commits into from
Aug 16, 2018
Merged

Add Alibaba auth method #8

merged 26 commits into from
Aug 16, 2018

Conversation

tyrannosaurus-becks
Copy link
Contributor

@tyrannosaurus-becks tyrannosaurus-becks commented Jul 11, 2018

This PR adds the logical code itself for the Alibaba auth method.

The approach mimics that of the AWS IAM auth method.

arn.go Outdated
"strings"
)

func parse(a string) (*arn, error) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Something like parseARN(s string) would be more self-descriptive.

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 can update that! Yeah that would make a lot more sense. :-)

path_login.go Outdated
return nil, fmt.Errorf(`unable to parse entity's arn %s due to %s`, callerIdentity.Arn, err)
}
if parsedARN.Type != arnTypeAssumedRole {
// We haven't tested with other arn types and would like to understand and test these use cases before blindly
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd probably remove this comment.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

OK, I have it in two places so I'll strip it in both. I wasn't sure if it'd seem helpful or terse.

path_login.go Outdated
if arn == "" {
return nil, errors.New("unable to retrieve arn from metadata during renewal")
}
roleName := req.Auth.InternalData["role_name"].(string)
Copy link
Contributor

Choose a reason for hiding this comment

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

Use the if roleName, ok := ... pattern instead so the type assertion doesn't panic if "role_name" isn't present.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh good catch. I actually copy pasta'd that code from here: https://github.com/hashicorp/vault/blob/master/builtin/credential/aws/path_login.go#L929. That may be a bug we should fix in Vault too.

path_role.go Outdated
},
"arn": {
Type: framework.TypeString,
Description: `arn of the RAM principals to bind to this 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 be "principal"?

Copy link
Contributor Author

@tyrannosaurus-becks tyrannosaurus-becks Jul 17, 2018

Choose a reason for hiding this comment

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

Actually I should probably take the word "principal" out because while there is a principal ID mentioned in the Alibaba docs, it's not an entity that's at the forefront of how people using this auth method will be thinking. Rewriting that to, "ARN of the RAM to bind to this role."

path_login.go Outdated

parsedARN, err := parse(callerIdentity.Arn)
if err != nil {
return nil, fmt.Errorf(`unable to parse entity's arn %s due to %s`, callerIdentity.Arn, err)
Copy link
Contributor

Choose a reason for hiding this comment

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

Backticks intended?

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, but this and the next do differ from the rest, so I'll update them to match.

path_login.go Outdated
// We haven't tested with other arn types and would like to understand and test these use cases before blindly
// granting access so we don't create a security vulnerability. Please open a ticket describing your use case
// for another arn type to get that started.
return nil, fmt.Errorf(`only role arn types are supported at this time, but %s was provided`, callerIdentity.Arn)
Copy link
Contributor

Choose a reason for hiding this comment

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

Backticks intended?

path_login.go Outdated
if response.StatusCode != 200 {
b, err := ioutil.ReadAll(response.Body)
if err != nil {
return nil, fmt.Errorf("error reading reponse body: %s", err)
Copy link
Contributor

Choose a reason for hiding this comment

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

typo: response

Copy link
Contributor Author

Choose a reason for hiding this comment

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

What? You don't know about the less-known child of a response, a reponse?

path_role.go Outdated
},
"arn": {
Type: framework.TypeString,
Description: `arn of the RAM principals to bind to this role.`,
Copy link
Contributor

Choose a reason for hiding this comment

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

Backticks intended?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed the backtickathon.

}
}

func pathListRoles(b *backend) *framework.Path {
Copy link
Contributor

@kalafut kalafut Jul 17, 2018

Choose a reason for hiding this comment

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

Why support both role/ and roles/? Just role/ seems pretty common.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Because AWS does? /shrug :-)

I must admit I do like the approach because I think in the CLI it makes sense to type $ vault list roles; whereas in the API I think it makes sense to GET /alibaba/role and expect a list back. Gives the user choice of either.

I also must admit I think it's odd there are two *framework.Path objects for the two endpoints, when it would be possible to make a regex that matched both. However, since the AWS code did it that way I did too because I thought it might be preferred for some reason.

Copy link
Contributor

Choose a reason for hiding this comment

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

You might want to survey some other backends as AWS might be the exception. Personally, I like one way mainly because when I see two different paths (docs/path-help/etc.) I'm going to want to know what is different between them and which I need to use.

If you stick with two, I'd go with the roles?/ regex method vs. a separate path.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh man, that's a tough one, because I see it done different ways. For instance, the AWS auth method does it this way (ex. GET /role/elk, LIST /roles, LIST /role), but the AppRole auth method does it differently (ex. GET /role/elk, LIST /role), and the ActiveDirectory secrets backend does it differently (ex. GET /roles/elk, LIST /roles).

I must say that I like the last the best, but you raise a very good point and I will check in with the team about this because consistency would increase usability.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Jeff thinks the way it currently is is the current best practice. However, more of the team may or may not weigh in.

Copy link
Member

Choose a reason for hiding this comment

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

It's all a mess. Whatever we decided in standup :-)

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 asked Chris about it after standup today in our 1:1 and we decided to leave it as-is for now.

Choose a reason for hiding this comment

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

The reason for leaving this alone was it matched how "roles" were defined across the other auth methods, not that we decided anything. I think the general rule should probably be in context of the thing being developed. For examples, most, if not all, auth methods use "role", let's go with that. That does not generally mean that we should use singular vs plural names for paths just that in the context of auth methods, that seemed the most right.

path_role.go Outdated
func (b *backend) operationRoleCreate(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {

roleName := data.Get("role").(string)
if roleName == "" {
Copy link
Contributor

Choose a reason for hiding this comment

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

Can this ever be true? If not present the route shouldn't match.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Oh! Yes, you're absolutely right. Will strip that out. Less is more.

path_role.go Outdated
func (b *backend) operationRoleUpdate(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {

roleName := data.Get("role").(string)
if roleName == "" {
Copy link
Contributor

Choose a reason for hiding this comment

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

Can this ever be true? If not present the route shouldn't match.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

✔️

path_role.go Outdated
// We haven't tested with other arn types and would like to understand and test these use cases before blindly
// granting access so we don't create a security vulnerability. Please open a ticket describing your use case
// for another arn type to get that started.
return nil, fmt.Errorf(`only role arn types are supported at this time, but %s was provided`, entry.ARN)
Copy link
Contributor

Choose a reason for hiding this comment

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

Are backticks intended?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Will fix.

path_role.go Outdated

func (b *backend) operationRoleDelete(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
roleName := data.Get("role").(string)
if roleName == "" {
Copy link
Contributor

Choose a reason for hiding this comment

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

Can this ever be true? If not present the route shouldn't match.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

✔️

path_role.go Outdated
Pattern: "role/" + framework.GenericNameRegex("role"),
Fields: map[string]*framework.FieldSchema{
"role": {
Type: framework.TypeString,
Copy link
Contributor

Choose a reason for hiding this comment

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

Probably want this to be the new framework.TypeLowerCaseString

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Very good point. To make sure this was safe to do, I tried to create a couple roles in Alibaba with the same name but different casing. It turned out I couldn't, which is good because the arn is always lower case, and things could have gotten weird with matching role names to arn names. I'll lower-case it, it should be safe.

role_manager.go Outdated
if roleName == "" {
return nil, errors.New("missing role name")
}
entry, err := s.Get(ctx, "role/"+strings.ToLower(roleName))
Copy link
Contributor

Choose a reason for hiding this comment

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

Can eliminate ToLower if the roles are lowercase type in path_role.go

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! Yeah I ended up stripping that in a whole bunch of places.

role_manager.go Outdated
func (m *RoleManager) Delete(ctx context.Context, s logical.Storage, roleName string) error {
m.roleMutex.Lock()
defer m.roleMutex.Unlock()
return s.Delete(ctx, "role/"+strings.ToLower(roleName))
Copy link
Contributor

Choose a reason for hiding this comment

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

Can eliminate ToLower if the roles are lowercase type in path_role.go

Copy link
Contributor Author

Choose a reason for hiding this comment

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

✔️

role_manager.go Outdated
if roleEntry == nil {
return errors.New("nil role entry")
}
entry, err := logical.StorageEntryJSON("role/"+strings.ToLower(roleName), roleEntry)
Copy link
Contributor

Choose a reason for hiding this comment

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

Can eliminate ToLower if the roles are lowercase type in path_role.go

Copy link
Contributor Author

Choose a reason for hiding this comment

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

✔️

role_manager.go Outdated
)

type RoleManager struct {
roleMutex sync.RWMutex
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this needed?

Copy link
Contributor Author

@tyrannosaurus-becks tyrannosaurus-becks Jul 18, 2018

Choose a reason for hiding this comment

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

I think so.

Without it, one thread could read a role, another could update it, and then the previously read into memory role that's now outdated could be acted upon.

If we think that's harmless, we could do away with it because the mutex does cause a performance hit. And it could be argued that the mutex doesn't really solve that because while it prevents concurrent writes to the same object, it doesn't enforce the proper order of operations unless the lock is awarded based on the timestamp of when it was requested... and there's no promise of that.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

And technically, I don't think a role that's read from storage, even if it's the same role, will have the same pointer as one on a different thread. So without the mutex, the -race flag still won't pick up a race.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, I see a mix of lock usage for role type operations, with probably more not using them. Thoughts on this @briankassouf ?

But assuming we did want to lock for some operations, does this approach achieve that? For example, after I call roleMgr.Read() I have a role but the mutex is unlocked and another goroutine could update the role before I complete whatever I needed to do. Where I've see locking, it is at a higher level, like at the start of pathRoleUpdate with a defer lock.unlock().

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hm, that's a good point. Yes, I did reduce the amount of code inside the mutex.

I think I'll just do away with it since it adds complexity, yet its value is negotiable since it doesn't necessarily ensure operations occur in any proper order. I mean, yes, while you could get funny outcomes from two competing threads mutating and/or reading the same role, you could also get funny outcomes from using a mutex the whole way through but having them happen in an odd order.

In edge cases where two actors are working upon the same role at the same time, neither actor would have knowledge of the other or would be surprised by an outcome effected by lacking mutexes.

So, yeah, stripping it.

Copy link
Contributor

@kalafut kalafut left a comment

Choose a reason for hiding this comment

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

Looking good! Only minor comments from me on the implementation (haven't dug into the tests yet)

path_role.go Outdated
return nil, fmt.Errorf("role name must match arn name of %s", arn.RoleName)
}
if entry.ARN.Type != arnTypeRole {
// We haven't tested with other arn types and would like to understand and test these use cases before blindly
Copy link
Contributor

Choose a reason for hiding this comment

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

I'd remove this comment.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

✔️

path_login.go Outdated
if err != nil {
return nil, fmt.Errorf(`unable to parse entity's arn %s due to %s`, callerIdentity.Arn, err)
return nil, fmt.Errorf("unable to parseARN entity's arn %s due to %s", callerIdentity.Arn, err)
Copy link
Contributor

Choose a reason for hiding this comment

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

Overzealous search and replace 🙂

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hunted these down and fixed them!

path_role.go Outdated
if err != nil {
return nil, fmt.Errorf("unable to parse arn %s: %s", arn, err)
return nil, fmt.Errorf("unable to parseARN arn %s: %s", arn, err)
Copy link
Contributor

Choose a reason for hiding this comment

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

Overzealous search and replace 🙂

path_role.go Outdated
if err != nil {
return nil, fmt.Errorf("unable to parse arn %s: %s", arn, err)
return nil, fmt.Errorf("unable to parseARN arn %s: %s", arn, err)
Copy link
Contributor

Choose a reason for hiding this comment

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

Overzealous search and replace 🙂

path_login.go Outdated
}
headers, err := parseHeaders(b64Header)
if err != nil {
return nil, fmt.Errorf("error parsing identity_request_headers: %v", err)
Copy link
Member

Choose a reason for hiding this comment

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

Please use errwrap here and in the blocks below.

path_login.go Outdated

response, err := b.getCallerIdentityClient.Do(request)
if err != nil {
return nil, fmt.Errorf("error making request: %s", err)
Copy link
Member

Choose a reason for hiding this comment

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

errwrap

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! I grepped for all my uses of Errorf and went through each of them and anything that had an err involved, I used errwrap.Wrapf instead, so I got the ones you pointed out, and the rest of them too. Just pushed that up.

path_login.go Outdated
if b64Url == "" {
return nil, errors.New("missing identity_request_url")
}
identityReqUrl, err := base64.StdEncoding.DecodeString(b64Url)
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: golint would prefer identityReqURL

Copy link
Contributor Author

Choose a reason for hiding this comment

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

✔️

@tyrannosaurus-becks
Copy link
Contributor Author

This PR is currently blocked by hashicorp/vault#4993 adding a header field type.

arn.go Outdated
return nil, fmt.Errorf("unrecognized arn: contains %d colon-separated fields, expected 5", len(outerFields))
}
if outerFields[0] != "acs" {
return nil, errors.New("unrecognized arn: does not begin with \"acs:\"")
Copy link
Member

Choose a reason for hiding this comment

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

You can use backticks here to avoid having to escape quotes

Copy link
Contributor Author

Choose a reason for hiding this comment

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

✔️

}

u := base64.StdEncoding.EncodeToString([]byte(getCallerIdentityRequest.URL.String()))
b, err := json.Marshal(getCallerIdentityRequest.Header)
Copy link
Member

Choose a reason for hiding this comment

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

Once the headers PR lands this should just embed the Header directly and let it be JSON encoded, then decoded by TypeHeader.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Cool! I just updated that PR to support b64 and pushed the changes. When it's merged I'll add it to this PR, and then to AWS auth.

Type: framework.TypeString,
Description: "Base64-encoded full URL against which to make the Alibaba request.",
},
"identity_request_headers": {
Copy link
Member

Choose a reason for hiding this comment

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

TypeHeader (just adding this as a reminder!)

path_login.go Outdated
"role_name": parsedARN.RoleName,
},
InternalData: map[string]interface{}{
"role_name": parsedARN.RoleName,
Copy link
Member

Choose a reason for hiding this comment

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

This is duplicated between here and Metadata -- only need one.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

✔️

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 really good! Just swapping out the header stuff and changing the endpoint check.

path_login.go Outdated
Description: `Base64-encoded JSON representation of the request headers.
This must include the headers over which Alibaba has included a signature.`,
Type: framework.TypeHeader,
Description: `The request headers. This must include the headers over which Alibaba
Copy link
Contributor

Choose a reason for hiding this comment

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

Alicloud?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Just a reminder that I'm going to do that in an immediate follow-on PR when this is merged. Going to do it all at once so I don't miss anything.

path_login.go Outdated
@@ -60,19 +60,12 @@ func (b *backend) pathLoginUpdate(ctx context.Context, req *logical.Request, dat
return nil, errwrap.Wrapf("error parsing identity_request_url: {{err}}", err)
}

b64Header := data.Get("identity_request_headers").(string)
if b64Header == "" {
header := data.Get("identity_request_headers").(http.Header)
Copy link
Contributor

Choose a reason for hiding this comment

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

headers seems more accurate since this is a collection.

@tyrannosaurus-becks tyrannosaurus-becks merged commit 791ef1b into master Aug 16, 2018
@tyrannosaurus-becks tyrannosaurus-becks deleted the add-ram-auth-code branch August 16, 2018 18:00
tyrannosaurus-becks pushed a commit that referenced this pull request Aug 16, 2018
tyrannosaurus-becks pushed a commit that referenced this pull request Aug 16, 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
Development

Successfully merging this pull request may close these issues.

6 participants