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

Init create #3

Merged
merged 10 commits into from
Jan 19, 2018
Merged

Init create #3

merged 10 commits into from
Jan 19, 2018

Conversation

nate-yocom
Copy link
Contributor

Updated PR after addressing review feedback from #2

Nate Yocom added 6 commits November 16, 2017 17:19
… lookup to get resolved username/uuid of user from directory service.
    - Don't default mode in cli, path_login handles this already
    - Don't apply a default policy
    - Add an existence check
    - Config not found should return same as empty config
    - Snake case struct field tags
@jefferai
Copy link
Member

jefferai commented Dec 7, 2017

@nate-yocom You don't need to close PRs and open new ones -- this makes it impossible to track comments as you make changes. You can just push new commits to the branch the PR is based off of.

@nate-yocom
Copy link
Contributor Author

Ah, good to know. Apologies. However you'd like to proceed let me know.

path_config.go Outdated
} else if req.Operation == logical.CreateOperation {
config.AppID = data.Get("app_id").(string)
}
if config.AppID == "" {
Copy link
Member

Choose a reason for hiding this comment

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

You should either do the same thing as above, where if this is a required parameter you error out if it's not set, or remove this code -- but this basically sets defaults, which the framework will already do for you when you call data.Get().

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah yeah, good catch - i'd added this before realizing defaults could be set by framework. Thanks.

path_config.go Outdated
}

// Munge on the service a little bit, force it to have no trailing / and always start with https://
var normalizedService = strings.TrimPrefix(config.ServiceURL, "http://")
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 https://golang.org/pkg/net/url/ for this -- you can parse it (while making sure it's a valid URL), then manually adjust the scheme and get a string representation.

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.

path_config.go Outdated
if err != nil {
return nil, err
}

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 preferable to return nil, nil at this point if entry == nil. That gives more flexibility to other code trying to understand whether a config existed or not (e.g. in the create or update 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.

done

path_config.go Outdated
}

