-
Notifications
You must be signed in to change notification settings - Fork 4
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
Init create #3
Conversation
…t config to match
…ng-sdk rather than internal lib
… 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
@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. |
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 == "" { |
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 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()
.
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 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://") |
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.
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.
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.
path_config.go
Outdated
if err != nil { | ||
return nil, err | ||
} | ||
|
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'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).
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
path_config.go
Outdated
} | ||
|
||
type config struct { | ||
ClientID string `json:"client_id" structs:"client_id" mapstructure:"client_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.
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.)
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.
agreed, removed.
path_login.go
Outdated
"github.com/hashicorp/vault/logical/framework" | ||
) | ||
|
||
const defaultAuthMode = "ro" |
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 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.
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
path_login.go
Outdated
|
||
if mode == "cc" { | ||
oclient, err = oauth.GetNewConfidentialClient(config.ServiceURL, username, password, cleanhttp.DefaultClient) | ||
oclient.SourceHeader = "vault-auth-plugin" |
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'd be preferable to name this the same as the repo name for clarity -- vault-plugin-auth-centrify
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 (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" |
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 here.
path_login.go
Outdated
} | ||
|
||
restClient.Headers["Authorization"] = accessToken.TokenType + " " + accessToken.AccessToken | ||
restClient.SourceHeader = "vault-auth-plugin" |
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 here
path_login.go
Outdated
resp := &logical.Response{ | ||
Auth: &logical.Auth{ | ||
InternalData: map[string]interface{}{ | ||
"access_token": token, |
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.
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.
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 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.
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 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), |
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 doesn't need to be set (and will be ignored).
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
- Client ID Type -> Confidential (must be OAuth client) | ||
- Tokens: | ||
- Token Type: JwtRS256 |
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 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.
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.
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) |
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.
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.
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 is likely safer to create a new client for each request.
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 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
.
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, 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) |
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 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) |
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 comment as above.
path_login.go
Outdated
var token *oauth.TokenResponse | ||
var failure *oauth.ErrorResponse | ||
|
||
if mode == "cc" { |
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.
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) |
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 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) |
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 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 |
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 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, |
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 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 { |
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 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.
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.
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.
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.
@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!
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.
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!
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.
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" |
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.
See comment above.
uinfo.roles = append(uinfo.roles, row["Name"].(string)) | ||
} | ||
} else { | ||
b.Logger().Error("centrify: failed to get user roles", "error", rolesAndRightsResult.Message) |
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.
Just want to check if you intend to continue when this call result returns unsuccessful and just logs to vault's log.
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, 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.
- 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)
* commit '412b98b0d536c855829996e31d895eb50f436a94': Address review feedback from PR #3: # Conflicts: # path_login.go
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 good!
Updated PR after addressing review feedback from #2