type config struct {
ClientID string `json:"client_id" structs:"client_id" mapstructure:"client_id"`
Copy link
Member

Choose a reason for hiding this comment

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

You don't need the structs/mapstructure bits unless you specifically know you're using those things. (We're actually divesting the Vault codebase of their usage as much as possible these days.)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

agreed, removed.

path_login.go Outdated
"github.com/hashicorp/vault/logical/framework"
)

const defaultAuthMode = "ro"
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 set the Default option for mode in the field schema instead of doing this. A const can be nice if you're using it in multiple places, but you're not currently.

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

path_login.go Outdated

if mode == "cc" {
oclient, err = oauth.GetNewConfidentialClient(config.ServiceURL, username, password, cleanhttp.DefaultClient)
oclient.SourceHeader = "vault-auth-plugin"
Copy link
Member

Choose a reason for hiding this comment

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

It'd be preferable to name this the same as the repo name for clarity -- vault-plugin-auth-centrify

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 (everywhere)

path_login.go Outdated
token, failure, err = oclient.ClientCredentials(config.AppID, config.Scope)
} else if mode == "ro" {
oclient, err = oauth.GetNewConfidentialClient(config.ServiceURL, config.ClientID, config.ClientSecret, cleanhttp.DefaultClient)
oclient.SourceHeader = "vault-auth-plugin"
Copy link
Member

Choose a reason for hiding this comment

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

Same here.

path_login.go Outdated
}

restClient.Headers["Authorization"] = accessToken.TokenType + " " + accessToken.AccessToken
restClient.SourceHeader = "vault-auth-plugin"
Copy link
Member

Choose a reason for hiding this comment

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

Same here

path_login.go Outdated
resp := &logical.Response{
Auth: &logical.Auth{
InternalData: map[string]interface{}{
"access_token": token,
Copy link
Member

Choose a reason for hiding this comment

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

If you aren't supporting renewal, you don't need to store this -- InternalData is just there to round-trip things back to the renew function.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We don't support it today, but this token does contain a refresh token (optionally based on cloud side service configuration), so we may support it later. leaving this for future proofing.

Choose a reason for hiding this comment

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

It might be safer and a bit easier later on if you just store the items from the TokenResponse that may be required for renewal, or possibly each field separately. If you store the original response object you will need to use something like mapstructure or Marshal/Unmarshal to get this in a usable form for further processing.

path_login.go Outdated
Alias: &logical.Alias{
Name: username,
},
EntityID: strings.ToLower(uinfo.uuid),
Copy link
Member

Choose a reason for hiding this comment

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

This doesn't need to be set (and will be ignored).

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

- Client ID Type -> Confidential (must be OAuth client)
- Tokens:
- Token Type: JwtRS256
Copy link
Member

Choose a reason for hiding this comment

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

Do you actually care about the contents of the JWT or just pass it around as a token? I didn't see JWT parsing code here or in the SDK; if there is any you are using I'd like to skim it real quick.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Nope, not parsing it today

   - Default mode via Fields default
   - Defaults are handled via framework, no need to force them in config set
   - Use net/url to parse/validate service_url and force scheme/normalize path
   - return nil for config if not-existent, so exist check actually works :)
   - remove mapstructure/structs tags
   - rest client source-header set to name of repo
   - don't set EntityID on Auth result
func (b *backend) getUserInfo(accessToken *oauth.TokenResponse, serviceUrl string) (*userinfo, error) {
uinfo := &userinfo{}

restClient, err := restapi.GetNewRestClient(serviceUrl, cleanhttp.DefaultClient)
Copy link

Choose a reason for hiding this comment

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

restClient *restapi.RestClient could be part of type backend struct, and this call to instantiate a client can be made in Backend() to avoid creating a client on every login request. However, you'll need to flush the token after the request since all login requests will share the same client.

Choose a reason for hiding this comment

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

I think it is likely safer to create a new client for each request.

Copy link

Choose a reason for hiding this comment

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

Yeah that's probably the right call. I was looking at auth/aws which stores a map of EC2/IAM clients to avoid recreating clients on renewal, but this backend doesn't support renewals yet. If the discussion on renewal above means that there's will be support for it in the future, then we could store a map of RestClient instead of storing the token in InternalData.

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, keeping it simple for now - new client for each request means no potential for crossing streams (accidentally sharing headers etc), allows our cloud service to best load balance connections, and keeps it simple. We can get fancier if/when renewal support comes online and if optimization here then makes sense.

path_login.go Outdated
oclient, err = oauth.GetNewConfidentialClient(config.ServiceURL, username, password, cleanhttp.DefaultClient)
oclient.SourceHeader = "vault-plugin-auth-centrify"
if err != nil {
log.Fatal(err)

Choose a reason for hiding this comment

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

This log.Fatal will shutdown the plugin. This should probably just return nil, err.

path_login.go Outdated
oclient, err = oauth.GetNewConfidentialClient(config.ServiceURL, config.ClientID, config.ClientSecret, cleanhttp.DefaultClient)
oclient.SourceHeader = "vault-plugin-auth-centrify"
if err != nil {
log.Fatal(err)

Choose a reason for hiding this comment

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

Same comment as above.

path_login.go Outdated
var token *oauth.TokenResponse
var failure *oauth.ErrorResponse

if mode == "cc" {

Choose a reason for hiding this comment

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

Minor: This would read a little better as a switch statement on mode instead of if, else if...

path_login.go Outdated
}

uinfo, err := b.getUserInfo(token, config.ServiceURL)
b.Logger().Trace("centrify authenticated user", "userinfo", uinfo)

Choose a reason for hiding this comment

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

This probably makes sense to move after the error checking since it will log the nil user on every failure here.

func (b *backend) getUserInfo(accessToken *oauth.TokenResponse, serviceUrl string) (*userinfo, error) {
uinfo := &userinfo{}

restClient, err := restapi.GetNewRestClient(serviceUrl, cleanhttp.DefaultClient)

Choose a reason for hiding this comment

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

I think it is likely safer to create a new client for each request.

path_login.go Outdated
return nil, errors.New("centrify auth plugin configuration not set")
}

var oclient *oauth.OauthClient

Choose a reason for hiding this comment

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

It looks like you can scope oclient to the conditional below. This value is not used outside that scope.

path_login.go Outdated
resp := &logical.Response{
Auth: &logical.Auth{
InternalData: map[string]interface{}{
"access_token": token,

Choose a reason for hiding this comment

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

It might be safer and a bit easier later on if you just store the items from the TokenResponse that may be required for renewal, or possibly each field separately. If you store the original response object you will need to use something like mapstructure or Marshal/Unmarshal to get this in a usable form for further processing.

path_login.go Outdated

var rolePolicies []string
if config.RolesAsPolicies {
for _, role := range uinfo.roles {

Choose a reason for hiding this comment

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

I see that this is configurable but having implicit policies based on role name still seems a little risky to me. This could lead someone that is administering Centrify roles to accidentally escalate privileges to a user not knowing that a policy may exists with the same name in Vault. I would suggest providing an explicit mappings from roles to policies. An example would be ldap groups https://www.vaultproject.io/api/auth/ldap/index.html#read-ldap-group.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Actually, we're returning our roles as groups in Auth.GroupAliases - is there a generic vault means by which policies can be assigned via group alias? if so, this obviates the need for either RolesAsPolicies or a new group->policy mapping indirection.

Copy link
Member

Choose a reason for hiding this comment

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

@nate-yocom Indeed, you can assign policies to groups. I think you're right that this obviates the need for RolesAsPolicies. It's very new-style but it's the direction we're headed!

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'll remove the RolesAsPolicies - is the group alias -> policy stuff doc'd somewhere? I can update our doc to link to the same. Thanks!

Copy link
Member

Choose a reason for hiding this comment

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

See https://www.vaultproject.io/api/secret/identity/group-alias.html

Basically the admin of Vault needs to create the group aliases. The reason is that if we do it opportunistically we can easily end up creating 1000 aliases when 5 are needed and pollute the Identity store. But it's relatively easy to write a script to do it (one that lives in your plugin repo would probably be super nice to have as a reference!).

path_login.go Outdated
token, failure, err = oclient.ClientCredentials(config.AppID, config.Scope)
} else if mode == "ro" {
oclient, err = oauth.GetNewConfidentialClient(config.ServiceURL, config.ClientID, config.ClientSecret, cleanhttp.DefaultClient)
oclient.SourceHeader = "vault-plugin-auth-centrify"

Choose a reason for hiding this comment

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

See comment above.

uinfo.roles = append(uinfo.roles, row["Name"].(string))
}
} else {
b.Logger().Error("centrify: failed to get user roles", "error", rolesAndRightsResult.Message)

Choose a reason for hiding this comment

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

Just want to check if you intend to continue when this call result returns unsuccessful and just logs to vault's log.

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, authentication has succeeded - inability to get roles may, or may not, mean the user can do anything however - so it seems appropriate to continue, having logged an error.

Nate Yocom added 3 commits January 8, 2018 07:33
  - replace log.Fatal with err return
  - Inline error checking in a couple places
  - const for client source header
  - change if/elseif/else to a switch statement
  - don't trace user if we cant get info on them
  - remove use of logical.Response.Auth.InternalData for now (renewal not supported)
nate-yocom pushed a commit that referenced this pull request Jan 8, 2018
* commit '412b98b0d536c855829996e31d895eb50f436a94':
  Address review feedback from PR #3:

# Conflicts:
#	path_login.go
Copy link

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

Looks good!

@jefferai jefferai merged commit 50def81 into master Jan 19, 2018
@jefferai jefferai deleted the init-create branch January 19, 2018 06:04
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