diff --git a/CHANGELOG.md b/CHANGELOG.md index 79cf7aabd..193b744d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,9 +18,22 @@ Canonical reference for changes, improvements, and bugfixes for Boundary. to better match other types of resources. `credentials` will still work for now but will be removed in a few releases. Finally, in the Go SDK, the `Authenticate` function now requires a `command` value to be passed in. +* Related to the above change, the output of an API + `auth-methods/:authenticate` call will return the given `command` value + and a map of attributes that depend on the given command. On the SDK side, the + output of the `Authenticate` function returns a map, from which a concrete + type can be easily umarshaled (see the updated `authenticate password` command + for an example). ### New and Improved +* cli/api/sdk: New OIDC auth method type added with support for create, read, + update, delete, and list (see new cli `oidc` subcommands available on CRUDL + operations for examples). + [PR](https://github.com/hashicorp/boundary/pull/1090) +* cli: support to login using an OIDC auth method (see the new `authenticate + password oidc` subcommand for an example) + [PR](https://github.com/hashicorp/boundary/pull/1090) * server: When performing recursive listing, `list` action is not longer required to be granted to the calling user. Instead, the given scope acts as the root point (so only results under that scope will be shown), and `list` diff --git a/Makefile b/Makefile index 48cdb3112..af024bec0 100644 --- a/Makefile +++ b/Makefile @@ -132,6 +132,7 @@ protobuild: @protoc-go-inject-tag -input=./internal/kms/store/session_key.pb.go @protoc-go-inject-tag -input=./internal/kms/store/oidc_key.pb.go @protoc-go-inject-tag -input=./internal/target/store/target.pb.go + @protoc-go-inject-tag -input=./internal/auth/oidc/store/oidc.pb.go @rm -R ${TMP_DIR} diff --git a/api/accounts/oidc_account_attributes.gen.go b/api/accounts/oidc_account_attributes.gen.go new file mode 100644 index 000000000..6d315c374 --- /dev/null +++ b/api/accounts/oidc_account_attributes.gen.go @@ -0,0 +1,9 @@ +// Code generated by "make api"; DO NOT EDIT. +package accounts + +type OidcAccountAttributes struct { + Issuer string `json:"issuer,omitempty"` + Subject string `json:"subject,omitempty"` + FullName string `json:"full_name,omitempty"` + Email string `json:"email,omitempty"` +} diff --git a/api/accounts/option.gen.go b/api/accounts/option.gen.go index a877bf58c..146dce4ac 100644 --- a/api/accounts/option.gen.go +++ b/api/accounts/option.gen.go @@ -33,7 +33,9 @@ func getDefaultOptions() options { func getOpts(opt ...Option) (options, []api.Option) { opts := getDefaultOptions() for _, o := range opt { - o(&opts) + if o != nil { + o(&opts) + } } var apiOpts []api.Option if opts.withSkipCurlOutput { @@ -96,6 +98,30 @@ func DefaultDescription() Option { } } +func WithOidcAccountIssuer(inIssuer string) Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["issuer"] = inIssuer + o.postMap["attributes"] = val + } +} + +func DefaultOidcAccountIssuer() Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["issuer"] = nil + o.postMap["attributes"] = val + } +} + func WithPasswordAccountLoginName(inLoginName string) Option { return func(o *options) { raw, ok := o.postMap["attributes"] @@ -155,3 +181,27 @@ func DefaultPasswordAccountPassword() Option { o.postMap["attributes"] = val } } + +func WithOidcAccountSubject(inSubject string) Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["subject"] = inSubject + o.postMap["attributes"] = val + } +} + +func DefaultOidcAccountSubject() Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["subject"] = nil + o.postMap["attributes"] = val + } +} diff --git a/api/authmethods/authenticate.go b/api/authmethods/authenticate.go index 388a05e0e..c586a6f26 100644 --- a/api/authmethods/authenticate.go +++ b/api/authmethods/authenticate.go @@ -2,21 +2,79 @@ package authmethods import ( "context" + "encoding/json" "fmt" - "github.com/hashicorp/boundary/api/authtokens" + "github.com/hashicorp/boundary/api" ) -func (c *Client) Authenticate(ctx context.Context, authMethodId, command string, attributes map[string]interface{}, opt ...Option) (*authtokens.AuthTokenReadResult, error) { +type AuthenticateResult struct { + Command string `json:"-"` + Attributes map[string]interface{} `json:"-"` + attributesRaw json.RawMessage + + response *api.Response +} + +func (a *AuthenticateResult) MarshalJSON() ([]byte, error) { + // Note that a will not be nil per contract of the interface. Using a map + // here causes Go to sort the resulting JSON, which makes tests easier and + // output more predictable. + out := make(map[string]interface{}) + if a.Command != "" { + out["command"] = a.Command + } + if a.Attributes != nil { + out["attributes"] = a.Attributes + } + return json.Marshal(out) +} + +func (a *AuthenticateResult) UnmarshalJSON(inBytes []byte) error { + type in struct { + Command string `json:"command"` + Attributes json.RawMessage `json:"attributes"` + } + i := new(in) + if err := json.Unmarshal(inBytes, i); err != nil { + return err + } + a.Command = i.Command + a.attributesRaw = i.Attributes + a.Attributes = make(map[string]interface{}) + if err := json.Unmarshal(i.Attributes, &a.Attributes); err != nil { + return err + } + return nil +} + +func (n AuthenticateResult) GetRawAttributes() json.RawMessage { + return n.attributesRaw +} + +func (n AuthenticateResult) GetResponse() *api.Response { + return n.response +} + +// Authenticate is a generic authenticate API call that returns a generic result. +func (c *Client) Authenticate(ctx context.Context, authMethodId, command string, attributes map[string]interface{}, opt ...Option) (*AuthenticateResult, error) { if c.client == nil { return nil, fmt.Errorf("nil client in Authenticate request") } + if authMethodId == "" { + return nil, fmt.Errorf("empty auth method passed into Authenticate request") + } + if command == "" { + return nil, fmt.Errorf("empty command passed into Authenticate request") + } _, apiOpts := getOpts(opt...) reqBody := map[string]interface{}{ - "command": command, - "attributes": attributes, + "command": command, + } + if attributes != nil { + reqBody["attributes"] = attributes } req, err := c.client.NewRequest(ctx, "POST", fmt.Sprintf("auth-methods/%s:authenticate", authMethodId), reqBody, apiOpts...) @@ -29,15 +87,15 @@ func (c *Client) Authenticate(ctx context.Context, authMethodId, command string, return nil, fmt.Errorf("error performing client request during Authenticate call: %w", err) } - target := new(authtokens.AuthTokenReadResult) - target.Item = new(authtokens.AuthToken) - apiErr, err := resp.Decode(target.Item) + target := new(AuthenticateResult) + apiErr, err := resp.Decode(target) if err != nil { return nil, fmt.Errorf("error decoding Authenticate response: %w", err) } if apiErr != nil { return nil, apiErr } + target.response = resp return target, nil } diff --git a/api/authmethods/authenticate_test.go b/api/authmethods/authenticate_test.go new file mode 100644 index 000000000..932a5fa74 --- /dev/null +++ b/api/authmethods/authenticate_test.go @@ -0,0 +1,28 @@ +package authmethods + +import ( + "encoding/json" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestAuthenticateResultMarshaling(t *testing.T) { + rawJson := []byte(`{"attributes":{"key":"value"},"command":"foo"}`) + a := new(AuthenticateResult) + require.NoError(t, json.Unmarshal(rawJson, a)) + + exp := &AuthenticateResult{ + Command: "foo", + Attributes: map[string]interface{}{ + "key": "value", + }, + attributesRaw: json.RawMessage(`{"key":"value"}`), + } + + require.Equal(t, exp, a) + + mBytes, err := json.Marshal(exp) + require.NoError(t, err) + require.Equal(t, rawJson, mBytes) +} diff --git a/api/authmethods/authmethods.gen.go b/api/authmethods/authmethods.gen.go index be9bcb984..9c57afd74 100644 --- a/api/authmethods/authmethods.gen.go +++ b/api/authmethods/authmethods.gen.go @@ -23,6 +23,7 @@ type AuthMethod struct { Version uint32 `json:"version,omitempty"` Type string `json:"type,omitempty"` Attributes map[string]interface{} `json:"attributes,omitempty"` + IsPrimary bool `json:"is_primary,omitempty"` AuthorizedActions []string `json:"authorized_actions,omitempty"` AuthorizedCollectionActions map[string][]string `json:"authorized_collection_actions,omitempty"` diff --git a/api/authmethods/changestate.go b/api/authmethods/changestate.go new file mode 100644 index 000000000..dbfdac467 --- /dev/null +++ b/api/authmethods/changestate.go @@ -0,0 +1,68 @@ +package authmethods + +import ( + "context" + "errors" + "fmt" + + "github.com/hashicorp/boundary/api" +) + +func (c *Client) ChangeState(ctx context.Context, authMethodId string, version uint32, state string, opt ...Option) (*AuthMethodUpdateResult, error) { + if authMethodId == "" { + return nil, fmt.Errorf("empty authMethodId value passed into ChangeState request") + } + if state == "" { + return nil, fmt.Errorf("empty state value passed into ChangeState request") + } + if c.client == nil { + return nil, fmt.Errorf("nil client in ChangeState request") + } + + opts, apiOpts := getOpts(opt...) + + if version == 0 { + if !opts.withAutomaticVersioning { + return nil, errors.New("zero version number passed into ChangeState request and automatic versioning not specified") + } + existingTarget, existingErr := c.Read(ctx, authMethodId, opt...) + if existingErr != nil { + if api.AsServerError(existingErr) != nil { + return nil, fmt.Errorf("error from controller when performing initial check-and-set read: %w", existingErr) + } + return nil, fmt.Errorf("error performing initial check-and-set read: %w", existingErr) + } + if existingTarget == nil { + return nil, errors.New("nil resource found when performing initial check-and-set read") + } + version = existingTarget.Item.Version + } + + reqBody := map[string]interface{}{ + "version": version, + "attributes": map[string]interface{}{"state": state}, + } + + req, err := c.client.NewRequest(ctx, "POST", fmt.Sprintf("auth-methods/%s:change-state", authMethodId), reqBody, apiOpts...) + if err != nil { + return nil, fmt.Errorf("error creating ChangeState request: %w", err) + } + + resp, err := c.client.Do(req) + if err != nil { + return nil, fmt.Errorf("error performing client request during ChangeState call: %w", err) + } + + target := new(AuthMethodUpdateResult) + target.Item = new(AuthMethod) + apiErr, err := resp.Decode(target.Item) + if err != nil { + return nil, fmt.Errorf("error decoding ChangeState response: %w", err) + } + + if apiErr != nil { + return nil, apiErr + } + + return target, nil +} diff --git a/api/authmethods/oidc_auth_method_attributes.gen.go b/api/authmethods/oidc_auth_method_attributes.gen.go new file mode 100644 index 000000000..fd25f1edd --- /dev/null +++ b/api/authmethods/oidc_auth_method_attributes.gen.go @@ -0,0 +1,18 @@ +// Code generated by "make api"; DO NOT EDIT. +package authmethods + +type OidcAuthMethodAttributes struct { + State string `json:"state,omitempty"` + Issuer string `json:"issuer,omitempty"` + ClientId string `json:"client_id,omitempty"` + ClientSecret string `json:"client_secret,omitempty"` + ClientSecretHmac string `json:"client_secret_hmac,omitempty"` + MaxAge uint32 `json:"max_age,omitempty"` + SigningAlgorithms []string `json:"signing_algorithms,omitempty"` + ApiUrlPrefix string `json:"api_url_prefix,omitempty"` + CallbackUrl string `json:"callback_url,omitempty"` + IdpCaCerts []string `json:"idp_ca_certs,omitempty"` + AllowedAudiences []string `json:"allowed_audiences,omitempty"` + DisableDiscoveredConfigValidation bool `json:"disable_discovered_config_validation,omitempty"` + DryRun bool `json:"dry_run,omitempty"` +} diff --git a/api/authmethods/oidc_auth_method_authenticate_start_response.gen.go b/api/authmethods/oidc_auth_method_authenticate_start_response.gen.go new file mode 100644 index 000000000..8573fa600 --- /dev/null +++ b/api/authmethods/oidc_auth_method_authenticate_start_response.gen.go @@ -0,0 +1,8 @@ +// Code generated by "make api"; DO NOT EDIT. +package authmethods + +type OidcAuthMethodAuthenticateStartResponse struct { + AuthUrl string `json:"auth_url,omitempty"` + TokenUrl string `json:"token_url,omitempty"` + TokenId string `json:"token_id,omitempty"` +} diff --git a/api/authmethods/option.gen.go b/api/authmethods/option.gen.go index 4a910c0e5..7e53e3b44 100644 --- a/api/authmethods/option.gen.go +++ b/api/authmethods/option.gen.go @@ -35,7 +35,9 @@ func getDefaultOptions() options { func getOpts(opt ...Option) (options, []api.Option) { opts := getDefaultOptions() for _, o := range opt { - o(&opts) + if o != nil { + o(&opts) + } } var apiOpts []api.Option if opts.withSkipCurlOutput { @@ -85,6 +87,54 @@ func WithRecursive(recurse bool) Option { } } +func WithOidcAuthMethodAllowedAudiences(inAllowedAudiences []string) Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["allowed_audiences"] = inAllowedAudiences + o.postMap["attributes"] = val + } +} + +func DefaultOidcAuthMethodAllowedAudiences() Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["allowed_audiences"] = nil + o.postMap["attributes"] = val + } +} + +func WithOidcAuthMethodApiUrlPrefix(inApiUrlPrefix string) Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["api_url_prefix"] = inApiUrlPrefix + o.postMap["attributes"] = val + } +} + +func DefaultOidcAuthMethodApiUrlPrefix() Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["api_url_prefix"] = nil + o.postMap["attributes"] = val + } +} + func WithAttributes(inAttributes map[string]interface{}) Option { return func(o *options) { o.postMap["attributes"] = inAttributes @@ -97,6 +147,54 @@ func DefaultAttributes() Option { } } +func WithOidcAuthMethodClientId(inClientId string) Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["client_id"] = inClientId + o.postMap["attributes"] = val + } +} + +func DefaultOidcAuthMethodClientId() Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["client_id"] = nil + o.postMap["attributes"] = val + } +} + +func WithOidcAuthMethodClientSecret(inClientSecret string) Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["client_secret"] = inClientSecret + o.postMap["attributes"] = val + } +} + +func DefaultOidcAuthMethodClientSecret() Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["client_secret"] = nil + o.postMap["attributes"] = val + } +} + func WithDescription(inDescription string) Option { return func(o *options) { o.postMap["description"] = inDescription @@ -109,6 +207,126 @@ func DefaultDescription() Option { } } +func WithOidcAuthMethodDisableDiscoveredConfigValidation(inDisableDiscoveredConfigValidation bool) Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["disable_discovered_config_validation"] = inDisableDiscoveredConfigValidation + o.postMap["attributes"] = val + } +} + +func DefaultOidcAuthMethodDisableDiscoveredConfigValidation() Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["disable_discovered_config_validation"] = nil + o.postMap["attributes"] = val + } +} + +func WithOidcAuthMethodDryRun(inDryRun bool) Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["dry_run"] = inDryRun + o.postMap["attributes"] = val + } +} + +func DefaultOidcAuthMethodDryRun() Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["dry_run"] = nil + o.postMap["attributes"] = val + } +} + +func WithOidcAuthMethodIdpCaCerts(inIdpCaCerts []string) Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["idp_ca_certs"] = inIdpCaCerts + o.postMap["attributes"] = val + } +} + +func DefaultOidcAuthMethodIdpCaCerts() Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["idp_ca_certs"] = nil + o.postMap["attributes"] = val + } +} + +func WithOidcAuthMethodIssuer(inIssuer string) Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["issuer"] = inIssuer + o.postMap["attributes"] = val + } +} + +func DefaultOidcAuthMethodIssuer() Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["issuer"] = nil + o.postMap["attributes"] = val + } +} + +func WithOidcAuthMethodMaxAge(inMaxAge uint32) Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["max_age"] = inMaxAge + o.postMap["attributes"] = val + } +} + +func DefaultOidcAuthMethodMaxAge() Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["max_age"] = nil + o.postMap["attributes"] = val + } +} + func WithPasswordAuthMethodMinLoginNameLength(inMinLoginNameLength uint32) Option { return func(o *options) { raw, ok := o.postMap["attributes"] @@ -168,3 +386,27 @@ func DefaultName() Option { o.postMap["name"] = nil } } + +func WithOidcAuthMethodSigningAlgorithms(inSigningAlgorithms []string) Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["signing_algorithms"] = inSigningAlgorithms + o.postMap["attributes"] = val + } +} + +func DefaultOidcAuthMethodSigningAlgorithms() Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["signing_algorithms"] = nil + o.postMap["attributes"] = val + } +} diff --git a/api/authtokens/option.gen.go b/api/authtokens/option.gen.go index 4b8a1f10b..012f06624 100644 --- a/api/authtokens/option.gen.go +++ b/api/authtokens/option.gen.go @@ -35,7 +35,9 @@ func getDefaultOptions() options { func getOpts(opt ...Option) (options, []api.Option) { opts := getDefaultOptions() for _, o := range opt { - o(&opts) + if o != nil { + o(&opts) + } } var apiOpts []api.Option if opts.withSkipCurlOutput { diff --git a/api/groups/option.gen.go b/api/groups/option.gen.go index 108cf72f4..b6bb9f1a9 100644 --- a/api/groups/option.gen.go +++ b/api/groups/option.gen.go @@ -35,7 +35,9 @@ func getDefaultOptions() options { func getOpts(opt ...Option) (options, []api.Option) { opts := getDefaultOptions() for _, o := range opt { - o(&opts) + if o != nil { + o(&opts) + } } var apiOpts []api.Option if opts.withSkipCurlOutput { diff --git a/api/hostcatalogs/option.gen.go b/api/hostcatalogs/option.gen.go index 62e0c15bc..f968ede87 100644 --- a/api/hostcatalogs/option.gen.go +++ b/api/hostcatalogs/option.gen.go @@ -35,7 +35,9 @@ func getDefaultOptions() options { func getOpts(opt ...Option) (options, []api.Option) { opts := getDefaultOptions() for _, o := range opt { - o(&opts) + if o != nil { + o(&opts) + } } var apiOpts []api.Option if opts.withSkipCurlOutput { diff --git a/api/hosts/option.gen.go b/api/hosts/option.gen.go index bf886feb9..59c519b46 100644 --- a/api/hosts/option.gen.go +++ b/api/hosts/option.gen.go @@ -33,7 +33,9 @@ func getDefaultOptions() options { func getOpts(opt ...Option) (options, []api.Option) { opts := getDefaultOptions() for _, o := range opt { - o(&opts) + if o != nil { + o(&opts) + } } var apiOpts []api.Option if opts.withSkipCurlOutput { diff --git a/api/hostsets/option.gen.go b/api/hostsets/option.gen.go index 2f72bffb5..f27578eb9 100644 --- a/api/hostsets/option.gen.go +++ b/api/hostsets/option.gen.go @@ -33,7 +33,9 @@ func getDefaultOptions() options { func getOpts(opt ...Option) (options, []api.Option) { opts := getDefaultOptions() for _, o := range opt { - o(&opts) + if o != nil { + o(&opts) + } } var apiOpts []api.Option if opts.withSkipCurlOutput { diff --git a/api/roles/option.gen.go b/api/roles/option.gen.go index 006d4d2f1..47108e8ed 100644 --- a/api/roles/option.gen.go +++ b/api/roles/option.gen.go @@ -35,7 +35,9 @@ func getDefaultOptions() options { func getOpts(opt ...Option) (options, []api.Option) { opts := getDefaultOptions() for _, o := range opt { - o(&opts) + if o != nil { + o(&opts) + } } var apiOpts []api.Option if opts.withSkipCurlOutput { diff --git a/api/scopes/option.gen.go b/api/scopes/option.gen.go index f53f93978..1ed1c9373 100644 --- a/api/scopes/option.gen.go +++ b/api/scopes/option.gen.go @@ -36,7 +36,9 @@ func getDefaultOptions() options { func getOpts(opt ...Option) (options, []api.Option) { opts := getDefaultOptions() for _, o := range opt { - o(&opts) + if o != nil { + o(&opts) + } } var apiOpts []api.Option if opts.withSkipCurlOutput { @@ -110,6 +112,18 @@ func DefaultName() Option { } } +func WithPrimaryAuthMethodId(inPrimaryAuthMethodId string) Option { + return func(o *options) { + o.postMap["primary_auth_method_id"] = inPrimaryAuthMethodId + } +} + +func DefaultPrimaryAuthMethodId() Option { + return func(o *options) { + o.postMap["primary_auth_method_id"] = nil + } +} + func WithSkipAdminRoleCreation(inSkipAdminRoleCreation bool) Option { return func(o *options) { o.queryMap["skip_admin_role_creation"] = fmt.Sprintf("%v", inSkipAdminRoleCreation) diff --git a/api/scopes/scope.gen.go b/api/scopes/scope.gen.go index 85bc1c9cc..e87b45d22 100644 --- a/api/scopes/scope.gen.go +++ b/api/scopes/scope.gen.go @@ -21,6 +21,7 @@ type Scope struct { UpdatedTime time.Time `json:"updated_time,omitempty"` Version uint32 `json:"version,omitempty"` Type string `json:"type,omitempty"` + PrimaryAuthMethodId string `json:"primary_auth_method_id,omitempty"` AuthorizedActions []string `json:"authorized_actions,omitempty"` AuthorizedCollectionActions map[string][]string `json:"authorized_collection_actions,omitempty"` diff --git a/api/sessions/option.gen.go b/api/sessions/option.gen.go index f4437c78c..6367b922b 100644 --- a/api/sessions/option.gen.go +++ b/api/sessions/option.gen.go @@ -35,7 +35,9 @@ func getDefaultOptions() options { func getOpts(opt ...Option) (options, []api.Option) { opts := getDefaultOptions() for _, o := range opt { - o(&opts) + if o != nil { + o(&opts) + } } var apiOpts []api.Option if opts.withSkipCurlOutput { diff --git a/api/targets/option.gen.go b/api/targets/option.gen.go index cf375363d..6836a226c 100644 --- a/api/targets/option.gen.go +++ b/api/targets/option.gen.go @@ -35,7 +35,9 @@ func getDefaultOptions() options { func getOpts(opt ...Option) (options, []api.Option) { opts := getDefaultOptions() for _, o := range opt { - o(&opts) + if o != nil { + o(&opts) + } } var apiOpts []api.Option if opts.withSkipCurlOutput { diff --git a/api/users/option.gen.go b/api/users/option.gen.go index e442074af..766aa6976 100644 --- a/api/users/option.gen.go +++ b/api/users/option.gen.go @@ -35,7 +35,9 @@ func getDefaultOptions() options { func getOpts(opt ...Option) (options, []api.Option) { opts := getDefaultOptions() for _, o := range opt { - o(&opts) + if o != nil { + o(&opts) + } } var apiOpts []api.Option if opts.withSkipCurlOutput { diff --git a/go.mod b/go.mod index c4b1fc6fa..78a87a344 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,7 @@ require ( github.com/grpc-ecosystem/grpc-gateway/v2 v2.3.0 github.com/hashicorp/boundary/api v0.0.8 github.com/hashicorp/boundary/sdk v0.0.4 + github.com/hashicorp/cap v0.0.0-20210408110729-0dd65efe5473 github.com/hashicorp/dbassert v0.0.0-20200930125617-6218396928df github.com/hashicorp/errwrap v1.1.0 github.com/hashicorp/go-bexpr v0.1.7 @@ -33,6 +34,7 @@ require ( github.com/iancoleman/strcase v0.1.3 github.com/jefferai/keyring v1.1.7-0.20210105022822-8749b3d9ce79 github.com/jinzhu/gorm v1.9.16 + github.com/jinzhu/now v1.1.1 // indirect github.com/kr/pretty v0.2.1 github.com/kr/text v0.2.0 github.com/lib/pq v1.9.0 @@ -52,7 +54,7 @@ require ( github.com/stretchr/testify v1.7.0 github.com/zalando/go-keyring v0.1.1 go.uber.org/atomic v1.7.0 - golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 + golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43 golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d golang.org/x/tools v0.1.0 diff --git a/go.sum b/go.sum index 2faf5bdae..650c7c14c 100644 --- a/go.sum +++ b/go.sum @@ -196,6 +196,8 @@ github.com/containerd/ttrpc v0.0.0-20190828154514-0e0f228740de/go.mod h1:PvCDdDG github.com/containerd/typeurl v0.0.0-20180627222232-a93fcdb778cd/go.mod h1:Cm3kwCdlkCfMSHURc+r6fwoGH6/F1hH3S4sg0rLFWPc= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/go-oidc v2.2.1+incompatible h1:mh48q/BqXqgjVHpy2ZY7WnWAbenxRjsz9N1i1YxjHAk= +github.com/coreos/go-oidc v2.2.1+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= @@ -431,6 +433,8 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.3.0/go.mod h1:d2gYTOTUQklu06xp0AJYY github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4= github.com/hashicorp/boundary/sdk v0.0.4 h1:l3uuXOL5ZQwYc63Bm8sLY36XGUlIN/4Q2r2SqkmGTyw= github.com/hashicorp/boundary/sdk v0.0.4/go.mod h1:MQ8FJhseJrKpwvFDTn+AUAcJ1vdk1NKUxk7DxQgH7oQ= +github.com/hashicorp/cap v0.0.0-20210408110729-0dd65efe5473 h1:qEikB8/ychrgsll9feuGjVLt0qo5oktfyTWi7BLsUrg= +github.com/hashicorp/cap v0.0.0-20210408110729-0dd65efe5473/go.mod h1:hjMYKmFvk9AOL69NbqrahCHoMPZHRWqvRcdr7ciPU7g= github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q= github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE= github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= @@ -575,8 +579,9 @@ github.com/jinzhu/gorm v1.9.16 h1:+IyIjPEABKRpsu/F8OvDPy9fyQlgsg2luMV2ZIH5i5o= github.com/jinzhu/gorm v1.9.16/go.mod h1:G3LB3wezTOWM2ITLzPxEXgSkOXAntiLHS7UdBefADcs= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= -github.com/jinzhu/now v1.0.1 h1:HjfetcXq097iXP0uoPCdnM4Efp5/9MsM0/M+XOTeR3M= github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jinzhu/now v1.1.1 h1:g39TucaRWyV3dwDO++eEc6qf8TVIQ/Da48WmqjZ3i7E= +github.com/jinzhu/now v1.1.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= github.com/jmespath/go-jmespath v0.3.0 h1:OS12ieG61fsCg5+qLJ+SsW9NicxNkg3b25OyT2yCeUc= github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik= @@ -796,6 +801,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXqo= github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= +github.com/pquerna/cachecontrol v0.0.0-20201205024021-ac21108117ac h1:jWKYCNlX4J5s8M0nHYkh7Y7c9gRVDEb3mq51j5J0F5M= +github.com/pquerna/cachecontrol v0.0.0-20201205024021-ac21108117ac/go.mod h1:hoLfEwdY11HjRfKFH6KqnPsfxlo3BP6bJehpDv8t6sQ= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM= github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= @@ -926,6 +933,7 @@ github.com/xdg/stringprep v1.0.0/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0 github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/yandex-cloud/go-genproto v0.0.0-20200722140432-762fe965ce77/go.mod h1:HEUYX/p8966tMUHHT+TsS0hF/Ca/NYwqprC5WXSDMfE= github.com/yandex-cloud/go-sdk v0.0.0-20200722140627-2194e5077f13/go.mod h1:LEdAMqa1v/7KYe4b13ALLkonuDxLph57ibUb50ctvJk= +github.com/yhat/scrape v0.0.0-20161128144610-24b7890b0945/go.mod h1:4vRFPPNYllgCacoj+0FoKOjTW68rUhEfqPLiEJaK2w8= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -986,8 +994,8 @@ golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 h1:/ZScEX8SfEmUGRHs0gxpqteO5nfNW6axyZbBdw9A12g= -golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 h1:It14KIkyBFYkHkwZ7k45minvA9aorojkyjGk9KJ5B/w= +golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -1067,8 +1075,9 @@ golang.org/x/net v0.0.0-20200904194848-62affa334b73/go.mod h1:/O7V0waA8r7cgGh81R golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201029221708-28c70e62bb1d/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20210119194325-5f4716e94777 h1:003p0dJM77cxMSyCPFphvZf/Y5/NXf5fzg6ufd1/Oew= golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 h1:qWPm9rbaAMKs8Bq/9LRpbMqxWRVUAQwMI9fVrssnTfw= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/oauth2 v0.0.0-20180227000427-d7d64896b5ff/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -1148,7 +1157,6 @@ golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43 h1:SgQ6LNaYJU0JIuEHv9+s6EbhSCwYeAf5Yvj6lpYlqAE= golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d h1:SZxvLBoTP5yHO3Frd4z4vrF+DBX9vMVanchswa69toE= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= diff --git a/internal/adding-a-new-field-readme.md b/internal/adding-a-new-field-readme.md new file mode 100644 index 000000000..753309df8 --- /dev/null +++ b/internal/adding-a-new-field-readme.md @@ -0,0 +1,56 @@ +# Extending Boundary: adding additional field to an existing API + +A very high level list of things to-do when you're adding an additional field to an existing API and corresponding CLI command. + +## Add the new field to a repository +Once you've figured out that you need an additional field in Boundary's domain model, then you need to build in support for your new field from the bottom up starting with a repository's database schema. + +* Make schema changes: + * Define the new column and provide for the migration of existing data + * Create a new migration under `internal/db/schema/migrations/postgres` + * Run `make migrations` after modifying DDL in SQL files. + +* Add the new field to the storage protobuf + * storage protobufs are under: `internal/proto/local/controller/storage` + * Define a gorm tag for the new field via `@inject_tag` + * Define a `custom_options.v1.mask_mapping` for the field which maps the storage `this` field to the API `that` field (yes, it's the opposite of how it's defined for the API protobuf) + * Run `make proto` after modifying storage protobuf + +* Extend the existing repository function for Updating the resource to incorporate the new field. This could/may entail defining new options for the Update function. + +* Check the repository's Create function for the resource, since you may need to incorporate the new field here as well. + +## Add new fields to the API/SDK resource protobufs +Now that the repository supports the new field, you can move on to adding this new field to Boundary's API layer and the generated SDK. The first step in the process is to add it to the API/SDK protobufs. + +* API protobufs are under: `internal/proto/local/controller/api/resources` + +* Define a `custom_options.v1.mask_mapping` tag for the field which maps the API `this` field to the storage `that` field (yes, it's the opposite of how it's defined for a storage protobuf) + +* Define a `custom_options.v1.generate_sdk_option` tag for the SDK + +* Run `make proto` and `make api` after modifying the API/SDK protobufs + +## Update the API handler service +Now that the API protobufs have been updated, you can move on to adding this new field to the API service handler. + +* Service handlers are under: `internal/servers/controller/handlers` + +* Incorporate the new field into the handlers `updateInRepo` func. You'll need to incorporate the inbound protobuf's field with the repository's update function. This could include adding repository options to the Update function call or passing the new field as a parameter to the call. This work will be based on how you extended the repository in the previous steps. + +* Incorporate the new field into the handler's `ToProto` function. + +## Update the CLI's command for updating the resource + +At this point, the new field is available via Boundary's API. All that's left is to incorporate it into Boundary's CLI for the appropriate command. + +* CLI commands are under: `internal/cmd/commands` + +* Incorporate the new fields to the command's `funcs.go`. Just a suggestion here: you may want to define a const for the new field name and reuse it everywhere it's required (which is several places) + +* Run `make cli` and `make dev`, before attempting to test cli changes + + + + + diff --git a/internal/api/genapi/input.go b/internal/api/genapi/input.go index a529aab4f..af51fe183 100644 --- a/internal/api/genapi/input.go +++ b/internal/api/genapi/input.go @@ -58,7 +58,7 @@ type structInfo struct { sliceSubTypes map[string]string // outputOnly indicates that we shouldn't create options for setting members - // for this struct + // for mapping src field struct outputOnly bool // versionEnabled indicates that we should build a Version handler in @@ -244,6 +244,16 @@ var inputStructs = []*structInfo{ outFile: "authmethods/password_auth_method_attributes.gen.go", subtypeName: "PasswordAuthMethod", }, + { + inProto: &authmethods.OidcAuthMethodAttributes{}, + outFile: "authmethods/oidc_auth_method_attributes.gen.go", + subtypeName: "OidcAuthMethod", + }, + { + inProto: &authmethods.OidcAuthMethodAuthenticateStartResponse{}, + outFile: "authmethods/oidc_auth_method_authenticate_start_response.gen.go", + subtypeName: "OidcAuthMethod", + }, { inProto: &authmethods.AuthMethod{}, outFile: "authmethods/authmethods.gen.go", @@ -267,6 +277,11 @@ var inputStructs = []*structInfo{ outFile: "accounts/password_account_attributes.gen.go", subtypeName: "PasswordAccount", }, + { + inProto: &accounts.OidcAccountAttributes{}, + outFile: "accounts/oidc_account_attributes.gen.go", + subtypeName: "OidcAccount", + }, { inProto: &accounts.Account{}, outFile: "accounts/account.gen.go", diff --git a/internal/api/genapi/templates.go b/internal/api/genapi/templates.go index 7c77f2a03..cd743da58 100644 --- a/internal/api/genapi/templates.go +++ b/internal/api/genapi/templates.go @@ -645,7 +645,9 @@ func getDefaultOptions() options { func getOpts(opt ...Option) (options, []api.Option) { opts := getDefaultOptions() for _, o := range opt { - o(&opts) + if o != nil { + o(&opts) + } } var apiOpts []api.Option if opts.withSkipCurlOutput { diff --git a/internal/auth/db_test.go b/internal/auth/db_test.go index d656030d2..ad20ca967 100644 --- a/internal/auth/db_test.go +++ b/internal/auth/db_test.go @@ -15,7 +15,8 @@ func TestDB_AuthMethodIDTrigger(t *testing.T) { createTable = ` create table if not exists test_auth_method ( public_id wt_public_id primary key, - scope_id wt_public_id not null references iam_scope(public_id) + scope_id wt_public_id not null references iam_scope(public_id), + name wt_name ); ` insert = ` diff --git a/internal/auth/oidc/account.go b/internal/auth/oidc/account.go new file mode 100644 index 000000000..c87dfe368 --- /dev/null +++ b/internal/auth/oidc/account.go @@ -0,0 +1,128 @@ +package oidc + +import ( + "net/url" + + "github.com/hashicorp/boundary/internal/auth/oidc/store" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/oplog" + "google.golang.org/protobuf/proto" +) + +// defaultAccountTableName defines the default table name for an Account +const defaultAccountTableName = "auth_oidc_account" + +// Account contains an OIDC auth account. It is assigned to an OIDC AuthMethod +// and updates/deletes to that AuthMethod are cascaded to its Accounts. +type Account struct { + *store.Account + tableName string +} + +// NewAccount creates a new in memory Account assigned to OIDC AuthMethod. +// WithIssuer, WithFullName, WithEmail, WithName and WithDescription are +// the only valid options. All other options are ignored. +// +// Subject equals the locally unique and never reassigned identifier within +// the Issuer for the End-User, which is intended to be consumed by the Client. +// +// Issuer equals the Verifiable Identifier for an Issuer. An Issuer +// Identifier is a case sensitive URL using the https scheme that contains +// scheme, host, and optionally, port number and path components and no query or +// fragment components. +// +// FullName equals the End-User's full name in displayable form including all name +// parts, possibly including titles and suffixes, ordered according to the +// End-User's locale and preferences. +// +// Email equals the End-User's preferred e-mail address. Its value MUST conform +// to the RFC 5322 [RFC5322] addr-spec syntax. The RP MUST NOT rely upon this +// value being unique +// +// See: https://openid.net/specs/openid-connect-core-1_0.html +func NewAccount(authMethodId string, subject string, opt ...Option) (*Account, error) { + const op = "oidc.NewAccount" + opts := getOpts(opt...) + a := &Account{ + Account: &store.Account{ + AuthMethodId: authMethodId, + Subject: subject, + Name: opts.withName, + Description: opts.withDescription, + FullName: opts.withFullName, + Email: opts.withEmail, + }, + } + if opts.withIssuer != nil { + a.Issuer = opts.withIssuer.String() + } + if err := a.validate(op); err != nil { + return nil, err // intentionally not wrapped. + } + + return a, nil +} + +// validate the Account. On success, it will return nil. +func (a *Account) validate(caller errors.Op) error { + if a.AuthMethodId == "" { + return errors.New(errors.InvalidParameter, caller, "missing auth method id") + } + if a.Subject == "" { + return errors.New(errors.InvalidParameter, caller, "missing subject") + } + if _, err := url.Parse(a.Issuer); a.Issuer != "" && err != nil { + return errors.New(errors.InvalidParameter, caller, "not a valid issuer", errors.WithWrap(err)) + } + if a.Email != "" && len(a.Email) > 320 { + return errors.New(errors.InvalidParameter, caller, "email address is too long") + } + if a.FullName != "" && len(a.FullName) > 512 { + return errors.New(errors.InvalidParameter, caller, "full name is too long") + } + return nil +} + +// AllocAccount makes an empty one in memory +func AllocAccount() *Account { + return &Account{ + Account: &store.Account{}, + } +} + +// Clone an Account. +func (a *Account) Clone() *Account { + cp := proto.Clone(a.Account) + return &Account{ + Account: cp.(*store.Account), + } +} + +// TableName returns the table name. +func (a *Account) TableName() string { + if a.tableName != "" { + return a.tableName + } + return defaultAccountTableName +} + +// SetTableName sets the table name. +func (a *Account) SetTableName(n string) { + a.tableName = n +} + +// oplog will create oplog metadata for the Account. +func (c *Account) oplog(op oplog.OpType, authMethodScopeId string) oplog.Metadata { + metadata := oplog.Metadata{ + "resource-public-id": []string{c.GetPublicId()}, + "resource-type": []string{"oidc account"}, + "op-type": []string{op.String()}, + } + if c.AuthMethodId != "" { + metadata["auth-method-id"] = []string{c.AuthMethodId} + } + if authMethodScopeId != "" { + metadata["scope-id"] = []string{authMethodScopeId} + } + return metadata +} diff --git a/internal/auth/oidc/account_test.go b/internal/auth/oidc/account_test.go new file mode 100644 index 000000000..9ac92fff5 --- /dev/null +++ b/internal/auth/oidc/account_test.go @@ -0,0 +1,372 @@ +package oidc + +import ( + "context" + "net/url" + "strings" + "testing" + + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/kms" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" +) + +func TestAccount_Create(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, wrapper) + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + rw := db.New(conn) + + databaseWrapper, err := kmsCache.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + + testAuthMethod := TestAuthMethod(t, conn, databaseWrapper, org.PublicId, InactiveState, "alice_rp", + "my-dogs-name", WithIssuer(TestConvertToUrls(t, "https://alice.com")[0]), WithApiUrl(TestConvertToUrls(t, "https://api.com")[0])) + + type args struct { + authMethodId string + subject string + opts []Option + } + tests := []struct { + name string + args args + want *Account + wantErr bool + wantIsErr errors.Code + create bool + wantCreateErr bool + wantCreateIsErr errors.Code + }{ + { + name: "valid", + args: args{ + authMethodId: testAuthMethod.PublicId, + subject: "alice", + opts: []Option{WithIssuer(TestConvertToUrls(t, "https://alice.com")[0]), WithEmail("alice@alice.com"), WithFullName("Alice Eve Smith"), WithName("alice's restuarant"), WithDescription("A good place to eat")}, + }, + create: true, + want: func() *Account { + want, err := NewAccount(testAuthMethod.PublicId, "alice", WithIssuer(TestConvertToUrls(t, "https://alice.com")[0]), WithEmail("alice@alice.com"), WithFullName("Alice Eve Smith"), WithName("alice's restuarant"), WithDescription("A good place to eat")) + require.NoError(t, err) + return want + }(), + }, + { + name: "dup", // must follow "valid" test. + args: args{ + authMethodId: testAuthMethod.PublicId, + subject: "alice", + opts: []Option{WithIssuer(TestConvertToUrls(t, "https://alice.com")[0]), WithEmail("alice@alice.com"), WithFullName("Alice Eve Smith"), WithName("alice's restuarant"), WithDescription("A good place to eat")}, + }, + create: true, + want: func() *Account { + want, err := NewAccount(testAuthMethod.PublicId, "alice", WithIssuer(TestConvertToUrls(t, "https://alice.com")[0]), WithEmail("alice@alice.com"), WithFullName("Alice Eve Smith"), WithName("alice's restuarant"), WithDescription("A good place to eat")) + require.NoError(t, err) + return want + }(), + wantCreateErr: true, + wantCreateIsErr: errors.NotUnique, + }, + { + name: "mismatch issuer", + args: args{ + authMethodId: testAuthMethod.PublicId, + subject: "newsubject", + opts: []Option{WithIssuer(TestConvertToUrls(t, "https://somethingelse.com")[0])}, + }, + create: true, + want: func() *Account { + want, err := NewAccount(testAuthMethod.PublicId, "newsubject", WithIssuer(TestConvertToUrls(t, "https://somethingelse.com")[0])) + require.NoError(t, err) + return want + }(), + }, + { + name: "empty-auth-method", + args: args{ + authMethodId: "", + subject: "alice", + }, + wantErr: true, + wantIsErr: errors.InvalidParameter, + }, + { + name: "empty-subject", + args: args{ + authMethodId: testAuthMethod.PublicId, + subject: "", + }, + wantErr: true, + wantIsErr: errors.InvalidParameter, + }, + { + name: "email-too-long", + args: args{ + authMethodId: testAuthMethod.PublicId, + subject: "alice", + opts: []Option{WithEmail(strings.Repeat("a", 500) + "@alice.com")}, + }, + wantErr: true, + wantIsErr: errors.InvalidParameter, + }, + { + name: "name-too-long", + args: args{ + authMethodId: testAuthMethod.PublicId, + subject: "alice", + opts: []Option{WithFullName(strings.Repeat("a", 750))}, + }, + wantErr: true, + wantIsErr: errors.InvalidParameter, + }, + { + name: "empty-issuer", + args: args{ + authMethodId: testAuthMethod.PublicId, + subject: "alice", + opts: []Option{WithIssuer(&url.URL{})}, + }, + create: true, + want: func() *Account { + want, err := NewAccount(testAuthMethod.PublicId, "alice", WithIssuer(&url.URL{})) + require.NoError(t, err) + return want + }(), + wantCreateErr: true, + wantCreateIsErr: errors.CheckConstraint, + }, + { + name: "nil-issuer", + args: args{ + authMethodId: testAuthMethod.PublicId, + subject: "alice", + }, + create: true, + want: func() *Account { + want, err := NewAccount(testAuthMethod.PublicId, "alice") + require.NoError(t, err) + return want + }(), + wantCreateErr: true, + wantCreateIsErr: errors.CheckConstraint, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + got, err := NewAccount(tt.args.authMethodId, tt.args.subject, tt.args.opts...) + if tt.wantErr { + require.Error(err) + assert.True(errors.Match(errors.T(tt.wantIsErr), err)) + return + } + require.NoError(err) + assert.Equal(tt.want, got) + if tt.create { + ctx := context.Background() + id, err := newAccountId(testAuthMethod.GetPublicId(), testAuthMethod.GetIssuer(), tt.args.subject) + require.NoError(err) + got.PublicId = id + err = rw.Create(ctx, got) + if tt.wantCreateErr { + assert.Error(err) + assert.True(errors.Match(errors.T(tt.wantCreateIsErr), err), err) + return + } else { + assert.NoError(err) + } + found := AllocAccount() + found.PublicId = id + err = rw.LookupByPublicId(ctx, found) + require.NoError(err) + assert.Equal(got, found) + } + }) + } + + t.Run("account issuer stays when auth method discovery url changes", func(t *testing.T) { + am := TestAuthMethod(t, conn, databaseWrapper, org.PublicId, InactiveState, "client", "secret", + WithIssuer(TestConvertToUrls(t, "https://discovery.com")[0]), WithApiUrl(TestConvertToUrls(t, "https://api.com")[0])) + a, err := NewAccount(am.GetPublicId(), "subject", WithIssuer(TestConvertToUrls(t, am.GetIssuer())[0])) + require.NoError(t, err) + id, err := newAccountId(am.GetPublicId(), am.GetIssuer(), a.GetSubject()) + require.NoError(t, err) + a.PublicId = id + ctx := context.Background() + require.NoError(t, rw.Create(ctx, a)) + assert.Equal(t, am.GetIssuer(), a.GetIssuer()) + + discoUrl := am.GetIssuer() + newDiscoUrl := "https://changed.com" + am.Issuer = newDiscoUrl + n, err := rw.Update(ctx, am, []string{"Issuer"}, nil) + require.NoError(t, err) + assert.Equal(t, 1, n) + assert.Equal(t, newDiscoUrl, am.GetIssuer()) + + require.NoError(t, rw.LookupById(ctx, a)) + assert.Equal(t, discoUrl, a.GetIssuer()) + }) +} + +func TestAccount_Delete(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, wrapper) + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + rw := db.New(conn) + + databaseWrapper, err := kmsCache.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + + testAuthMethod := + TestAuthMethod( + t, + conn, + databaseWrapper, + org.PublicId, + InactiveState, + "alice_rp", + "my-dogs-name", + WithApiUrl(TestConvertToUrls(t, "https://api.com")[0]), + WithIssuer(TestConvertToUrls(t, "https://alice.com")[0])) + + testResource := func(authMethodId string, subject string) *Account { + u, err := url.Parse(testAuthMethod.GetIssuer()) + require.NoError(t, err) + a, err := NewAccount(authMethodId, subject, WithIssuer(u)) + require.NoError(t, err) + id, err := newAccountId(testAuthMethod.GetPublicId(), testAuthMethod.GetIssuer(), subject) + require.NoError(t, err) + a.PublicId = id + return a + } + + // seed an extra callback url to just make sure the delete only gets the right num of rows + seedAccount := testResource(testAuthMethod.PublicId, "jane") + require.NoError(t, rw.Create(context.Background(), &seedAccount)) + + tests := []struct { + name string + Account *Account + wantRowsDeleted int + overrides func(*Account) + wantErr bool + wantErrMsg string + }{ + { + name: "valid", + Account: testResource(testAuthMethod.PublicId, "alice"), + wantErr: false, + wantRowsDeleted: 1, + }, + { + name: "bad-publicId", + Account: testResource(testAuthMethod.PublicId, "bad-publicId"), + overrides: func(c *Account) { c.PublicId = "bad-id" }, + wantErr: false, + wantRowsDeleted: 0, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + ctx := context.Background() + cp := tt.Account.Clone() + require.NoError(rw.Create(ctx, &cp)) + + if tt.overrides != nil { + tt.overrides(cp) + } + deletedRows, err := rw.Delete(ctx, &cp) + if tt.wantErr { + require.Error(err) + return + } + require.NoError(err) + if tt.wantRowsDeleted == 0 { + assert.Equal(tt.wantRowsDeleted, deletedRows) + return + } + assert.Equal(tt.wantRowsDeleted, deletedRows) + found := AllocAccount() + found.PublicId = tt.Account.PublicId + err = rw.LookupByPublicId(ctx, found) + assert.True(errors.IsNotFoundError(err)) + }) + } +} + +func TestAccount_Clone(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, wrapper) + + t.Run("valid", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + databaseWrapper, err := kmsCache.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase) + require.NoError(err) + m := TestAuthMethod(t, conn, databaseWrapper, org.PublicId, InactiveState, "alice_rp", "my-dogs-name", + WithIssuer(TestConvertToUrls(t, "https://alice.com")[0]), WithApiUrl(TestConvertToUrls(t, "https://api.com")[0])) + orig, err := NewAccount(m.PublicId, "alice", WithFullName("Alice Eve Smith"), WithEmail("alice@alice.com")) + require.NoError(err) + cp := orig.Clone() + assert.True(proto.Equal(cp.Account, orig.Account)) + }) + t.Run("not-equal", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + databaseWrapper, err := kmsCache.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase) + require.NoError(err) + m := TestAuthMethod(t, conn, databaseWrapper, org.PublicId, InactiveState, "alice_rp", "my-dogs-name", + WithIssuer(TestConvertToUrls(t, "https://alice.com")[0]), WithApiUrl(TestConvertToUrls(t, "https://api.com")[0])) + orig, err := NewAccount(m.PublicId, "alice", WithFullName("Alice Eve Smith"), WithEmail("alice@alice.com")) + require.NoError(err) + orig2, err := NewAccount(m.PublicId, "bob", WithFullName("Bob Eve Smith"), WithEmail("bob@alice.com")) + require.NoError(err) + + cp := orig.Clone() + assert.True(!proto.Equal(cp.Account, orig2.Account)) + }) +} + +func TestAccount_SetTableName(t *testing.T) { + t.Parallel() + defaultTableName := defaultAccountTableName + tests := []struct { + name string + setNameTo string + want string + }{ + { + name: "new-name", + setNameTo: "new-name", + want: "new-name", + }, + { + name: "reset to default", + setNameTo: "", + want: defaultTableName, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + def := AllocAccount() + require.Equal(defaultTableName, def.TableName()) + m := AllocAccount() + m.SetTableName(tt.setNameTo) + assert.Equal(tt.want, m.TableName()) + }) + } +} diff --git a/internal/auth/oidc/aud_claim.go b/internal/auth/oidc/aud_claim.go new file mode 100644 index 000000000..e8d388790 --- /dev/null +++ b/internal/auth/oidc/aud_claim.go @@ -0,0 +1,84 @@ +package oidc + +import ( + "github.com/hashicorp/boundary/internal/auth/oidc/store" + "github.com/hashicorp/boundary/internal/errors" + "google.golang.org/protobuf/proto" +) + +// defaultAudClaimTableName defines the default table name for an AudClaim +const defaultAudClaimTableName = "auth_oidc_aud_claim" + +// AudClaim defines an audience claim for an OIDC auth method. It is assigned +// to an OIDC AuthMethod and updates/deletes to that AuthMethod are cascaded to +// its AudClaims. AudClaims are value objects of an AuthMethod, therefore +// there's no need for oplog metadata, since only the AuthMethod will have +// metadata because it's the root aggregate. +// +// see aud claim in the oidc spec: +// https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest +type AudClaim struct { + *store.AudClaim + tableName string +} + +// NewAudClaim creates a new in memory audience claim assigned to an OIDC +// AuthMethod. It supports no options. If an AuthMethod as assigned AudClaims, +// then ID tokens issued from the provider must contain one of the assigned +// audiences to be valid. +// +// For more info on oidc aud claims, see the oidc spec: +// https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest +func NewAudClaim(authMethodId string, audClaim string) (*AudClaim, error) { + const op = "oidc.NewAudClaim" + + c := &AudClaim{ + AudClaim: &store.AudClaim{ + OidcMethodId: authMethodId, + Aud: audClaim, + }, + } + if err := c.validate(op); err != nil { + return nil, err // intentionally not wrapped + } + return c, nil +} + +// validate the AudClaim. On success, it will return nil. +func (a *AudClaim) validate(caller errors.Op) error { + if a.OidcMethodId == "" { + return errors.New(errors.InvalidParameter, caller, "missing oidc auth method id") + } + if a.Aud == "" { + return errors.New(errors.InvalidParameter, caller, "missing aud claim") + } + return nil +} + +// AllocAudClaim make an empty one in memory. +func AllocAudClaim() AudClaim { + return AudClaim{ + AudClaim: &store.AudClaim{}, + } +} + +// Clone an AudClaim +func (c *AudClaim) Clone() *AudClaim { + cp := proto.Clone(c.AudClaim) + return &AudClaim{ + AudClaim: cp.(*store.AudClaim), + } +} + +// TableName returns the table name. +func (c *AudClaim) TableName() string { + if c.tableName != "" { + return c.tableName + } + return defaultAudClaimTableName +} + +// SetTableName sets the table name. +func (c *AudClaim) SetTableName(n string) { + c.tableName = n +} diff --git a/internal/auth/oidc/aud_claim_test.go b/internal/auth/oidc/aud_claim_test.go new file mode 100644 index 000000000..d80a76201 --- /dev/null +++ b/internal/auth/oidc/aud_claim_test.go @@ -0,0 +1,263 @@ +package oidc + +import ( + "context" + "testing" + + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/kms" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" +) + +func TestAudClaim_Create(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, wrapper) + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + rw := db.New(conn) + + databaseWrapper, err := kmsCache.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + + testAuthMethod := TestAuthMethod(t, conn, databaseWrapper, org.PublicId, InactiveState, "alice_rp", "my-dogs-name", + WithIssuer(TestConvertToUrls(t, "https://alice.com")[0]), WithApiUrl(TestConvertToUrls(t, "https://api.com")[0])) + + type args struct { + authMethodId string + aud string + } + tests := []struct { + name string + args args + want *AudClaim + wantErr bool + wantIsErr errors.Code + create bool + wantCreateErr bool + wantCreateIsErr errors.Code + }{ + { + name: "valid", + args: args{ + authMethodId: testAuthMethod.PublicId, + aud: "valid", + }, + create: true, + want: func() *AudClaim { + want := AllocAudClaim() + want.OidcMethodId = testAuthMethod.PublicId + want.Aud = "valid" + return &want + }(), + }, + { + name: "dup", // must follow "valid" test. Url must be be unique for an OidcMethodId + args: args{ + authMethodId: testAuthMethod.PublicId, + aud: "valid", + }, + create: true, + want: func() *AudClaim { + want := AllocAudClaim() + want.OidcMethodId = testAuthMethod.PublicId + want.Aud = "valid" + return &want + }(), + wantCreateErr: true, + wantCreateIsErr: errors.NotUnique, + }, + { + name: "empty-auth-method", + args: args{ + authMethodId: "", + aud: "empty-auth-method", + }, + wantErr: true, + wantIsErr: errors.InvalidParameter, + }, + { + name: "empty-aud", + args: args{ + authMethodId: testAuthMethod.PublicId, + aud: "", + }, + wantErr: true, + wantIsErr: errors.InvalidParameter, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + got, err := NewAudClaim(tt.args.authMethodId, tt.args.aud) + if tt.wantErr { + require.Error(err) + assert.True(errors.Match(errors.T(tt.wantIsErr), err)) + return + } + require.NoError(err) + assert.Equal(tt.want, got) + if tt.create { + ctx := context.Background() + err = rw.Create(ctx, got) + if tt.wantCreateErr { + assert.Error(err) + assert.True(errors.Match(errors.T(tt.wantCreateIsErr), err)) + return + } else { + assert.NoError(err) + } + found := AllocAudClaim() + require.NoError(rw.LookupWhere(ctx, &found, "oidc_method_id = ? and aud_claim = ?", tt.args.authMethodId, tt.args.aud)) + assert.Equal(got, &found) + } + }) + } +} + +func TestAudClaim_Delete(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, wrapper) + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + rw := db.New(conn) + + databaseWrapper, err := kmsCache.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + + testAuthMethod := + TestAuthMethod(t, conn, databaseWrapper, org.PublicId, InactiveState, "alice_rp", "my-dogs-name", + WithApiUrl(TestConvertToUrls(t, "https://api.com")[0]), WithAudClaims("alice.com")) // seed an extra callback url to just make sure the delete only gets the right num of rows + + testResource := func(authMethodId string, AudClaim string) *AudClaim { + c, err := NewAudClaim(authMethodId, AudClaim) + require.NoError(t, err) + return c + } + tests := []struct { + name string + AudClaim *AudClaim + wantRowsDeleted int + overrides func(*AudClaim) + wantErr bool + wantErrMsg string + }{ + { + name: "valid", + AudClaim: testResource(testAuthMethod.PublicId, "valid"), + wantErr: false, + wantRowsDeleted: 1, + }, + { + name: "bad-OidcMethodId", + AudClaim: testResource(testAuthMethod.PublicId, "valid-2"), + overrides: func(c *AudClaim) { c.OidcMethodId = "bad-id" }, + wantErr: false, + wantRowsDeleted: 0, + }, + { + name: "bad-aud", + AudClaim: testResource(testAuthMethod.PublicId, "valid-3"), + overrides: func(c *AudClaim) { c.Aud = "bad-aud" }, + wantErr: false, + wantRowsDeleted: 0, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + ctx := context.Background() + cp := tt.AudClaim.Clone() + require.NoError(rw.Create(ctx, &cp)) + + if tt.overrides != nil { + tt.overrides(cp) + } + deletedRows, err := rw.Delete(ctx, &cp) + if tt.wantErr { + require.Error(err) + return + } + require.NoError(err) + if tt.wantRowsDeleted == 0 { + assert.Equal(tt.wantRowsDeleted, deletedRows) + return + } + assert.Equal(tt.wantRowsDeleted, deletedRows) + found := AllocAudClaim() + err = rw.LookupWhere(ctx, &found, "oidc_method_id = ? and aud_claim = ?", tt.AudClaim.OidcMethodId, tt.AudClaim.Aud) + assert.True(errors.IsNotFoundError(err)) + }) + } +} + +func TestAudClaim_Clone(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, wrapper) + + t.Run("valid", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + databaseWrapper, err := kmsCache.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase) + require.NoError(err) + m := TestAuthMethod(t, conn, databaseWrapper, org.PublicId, InactiveState, "alice_rp", "my-dogs-name", + WithIssuer(TestConvertToUrls(t, "https://alice.com")[0]), WithApiUrl(TestConvertToUrls(t, "https://api.com")[0])) + orig, err := NewAudClaim(m.PublicId, "eve.com") + require.NoError(err) + cp := orig.Clone() + assert.True(proto.Equal(cp.AudClaim, orig.AudClaim)) + }) + t.Run("not-equal", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + databaseWrapper, err := kmsCache.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase) + require.NoError(err) + m := TestAuthMethod(t, conn, databaseWrapper, org.PublicId, InactiveState, "alice_rp", "my-dogs-name", + WithIssuer(TestConvertToUrls(t, "https://alice.com")[0]), WithApiUrl(TestConvertToUrls(t, "https://api.com")[0])) + orig, err := NewAudClaim(m.PublicId, "eve.com") + require.NoError(err) + orig2, err := NewAudClaim(m.PublicId, "alice.com") + require.NoError(err) + + cp := orig.Clone() + assert.True(!proto.Equal(cp.AudClaim, orig2.AudClaim)) + }) +} + +func TestAudClaim_SetTableName(t *testing.T) { + t.Parallel() + defaultTableName := defaultAudClaimTableName + tests := []struct { + name string + setNameTo string + want string + }{ + { + name: "new-name", + setNameTo: "new-name", + want: "new-name", + }, + { + name: "reset to default", + setNameTo: "", + want: defaultTableName, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + def := AllocAudClaim() + require.Equal(defaultTableName, def.TableName()) + m := AllocAudClaim() + m.SetTableName(tt.setNameTo) + assert.Equal(tt.want, m.TableName()) + }) + } +} diff --git a/internal/auth/oidc/auth_method.go b/internal/auth/oidc/auth_method.go new file mode 100644 index 000000000..f7f51b0fa --- /dev/null +++ b/internal/auth/oidc/auth_method.go @@ -0,0 +1,358 @@ +package oidc + +import ( + "context" + "crypto/ed25519" + "crypto/hmac" + "crypto/sha256" + "encoding/base64" + "fmt" + "net/url" + "strings" + + "github.com/hashicorp/boundary/internal/auth/oidc/store" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/oplog" + wrapping "github.com/hashicorp/go-kms-wrapping" + "github.com/hashicorp/go-kms-wrapping/structwrapping" + "github.com/hashicorp/go-multierror" + "google.golang.org/protobuf/proto" +) + +// defaultAuthMethodTableName defines the default table name for an AuthMethod +const defaultAuthMethodTableName = "auth_oidc_method" + +// AuthMethod contains an OIDC auth method configuration. It is owned +// by a scope. AuthMethods can have Accounts, AudClaims, +// CallbackUrls, Certificates, SigningAlgs. AuthMethods also have one State at +// any given time which determines it's behavior for many its operations. +type AuthMethod struct { + *store.AuthMethod + tableName string +} + +// NewAuthMethod creates a new in memory AuthMethod assigned to scopeId. +// WithMaxAge, WithName and WithDescription are the only valid options. All +// other options are ignored. +// +// State equals the state of the OIDC auth method. State is not a supported +// parameter when creating new AuthMethod's since it must be Inactive for all +// new AuthMethods. +// +// Issuer equals a URL that identifies the OIDC provider. +// Boundary will strip off anything beyond scheme, host and port +// +// ClientId equals an OAuth 2.0 Client Identifier valid at the Authorization +// Server. +// +// ClientSecret equals the client's secret which will be encrypted when stored +// in the database and an hmac representation will also be stored when ever the +// secret changes. The secret is not returned via the API, the hmac is returned +// so callers can determine if it's been updated. +// +// MaxAge equals the Maximum Authentication Age. Specifies the allowable elapsed +// time in seconds since the last time the End-User was actively authenticated +// by the OP. If the elapsed time is greater than this value, the OP MUST +// attempt to actively re-authenticate the End-User. A value -1 basically +// forces the IdP to re-authenticate the End-User. Zero is not a valid value. +// +// See: https://openid.net/specs/openid-connect-core-1_0.html +// +// Supports the options of WithMaxAge, WithSigningAlgs, WithAudClaims, +// WithApiUrl and WithCertificates and all other options are ignored. +func NewAuthMethod(scopeId string, clientId string, clientSecret ClientSecret, opt ...Option) (*AuthMethod, error) { + const op = "oidc.NewAuthMethod" + + opts := getOpts(opt...) + var u string + switch { + case opts.withIssuer != nil: + // trim off anything beyond scheme, host and port + u = strings.SplitN(opts.withIssuer.String(), ".well-known/", 2)[0] + } + + a := &AuthMethod{ + AuthMethod: &store.AuthMethod{ + ScopeId: scopeId, + Name: opts.withName, + Description: opts.withDescription, + OperationalState: string(opts.withOperationalState), + Issuer: u, + ClientId: clientId, + ClientSecret: string(clientSecret), + MaxAge: int32(opts.withMaxAge), + }, + } + if opts.withApiUrl != nil { + a.ApiUrl = opts.withApiUrl.String() + } + if len(opts.withAudClaims) > 0 { + a.AudClaims = make([]string, 0, len(opts.withAudClaims)) + a.AudClaims = append(a.AudClaims, opts.withAudClaims...) + } + if len(opts.withCertificates) > 0 { + a.Certificates = make([]string, 0, len(opts.withCertificates)) + pem, err := EncodeCertificates(opts.withCertificates...) + if err != nil { + return nil, errors.Wrap(err, op) + } + a.Certificates = append(a.Certificates, pem...) + + } + if len(opts.withSigningAlgs) > 0 { + a.SigningAlgs = make([]string, 0, len(opts.withSigningAlgs)) + for _, alg := range opts.withSigningAlgs { + a.SigningAlgs = append(a.SigningAlgs, string(alg)) + } + } + + if a.OperationalState != string(InactiveState) { + if err := a.isComplete(); err != nil { + return nil, errors.Wrap(err, op, errors.WithMsg("new auth method being created with incomplete data but non-inactive state")) + } + } + + if err := a.validate(op); err != nil { + return nil, err // intentionally not wrapped. + } + if a.ClientSecretHmac != "" { + return nil, errors.New(errors.InvalidParameter, op, "client secret hmac should be empty") + } + return a, nil +} + +// validate the AuthMethod. On success, it will return nil. Since setting up an +// OIDC auth method requires a dance with the IdP, where you're need X before you +// can configure Y, we allow things like the discovery URL, client ID, client +// secret, etc to be empty until the AuthMethod moves into a PublicActive state. +// That means validate can't completely ensure the data is valid and ultimately +// we must rely on the database constraints/triggers to ensure the AuthMethod's +// data integrity. +// +// Also, you can't enforce that MaxAge can't equal zero, since the zero value == +// NULL in the database and that's what you want if it's unset. A db constraint +// will enforce that MaxAge is either -1, NULL or greater than zero. +func (a *AuthMethod) validate(caller errors.Op) error { + if a.ScopeId == "" { + return errors.New(errors.InvalidParameter, caller, "missing scope id") + } + if !validState(a.OperationalState) { + return errors.New(errors.InvalidParameter, caller, fmt.Sprintf("invalid state: %s", a.OperationalState)) + } + if a.Issuer != "" { + if _, err := url.Parse(a.Issuer); err != nil { + return errors.New(errors.InvalidParameter, caller, "not a valid issuer", errors.WithWrap(err)) + } + } + if a.ApiUrl != "" { + if _, err := url.Parse(a.ApiUrl); err != nil { + return errors.New(errors.InvalidParameter, caller, "not a valid api url", errors.WithWrap(err)) + } + } + if a.MaxAge < -1 { + return errors.New(errors.InvalidParameter, caller, "max age cannot be less than -1") + } + return nil +} + +// AllocAuthMethod makes an empty one in memory +func AllocAuthMethod() AuthMethod { + return AuthMethod{ + AuthMethod: &store.AuthMethod{}, + } +} + +// Clone an AuthMethod. +func (a *AuthMethod) Clone() *AuthMethod { + cp := proto.Clone(a.AuthMethod) + return &AuthMethod{ + AuthMethod: cp.(*store.AuthMethod), + } +} + +// TableName returns the table name. +func (a *AuthMethod) TableName() string { + if a.tableName != "" { + return a.tableName + } + return defaultAuthMethodTableName +} + +// SetTableName sets the table name. +func (a *AuthMethod) SetTableName(n string) { + a.tableName = n +} + +// oplog will create oplog metadata for the AuthMethod. +func (a *AuthMethod) oplog(op oplog.OpType) oplog.Metadata { + metadata := oplog.Metadata{ + "resource-public-id": []string{a.GetPublicId()}, + "resource-type": []string{"oidc auth method"}, + "op-type": []string{op.String()}, + "scope-id": []string{a.ScopeId}, + } + return metadata +} + +// encrypt the auth method before writing it to the db +func (a *AuthMethod) encrypt(ctx context.Context, cipher wrapping.Wrapper) error { + const op = "oidc.(AuthMethod).encrypt" + if cipher == nil { + return errors.New(errors.InvalidParameter, op, "missing cipher") + } + if err := structwrapping.WrapStruct(ctx, cipher, a.AuthMethod, nil); err != nil { + return errors.Wrap(err, op, errors.WithCode(errors.Encrypt)) + } + a.KeyId = cipher.KeyID() + if err := a.hmacClientSecret(ctx, cipher); err != nil { + return errors.Wrap(err, op) + } + return nil +} + +// decrypt the auth method after reading it from the db +func (a *AuthMethod) decrypt(ctx context.Context, cipher wrapping.Wrapper) error { + const op = "oidc.(AuthMethod).decrypt" + if cipher == nil { + return errors.New(errors.InvalidParameter, op, "missing cipher") + } + if err := structwrapping.UnwrapStruct(ctx, cipher, a.AuthMethod, nil); err != nil { + return errors.Wrap(err, op, errors.WithCode(errors.Decrypt)) + } + return nil +} + +// hmacClientSecret before writing it to the db +func (a *AuthMethod) hmacClientSecret(ctx context.Context, cipher wrapping.Wrapper) error { + const op = "oidc.(AuthMethod).hmacClientSecret" + if cipher == nil { + return errors.New(errors.InvalidParameter, op, "missing cipher") + } + reader, err := kms.NewDerivedReader(cipher, 32, []byte(a.PublicId), nil) + if err != nil { + return errors.Wrap(err, op) + } + key, _, err := ed25519.GenerateKey(reader) + if err != nil { + return errors.New(errors.Encrypt, op, "unable to generate derived key") + } + mac := hmac.New(sha256.New, key) + _, _ = mac.Write([]byte(a.ClientSecret)) + a.ClientSecretHmac = base64.RawURLEncoding.EncodeToString(mac.Sum(nil)) + return nil +} + +// isComplete() checks the auth method to see if it has all the required +// components of a complete/valid oidc auth method. +func (am *AuthMethod) isComplete() error { + const op = "oidc.(AuthMethod).isComplete" + var result *multierror.Error + if err := am.validate(op); err != nil { + result = multierror.Append(result, errors.Wrap(err, op)) + } + if am.Issuer == "" { + result = multierror.Append(result, errors.New(errors.InvalidParameter, op, "missing issuer")) + } + if am.ApiUrl == "" { + result = multierror.Append(result, errors.New(errors.InvalidParameter, op, "missing api url")) + } + if am.ClientId == "" { + result = multierror.Append(result, errors.New(errors.InvalidParameter, op, "missing client id")) + } + if am.ClientSecret == "" { + result = multierror.Append(result, errors.New(errors.InvalidParameter, op, "missing client secret")) + } + if len(am.SigningAlgs) == 0 { + result = multierror.Append(result, errors.New(errors.InvalidParameter, op, "missing signing algorithms")) + } + return result.ErrorOrNil() +} + +type convertedValues struct { + Algs []interface{} + Auds []interface{} + Certs []interface{} +} + +// convertValueObjects converts the embedded value objects. It will return an +// error if the AuthMethod's public id is not set. +func (am *AuthMethod) convertValueObjects() (*convertedValues, error) { + const op = "oidc.(AuthMethod).valueObjects" + if am.PublicId == "" { + return nil, errors.New(errors.InvalidPublicId, op, "missing public id") + } + var err error + var addAlgs, addAuds, addCerts []interface{} + if addAlgs, err = am.convertSigningAlgs(); err != nil { + return nil, errors.Wrap(err, op) + } + if addAuds, err = am.convertAudClaims(); err != nil { + return nil, errors.Wrap(err, op) + } + if addCerts, err = am.convertCertificates(); err != nil { + return nil, errors.Wrap(err, op) + } + return &convertedValues{ + Algs: addAlgs, + Auds: addAuds, + Certs: addCerts, + }, nil +} + +// convertSigningAlgs converts the embedded signing algorithms from []string +// to []interface{} where each slice element is a *SigningAlg. It will return an +// error if the AuthMethod's public id is not set. +func (am *AuthMethod) convertSigningAlgs() ([]interface{}, error) { + const op = "oidc.(AuthMethod).convertSigningAlgs" + if am.PublicId == "" { + return nil, errors.New(errors.InvalidPublicId, op, "missing public id") + } + newInterfaces := make([]interface{}, 0, len(am.SigningAlgs)) + for _, a := range am.SigningAlgs { + obj, err := NewSigningAlg(am.PublicId, Alg(a)) + if err != nil { + return nil, errors.Wrap(err, op) + } + newInterfaces = append(newInterfaces, obj) + } + return newInterfaces, nil +} + +// convertAudClaims converts the embedded audience claims from []string +// to []interface{} where each slice element is a *AudClaim. It will return an +// error if the AuthMethod's public id is not set. +func (am *AuthMethod) convertAudClaims() ([]interface{}, error) { + const op = "oidc.(AuthMethod).convertAudClaims" + if am.PublicId == "" { + return nil, errors.New(errors.InvalidPublicId, op, "missing public id") + } + newInterfaces := make([]interface{}, 0, len(am.AudClaims)) + for _, a := range am.AudClaims { + obj, err := NewAudClaim(am.PublicId, a) + if err != nil { + return nil, errors.Wrap(err, op) + } + newInterfaces = append(newInterfaces, obj) + } + return newInterfaces, nil +} + +// convertCertificates converts the embedded certificates from []string +// to []interface{} where each slice element is a *Certificate. It will return an +// error if the AuthMethod's public id is not set. +func (am *AuthMethod) convertCertificates() ([]interface{}, error) { + const op = "oidc.(AuthMethod).convertCertificates" + if am.PublicId == "" { + return nil, errors.New(errors.InvalidPublicId, op, "missing public id") + } + newInterfaces := make([]interface{}, 0, len(am.Certificates)) + for _, cert := range am.Certificates { + obj, err := NewCertificate(am.PublicId, cert) + if err != nil { + return nil, errors.Wrap(err, op) + } + newInterfaces = append(newInterfaces, obj) + } + return newInterfaces, nil +} diff --git a/internal/auth/oidc/auth_method_test.go b/internal/auth/oidc/auth_method_test.go new file mode 100644 index 000000000..f8c6af3cb --- /dev/null +++ b/internal/auth/oidc/auth_method_test.go @@ -0,0 +1,645 @@ +package oidc + +import ( + "context" + "net/url" + "testing" + + "github.com/hashicorp/boundary/internal/auth/oidc/store" + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/kms" + wrapping "github.com/hashicorp/go-kms-wrapping" + "github.com/hashicorp/go-kms-wrapping/wrappers/aead" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" +) + +func TestAuthMethod_Create(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, wrapper) + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + rw := db.New(conn) + + databaseWrapper, err := kmsCache.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + + type args struct { + scopeId string + issuer *url.URL + clientId string + clientSecret ClientSecret + opt []Option + } + tests := []struct { + name string + args args + want *AuthMethod + wantErr bool + wantIsErr errors.Code + create bool + wantNewErr bool + wantNewIsErr errors.Code + wantCreateErr bool + wantCreateIsErr errors.Code + }{ + { + name: "valid", + args: args{ + scopeId: org.PublicId, + clientId: "alice_rp", + clientSecret: ClientSecret("rp-secret"), + opt: []Option{ + WithIssuer(TestConvertToUrls(t, "http://alice.com")[0]), + WithApiUrl(TestConvertToUrls(t, "https://api.com")[0]), + WithDescription("alice's restaurant rp"), + WithName("alice.com"), + WithMaxAge(-1), + }, + }, + create: true, + want: func() *AuthMethod { + a := AllocAuthMethod() + a.ScopeId = org.PublicId + a.OperationalState = string(InactiveState) + a.Issuer = "http://alice.com" + a.ClientId = "alice_rp" + a.ClientSecret = "rp-secret" + a.MaxAge = -1 + a.Name = "alice.com" + a.Description = "alice's restaurant rp" + a.ApiUrl = "https://api.com" + return &a + }(), + }, + { + name: "valid with operational state set active", + args: args{ + scopeId: org.PublicId, + clientId: "alice_rp2", + clientSecret: ClientSecret("rp-secret2"), + opt: []Option{ + WithIssuer(TestConvertToUrls(t, "http://alice.com")[0]), + WithApiUrl(TestConvertToUrls(t, "https://api.com")[0]), + WithDescription("alice's restaurant rp"), + WithName("alice2.com"), + WithMaxAge(-1), + WithOperationalState(ActivePublicState), + WithSigningAlgs(EdDSA), + }, + }, + want: func() *AuthMethod { + a := AllocAuthMethod() + a.ScopeId = org.PublicId + a.OperationalState = string(ActivePublicState) + a.Issuer = "http://alice.com" + a.ClientId = "alice_rp2" + a.ClientSecret = "rp-secret2" + a.MaxAge = -1 + a.Name = "alice2.com" + a.Description = "alice's restaurant rp" + a.ApiUrl = "https://api.com" + a.SigningAlgs = []string{string(EdDSA)} + return &a + }(), + }, + { + name: "incomplete with operational state set active", + args: args{ + scopeId: org.PublicId, + clientId: "alice_rp3", + clientSecret: ClientSecret("rp-secret3"), + opt: []Option{ + WithIssuer(TestConvertToUrls(t, "http://alice.com")[0]), + WithApiUrl(TestConvertToUrls(t, "https://api.com")[0]), + WithDescription("alice's restaurant rp"), + WithName("alice3.com"), + WithMaxAge(-1), + WithOperationalState(ActivePublicState), + }, + }, + wantNewErr: true, + wantNewIsErr: errors.InvalidParameter, + }, + { + name: "dup", // must follow "valid" test. combination of ScopeId, Issuer and ClientId must be unique. + args: args{ + scopeId: org.PublicId, + clientId: "alice_rp", + clientSecret: ClientSecret("rp-secret"), + opt: []Option{WithIssuer(TestConvertToUrls(t, "http://alice.com")[0]), WithApiUrl(TestConvertToUrls(t, "https://api.com")[0]), WithDescription("alice's restaurant rp"), WithName("alice.com"), WithMaxAge(-1)}, + }, + create: true, + want: func() *AuthMethod { + a := AllocAuthMethod() + a.ScopeId = org.PublicId + a.OperationalState = string(InactiveState) + a.Issuer = "http://alice.com" + a.ClientId = "alice_rp" + a.ClientSecret = "rp-secret" + a.MaxAge = -1 + a.Name = "alice.com" + a.Description = "alice's restaurant rp" + a.ApiUrl = "https://api.com" + return &a + }(), + wantCreateErr: true, + wantCreateIsErr: errors.NotUnique, + }, + { + name: "valid-with-no-options", + args: args{ + scopeId: org.PublicId, + clientId: "eve_rp", + clientSecret: ClientSecret("rp-secret"), + }, + want: func() *AuthMethod { + a := AllocAuthMethod() + a.ScopeId = org.PublicId + a.OperationalState = string(InactiveState) + a.ClientId = "eve_rp" + a.ClientSecret = "rp-secret" + return &a + }(), + }, + { + name: "empty-scope-id", + args: args{ + scopeId: "", + clientId: "alice_rp", + clientSecret: ClientSecret("rp-secret"), + opt: []Option{WithIssuer(TestConvertToUrls(t, "https://alice.com")[0]), WithDescription("alice's restaurant rp"), WithName("alice.com")}, + }, + wantErr: true, + wantIsErr: errors.InvalidParameter, + }, + { + name: "nil-issuer", // should succeed. + args: args{ + scopeId: org.PublicId, + clientId: "alice_rp", + clientSecret: ClientSecret("rp-secret"), + opt: []Option{WithApiUrl(TestConvertToUrls(t, "https://api.com")[0])}, + }, + create: true, + want: func() *AuthMethod { + a := AllocAuthMethod() + a.ScopeId = org.PublicId + a.OperationalState = string(InactiveState) + a.Issuer = "" + a.ClientId = "alice_rp" + a.ClientSecret = "rp-secret" + a.MaxAge = 0 + a.ApiUrl = "https://api.com" + return &a + }(), + }, + { + name: "missing-client-id", // should succeed. + args: args{ + scopeId: org.PublicId, + clientId: "", + clientSecret: ClientSecret("rp-secret"), + opt: []Option{WithApiUrl(TestConvertToUrls(t, "https://api.com")[0]), WithIssuer(TestConvertToUrls(t, "http://alice.com")[0])}, + }, + want: func() *AuthMethod { + a := AllocAuthMethod() + a.ScopeId = org.PublicId + a.OperationalState = string(InactiveState) + a.Issuer = "http://alice.com" + a.ClientId = "" + a.ClientSecret = "rp-secret" + a.MaxAge = 0 + a.ApiUrl = "https://api.com" + return &a + }(), + }, + { + name: "missing-client-secret", // should succeed + args: args{ + scopeId: org.PublicId, + clientId: "alice_rp", + clientSecret: ClientSecret(""), + opt: []Option{WithApiUrl(TestConvertToUrls(t, "https://api.com")[0]), WithIssuer(TestConvertToUrls(t, "http://alice.com")[0])}, + }, + want: func() *AuthMethod { + a := AllocAuthMethod() + a.ScopeId = org.PublicId + a.OperationalState = string(InactiveState) + a.Issuer = "http://alice.com" + a.ClientId = "alice_rp" + a.ClientSecret = "" + a.MaxAge = 0 + a.ApiUrl = "https://api.com" + return &a + }(), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + got, err := NewAuthMethod(tt.args.scopeId, tt.args.clientId, tt.args.clientSecret, tt.args.opt...) + if tt.wantErr { + require.Error(err) + assert.True(errors.Match(errors.T(tt.wantIsErr), err)) + return + } + if tt.wantNewErr { + assert.Error(err) + assert.True(errors.Match(errors.T(tt.wantNewIsErr), err)) + return + } + require.NoError(err) + assert.Equal(tt.want, got) + if tt.create { + ctx := context.Background() + id, err := newAuthMethodId() + require.NoError(err) + got.PublicId = id + err = got.encrypt(ctx, databaseWrapper) + require.NoError(err) + err = rw.Create(ctx, got) + if tt.wantCreateErr { + assert.Error(err) + assert.True(errors.Match(errors.T(tt.wantCreateIsErr), err)) + return + } else { + assert.NoError(err) + } + + found := AllocAuthMethod() + found.PublicId = got.PublicId + require.NoError(rw.LookupByPublicId(ctx, &found)) + require.NoError(found.decrypt(ctx, databaseWrapper)) + assert.Equal(got, &found) + } + }) + } +} + +func TestAuthMethod_Delete(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, wrapper) + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + rw := db.New(conn) + ctx := context.Background() + + databaseWrapper, err := kmsCache.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + + testResource := func(issuer string, clientId, clientSecret string) *AuthMethod { + got, err := NewAuthMethod(org.PublicId, clientId, ClientSecret(clientSecret), + WithIssuer(TestConvertToUrls(t, issuer)[0]), WithApiUrl(TestConvertToUrls(t, "http://api.com")[0])) + require.NoError(t, err) + id, err := newAuthMethodId() + require.NoError(t, err) + got.PublicId = id + err = got.encrypt(ctx, databaseWrapper) + require.NoError(t, err) + return got + } + tests := []struct { + name string + authMethod *AuthMethod + overrides func(*AuthMethod) + wantRowsDeleted int + wantErr bool + wantErrMsg string + }{ + { + name: "valid", + authMethod: testResource("https://alice.com", "alice-rp", "alice's dog's name"), + wantErr: false, + wantRowsDeleted: 1, + }, + { + name: "bad-id", + authMethod: testResource("https://alice.com", "alice-rp", "alice's dog's name"), + overrides: func(a *AuthMethod) { a.PublicId = "bad-id" }, + wantErr: false, + wantRowsDeleted: 0, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + ctx := context.Background() + cp := tt.authMethod.Clone() + require.NoError(rw.Create(ctx, &cp)) + + if tt.overrides != nil { + tt.overrides(cp) + } + deletedRows, err := rw.Delete(context.Background(), &cp) + if tt.wantErr { + require.Error(err) + return + } + require.NoError(err) + if tt.wantRowsDeleted == 0 { + assert.Equal(tt.wantRowsDeleted, deletedRows) + return + } + assert.Equal(tt.wantRowsDeleted, deletedRows) + foundAuthMethod := AllocAuthMethod() + foundAuthMethod.PublicId = tt.authMethod.PublicId + err = rw.LookupById(context.Background(), &foundAuthMethod) + require.Error(err) + assert.True(errors.IsNotFoundError(err)) + }) + } +} + +func TestAuthMethod_Clone(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, wrapper) + + t.Run("valid", func(t *testing.T) { + assert := assert.New(t) + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + databaseWrapper, err := kmsCache.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + m := TestAuthMethod(t, conn, databaseWrapper, org.PublicId, InactiveState, "alice_rp", "my-dogs-name", + WithIssuer(TestConvertToUrls(t, "https://alice.com")[0]), WithApiUrl(TestConvertToUrls(t, "https://alice.com")[0])) + m.DisableDiscoveredConfigValidation = true + + cp := m.Clone() + assert.Equal(cp.DisableDiscoveredConfigValidation, true) + assert.True(proto.Equal(cp.AuthMethod, m.AuthMethod)) + }) + t.Run("not-equal", func(t *testing.T) { + assert := assert.New(t) + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + databaseWrapper, err := kmsCache.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + m := TestAuthMethod(t, conn, databaseWrapper, org.PublicId, InactiveState, "alice_rp", "my-dogs-name", + WithIssuer(TestConvertToUrls(t, "https://alice.com")[0]), WithApiUrl(TestConvertToUrls(t, "https://api.com")[0])) + m2 := TestAuthMethod(t, conn, databaseWrapper, org.PublicId, InactiveState, "alice_rp2", "my-dogs-name", + WithIssuer(TestConvertToUrls(t, "https://alice.com")[0]), WithApiUrl(TestConvertToUrls(t, "https://api.com")[0])) + + cp := m.Clone() + assert.True(!proto.Equal(cp.AuthMethod, m2.AuthMethod)) + }) +} + +func TestAuthMethod_SetTableName(t *testing.T) { + t.Parallel() + defaultTableName := defaultAuthMethodTableName + tests := []struct { + name string + setNameTo string + want string + }{ + { + name: "new-name", + setNameTo: "new-name", + want: "new-name", + }, + { + name: "reset to default", + setNameTo: "", + want: defaultTableName, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + def := AllocAuthMethod() + require.Equal(defaultTableName, def.TableName()) + m := AllocAuthMethod() + m.SetTableName(tt.setNameTo) + assert.Equal(tt.want, m.TableName()) + }) + } +} + +func Test_encrypt_decrypt_hmac(t *testing.T) { + ctx := context.Background() + conn, _ := db.TestSetup(t, "postgres") + rootWrapper := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, rootWrapper) + org, proj := iam.TestScopes(t, iam.TestRepo(t, conn, rootWrapper)) + databaseWrapper, err := kmsCache.GetWrapper(ctx, org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + projDatabaseWrapper, err := kmsCache.GetWrapper(ctx, proj.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + m := TestAuthMethod(t, conn, databaseWrapper, org.PublicId, InactiveState, "alice_rp", "my-dogs-name", + WithIssuer(TestConvertToUrls(t, "https://alice.com")[0]), WithApiUrl(TestConvertToUrls(t, "https://api.com")[0])) + + tests := []struct { + name string + am *AuthMethod + hmacWrapper wrapping.Wrapper + wantHmacErrMatch *errors.Template + encryptWrapper wrapping.Wrapper + wantEncryptErrMatch *errors.Template + decryptWrapper wrapping.Wrapper + wantDecryptErrMatch *errors.Template + }{ + { + name: "success", + am: m, + hmacWrapper: databaseWrapper, + encryptWrapper: databaseWrapper, + decryptWrapper: databaseWrapper, + }, + { + name: "encrypt-missing-wrapper", + am: m, + hmacWrapper: databaseWrapper, + wantEncryptErrMatch: errors.T(errors.InvalidParameter), + }, + { + name: "encrypt-bad-wrapper", + am: m, + hmacWrapper: databaseWrapper, + encryptWrapper: &aead.Wrapper{}, + wantEncryptErrMatch: errors.T(errors.Encrypt), + }, + { + name: "encrypt-missing-wrapper", + am: m, + hmacWrapper: databaseWrapper, + encryptWrapper: databaseWrapper, + wantDecryptErrMatch: errors.T(errors.InvalidParameter), + }, + { + name: "decrypt-bad-wrapper", + am: m, + hmacWrapper: databaseWrapper, + encryptWrapper: databaseWrapper, + decryptWrapper: &aead.Wrapper{}, + wantDecryptErrMatch: errors.T(errors.Decrypt), + }, + { + name: "decrypt-wrong-wrapper", + am: m, + hmacWrapper: databaseWrapper, + encryptWrapper: databaseWrapper, + decryptWrapper: projDatabaseWrapper, + wantDecryptErrMatch: errors.T(errors.Decrypt), + }, + { + name: "hmac-missing-wrapper", + am: m, + encryptWrapper: databaseWrapper, + decryptWrapper: databaseWrapper, + wantHmacErrMatch: errors.T(errors.InvalidParameter), + }, + { + name: "hmac-bad-wrapper", + am: m, + hmacWrapper: &aead.Wrapper{}, + encryptWrapper: databaseWrapper, + decryptWrapper: databaseWrapper, + wantHmacErrMatch: errors.T(errors.InvalidParameter), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + + hmacAuthMethod := tt.am.Clone() + err = hmacAuthMethod.hmacClientSecret(ctx, tt.hmacWrapper) + if tt.wantHmacErrMatch != nil { + require.Error(err) + } else { + require.NoError(err) + } + + encryptedAuthMethod := tt.am.Clone() + err = encryptedAuthMethod.encrypt(ctx, tt.encryptWrapper) + if tt.wantEncryptErrMatch != nil { + require.Error(err) + assert.Truef(errors.Match(tt.wantEncryptErrMatch, err), "expected %q and got err: %+v", tt.wantEncryptErrMatch.Code, err) + return + } + require.NoError(err) + assert.NotEmpty(encryptedAuthMethod.CtClientSecret) + assert.NotEmpty(encryptedAuthMethod.ClientSecretHmac) + + decryptedAuthMethod := encryptedAuthMethod.Clone() + decryptedAuthMethod.ClientSecret = "" + err = decryptedAuthMethod.decrypt(ctx, tt.decryptWrapper) + if tt.wantDecryptErrMatch != nil { + require.Error(err) + assert.Truef(errors.Match(tt.wantDecryptErrMatch, err), "expected %q and got err: %+v", tt.wantDecryptErrMatch.Code, err) + return + } + require.NoError(err) + assert.Equal(tt.am.ClientSecret, decryptedAuthMethod.ClientSecret) + }) + } +} + +func Test_convertValueObjects(t *testing.T) { + testPublicId := "test-id" + + testAlgs := []string{string(RS256), string(RS384)} + var testSigningAlgs []interface{} + for _, a := range []Alg{RS256, RS384} { + obj, err := NewSigningAlg(testPublicId, Alg(a)) + require.NoError(t, err) + testSigningAlgs = append(testSigningAlgs, obj) + } + + testAuds := []string{"alice", "eve"} + var testAudiences []interface{} + for _, a := range testAuds { + obj, err := NewAudClaim(testPublicId, a) + require.NoError(t, err) + testAudiences = append(testAudiences, obj) + } + + _, pem := testGenerateCA(t, "localhost") + testCerts := []string{pem} + c, err := NewCertificate(testPublicId, pem) + require.NoError(t, err) + testCertificates := []interface{}{c} + + tests := []struct { + name string + authMethodId string + algs []string + auds []string + certs []string + wantValues *convertedValues + wantErrMatch *errors.Template + wantErrContains string + }{ + { + name: "success", + authMethodId: testPublicId, + algs: testAlgs, + auds: testAuds, + certs: testCerts, + wantValues: &convertedValues{ + Algs: testSigningAlgs, + Auds: testAudiences, + Certs: testCertificates, + }, + }, + { + name: "missing-public-id", + algs: testAlgs, + wantErrMatch: errors.T(errors.InvalidPublicId), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + am := &AuthMethod{ + AuthMethod: &store.AuthMethod{ + PublicId: tt.authMethodId, + SigningAlgs: tt.algs, + AudClaims: tt.auds, + Certificates: tt.certs, + }, + } + + convertedAlgs, err := am.convertSigningAlgs() + if tt.wantErrMatch != nil { + require.Error(err) + assert.Truef(errors.Match(tt.wantErrMatch, err), "wanted err %q and got: %+v", tt.wantErrMatch.Code, err) + } else { + assert.Equal(tt.wantValues.Algs, convertedAlgs) + } + + convertedAuds, err := am.convertAudClaims() + if tt.wantErrMatch != nil { + require.Error(err) + assert.Truef(errors.Match(tt.wantErrMatch, err), "wanted err %q and got: %+v", tt.wantErrMatch.Code, err) + } else { + assert.Equal(tt.wantValues.Auds, convertedAuds) + } + + convertedCerts, err := am.convertCertificates() + if tt.wantErrMatch != nil { + require.Error(err) + assert.Truef(errors.Match(tt.wantErrMatch, err), "wanted err %q and got: %+v", tt.wantErrMatch.Code, err) + } else { + assert.Equal(tt.wantValues.Certs, convertedCerts) + } + + values, err := am.convertValueObjects() + if tt.wantErrMatch != nil { + require.Error(err) + assert.Truef(errors.Match(tt.wantErrMatch, err), "wanted err %q and got: %+v", tt.wantErrMatch.Code, err) + if tt.wantErrContains != "" { + assert.Contains(err.Error(), tt.wantErrContains) + } + return + } + require.NoError(err) + assert.Equal(tt.wantValues, values) + }) + } +} diff --git a/internal/auth/oidc/certificate.go b/internal/auth/oidc/certificate.go new file mode 100644 index 000000000..cd1bcb3a3 --- /dev/null +++ b/internal/auth/oidc/certificate.go @@ -0,0 +1,89 @@ +package oidc + +import ( + "crypto/x509" + "encoding/pem" + "fmt" + + "github.com/hashicorp/boundary/internal/auth/oidc/store" + "github.com/hashicorp/boundary/internal/errors" + "google.golang.org/protobuf/proto" +) + +// defaultCertificateTableName defines the default table name for a certificate +const defaultCertificateTableName = "auth_oidc_certificate" + +// Certificate defines a certificate to use as part of a trust root when +// connecting to the auth method's OIDC Provider. It is assigned to an OIDC +// AuthMethod and updates/deletes to that AuthMethod are cascaded to its +// Certificates. Certificates are value objects of an AuthMethod, therefore +// there's no need for oplog metadata, since only the AuthMethod will have +// metadata because it's the root aggregate. +type Certificate struct { + *store.Certificate + tableName string +} + +// NewCertificate creates a new in memory certificate assigned to and OIDC auth +// method. +func NewCertificate(authMethodId string, certificatePem string) (*Certificate, error) { + const op = "oidc.NewCertificate" + + c := &Certificate{ + Certificate: &store.Certificate{ + OidcMethodId: authMethodId, + Cert: certificatePem, + }, + } + if err := c.validate(op); err != nil { + return nil, err // intentionally not wrapped + } + return c, nil +} + +// validate the Certifcate and on success return nil +func (c *Certificate) validate(caller errors.Op) error { + if c.OidcMethodId == "" { + return errors.New(errors.InvalidParameter, caller, "missing oidc auth method id") + } + if c.Cert == "" { + return errors.New(errors.InvalidParameter, caller, "empty cert") + } + block, _ := pem.Decode([]byte(c.Cert)) + if block == nil { + return errors.New(errors.InvalidParameter, caller, "failed to parse certificate PEM") + } + _, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return errors.New(errors.InvalidParameter, caller, fmt.Sprintf("failed to parse certificate: %s"+err.Error()), errors.WithWrap(err)) + } + return nil +} + +// AllocCertificate makes an empty one in memory +func AllocCertificate() Certificate { + return Certificate{ + Certificate: &store.Certificate{}, + } +} + +// Clone a Certificate +func (c *Certificate) Clone() *Certificate { + cp := proto.Clone(c.Certificate) + return &Certificate{ + Certificate: cp.(*store.Certificate), + } +} + +// TableName returns the table name. +func (c *Certificate) TableName() string { + if c.tableName != "" { + return c.tableName + } + return defaultCertificateTableName +} + +// SetTableName sets the table name. +func (c *Certificate) SetTableName(n string) { + c.tableName = n +} diff --git a/internal/auth/oidc/certificate_test.go b/internal/auth/oidc/certificate_test.go new file mode 100644 index 000000000..3bc55b792 --- /dev/null +++ b/internal/auth/oidc/certificate_test.go @@ -0,0 +1,270 @@ +package oidc + +import ( + "context" + "testing" + + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/kms" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" +) + +func TestCertificate_Create(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, wrapper) + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + rw := db.New(conn) + + databaseWrapper, err := kmsCache.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + + testAuthMethod := TestAuthMethod(t, conn, databaseWrapper, org.PublicId, InactiveState, "alice_rp", "my-dogs-name", + WithIssuer(TestConvertToUrls(t, "https://alice.com")[0]), WithApiUrl(TestConvertToUrls(t, "https://api.com")[0])) + + _, pem := testGenerateCA(t, "localhost") + type args struct { + authMethodId string + certificate string + } + tests := []struct { + name string + args args + want *Certificate + wantErr bool + wantIsErr errors.Code + create bool + wantCreateErr bool + wantCreateIsErr errors.Code + }{ + { + name: "valid", + args: args{ + authMethodId: testAuthMethod.PublicId, + certificate: pem, + }, + create: true, + want: func() *Certificate { + want, err := NewCertificate(testAuthMethod.PublicId, pem) + require.NoError(t, err) + return want + }(), + }, + { + name: "dup", + args: args{ + authMethodId: testAuthMethod.PublicId, + certificate: pem, + }, + create: true, + want: func() *Certificate { + want, err := NewCertificate(testAuthMethod.PublicId, pem) + require.NoError(t, err) + return want + }(), + wantCreateErr: true, + wantCreateIsErr: errors.NotUnique, + }, + { + name: "empty-auth-method", + args: args{ + authMethodId: "", + certificate: pem, + }, + wantErr: true, + wantIsErr: errors.InvalidParameter, + }, + { + name: "empty-certificate", + args: args{ + authMethodId: testAuthMethod.PublicId, + certificate: "", + }, + wantErr: true, + wantIsErr: errors.InvalidParameter, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + got, err := NewCertificate(tt.args.authMethodId, tt.args.certificate) + if tt.wantErr { + require.Error(err) + assert.True(errors.Match(errors.T(tt.wantIsErr), err)) + return + } + require.NoError(err) + assert.Equal(tt.want, got) + if tt.create { + ctx := context.Background() + err = rw.Create(ctx, got) + if tt.wantCreateErr { + assert.Error(err) + assert.True(errors.Match(errors.T(tt.wantCreateIsErr), err)) + return + } else { + assert.NoError(err) + } + found := AllocCertificate() + require.NoError(rw.LookupWhere(ctx, &found, "oidc_method_id = ? and certificate = ?", tt.args.authMethodId, tt.args.certificate)) + assert.Equal(got, &found) + } + }) + } +} + +func TestCertificate_Delete(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, wrapper) + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + rw := db.New(conn) + + databaseWrapper, err := kmsCache.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + + cert, _ := testGenerateCA(t, "localhost") + + testAuthMethod := + TestAuthMethod(t, conn, databaseWrapper, org.PublicId, InactiveState, "alice_rp", "my-dogs-name", WithApiUrl(TestConvertToUrls(t, "https://apiurl.com")[0]), WithCertificates(cert)) // seed an extra callback url to just make sure the delete only gets the right num of rows + + // make another test cert + _, pem2 := testGenerateCA(t, "localhost") + _, pem3 := testGenerateCA(t, "localhost") + _, pem4 := testGenerateCA(t, "localhost") + + testResource := func(authMethodId string, cert string) *Certificate { + c, err := NewCertificate(authMethodId, cert) + require.NoError(t, err) + return c + } + tests := []struct { + name string + Certificate *Certificate + wantRowsDeleted int + overrides func(*Certificate) + wantErr bool + wantErrMsg string + }{ + { + name: "valid", + Certificate: testResource(testAuthMethod.PublicId, pem2), + wantErr: false, + wantRowsDeleted: 1, + }, + { + name: "bad-OidcMethodId", + Certificate: testResource(testAuthMethod.PublicId, pem3), + overrides: func(c *Certificate) { c.OidcMethodId = "bad-id" }, + wantErr: false, + wantRowsDeleted: 0, + }, + { + name: "bad-pem", + Certificate: testResource(testAuthMethod.PublicId, pem4), + overrides: func(c *Certificate) { c.Cert = "bad-pem" }, + wantErr: false, + wantRowsDeleted: 0, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + ctx := context.Background() + cp := tt.Certificate.Clone() + require.NoError(rw.Create(ctx, &cp)) + + if tt.overrides != nil { + tt.overrides(cp) + } + deletedRows, err := rw.Delete(ctx, &cp) + if tt.wantErr { + require.Error(err) + return + } + require.NoError(err) + if tt.wantRowsDeleted == 0 { + assert.Equal(tt.wantRowsDeleted, deletedRows) + return + } + assert.Equal(tt.wantRowsDeleted, deletedRows) + found := AllocCertificate() + err = rw.LookupWhere(ctx, &found, "oidc_method_id = ? and certificate = ?", tt.Certificate.OidcMethodId, tt.Certificate.Cert) + assert.True(errors.IsNotFoundError(err)) + }) + } +} + +func TestCertificate_Clone(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, wrapper) + _, pem := testGenerateCA(t, "localhost") + _, pem2 := testGenerateCA(t, "127.0.0.1") + + t.Run("valid", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + databaseWrapper, err := kmsCache.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase) + require.NoError(err) + m := TestAuthMethod(t, conn, databaseWrapper, org.PublicId, InactiveState, "alice_rp", "my-dogs-name", + WithIssuer(TestConvertToUrls(t, "https://alice.com")[0]), WithApiUrl(TestConvertToUrls(t, "https://api.com")[0])) + orig, err := NewCertificate(m.PublicId, pem) + require.NoError(err) + cp := orig.Clone() + assert.True(proto.Equal(cp.Certificate, orig.Certificate)) + }) + t.Run("not-equal", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + databaseWrapper, err := kmsCache.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase) + require.NoError(err) + m := TestAuthMethod(t, conn, databaseWrapper, org.PublicId, InactiveState, "alice_rp", "my-dogs-name", + WithIssuer(TestConvertToUrls(t, "https://alice.com")[0]), WithApiUrl(TestConvertToUrls(t, "https://api.com")[0])) + orig, err := NewCertificate(m.PublicId, pem) + require.NoError(err) + orig2, err := NewCertificate(m.PublicId, pem2) + require.NoError(err) + + cp := orig.Clone() + assert.True(!proto.Equal(cp.Certificate, orig2.Certificate)) + }) +} + +func TestCertificate_SetTableName(t *testing.T) { + t.Parallel() + defaultTableName := defaultCertificateTableName + tests := []struct { + name string + setNameTo string + want string + }{ + { + name: "new-name", + setNameTo: "new-name", + want: "new-name", + }, + { + name: "reset to default", + setNameTo: "", + want: defaultTableName, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + def := AllocCertificate() + require.Equal(defaultTableName, def.TableName()) + m := AllocCertificate() + m.SetTableName(tt.setNameTo) + assert.Equal(tt.want, m.TableName()) + }) + } +} diff --git a/internal/auth/oidc/certificate_utils.go b/internal/auth/oidc/certificate_utils.go new file mode 100644 index 000000000..50087e96f --- /dev/null +++ b/internal/auth/oidc/certificate_utils.go @@ -0,0 +1,57 @@ +package oidc + +import ( + "bytes" + "crypto/x509" + "encoding/pem" + + "github.com/hashicorp/boundary/internal/errors" +) + +// EncodeCertificates will encode a number of x509 certificates to PEMs. +func EncodeCertificates(certs ...*x509.Certificate) ([]string, error) { + const op = "oidc.EncodeCertificates" + if len(certs) == 0 { + return nil, errors.New(errors.InvalidParameter, op, "no certs provided") + } + var pems []string + for _, cert := range certs { + if cert == nil { + return nil, errors.New(errors.InvalidParameter, op, "nil cert") + } + var buffer bytes.Buffer + err := pem.Encode(&buffer, &pem.Block{ + Type: "CERTIFICATE", + Bytes: cert.Raw, + }) + if err != nil { + return nil, errors.New(errors.InvalidParameter, op, "failed to encode cert: "+err.Error(), errors.WithWrap(err)) + } + pems = append(pems, buffer.String()) + } + return pems, nil +} + +// ParseCertificates will parse a number of certificates PEMs to x509s. +func ParseCertificates(pems ...string) ([]*x509.Certificate, error) { + const op = "oidc.ParseCertificates" + if len(pems) == 0 { + return nil, errors.New(errors.InvalidParameter, op, "no PEMs provided") + } + var certs []*x509.Certificate + for _, p := range pems { + if p == "" { + return nil, errors.New(errors.InvalidParameter, op, "empty certificate PEM") + } + block, _ := pem.Decode([]byte(p)) + if block == nil { + return nil, errors.New(errors.InvalidParameter, op, "failed to parse certificate PEM") + } + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return nil, errors.New(errors.InvalidParameter, op, "failed to parse certificate: "+err.Error(), errors.WithWrap(err)) + } + certs = append(certs, cert) + } + return certs, nil +} diff --git a/internal/auth/oidc/certificate_utils_test.go b/internal/auth/oidc/certificate_utils_test.go new file mode 100644 index 000000000..5889899ff --- /dev/null +++ b/internal/auth/oidc/certificate_utils_test.go @@ -0,0 +1,122 @@ +package oidc + +import ( + "crypto/x509" + "testing" + + "github.com/hashicorp/boundary/internal/errors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestEncodeCertificates(t *testing.T) { + tests := []struct { + name string + setup func() ([]*x509.Certificate, []string) + wantErr bool + wantIsErr errors.Code + }{ + { + name: "valid", + setup: func() ([]*x509.Certificate, []string) { + c1, p1 := testGenerateCA(t, "localhost") + c2, p2 := testGenerateCA(t, "alice.com") + + return []*x509.Certificate{c1, c2}, []string{p1, p2} + }, + wantErr: false, + }, + { + name: "empty-cert", + setup: func() ([]*x509.Certificate, []string) { + _, p1 := testGenerateCA(t, "localhost") + c2, p2 := testGenerateCA(t, "alice.com") + + return []*x509.Certificate{nil, c2}, []string{p1, p2} + }, + wantErr: true, + wantIsErr: errors.InvalidParameter, + }, + { + name: "nil-certs", + setup: func() ([]*x509.Certificate, []string) { + _, p1 := testGenerateCA(t, "localhost") + _, p2 := testGenerateCA(t, "alice.com") + + return nil, []string{p1, p2} + }, + wantErr: true, + wantIsErr: errors.InvalidParameter, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + certs, pems := tt.setup() + got, err := EncodeCertificates(certs...) + if tt.wantErr { + require.Error(err) + assert.True(errors.Match(errors.T(tt.wantIsErr), err)) + return + } + require.NoError(err) + assert.Equal(pems, got) + }) + } +} + +func TestParseCertificates(t *testing.T) { + tests := []struct { + name string + setup func() ([]*x509.Certificate, []string) + wantErr bool + wantIsErr errors.Code + }{ + { + name: "valid", + setup: func() ([]*x509.Certificate, []string) { + c1, p1 := testGenerateCA(t, "localhost") + c2, p2 := testGenerateCA(t, "alice.com") + + return []*x509.Certificate{c1, c2}, []string{p1, p2} + }, + wantErr: false, + }, + { + name: "empty-pem", + setup: func() ([]*x509.Certificate, []string) { + c1, _ := testGenerateCA(t, "localhost") + c2, p2 := testGenerateCA(t, "alice.com") + + return []*x509.Certificate{c1, c2}, []string{"", p2} + }, + wantErr: true, + wantIsErr: errors.InvalidParameter, + }, + { + name: "nil-pem", + setup: func() ([]*x509.Certificate, []string) { + c1, _ := testGenerateCA(t, "localhost") + c2, _ := testGenerateCA(t, "alice.com") + + return []*x509.Certificate{c1, c2}, nil + }, + wantErr: true, + wantIsErr: errors.InvalidParameter, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + certs, pems := tt.setup() + got, err := ParseCertificates(pems...) + if tt.wantErr { + require.Error(err) + assert.True(errors.Match(errors.T(tt.wantIsErr), err)) + return + } + require.NoError(err) + assert.Equal(certs, got) + }) + } +} diff --git a/internal/auth/oidc/client_secret.go b/internal/auth/oidc/client_secret.go new file mode 100644 index 000000000..0879b96ff --- /dev/null +++ b/internal/auth/oidc/client_secret.go @@ -0,0 +1,22 @@ +package oidc + +import ( + "encoding/json" +) + +// ClientSecret equals an AuthMethod's client secret. This type provides a +// wrapper so the secret isn't inadvertently leaked into a log or error. +type ClientSecret string + +// RedactedClientSecret is the redacted string or json for an oidc id_token. +const redactedClientSecret = "[REDACTED: OIDC client_secret]" + +// String will redact the client_secret. +func (s ClientSecret) String() string { + return redactedClientSecret +} + +// MarshalJSON will redact the client_secret. +func (s ClientSecret) MarshalJSON() ([]byte, error) { + return json.Marshal(redactedClientSecret) +} diff --git a/internal/auth/oidc/client_secret_test.go b/internal/auth/oidc/client_secret_test.go new file mode 100644 index 000000000..acb018a47 --- /dev/null +++ b/internal/auth/oidc/client_secret_test.go @@ -0,0 +1,31 @@ +package oidc + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestClientSecret_String(t *testing.T) { + t.Parallel() + t.Run("redacted", func(t *testing.T) { + assert := assert.New(t) + const want = redactedClientSecret + tk := ClientSecret("super secret token") + assert.Equalf(want, tk.String(), "AccessToken.String() = %v, want %v", tk.String(), want) + }) +} + +func TestClientSecret_MarshalJSON(t *testing.T) { + t.Parallel() + t.Run("redacted", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + want := fmt.Sprintf(`"%s"`, redactedClientSecret) + tk := ClientSecret("super secret token") + got, err := tk.MarshalJSON() + require.NoError(err) + assert.Equalf([]byte(want), got, "AccessToken.MarshalJSON() = %s, want %s", got, want) + }) +} diff --git a/internal/auth/oidc/ids.go b/internal/auth/oidc/ids.go new file mode 100644 index 000000000..05ef04464 --- /dev/null +++ b/internal/auth/oidc/ids.go @@ -0,0 +1,40 @@ +package oidc + +import ( + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/errors" +) + +const ( + // AuthMethodPrefix defines the prefix for AuthMethod public ids. + AuthMethodPrefix = "amoidc" + // AccountPrefix defines the prefix for Account public ids. + AccountPrefix = "acctoidc" +) + +func newAuthMethodId() (string, error) { + const op = "oidc.newAuthMethodId" + id, err := db.NewPublicId(AuthMethodPrefix) + if err != nil { + return "", errors.Wrap(err, op) + } + return id, nil +} + +func newAccountId(authMethodId, issuer, sub string) (string, error) { + const op = "oidc.newAccountId" + if authMethodId == "" { + return "", errors.New(errors.InvalidParameter, op, "missing auth method id") + } + if issuer == "" { + return "", errors.New(errors.InvalidParameter, op, "missing issuer") + } + if sub == "" { + return "", errors.New(errors.InvalidParameter, op, "missing subject") + } + id, err := db.NewPublicId(AccountPrefix, db.WithPrngValues([]string{authMethodId, issuer, sub})) + if err != nil { + return "", errors.Wrap(err, op) + } + return id, nil +} diff --git a/internal/auth/oidc/ids_test.go b/internal/auth/oidc/ids_test.go new file mode 100644 index 000000000..3e254c90e --- /dev/null +++ b/internal/auth/oidc/ids_test.go @@ -0,0 +1,23 @@ +package oidc + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_Ids(t *testing.T) { + t.Parallel() + t.Run(AuthMethodPrefix, func(t *testing.T) { + id, err := newAuthMethodId() + require.NoError(t, err) + assert.True(t, strings.HasPrefix(id, AuthMethodPrefix+"_")) + }) + t.Run(AccountPrefix, func(t *testing.T) { + id, err := newAccountId("public-id", "test-issuer", "test-subject") + require.NoError(t, err) + assert.True(t, strings.HasPrefix(id, AccountPrefix+"_")) + }) +} diff --git a/internal/auth/oidc/immutable_fields_test.go b/internal/auth/oidc/immutable_fields_test.go new file mode 100644 index 000000000..5799bf8e3 --- /dev/null +++ b/internal/auth/oidc/immutable_fields_test.go @@ -0,0 +1,429 @@ +package oidc + +import ( + "context" + "testing" + + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/db/timestamp" + "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/kms" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/timestamppb" +) + +func TestAuthMethod_ImmutableFields(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, wrapper) + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + rw := db.New(conn) + + databaseWrapper, err := kmsCache.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + + ts := timestamp.Timestamp{Timestamp: ×tamppb.Timestamp{Seconds: 0, Nanos: 0}} + + new := TestAuthMethod(t, conn, databaseWrapper, org.PublicId, InactiveState, "alice_rp", "my-dogs-name", + WithIssuer(TestConvertToUrls(t, "https://alice.com")[0]), WithApiUrl(TestConvertToUrls(t, "https://api.com")[0])) + + tests := []struct { + name string + update *AuthMethod + fieldMask []string + }{ + { + name: "public_id", + update: func() *AuthMethod { + cp := new.Clone() + cp.PublicId = "p_thisIsNotAValidId" + return cp + }(), + fieldMask: []string{"PublicId"}, + }, + { + name: "create time", + update: func() *AuthMethod { + cp := new.Clone() + cp.CreateTime = &ts + return cp + }(), + fieldMask: []string{"CreateTime"}, + }, + { + name: "scope_id", + update: func() *AuthMethod { + cp := new.Clone() + cp.ScopeId = "o_thisIsNotAValidId" + return cp + }(), + fieldMask: []string{"ScopeId"}, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + + orig := new.Clone() + orig.SetTableName(defaultAuthMethodTableName) + err := rw.LookupById(context.Background(), orig) + require.NoError(err) + + tt.update.SetTableName(defaultAuthMethodTableName) + rowsUpdated, err := rw.Update(context.Background(), tt.update, tt.fieldMask, nil, db.WithSkipVetForWrite(true)) + require.Error(err) + assert.Equal(0, rowsUpdated) + + after := new.Clone() + after.SetTableName(defaultAuthMethodTableName) + err = rw.LookupById(context.Background(), after) + require.NoError(err) + + assert.True(proto.Equal(orig, after)) + }) + } +} + +func TestAudClaim_ImmutableFields(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, wrapper) + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + rw := db.New(conn) + ctx := context.Background() + databaseWrapper, err := kmsCache.GetWrapper(ctx, org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + + ts := timestamp.Timestamp{Timestamp: ×tamppb.Timestamp{Seconds: 0, Nanos: 0}} + + am := TestAuthMethod(t, conn, databaseWrapper, org.PublicId, InactiveState, "alice_rp", "my-dogs-name", + WithApiUrl(TestConvertToUrls(t, "https://api.com")[0]), WithAudClaims("alice.com")) + + new := AllocAudClaim() + require.NoError(t, rw.LookupWhere(ctx, &new, "oidc_method_id = ? and aud_claim = ?", am.PublicId, "alice.com")) + + tests := []struct { + name string + update *AudClaim + fieldMask []string + }{ + { + name: "oidc_method_id", + update: func() *AudClaim { + cp := new.Clone() + cp.OidcMethodId = "p_thisIsNotAValidId" + return cp + }(), + fieldMask: []string{"PublicId"}, + }, + { + name: "create time", + update: func() *AudClaim { + cp := new.Clone() + cp.CreateTime = &ts + return cp + }(), + fieldMask: []string{"CreateTime"}, + }, + { + name: "aud", + update: func() *AudClaim { + cp := new.Clone() + cp.Aud = "o_thisIsNotAValidId" + return cp + }(), + fieldMask: []string{"Aud"}, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + + orig := new.Clone() + orig.SetTableName(defaultAuthMethodTableName) + require.NoError(rw.LookupWhere(ctx, &new, "oidc_method_id = ? and aud_claim = ?", orig.OidcMethodId, orig.Aud)) + + require.NoError(err) + + tt.update.SetTableName(defaultAuthMethodTableName) + rowsUpdated, err := rw.Update(context.Background(), tt.update, tt.fieldMask, nil, db.WithSkipVetForWrite(true)) + require.Error(err) + assert.Equal(0, rowsUpdated) + + after := new.Clone() + after.SetTableName(defaultAuthMethodTableName) + require.NoError(rw.LookupWhere(ctx, &new, "oidc_method_id = ? and aud_claim = ?", after.OidcMethodId, after.Aud)) + + assert.True(proto.Equal(orig, after)) + }) + } +} + +func TestCertificate_ImmutableFields(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, wrapper) + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + rw := db.New(conn) + ctx := context.Background() + databaseWrapper, err := kmsCache.GetWrapper(ctx, org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + + ts := timestamp.Timestamp{Timestamp: ×tamppb.Timestamp{Seconds: 0, Nanos: 0}} + + x509, pem := testGenerateCA(t, "www.alice.com") + + _, pem2 := testGenerateCA(t, "www.bob.com") + + am := TestAuthMethod(t, conn, databaseWrapper, org.PublicId, InactiveState, "alice_rp", "my-dogs-name", + WithApiUrl(TestConvertToUrls(t, "https://api.com")[0]), WithCertificates(x509)) + + new := AllocCertificate() + require.NoError(t, rw.LookupWhere(ctx, &new, "oidc_method_id = ? and certificate = ?", am.PublicId, pem)) + + tests := []struct { + name string + update *Certificate + fieldMask []string + }{ + { + name: "oidc_method_id", + update: func() *Certificate { + cp := new.Clone() + cp.OidcMethodId = "p_thisIsNotAValidId" + return cp + }(), + fieldMask: []string{"PublicId"}, + }, + { + name: "create time", + update: func() *Certificate { + cp := new.Clone() + cp.CreateTime = &ts + return cp + }(), + fieldMask: []string{"CreateTime"}, + }, + { + name: "cert", + update: func() *Certificate { + cp := new.Clone() + cp.Cert = pem2 + return cp + }(), + fieldMask: []string{"Cert"}, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + + orig := new.Clone() + orig.SetTableName(defaultAuthMethodTableName) + require.NoError(rw.LookupWhere(ctx, &new, "oidc_method_id = ? and certificate = ?", orig.OidcMethodId, orig.Cert)) + + require.NoError(err) + + tt.update.SetTableName(defaultAuthMethodTableName) + rowsUpdated, err := rw.Update(context.Background(), tt.update, tt.fieldMask, nil, db.WithSkipVetForWrite(true)) + require.Error(err) + assert.Equal(0, rowsUpdated) + + after := new.Clone() + after.SetTableName(defaultAuthMethodTableName) + require.NoError(rw.LookupWhere(ctx, &new, "oidc_method_id = ? and certificate = ?", after.OidcMethodId, after.Cert)) + + assert.True(proto.Equal(orig, after)) + }) + } +} + +func TestSigningAlg_ImmutableFields(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, wrapper) + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + rw := db.New(conn) + ctx := context.Background() + databaseWrapper, err := kmsCache.GetWrapper(ctx, org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + + ts := timestamp.Timestamp{Timestamp: ×tamppb.Timestamp{Seconds: 0, Nanos: 0}} + + am := TestAuthMethod(t, conn, databaseWrapper, org.PublicId, InactiveState, "alice_rp", "my-dogs-name", + WithApiUrl(TestConvertToUrls(t, "https://api.com")[0]), WithSigningAlgs(RS256)) + + new := AllocSigningAlg() + require.NoError(t, rw.LookupWhere(ctx, &new, "oidc_method_id = ? and signing_alg_name = ?", am.PublicId, RS256)) + + tests := []struct { + name string + update *SigningAlg + fieldMask []string + }{ + { + name: "oidc_method_id", + update: func() *SigningAlg { + cp := new.Clone() + cp.OidcMethodId = "p_thisIsNotAValidId" + return cp + }(), + fieldMask: []string{"PublicId"}, + }, + { + name: "create time", + update: func() *SigningAlg { + cp := new.Clone() + cp.CreateTime = &ts + return cp + }(), + fieldMask: []string{"CreateTime"}, + }, + { + name: "alg", + update: func() *SigningAlg { + cp := new.Clone() + cp.Alg = string(RS384) + return cp + }(), + fieldMask: []string{"Alg"}, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + + orig := new.Clone() + orig.SetTableName(defaultAuthMethodTableName) + require.NoError(rw.LookupWhere(ctx, &new, "oidc_method_id = ? and signing_alg_name = ?", orig.OidcMethodId, orig.Alg)) + + require.NoError(err) + + tt.update.SetTableName(defaultAuthMethodTableName) + rowsUpdated, err := rw.Update(context.Background(), tt.update, tt.fieldMask, nil, db.WithSkipVetForWrite(true)) + require.Error(err) + assert.Equal(0, rowsUpdated) + + after := new.Clone() + after.SetTableName(defaultAuthMethodTableName) + require.NoError(rw.LookupWhere(ctx, &new, "oidc_method_id = ? and signing_alg_name = ?", after.OidcMethodId, after.Alg)) + + assert.True(proto.Equal(orig, after)) + }) + } +} + +func TestAccount_ImmutableFields(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, wrapper) + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + rw := db.New(conn) + + databaseWrapper, err := kmsCache.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + + ts := timestamp.Timestamp{Timestamp: ×tamppb.Timestamp{Seconds: 0, Nanos: 0}} + + u := TestConvertToUrls(t, "https://alice.com")[0] + + am := TestAuthMethod(t, conn, databaseWrapper, org.PublicId, InactiveState, "alice_rp", "my-dogs-name", + WithIssuer(u), WithApiUrl(TestConvertToUrls(t, "https://api.com")[0])) + + new := TestAccount(t, conn, am, "alice", WithName("Alice"), WithDescription("alice's account"), WithFullName("Alice Smith"), WithEmail("alice@alice.com")) + + tests := []struct { + name string + update *Account + fieldMask []string + }{ + { + name: "public_id", + update: func() *Account { + cp := new.Clone() + cp.PublicId = "p_thisIsNotAValidId" + return cp + }(), + fieldMask: []string{"PublicId"}, + }, + { + name: "create time", + update: func() *Account { + cp := new.Clone() + cp.CreateTime = &ts + return cp + }(), + fieldMask: []string{"CreateTime"}, + }, + { + name: "update time", + update: func() *Account { + cp := new.Clone() + cp.UpdateTime = &ts + return cp + }(), + fieldMask: []string{"UpdateTime"}, + }, + { + name: "oidc-auth-method-id", + update: func() *Account { + cp := new.Clone() + cp.AuthMethodId = "aodic_thisIsNotAValidId" + return cp + }(), + fieldMask: []string{"AuthMethodId"}, + }, + { + name: "issuer", + update: func() *Account { + cp := new.Clone() + cp.Issuer = "bob.com" + return cp + }(), + fieldMask: []string{"Issuer"}, + }, + { + name: "subject", + update: func() *Account { + cp := new.Clone() + cp.Subject = "bob" + return cp + }(), + fieldMask: []string{"Subject"}, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + + orig := new.Clone() + orig.SetTableName(defaultAccountTableName) + err := rw.LookupById(context.Background(), orig) + require.NoError(err) + + tt.update.SetTableName(defaultAccountTableName) + rowsUpdated, err := rw.Update(context.Background(), tt.update, tt.fieldMask, nil, db.WithSkipVetForWrite(true)) + require.Error(err) + assert.Equal(0, rowsUpdated) + + after := new.Clone() + after.SetTableName(defaultAccountTableName) + err = rw.LookupById(context.Background(), after) + require.NoError(err) + + assert.True(proto.Equal(orig, after)) + }) + } +} diff --git a/internal/auth/oidc/options.go b/internal/auth/oidc/options.go new file mode 100644 index 000000000..fa52070cb --- /dev/null +++ b/internal/auth/oidc/options.go @@ -0,0 +1,204 @@ +package oidc + +import ( + "crypto/x509" + "net/url" +) + +// getOpts - iterate the inbound Options and return a struct. +func getOpts(opt ...Option) options { + opts := getDefaultOptions() + for _, o := range opt { + o(&opts) + } + return opts +} + +// Option - how Options are passed as arguments. +type Option func(*options) + +// options = how options are represented +type options struct { + withName string + withDescription string + withLimit int + withMaxAge int + withApiUrl *url.URL + withCertificates []*x509.Certificate + withAudClaims []string + withSigningAlgs []Alg + withEmail string + withFullName string + withOrderByCreateTime bool + ascending bool + withUnauthenticatedUser bool + withForce bool + withDryRun bool + withAuthMethod *AuthMethod + withPublicId string + withRoundtripPayload string + withKeyId string + withIssuer *url.URL + withOperationalState AuthMethodState +} + +func getDefaultOptions() options { + return options{ + withOperationalState: InactiveState, + } +} + +// WithDescription provides an optional description. +func WithDescription(desc string) Option { + return func(o *options) { + o.withDescription = desc + } +} + +// WithName provides an optional name. +func WithName(name string) Option { + return func(o *options) { + o.withName = name + } +} + +// WithLimit provides an option to provide a limit. Intentionally allowing +// negative integers. If WithLimit < 0, then unlimited results are returned. +// If WithLimit == 0, then default limits are used for results. +func WithLimit(l int) Option { + return func(o *options) { + o.withLimit = l + } +} + +// WithMaxAge provides an optional max age. Specifies the allowable elapsed +// time in seconds since the last time the End-User was actively authenticated +// by the OP. If the elapsed time is greater than this value, the OP MUST +// attempt to actively re-authenticate the End-User. A value -1 basically +// forces the IdP to re-authenticate the End-User. Zero is not a valid value. +// +// see: https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest +func WithMaxAge(max int) Option { + return func(o *options) { + o.withMaxAge = max + } +} + +// WithApiUrl provides optional api URL to use in the various +func WithApiUrl(urls *url.URL) Option { + return func(o *options) { + o.withApiUrl = urls + } +} + +// WithCertificates provides optional certificates. +func WithCertificates(certs ...*x509.Certificate) Option { + return func(o *options) { + o.withCertificates = certs + } +} + +// WithAudClaims provides optional audience claims +func WithAudClaims(aud ...string) Option { + return func(o *options) { + o.withAudClaims = aud + } +} + +// WithSigningAlgs provides optional signing algorithms +func WithSigningAlgs(alg ...Alg) Option { + return func(o *options) { + o.withSigningAlgs = alg + } +} + +// WithEmail provides an optional email address for the account. +func WithEmail(email string) Option { + return func(o *options) { + o.withEmail = email + } +} + +// WithFullName provides an optional full name for the account. +func WithFullName(n string) Option { + return func(o *options) { + o.withFullName = n + } +} + +// WithOrderByCreateTime provides an option to specify ordering by the +// CreateTime field. +func WithOrderByCreateTime(ascending bool) Option { + return func(o *options) { + o.withOrderByCreateTime = true + o.ascending = ascending + } +} + +// WithUnauthenticatedUser provides an option for filtering results for +// an unauthenticated users. +func WithUnauthenticatedUser(enabled bool) Option { + return func(o *options) { + o.withUnauthenticatedUser = enabled + } +} + +// WithForce provides an option to force the write operation, regardless of +// whether or not it's pre-verification succeeds. +func WithForce() Option { + return func(o *options) { + o.withForce = true + } +} + +// WithDryRun provides an option to do a "dry run" of a write operation, which +// will run verification steps and return any errors, but will not persist the +// data into the repository. +func WithDryRun() Option { + return func(o *options) { + o.withDryRun = true + } +} + +// WithAuthMethod provides an option for passing an AuthMethod to the operation +func WithAuthMethod(am *AuthMethod) Option { + return func(o *options) { + o.withAuthMethod = am + } +} + +// WithPublicId provides an option for passing a public id to the operation +func WithPublicId(publicId string) Option { + return func(o *options) { + o.withPublicId = publicId + } +} + +// WithRoundTripPayload provides an option for passing an payload to be +// roundtripped during an authentication process. +func WithRoundtripPayload(payload string) Option { + return func(o *options) { + o.withRoundtripPayload = payload + } +} + +// WithKeyId provides an option for specifying a key id. +func WithKeyId(id string) Option { + return func(o *options) { + o.withKeyId = id + } +} + +// WithIssuer provides an option for specifying an issuer. +func WithIssuer(iss *url.URL) Option { + return func(o *options) { + o.withIssuer = iss + } +} + +// WithOperationalState provides an option for specifying an issuer. +func WithOperationalState(state AuthMethodState) Option { + return func(o *options) { + o.withOperationalState = state + } +} diff --git a/internal/auth/oidc/options_test.go b/internal/auth/oidc/options_test.go new file mode 100644 index 000000000..f3c6452eb --- /dev/null +++ b/internal/auth/oidc/options_test.go @@ -0,0 +1,178 @@ +package oidc + +import ( + "crypto/x509" + "net/url" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// Test_GetOpts provides unit tests for GetOpts and all the options +func Test_GetOpts(t *testing.T) { + t.Parallel() + t.Run("WithName", func(t *testing.T) { + assert := assert.New(t) + opts := getOpts(WithName("test")) + testOpts := getDefaultOptions() + testOpts.withName = "test" + assert.Equal(opts, testOpts) + }) + t.Run("WithDescription", func(t *testing.T) { + assert := assert.New(t) + opts := getOpts(WithDescription("test desc")) + testOpts := getDefaultOptions() + testOpts.withDescription = "test desc" + assert.Equal(opts, testOpts) + }) + t.Run("WithLimit", func(t *testing.T) { + assert := assert.New(t) + // test default of 0 + opts := getOpts() + testOpts := getDefaultOptions() + testOpts.withLimit = 0 + assert.Equal(opts, testOpts) + + opts = getOpts(WithLimit(-1)) + testOpts = getDefaultOptions() + testOpts.withLimit = -1 + assert.Equal(opts, testOpts) + + opts = getOpts(WithLimit(1)) + testOpts = getDefaultOptions() + testOpts.withLimit = 1 + assert.Equal(opts, testOpts) + }) + t.Run("WithMaxAge", func(t *testing.T) { + assert := assert.New(t) + opts := getOpts(WithMaxAge(1000)) + testOpts := getDefaultOptions() + testOpts.withMaxAge = 1000 + assert.Equal(opts, testOpts) + }) + t.Run("WithApiUrl", func(t *testing.T) { + assert := assert.New(t) + u := TestConvertToUrls(t, "https://alice.com")[0] + opts := getOpts(WithApiUrl(u)) + testOpts := getDefaultOptions() + testOpts.withApiUrl = u + assert.Equal(opts, testOpts) + }) + t.Run("WithCertificates", func(t *testing.T) { + assert := assert.New(t) + testCert, _ := testGenerateCA(t, "localhost") + testCert2, _ := testGenerateCA(t, "127.0.0.1") + + opts := getOpts(WithCertificates(testCert, testCert2)) + testOpts := getDefaultOptions() + testOpts.withCertificates = []*x509.Certificate{testCert, testCert2} + assert.Equal(opts, testOpts) + }) + t.Run("WithAudClaims", func(t *testing.T) { + assert := assert.New(t) + aud := []string{"alice.com", "eve.com", "bob.com"} + opts := getOpts(WithAudClaims(aud...)) + testOpts := getDefaultOptions() + testOpts.withAudClaims = aud + assert.Equal(opts, testOpts) + }) + t.Run("WithSigningAlgs", func(t *testing.T) { + assert := assert.New(t) + algs := []Alg{RS256, RS384, RS512} + opts := getOpts(WithSigningAlgs(algs...)) + testOpts := getDefaultOptions() + testOpts.withSigningAlgs = algs + assert.Equal(opts, testOpts) + }) + t.Run("WithEmail", func(t *testing.T) { + assert := assert.New(t) + opts := getOpts(WithEmail("bob@alice.com")) + testOpts := getDefaultOptions() + testOpts.withEmail = "bob@alice.com" + assert.Equal(opts, testOpts) + }) + t.Run("WithFullName", func(t *testing.T) { + assert := assert.New(t) + opts := getOpts(WithFullName("Bob Eve Alice")) + testOpts := getDefaultOptions() + testOpts.withFullName = "Bob Eve Alice" + assert.Equal(opts, testOpts) + }) + t.Run("WithOrderByCreateTime", func(t *testing.T) { + assert := assert.New(t) + opts := getOpts(WithOrderByCreateTime(true)) + testOpts := getDefaultOptions() + testOpts.withOrderByCreateTime = true + testOpts.ascending = true + assert.Equal(opts, testOpts) + }) + t.Run("WithUnauthenticatedUser", func(t *testing.T) { + assert := assert.New(t) + opts := getOpts(WithUnauthenticatedUser(true)) + testOpts := getDefaultOptions() + testOpts.withUnauthenticatedUser = true + assert.Equal(opts, testOpts) + }) + t.Run("WithForce", func(t *testing.T) { + assert := assert.New(t) + opts := getOpts(WithForce()) + testOpts := getDefaultOptions() + testOpts.withForce = true + assert.Equal(opts, testOpts) + }) + t.Run("WithDryRun", func(t *testing.T) { + assert := assert.New(t) + opts := getOpts(WithDryRun()) + testOpts := getDefaultOptions() + testOpts.withDryRun = true + assert.Equal(opts, testOpts) + }) + t.Run("WithAuthMethod", func(t *testing.T) { + assert := assert.New(t) + am := AllocAuthMethod() + am.PublicId = "alice's-auth-method" + opts := getOpts(WithAuthMethod(&am)) + testOpts := getDefaultOptions() + testOpts.withAuthMethod = &am + assert.Equal(opts, testOpts) + }) + t.Run("WithPublicId", func(t *testing.T) { + assert := assert.New(t) + id := "alice's-auth-method" + opts := getOpts(WithPublicId(id)) + testOpts := getDefaultOptions() + testOpts.withPublicId = id + assert.Equal(opts, testOpts) + }) + t.Run("WithRoundtripPayload", func(t *testing.T) { + assert := assert.New(t) + opts := getOpts(WithRoundtripPayload("payload")) + testOpts := getDefaultOptions() + testOpts.withRoundtripPayload = "payload" + assert.Equal(opts, testOpts) + }) + t.Run("WithKeyId", func(t *testing.T) { + assert := assert.New(t) + opts := getOpts(WithKeyId("specific_id")) + testOpts := getDefaultOptions() + testOpts.withKeyId = "specific_id" + assert.Equal(opts, testOpts) + }) + t.Run("WithIssuer", func(t *testing.T) { + assert := assert.New(t) + u, err := url.Parse("http://test.com") + require.NoError(t, err) + opts := getOpts(WithIssuer(u)) + testOpts := getDefaultOptions() + testOpts.withIssuer = u + assert.Equal(opts, testOpts) + }) + t.Run("WithOperationalState", func(t *testing.T) { + assert := assert.New(t) + testOpts := getDefaultOptions() + assert.Equal(InactiveState, testOpts.withOperationalState) + opts := getOpts(WithOperationalState(ActivePublicState)) + assert.Equal(ActivePublicState, opts.withOperationalState) + }) +} diff --git a/internal/auth/oidc/provider.go b/internal/auth/oidc/provider.go new file mode 100644 index 000000000..629060ef9 --- /dev/null +++ b/internal/auth/oidc/provider.go @@ -0,0 +1,129 @@ +package oidc + +import ( + "context" + "fmt" + "strings" + "sync" + + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/cap/oidc" +) + +var ( + // cachedProviders provides a cache of oidc.Providers. This cache can't be + // done within the Repository, since a new Repository is created for every + // request. + cachedProviders *providers + initCachedProviders sync.Once +) + +// providerCache returns the cache of providers +func providerCache() *providers { + initCachedProviders.Do(func() { + cachedProviders = newProviderCache() + }) + return cachedProviders +} + +// providers is a cache of oidc.Provider types used by the Repository to +// make requests to the IdP, verify ID tokens, etc. For more info on +// oidc.Provider capabilities see: https://github.com/hashicorp/cap +type providers struct { + cache map[string]*oidc.Provider + mu *sync.RWMutex +} + +// newProviderCache make a new cache +func newProviderCache() *providers { + return &providers{ + cache: map[string]*oidc.Provider{}, + mu: &sync.RWMutex{}, + } +} + +// get determines if there's already a cached oidc.Provider for the +// current AuthMethod from the DB. The cached oidc.Provider is preferred since +// it maintains a cache of the JWKs required to verify ID Tokens from the IdP. +// +// Before returning a cached oidc.Provider, get ensures that the +// AuthMethod data used for the oidc.Provider's configuration hasn't changed +// since it was cached. This is necessary because another controller could +// update the AuthMethod in the DB, which would require changing the +// configuration of the cached provider. +// +// get will update the providerCache with the new AuthMethod, if it +// determines the provider's configuration has been updated in the DB. +func (c *providers) get(ctx context.Context, currentFromDb *AuthMethod) (*oidc.Provider, error) { + const op = "oidc.(providers).getProvider" + storedProvider, err := convertToProvider(ctx, currentFromDb) + if err != nil { + return nil, errors.Wrap(err, op) + } + c.mu.RLock() + p, ok := c.cache[currentFromDb.PublicId] + c.mu.RUnlock() + if ok { + cachedHash, err := p.ConfigHash() + if err != nil { + return nil, errors.Wrap(err, op, errors.WithMsg("unable to hash provider from cache")) + } + storedHash, err := storedProvider.ConfigHash() + if err != nil { + return nil, errors.Wrap(err, op, errors.WithMsg("unable to hash provider from database")) + } + switch cachedHash == storedHash { + case true: + return p, nil + default: + c.delete(ctx, currentFromDb.PublicId) + } + } + c.set(ctx, currentFromDb.PublicId, storedProvider) + return storedProvider, nil +} + +// set will set an entry in the cache. +func (c *providers) set(ctx context.Context, authMethodId string, p *oidc.Provider) { + c.mu.Lock() + defer c.mu.Unlock() + c.cache[authMethodId] = p +} + +// delete will delete an entry in the cache. +func (c *providers) delete(ctx context.Context, authMethodId string) { + c.mu.Lock() + defer c.mu.Unlock() + delete(c.cache, authMethodId) +} + +func convertToProvider(ctx context.Context, am *AuthMethod) (*oidc.Provider, error) { + const op = "oidc.convertToProvider" + if am == nil { + return nil, errors.New(errors.InvalidParameter, op, "missing auth method") + } + if err := am.isComplete(); err != nil { + return nil, errors.Wrap(err, op) + } + algs := make([]oidc.Alg, 0, len(am.SigningAlgs)) + for _, a := range am.SigningAlgs { + algs = append(algs, oidc.Alg(a)) + } + c, err := oidc.NewConfig( + am.Issuer, + am.ClientId, + oidc.ClientSecret(am.ClientSecret), + algs, + []string{fmt.Sprintf(CallbackEndpoint, am.GetApiUrl(), am.PublicId)}, + oidc.WithAudiences(am.AudClaims...), + oidc.WithProviderCA(strings.Join(am.Certificates, "\n")), + ) + if err != nil { + return nil, errors.New(errors.InvalidParameter, op, "AuthMethod cannot be converted to a valid OIDC Provider Configuration", errors.WithWrap(err)) + } + p, err := oidc.NewProvider(c) + if err != nil { + return nil, errors.New(errors.InvalidParameter, op, "AuthMethod cannot be converted to a valid OIDC Provider", errors.WithWrap(err)) + } + return p, nil +} diff --git a/internal/auth/oidc/provider_test.go b/internal/auth/oidc/provider_test.go new file mode 100644 index 000000000..0a8432295 --- /dev/null +++ b/internal/auth/oidc/provider_test.go @@ -0,0 +1,140 @@ +package oidc + +import ( + "context" + "fmt" + "net/url" + "testing" + + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/cap/oidc" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_ProviderCaching(t *testing.T) { + t.Parallel() + ctx := context.Background() + tp := oidc.StartTestProvider(t) + issuer, err := url.Parse(tp.Addr()) + require.NoError(t, err) + + _, _, signingAlg, _ := tp.SigningKeys() + allowedRedirect := "https://alice.com/callback" + authMethodId, err := newAuthMethodId() + require.NoError(t, err) + id := authMethodId + secret := authMethodId + p1 := testProvider(t, id, secret, fmt.Sprintf(CallbackEndpoint, allowedRedirect, authMethodId), tp) // provider needs the complete callback URL + + testAm, err := NewAuthMethod("fake-org", id, ClientSecret(secret), + WithIssuer(issuer), WithApiUrl(TestConvertToUrls(t, allowedRedirect)[0])) + require.NoError(t, err) + + testAm.PublicId = authMethodId + testAm.SigningAlgs = []string{string(signingAlg)} + testAm.ApiUrl = allowedRedirect + testAm.Certificates = []string{tp.CACert()} + + t.Run("get-equal-providers", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + cache := newProviderCache() + assert.Equal(0, len(cache.cache)) + cache.set(ctx, authMethodId, p1) + got, err := cache.get(ctx, testAm) + require.NoError(err) + assert.Equal(p1, got) + }) + t.Run("get-unequal-providers", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + cache := newProviderCache() + assert.Equal(0, len(cache.cache)) + cache.set(ctx, authMethodId, p1) + newAm := testAm.Clone() + newAm.ClientId = "new-client-id" + got, err := cache.get(ctx, newAm) + require.NoError(err) + assert.NotEqual(p1, got) + }) + t.Run("refetch-from-singleton", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + // use the singleton + cache := providerCache() + cache.set(ctx, authMethodId, p1) + + // use the singleton via a new var + cache2 := providerCache() + got, err := cache2.get(ctx, testAm) + require.NoError(err) + assert.Equal(p1, got) + }) + t.Run("delete", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + // use the singleton + cache := newProviderCache() + cache.set(ctx, authMethodId, p1) + require.Equal(1, len(cache.cache)) + + cache.delete(ctx, authMethodId) + assert.Equal(0, len(cache.cache)) + }) +} + +func Test_convertToProvider(t *testing.T) { + t.Parallel() + ctx := context.Background() + tp := oidc.StartTestProvider(t) + issuer, err := url.Parse(tp.Addr()) + require.NoError(t, err) + + _, _, signingAlg, _ := tp.SigningKeys() + allowedRedirect := "https://alice.com/callback" + + authMethodId, err := newAuthMethodId() + require.NoError(t, err) + id := authMethodId + secret := authMethodId + p := testProvider(t, id, secret, fmt.Sprintf(CallbackEndpoint, allowedRedirect, authMethodId), tp) // provider callback needs the complete URL + testAm, err := NewAuthMethod("fake-org", id, ClientSecret(secret), + WithIssuer(issuer), WithApiUrl(TestConvertToUrls(t, allowedRedirect)[0])) + require.NoError(t, err) + + testAm.PublicId = authMethodId + testAm.SigningAlgs = []string{string(signingAlg)} + testAm.ApiUrl = allowedRedirect + testAm.Certificates = []string{tp.CACert()} + + type args struct{} + tests := []struct { + name string + am *AuthMethod + want *oidc.Provider + wantErr bool + wantErrCode errors.Code + }{ + {"equal", testAm, p, false, errors.Unknown}, + {"missing-issuer", func() *AuthMethod { cp := testAm.Clone(); cp.Issuer = ""; return cp }(), nil, true, errors.InvalidParameter}, + {"missing-client-id", func() *AuthMethod { cp := testAm.Clone(); cp.ClientId = ""; return cp }(), nil, true, errors.InvalidParameter}, + {"missing-client-secret", func() *AuthMethod { cp := testAm.Clone(); cp.ClientSecret = ""; return cp }(), nil, true, errors.InvalidParameter}, + {"missing-algs", func() *AuthMethod { cp := testAm.Clone(); cp.SigningAlgs = nil; return cp }(), nil, true, errors.InvalidParameter}, + {"missing-api-url", func() *AuthMethod { cp := testAm.Clone(); cp.ApiUrl = ""; return cp }(), nil, true, errors.InvalidParameter}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + + got, err := convertToProvider(ctx, tt.am) + if tt.wantErr { + require.Error(err) + assert.Truef(errors.Match(errors.T(tt.wantErrCode), err), "%v", err) + return + } + require.NoError(err) + wantHash, err := tt.want.ConfigHash() + require.NoError(err) + gotHash, err := got.ConfigHash() + require.NoError(err) + assert.Equal(wantHash, gotHash) + }) + } +} diff --git a/internal/auth/oidc/query.go b/internal/auth/oidc/query.go new file mode 100644 index 000000000..725b6e583 --- /dev/null +++ b/internal/auth/oidc/query.go @@ -0,0 +1,15 @@ +package oidc + +const ( + acctUpsertQuery = ` + insert into auth_oidc_account + (%s) + values + (%s) + on conflict on constraint + auth_oidc_account_auth_method_id_issuer_subject_uq + do update set + %s + returning public_id, version + ` +) diff --git a/internal/auth/oidc/repository.go b/internal/auth/oidc/repository.go new file mode 100644 index 000000000..9dbd9d263 --- /dev/null +++ b/internal/auth/oidc/repository.go @@ -0,0 +1,43 @@ +package oidc + +import ( + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/kms" +) + +// Repository is the oidc repository +type Repository struct { + reader db.Reader + writer db.Writer + kms *kms.Kms + + // defaultLimit provides a default for limiting the number of results returned from the repo + defaultLimit int +} + +// NewRepository creates a new oidc Repository. Supports the options: WithLimit +// which sets a default limit on results returned by repo operations. +func NewRepository(r db.Reader, w db.Writer, kms *kms.Kms, opt ...Option) (*Repository, error) { + const op = "oidc.NewRepository" + if r == nil { + return nil, errors.New(errors.InvalidParameter, op, "reader is nil") + } + if w == nil { + return nil, errors.New(errors.InvalidParameter, op, "writer is nil") + } + if kms == nil { + return nil, errors.New(errors.InvalidParameter, op, "kms is nil") + } + opts := getOpts(opt...) + if opts.withLimit == 0 { + // zero signals the boundary defaults should be used. + opts.withLimit = db.DefaultLimit + } + return &Repository{ + reader: r, + writer: w, + kms: kms, + defaultLimit: opts.withLimit, + }, nil +} diff --git a/internal/auth/oidc/repository_account.go b/internal/auth/oidc/repository_account.go new file mode 100644 index 000000000..e8732a078 --- /dev/null +++ b/internal/auth/oidc/repository_account.go @@ -0,0 +1,271 @@ +package oidc + +import ( + "context" + "fmt" + "strings" + + "github.com/hashicorp/boundary/internal/db" + dbcommon "github.com/hashicorp/boundary/internal/db/common" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/oplog" +) + +// CreateAccount inserts an Account, a, into the repository and returns a +// new Account containing its PublicId. a is not changed. a must contain a +// valid AuthMethodId. a must not contain a PublicId. The PublicId is +// generated and assigned by this method. a must not contain an Issuer. +// The Issuer is retrieved from the auth method. If it does not contain an +// Issuer an error is returned. +// +// a must contain a valid Subject. a.Subject must be unique for an +// a.AuthMethod/Issuer pair. +// +// Both a.Name and a.Description are optional. If a.Name is set, it must be +// unique within a.AuthMethodId. +func (r *Repository) CreateAccount(ctx context.Context, scopeId string, a *Account, opt ...Option) (*Account, error) { + const op = "oidc.(Repository).CreateAccount" + if a == nil { + return nil, errors.New(errors.InvalidParameter, op, "missing Account") + } + if a.Account == nil { + return nil, errors.New(errors.InvalidParameter, op, "missing embedded Account") + } + if a.AuthMethodId == "" { + return nil, errors.New(errors.InvalidParameter, op, "missing auth method id") + } + if a.Subject == "" { + return nil, errors.New(errors.InvalidParameter, op, "missing subject") + } + if a.PublicId != "" { + return nil, errors.New(errors.InvalidParameter, op, "public id must be empty") + } + if scopeId == "" { + return nil, errors.New(errors.InvalidParameter, op, "missing scope id") + } + + a = a.Clone() + + // If the account doesn't provide an issuer, default to the one provided by + // the auth method. While this potentially creates a race condition between + // modifying the auth method's issuer and setting the it on the account + // the value set on the account is reported back to the requester by the api + // and setting an issuer on an account that doesn't match the auth method is + // perfectly valid and allows an operator to provision accounts prior to + // configuring the auth method to specify issuer. + if a.Issuer == "" { + am, err := r.LookupAuthMethod(ctx, a.AuthMethodId) + if err != nil { + return nil, errors.Wrap(err, op, errors.WithMsg("unable to get auth method")) + } + if am.GetIssuer() == "" { + return nil, errors.New(errors.InvalidParameter, op, "no issuer on auth method") + } + a.Issuer = am.GetIssuer() + } + if a.Issuer == "" { + return nil, errors.New(errors.InvalidParameter, op, "no issuer provided or defined in auth method") + } + id, err := newAccountId(a.AuthMethodId, a.Issuer, a.Subject) + if err != nil { + return nil, errors.Wrap(err, op) + } + a.PublicId = id + + oplogWrapper, err := r.kms.GetWrapper(ctx, scopeId, kms.KeyPurposeOplog) + if err != nil { + return nil, errors.Wrap(err, op, errors.WithMsg("unable to get oplog wrapper"), errors.WithCode(errors.Encrypt)) + } + + var newAccount *Account + _, err = r.writer.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, + func(_ db.Reader, w db.Writer) error { + newAccount = a.Clone() + if err := w.Create(ctx, newAccount, db.WithOplog(oplogWrapper, a.oplog(oplog.OpType_OP_TYPE_CREATE, scopeId))); err != nil { + return errors.Wrap(err, op) + } + return nil + }, + ) + + if err != nil { + if errors.IsUniqueError(err) { + return nil, errors.New(errors.NotUnique, op, fmt.Sprintf( + "in auth method %s: name %q already exists or subject %q already exists for issuer %q in scope %s", + a.AuthMethodId, a.Name, a.Subject, a.Issuer, scopeId)) + } + return nil, errors.Wrap(err, op, errors.WithMsg(a.AuthMethodId)) + } + return newAccount, nil +} + +// LookupAccount will look up an account in the repository. If the account is not +// found, it will return nil, nil. All options are ignored. +func (r *Repository) LookupAccount(ctx context.Context, withPublicId string, opt ...Option) (*Account, error) { + const op = "oidc.(Repository).LookupAccount" + if withPublicId == "" { + return nil, errors.New(errors.InvalidPublicId, op, "missing public id") + } + a := AllocAccount() + a.PublicId = withPublicId + if err := r.reader.LookupByPublicId(ctx, a); err != nil { + if errors.IsNotFoundError(err) { + return nil, nil + } + return nil, errors.Wrap(err, op, errors.WithMsg(fmt.Sprintf("failed for %s", withPublicId))) + } + return a, nil +} + +// ListAccounts in an auth method and supports WithLimit option. +func (r *Repository) ListAccounts(ctx context.Context, withAuthMethodId string, opt ...Option) ([]*Account, error) { + const op = "oidc.(Repository).ListAccounts" + if withAuthMethodId == "" { + return nil, errors.New(errors.InvalidParameter, op, "missing auth method id") + } + opts := getOpts(opt...) + limit := r.defaultLimit + if opts.withLimit != 0 { + // non-zero signals an override of the default limit for the repo. + limit = opts.withLimit + } + var accts []*Account + err := r.reader.SearchWhere(ctx, &accts, "auth_method_id = ?", []interface{}{withAuthMethodId}, db.WithLimit(limit)) + if err != nil { + return nil, errors.Wrap(err, op) + } + return accts, nil +} + +// DeleteAccount deletes the account for the provided id from the repository returning a count of the +// number of records deleted. All options are ignored. +func (r *Repository) DeleteAccount(ctx context.Context, scopeId, withPublicId string, opt ...Option) (int, error) { + const op = "oidc.(Repository).DeleteAccount" + if withPublicId == "" { + return db.NoRowsAffected, errors.New(errors.InvalidPublicId, op, "missing public id") + } + if scopeId == "" { + return db.NoRowsAffected, errors.New(errors.InvalidParameter, op, "missing scope id") + } + ac := AllocAccount() + ac.PublicId = withPublicId + + oplogWrapper, err := r.kms.GetWrapper(ctx, scopeId, kms.KeyPurposeOplog) + if err != nil { + return db.NoRowsAffected, errors.Wrap(err, op, errors.WithCode(errors.Encrypt), errors.WithMsg("unable to get oplog wrapper")) + } + + var rowsDeleted int + _, err = r.writer.DoTx( + ctx, + db.StdRetryCnt, + db.ExpBackoff{}, + func(_ db.Reader, w db.Writer) (err error) { + metadata := ac.oplog(oplog.OpType_OP_TYPE_DELETE, scopeId) + dAc := ac.Clone() + rowsDeleted, err = w.Delete(ctx, dAc, db.WithOplog(oplogWrapper, metadata)) + if err != nil { + return errors.Wrap(err, op) + } + if rowsDeleted > 1 { + return errors.New(errors.MultipleRecords, op, "more than 1 resource would have been deleted") + } + return nil + }, + ) + + if err != nil { + return db.NoRowsAffected, errors.Wrap(err, op, errors.WithMsg(withPublicId)) + } + + return rowsDeleted, nil +} + +// UpdateAccount updates the repository entry for a.PublicId with the +// values in a for the fields listed in fieldMaskPaths. It returns a new +// Account containing the updated values and a count of the number of +// records updated. a is not changed. +// +// a must contain a valid PublicId. Only a.Name and a.Description can be +// updated. If a.Name is set to a non-empty string, it must be unique within +// a.AuthMethodId. +// +// An attribute of a will be set to NULL in the database if the attribute +// in a is the zero value and it is included in fieldMaskPaths. +func (r *Repository) UpdateAccount(ctx context.Context, scopeId string, a *Account, version uint32, fieldMaskPaths []string, opt ...Option) (*Account, int, error) { + const op = "oidc.(Repository).UpdateAccount" + if a == nil { + return nil, db.NoRowsAffected, errors.New(errors.InvalidParameter, op, "missing Account") + } + if a.Account == nil { + return nil, db.NoRowsAffected, errors.New(errors.InvalidParameter, op, "missing embedded Account") + } + if a.PublicId == "" { + return nil, db.NoRowsAffected, errors.New(errors.InvalidPublicId, op, "missing public id") + } + if version == 0 { + return nil, db.NoRowsAffected, errors.New(errors.InvalidParameter, op, "missing version") + } + if scopeId == "" { + return nil, db.NoRowsAffected, errors.New(errors.InvalidParameter, op, "missing scope id") + } + + for _, f := range fieldMaskPaths { + switch { + case strings.EqualFold("Name", f): + case strings.EqualFold("Description", f): + default: + return nil, db.NoRowsAffected, errors.New(errors.InvalidFieldMask, op, f) + } + } + var dbMask, nullFields []string + dbMask, nullFields = dbcommon.BuildUpdatePaths( + map[string]interface{}{ + "Name": a.Name, + "Description": a.Description, + }, + fieldMaskPaths, + nil, + ) + if len(dbMask) == 0 && len(nullFields) == 0 { + return nil, db.NoRowsAffected, errors.New(errors.EmptyFieldMask, op, "missing field mask") + } + + oplogWrapper, err := r.kms.GetWrapper(ctx, scopeId, kms.KeyPurposeOplog) + if err != nil { + return nil, db.NoRowsAffected, errors.Wrap(err, op, errors.WithCode(errors.Encrypt), + errors.WithMsg(("unable to get oplog wrapper"))) + } + + a = a.Clone() + + metadata := a.oplog(oplog.OpType_OP_TYPE_UPDATE, scopeId) + + var rowsUpdated int + var returnedAccount *Account + _, err = r.writer.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, + func(_ db.Reader, w db.Writer) error { + returnedAccount = a.Clone() + var err error + rowsUpdated, err = w.Update(ctx, returnedAccount, dbMask, nullFields, db.WithOplog(oplogWrapper, metadata), db.WithVersion(&version)) + if err != nil { + return errors.Wrap(err, op) + } + if rowsUpdated > 1 { + return errors.New(errors.MultipleRecords, op, "more than 1 resource would have been updated") + } + return nil + }, + ) + + if err != nil { + if errors.IsUniqueError(err) { + return nil, db.NoRowsAffected, errors.New(errors.NotUnique, op, + fmt.Sprintf("name %s already exists: %s", a.Name, a.PublicId)) + } + return nil, db.NoRowsAffected, errors.Wrap(err, op, errors.WithMsg(a.PublicId)) + } + + return returnedAccount, rowsUpdated, nil +} diff --git a/internal/auth/oidc/repository_account_test.go b/internal/auth/oidc/repository_account_test.go new file mode 100644 index 000000000..51f763d8d --- /dev/null +++ b/internal/auth/oidc/repository_account_test.go @@ -0,0 +1,1316 @@ +package oidc + +import ( + "context" + "fmt" + "sort" + "strings" + "testing" + "time" + + "github.com/hashicorp/boundary/internal/auth/oidc/store" + "github.com/hashicorp/boundary/internal/db" + dbassert "github.com/hashicorp/boundary/internal/db/assert" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/oplog" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRepository_CreateAccount(t *testing.T) { + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + + kmsCache := kms.TestKms(t, conn, wrapper) + iamRepo := iam.TestRepo(t, conn, wrapper) + org, _ := iam.TestScopes(t, iamRepo) + + ctx := context.Background() + databaseWrapper, err := kmsCache.GetWrapper(ctx, org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + + authMethod := TestAuthMethod( + t, conn, databaseWrapper, org.GetPublicId(), ActivePrivateState, + "alice-rp", "fido", + WithSigningAlgs(RS256), + WithIssuer(TestConvertToUrls(t, "https://www.alice.com")[0]), + WithApiUrl(TestConvertToUrls(t, "https://www.alice.com/callback")[0]), + ) + + noIssuerAm := TestAuthMethod( + t, conn, databaseWrapper, org.GetPublicId(), InactiveState, + "client", "fido", + WithSigningAlgs(RS256), + WithApiUrl(TestConvertToUrls(t, "https://www.alice.com/callback")[0]), + ) + + tests := []struct { + name string + in *Account + opts []Option + want *Account + wantIsErr errors.Code + wantErrMsg string + }{ + { + name: "nil-Account", + wantIsErr: errors.InvalidParameter, + wantErrMsg: "oidc.(Repository).CreateAccount: missing Account: parameter violation: error #100", + }, + { + name: "nil-embedded-Account", + in: &Account{}, + wantIsErr: errors.InvalidParameter, + wantErrMsg: "oidc.(Repository).CreateAccount: missing embedded Account: parameter violation: error #100", + }, + { + name: "invalid-no-scope-id", + in: &Account{ + Account: &store.Account{}, + }, + wantIsErr: errors.InvalidParameter, + wantErrMsg: "oidc.(Repository).CreateAccount: missing auth method id: parameter violation: error #100", + }, + { + name: "invalid-public-id-set", + in: &Account{ + Account: &store.Account{ + AuthMethodId: authMethod.PublicId, + PublicId: "hcst_OOOOOOOOOO", + Subject: "invalid public id set", + }, + }, + wantIsErr: errors.InvalidParameter, + wantErrMsg: "oidc.(Repository).CreateAccount: public id must be empty: parameter violation: error #100", + }, + { + name: "invalid-no-subject", + in: &Account{ + Account: &store.Account{ + AuthMethodId: authMethod.PublicId, + }, + }, + wantIsErr: errors.InvalidParameter, + wantErrMsg: "oidc.(Repository).CreateAccount: missing subject: parameter violation: error #100", + }, + { + name: "invalid-no-issuer-authmethod-no-issuer", + in: &Account{ + Account: &store.Account{ + AuthMethodId: noIssuerAm.PublicId, + Subject: "invalid no issuer authmethod no issuer", + }, + }, + wantIsErr: errors.InvalidParameter, + wantErrMsg: "oidc.(Repository).CreateAccount: no issuer on auth method: parameter violation: error #100", + }, + { + name: "valid-provide-issuer-authmethod-no-issuer", + in: &Account{ + Account: &store.Account{ + AuthMethodId: noIssuerAm.PublicId, + Subject: "valid provide issuer authmethod no issuer", + Issuer: "https://overwrite.com", + }, + }, + want: &Account{ + Account: &store.Account{ + AuthMethodId: authMethod.PublicId, + Issuer: "https://overwrite.com", + Subject: "valid provide issuer authmethod no issuer", + }, + }, + }, + { + name: "valid-no-options", + in: &Account{ + Account: &store.Account{ + AuthMethodId: authMethod.PublicId, + Subject: "valid-no-options", + }, + }, + want: &Account{ + Account: &store.Account{ + AuthMethodId: authMethod.PublicId, + Issuer: "https://www.alice.com", + Subject: "valid-no-options", + }, + }, + }, + { + name: "valid-with-name", + in: &Account{ + Account: &store.Account{ + AuthMethodId: authMethod.PublicId, + Subject: "valid-with-name", + Name: "test-name-repo", + }, + }, + want: &Account{ + Account: &store.Account{ + AuthMethodId: authMethod.PublicId, + Issuer: "https://www.alice.com", + Subject: "valid-with-name", + Name: "test-name-repo", + }, + }, + }, + { + name: "valid-with-description", + in: &Account{ + Account: &store.Account{ + AuthMethodId: authMethod.PublicId, + Subject: "valid-with-description", + Description: ("test-description-repo"), + }, + }, + want: &Account{ + Account: &store.Account{ + AuthMethodId: authMethod.PublicId, + Issuer: "https://www.alice.com", + Subject: "valid-with-description", + Description: ("test-description-repo"), + }, + }, + }, + { + name: "valid-overwrite-issuer", + in: &Account{ + Account: &store.Account{ + AuthMethodId: authMethod.PublicId, + Subject: "valid-overwrite-issuer", + Issuer: "https://overwrite.com", + }, + }, + want: &Account{ + Account: &store.Account{ + AuthMethodId: authMethod.PublicId, + Issuer: "https://overwrite.com", + Subject: "valid-overwrite-issuer", + }, + }, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + repo, err := NewRepository(rw, rw, kmsCache) + assert.NoError(err) + require.NotNil(repo) + got, err := repo.CreateAccount(context.Background(), org.GetPublicId(), tt.in, tt.opts...) + if tt.wantIsErr != 0 { + assert.Truef(errors.Match(errors.T(tt.wantIsErr), err), "Unexpected error %s", err) + assert.Equal(tt.wantErrMsg, err.Error()) + return + } + require.NoError(err) + assert.Empty(tt.in.PublicId) + require.NotNil(got) + assertPublicId(t, AccountPrefix, got.PublicId) + assert.NotSame(tt.in, got) + assert.Equal(tt.want.Name, got.Name) + assert.Equal(tt.want.Description, got.Description) + assert.Equal(tt.want.Subject, got.Subject) + assert.Equal(tt.want.Issuer, got.Issuer) + assert.Equal(got.CreateTime, got.UpdateTime) + + assert.NoError(db.TestVerifyOplog(t, rw, got.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_CREATE), db.WithCreateNotBefore(10*time.Second))) + }) + } +} + +func TestRepository_CreateAccount_DuplicateFields(t *testing.T) { + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + + kmsCache := kms.TestKms(t, conn, wrapper) + iamRepo := iam.TestRepo(t, conn, wrapper) + ctx := context.Background() + + t.Run("invalid-duplicate-names", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + repo, err := NewRepository(rw, rw, kmsCache) + assert.NoError(err) + require.NotNil(repo) + + org, _ := iam.TestScopes(t, iamRepo) + databaseWrapper, err := kmsCache.GetWrapper(ctx, org.PublicId, kms.KeyPurposeDatabase) + require.NoError(err) + + authMethod := TestAuthMethod( + t, conn, databaseWrapper, org.PublicId, ActivePrivateState, + "alice-rp", "fido", + WithSigningAlgs(RS256), + WithIssuer(TestConvertToUrls(t, "https://www.alice.com")[0]), + WithApiUrl(TestConvertToUrls(t, "https://www.alice.com/callback")[0]), + ) + + in := &Account{ + Account: &store.Account{ + AuthMethodId: authMethod.GetPublicId(), + Name: "test-name-repo", + Subject: "subject", + }, + } + + got, err := repo.CreateAccount(context.Background(), org.GetPublicId(), in) + require.NoError(err) + require.NotNil(got) + assertPublicId(t, AccountPrefix, got.PublicId) + assert.NotSame(in, got) + assert.Equal(in.Name, got.Name) + assert.Equal(in.Description, got.Description) + assert.Equal(got.CreateTime, got.UpdateTime) + + got2, err := repo.CreateAccount(context.Background(), org.GetPublicId(), in) + assert.Truef(errors.Match(errors.T(errors.NotUnique), err), "Unexpected error %s", err) + assert.Nil(got2) + }) + + t.Run("valid-duplicate-names-diff-parents", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + repo, err := NewRepository(rw, rw, kmsCache) + assert.NoError(err) + require.NotNil(repo) + + org, _ := iam.TestScopes(t, iamRepo) + databaseWrapper, err := kmsCache.GetWrapper(ctx, org.PublicId, kms.KeyPurposeDatabase) + require.NoError(err) + + authMethoda := TestAuthMethod( + t, conn, databaseWrapper, org.PublicId, ActivePrivateState, + "alice-rp1", "fido", + WithSigningAlgs(RS256), + WithIssuer(TestConvertToUrls(t, "https://www.alice1.com")[0]), + WithApiUrl(TestConvertToUrls(t, "https://www.alice.com/callback")[0]), + ) + authMethodb := TestAuthMethod( + t, conn, databaseWrapper, org.PublicId, ActivePrivateState, + "alice-rp2", "fido", + WithIssuer(TestConvertToUrls(t, "https://www.alice2.com")[0]), + WithSigningAlgs(RS256), + WithApiUrl(TestConvertToUrls(t, "https://www.alice.com/callback")[0]), + ) + in := &Account{ + Account: &store.Account{ + Name: "test-name-repo", + Subject: "subject1", + }, + } + in2 := in.Clone() + + in.AuthMethodId = authMethoda.GetPublicId() + got, err := repo.CreateAccount(context.Background(), org.GetPublicId(), in) + require.NoError(err) + require.NotNil(got) + assertPublicId(t, AccountPrefix, got.PublicId) + assert.NotSame(in, got) + assert.Equal(in.Name, got.Name) + assert.Equal(in.Description, got.Description) + assert.Equal(got.CreateTime, got.UpdateTime) + + in2.AuthMethodId = authMethodb.GetPublicId() + got2, err := repo.CreateAccount(context.Background(), org.GetPublicId(), in2) + assert.NoError(err) + require.NotNil(got2) + assertPublicId(t, AccountPrefix, got2.PublicId) + assert.NotSame(in2, got2) + assert.Equal(in2.Name, got2.Name) + assert.Equal(in2.Description, got2.Description) + assert.Equal(got2.CreateTime, got2.UpdateTime) + }) + + t.Run("invalid-duplicate-subjects", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + repo, err := NewRepository(rw, rw, kmsCache) + assert.NoError(err) + require.NotNil(repo) + + org, _ := iam.TestScopes(t, iamRepo) + databaseWrapper, err := kmsCache.GetWrapper(ctx, org.PublicId, kms.KeyPurposeDatabase) + require.NoError(err) + + authMethod := TestAuthMethod( + t, conn, databaseWrapper, org.PublicId, ActivePrivateState, + "alice-rp", "fido", + WithSigningAlgs(RS256), + WithIssuer(TestConvertToUrls(t, "https://www.alice.com")[0]), + WithApiUrl(TestConvertToUrls(t, "https://www.alice.com/callback")[0]), + ) + + in := &Account{ + Account: &store.Account{ + AuthMethodId: authMethod.GetPublicId(), + Subject: "subject1", + }, + } + + got, err := repo.CreateAccount(context.Background(), org.GetPublicId(), in) + require.NoError(err) + require.NotNil(got) + assertPublicId(t, AccountPrefix, got.PublicId) + assert.NotSame(in, got) + assert.Equal(in.Name, got.Name) + assert.Equal(in.Description, got.Description) + assert.Equal(got.CreateTime, got.UpdateTime) + + got2, err := repo.CreateAccount(context.Background(), org.GetPublicId(), in) + assert.Truef(errors.Match(errors.T(errors.NotUnique), err), "Unexpected error %s", err) + assert.Nil(got2) + }) + + t.Run("valid-duplicate-subject-diff-authmethod", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + repo, err := NewRepository(rw, rw, kmsCache) + assert.NoError(err) + require.NotNil(repo) + + org, _ := iam.TestScopes(t, iamRepo) + databaseWrapper, err := kmsCache.GetWrapper(ctx, org.PublicId, kms.KeyPurposeDatabase) + require.NoError(err) + + authMethoda := TestAuthMethod( + t, conn, databaseWrapper, org.PublicId, ActivePrivateState, + "alice-rp1", "fido", + WithSigningAlgs(RS256), + WithIssuer(TestConvertToUrls(t, "https://www.alice1.com")[0]), + WithApiUrl(TestConvertToUrls(t, "https://www.alice.com/callback")[0]), + ) + authMethodb := TestAuthMethod( + t, conn, databaseWrapper, org.PublicId, ActivePrivateState, + "alice-rp2", "fido", + WithSigningAlgs(RS256), + WithIssuer(TestConvertToUrls(t, "https://www.alice2.com")[0]), + WithApiUrl(TestConvertToUrls(t, "https://www.alice.com/callback")[0]), + ) + in := &Account{ + Account: &store.Account{ + Subject: "subject1", + }, + } + in2 := in.Clone() + + in.AuthMethodId = authMethoda.GetPublicId() + got, err := repo.CreateAccount(context.Background(), org.GetPublicId(), in) + require.NoError(err) + require.NotNil(got) + assertPublicId(t, AccountPrefix, got.PublicId) + assert.NotSame(in, got) + assert.Equal(in.Name, got.Name) + assert.Equal(in.Description, got.Description) + assert.Equal(in.Subject, got.Subject) + assert.Equal(authMethoda.GetIssuer(), got.Issuer) + assert.Equal(got.CreateTime, got.UpdateTime) + + in2.AuthMethodId = authMethodb.GetPublicId() + got2, err := repo.CreateAccount(context.Background(), org.GetPublicId(), in2) + assert.NoError(err) + require.NotNil(got2) + assertPublicId(t, AccountPrefix, got2.PublicId) + assert.NotSame(in2, got2) + assert.Equal(in2.Name, got2.Name) + assert.Equal(in2.Description, got2.Description) + assert.Equal(in2.Subject, got2.Subject) + assert.Equal(authMethodb.GetIssuer(), got2.Issuer) + assert.Equal(got2.CreateTime, got2.UpdateTime) + }) + + t.Run("valid-duplicate-subject-diff-issuer", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + repo, err := NewRepository(rw, rw, kmsCache) + assert.NoError(err) + require.NotNil(repo) + + org, _ := iam.TestScopes(t, iamRepo) + databaseWrapper, err := kmsCache.GetWrapper(ctx, org.PublicId, kms.KeyPurposeDatabase) + require.NoError(err) + + authMethod := TestAuthMethod( + t, conn, databaseWrapper, org.PublicId, ActivePrivateState, + "alice-rp1", "fido", + WithSigningAlgs(RS256), + WithIssuer(TestConvertToUrls(t, "https://www.alice1.com")[0]), + WithApiUrl(TestConvertToUrls(t, "https://www.alice.com/callback")[0]), + ) + in := &Account{ + Account: &store.Account{ + AuthMethodId: authMethod.GetPublicId(), + Subject: "subject1", + }, + } + in2 := in.Clone() + + got, err := repo.CreateAccount(context.Background(), org.GetPublicId(), in) + require.NoError(err) + require.NotNil(got) + assertPublicId(t, AccountPrefix, got.PublicId) + assert.NotSame(in, got) + assert.Equal(in.Name, got.Name) + assert.Equal(in.Description, got.Description) + assert.Equal(in.Subject, got.Subject) + assert.Equal(authMethod.Issuer, got.Issuer) + assert.Equal(got.CreateTime, got.UpdateTime) + + authMethod.Issuer = "https://somethingelse.com" + authMethod, _, err = repo.UpdateAuthMethod(ctx, authMethod, authMethod.Version, []string{"Issuer"}, WithForce()) + require.NoError(err) + + got2, err := repo.CreateAccount(context.Background(), org.GetPublicId(), in2) + assert.NoError(err) + require.NotNil(got2) + assertPublicId(t, AccountPrefix, got2.PublicId) + assert.NotSame(in2, got2) + assert.Equal(in2.Name, got2.Name) + assert.Equal(in2.Description, got2.Description) + assert.Equal(in2.Subject, got2.Subject) + assert.Equal(authMethod.Issuer, got2.Issuer) + assert.Equal(got2.CreateTime, got2.UpdateTime) + }) +} + +func TestRepository_LookupAccount(t *testing.T) { + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + + kmsCache := kms.TestKms(t, conn, wrapper) + iamRepo := iam.TestRepo(t, conn, wrapper) + org, _ := iam.TestScopes(t, iamRepo) + + ctx := context.Background() + databaseWrapper, err := kmsCache.GetWrapper(ctx, org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + + authMethod := TestAuthMethod( + t, conn, databaseWrapper, org.PublicId, ActivePrivateState, + "alice-rp", "fido", + WithSigningAlgs(RS256), + WithIssuer(TestConvertToUrls(t, "https://www.alice.com")[0]), + WithApiUrl(TestConvertToUrls(t, "https://www.alice.com/callback")[0]), + ) + account := TestAccount(t, conn, authMethod, "test-subject") + + newAcctId, err := newAccountId(authMethod.GetPublicId(), authMethod.Issuer, "random-id") + require.NoError(t, err) + tests := []struct { + name string + in string + want *Account + wantIsErr errors.Code + wantErrMsg string + }{ + { + name: "With no public id", + wantIsErr: errors.InvalidPublicId, + wantErrMsg: "oidc.(Repository).LookupAccount: missing public id: parameter violation: error #102", + }, + { + name: "With non existing account id", + in: newAcctId, + }, + { + name: "With existing account id", + in: account.GetPublicId(), + want: account, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + repo, err := NewRepository(rw, rw, kmsCache) + assert.NoError(err) + require.NotNil(repo) + got, err := repo.LookupAccount(context.Background(), tt.in) + if tt.wantIsErr != 0 { + assert.Truef(errors.Match(errors.T(tt.wantIsErr), err), "Unexpected error %s", err) + assert.Equal(tt.wantErrMsg, err.Error()) + return + } + require.NoError(err) + assert.EqualValues(tt.want, got) + }) + } +} + +func TestRepository_DeleteAccount(t *testing.T) { + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + + ctx := context.Background() + kmsCache := kms.TestKms(t, conn, wrapper) + iamRepo := iam.TestRepo(t, conn, wrapper) + org, _ := iam.TestScopes(t, iamRepo) + + databaseWrapper, err := kmsCache.GetWrapper(ctx, org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + authMethod := TestAuthMethod( + t, conn, databaseWrapper, org.PublicId, ActivePrivateState, + "alice-rp", "fido", + WithSigningAlgs(RS256), + WithIssuer(TestConvertToUrls(t, "https://www.alice.com")[0]), + WithApiUrl(TestConvertToUrls(t, "https://www.alice.com/callback")[0]), + ) + account := TestAccount(t, conn, authMethod, "create-success") + newAcctId, err := newAccountId(authMethod.GetPublicId(), authMethod.Issuer, "random-subject") + require.NoError(t, err) + tests := []struct { + name string + scopeId string + in string + want int + wantIsErr errors.Code + wantErrMsg string + }{ + { + name: "With no scope id", + scopeId: "", + in: account.GetPublicId(), + wantIsErr: errors.InvalidParameter, + wantErrMsg: "oidc.(Repository).DeleteAccount: missing scope id: parameter violation: error #100", + }, + { + name: "With no public id", + scopeId: org.GetPublicId(), + wantIsErr: errors.InvalidPublicId, + wantErrMsg: "oidc.(Repository).DeleteAccount: missing public id: parameter violation: error #102", + }, + { + name: "With non existing account id", + scopeId: org.GetPublicId(), + in: newAcctId, + want: 0, + }, + { + name: "With existing account id", + scopeId: org.GetPublicId(), + in: account.GetPublicId(), + want: 1, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + repo, err := NewRepository(rw, rw, kmsCache) + assert.NoError(err) + require.NotNil(repo) + got, err := repo.DeleteAccount(context.Background(), tt.scopeId, tt.in) + if tt.wantIsErr != 0 { + assert.Truef(errors.Match(errors.T(tt.wantIsErr), err), "Unexpected error %s", err) + assert.Equal(tt.wantErrMsg, err.Error()) + return + } + require.NoError(err) + assert.EqualValues(tt.want, got) + }) + } +} + +func TestRepository_ListAccounts(t *testing.T) { + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + + ctx := context.Background() + kmsCache := kms.TestKms(t, conn, wrapper) + iamRepo := iam.TestRepo(t, conn, wrapper) + org, _ := iam.TestScopes(t, iamRepo) + + databaseWrapper, err := kmsCache.GetWrapper(ctx, org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + authMethod1 := TestAuthMethod( + t, conn, databaseWrapper, org.PublicId, ActivePrivateState, + "alice-rp", "fido", + WithSigningAlgs(RS256), + WithIssuer(TestConvertToUrls(t, "https://www.alice1.com")[0]), + WithApiUrl(TestConvertToUrls(t, "https://www.alice.com/callback")[0]), + ) + authMethod2 := TestAuthMethod( + t, conn, databaseWrapper, org.PublicId, ActivePrivateState, + "alice-rp", "fido", + WithSigningAlgs(RS256), + WithIssuer(TestConvertToUrls(t, "https://www.alice2.com")[0]), + WithApiUrl(TestConvertToUrls(t, "https://www.alice.com/callback")[0]), + ) + authMethod3 := TestAuthMethod( + t, conn, databaseWrapper, org.PublicId, ActivePrivateState, + "alice-rp", "fido", + WithSigningAlgs(RS256), + WithIssuer(TestConvertToUrls(t, "https://www.alice3.com")[0]), + WithApiUrl(TestConvertToUrls(t, "https://www.alice.com/callback")[0]), + ) + accounts1 := []*Account{ + TestAccount(t, conn, authMethod1, "create-success"), + TestAccount(t, conn, authMethod1, "create-success2"), + TestAccount(t, conn, authMethod1, "create-success3"), + } + accounts2 := []*Account{ + TestAccount(t, conn, authMethod2, "create-success"), + TestAccount(t, conn, authMethod2, "create-success2"), + TestAccount(t, conn, authMethod2, "create-success3"), + } + _ = accounts2 + + tests := []struct { + name string + in string + opts []Option + want []*Account + wantIsErr errors.Code + wantErrMsg string + }{ + { + name: "With no auth method id", + wantIsErr: errors.InvalidParameter, + wantErrMsg: "oidc.(Repository).ListAccounts: missing auth method id: parameter violation: error #100", + }, + { + name: "With no accounts id", + in: authMethod3.GetPublicId(), + want: []*Account{}, + }, + { + name: "With first auth method id", + in: authMethod1.GetPublicId(), + want: accounts1, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + repo, err := NewRepository(rw, rw, kmsCache) + assert.NoError(err) + require.NotNil(repo) + got, err := repo.ListAccounts(context.Background(), tt.in, tt.opts...) + if tt.wantIsErr != 0 { + assert.Truef(errors.Match(errors.T(tt.wantIsErr), err), "Unexpected error %s", err) + assert.Equal(tt.wantErrMsg, err.Error()) + return + } + require.NoError(err) + + sort.Slice(got, func(i, j int) bool { + return strings.Compare(got[i].Subject, got[j].Subject) < 0 + }) + assert.EqualValues(tt.want, got) + }) + } +} + +func TestRepository_ListAccounts_Limits(t *testing.T) { + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + + ctx := context.Background() + kmsCache := kms.TestKms(t, conn, wrapper) + iamRepo := iam.TestRepo(t, conn, wrapper) + org, _ := iam.TestScopes(t, iamRepo) + + databaseWrapper, err := kmsCache.GetWrapper(ctx, org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + + am := TestAuthMethod( + t, conn, databaseWrapper, org.PublicId, ActivePrivateState, + "alice-rp", "fido", + WithSigningAlgs(RS256), + WithIssuer(TestConvertToUrls(t, "https://www.alice1.com")[0]), + WithApiUrl(TestConvertToUrls(t, "https://www.alice.com/callback")[0]), + ) + + accountCount := 10 + for i := 0; i < accountCount; i++ { + TestAccount(t, conn, am, fmt.Sprintf("create-success-%d", i)) + } + + tests := []struct { + name string + repoOpts []Option + listOpts []Option + wantLen int + }{ + { + name: "With no limits", + wantLen: accountCount, + }, + { + name: "With repo limit", + repoOpts: []Option{WithLimit(3)}, + wantLen: 3, + }, + { + name: "With negative repo limit", + repoOpts: []Option{WithLimit(-1)}, + wantLen: accountCount, + }, + { + name: "With List limit", + listOpts: []Option{WithLimit(3)}, + wantLen: 3, + }, + { + name: "With negative List limit", + listOpts: []Option{WithLimit(-1)}, + wantLen: accountCount, + }, + { + name: "With repo smaller than list limit", + repoOpts: []Option{WithLimit(2)}, + listOpts: []Option{WithLimit(6)}, + wantLen: 6, + }, + { + name: "With repo larger than list limit", + repoOpts: []Option{WithLimit(6)}, + listOpts: []Option{WithLimit(2)}, + wantLen: 2, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + repo, err := NewRepository(rw, rw, kmsCache, tt.repoOpts...) + assert.NoError(err) + require.NotNil(repo) + got, err := repo.ListAccounts(context.Background(), am.GetPublicId(), tt.listOpts...) + require.NoError(err) + assert.Len(got, tt.wantLen) + }) + } +} + +func TestRepository_UpdateAccount(t *testing.T) { + ctx := context.Background() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, wrapper) + iamRepo := iam.TestRepo(t, conn, wrapper) + org, _ := iam.TestScopes(t, iamRepo) + + databaseWrapper, err := kmsCache.GetWrapper(ctx, org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + am := TestAuthMethod( + t, conn, databaseWrapper, org.PublicId, ActivePrivateState, + "alice-rp", "fido", + WithSigningAlgs(RS256), + WithIssuer(TestConvertToUrls(t, "https://www.alice.com")[0]), + WithApiUrl(TestConvertToUrls(t, "https://www.alice.com/callback")[0]), + ) + + changeName := func(s string) func(*Account) *Account { + return func(a *Account) *Account { + a.Name = s + return a + } + } + + changeDescription := func(s string) func(*Account) *Account { + return func(a *Account) *Account { + a.Description = s + return a + } + } + + makeNil := func() func(*Account) *Account { + return func(a *Account) *Account { + return nil + } + } + + makeEmbeddedNil := func() func(*Account) *Account { + return func(a *Account) *Account { + return &Account{} + } + } + + deletePublicId := func() func(*Account) *Account { + return func(a *Account) *Account { + a.PublicId = "" + return a + } + } + + nonExistentPublicId := func() func(*Account) *Account { + return func(a *Account) *Account { + a.PublicId = "abcd_OOOOOOOOOO" + return a + } + } + + combine := func(fns ...func(a *Account) *Account) func(*Account) *Account { + return func(a *Account) *Account { + for _, fn := range fns { + a = fn(a) + } + return a + } + } + + tests := []struct { + name string + scopeId string + version uint32 + orig *Account + chgFn func(*Account) *Account + masks []string + want *Account + wantCount int + wantIsErr errors.Code + wantErrMsg string + }{ + { + name: "nil-Account", + scopeId: org.GetPublicId(), + version: 1, + orig: &Account{ + Account: &store.Account{}, + }, + chgFn: makeNil(), + masks: []string{"Name", "Description"}, + wantIsErr: errors.InvalidParameter, + wantErrMsg: "oidc.(Repository).UpdateAccount: missing Account: parameter violation: error #100", + }, + { + name: "nil-embedded-Account", + scopeId: org.GetPublicId(), + version: 1, + orig: &Account{ + Account: &store.Account{}, + }, + chgFn: makeEmbeddedNil(), + masks: []string{"Name", "Description"}, + wantIsErr: errors.InvalidParameter, + wantErrMsg: "oidc.(Repository).UpdateAccount: missing embedded Account: parameter violation: error #100", + }, + { + name: "no-scope-id", + version: 1, + orig: &Account{ + Account: &store.Account{ + Name: "no-scope-id-test-name-repo", + }, + }, + chgFn: changeName("no-scope-id-test-update-name-repo"), + masks: []string{"Name"}, + wantIsErr: errors.InvalidParameter, + wantErrMsg: "oidc.(Repository).UpdateAccount: missing scope id: parameter violation: error #100", + }, + { + name: "missing-version", + scopeId: org.GetPublicId(), + orig: &Account{ + Account: &store.Account{ + Name: "missing-version-test-name-repo", + }, + }, + chgFn: changeName("test-update-name-repo"), + masks: []string{"Name"}, + wantIsErr: errors.InvalidParameter, + wantErrMsg: "oidc.(Repository).UpdateAccount: missing version: parameter violation: error #100", + }, + { + name: "no-public-id", + scopeId: org.GetPublicId(), + version: 1, + orig: &Account{ + Account: &store.Account{}, + }, + chgFn: deletePublicId(), + masks: []string{"Name", "Description"}, + wantIsErr: errors.InvalidPublicId, + wantErrMsg: "oidc.(Repository).UpdateAccount: missing public id: parameter violation: error #102", + }, + { + name: "updating-non-existent-Account", + scopeId: org.GetPublicId(), + version: 1, + orig: &Account{ + Account: &store.Account{ + Name: "updating-non-existent-Account-test-name-repo", + }, + }, + chgFn: combine(nonExistentPublicId(), changeName("updating-non-existent-Account-test-update-name-repo")), + masks: []string{"Name"}, + wantIsErr: errors.RecordNotFound, + wantErrMsg: "oidc.(Repository).UpdateAccount: abcd_OOOOOOOOOO: db.DoTx: oidc.(Repository).UpdateAccount: db.Update: db.lookupAfterWrite: db.LookupById: record not found, search issue: error #1100", + }, + { + name: "empty-field-mask", + scopeId: org.GetPublicId(), + version: 1, + orig: &Account{ + Account: &store.Account{ + Name: "empty-field-mask-test-name-repo", + }, + }, + chgFn: changeName("empty-field-mask-test-update-name-repo"), + wantIsErr: errors.EmptyFieldMask, + wantErrMsg: "oidc.(Repository).UpdateAccount: missing field mask: parameter violation: error #104", + }, + { + name: "read-only-fields-in-field-mask", + scopeId: org.GetPublicId(), + version: 1, + orig: &Account{ + Account: &store.Account{ + Name: "read-only-fields-in-field-mask-test-name-repo", + }, + }, + chgFn: changeName("read-only-fields-in-field-mask-test-update-name-repo"), + masks: []string{"PublicId", "CreateTime", "UpdateTime", "AuthMethodId"}, + wantIsErr: errors.InvalidFieldMask, + wantErrMsg: "oidc.(Repository).UpdateAccount: PublicId: parameter violation: error #103", + }, + { + name: "unknown-field-in-field-mask", + scopeId: org.GetPublicId(), + version: 1, + orig: &Account{ + Account: &store.Account{ + Name: "unknown-field-in-field-mask-test-name-repo", + }, + }, + chgFn: changeName("unknown-field-in-field-mask-test-update-name-repo"), + masks: []string{"Bilbo"}, + wantIsErr: errors.InvalidFieldMask, + wantErrMsg: "oidc.(Repository).UpdateAccount: Bilbo: parameter violation: error #103", + }, + { + name: "change-name", + scopeId: org.GetPublicId(), + version: 1, + orig: &Account{ + Account: &store.Account{ + Name: "change-name-test-name-repo", + }, + }, + chgFn: changeName("change-name-test-update-name-repo"), + masks: []string{"Name"}, + want: &Account{ + Account: &store.Account{ + Name: "change-name-test-update-name-repo", + }, + }, + wantCount: 1, + }, + { + name: "change-description", + scopeId: org.GetPublicId(), + version: 1, + orig: &Account{ + Account: &store.Account{ + Description: "test-description-repo", + }, + }, + chgFn: changeDescription("test-update-description-repo"), + masks: []string{"Description"}, + want: &Account{ + Account: &store.Account{ + Description: "test-update-description-repo", + }, + }, + wantCount: 1, + }, + { + name: "change-name-and-description", + scopeId: org.GetPublicId(), + version: 1, + orig: &Account{ + Account: &store.Account{ + Name: "change-name-and-description-test-name-repo", + Description: "test-description-repo", + }, + }, + chgFn: combine(changeDescription("test-update-description-repo"), changeName("change-name-and-description-test-update-name-repo")), + masks: []string{"Name", "Description"}, + want: &Account{ + Account: &store.Account{ + Name: "change-name-and-description-test-update-name-repo", + Description: "test-update-description-repo", + }, + }, + wantCount: 1, + }, + { + name: "delete-name", + scopeId: org.GetPublicId(), + version: 1, + orig: &Account{ + Account: &store.Account{ + Name: "delete-name-test-name-repo", + Description: "test-description-repo", + }, + }, + masks: []string{"Name"}, + chgFn: combine(changeDescription("test-update-description-repo"), changeName("")), + want: &Account{ + Account: &store.Account{ + Description: "test-description-repo", + }, + }, + wantCount: 1, + }, + { + name: "delete-description", + scopeId: org.GetPublicId(), + version: 1, + orig: &Account{ + Account: &store.Account{ + Name: "delete-description-test-name-repo", + Description: "test-description-repo", + }, + }, + masks: []string{"Description"}, + chgFn: combine(changeDescription(""), changeName("test-update-name-repo")), + want: &Account{ + Account: &store.Account{ + Name: "delete-description-test-name-repo", + }, + }, + wantCount: 1, + }, + { + name: "do-not-delete-name", + scopeId: org.GetPublicId(), + version: 1, + orig: &Account{ + Account: &store.Account{ + Name: "do-not-delete-name-test-name-repo", + Description: "test-description-repo", + }, + }, + masks: []string{"Description"}, + chgFn: combine(changeDescription("test-update-description-repo"), changeName("")), + want: &Account{ + Account: &store.Account{ + Name: "do-not-delete-name-test-name-repo", + Description: "test-update-description-repo", + }, + }, + wantCount: 1, + }, + { + name: "do-not-delete-description", + scopeId: org.GetPublicId(), + version: 1, + orig: &Account{ + Account: &store.Account{ + Name: "do-not-delete-description-test-name-repo", + Description: "test-description-repo", + }, + }, + masks: []string{"Name"}, + chgFn: combine(changeDescription(""), changeName("do-not-delete-description-test-update-name-repo")), + want: &Account{ + Account: &store.Account{ + Name: "do-not-delete-description-test-update-name-repo", + Description: "test-description-repo", + }, + }, + wantCount: 1, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + repo, err := NewRepository(rw, rw, kmsCache) + assert.NoError(err) + require.NotNil(repo) + + orig := TestAccount(t, conn, am, tt.name, WithName(tt.orig.GetName()), WithDescription(tt.orig.GetDescription())) + + tt.orig.AuthMethodId = am.PublicId + if tt.chgFn != nil { + orig = tt.chgFn(orig) + } + got, gotCount, err := repo.UpdateAccount(context.Background(), tt.scopeId, orig, tt.version, tt.masks) + if tt.wantIsErr != 0 { + assert.Truef(errors.Match(errors.T(tt.wantIsErr), err), "want err: %q got: %q", tt.wantIsErr, err) + assert.Equal(tt.wantErrMsg, err.Error()) + assert.Equal(tt.wantCount, gotCount, "row count") + assert.Nil(got) + return + } + assert.NoError(err) + assert.Empty(tt.orig.PublicId) + if tt.wantCount == 0 { + assert.Equal(tt.wantCount, gotCount, "row count") + assert.Nil(got) + return + } + require.NotNil(got) + assertPublicId(t, AccountPrefix, got.PublicId) + assert.Equal(tt.wantCount, gotCount, "row count") + assert.NotSame(tt.orig, got) + assert.Equal(tt.orig.AuthMethodId, got.AuthMethodId) + dbassert := dbassert.New(t, conn.DB()) + if tt.want.Name == "" { + dbassert.IsNull(got, "name") + return + } + assert.Equal(tt.want.Name, got.Name) + if tt.want.Description == "" { + dbassert.IsNull(got, "description") + return + } + assert.Equal(tt.want.Description, got.Description) + if tt.wantCount > 0 { + assert.NoError(db.TestVerifyOplog(t, rw, got.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_UPDATE), db.WithCreateNotBefore(10*time.Second))) + } + }) + } +} + +func TestRepository_UpdateAccount_DupeNames(t *testing.T) { + ctx := context.Background() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, wrapper) + iamRepo := iam.TestRepo(t, conn, wrapper) + + t.Run("invalid-duplicate-names", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + repo, err := NewRepository(rw, rw, kmsCache) + assert.NoError(err) + require.NotNil(repo) + + name := "test-dup-name" + org, _ := iam.TestScopes(t, iamRepo) + databaseWrapper, err := kmsCache.GetWrapper(ctx, org.PublicId, kms.KeyPurposeDatabase) + require.NoError(err) + + am := TestAuthMethod( + t, conn, databaseWrapper, org.PublicId, ActivePrivateState, + "alice-rp", "fido", + WithSigningAlgs(RS256), + WithIssuer(TestConvertToUrls(t, "https://www.alice.com")[0]), + WithApiUrl(TestConvertToUrls(t, "https://www.alice.com/callback")[0]), + ) + aa := TestAccount(t, conn, am, "create-success1") + ab := TestAccount(t, conn, am, "create-success2") + + aa.Name = name + got1, gotCount1, err := repo.UpdateAccount(context.Background(), org.GetPublicId(), aa, 1, []string{"name"}) + assert.NoError(err) + require.NotNil(got1) + assert.Equal(name, got1.Name) + assert.Equal(1, gotCount1, "row count") + assert.NoError(db.TestVerifyOplog(t, rw, aa.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_UPDATE), db.WithCreateNotBefore(10*time.Second))) + + ab.Name = name + got2, gotCount2, err := repo.UpdateAccount(context.Background(), org.GetPublicId(), ab, 1, []string{"name"}) + assert.Truef(errors.Match(errors.T(errors.NotUnique), err), "Unexpected error %s", err) + assert.Nil(got2) + assert.Equal(db.NoRowsAffected, gotCount2, "row count") + err = db.TestVerifyOplog(t, rw, ab.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_UPDATE), db.WithCreateNotBefore(10*time.Second)) + assert.Error(err) + assert.True(errors.IsNotFoundError(err)) + }) + + t.Run("valid-duplicate-names-diff-AuthMethods", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + repo, err := NewRepository(rw, rw, kmsCache) + assert.NoError(err) + require.NotNil(repo) + + org, _ := iam.TestScopes(t, iamRepo) + databaseWrapper, err := kmsCache.GetWrapper(ctx, org.PublicId, kms.KeyPurposeDatabase) + require.NoError(err) + + ama := TestAuthMethod( + t, conn, databaseWrapper, org.PublicId, ActivePrivateState, + "alice-rp", "fido", + WithSigningAlgs(RS256), + WithIssuer(TestConvertToUrls(t, "https://www.alice.com")[0]), + WithApiUrl(TestConvertToUrls(t, "https://www.alice.com/callback")[0]), + ) + aa := TestAccount(t, conn, ama, "create-success1", WithName("test-name-aa")) + + amb := TestAuthMethod( + t, conn, databaseWrapper, org.PublicId, ActivePrivateState, + "alice-rp", "fido2", + WithSigningAlgs(RS256), + WithIssuer(TestConvertToUrls(t, "https://www.alice2.com")[0]), + WithApiUrl(TestConvertToUrls(t, "https://www.alice.com/callback")[0]), + ) + ab := TestAccount(t, conn, amb, "create-success2", WithName("test-name-ab")) + + ab.Name = aa.Name + got3, gotCount3, err := repo.UpdateAccount(context.Background(), org.GetPublicId(), ab, 1, []string{"name"}) + assert.NoError(err) + require.NotNil(got3) + assert.NotSame(ab, got3) + assert.Equal(aa.Name, got3.Name) + assert.Equal(1, gotCount3, "row count") + assert.NoError(db.TestVerifyOplog(t, rw, ab.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_UPDATE), db.WithCreateNotBefore(10*time.Second))) + }) + + t.Run("change-authmethod-id", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + repo, err := NewRepository(rw, rw, kmsCache) + assert.NoError(err) + require.NotNil(repo) + + org, _ := iam.TestScopes(t, iamRepo) + databaseWrapper, err := kmsCache.GetWrapper(ctx, org.PublicId, kms.KeyPurposeDatabase) + require.NoError(err) + + ama := TestAuthMethod( + t, conn, databaseWrapper, org.PublicId, ActivePrivateState, + "alice-rp", "fido", + WithSigningAlgs(RS256), + WithIssuer(TestConvertToUrls(t, "https://www.alice.com")[0]), + WithApiUrl(TestConvertToUrls(t, "https://www.alice.com/callback")[0]), + ) + aa := TestAccount(t, conn, ama, "create-success1") + + amb := TestAuthMethod( + t, conn, databaseWrapper, org.PublicId, ActivePrivateState, + "alice-rp", "fido2", + WithSigningAlgs(RS256), + WithIssuer(TestConvertToUrls(t, "https://www.alice2.com")[0]), + WithApiUrl(TestConvertToUrls(t, "https://www.alice.com/callback")[0]), + ) + ab := TestAccount(t, conn, amb, "create-success2") + + assert.NotEqual(aa.AuthMethodId, ab.AuthMethodId) + orig := aa.Clone() + + aa.AuthMethodId = ab.AuthMethodId + assert.Equal(aa.AuthMethodId, ab.AuthMethodId) + + got1, gotCount1, err := repo.UpdateAccount(context.Background(), org.GetPublicId(), aa, 1, []string{"name"}) + + assert.NoError(err) + require.NotNil(got1) + assert.Equal(orig.AuthMethodId, got1.AuthMethodId) + assert.Equal(1, gotCount1, "row count") + assert.NoError(db.TestVerifyOplog(t, rw, aa.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_UPDATE), db.WithCreateNotBefore(10*time.Second))) + }) +} + +func assertPublicId(t *testing.T, prefix, actual string) { + t.Helper() + assert.NotEmpty(t, actual) + parts := strings.Split(actual, "_") + assert.Equalf(t, 2, len(parts), "want one '_' in PublicId, got multiple in %q", actual) + assert.Equalf(t, prefix, parts[0], "PublicId want prefix: %q, got: %q in %q", prefix, parts[0], actual) +} diff --git a/internal/auth/oidc/repository_auth_method.go b/internal/auth/oidc/repository_auth_method.go new file mode 100644 index 000000000..959c3c931 --- /dev/null +++ b/internal/auth/oidc/repository_auth_method.go @@ -0,0 +1,219 @@ +package oidc + +import ( + "context" + "fmt" + "net/url" + "strings" + + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/oplog" + wrapping "github.com/hashicorp/go-kms-wrapping" + "google.golang.org/protobuf/proto" +) + +// Account must implement oplog.Replayable for upsertAccount to work +var _ oplog.ReplayableMessage = (*Account)(nil) + +// Account must implement proto.Message for upsertAccount to work +var _ proto.Message = (*Account)(nil) + +// upsertAccount will create/update account using claims from the user's ID and Access Tokens. +func (r *Repository) upsertAccount(ctx context.Context, am *AuthMethod, IdTokenClaims, AccessTokenClaims map[string]interface{}) (*Account, error) { + const op = "oidc.(Repository).upsertAccount" + if am == nil || am.AuthMethod == nil { + return nil, errors.New(errors.InvalidParameter, op, "missing auth method") + } + if IdTokenClaims == nil { + return nil, errors.New(errors.InvalidParameter, op, "missing ID Token claims") + } + if AccessTokenClaims == nil { + return nil, errors.New(errors.InvalidParameter, op, "missing Access Token claims") + } + var iss, sub string + var ok bool + if iss, ok = IdTokenClaims["iss"].(string); !ok { + return nil, errors.New(errors.Unknown, op, "issuer is not present in ID Token, which should not be possible") + } + if sub, ok = IdTokenClaims["sub"].(string); !ok { + return nil, errors.New(errors.Unknown, op, "subject is not present in ID Token, which should not be possible") + } + pubId, err := newAccountId(am.GetPublicId(), iss, sub) + if err != nil { + return nil, errors.Wrap(err, op) + } + + columns := []string{"public_id", "auth_method_id", "issuer", "subject"} + values := []interface{}{pubId, am.PublicId, iss, sub} + var conflictClauses, fieldMasks, nullMasks []string + + var foundEmail, foundName interface{} + switch { + case AccessTokenClaims["name"] != nil: + foundName = AccessTokenClaims["name"] + columns, values = append(columns, "full_name"), append(values, foundName) + case IdTokenClaims["name"] != nil: + foundName = IdTokenClaims["name"] + columns, values = append(columns, "full_name"), append(values, foundName) + default: + conflictClauses = append(conflictClauses, "full_name = NULL") + nullMasks = append(nullMasks, "Name") + } + switch { + case AccessTokenClaims["email"] != nil: + foundEmail = AccessTokenClaims["email"] + columns, values = append(columns, "email"), append(values, foundEmail) + case IdTokenClaims["email"] != nil: + foundEmail = IdTokenClaims["email"] + columns, values = append(columns, "email"), append(values, foundEmail) + default: + conflictClauses = append(conflictClauses, "email = NULL") + nullMasks = append(nullMasks, "Email") + } + + if foundName != nil { + values = append(values, foundName) + conflictClauses = append(conflictClauses, fmt.Sprintf("full_name = $%d", len(values))) + fieldMasks = append(fieldMasks, "Name") + } + if foundEmail != nil { + values = append(values, foundEmail) + conflictClauses = append(conflictClauses, fmt.Sprintf("email = $%d", len(values))) + fieldMasks = append(fieldMasks, "Email") + } + + issAsUrl, err := url.Parse(iss) + if err != nil { + return nil, errors.New(errors.Unknown, op, "unable to parse issuer", errors.WithWrap(err)) + } + acctForOplog, err := NewAccount(am.PublicId, sub, WithIssuer(issAsUrl)) + if err != nil { + return nil, errors.Wrap(err, op, errors.WithMsg("unable to create new acct for oplog")) + } + + if foundName != nil { + acctForOplog.FullName = foundName.(string) + } + if foundEmail != nil { + acctForOplog.Email = foundEmail.(string) + } + + placeHolders := make([]string, 0, len(columns)) + for colNum := range columns { + placeHolders = append(placeHolders, fmt.Sprintf("$%d", colNum+1)) + } + query := fmt.Sprintf(acctUpsertQuery, strings.Join(columns, ", "), strings.Join(placeHolders, ", "), strings.Join(conflictClauses, ", ")) + + oplogWrapper, err := r.kms.GetWrapper(ctx, am.ScopeId, kms.KeyPurposeOplog) + if err != nil { + return nil, errors.Wrap(err, op, errors.WithMsg("unable to get oplog wrapper")) + } + + updatedAcct := AllocAccount() + _, err = r.writer.DoTx( + ctx, + db.StdRetryCnt, + db.ExpBackoff{}, + func(reader db.Reader, w db.Writer) error { + var err error + rows, err := w.Query(ctx, query, values) + if err != nil { + return errors.Wrap(err, op, errors.WithMsg("unable to insert/update auth oidc account")) + } + defer rows.Close() + result := struct { + PublicId string + Version int + }{} + var rowCnt int + for rows.Next() { + rowCnt += 1 + err = r.reader.ScanRows(rows, &result) + if err != nil { + return errors.Wrap(err, op, errors.WithMsg("unable to scan rows for account")) + } + } + if rowCnt > 1 { + return errors.New(errors.MultipleRecords, op, fmt.Sprintf("expected 1 row but got: %d", rowCnt)) + } + if err := reader.LookupWhere(ctx, &updatedAcct, "auth_method_id = ? and issuer = ? and subject = ?", am.PublicId, iss, sub); err != nil { + return errors.Wrap(err, op, errors.WithMsg(fmt.Sprintf("unable to look up auth oidc account for: %s / %s / %s", am.PublicId, iss, sub))) + } + // include the version incase of predictable account public ids based on a calculation using authmethod id and subject + if result.Version == 1 && updatedAcct.PublicId == pubId { + if err := upsertOplog(ctx, w, oplogWrapper, oplog.OpType_OP_TYPE_CREATE, am.ScopeId, updatedAcct, nil, nil); err != nil { + return errors.Wrap(err, op, errors.WithMsg("unable to write create oplog for account")) + } + } else { + if len(fieldMasks) > 0 || len(nullMasks) > 0 { + acctForOplog := AllocAccount() + acctForOplog.PublicId = updatedAcct.PublicId + if foundEmail != nil { + acctForOplog.Email = foundEmail.(string) + } + if foundName != nil { + acctForOplog.FullName = foundName.(string) + } + if err := upsertOplog(ctx, w, oplogWrapper, oplog.OpType_OP_TYPE_UPDATE, am.ScopeId, acctForOplog, fieldMasks, nullMasks); err != nil { + return errors.Wrap(err, op, errors.WithMsg("unable to write update oplog for account")) + } + } + } + return nil + }, + ) + if err != nil { + return nil, errors.Wrap(err, op) + } + return updatedAcct, nil +} + +// upsertOplog will write oplog msgs for account upserts. The db.Writer needs to be the writer for the current +// transaction that's executing the upsert. Both fieldMasks and nullMasks are allowed to be nil for update operations. +func upsertOplog(ctx context.Context, w db.Writer, oplogWrapper wrapping.Wrapper, operation oplog.OpType, scopeId string, acct *Account, fieldMasks, nullMasks []string) error { + const op = "oidc.upsertOplog" + if w == nil { + return errors.New(errors.InvalidParameter, op, "missing db writer") + } + if oplogWrapper == nil { + return errors.New(errors.InvalidParameter, op, "missing oplog wrapper") + } + if operation != oplog.OpType_OP_TYPE_CREATE && operation != oplog.OpType_OP_TYPE_UPDATE { + return errors.New(errors.Internal, op, fmt.Sprintf("not a supported operation: %s", operation)) + } + if scopeId == "" { + return errors.New(errors.InvalidParameter, op, "missing scope id") + } + if acct == nil || acct.Account == nil { + return errors.New(errors.InvalidParameter, op, "missing account") + } + if operation == oplog.OpType_OP_TYPE_UPDATE && len(fieldMasks) == 0 && len(nullMasks) == 0 { + return errors.New(errors.InvalidParameter, op, "update operations must specify field masks and/or null masks") + } + ticket, err := w.GetTicket(acct) + if err != nil { + return errors.Wrap(err, op, errors.WithMsg("unable to get ticket")) + } + metadata := acct.oplog(operation, scopeId) + acctAsReplayable, ok := interface{}(acct).(oplog.ReplayableMessage) + if !ok { + return errors.New(errors.Internal, op, "account is not replayable") + } + acctAsProto, ok := interface{}(acct).(proto.Message) + if !ok { + return errors.New(errors.Internal, op, "account is not a proto message") + } + msg := oplog.Message{ + Message: acctAsProto, + TypeName: acctAsReplayable.TableName(), + OpType: oplog.OpType_OP_TYPE_CREATE, + FieldMaskPaths: fieldMasks, + SetToNullPaths: nullMasks, + } + if err := w.WriteOplogEntryWith(ctx, oplogWrapper, ticket, metadata, []*oplog.Message{&msg}); err != nil { + return errors.Wrap(err, op) + } + return nil +} diff --git a/internal/auth/oidc/repository_auth_method_create.go b/internal/auth/oidc/repository_auth_method_create.go new file mode 100644 index 000000000..d677ba442 --- /dev/null +++ b/internal/auth/oidc/repository_auth_method_create.go @@ -0,0 +1,120 @@ +package oidc + +import ( + "context" + "strings" + + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/oplog" +) + +// CreateAuthMethod creates am (*AuthMethod) in the repo along with its +// associated embedded optional value objects of SigningAlgs, AudClaims, +// and Certificates and returns the newly created AuthMethod +// (with its PublicId set) +// +// The AuthMethod's public id and version must be empty (zero values). +// +// All options are ignored. +func (r *Repository) CreateAuthMethod(ctx context.Context, am *AuthMethod, opt ...Option) (*AuthMethod, error) { + const op = "oidc.(Repository).CreateAuthMethod" + if am == nil { + return nil, errors.New(errors.InvalidParameter, op, "missing auth method") + } + if am.PublicId != "" { + return nil, errors.New(errors.InvalidParameter, op, "public id must be empty") + } + if am.Version != 0 { + return nil, errors.New(errors.InvalidParameter, op, "version must be empty") + } + if err := am.validate(op); err != nil { + return nil, err // validate properly sets the op to the caller, the code and the msg, so just return it. + } + + opts := getOpts(opt...) + am.PublicId = opts.withPublicId + if am.PublicId == "" { + id, err := newAuthMethodId() + if err != nil { + return nil, errors.Wrap(err, op) + } + am.PublicId = id + } else { + if !strings.HasPrefix(am.PublicId, AuthMethodPrefix+"_") { + return nil, errors.New(errors.InvalidParameter, op, "wrong auth method id prefix") + } + } + + vo, err := am.convertValueObjects() + if err != nil { + return nil, errors.Wrap(err, op) + } + + oplogWrapper, err := r.kms.GetWrapper(ctx, am.ScopeId, kms.KeyPurposeOplog) + if err != nil { + return nil, errors.Wrap(err, op, errors.WithMsg("unable to get oplog wrapper")) + } + + databaseWrapper, err := r.kms.GetWrapper(context.Background(), am.ScopeId, kms.KeyPurposeDatabase) + if err != nil { + return nil, errors.Wrap(err, op, errors.WithMsg("unable to get database wrapper")) + } + if err := am.encrypt(ctx, databaseWrapper); err != nil { + return nil, errors.Wrap(err, op) + } + + var returnedAuthMethod *AuthMethod + _, err = r.writer.DoTx( + ctx, + db.StdRetryCnt, + db.ExpBackoff{}, + func(_ db.Reader, w db.Writer) error { + msgs := make([]*oplog.Message, 0, 5) + ticket, err := w.GetTicket(am) + if err != nil { + return errors.Wrap(err, op, errors.WithMsg("unable to get ticket")) + } + + returnedAuthMethod = am.Clone() + var amOplogMsg oplog.Message + if err := w.Create(ctx, returnedAuthMethod, db.NewOplogMsg(&amOplogMsg)); err != nil { + return err + } + msgs = append(msgs, &amOplogMsg) + + if len(vo.Algs) > 0 { + algOplogMsgs := make([]*oplog.Message, 0, len(vo.Algs)) + if err := w.CreateItems(ctx, vo.Algs, db.NewOplogMsgs(&algOplogMsgs)); err != nil { + return err + } + msgs = append(msgs, algOplogMsgs...) + } + if len(vo.Auds) > 0 { + audOplogMsgs := make([]*oplog.Message, 0, len(vo.Auds)) + if err := w.CreateItems(ctx, vo.Auds, db.NewOplogMsgs(&audOplogMsgs)); err != nil { + return err + } + msgs = append(msgs, audOplogMsgs...) + } + if len(vo.Certs) > 0 { + certOplogMsgs := make([]*oplog.Message, 0, len(vo.Certs)) + if err := w.CreateItems(ctx, vo.Certs, db.NewOplogMsgs(&certOplogMsgs)); err != nil { + return err + } + msgs = append(msgs, certOplogMsgs...) + } + + metadata := am.oplog(oplog.OpType_OP_TYPE_CREATE) + if err := w.WriteOplogEntryWith(ctx, oplogWrapper, ticket, metadata, msgs); err != nil { + return errors.Wrap(err, op, errors.WithMsg("unable to write oplog")) + } + return nil + }, + ) + if err != nil { + return nil, errors.Wrap(err, op) + } + return returnedAuthMethod, nil +} diff --git a/internal/auth/oidc/repository_auth_method_create_test.go b/internal/auth/oidc/repository_auth_method_create_test.go new file mode 100644 index 000000000..ef73ac90b --- /dev/null +++ b/internal/auth/oidc/repository_auth_method_create_test.go @@ -0,0 +1,213 @@ +package oidc + +import ( + "context" + "crypto/x509" + "testing" + "time" + + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/oplog" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" +) + +func TestRepository_CreateAuthMethod(t *testing.T) { + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, wrapper) + ctx := context.Background() + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + + convertAlg := func(alg ...Alg) []string { + s := make([]string, 0, len(alg)) + for _, a := range alg { + s = append(s, string(a)) + } + return s + } + tests := []struct { + name string + am func(*testing.T) *AuthMethod + opt []Option + wantErrMatch *errors.Template + }{ + { + name: "valid", + am: func(t *testing.T) *AuthMethod { + algs := []Alg{RS256, ES256} + cbs := TestConvertToUrls(t, "https://www.alice.com/callback")[0] + auds := []string{"alice-rp", "bob-rp"} + cert1, pem1 := testGenerateCA(t, "localhost") + cert2, pem2 := testGenerateCA(t, "localhost") + certs := []*x509.Certificate{cert1, cert2} + pems := []string{pem1, pem2} + am, err := NewAuthMethod( + org.PublicId, + "alice-rp", + "alice-secret", WithAudClaims("alice-rp"), + WithAudClaims(auds...), + WithIssuer(TestConvertToUrls(t, "https://www.alice.com")[0]), + WithApiUrl(cbs), + WithSigningAlgs(algs...), + WithCertificates(certs...), + WithName("alice's restaurant"), + WithDescription("it's a good place to eat"), + ) + require.NoError(t, err) + require.Equal(t, am.SigningAlgs, convertAlg(algs...)) + require.Equal(t, am.ApiUrl, cbs.String()) + require.Equal(t, "https://www.alice.com", am.Issuer) + require.Equal(t, am.AudClaims, auds) + require.Equal(t, am.Certificates, pems) + require.Equal(t, am.OperationalState, string(InactiveState)) + return am + }, + }, + { + name: "valid with custom ID", + am: func(t *testing.T) *AuthMethod { + algs := []Alg{RS256, ES256} + cbs := TestConvertToUrls(t, "https://www.alice.com/callback")[0] + auds := []string{"alice-rp-custom", "bob-rp-custom"} + cert1, pem1 := testGenerateCA(t, "localhost") + cert2, pem2 := testGenerateCA(t, "localhost") + certs := []*x509.Certificate{cert1, cert2} + pems := []string{pem1, pem2} + am, err := NewAuthMethod( + org.PublicId, + "alice-rp-custom", + "alice-secret-custom", WithAudClaims("alice-rp-custom"), + WithAudClaims(auds...), + WithIssuer(TestConvertToUrls(t, "https://www.alice.com")[0]), + WithApiUrl(cbs), + WithSigningAlgs(algs...), + WithCertificates(certs...), + WithName("alice's restaurant with a twist"), + WithDescription("it's an okay but kinda weird place to eat"), + ) + require.NoError(t, err) + require.Equal(t, am.SigningAlgs, convertAlg(algs...)) + require.Equal(t, am.ApiUrl, cbs.String()) + require.Equal(t, "https://www.alice.com", am.Issuer) + require.Equal(t, am.AudClaims, auds) + require.Equal(t, am.Certificates, pems) + require.Equal(t, am.OperationalState, string(InactiveState)) + return am + }, + opt: []Option{WithPublicId("amoidc_1234567890")}, + }, + { + name: "bad custom ID", + am: func(t *testing.T) *AuthMethod { + algs := []Alg{RS256, ES256} + cbs := TestConvertToUrls(t, "https://www.alice.com/callback")[0] + auds := []string{"alice-rp-bad", "bob-rp-bad"} + cert1, pem1 := testGenerateCA(t, "localhost") + cert2, pem2 := testGenerateCA(t, "localhost") + certs := []*x509.Certificate{cert1, cert2} + pems := []string{pem1, pem2} + am, err := NewAuthMethod( + org.PublicId, + "alice-rp-bad", + "alice-secret-bad", WithAudClaims("alice-rp-bad"), + WithAudClaims(auds...), + WithIssuer(TestConvertToUrls(t, "https://www.alice.com")[0]), + WithApiUrl(cbs), + WithSigningAlgs(algs...), + WithCertificates(certs...), + WithName("alice's restaurant is bad"), + WithDescription("their food is awful"), + ) + require.NoError(t, err) + require.Equal(t, am.SigningAlgs, convertAlg(algs...)) + require.Equal(t, am.ApiUrl, cbs.String()) + require.Equal(t, "https://www.alice.com", am.Issuer) + require.Equal(t, am.AudClaims, auds) + require.Equal(t, am.Certificates, pems) + require.Equal(t, am.OperationalState, string(InactiveState)) + return am + }, + opt: []Option{WithPublicId("amoic_1234567890")}, + wantErrMatch: errors.T(errors.InvalidParameter), + }, + { + name: "bad-state", + am: func(t *testing.T) *AuthMethod { + am, err := NewAuthMethod(org.PublicId, "bad-state-rp", "alice-secret", + WithAudClaims("alice-rp"), WithApiUrl(TestConvertToUrls(t, "https://api.com")[0])) + require.NoError(t, err) + am.OperationalState = "not-a-valid-state" + return am + }, + wantErrMatch: errors.T(errors.InvalidParameter), + }, + { + name: "missing-auth-method", + am: func(t *testing.T) *AuthMethod { + return nil + }, + wantErrMatch: errors.T(errors.InvalidParameter), + }, + { + name: "bad-public-id", + am: func(t *testing.T) *AuthMethod { + id, err := newAuthMethodId() + require.NoError(t, err) + am := AllocAuthMethod() + am.PublicId = id + return &am + }, + wantErrMatch: errors.T(errors.InvalidParameter), + }, + { + name: "bad-version", + am: func(t *testing.T) *AuthMethod { + am := AllocAuthMethod() + am.Version = 22 + return &am + }, + wantErrMatch: errors.T(errors.InvalidParameter), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + repo, err := NewRepository(rw, rw, kmsCache) + assert.NoError(err) + require.NotNil(repo) + am := tt.am(t) + got, err := repo.CreateAuthMethod(ctx, am, tt.opt...) + if tt.wantErrMatch != nil { + require.Error(err) + assert.Truef(errors.Match(tt.wantErrMatch, err), "want err code: %q got: %q", tt.wantErrMatch, err) + assert.Nil(got) + + if am != nil { + err := db.TestVerifyOplog(t, rw, am.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_CREATE), db.WithCreateNotBefore(10*time.Second)) + require.Errorf(err, "should not have found oplog entry for %s", am.PublicId) + } + return + } + require.NoError(err) + if tt.opt != nil { + if opts := getOpts(tt.opt...); opts.withPublicId != "" { + require.Equal(opts.withPublicId, got.PublicId) + } + } + am.PublicId = got.PublicId + am.CreateTime = got.CreateTime + am.UpdateTime = got.UpdateTime + am.Version = got.Version + assert.Truef(proto.Equal(am.AuthMethod, got.AuthMethod), "got %+v expected %+v", got, tt.am) + + err = db.TestVerifyOplog(t, rw, am.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_CREATE), db.WithCreateNotBefore(10*time.Second)) + require.NoErrorf(err, "unexpected error verifying oplog entry: %s", err) + }) + } +} diff --git a/internal/auth/oidc/repository_auth_method_delete.go b/internal/auth/oidc/repository_auth_method_delete.go new file mode 100644 index 000000000..47e12dc0a --- /dev/null +++ b/internal/auth/oidc/repository_auth_method_delete.go @@ -0,0 +1,56 @@ +package oidc + +import ( + "context" + "fmt" + + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/oplog" +) + +// DeleteAuthMethod will delete the auth method from the repository. It is +// idempotent so if the auth method was not found, return 0 (no rows affected) +// and nil. No options are currently supported. +func (r *Repository) DeleteAuthMethod(ctx context.Context, publicId string, _ ...Option) (int, error) { + const op = "oidc.(Repository).DeleteAuthMethod" + if publicId == "" { + return db.NoRowsAffected, errors.New(errors.InvalidPublicId, op, "missing public id") + } + am, err := r.LookupAuthMethod(ctx, publicId) + if err != nil { + return db.NoRowsAffected, errors.Wrap(err, op) + } + if am == nil { + // already deleted and this is not an error. + return db.NoRowsAffected, nil + } + + oplogWrapper, err := r.kms.GetWrapper(ctx, am.ScopeId, kms.KeyPurposeOplog) + if err != nil { + return db.NoRowsAffected, errors.Wrap(err, op, errors.WithMsg("unable to get oplog wrapper")) + } + metadata := am.oplog(oplog.OpType_OP_TYPE_DELETE) + var rowsDeleted int + _, err = r.writer.DoTx( + ctx, + db.StdRetryCnt, + db.ExpBackoff{}, + func(_ db.Reader, w db.Writer) error { + cp := am.Clone() + rowsDeleted, err = w.Delete(ctx, cp, db.WithOplog(oplogWrapper, metadata)) + if err != nil { + return err + } + if rowsDeleted > 1 { + return errors.New(errors.MultipleRecords, op, "more than 1 auth method would have been deleted") + } + return nil + }, + ) + if err != nil { + return db.NoRowsAffected, errors.Wrap(err, op, errors.WithMsg(fmt.Sprintf("unable to delete %s", publicId))) + } + return rowsDeleted, nil +} diff --git a/internal/auth/oidc/repository_auth_method_delete_test.go b/internal/auth/oidc/repository_auth_method_delete_test.go new file mode 100644 index 000000000..ffa3badc6 --- /dev/null +++ b/internal/auth/oidc/repository_auth_method_delete_test.go @@ -0,0 +1,88 @@ +package oidc + +import ( + "context" + "testing" + "time" + + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/oplog" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRepository_DeleteAuthMethod(t *testing.T) { + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, wrapper) + ctx := context.Background() + + tests := []struct { + name string + authMethod *AuthMethod + wantRowsDeleted int + wantErrMatch *errors.Template + }{ + { + name: "valid", + authMethod: func() *AuthMethod { + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + databaseWrapper, err := kmsCache.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + return TestAuthMethod(t, conn, databaseWrapper, org.PublicId, InactiveState, "alice_rp", "alices-dogs-name", + WithIssuer(TestConvertToUrls(t, "https://alice.com")[0]), WithApiUrl(TestConvertToUrls(t, "https://api.com")[0])) + }(), + wantRowsDeleted: 1, + }, + { + name: "no-public-id", + authMethod: func() *AuthMethod { am := AllocAuthMethod(); return &am }(), + wantErrMatch: errors.T(errors.InvalidPublicId), + }, + { + name: "not-found", + authMethod: func() *AuthMethod { + am := AllocAuthMethod() + var err error + am.PublicId, err = newAuthMethodId() + require.NoError(t, err) + return &am + }(), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + repo, err := NewRepository(rw, rw, kmsCache) + require.NoError(err) + deletedRows, err := repo.DeleteAuthMethod(ctx, tt.authMethod.PublicId) + if tt.wantErrMatch != nil { + require.Error(err) + + assert.Truef(errors.Match(tt.wantErrMatch, err), "want err: %q got: %q", tt.wantErrMatch.Msg, err) + + assert.Equalf(0, deletedRows, "expected 0 deleted rows and got %d", deletedRows) + + err := db.TestVerifyOplog(t, rw, tt.authMethod.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_DELETE), db.WithCreateNotBefore(10*time.Second)) + require.Errorf(err, "should not have found oplog entry for %s", tt.authMethod.PublicId) + assert.Truef(errors.Match(errors.T(errors.RecordNotFound), err), "expected error code %s and got %s", errors.RecordNotFound, err) + + return + } + require.NoError(err) + assert.Equalf(tt.wantRowsDeleted, deletedRows, "expected rows deleted == %d and got %d", tt.wantRowsDeleted, deletedRows) + + if tt.wantRowsDeleted > 0 { + err = db.TestVerifyOplog(t, rw, tt.authMethod.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_DELETE), db.WithCreateNotBefore(10*time.Second)) + require.NoErrorf(err, "unexpected error verifying oplog entry: %s", err) + } + found, err := repo.LookupAuthMethod(ctx, tt.authMethod.PublicId) + require.NoError(err) + assert.Nil(found) + }) + } +} diff --git a/internal/auth/oidc/repository_auth_method_operational_state.go b/internal/auth/oidc/repository_auth_method_operational_state.go new file mode 100644 index 000000000..257acb190 --- /dev/null +++ b/internal/auth/oidc/repository_auth_method_operational_state.go @@ -0,0 +1,137 @@ +package oidc + +import ( + "context" + "fmt" + + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/oplog" +) + +// MakeInactive will transision an OIDC auth method from either the +// ActivePrivateState or the ActivePublicState to the InactiveState. +// No options are supported. +func (r *Repository) MakeInactive(ctx context.Context, authMethodId string, version uint32, _ ...Option) (*AuthMethod, error) { + const op = "oidc.(Repository).MakeInactive" + updated, err := r.transitionAuthMethodTo(ctx, authMethodId, InactiveState, version) + if err != nil { + return nil, errors.Wrap(err, op) + } + return updated, nil +} + +// MakePrivate will transision an OIDC auth method from either the +// InactiveState or the ActivePublicState to the ActivePrivateState. If +// transitioning from the InactiveState, the transition will only succeed if +// the auth method is complete and the oidc.ValidateAuthMethod(...) succeeds. +// If the WithForce option is provided, oidc.ValidateAuthMethod(...) success +// is not required. +func (r *Repository) MakePrivate(ctx context.Context, authMethodId string, version uint32, opt ...Option) (*AuthMethod, error) { + const op = "oidc.(Repository).MakePrivate" + updated, err := r.transitionAuthMethodTo(ctx, authMethodId, ActivePrivateState, version, opt...) + if err != nil { + return nil, errors.Wrap(err, op) + } + return updated, nil +} + +// MakePublic will transision an OIDC auth method from either the +// InactiveState or the ActivePrivateState to the ActivePublicState. If +// transitioning from the InactiveState, the transition will only succeed if +// the auth method is complete and the oidc.ValidateAuthMethod(...) succeeds. +// If the WithForce option is provided, oidc.ValidateAuthMethod(...) success +// is not required. +func (r *Repository) MakePublic(ctx context.Context, authMethodId string, version uint32, opt ...Option) (*AuthMethod, error) { + const op = "oidc.(Repository).MakePublic" + updated, err := r.transitionAuthMethodTo(ctx, authMethodId, ActivePublicState, version, opt...) + if err != nil { + return nil, errors.Wrap(err, op) + } + return updated, nil +} + +func (r *Repository) transitionAuthMethodTo(ctx context.Context, authMethodId string, desiredState AuthMethodState, version uint32, opt ...Option) (*AuthMethod, error) { + const op = "oidc.(Repository).transitionAuthMethodTo" + if authMethodId == "" { + return nil, errors.New(errors.InvalidParameter, op, "missing auth method id") + } + if !validState(string(desiredState)) { + return nil, errors.New(errors.InvalidParameter, op, fmt.Sprintf("%s is not a valid auth method state", desiredState)) + } + am, err := r.lookupAuthMethod(ctx, authMethodId) + if err != nil { + return nil, errors.Wrap(err, op) + } + if am == nil { + return nil, errors.New(errors.RecordNotFound, op, fmt.Sprintf("%s auth method not found", authMethodId)) + } + opts := getOpts(opt...) + if am.OperationalState == string(desiredState) && am.DisableDiscoveredConfigValidation == opts.withForce { + return am, nil + } + if am.OperationalState == string(InactiveState) { + if !opts.withForce { + updatedAm := am.Clone() + updatedAm.OperationalState = string(desiredState) + if err := r.ValidateDiscoveryInfo(ctx, WithAuthMethod(updatedAm)); err != nil { + return nil, errors.Wrap(err, op) + } + } + if err := am.isComplete(); err != nil { + return nil, errors.Wrap(err, op, errors.WithMsg(fmt.Sprintf("unable to transition from %s to %s", InactiveState, desiredState))) + } + } + + oplogWrapper, err := r.kms.GetWrapper(ctx, am.ScopeId, kms.KeyPurposeOplog) + if err != nil { + return nil, errors.Wrap(err, op, errors.WithMsg("unable to get oplog wrapper")) + } + + var updatedAm *AuthMethod + _, err = r.writer.DoTx( + ctx, + db.StdRetryCnt, + db.ExpBackoff{}, + func(reader db.Reader, w db.Writer) error { + updatedAm = am.Clone() + updatedAm.OperationalState = string(desiredState) + updatedAm.DisableDiscoveredConfigValidation = opts.withForce + dbMask := []string{"OperationalState", "DisableDiscoveredConfigValidation"} + rowsUpdated, err := w.Update(ctx, updatedAm, dbMask, nil, db.WithOplog(oplogWrapper, updatedAm.oplog(oplog.OpType_OP_TYPE_UPDATE)), db.WithVersion(&version)) + switch { + case err != nil: + return errors.Wrap(err, op, errors.WithMsg("unable to update auth method")) + case err == nil && rowsUpdated > 1: + return errors.New(errors.MultipleRecords, op, fmt.Sprintf("updated auth method and %d rows updated", rowsUpdated)) + case err == nil && rowsUpdated == 0: + // this is different than how "no rows updated" is handled in + // the typical update pattern since we are not returning the + // number of rows updated, we need to raise an error here. + return errors.New(errors.RecordNotFound, op, fmt.Sprintf("updated auth method and %d rows updated", rowsUpdated)) + default: + } + // we need a new repo, that's using the same reader/writer as this TxHandler + txRepo := &Repository{ + reader: reader, + writer: w, + kms: r.kms, + // intentionally not setting the defaultLimit, so we'll get all + // the account ids without a limit + } + updatedAm, err = txRepo.lookupAuthMethod(ctx, updatedAm.PublicId) + if err != nil { + return errors.Wrap(err, op, errors.WithMsg("unable to lookup auth method after update")) + } + if updatedAm == nil { + return errors.New(errors.RecordNotFound, op, "unable to lookup auth method after update") + } + return err + }, + ) + if err != nil { + return nil, errors.Wrap(err, op) + } + return updatedAm, nil +} diff --git a/internal/auth/oidc/repository_auth_method_operational_state_test.go b/internal/auth/oidc/repository_auth_method_operational_state_test.go new file mode 100644 index 000000000..23b598915 --- /dev/null +++ b/internal/auth/oidc/repository_auth_method_operational_state_test.go @@ -0,0 +1,420 @@ +package oidc + +import ( + "context" + "testing" + "time" + + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/oplog" + "github.com/hashicorp/cap/oidc" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_MakeInactive_MakePrivate_MakePublic(t *testing.T) { + t.Parallel() + ctx := context.Background() + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, wrapper) + + tp := oidc.StartTestProvider(t) + tpClientId := "alice-rp" + tpClientSecret := "her-dog's-name" + tp.SetClientCreds(tpClientId, tpClientSecret) + _, _, tpAlg, _ := tp.SigningKeys() + tpCert, err := ParseCertificates(tp.CACert()) + require.NoError(t, err) + require.Equal(t, 1, len(tpCert)) + + rw := db.New(conn) + repo, err := NewRepository(rw, rw, kmsCache) + require.NoError(t, err) + + tests := []struct { + name string + toState AuthMethodState + operateOn string + opt []Option + version uint32 + wantNoRowsUpdated bool + wantErrMatch *errors.Template + wantErrContains string + wantNoOplog bool + }{ + { + name: "ActivePrivate-to-InActive", + toState: InactiveState, + operateOn: func() string { + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + databaseWrapper, err := kmsCache.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + return TestAuthMethod(t, + conn, databaseWrapper, + org.PublicId, + ActivePrivateState, + "alice-rp", "alice-secret", + WithCertificates(tpCert[0]), + WithSigningAlgs(Alg(tpAlg)), + WithApiUrl(TestConvertToUrls(t, "https://www.alice.com/callback")[0]), + ).PublicId + }(), + version: 2, + }, + { + name: "ActivePublic-to-InActive", + toState: InactiveState, + operateOn: func() string { + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + databaseWrapper, err := kmsCache.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + return TestAuthMethod(t, + conn, databaseWrapper, + org.PublicId, + ActivePublicState, + "alice-rp", "alice-secret", + WithCertificates(tpCert[0]), + WithSigningAlgs(Alg(tpAlg)), + WithApiUrl(TestConvertToUrls(t, "https://www.alice.com/callback")[0]), + ).PublicId + }(), + version: 2, + }, + { + name: "Inactive-to-Inactive", + toState: InactiveState, + operateOn: func() string { + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + databaseWrapper, err := kmsCache.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + return TestAuthMethod(t, + conn, databaseWrapper, + org.PublicId, + InactiveState, + "alice-rp", "alice-secret", + WithCertificates(tpCert[0]), + WithSigningAlgs(Alg(tpAlg)), + WithApiUrl(TestConvertToUrls(t, "https://www.alice.com/callback")[0]), + ).PublicId + }(), + version: 1, + wantNoOplog: true, + wantNoRowsUpdated: true, + }, + { + name: "InActive-to-ActivePrivate", + toState: ActivePrivateState, + operateOn: func() string { + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + databaseWrapper, err := kmsCache.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + return TestAuthMethod(t, + conn, databaseWrapper, + org.PublicId, + InactiveState, + "alice-rp", "alice-secret", + WithIssuer(TestConvertToUrls(t, tp.Addr())[0]), + WithCertificates(tpCert[0]), + WithSigningAlgs(Alg(tpAlg)), + WithApiUrl(TestConvertToUrls(t, "https://www.alice.com/callback")[0]), + ).PublicId + }(), + version: 1, + }, + { + name: "ActivePublic-to-ActivePrivate", + toState: ActivePrivateState, + operateOn: func() string { + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + databaseWrapper, err := kmsCache.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + return TestAuthMethod(t, + conn, databaseWrapper, + org.PublicId, + ActivePublicState, + "alice-rp", "alice-secret", + WithIssuer(TestConvertToUrls(t, tp.Addr())[0]), + WithCertificates(tpCert[0]), + WithSigningAlgs(Alg(tpAlg)), + WithApiUrl(TestConvertToUrls(t, "https://www.alice.com/callback")[0]), + ).PublicId + }(), + version: 2, + }, + { + name: "ActivePrivate-to-ActivePrivate", + toState: ActivePrivateState, + operateOn: func() string { + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + databaseWrapper, err := kmsCache.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + return TestAuthMethod(t, + conn, databaseWrapper, + org.PublicId, + ActivePrivateState, + "alice-rp", "alice-secret", + WithIssuer(TestConvertToUrls(t, tp.Addr())[0]), + WithCertificates(tpCert[0]), + WithSigningAlgs(Alg(tpAlg)), + WithApiUrl(TestConvertToUrls(t, "https://www.alice.com/callback")[0]), + ).PublicId + }(), + version: 2, + wantNoRowsUpdated: true, + wantNoOplog: true, + }, + { + name: "InActive-to-ActivePublic", + toState: ActivePublicState, + operateOn: func() string { + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + databaseWrapper, err := kmsCache.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + return TestAuthMethod(t, + conn, databaseWrapper, + org.PublicId, + InactiveState, + "alice-rp", "alice-secret", + WithIssuer(TestConvertToUrls(t, tp.Addr())[0]), + WithCertificates(tpCert[0]), + WithSigningAlgs(Alg(tpAlg)), + WithApiUrl(TestConvertToUrls(t, "https://www.alice.com/callback")[0]), + ).PublicId + }(), + version: 1, + }, + { + name: "ActivePrivate-to-ActivePublic", + toState: ActivePublicState, + operateOn: func() string { + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + databaseWrapper, err := kmsCache.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + return TestAuthMethod(t, + conn, databaseWrapper, + org.PublicId, + ActivePrivateState, + "alice-rp", "alice-secret", + WithCertificates(tpCert[0]), + WithSigningAlgs(Alg(tpAlg)), + WithApiUrl(TestConvertToUrls(t, "https://www.alice.com/callback")[0]), + ).PublicId + }(), + version: 2, + }, + { + name: "ActivePublic-to-ActivePublic", + toState: ActivePublicState, + operateOn: func() string { + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + databaseWrapper, err := kmsCache.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + return TestAuthMethod(t, + conn, databaseWrapper, + org.PublicId, + ActivePublicState, + "alice-rp", "alice-secret", + WithCertificates(tpCert[0]), + WithSigningAlgs(Alg(tpAlg)), + WithApiUrl(TestConvertToUrls(t, "https://www.alice.com/callback")[0]), + ).PublicId + }(), + version: 2, + wantNoRowsUpdated: true, + wantNoOplog: true, + }, + { + name: "bad-version", + toState: InactiveState, + operateOn: func() string { + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + databaseWrapper, err := kmsCache.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + return TestAuthMethod(t, + conn, databaseWrapper, + org.PublicId, + ActivePrivateState, + "alice-rp", "alice-secret", + WithCertificates(tpCert[0]), + WithSigningAlgs(Alg(tpAlg)), + WithApiUrl(TestConvertToUrls(t, "https://www.alice.com/callback")[0]), + ).PublicId + }(), + version: 111111, + wantErrMatch: errors.T(errors.RecordNotFound), + wantErrContains: "updated auth method and 0 rows updated", + }, + { + name: "missing-auth-method-id", + toState: InactiveState, + operateOn: "", + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "missing auth method id", + }, + { + name: "not-found", + toState: InactiveState, + operateOn: "not-found-auth-method-id", + wantErrMatch: errors.T(errors.RecordNotFound), + wantErrContains: "auth method not found", + }, + { + name: "error-for-InActive-to-ActivePrivate", + toState: ActivePrivateState, + operateOn: func() string { + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + databaseWrapper, err := kmsCache.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + // the returned oidc auth method is incomplete + return TestAuthMethod(t, + conn, databaseWrapper, + org.PublicId, + InactiveState, + "alice-rp", "alice-secret", + WithIssuer(TestConvertToUrls(t, tp.Addr())[0]), + WithApiUrl(TestConvertToUrls(t, "https://api.com")[0]), + ).PublicId + }(), + opt: []Option{WithForce()}, + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "unable to transition from inactive", + }, + { + name: "error-for-InActive-to-ActivePublic", + toState: ActivePublicState, + operateOn: func() string { + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + databaseWrapper, err := kmsCache.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + // the returned oidc auth method is incomplete + return TestAuthMethod(t, + conn, databaseWrapper, + org.PublicId, + InactiveState, + "alice-rp", "alice-secret", + WithIssuer(TestConvertToUrls(t, tp.Addr())[0]), + WithApiUrl(TestConvertToUrls(t, "https://api.com")[0]), + ).PublicId + }(), + opt: []Option{WithForce()}, + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "unable to transition from inactive", + }, + { + name: "InActive-to-ActivePublic-no-force", + toState: ActivePublicState, + operateOn: func() string { + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + databaseWrapper, err := kmsCache.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + return TestAuthMethod(t, + conn, databaseWrapper, + org.PublicId, + InactiveState, + "alice-rp", "alice-secret", + WithCertificates(tpCert[0]), + WithSigningAlgs(ES512), // won't match discovery info returned + WithApiUrl(TestConvertToUrls(t, "https://www.alice.com/callback")[0]), + WithIssuer(TestConvertToUrls(t, tp.Addr())[0]), + ).PublicId + }(), + version: 1, + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "auth method signing alg is not in discovered", + }, + { + name: "InActive-to-ActivePublic-with-force", + toState: ActivePublicState, + operateOn: func() string { + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + databaseWrapper, err := kmsCache.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + return TestAuthMethod(t, + conn, databaseWrapper, + org.PublicId, + InactiveState, + "alice-rp", "alice-secret", + WithIssuer(TestConvertToUrls(t, tp.Addr())[0]), + WithCertificates(tpCert[0]), + WithSigningAlgs(ES512), // won't match discovery info returned + WithApiUrl(TestConvertToUrls(t, "https://www.alice.com/callback")[0]), + ).PublicId + }(), + version: 1, + opt: []Option{WithForce()}, + }, + { + name: "bad cert InActive-to-ActivePublic", + toState: ActivePublicState, + operateOn: func() string { + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + databaseWrapper, err := kmsCache.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + return TestAuthMethod(t, + conn, databaseWrapper, + org.PublicId, + InactiveState, + "alice-rp", "alice-secret", + WithIssuer(TestConvertToUrls(t, tp.Addr())[0]), + WithSigningAlgs(Alg(tpAlg)), // won't match discovery info returned + WithApiUrl(TestConvertToUrls(t, "https://www.alice.com/callback")[0]), + ).PublicId + }(), + version: 1, + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "certificate signed by unknown authority", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + var err error + var updated *AuthMethod + switch tt.toState { + case InactiveState: + updated, err = repo.MakeInactive(ctx, tt.operateOn, tt.version, tt.opt...) + case ActivePrivateState: + updated, err = repo.MakePrivate(ctx, tt.operateOn, tt.version, tt.opt...) + case ActivePublicState: + updated, err = repo.MakePublic(ctx, tt.operateOn, tt.version, tt.opt...) + default: + require.Fail("unknown toState %s for test", tt.toState) + } + if tt.wantErrMatch != nil { + require.Error(err) + assert.Nil(updated) + assert.Truef(errors.Match(tt.wantErrMatch, err), "want err code: %q got: %q", tt.wantErrMatch.Code, err) + if tt.wantErrContains != "" { + assert.Contains(err.Error(), tt.wantErrContains) + } + + err := db.TestVerifyOplog(t, rw, tt.operateOn, db.WithOperation(oplog.OpType_OP_TYPE_UPDATE), db.WithCreateNotBefore(10*time.Second)) + require.Errorf(err, "should not have found oplog entry for %s", tt.operateOn) + return + } + require.NoError(err) + require.NotNil(updated) + + found, err := repo.LookupAuthMethod(ctx, tt.operateOn) + require.NoError(err) + require.NotEmpty(found) + if !tt.wantNoRowsUpdated { + assert.Equal(updated.OperationalState, string(tt.toState)) + assert.Equal(string(tt.toState), found.OperationalState) + opts := getOpts(tt.opt...) + if opts.withForce { + assert.Equal(true, updated.DisableDiscoveredConfigValidation) + } + } + if !tt.wantNoOplog { + err = db.TestVerifyOplog(t, rw, tt.operateOn, db.WithOperation(oplog.OpType_OP_TYPE_UPDATE), db.WithCreateNotBefore(10*time.Second)) + require.NoErrorf(err, "unexpected error verifying oplog entry: %s", err) + } + }) + } +} diff --git a/internal/auth/oidc/repository_auth_method_read.go b/internal/auth/oidc/repository_auth_method_read.go new file mode 100644 index 000000000..1701842aa --- /dev/null +++ b/internal/auth/oidc/repository_auth_method_read.go @@ -0,0 +1,193 @@ +package oidc + +import ( + "context" + "fmt" + "strings" + + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/db/timestamp" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/go-kms-wrapping/structwrapping" +) + +// LookupAuthMethod will lookup an auth method in the repo, along with its +// associated Value Objects of SigningAlgs, CallbackUrls, AudClaims and +// Certificates. If it's not found, it will return nil, nil. The +// WithUnauthenticatedUser options is supported and all other options are +// ignored. +func (r *Repository) LookupAuthMethod(ctx context.Context, publicId string, opt ...Option) (*AuthMethod, error) { + const op = "oidc.(Repository).LookupAuthMethod" + if publicId == "" { + return nil, errors.New(errors.InvalidParameter, op, "missing public id") + } + opts := getOpts(opt...) + return r.lookupAuthMethod(ctx, publicId, WithUnauthenticatedUser(opts.withUnauthenticatedUser)) +} + +// ListAuthMethods returns a slice of AuthMethods for the scopeId. The +// WithUnauthenticatedUser, WithLimit and WithOrder options are supported and +// all other options are ignored. +func (r *Repository) ListAuthMethods(ctx context.Context, scopeIds []string, opt ...Option) ([]*AuthMethod, error) { + const op = "oidc.(Repository).ListAuthMethods" + if len(scopeIds) == 0 { + return nil, errors.New(errors.InvalidParameter, op, "missing scope IDs") + } + authMethods, err := r.getAuthMethods(ctx, "", scopeIds, opt...) + if err != nil { + return nil, errors.Wrap(err, op) + } + return authMethods, nil +} + +// lookupAuthMethod will lookup a single auth method +func (r *Repository) lookupAuthMethod(ctx context.Context, authMethodId string, opt ...Option) (*AuthMethod, error) { + const op = "oidc.(Repository).lookupAuthMethod" + var err error + ams, err := r.getAuthMethods(ctx, authMethodId, nil, opt...) + if err != nil { + return nil, errors.Wrap(err, op) + } + switch { + case len(ams) == 0: + return nil, nil // not an error to return no rows for a "lookup" + case len(ams) > 1: + return nil, errors.New(errors.NotSpecificIntegrity, op, fmt.Sprintf("%s matched more than 1 ", authMethodId)) + default: + return ams[0], nil + } +} + +// getAuthMethods allows the caller to either lookup a specific AuthMethod via +// its id or search for a set AuthMethods within a set of scopes. Passing both +// scopeIds and a authMethodId is an error. The WithUnauthenticatedUser, +// WithLimit and WithOrder options are supported and all other options are +// ignored. +// +// The AuthMethod returned has its value objects populated (SigningAlgs, +// CallbackUrls, AudClaims and Certificates). The AuthMethod returned has its +// IsPrimaryAuthMethod bool set. +// +// When no record is found it returns nil, nil +func (r *Repository) getAuthMethods(ctx context.Context, authMethodId string, scopeIds []string, opt ...Option) ([]*AuthMethod, error) { + const op = "oidc.(Repository).getAuthMethods" + if authMethodId == "" && len(scopeIds) == 0 { + return nil, errors.New(errors.InvalidParameter, op, "missing search criteria: both auth method id and Scope IDs are empty") + } + if authMethodId != "" && len(scopeIds) > 0 { + return nil, errors.New(errors.InvalidParameter, op, "searching for both an auth method id and Scope IDs is not supported") + } + + const aggregateDelimiter = "|" + + dbArgs := []db.Option{} + opts := getOpts(opt...) + limit := r.defaultLimit + if opts.withLimit != 0 { + // non-zero signals an override of the default limit for the repo. + limit = opts.withLimit + } + dbArgs = append(dbArgs, db.WithLimit(limit)) + + if opts.withOrderByCreateTime { + if opts.ascending { + dbArgs = append(dbArgs, db.WithOrder("create_time asc")) + } else { + dbArgs = append(dbArgs, db.WithOrder("create_time")) + } + } + + var args []interface{} + var where []string + switch { + case authMethodId != "": + where, args = append(where, "public_id = ?"), append(args, authMethodId) + default: + where, args = append(where, "scope_id in(?)"), append(args, scopeIds) + } + + if opts.withUnauthenticatedUser { + where, args = append(where, "state = ?"), append(args, string(ActivePublicState)) + } + + var aggAuthMethods []*authMethodAgg + err := r.reader.SearchWhere(ctx, &aggAuthMethods, strings.Join(where, " and "), args, dbArgs...) + if err != nil { + return nil, errors.Wrap(err, op) + } + + if len(aggAuthMethods) == 0 { // we're done if nothing is found. + return nil, nil + } + + authMethods := make([]*AuthMethod, 0, len(aggAuthMethods)) + for _, agg := range aggAuthMethods { + databaseWrapper, err := r.kms.GetWrapper(ctx, agg.ScopeId, kms.KeyPurposeDatabase, kms.WithKeyId(agg.KeyId)) + if err != nil { + return nil, errors.Wrap(err, op, errors.WithMsg("unable to get database wrapper")) + } + if err := structwrapping.UnwrapStruct(ctx, databaseWrapper, agg, nil); err != nil { + return nil, errors.Wrap(err, op, errors.WithCode(errors.Decrypt)) + } + am := AllocAuthMethod() + am.PublicId = agg.PublicId + am.ScopeId = agg.ScopeId + am.IsPrimaryAuthMethod = agg.IsPrimaryAuthMethod + am.Name = agg.Name + am.Description = agg.Description + am.CreateTime = agg.CreateTime + am.UpdateTime = agg.UpdateTime + am.Version = agg.Version + am.OperationalState = agg.State + am.DisableDiscoveredConfigValidation = agg.DisableDiscoveredConfigValidation + am.Issuer = agg.Issuer + am.ClientId = agg.ClientId + am.CtClientSecret = agg.CtClientSecret + am.ClientSecret = agg.ClientSecret + am.ClientSecretHmac = agg.ClientSecretHmac + am.KeyId = agg.KeyId + am.MaxAge = int32(agg.MaxAge) + am.ApiUrl = agg.ApiUrl + if agg.Algs != "" { + am.SigningAlgs = strings.Split(agg.Algs, aggregateDelimiter) + } + if agg.Auds != "" { + am.AudClaims = strings.Split(agg.Auds, aggregateDelimiter) + } + if agg.Certs != "" { + am.Certificates = strings.Split(agg.Certs, aggregateDelimiter) + } + authMethods = append(authMethods, &am) + } + return authMethods, nil +} + +// authMethodAgg is a view that aggregates the auth method's value objects in to +// string fields delimited with the aggregateDelimiter of "|" +type authMethodAgg struct { + PublicId string `gorm:"primary_key"` + ScopeId string + IsPrimaryAuthMethod bool + Name string + Description string + CreateTime *timestamp.Timestamp + UpdateTime *timestamp.Timestamp + Version uint32 + State string + DisableDiscoveredConfigValidation bool + Issuer string + ClientId string + CtClientSecret []byte `gorm:"column:client_secret;not_null" wrapping:"ct,client_secret"` + ClientSecret string `gorm:"-" wrapping:"pt,client_secret"` + ClientSecretHmac string + KeyId string + MaxAge int + Algs string + ApiUrl string + Auds string + Certs string +} + +// TableName returns the table name for gorm +func (agg *authMethodAgg) TableName() string { return "oidc_auth_method_with_value_obj" } diff --git a/internal/auth/oidc/repository_auth_method_read_test.go b/internal/auth/oidc/repository_auth_method_read_test.go new file mode 100644 index 000000000..7398bf746 --- /dev/null +++ b/internal/auth/oidc/repository_auth_method_read_test.go @@ -0,0 +1,354 @@ +package oidc + +import ( + "context" + "sort" + "testing" + + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/kms" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRepository_LookupAuthMethod(t *testing.T) { + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, wrapper) + ctx := context.Background() + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + databaseWrapper, err := kmsCache.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + amInactive := TestAuthMethod(t, conn, databaseWrapper, org.PublicId, InactiveState, "alice_rp", "alices-dogs-name", + WithIssuer(TestConvertToUrls(t, "https://alice-inactive.com")[0]), WithApiUrl(TestConvertToUrls(t, "https://api.com")[0])) + amActivePriv := TestAuthMethod( + t, + conn, databaseWrapper, org.PublicId, ActivePrivateState, + "alice_rp", "alices-dogs-name", + WithApiUrl(TestConvertToUrls(t, "https://alice-active-priv.com/callback")[0]), + WithSigningAlgs(RS256)) + amActivePub := TestAuthMethod( + t, + conn, databaseWrapper, org.PublicId, ActivePublicState, + "alice_rp", "alices-dogs-name", + WithApiUrl(TestConvertToUrls(t, "https://alice-active-pub.com/callback")[0]), + WithSigningAlgs(RS256)) + amActivePub.IsPrimaryAuthMethod = true + iam.TestSetPrimaryAuthMethod(t, iam.TestRepo(t, conn, wrapper), org, amActivePub.PublicId) + + amId, err := newAuthMethodId() + require.NoError(t, err) + tests := []struct { + name string + in string + opt []Option + want *AuthMethod + wantIsPrimary bool + wantErrMatch *errors.Template + }{ + { + name: "With no public id", + wantErrMatch: errors.T(errors.InvalidParameter), + }, + { + name: "With non existing auth method id", + in: amId, + }, + { + name: "With existing auth method id", + in: amActivePriv.GetPublicId(), + want: amActivePriv, + }, + { + name: "unauthenticated user - not found using active priv", + in: amActivePriv.GetPublicId(), + opt: []Option{WithUnauthenticatedUser(true)}, + want: nil, + }, + { + name: "unauthenticated user - found active pub", + in: amActivePub.GetPublicId(), + opt: []Option{WithUnauthenticatedUser(true)}, + want: amActivePub, + wantIsPrimary: true, + }, + { + name: "unauthenticated user - found inactive", + in: amInactive.GetPublicId(), + opt: []Option{WithUnauthenticatedUser(true)}, + want: nil, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + repo, err := NewRepository(rw, rw, kmsCache) + assert.NoError(err) + require.NotNil(repo) + got, err := repo.LookupAuthMethod(ctx, tt.in, tt.opt...) + if tt.wantErrMatch != nil { + require.Error(err) + assert.Truef(errors.Match(tt.wantErrMatch, err), "want err code: %q got: %q", tt.wantErrMatch, err) + assert.Nil(got) + return + } + require.NoError(err) + assert.EqualValues(tt.want, got) + }) + } +} + +// TestRepository_ListAuthMethods only covers error conditions, since all the +// search criteria testing is handled in the TestRepository_getAuthMethods unit +// tests. +func TestRepository_ListAuthMethods(t *testing.T) { + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, wrapper) + ctx := context.Background() + iamRepo := iam.TestRepo(t, conn, wrapper) + + tests := []struct { + name string + setupFn func() (scopeIds []string, want []*AuthMethod, wantPrimaryAuthMethodId string) + opt []Option + wantErrMatch *errors.Template + }{ + { + name: "with-limits", + setupFn: func() ([]string, []*AuthMethod, string) { + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + databaseWrapper, err := kmsCache.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + + am1a := TestAuthMethod(t, conn, databaseWrapper, org.PublicId, InactiveState, "alice_rp", "alices-dogs-name", + WithIssuer(TestConvertToUrls(t, "https://alice.com")[0]), WithApiUrl(TestConvertToUrls(t, "https://api.com")[0])) + iam.TestSetPrimaryAuthMethod(t, iamRepo, org, am1a.PublicId) + am1a.IsPrimaryAuthMethod = true + + _ = TestAuthMethod(t, conn, databaseWrapper, org.PublicId, InactiveState, "alice_rp-2", "alices-cat-name", + WithIssuer(TestConvertToUrls(t, "https://alice.com")[0]), WithApiUrl(TestConvertToUrls(t, "https://api.com")[0])) + + return []string{am1a.ScopeId}, []*AuthMethod{am1a}, am1a.PublicId + }, + opt: []Option{WithLimit(1), WithOrderByCreateTime(true)}, + }, + { + name: "no-search-criteria", + setupFn: func() ([]string, []*AuthMethod, string) { + return nil, nil, "" + }, + wantErrMatch: errors.T(errors.InvalidParameter), + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + repo, err := NewRepository(rw, rw, kmsCache) + assert.NoError(err) + scopeIds, want, wantPrimaryAuthMethodId := tt.setupFn() + + got, err := repo.ListAuthMethods(ctx, scopeIds, tt.opt...) + if tt.wantErrMatch != nil { + require.Error(err) + assert.Truef(errors.Match(tt.wantErrMatch, err), "want err code: %q got: %q", tt.wantErrMatch, err) + assert.Nil(got) + return + } + require.NoError(err) + sort.Slice(want, func(a, b int) bool { + return want[a].PublicId < want[b].PublicId + }) + sort.Slice(got, func(a, b int) bool { + return got[a].PublicId < got[b].PublicId + }) + assert.Equal(want, got) + if wantPrimaryAuthMethodId != "" { + found := false + for _, am := range got { + if am.PublicId == wantPrimaryAuthMethodId { + assert.Truef(am.IsPrimaryAuthMethod, "expected IsPrimaryAuthMethod to be true for: %s", am.PublicId) + if am.IsPrimaryAuthMethod { + found = true + } + } + } + assert.Truef(found, "expected to find primary id %s in: %+v", wantPrimaryAuthMethodId, got) + } else { + for _, am := range got { + assert.Falsef(am.IsPrimaryAuthMethod, "did not expect %s to be IsPrimaryAuthMethod", am.PublicId) + } + } + }) + } +} + +func TestRepository_getAuthMethods(t *testing.T) { + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, wrapper) + ctx := context.Background() + + tests := []struct { + name string + setupFn func() (authMethodId string, scopeIds []string, want []*AuthMethod) + opt []Option + wantErrMatch *errors.Template + }{ + { + name: "valid-multi-scopes", + setupFn: func() (string, []string, []*AuthMethod) { + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + databaseWrapper, err := kmsCache.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + org2, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + databaseWrapper2, err := kmsCache.GetWrapper(context.Background(), org2.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + cert1, _ := testGenerateCA(t, "localhost") + cert2, _ := testGenerateCA(t, "127.0.0.1") + + // make a test auth method with all options + am1a := TestAuthMethod( + t, + conn, + databaseWrapper, + org.PublicId, + InactiveState, + "alice_rp", + "alices-dogs-name", + WithAudClaims("alice_rp", "alice_rp-2"), + WithApiUrl(TestConvertToUrls(t, "https://alice.com/callback")[0]), + WithSigningAlgs(RS256, ES256), + WithCertificates(cert1, cert2), + ) + am1b := TestAuthMethod(t, conn, databaseWrapper, org.PublicId, InactiveState, "alice_rp-2", "alices-cat-name", + WithIssuer(TestConvertToUrls(t, "https://alice.com")[0]), WithApiUrl(TestConvertToUrls(t, "https://api.com")[0])) + + am2 := TestAuthMethod(t, conn, databaseWrapper2, org2.PublicId, InactiveState, "bob_rp", "bobs-dogs-name", + WithIssuer(TestConvertToUrls(t, "https://bob.com")[0]), WithApiUrl(TestConvertToUrls(t, "https://api.com")[0])) + return "", []string{am1a.ScopeId, am1b.ScopeId, am2.ScopeId}, []*AuthMethod{am1a, am1b, am2} + }, + }, + { + name: "valid-single-scopes", + setupFn: func() (string, []string, []*AuthMethod) { + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + databaseWrapper, err := kmsCache.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + am1a := TestAuthMethod(t, conn, databaseWrapper, org.PublicId, InactiveState, "alice_rp", "alices-dogs-name", + WithIssuer(TestConvertToUrls(t, "https://alice.com")[0]), WithApiUrl(TestConvertToUrls(t, "https://api.com")[0])) + am1b := TestAuthMethod(t, conn, databaseWrapper, org.PublicId, InactiveState, "alice_rp-2", "alices-cat-name", + WithIssuer(TestConvertToUrls(t, "https://alice.com")[0]), WithApiUrl(TestConvertToUrls(t, "https://api.com")[0])) + + return "", []string{am1a.ScopeId}, []*AuthMethod{am1a, am1b} + }, + }, + { + name: "valid-auth-method-id", + setupFn: func() (string, []string, []*AuthMethod) { + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + databaseWrapper, err := kmsCache.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + am := TestAuthMethod(t, conn, databaseWrapper, org.PublicId, InactiveState, "alice_rp", "alices-dogs-name", + WithIssuer(TestConvertToUrls(t, "https://alice.com")[0]), WithApiUrl(TestConvertToUrls(t, "https://api.com")[0])) + + return am.PublicId, nil, []*AuthMethod{am} + }, + }, + { + name: "with-limits", + setupFn: func() (string, []string, []*AuthMethod) { + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + databaseWrapper, err := kmsCache.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + am1a := TestAuthMethod(t, conn, databaseWrapper, org.PublicId, InactiveState, "alice_rp", "alices-dogs-name", + WithIssuer(TestConvertToUrls(t, "https://alice.com")[0]), WithApiUrl(TestConvertToUrls(t, "https://api.com")[0])) + _ = TestAuthMethod(t, conn, databaseWrapper, org.PublicId, InactiveState, "alice_rp-2", "alices-cat-name", + WithIssuer(TestConvertToUrls(t, "https://alice.com")[0]), WithApiUrl(TestConvertToUrls(t, "https://api.com")[0])) + + return "", []string{am1a.ScopeId}, []*AuthMethod{am1a} + }, + opt: []Option{WithLimit(1), WithOrderByCreateTime(true)}, + }, + { + name: "unauthenticated-user", + setupFn: func() (string, []string, []*AuthMethod) { + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + databaseWrapper, err := kmsCache.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + _ = TestAuthMethod(t, conn, databaseWrapper, org.PublicId, InactiveState, "alice_rp", "alices-dogs-name", + WithIssuer(TestConvertToUrls(t, "https://alice.com")[0]), WithApiUrl(TestConvertToUrls(t, "https://api.com")[0])) + _ = TestAuthMethod( + t, conn, databaseWrapper, org.PublicId, ActivePrivateState, + "alice_rp", "alices-dogs-name", + WithApiUrl(TestConvertToUrls(t, "https://alice-active-priv.com/callback")[0]), + WithSigningAlgs(RS256)) + amActivePub := TestAuthMethod( + t, conn, databaseWrapper, org.PublicId, ActivePublicState, + "alice_rp", "alices-dogs-name", + WithApiUrl(TestConvertToUrls(t, "https://alice-active-pub.com")[0]), + WithSigningAlgs(RS256)) + return "", []string{amActivePub.ScopeId}, []*AuthMethod{amActivePub} + }, + opt: []Option{WithUnauthenticatedUser(true)}, + }, + + { + name: "not-found-auth-method-id", + setupFn: func() (string, []string, []*AuthMethod) { + return "not-a-valid-id", nil, nil + }, + }, + { + name: "not-found-scope-ids", + setupFn: func() (string, []string, []*AuthMethod) { + return "", []string{"not-valid-scope-1", "not-valid-scope-2"}, nil + }, + }, + { + name: "no-search-criteria", + setupFn: func() (string, []string, []*AuthMethod) { + return "", nil, nil + }, + wantErrMatch: errors.T(errors.InvalidParameter), + }, + { + name: "search-too-many", + setupFn: func() (string, []string, []*AuthMethod) { + return "auth-method-id", []string{"scope-id"}, nil + }, + wantErrMatch: errors.T(errors.InvalidParameter), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + r, err := NewRepository(rw, rw, kmsCache) + require.NoError(err) + + authMethodId, scopeIds, want := tt.setupFn() + + got, err := r.getAuthMethods(ctx, authMethodId, scopeIds, tt.opt...) + + if tt.wantErrMatch != nil { + require.Error(err) + assert.Truef(errors.Match(tt.wantErrMatch, err), "want err code: %q got: %q", tt.wantErrMatch, err) + assert.Nil(got) + return + } + require.NoError(err) + TestSortAuthMethods(t, want) + TestSortAuthMethods(t, got) + assert.Equal(want, got) + }) + } +} diff --git a/internal/auth/oidc/repository_auth_method_test.go b/internal/auth/oidc/repository_auth_method_test.go new file mode 100644 index 000000000..e9cc010a2 --- /dev/null +++ b/internal/auth/oidc/repository_auth_method_test.go @@ -0,0 +1,328 @@ +package oidc + +import ( + "context" + "testing" + "time" + + "github.com/hashicorp/boundary/internal/auth/oidc/store" + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/oplog" + wrapping "github.com/hashicorp/go-kms-wrapping" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" +) + +func Test_upsertAccount(t *testing.T) { + t.Parallel() + ctx := context.Background() + conn, _ := db.TestSetup(t, "postgres") + rootWrapper := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, rootWrapper) + rw := db.New(conn) + + r, err := NewRepository(rw, rw, kmsCache) + require.NoError(t, err) + + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, rootWrapper)) + databaseWrapper, err := kmsCache.GetWrapper(ctx, org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + amActivePriv := TestAuthMethod( + t, + conn, databaseWrapper, org.PublicId, ActivePrivateState, + "alice_rp", "fido", + WithApiUrl(TestConvertToUrls(t, "https://alice-active-priv.com/callback")[0]), + WithSigningAlgs(RS256)) + + tests := []struct { + name string + am *AuthMethod + idClaims map[string]interface{} + atClaims map[string]interface{} + wantAcct *Account + wantErrMatch *errors.Template + wantErrContains string + }{ + { + name: "success-defaults", + am: amActivePriv, + idClaims: map[string]interface{}{"iss": "https://alice-active-priv.com", "sub": "success-defaults"}, + atClaims: map[string]interface{}{}, + wantAcct: &Account{Account: &store.Account{ + AuthMethodId: amActivePriv.PublicId, + Issuer: "https://alice-active-priv.com", + Subject: "success-defaults", + }}, + }, + { + name: "success-atTk-full-name-and-email", + am: amActivePriv, + idClaims: map[string]interface{}{"iss": "https://alice-active-priv.com", "sub": "success-atTk-full-name-and-email"}, + atClaims: map[string]interface{}{"name": "alice eve-smith", "email": "alice@alice.com"}, + wantAcct: &Account{Account: &store.Account{ + AuthMethodId: amActivePriv.PublicId, + Issuer: "https://alice-active-priv.com", + Subject: "success-atTk-full-name-and-email", + Email: "alice@alice.com", + FullName: "alice eve-smith", + }}, + }, + { + name: "success-idTk-full-name-and-email", + am: amActivePriv, + idClaims: map[string]interface{}{"iss": "https://alice-active-priv.com", "sub": "success-idTk-full-name-and-email", "name": "alice eve-smith", "email": "alice@alice.com"}, + atClaims: map[string]interface{}{}, + wantAcct: &Account{Account: &store.Account{ + AuthMethodId: amActivePriv.PublicId, + Issuer: "https://alice-active-priv.com", + Subject: "success-idTk-full-name-and-email", + Email: "alice@alice.com", + FullName: "alice eve-smith", + }}, + }, + { + name: "non-existent-auth-method-scope-id", + am: func() *AuthMethod { cp := amActivePriv.Clone(); cp.ScopeId = "non-existent-scope-id"; return cp }(), + idClaims: map[string]interface{}{"iss": "https://alice.com", "sub": "non-existent-auth-method-scope-id"}, + atClaims: map[string]interface{}{}, + wantErrMatch: errors.T(errors.Unknown), + wantErrContains: "unable to get oplog wrapper", + }, + { + name: "non-parsable-issuer", + am: amActivePriv, + idClaims: map[string]interface{}{"iss": ":::::alice.com", "sub": "non-parsable-issuer"}, + atClaims: map[string]interface{}{}, + wantErrMatch: errors.T(errors.Unknown), + wantErrContains: "unable to parse issuer", + }, + { + name: "empty-sub", + am: amActivePriv, + idClaims: map[string]interface{}{"iss": "https://alice.com", "sub": ""}, + atClaims: map[string]interface{}{}, + wantErrMatch: errors.T(errors.Unknown), + wantErrContains: "missing subject", + }, + { + name: "empty-issuer", + am: amActivePriv, + idClaims: map[string]interface{}{"iss": "", "sub": "bad-issuer"}, + atClaims: map[string]interface{}{}, + wantErrMatch: errors.T(errors.Unknown), + wantErrContains: "missing issuer", + }, + { + name: "missing-id-token-issuer", + am: amActivePriv, + idClaims: map[string]interface{}{}, + atClaims: map[string]interface{}{}, + wantErrMatch: errors.T(errors.Unknown), + wantErrContains: "issuer is not present in ID Token", + }, + { + name: "missing-id-token-sub", + am: amActivePriv, + idClaims: map[string]interface{}{"iss": "https://alice.com"}, + atClaims: map[string]interface{}{}, + wantErrMatch: errors.T(errors.Unknown), + wantErrContains: "subject is not present", + }, + { + name: "missing-id-token-claims", + am: amActivePriv, + atClaims: map[string]interface{}{}, + wantErrMatch: errors.T(errors.Unknown), + wantErrContains: "missing ID Token claims", + }, + { + name: "missing-access-token-claims", + am: amActivePriv, + idClaims: map[string]interface{}{}, + wantErrMatch: errors.T(errors.Unknown), + wantErrContains: "missing Access Token claims", + }, + { + name: "missing-auth-method", + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "missing auth method", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + gotAcct, err := r.upsertAccount(ctx, tt.am, tt.idClaims, tt.atClaims) + if tt.wantErrMatch != nil { + require.Error(err) + assert.Truef(errors.Match(tt.wantErrMatch, err), "got err %s but wanted: %+v", tt.wantErrMatch.Code, err) + if tt.wantErrContains != "" { + assert.Contains(err.Error(), tt.wantErrContains) + } + return + } + require.NoError(err) + tt.wantAcct.PublicId = gotAcct.PublicId + tt.wantAcct.Version = gotAcct.Version + tt.wantAcct.CreateTime = gotAcct.CreateTime + tt.wantAcct.UpdateTime = gotAcct.UpdateTime + require.NoError(err) + ok := proto.Equal(tt.wantAcct.Account, gotAcct.Account) + assert.True(ok) + if !ok { + assert.Equalf(tt.wantAcct, gotAcct, "using assert.Equal to just show the diff between want and got") + } + }) + } +} + +func Test_upsertOplog(t *testing.T) { + t.Parallel() + ctx := context.Background() + conn, _ := db.TestSetup(t, "postgres") + rootWrapper := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, rootWrapper) + rw := db.New(conn) + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, rootWrapper)) + databaseWrapper, err := kmsCache.GetWrapper(ctx, org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + oplogWrapper, err := kmsCache.GetWrapper(ctx, org.PublicId, kms.KeyPurposeOplog) + require.NoError(t, err) + + testAuthMethod := TestAuthMethod( + t, conn, databaseWrapper, org.PublicId, ActivePrivateState, + "alice-rp", "fido", + WithSigningAlgs(RS256), + WithIssuer(TestConvertToUrls(t, "https://www.alice.com")[0]), + WithApiUrl(TestConvertToUrls(t, "https://www.alice.com/callback")[0]), + ) + + tests := []struct { + name string + writer db.Writer + wrapper wrapping.Wrapper + op oplog.OpType + scopeId string + acct *Account + fieldMasks []string + nullFields []string + wantErrMatch *errors.Template + wantErrContains string + }{ + { + name: "create-success", + writer: rw, + wrapper: oplogWrapper, + op: oplog.OpType_OP_TYPE_CREATE, + scopeId: org.PublicId, + acct: TestAccount(t, conn, testAuthMethod, "create-success"), + }, + { + name: "update-success-withFieldMask-only", + writer: rw, + wrapper: oplogWrapper, + op: oplog.OpType_OP_TYPE_UPDATE, + fieldMasks: []string{"FullName"}, + scopeId: org.PublicId, + acct: TestAccount(t, conn, testAuthMethod, "update-success-withFieldMask-only"), + }, + { + name: "update-success-withNullFields-only", + writer: rw, + wrapper: oplogWrapper, + op: oplog.OpType_OP_TYPE_UPDATE, + nullFields: []string{"FullName"}, + scopeId: org.PublicId, + acct: TestAccount(t, conn, testAuthMethod, "update-success-withNullFields-only"), + }, + { + name: "update-missing-fieldMask-and-nullFields", + writer: rw, + wrapper: oplogWrapper, + op: oplog.OpType_OP_TYPE_UPDATE, + scopeId: org.PublicId, + acct: TestAccount(t, conn, testAuthMethod, "update-missing-fieldMask-and-nullFields"), + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "update operations must specify field masks and/or null masks", + }, + { + name: "missing-writer", + wrapper: oplogWrapper, + op: oplog.OpType_OP_TYPE_CREATE, + scopeId: org.PublicId, + acct: TestAccount(t, conn, testAuthMethod, "missing-writer"), + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "missing db writer", + }, + { + name: "missing-oplog-wrapper", + writer: rw, + op: oplog.OpType_OP_TYPE_CREATE, + scopeId: org.PublicId, + acct: TestAccount(t, conn, testAuthMethod, "missing-oplog-wrapper"), + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "missing oplog wrapper", + }, + { + name: "bad-op-type", + writer: rw, + wrapper: oplogWrapper, + op: oplog.OpType_OP_TYPE_DELETE, + scopeId: org.PublicId, + acct: TestAccount(t, conn, testAuthMethod, "bad-op-type"), + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "not a supported operation", + }, + { + name: "missing-scope-id", + writer: rw, + wrapper: oplogWrapper, + op: oplog.OpType_OP_TYPE_UPDATE, + acct: TestAccount(t, conn, testAuthMethod, "missing-scope-id"), + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "missing scope id", + }, + { + name: "missing-account", + writer: rw, + wrapper: oplogWrapper, + op: oplog.OpType_OP_TYPE_UPDATE, + scopeId: org.PublicId, + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "missing account", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + + // start with no oplog entries + _, err = rw.Exec(ctx, "delete from oplog_entry", nil) + require.NoError(err) + + err := upsertOplog(ctx, tt.writer, tt.wrapper, tt.op, tt.scopeId, tt.acct, tt.fieldMasks, tt.nullFields) + if tt.wantErrMatch != nil { + require.Error(err) + if tt.wantErrContains != "" { + assert.Contains(err.Error(), tt.wantErrContains) + } + if tt.acct != nil { + // check for create + err := db.TestVerifyOplog(t, rw, tt.acct.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_CREATE), db.WithCreateNotBefore(10*time.Second)) + require.Errorf(err, "should not have found oplog entry for %s", tt.acct.AuthMethodId) + + // check for update + err = db.TestVerifyOplog(t, rw, tt.acct.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_UPDATE), db.WithCreateNotBefore(10*time.Second)) + require.Errorf(err, "should not have found oplog entry for %s", tt.acct.AuthMethodId) + } + return + } + require.NoError(err) + err = db.TestVerifyOplog(t, rw, tt.acct.PublicId, db.WithOperation(tt.op), db.WithCreateNotBefore(10*time.Second)) + require.NoErrorf(err, "unexpected error verifying oplog entry: %s", err) + }) + } +} diff --git a/internal/auth/oidc/repository_auth_method_update.go b/internal/auth/oidc/repository_auth_method_update.go new file mode 100644 index 000000000..4a5dc8e1e --- /dev/null +++ b/internal/auth/oidc/repository_auth_method_update.go @@ -0,0 +1,616 @@ +package oidc + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/hashicorp/boundary/internal/db" + dbcommon "github.com/hashicorp/boundary/internal/db/common" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/oplog" + "github.com/hashicorp/boundary/sdk/strutil" + "github.com/hashicorp/go-multierror" +) + +// UpdateAuthMethod will retrieve the auth method from the repository, +// and update it based on the field masks provided. +// +// The auth method will not be persisted in the repository if the auth +// method's OperationalStatus is currently ActivePublic or ActivePrivate +// and the update would have resulted in an incomplete/non-operational +// auth method. +// +// During update, the auth method will be tested/validated against its +// provider's published OIDC discovery document. If this validation +// succeeds, the auth method is persisted in the repository, and the +// written auth method is returned. +// +// fieldMaskPaths provides field_mask.proto paths for fields that should +// be updated. Fields will be set to NULL if the field is a +// zero value and included in fieldMask. Name, Description, Issuer, +// ClientId, ClientSecret, MaxAge are all updatable fields. The AuthMethod's +// Value Objects of SigningAlgs, CallbackUrls, AudClaims and Certificates are +// also updatable. if no updatable fields are included in the fieldMaskPaths, +// then an error is returned. +// +// Options supported: +// +// * WithDryRun: when this option is provided, the auth method is retrieved from +// the repo, updated based on the fieldMask, tested via Repository.ValidateDiscoveryInfo, +// the results of the update are returned, and and any errors reported. The +// updates are not peristed to the repository. +// +// * WithForce: when this option is provided, the auth method is persisted in +// the repository without testing it's validity against its provider's published +// OIDC discovery document. Even if this option is provided, the auth method will +// not be persisted in the repository when the update would have resulted in +// an incomplete/non-operational auth method and it's OperationalStatus is +// currently ActivePublic or ActivePrivate. +// +// Also, a successful update will invalidate (delete) the Repository's +// cache of the oidc.Provider for the AuthMethod. +func (r *Repository) UpdateAuthMethod(ctx context.Context, am *AuthMethod, version uint32, fieldMaskPaths []string, opt ...Option) (*AuthMethod, int, error) { + const op = "oidc.(Repository).UpdateAuthMethod" + if am == nil { + return nil, db.NoRowsAffected, errors.New(errors.InvalidParameter, op, "missing auth method") + } + if am.AuthMethod == nil { + return nil, db.NoRowsAffected, errors.New(errors.InvalidParameter, op, "missing auth method store") + } + if am.PublicId == "" { + return nil, db.NoRowsAffected, errors.New(errors.InvalidParameter, op, "missing public id") + } + + if err := validateFieldMask(fieldMaskPaths); err != nil { + return nil, db.NoRowsAffected, errors.Wrap(err, op) + } + + dbMask, nullFields := dbcommon.BuildUpdatePaths( + map[string]interface{}{ + "Name": am.Name, + "Description": am.Description, + "Issuer": am.Issuer, + "ClientId": am.ClientId, + "ClientSecret": am.ClientSecret, + "MaxAge": am.MaxAge, + "SigningAlgs": am.SigningAlgs, + "ApiUrl": am.ApiUrl, + "AudClaims": am.AudClaims, + "Certificates": am.Certificates, + }, + fieldMaskPaths, + nil, + ) + if len(dbMask) == 0 && len(nullFields) == 0 { + return nil, db.NoRowsAffected, errors.New(errors.EmptyFieldMask, op, "empty field mask") + } + + origAm, err := r.lookupAuthMethod(ctx, am.PublicId) + if err != nil { + return nil, db.NoRowsAffected, errors.Wrap(err, op) + } + if origAm == nil { + return nil, db.NoRowsAffected, errors.New(errors.RecordNotFound, op, fmt.Sprintf("auth method %s", am.PublicId)) + } + // there's no reason to continue if another controller has already updated this auth method. + if origAm.Version != version { + return nil, db.NoRowsAffected, errors.New(errors.VersionMismatch, op, fmt.Sprintf("update version %d doesn't match db version %d", version, origAm.Version)) + } + + opts := getOpts(opt...) + if opts.withDryRun { + updated := applyUpdate(am, origAm, fieldMaskPaths) + if err := updated.isComplete(); err != nil { + return updated, db.NoRowsAffected, err + } + err := r.ValidateDiscoveryInfo(ctx, WithAuthMethod(updated)) + return updated, db.NoRowsAffected, err + } + + // prevent an "active" auth method from being updated in a manner that would create + // an incomplete and unusable auth method. + if origAm.OperationalState != string(InactiveState) { + if err := applyUpdate(am, origAm, fieldMaskPaths).isComplete(); err != nil { + return nil, db.NoRowsAffected, errors.Wrap(err, op, errors.WithMsg("update would result in an incomplete auth method")) + } + } + + if !opts.withForce { + if err := r.ValidateDiscoveryInfo(ctx, WithAuthMethod(applyUpdate(am, origAm, fieldMaskPaths))); err != nil { + return nil, db.NoRowsAffected, errors.Wrap(err, op) + } + } + + addAlgs, deleteAlgs, err := valueObjectChanges(origAm.PublicId, SigningAlgVO, am.SigningAlgs, origAm.SigningAlgs, dbMask, nullFields) + if err != nil { + return nil, db.NoRowsAffected, errors.Wrap(err, op) + } + + addCerts, deleteCerts, err := valueObjectChanges(origAm.PublicId, CertificateVO, am.Certificates, origAm.Certificates, dbMask, nullFields) + if err != nil { + return nil, db.NoRowsAffected, errors.Wrap(err, op) + } + + addAuds, deleteAuds, err := valueObjectChanges(origAm.PublicId, AudClaimVO, am.AudClaims, origAm.AudClaims, dbMask, nullFields) + if err != nil { + return nil, db.NoRowsAffected, errors.Wrap(err, op) + } + + var filteredDbMask, filteredNullFields []string + for _, f := range dbMask { + switch f { + case "SigningAlgs", "CallbackUrls", "AudClaims", "Certificates": + continue + default: + filteredDbMask = append(filteredDbMask, f) + } + } + for _, f := range nullFields { + switch f { + case "SigningAlgs", "CallbackUrls", "AudClaims", "Certificates": + continue + default: + filteredNullFields = append(filteredNullFields, f) + } + } + + // handle no changes... + if len(filteredDbMask) == 0 && + len(filteredNullFields) == 0 && + len(addAlgs) == 0 && + len(deleteAlgs) == 0 && + len(addCerts) == 0 && + len(deleteCerts) == 0 && + len(addAuds) == 0 && + len(deleteAuds) == 0 { + return origAm, db.NoRowsAffected, nil + } + + databaseWrapper, err := r.kms.GetWrapper(ctx, origAm.ScopeId, kms.KeyPurposeDatabase) + if err != nil { + return nil, db.NoRowsAffected, errors.Wrap(err, op, errors.WithMsg("unable to get database wrapper")) + } + if err := am.encrypt(ctx, databaseWrapper); err != nil { + return nil, db.NoRowsAffected, errors.Wrap(err, op) + } + + oplogWrapper, err := r.kms.GetWrapper(ctx, origAm.ScopeId, kms.KeyPurposeOplog) + if err != nil { + return nil, db.NoRowsAffected, errors.Wrap(err, op, errors.WithMsg("unable to get oplog wrapper")) + } + + // we always set this to the current value of opts.withForce + am.DisableDiscoveredConfigValidation = opts.withForce + + var updatedAm *AuthMethod + var rowsUpdated int + _, err = r.writer.DoTx( + ctx, + db.StdRetryCnt, + db.ExpBackoff{}, + func(reader db.Reader, w db.Writer) error { + msgs := make([]*oplog.Message, 0, 7) // AuthMethod, Algs*2, Certs*2, Audiences*2 + ticket, err := w.GetTicket(am) + if err != nil { + return errors.Wrap(err, op, errors.WithMsg("unable to get ticket")) + } + var authMethodOplogMsg oplog.Message + switch { + case len(filteredDbMask) == 0 && len(filteredNullFields) == 0: + // the auth method's fields are not being updated, just it's value objects, so we need to just update the auth + // method's version. + updatedAm = am.Clone() + updatedAm.Version = uint32(version) + 1 + rowsUpdated, err = w.Update(ctx, updatedAm, []string{"Version", "DisableDiscoveredConfigValidation"}, nil, db.NewOplogMsg(&authMethodOplogMsg), db.WithVersion(&version)) + if err != nil { + return errors.Wrap(err, op, errors.WithMsg("unable to update auth method version")) + } + if rowsUpdated != 1 { + return errors.New(errors.MultipleRecords, op, fmt.Sprintf("updated auth method version and %d rows updated", rowsUpdated)) + } + default: + filteredDbMask = append(filteredDbMask, "DisableDiscoveredConfigValidation") + updatedAm = am.Clone() + rowsUpdated, err = w.Update(ctx, updatedAm, filteredDbMask, filteredNullFields, db.NewOplogMsg(&authMethodOplogMsg), db.WithVersion(&version)) + if err != nil { + return errors.Wrap(err, op, errors.WithMsg("unable to update auth method")) + } + if rowsUpdated != 1 { + return errors.New(errors.MultipleRecords, op, fmt.Sprintf("updated auth method and %d rows updated", rowsUpdated)) + } + } + msgs = append(msgs, &authMethodOplogMsg) + + if len(deleteAlgs) > 0 { + deleteAlgOplogMsgs := make([]*oplog.Message, 0, len(deleteAlgs)) + rowsDeleted, err := w.DeleteItems(ctx, deleteAlgs, db.NewOplogMsgs(&deleteAlgOplogMsgs)) + if err != nil { + return errors.Wrap(err, op, errors.WithMsg("unable to delete signing algorithms")) + } + if rowsDeleted != len(deleteAlgs) { + return errors.New(errors.MultipleRecords, op, fmt.Sprintf("signing algorithms deleted %d did not match request for %d", rowsDeleted, len(deleteAlgs))) + } + msgs = append(msgs, deleteAlgOplogMsgs...) + } + if len(addAlgs) > 0 { + addAlgsOplogMsgs := make([]*oplog.Message, 0, len(addAlgs)) + if err := w.CreateItems(ctx, addAlgs, db.NewOplogMsgs(&addAlgsOplogMsgs)); err != nil { + return errors.Wrap(err, op, errors.WithMsg("unable to add signing algorithms")) + } + msgs = append(msgs, addAlgsOplogMsgs...) + } + + if len(deleteCerts) > 0 { + deleteCertOplogMsgs := make([]*oplog.Message, 0, len(deleteCerts)) + rowsDeleted, err := w.DeleteItems(ctx, deleteCerts, db.NewOplogMsgs(&deleteCertOplogMsgs)) + if err != nil { + return errors.Wrap(err, op, errors.WithMsg("unable to delete certificates")) + } + if rowsDeleted != len(deleteCerts) { + return errors.New(errors.MultipleRecords, op, fmt.Sprintf("certificates deleted %d did not match request for %d", rowsDeleted, len(deleteCerts))) + } + msgs = append(msgs, deleteCertOplogMsgs...) + } + if len(addCerts) > 0 { + addCertsOplogMsgs := make([]*oplog.Message, 0, len(addCerts)) + if err := w.CreateItems(ctx, addCerts, db.NewOplogMsgs(&addCertsOplogMsgs)); err != nil { + return errors.Wrap(err, op, errors.WithMsg("unable to add certificates")) + } + msgs = append(msgs, addCertsOplogMsgs...) + } + + if len(deleteAuds) > 0 { + deleteAudsOplogMsgs := make([]*oplog.Message, 0, len(deleteAuds)) + rowsDeleted, err := w.DeleteItems(ctx, deleteAuds, db.NewOplogMsgs(&deleteAudsOplogMsgs)) + if err != nil { + return errors.Wrap(err, op, errors.WithMsg("unable to audiences URLs")) + } + if rowsDeleted != len(deleteAuds) { + return errors.New(errors.MultipleRecords, op, fmt.Sprintf("audiences deleted %d did not match request for %d", rowsDeleted, len(deleteAuds))) + } + msgs = append(msgs, deleteAudsOplogMsgs...) + } + if len(addAuds) > 0 { + addAudsOplogMsgs := make([]*oplog.Message, 0, len(addAuds)) + if err := w.CreateItems(ctx, addAuds, db.NewOplogMsgs(&addAudsOplogMsgs)); err != nil { + return errors.Wrap(err, op, errors.WithMsg("unable to add audiences URLs")) + } + msgs = append(msgs, addAudsOplogMsgs...) + } + + metadata := updatedAm.oplog(oplog.OpType_OP_TYPE_UPDATE) + if err := w.WriteOplogEntryWith(ctx, oplogWrapper, ticket, metadata, msgs); err != nil { + return errors.Wrap(err, op, errors.WithMsg("unable to write oplog")) + } + // we need a new repo, that's using the same reader/writer as this TxHandler + txRepo := &Repository{ + reader: reader, + writer: w, + kms: r.kms, + // intentionally not setting the defaultLimit, so we'll get all + // the account ids without a limit + } + updatedAm, err = txRepo.lookupAuthMethod(ctx, updatedAm.PublicId) + if err != nil { + return errors.Wrap(err, op, errors.WithMsg("unable to lookup auth method after update")) + } + if updatedAm == nil { + return errors.New(errors.RecordNotFound, op, "unable to lookup auth method after update") + } + return nil + }, + ) + if err != nil { + return nil, db.NoRowsAffected, errors.Wrap(err, op) + } + + providerCache().delete(ctx, updatedAm.PublicId) + + return updatedAm, rowsUpdated, nil +} + +// voName represents the names of auth method value objects +type voName string + +const ( + SigningAlgVO voName = "SigningAlgs" + CertificateVO voName = "Certificates" + AudClaimVO voName = "AudClaims" +) + +// validVoName decides if the name is valid +func validVoName(name voName) bool { + switch name { + case SigningAlgVO, CertificateVO, AudClaimVO: + return true + default: + return false + } +} + +// factoryFunc defines a func type for value object factories +type factoryFunc func(publicId string, i interface{}) (interface{}, error) + +// supportedFactories are the currently supported factoryFunc for value objects +var supportedFactories = map[voName]factoryFunc{ + SigningAlgVO: func(publicId string, i interface{}) (interface{}, error) { + str := fmt.Sprintf("%s", i) + return NewSigningAlg(publicId, Alg(str)) + }, + CertificateVO: func(publicId string, i interface{}) (interface{}, error) { + str := fmt.Sprintf("%s", i) + return NewCertificate(publicId, str) + }, + AudClaimVO: func(publicId string, i interface{}) (interface{}, error) { + str := fmt.Sprintf("%s", i) + return NewAudClaim(publicId, str) + }, +} + +// valueObjectChanges takes the new and old list of VOs (value objects) and +// using the dbMasks/nullFields it will return lists of VOs where need to be +// added and deleted in order to reconcile auth method's value objects. +func valueObjectChanges( + publicId string, + valueObjectName voName, + newVOs, + oldVOs, + dbMask, + nullFields []string, +) (add []interface{}, del []interface{}, e error) { + const op = "valueObjectChanges" + if publicId == "" { + return nil, nil, errors.New(errors.InvalidParameter, op, "missing public id") + } + if !validVoName(valueObjectName) { + return nil, nil, errors.New(errors.InvalidParameter, op, fmt.Sprintf("invalid value object name: %s", valueObjectName)) + } + if !strutil.StrListContains(dbMask, string(valueObjectName)) && !strutil.StrListContains(nullFields, string(valueObjectName)) { + return nil, nil, nil + } + if len(strutil.RemoveDuplicates(newVOs, false)) != len(newVOs) { + return nil, nil, errors.New(errors.InvalidParameter, op, fmt.Sprintf("duplicate new %s", valueObjectName)) + } + if len(strutil.RemoveDuplicates(oldVOs, false)) != len(oldVOs) { + return nil, nil, errors.New(errors.InvalidParameter, op, fmt.Sprintf("duplicate old %s", valueObjectName)) + } + + factory, ok := supportedFactories[valueObjectName] + if !ok { + return nil, nil, errors.New(errors.InvalidParameter, op, fmt.Sprintf("unsupported factory for value object: %s", valueObjectName)) + } + + if len(dbMask) == 0 && len(nullFields) == 0 { + return nil, nil, nil + } + + foundVOs := map[string]bool{} + for _, a := range oldVOs { + foundVOs[a] = true + } + var adds []interface{} + var deletes []interface{} + if strutil.StrListContains(nullFields, string(valueObjectName)) { + deletes = make([]interface{}, 0, len(oldVOs)) + for _, a := range oldVOs { + alg, err := factory(publicId, a) + if err != nil { + return nil, nil, errors.Wrap(err, op) + } + deletes = append(deletes, alg) + delete(foundVOs, a) + } + } + if strutil.StrListContains(dbMask, string(valueObjectName)) { + adds = make([]interface{}, 0, len(newVOs)) + for _, a := range newVOs { + if _, ok := foundVOs[a]; ok { + delete(foundVOs, a) + continue + } + alg, err := factory(publicId, a) + if err != nil { + return nil, nil, errors.Wrap(err, op) + } + adds = append(adds, alg) + delete(foundVOs, a) + } + } + if len(foundVOs) > 0 { + for a := range foundVOs { + alg, err := factory(publicId, a) + if err != nil { + return nil, nil, errors.Wrap(err, op) + } + deletes = append(deletes, alg) + delete(foundVOs, a) + } + } + return adds, deletes, nil +} + +// validateFieldMask check the field mask to ensure all the fields are updatable +func validateFieldMask(fieldMaskPaths []string) error { + const op = "validateFieldMask" + for _, f := range fieldMaskPaths { + switch { + case strings.EqualFold("Name", f): + case strings.EqualFold("Description", f): + case strings.EqualFold("Issuer", f): + case strings.EqualFold("ClientId", f): + case strings.EqualFold("ClientSecret", f): + case strings.EqualFold("MaxAge", f): + case strings.EqualFold("SigningAlgs", f): + case strings.EqualFold("ApiUrl", f): + case strings.EqualFold("AudClaims", f): + case strings.EqualFold("Certificates", f): + default: + return errors.New(errors.InvalidParameter, op, fmt.Sprintf("invalid field mask: %s", f)) + } + } + return nil +} + +// applyUpdate takes the new and applies it to the orig using the field masks +func applyUpdate(new, orig *AuthMethod, fieldMaskPaths []string) *AuthMethod { + cp := orig.Clone() + for _, f := range fieldMaskPaths { + switch f { + case "Name": + cp.Name = new.Name + case "Description": + cp.Description = new.Description + case "Issuer": + cp.Issuer = new.Issuer + case "ClientId": + cp.ClientId = new.ClientId + case "ClientSecret": + cp.ClientSecret = new.ClientSecret + case "MaxAge": + cp.MaxAge = new.MaxAge + case "ApiUrl": + cp.ApiUrl = new.ApiUrl + case "SigningAlgs": + switch { + case len(new.SigningAlgs) == 0: + cp.SigningAlgs = nil + default: + cp.SigningAlgs = make([]string, 0, len(new.SigningAlgs)) + cp.SigningAlgs = append(cp.SigningAlgs, new.SigningAlgs...) + } + case "AudClaims": + switch { + case len(new.AudClaims) == 0: + cp.AudClaims = nil + default: + cp.AudClaims = make([]string, 0, len(new.AudClaims)) + cp.AudClaims = append(cp.AudClaims, new.AudClaims...) + } + case "Certificates": + switch { + case len(new.Certificates) == 0: + cp.Certificates = nil + default: + cp.Certificates = make([]string, 0, len(new.Certificates)) + cp.Certificates = append(cp.Certificates, new.Certificates...) + } + } + } + return cp +} + +// ValidateDiscoveryInfo will test/validate the provided AuthMethod against +// the info from it's discovery URL. +// +// It will verify that all required fields for a working AuthMethod have values. +// +// If the AuthMethod is complete, ValidateDiscoveryInfo retrieves the auth +// method's OpenID Configuration document. The values in the AuthMethod +// (and associated data) are validated with the retrieved document. The issuer and +// id token signing algorithm in the configuration are validated with the +// retrieved document. ValidateDiscoveryInfo also verifies the authorization, token, +// and user_info endpoints by connecting to each and uses any certificates in the +// configuration as trust anchors to confirm connectivity. +// +// Options supported are: WithPublicId, WithAuthMethod +func (r *Repository) ValidateDiscoveryInfo(ctx context.Context, opt ...Option) error { + const op = "oidc.(Repository).ValidateDiscoveryInfo" + opts := getOpts(opt...) + var am *AuthMethod + switch { + case opts.withPublicId != "": + var err error + am, err = r.lookupAuthMethod(ctx, opts.withPublicId) + if err != nil { + return errors.Wrap(err, op) + } + if am == nil { + return errors.New(errors.RecordNotFound, op, fmt.Sprintf("unable to lookup auth method %s", opts.withPublicId)) + } + case opts.withAuthMethod != nil: + am = opts.withAuthMethod + default: + return errors.New(errors.InvalidParameter, op, "neither WithPublicId(...) nor WithAuthMethod(...) options were provided") + } + + // FYI: once converted to an oidc.Provider, any certs configured will be used as trust anchors for all HTTP requests + provider, err := convertToProvider(ctx, am) + if err != nil && am.OperationalState == string(InactiveState) { + return nil + } + if err != nil { + return errors.Wrap(err, op) + } + + info, err := provider.DiscoveryInfo(ctx) + if err != nil { + return errors.Wrap(err, op) + } + + var result *multierror.Error + if info.Issuer != am.Issuer { + result = multierror.Append(result, errors.New(errors.InvalidParameter, op, + fmt.Sprintf("auth method issuer doesn't match discovery issuer: expected %s and got %s", am.Issuer, info.Issuer))) + } + for _, a := range am.SigningAlgs { + if !strutil.StrListContains(info.IdTokenSigningAlgsSupported, a) { + result = multierror.Append(result, errors.New(errors.InvalidParameter, op, + fmt.Sprintf("auth method signing alg is not in discovered supported algs: expected %s and got %s", a, info.IdTokenSigningAlgsSupported))) + } + } + providerClient, err := provider.HTTPClient() + if err != nil { + result = multierror.Append(result, errors.New(errors.Unknown, op, "unable to get oidc http client", errors.WithWrap(err))) + return result.ErrorOrNil() + } + + // we need to prevent redirects during these tests... we don't want to have + // redirects going to the controller's callback (aka the configured provider's callback) + providerClient.CheckRedirect = func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + } + + // test JWKs URL + statusCode, err := pingEndpoint(ctx, providerClient, "JWKs", "GET", info.JWKSURL) + if err != nil { + result = multierror.Append(result, errors.New(errors.Unknown, op, fmt.Sprintf("unable to verify JWKs endpoint: %s", info.JWKSURL), errors.WithWrap(err))) + } + if statusCode != 200 { + result = multierror.Append(result, errors.New(errors.Unknown, op, fmt.Sprintf("non-200 status (%d) from JWKs endpoint: %s", statusCode, info.JWKSURL), errors.WithWrap(err))) + } + + // test Auth URL + if _, err := pingEndpoint(ctx, providerClient, "AuthURL", "GET", info.AuthURL); err != nil { + result = multierror.Append(result, errors.New(errors.Unknown, op, fmt.Sprintf("unable to verify authorize endpoint: %s", info.AuthURL), errors.WithWrap(err))) + } + + // test Token URL + if _, err := pingEndpoint(ctx, providerClient, "TokenURL", "POST", info.TokenURL); err != nil { + result = multierror.Append(result, errors.New(errors.Unknown, op, fmt.Sprintf("unable to verify token endpoint: %s", info.TokenURL), errors.WithWrap(err))) + } + + // we're not verifying the UserInfo URL, since it's not a required dependency. + + return result.ErrorOrNil() +} + +type HTTPClient interface { + Do(req *http.Request) (*http.Response, error) +} + +// pingEndpoint will make an attempted http request, return status code and errors +func pingEndpoint(ctx context.Context, client HTTPClient, endpointType, method, url string) (int, error) { + const op = "oidc.pingEndpoint" + req, err := http.NewRequestWithContext(ctx, method, url, nil) + if err != nil { + return 0, errors.New(errors.Unknown, op, fmt.Sprintf("unable to create %s http request", endpointType), errors.WithWrap(err)) + } + resp, err := client.Do(req) + if err != nil { + return 0, errors.New(errors.Unknown, op, fmt.Sprintf("request to %s endpoint failed", endpointType), errors.WithWrap(err)) + } + return resp.StatusCode, nil +} diff --git a/internal/auth/oidc/repository_auth_method_update_test.go b/internal/auth/oidc/repository_auth_method_update_test.go new file mode 100644 index 000000000..bede52aea --- /dev/null +++ b/internal/auth/oidc/repository_auth_method_update_test.go @@ -0,0 +1,1169 @@ +package oidc + +import ( + "context" + "fmt" + "net/http" + "net/url" + "sort" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/boundary/internal/auth/oidc/store" + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/oplog" + "github.com/hashicorp/boundary/sdk/testutil" + "github.com/hashicorp/cap/oidc" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/testing/protocmp" +) + +func Test_UpdateAuthMethod(t *testing.T) { + t.Parallel() + ctx := context.Background() + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, wrapper) + + tp := oidc.StartTestProvider(t) + tpClientId := "alice-rp" + tpClientSecret := "her-dog's-name" + tp.SetClientCreds(tpClientId, tpClientSecret) + _, _, tpAlg, _ := tp.SigningKeys() + tpCert, err := ParseCertificates(tp.CACert()) + require.NoError(t, err) + require.Equal(t, 1, len(tpCert)) + + rw := db.New(conn) + repo, err := NewRepository(rw, rw, kmsCache) + require.NoError(t, err) + + tests := []struct { + name string + setup func() *AuthMethod + updateWith func(orig *AuthMethod) *AuthMethod + fieldMasks []string + version uint32 + opt []Option + want func(orig, updateWith *AuthMethod) *AuthMethod + wantErrMatch *errors.Template + wantNoRowsUpdate bool + }{ + { + name: "very-simple", + setup: func() *AuthMethod { + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + databaseWrapper, err := kmsCache.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + return TestAuthMethod(t, + conn, databaseWrapper, + org.PublicId, + InactiveState, + "alice-rp", "alice-secret", + WithCertificates(tpCert[0]), + WithSigningAlgs(Alg(tpAlg)), + WithApiUrl(TestConvertToUrls(t, "https://www.alice.com/callback")[0]), + ) + }, + updateWith: func(orig *AuthMethod) *AuthMethod { + am := AllocAuthMethod() + am.PublicId = orig.PublicId + am.Name = "alice's restaurant" + am.Description = "the best place to eat" + am.AudClaims = []string{"www.alice.com", "www.alice.com/admin"} + return &am + }, + fieldMasks: []string{"Name", "Description", "AudClaims"}, + version: 1, + want: func(orig, updateWith *AuthMethod) *AuthMethod { + am := orig.Clone() + am.Name = updateWith.Name + am.Description = updateWith.Description + am.AudClaims = updateWith.AudClaims + return am + }, + }, + { + name: "with-force-all-value-objects", + setup: func() *AuthMethod { + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + databaseWrapper, err := kmsCache.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + return TestAuthMethod(t, + conn, databaseWrapper, + org.PublicId, + InactiveState, + "alice-rp", "alice-secret", + WithAudClaims("www.alice.com"), + WithCertificates(tpCert[0]), + WithSigningAlgs(Alg(tpAlg)), + WithApiUrl(TestConvertToUrls(t, "https://www.alice.com/callback")[0]), + ) + }, + updateWith: func(orig *AuthMethod) *AuthMethod { + _, pem := testGenerateCA(t, "127.0.0.1") + am := AllocAuthMethod() + am.PublicId = orig.PublicId + am.Name = "alice's restaurant" + am.Description = "the best place to eat" + am.ApiUrl = "https://www.bob.com/callback" + am.AudClaims = []string{"www.alice.com/admin"} + am.SigningAlgs = []string{string(ES384), string(ES512)} + am.Certificates = []string{pem} + return &am + }, + fieldMasks: []string{"Name", "Description", "AudClaims", "ApiUrl", "SigningAlgs", "Certificates"}, + version: 1, + opt: []Option{WithForce()}, + want: func(orig, updateWith *AuthMethod) *AuthMethod { + am := orig.Clone() + am.Name = updateWith.Name + am.Description = updateWith.Description + am.ApiUrl = updateWith.ApiUrl + am.AudClaims = updateWith.AudClaims + am.SigningAlgs = updateWith.SigningAlgs + am.Certificates = updateWith.Certificates + am.DisableDiscoveredConfigValidation = true + return am + }, + }, + { + name: "null-name-description", + setup: func() *AuthMethod { + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + databaseWrapper, err := kmsCache.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + return TestAuthMethod(t, conn, databaseWrapper, org.PublicId, InactiveState, "alice-rp", "alice-secret", WithName("alice's restaurant"), WithDescription("the best place to eat"), WithCertificates(tpCert[0]), WithSigningAlgs(Alg(tpAlg)), WithApiUrl(TestConvertToUrls(t, "https://www.alice.com/callback")[0])) + }, + updateWith: func(orig *AuthMethod) *AuthMethod { + am := AllocAuthMethod() + am.PublicId = orig.PublicId + am.Name = "" + am.Description = "" + return &am + }, + fieldMasks: []string{"Name", "Description"}, + version: 1, + want: func(orig, updateWith *AuthMethod) *AuthMethod { + am := orig.Clone() + am.Name = "" + am.Description = "" + return am + }, + }, + { + name: "null-signing-algs", + setup: func() *AuthMethod { + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + databaseWrapper, err := kmsCache.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + return TestAuthMethod(t, conn, databaseWrapper, org.PublicId, InactiveState, "alice-rp", "alice-secret", WithCertificates(tpCert[0]), WithSigningAlgs(Alg(tpAlg)), WithApiUrl(TestConvertToUrls(t, "https://www.alice.com/callback")[0])) + }, + updateWith: func(orig *AuthMethod) *AuthMethod { + am := AllocAuthMethod() + am.PublicId = orig.PublicId + return &am + }, + fieldMasks: []string{"SigningAlgs"}, + version: 1, + want: func(orig, updateWith *AuthMethod) *AuthMethod { + am := orig.Clone() + am.SigningAlgs = nil + return am + }, + }, + { + name: "change-callback-url", + setup: func() *AuthMethod { + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + databaseWrapper, err := kmsCache.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + return TestAuthMethod(t, conn, databaseWrapper, org.PublicId, InactiveState, "alice-rp", "alice-secret", WithCertificates(tpCert[0]), WithSigningAlgs(Alg(tpAlg)), WithApiUrl(TestConvertToUrls(t, "https://www.alice.com/callback")[0])) + }, + updateWith: func(orig *AuthMethod) *AuthMethod { + am := AllocAuthMethod() + am.PublicId = orig.PublicId + am.ApiUrl = "https://www.updated.com/callback" + return &am + }, + fieldMasks: []string{"ApiUrl"}, + version: 1, + want: func(orig, updateWith *AuthMethod) *AuthMethod { + am := orig.Clone() + am.ApiUrl = updateWith.ApiUrl + return am + }, + }, + { + name: "no-changes", + setup: func() *AuthMethod { + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + databaseWrapper, err := kmsCache.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + return TestAuthMethod(t, conn, databaseWrapper, org.PublicId, InactiveState, "alice-rp", "alice-secret", WithCertificates(tpCert[0]), WithSigningAlgs(Alg(tpAlg)), WithApiUrl(TestConvertToUrls(t, "https://www.alice.com/callback")[0])) + }, + updateWith: func(orig *AuthMethod) *AuthMethod { + am := AllocAuthMethod() + am.PublicId = orig.PublicId + am.SigningAlgs = []string{string(tpAlg)} + return &am + }, + fieldMasks: []string{"SigningAlgs"}, + version: 1, + want: func(orig, updateWith *AuthMethod) *AuthMethod { + am := orig.Clone() + return am + }, + wantNoRowsUpdate: true, + }, + { + name: "inactive-not-complete-no-with-force", + setup: func() *AuthMethod { + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + databaseWrapper, err := kmsCache.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + return TestAuthMethod(t, conn, databaseWrapper, org.PublicId, InactiveState, "alice-rp", "alice-secret", WithCertificates(tpCert[0]), WithApiUrl(TestConvertToUrls(t, "https://www.alice.com/callback")[0])) + }, + updateWith: func(orig *AuthMethod) *AuthMethod { + am := AllocAuthMethod() + am.PublicId = orig.PublicId + am.Name = "alice's restaurant" + am.Description = "the best place to eat" + am.AudClaims = []string{"www.alice.com", "www.alice.com/admin"} + return &am + }, + fieldMasks: []string{"Name", "Description", "AudClaims"}, + version: 1, + want: func(orig, updateWith *AuthMethod) *AuthMethod { + am := orig.Clone() + am.Name = updateWith.Name + am.Description = updateWith.Description + am.AudClaims = updateWith.AudClaims + return am + }, + }, + { + name: "with-dry-run", + setup: func() *AuthMethod { + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + databaseWrapper, err := kmsCache.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + return TestAuthMethod(t, + conn, databaseWrapper, + org.PublicId, + InactiveState, + "alice-rp", "alice-secret", + WithAudClaims("www.alice.com"), + WithCertificates(tpCert[0]), + WithSigningAlgs(Alg(tpAlg)), + WithIssuer(TestConvertToUrls(t, tp.Addr())[0]), + WithApiUrl(TestConvertToUrls(t, "https://www.alice.com/callback")[0]), + ) + }, + updateWith: func(orig *AuthMethod) *AuthMethod { + am := AllocAuthMethod() + am.PublicId = orig.PublicId + am.Name = "alice's restaurant" + am.Description = "the best place to eat" + am.AudClaims = []string{"www.alice.com/admin"} + am.ApiUrl = "https://www.bob.com/callback" + am.SigningAlgs = []string{string(ES384), string(ES512)} + return &am + }, + fieldMasks: []string{"Name", "Description", "AudClaims", "ApiUrl", "SigningAlgs"}, + version: 1, + opt: []Option{WithDryRun()}, + want: func(orig, updateWith *AuthMethod) *AuthMethod { + am := orig.Clone() + am.Name = updateWith.Name + am.Description = updateWith.Description + am.AudClaims = updateWith.AudClaims + am.ApiUrl = updateWith.ApiUrl + am.SigningAlgs = updateWith.SigningAlgs + return am + }, + wantErrMatch: errors.T(errors.InvalidParameter), + }, + { + name: "with-dry-run-on-validated-authmethod", + setup: func() *AuthMethod { + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + databaseWrapper, err := kmsCache.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + return TestAuthMethod(t, + conn, databaseWrapper, + org.PublicId, + InactiveState, + "alice-rp", "alice-secret", + WithAudClaims("www.alice.com"), + WithCertificates(tpCert[0]), + WithSigningAlgs(Alg(tpAlg)), + WithIssuer(TestConvertToUrls(t, tp.Addr())[0]), + WithApiUrl(TestConvertToUrls(t, "https://www.alice.com/callback")[0]), + ) + }, + updateWith: func(orig *AuthMethod) *AuthMethod { + am := AllocAuthMethod() + am.PublicId = orig.PublicId + am.Name = "alice's restaurant" + am.Description = "the best place to eat" + am.AudClaims = []string{"www.alice.com/admin"} + am.ApiUrl = "https://www.bob.com/callback" + return &am + }, + fieldMasks: []string{"Name", "Description", "AudClaims", "ApiUrl"}, + version: 1, + opt: []Option{WithDryRun()}, + want: func(orig, updateWith *AuthMethod) *AuthMethod { + am := orig.Clone() + am.Name = updateWith.Name + am.Description = updateWith.Description + am.AudClaims = updateWith.AudClaims + am.ApiUrl = updateWith.ApiUrl + return am + }, + }, + { + name: "nil-authMethod", + setup: func() *AuthMethod { return nil }, + updateWith: func(orig *AuthMethod) *AuthMethod { return nil }, + fieldMasks: []string{"Name"}, + version: 1, + wantErrMatch: errors.T(errors.InvalidParameter), + }, + { + name: "nil-authMethod-store", + setup: func() *AuthMethod { return nil }, + updateWith: func(orig *AuthMethod) *AuthMethod { return &AuthMethod{} }, + fieldMasks: []string{"Name"}, + version: 1, + wantErrMatch: errors.T(errors.InvalidParameter), + }, + { + name: "missing-public-id", + setup: func() *AuthMethod { return nil }, + updateWith: func(orig *AuthMethod) *AuthMethod { a := AllocAuthMethod(); return &a }, + fieldMasks: []string{"Name"}, + version: 1, + wantErrMatch: errors.T(errors.InvalidParameter), + }, + { + name: "bad-field-mask", + setup: func() *AuthMethod { return nil }, + updateWith: func(orig *AuthMethod) *AuthMethod { + a := AllocAuthMethod() + id, _ := newAuthMethodId() + a.PublicId = id + return &a + }, + fieldMasks: []string{"CreateTime"}, + version: 1, + wantErrMatch: errors.T(errors.InvalidParameter), + }, + { + name: "no-mask-or-null-fields", + setup: func() *AuthMethod { return nil }, + updateWith: func(orig *AuthMethod) *AuthMethod { + a := AllocAuthMethod() + id, _ := newAuthMethodId() + a.PublicId = id + return &a + }, + version: 1, + wantErrMatch: errors.T(errors.EmptyFieldMask), + }, + { + name: "not-found", + setup: func() *AuthMethod { return nil }, + updateWith: func(orig *AuthMethod) *AuthMethod { + a := AllocAuthMethod() + id, _ := newAuthMethodId() + a.PublicId = id + return &a + }, + fieldMasks: []string{"Name"}, + version: 1, + wantErrMatch: errors.T(errors.RecordNotFound), + }, + { + name: "bad-version", + setup: func() *AuthMethod { + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + databaseWrapper, err := kmsCache.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + return TestAuthMethod(t, conn, databaseWrapper, org.PublicId, InactiveState, "alice-rp", "alice-secret", WithAudClaims("www.alice.com"), WithCertificates(tpCert[0]), WithSigningAlgs(Alg(tpAlg)), WithApiUrl(TestConvertToUrls(t, "https://www.alice.com/callback")[0])) + }, + updateWith: func(orig *AuthMethod) *AuthMethod { + am := AllocAuthMethod() + am.PublicId = orig.PublicId + am.Name = "alice's restaurant" + return &am + }, + fieldMasks: []string{"Name"}, + version: 100, + wantErrMatch: errors.T(errors.VersionMismatch), + }, + { + name: "not-valid-auth-method", + setup: func() *AuthMethod { + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + databaseWrapper, err := kmsCache.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + return TestAuthMethod(t, + conn, databaseWrapper, + org.PublicId, + InactiveState, + "alice-rp", "alice-secret", + WithAudClaims("www.alice.com"), + WithCertificates(tpCert[0]), + WithSigningAlgs(Alg(tpAlg)), + WithIssuer(TestConvertToUrls(t, tp.Addr())[0]), + WithApiUrl(TestConvertToUrls(t, "https://www.alice.com/callback")[0]), + ) + }, + updateWith: func(orig *AuthMethod) *AuthMethod { + am := AllocAuthMethod() + am.PublicId = orig.PublicId + am.Name = "alice's restaurant" + am.Description = "the best place to eat" + am.SigningAlgs = []string{string(ES384), string(ES512)} + return &am + }, + fieldMasks: []string{"Name", "Description", "SigningAlgs"}, + version: 1, + wantErrMatch: errors.T(errors.InvalidParameter), + }, + { + name: "active-with-update-to-incomplete", + setup: func() *AuthMethod { + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + databaseWrapper, err := kmsCache.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + return TestAuthMethod(t, conn, databaseWrapper, org.PublicId, ActivePublicState, "alice-rp", "alice-secret", WithCertificates(tpCert[0]), WithSigningAlgs(Alg(tpAlg)), WithApiUrl(TestConvertToUrls(t, "https://www.alice.com/callback")[0])) + }, + updateWith: func(orig *AuthMethod) *AuthMethod { + am := AllocAuthMethod() + am.PublicId = orig.PublicId + return &am + }, + fieldMasks: []string{"SigningAlgs"}, + version: 2, // since TestAuthMethod(...) did an update to get it to ActivePublicState + wantErrMatch: errors.T(errors.InvalidParameter), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + orig := tt.setup() + updateWith := tt.updateWith(orig) + updated, rowsUpdated, err := repo.UpdateAuthMethod(ctx, updateWith, tt.version, tt.fieldMasks, tt.opt...) + opts := getOpts(tt.opt...) + if tt.wantErrMatch != nil && !opts.withDryRun { + require.Error(err) + assert.Equal(0, rowsUpdated) + assert.Nil(updated) + assert.Truef(errors.Match(tt.wantErrMatch, err), "want err code: %q got: %q", tt.wantErrMatch.Code, err) + + if updateWith != nil && updateWith.AuthMethod != nil && updateWith.PublicId != "" { + err := db.TestVerifyOplog(t, rw, updateWith.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_UPDATE), db.WithCreateNotBefore(10*time.Second)) + require.Errorf(err, "should not have found oplog entry for %s", updateWith.PublicId) + } + return + } + switch opts.withDryRun { + case true: + assert.Equal(0, rowsUpdated) + switch tt.wantErrMatch != nil { + case true: + require.Error(err) + default: + require.NoError(err) + } + require.NotNil(updated) + want := tt.want(orig, updateWith) + want.CreateTime = orig.CreateTime + want.UpdateTime = orig.UpdateTime + want.Version = orig.Version + TestSortAuthMethods(t, []*AuthMethod{want, updated}) + assert.Equal(want, updated) + + err := db.TestVerifyOplog(t, rw, updateWith.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_UPDATE), db.WithCreateNotBefore(10*time.Second)) + require.Errorf(err, "should not have found oplog entry for %s", updateWith.PublicId) + default: + require.NoError(err) + require.NotNil(updated) + want := tt.want(orig, updateWith) + want.CreateTime = updated.CreateTime + want.UpdateTime = updated.UpdateTime + want.Version = updated.Version + TestSortAuthMethods(t, []*AuthMethod{want, updated}) + assert.Empty(cmp.Diff(updated.AuthMethod, want.AuthMethod, protocmp.Transform())) + if !tt.wantNoRowsUpdate { + assert.Equal(1, rowsUpdated) + err = db.TestVerifyOplog(t, rw, updateWith.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_UPDATE), db.WithCreateNotBefore(10*time.Second)) + require.NoErrorf(err, "unexpected error verifying oplog entry: %s", err) + } + } + }) + } +} + +func Test_DisableDiscoveredConfigValidation(t *testing.T) { + t.Parallel() + assert, require := assert.New(t), require.New(t) + ctx := context.Background() + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, wrapper) + + tp := oidc.StartTestProvider(t) + tpClientId := "alice-rp" + tpClientSecret := "her-dog's-name" + tp.SetClientCreds(tpClientId, tpClientSecret) + _, _, tpAlg, _ := tp.SigningKeys() + tpCert, err := ParseCertificates(tp.CACert()) + require.NoError(err) + require.Equal(1, len(tpCert)) + + rw := db.New(conn) + repo, err := NewRepository(rw, rw, kmsCache) + require.NoError(err) + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + databaseWrapper, err := kmsCache.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase) + require.NoError(err) + am := TestAuthMethod(t, + conn, databaseWrapper, + org.PublicId, + InactiveState, + "alice-rp", "alice-secret", + WithAudClaims("www.alice.com"), + WithCertificates(tpCert[0]), + WithSigningAlgs(Alg(tpAlg)), + WithIssuer(TestConvertToUrls(t, tp.Addr())[0]), + WithApiUrl(TestConvertToUrls(t, "https://www.alice.com/callback")[0]), + ) + + updateWith := am.Clone() + updateWith.Name = "alice's restaurant" + updatedWithForce, rowsUpdated, err := repo.UpdateAuthMethod(ctx, updateWith, updateWith.Version, []string{"Name"}, WithForce()) + require.NoError(err) + assert.Equal(1, rowsUpdated) + assert.Equal(true, updatedWithForce.DisableDiscoveredConfigValidation) + + updateWith.Name = "alice and eve's restaurant" + updatedWithoutForce, rowsUpdated, err := repo.UpdateAuthMethod(ctx, updatedWithForce, updatedWithForce.Version, []string{"Name"}) + require.NoError(err) + assert.Equal(1, rowsUpdated) + assert.Equal(false, updatedWithoutForce.DisableDiscoveredConfigValidation) + + privWithForce, err := repo.MakePrivate(ctx, updatedWithForce.PublicId, updatedWithoutForce.Version, WithForce()) + require.NoError(err) + assert.Equal(true, privWithForce.DisableDiscoveredConfigValidation) + + privWithoutForce, err := repo.MakePrivate(ctx, privWithForce.PublicId, privWithForce.Version) + require.NoError(err) + assert.Equal(false, privWithoutForce.DisableDiscoveredConfigValidation) + + pubWithForce, err := repo.MakePublic(ctx, privWithoutForce.PublicId, privWithoutForce.Version, WithForce()) + require.NoError(err) + assert.Equal(true, pubWithForce.DisableDiscoveredConfigValidation) + pubWithoutForce, err := repo.MakePrivate(ctx, pubWithForce.PublicId, pubWithForce.Version) + require.NoError(err) + assert.Equal(false, pubWithoutForce.DisableDiscoveredConfigValidation) +} + +func Test_ValidateDiscoveryInfo(t *testing.T) { + // do not run these tests with t.Parallel() + ctx := context.Background() + + tp := oidc.StartTestProvider(t) + tpClientId := "alice-rp" + tpClientSecret := "her-dog's-name" + tp.SetClientCreds(tpClientId, tpClientSecret) + _, _, tpAlg, _ := tp.SigningKeys() + tpCert, err := ParseCertificates(tp.CACert()) + require.NoError(t, err) + require.Equal(t, 1, len(tpCert)) + + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, wrapper) + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + rw := db.New(conn) + repo, err := NewRepository(rw, rw, kmsCache) + require.NoError(t, err) + + databaseWrapper, err := kmsCache.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + port := testutil.TestFreePort(t) + testAuthMethodCallback, err := url.Parse(fmt.Sprintf("http://localhost:%d/callback", port)) + require.NoError(t, err) + testAuthMethod := TestAuthMethod(t, + conn, databaseWrapper, + org.PublicId, + ActivePrivateState, + tpClientId, ClientSecret(tpClientSecret), + WithCertificates(tpCert[0]), + WithSigningAlgs(Alg(tpAlg)), + WithIssuer(TestConvertToUrls(t, tp.Addr())[0]), + WithApiUrl(testAuthMethodCallback), + ) + tests := []struct { + name string + setup func() + cleanup func() + authMethod *AuthMethod + withAuthMethod bool + withPublicId bool + wantErrMatch *errors.Template + wantErrContains string + }{ + { + name: "simple-and-valid", + authMethod: testAuthMethod, + withAuthMethod: true, + }, + { + name: "simple-and-valid", + authMethod: testAuthMethod, + withPublicId: true, + }, + { + name: "missing-withPublicId-or-withAuthMethod", + authMethod: testAuthMethod, + wantErrMatch: errors.T(errors.InvalidParameter), + }, + { + name: "not-complete", + authMethod: func() *AuthMethod { cp := testAuthMethod.Clone(); cp.SigningAlgs = nil; return cp }(), + withAuthMethod: true, + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: " missing signing algorithms", + }, + { + name: "no-discovery", + authMethod: func() *AuthMethod { + cp := testAuthMethod.Clone() + port := testutil.TestFreePort(t) + cp.Issuer = fmt.Sprintf("http://localhost:%d", port) + return cp + }(), + withAuthMethod: true, + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "AuthMethod cannot be converted to a valid OIDC Provider", + }, + { + name: "fail-jwks", + setup: func() { tp.SetDisableJWKs(true) }, + cleanup: func() { tp.SetDisableJWKs(false) }, + authMethod: testAuthMethod, + withAuthMethod: true, + wantErrMatch: errors.T(errors.Unknown), + wantErrContains: "non-200", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + var opts []Option + switch { + case tt.withAuthMethod: + opts = append(opts, WithAuthMethod(tt.authMethod)) + case tt.withPublicId: + opts = append(opts, WithPublicId(tt.authMethod.PublicId)) + } + if tt.setup != nil { + tt.setup() + defer tt.cleanup() + } + err := repo.ValidateDiscoveryInfo(ctx, opts...) + if tt.wantErrMatch != nil { + require.Error(err) + assert.Truef(errors.Match(tt.wantErrMatch, err), "want err code: %q got: %q", tt.wantErrMatch.Code, err) + if tt.wantErrContains != "" { + assert.Containsf(err.Error(), tt.wantErrContains, "want err to contain %s got: %s", tt.wantErrContains, err.Error()) + } + return + } + require.NoError(err) + }) + } +} + +func Test_valueObjectChanges(t *testing.T) { + t.Parallel() + _, pem1 := testGenerateCA(t, "localhost") + _, pem2 := testGenerateCA(t, "127.0.0.1") + _, pem3 := testGenerateCA(t, "www.example.com") + tests := []struct { + name string + id string + voName voName + new []string + old []string + dbMask []string + nullFields []string + wantAdd []interface{} + wantDel []interface{} + wantErrMatch *errors.Template + }{ + { + name: string(SigningAlgVO), + id: "am-public-id", + voName: SigningAlgVO, + new: []string{"ES256", "ES384"}, + old: []string{"RS256", "RS384", "RS512"}, + dbMask: []string{string(SigningAlgVO)}, + wantAdd: func() []interface{} { + a, err := NewSigningAlg("am-public-id", ES256) + require.NoError(t, err) + a2, err := NewSigningAlg("am-public-id", ES384) + require.NoError(t, err) + return []interface{}{a, a2} + }(), + wantDel: func() []interface{} { + a, err := NewSigningAlg("am-public-id", RS256) + require.NoError(t, err) + a2, err := NewSigningAlg("am-public-id", RS384) + require.NoError(t, err) + a3, err := NewSigningAlg("am-public-id", RS512) + require.NoError(t, err) + return []interface{}{a, a2, a3} + }(), + }, + { + name: string(CertificateVO), + id: "am-public-id", + voName: CertificateVO, + new: []string{pem1, pem2}, + old: []string{pem3}, + dbMask: []string{string(CertificateVO)}, + wantAdd: func() []interface{} { + c, err := NewCertificate("am-public-id", pem1) + require.NoError(t, err) + c2, err := NewCertificate("am-public-id", pem2) + require.NoError(t, err) + return []interface{}{c, c2} + }(), + wantDel: func() []interface{} { + c, err := NewCertificate("am-public-id", pem3) + require.NoError(t, err) + return []interface{}{c} + }(), + }, + { + name: string(AudClaimVO), + id: "am-public-id", + voName: AudClaimVO, + new: []string{"new-aud1", "new-aud2"}, + old: []string{"old-aud1", "old-aud2", "old-aud3"}, + dbMask: []string{string(AudClaimVO)}, + wantAdd: func() []interface{} { + a, err := NewAudClaim("am-public-id", "new-aud1") + require.NoError(t, err) + a2, err := NewAudClaim("am-public-id", "new-aud2") + require.NoError(t, err) + return []interface{}{a, a2} + }(), + wantDel: func() []interface{} { + a, err := NewAudClaim("am-public-id", "old-aud1") + require.NoError(t, err) + a2, err := NewAudClaim("am-public-id", "old-aud2") + require.NoError(t, err) + a3, err := NewAudClaim("am-public-id", "old-aud3") + require.NoError(t, err) + return []interface{}{a, a2, a3} + }(), + }, + + { + name: string(AudClaimVO) + "-null-fields", + id: "am-public-id", + voName: AudClaimVO, + new: nil, + old: []string{"old-aud1", "old-aud2", "old-aud3"}, + nullFields: []string{string(AudClaimVO)}, + wantDel: func() []interface{} { + a, err := NewAudClaim("am-public-id", "old-aud1") + require.NoError(t, err) + a2, err := NewAudClaim("am-public-id", "old-aud2") + require.NoError(t, err) + a3, err := NewAudClaim("am-public-id", "old-aud3") + require.NoError(t, err) + return []interface{}{a, a2, a3} + }(), + }, + { + name: "missing-public-id", + voName: AudClaimVO, + new: nil, + old: []string{"old-aud1", "old-aud2", "old-aud3"}, + nullFields: []string{string(AudClaimVO)}, + wantDel: func() []interface{} { + a, err := NewAudClaim("am-public-id", "old-aud1") + require.NoError(t, err) + a2, err := NewAudClaim("am-public-id", "old-aud2") + require.NoError(t, err) + a3, err := NewAudClaim("am-public-id", "old-aud3") + require.NoError(t, err) + return []interface{}{a, a2, a3} + }(), + wantErrMatch: errors.T(errors.InvalidParameter), + }, + { + name: "invalid-vo-name", + voName: voName("invalid-name"), + id: "am-public-id", + new: nil, + old: []string{"old-aud1", "old-aud2", "old-aud3"}, + nullFields: []string{string(AudClaimVO)}, + wantDel: func() []interface{} { + a, err := NewAudClaim("am-public-id", "old-aud1") + require.NoError(t, err) + a2, err := NewAudClaim("am-public-id", "old-aud2") + require.NoError(t, err) + a3, err := NewAudClaim("am-public-id", "old-aud3") + require.NoError(t, err) + return []interface{}{a, a2, a3} + }(), + wantErrMatch: errors.T(errors.InvalidParameter), + }, + { + name: "dup-new", + id: "am-public-id", + voName: SigningAlgVO, + new: []string{"ES256", "ES256"}, + old: []string{"RS256", "RS384", "RS512"}, + dbMask: []string{string(SigningAlgVO)}, + wantAdd: func() []interface{} { + a, err := NewSigningAlg("am-public-id", ES256) + require.NoError(t, err) + a2, err := NewSigningAlg("am-public-id", ES384) + require.NoError(t, err) + return []interface{}{a, a2} + }(), + wantDel: func() []interface{} { + a, err := NewSigningAlg("am-public-id", RS256) + require.NoError(t, err) + a2, err := NewSigningAlg("am-public-id", RS384) + require.NoError(t, err) + a3, err := NewSigningAlg("am-public-id", RS512) + require.NoError(t, err) + return []interface{}{a, a2, a3} + }(), + wantErrMatch: errors.T(errors.InvalidParameter), + }, + { + name: "dup-old", + id: "am-public-id", + voName: SigningAlgVO, + new: []string{"ES256", "ES384"}, + old: []string{"RS256", "RS256", "RS512"}, + dbMask: []string{string(SigningAlgVO)}, + wantAdd: func() []interface{} { + a, err := NewSigningAlg("am-public-id", ES256) + require.NoError(t, err) + a2, err := NewSigningAlg("am-public-id", ES384) + require.NoError(t, err) + return []interface{}{a, a2} + }(), + wantDel: func() []interface{} { + a, err := NewSigningAlg("am-public-id", RS256) + require.NoError(t, err) + a2, err := NewSigningAlg("am-public-id", RS384) + require.NoError(t, err) + a3, err := NewSigningAlg("am-public-id", RS512) + require.NoError(t, err) + return []interface{}{a, a2, a3} + }(), + wantErrMatch: errors.T(errors.InvalidParameter), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + gotAdd, gotDel, err := valueObjectChanges(tt.id, tt.voName, tt.new, tt.old, tt.dbMask, tt.nullFields) + if tt.wantErrMatch != nil { + require.Error(err) + assert.Truef(errors.Match(tt.wantErrMatch, err), "want err code: %q got: %q", tt.wantErrMatch.Code, err) + return + } + require.NoError(err) + assert.Equal(tt.wantAdd, gotAdd) + + switch tt.voName { + case CertificateVO: + sort.Slice(gotDel, func(a, b int) bool { + aa := gotDel[a] + bb := gotDel[b] + return aa.(*Certificate).Cert < bb.(*Certificate).Cert + }) + case SigningAlgVO: + sort.Slice(gotDel, func(a, b int) bool { + aa := gotDel[a] + bb := gotDel[b] + return aa.(*SigningAlg).Alg < bb.(*SigningAlg).Alg + }) + case AudClaimVO: + sort.Slice(gotDel, func(a, b int) bool { + aa := gotDel[a] + bb := gotDel[b] + return aa.(*AudClaim).Aud < bb.(*AudClaim).Aud + }) + } + assert.Equalf(tt.wantDel, gotDel, "wantDel: %s\ngotDel: %s\n", tt.wantDel, gotDel) + }) + } +} + +func Test_validateFieldMask(t *testing.T) { + t.Parallel() + tests := []struct { + name string + fieldMask []string + wantErr bool + }{ + { + name: "all-valid-fields", + fieldMask: []string{ + "Name", + "Description", + "Issuer", + "ClientId", + "ClientSecret", + "MaxAge", + "SigningAlgs", + "ApiUrl", + "AudClaims", + "Certificates", + }, + }, + { + name: "invalid", + fieldMask: []string{"Invalid", "Name"}, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + require := require.New(t) + err := validateFieldMask(tt.fieldMask) + if tt.wantErr { + require.Error(err) + return + } + require.NoError(err) + }) + } +} + +func Test_applyUpdate(t *testing.T) { + t.Parallel() + tests := []struct { + name string + new *AuthMethod + orig *AuthMethod + fieldMask []string + want *AuthMethod + }{ + { + name: "valid-all-fields", + new: &AuthMethod{ + AuthMethod: &store.AuthMethod{ + Name: "new-name", + Description: "new-description", + OperationalState: string(ActivePublicState), + Issuer: "new-issuer", + ClientId: "new-client-id", + ClientSecret: "new-client-secret", + MaxAge: 100, + SigningAlgs: []string{"new-alg1", "new-alg2"}, + ApiUrl: "new-callback-1", + AudClaims: []string{"new-aud-1", "new-aud-2"}, + Certificates: []string{"new-pem1", "new-pem-2"}, + }, + }, + orig: &AuthMethod{ + AuthMethod: &store.AuthMethod{ + Name: "orig-name", + Description: "orig-description", + OperationalState: string(InactiveState), + Issuer: "orig-issuer", + ClientId: "orig-client-id", + ClientSecret: "orig-client-secret", + MaxAge: 100, + SigningAlgs: []string{"orig-alg1", "orig-alg2"}, + ApiUrl: "orig-callback-1", + AudClaims: []string{"orig-aud-1", "orig-aud-2"}, + Certificates: []string{"orig-pem1", "orig-pem-2"}, + }, + }, + want: &AuthMethod{ + AuthMethod: &store.AuthMethod{ + Name: "new-name", + Description: "new-description", + OperationalState: string(InactiveState), + Issuer: "new-issuer", + ClientId: "new-client-id", + ClientSecret: "new-client-secret", + MaxAge: 100, + SigningAlgs: []string{"new-alg1", "new-alg2"}, + ApiUrl: "new-callback-1", + AudClaims: []string{"new-aud-1", "new-aud-2"}, + Certificates: []string{"new-pem1", "new-pem-2"}, + }, + }, + fieldMask: []string{ + "Name", + "Description", + "Issuer", + "ClientId", + "ClientSecret", + "MaxAge", + "SigningAlgs", + "ApiUrl", + "AudClaims", + "Certificates", + }, + }, + { + name: "nil-value-objects", + new: &AuthMethod{ + AuthMethod: &store.AuthMethod{ + Name: "new-name", + Description: "new-description", + OperationalState: string(ActivePublicState), + Issuer: "new-issuer", + ClientId: "new-client-id", + ClientSecret: "new-client-secret", + MaxAge: 100, + }, + }, + orig: &AuthMethod{ + AuthMethod: &store.AuthMethod{ + Name: "orig-name", + Description: "orig-description", + OperationalState: string(InactiveState), + Issuer: "orig-issuer", + ClientId: "orig-client-id", + ClientSecret: "orig-client-secret", + MaxAge: 100, + SigningAlgs: []string{"orig-alg1", "orig-alg2"}, + ApiUrl: "orig-callback-1", + AudClaims: []string{"orig-aud-1", "orig-aud-2"}, + Certificates: []string{"orig-pem1", "orig-pem-2"}, + }, + }, + want: &AuthMethod{ + AuthMethod: &store.AuthMethod{ + Name: "new-name", + Description: "new-description", + OperationalState: string(InactiveState), + Issuer: "new-issuer", + ClientId: "new-client-id", + ClientSecret: "new-client-secret", + MaxAge: 100, + }, + }, + fieldMask: []string{ + "Name", + "Description", + "Issuer", + "ClientId", + "ClientSecret", + "MaxAge", + "SigningAlgs", + "ApiUrl", + "AudClaims", + "Certificates", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert := assert.New(t) + got := applyUpdate(tt.new, tt.orig, tt.fieldMask) + assert.Equal(got, tt.want) + }) + } +} + +type mockClient struct { + mockDo func(req *http.Request) (*http.Response, error) +} + +// Overriding what the Do function should "do" in our MockClient +func (m *mockClient) Do(req *http.Request) (*http.Response, error) { + return m.mockDo(req) +} + +func Test_pingEndpoint(t *testing.T) { + t.Parallel() + ctx := context.Background() + tests := []struct { + name string + setup func() (HTTPClient, string, string) + wantStatus int + wantErr bool + }{ + { + name: "valid-endpoint", + setup: func() (HTTPClient, string, string) { + client := &mockClient{ + mockDo: func(*http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: 200, + }, nil + }, + } + return client, http.MethodGet, "http://localhost/get" + }, + wantStatus: 200, + }, + { + name: "valid-500", + setup: func() (HTTPClient, string, string) { + client := &mockClient{ + mockDo: func(*http.Request) (*http.Response, error) { + return &http.Response{ + StatusCode: 500, + }, nil + }, + } + return client, http.MethodGet, "http://localhost/get" + }, + wantStatus: 500, + }, + { + name: "failed", + setup: func() (HTTPClient, string, string) { + client := &mockClient{ + mockDo: func(*http.Request) (*http.Response, error) { + return nil, fmt.Errorf("invalid request") + }, + } + return client, http.MethodGet, "http://localhost/get" + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + client, method, url := tt.setup() + gotStatus, err := pingEndpoint(ctx, client, tt.name, method, url) + assert.Equal(gotStatus, tt.wantStatus) + if tt.wantErr { + require.Error(err) + return + } + require.NoError(err) + }) + } +} diff --git a/internal/auth/oidc/repository_test.go b/internal/auth/oidc/repository_test.go new file mode 100644 index 000000000..99e95bd7c --- /dev/null +++ b/internal/auth/oidc/repository_test.go @@ -0,0 +1,116 @@ +package oidc + +import ( + "testing" + + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/kms" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNewRepository(t *testing.T) { + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, wrapper) + + type args struct { + r db.Reader + w db.Writer + kms *kms.Kms + opts []Option + } + tests := []struct { + name string + args args + want *Repository + wantErrMatch *errors.Template + }{ + { + name: "valid", + args: args{ + r: rw, + w: rw, + kms: kmsCache, + }, + want: &Repository{ + reader: rw, + writer: rw, + kms: kmsCache, + defaultLimit: db.DefaultLimit, + }, + }, + { + name: "valid with limit", + args: args{ + r: rw, + w: rw, + kms: kmsCache, + opts: []Option{WithLimit(5)}, + }, + want: &Repository{ + reader: rw, + writer: rw, + kms: kmsCache, + defaultLimit: 5, + }, + }, + { + name: "nil-reader", + args: args{ + r: nil, + w: rw, + kms: kmsCache, + }, + want: nil, + wantErrMatch: errors.T(errors.InvalidParameter), + }, + { + name: "nil-writer", + args: args{ + r: rw, + w: nil, + kms: kmsCache, + }, + want: nil, + wantErrMatch: errors.T(errors.InvalidParameter), + }, + { + name: "nil-wrapper", + args: args{ + r: rw, + w: rw, + kms: nil, + }, + want: nil, + wantErrMatch: errors.T(errors.InvalidParameter), + }, + { + name: "all-nils", + args: args{ + r: nil, + w: nil, + kms: nil, + }, + want: nil, + wantErrMatch: errors.T(errors.InvalidParameter), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + got, err := NewRepository(tt.args.r, tt.args.w, tt.args.kms, tt.args.opts...) + if tt.wantErrMatch != nil { + require.Error(err) + assert.Truef(errors.Match(tt.wantErrMatch, err), "want err code: %q got: %q", tt.wantErrMatch, err) + assert.Nil(got) + return + } + require.NoError(err) + require.NotNil(got) + assert.Equal(tt.want, got) + }) + } +} diff --git a/internal/auth/oidc/request/request.pb.go b/internal/auth/oidc/request/request.pb.go new file mode 100644 index 000000000..99cb7565c --- /dev/null +++ b/internal/auth/oidc/request/request.pb.go @@ -0,0 +1,426 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.26.0 +// protoc v3.12.4 +// source: controller/storage/auth/oidc/request/v1/request.proto + +// Package store provides protobufs for storing types in the password package. + +package request + +import ( + _ "github.com/golang/protobuf/ptypes/wrappers" + timestamp "github.com/hashicorp/boundary/internal/db/timestamp" + _ "github.com/hashicorp/boundary/internal/gen/controller/protooptions" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// First, State is used in constructing the authorization URL, in the first +// leg of the authen flow. State represents the unique data used to construct +// an oidc.Request (see: https://github.com/hashicorp/cap/blob/main/oidc/request.go). +// State needs enough information, that when combined with a Boundary oidc auth method, +// a proper oidc.Request can be recreated during the second leg of the authen flow. State +// also needs the provider.ConfigHash() used to from the first leg, so it can verify +// the Boundary's oidc auth method configuration hasn't changed since the authen flow +// began. +type State struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // token_request_id is the id. This id is used by the client to poll for a Boundary + // token, once the final leg of the authen flow is compeleted. The Callback uses this + // id to create a "pending" token for that polling process. + TokenRequestId string `protobuf:"bytes,10,opt,name=token_request_id,json=tokenRequestId,proto3" json:"token_request_id,omitempty"` + // create_time of the request that started the authentication flow. + CreateTime *timestamp.Timestamp `protobuf:"bytes,20,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty"` + // expiration_time of the authenticaion flow. + ExpirationTime *timestamp.Timestamp `protobuf:"bytes,30,opt,name=expiration_time,json=expirationTime,proto3" json:"expiration_time,omitempty"` + // final_redirect_url that will be sent back to the client after the callback + FinalRedirectUrl string `protobuf:"bytes,40,opt,name=final_redirect_url,json=finalRedirectUrl,proto3" json:"final_redirect_url,omitempty"` + // nonce of the request which is used to verify the ID Token in the third leg + // as a way to prevent replay attacks. + // + // See https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest + // and https://openid.net/specs/openid-connect-core-1_0.html#NonceNotes. + Nonce string `protobuf:"bytes,50,opt,name=nonce,proto3" json:"nonce,omitempty"` + // provider_config_hash can be used to see if the provider's config has changed + // since the request started. + ProviderConfigHash uint64 `protobuf:"varint,60,opt,name=provider_config_hash,json=providerConfigHash,proto3" json:"provider_config_hash,omitempty"` +} + +func (x *State) Reset() { + *x = State{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_storage_auth_oidc_request_v1_request_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *State) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*State) ProtoMessage() {} + +func (x *State) ProtoReflect() protoreflect.Message { + mi := &file_controller_storage_auth_oidc_request_v1_request_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use State.ProtoReflect.Descriptor instead. +func (*State) Descriptor() ([]byte, []int) { + return file_controller_storage_auth_oidc_request_v1_request_proto_rawDescGZIP(), []int{0} +} + +func (x *State) GetTokenRequestId() string { + if x != nil { + return x.TokenRequestId + } + return "" +} + +func (x *State) GetCreateTime() *timestamp.Timestamp { + if x != nil { + return x.CreateTime + } + return nil +} + +func (x *State) GetExpirationTime() *timestamp.Timestamp { + if x != nil { + return x.ExpirationTime + } + return nil +} + +func (x *State) GetFinalRedirectUrl() string { + if x != nil { + return x.FinalRedirectUrl + } + return "" +} + +func (x *State) GetNonce() string { + if x != nil { + return x.Nonce + } + return "" +} + +func (x *State) GetProviderConfigHash() uint64 { + if x != nil { + return x.ProviderConfigHash + } + return 0 +} + +// Token is the request token that's returned as part of the auth_token_url from +// oidc.StartAuth(...) +type Token struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // request_id for the token. + RequestId string `protobuf:"bytes,10,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` + // expiration_time of the authenticaion flow. + ExpirationTime *timestamp.Timestamp `protobuf:"bytes,20,opt,name=expiration_time,json=expirationTime,proto3" json:"expiration_time,omitempty"` +} + +func (x *Token) Reset() { + *x = Token{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_storage_auth_oidc_request_v1_request_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Token) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Token) ProtoMessage() {} + +func (x *Token) ProtoReflect() protoreflect.Message { + mi := &file_controller_storage_auth_oidc_request_v1_request_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Token.ProtoReflect.Descriptor instead. +func (*Token) Descriptor() ([]byte, []int) { + return file_controller_storage_auth_oidc_request_v1_request_proto_rawDescGZIP(), []int{1} +} + +func (x *Token) GetRequestId() string { + if x != nil { + return x.RequestId + } + return "" +} + +func (x *Token) GetExpirationTime() *timestamp.Timestamp { + if x != nil { + return x.ExpirationTime + } + return nil +} + +// Wrapper wraps an encrypted cipher text with non-sensitive info +// which allows Boundary to determine how to decrypt +// the wrappered cipher text (ct) field. +type Wrapper struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // auth_method_id is the auth method of the oidc request + AuthMethodId string `protobuf:"bytes,10,opt,name=auth_method_id,json=authMethodId,proto3" json:"auth_method_id,omitempty"` + // scope_id is the auth method's scope + ScopeId string `protobuf:"bytes,20,opt,name=scope_id,json=scopeId,proto3" json:"scope_id,omitempty"` + // wrapper_key_id is the DEK wrapper key id which was used to derive the + // cipher's key + WrapperKeyId string `protobuf:"bytes,30,opt,name=wrapper_key_id,json=wrapperKeyId,proto3" json:"wrapper_key_id,omitempty"` + // ct is the encrypted cipher text + Ct []byte `protobuf:"bytes,40,opt,name=ct,proto3" json:"ct,omitempty"` +} + +func (x *Wrapper) Reset() { + *x = Wrapper{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_storage_auth_oidc_request_v1_request_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Wrapper) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Wrapper) ProtoMessage() {} + +func (x *Wrapper) ProtoReflect() protoreflect.Message { + mi := &file_controller_storage_auth_oidc_request_v1_request_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Wrapper.ProtoReflect.Descriptor instead. +func (*Wrapper) Descriptor() ([]byte, []int) { + return file_controller_storage_auth_oidc_request_v1_request_proto_rawDescGZIP(), []int{2} +} + +func (x *Wrapper) GetAuthMethodId() string { + if x != nil { + return x.AuthMethodId + } + return "" +} + +func (x *Wrapper) GetScopeId() string { + if x != nil { + return x.ScopeId + } + return "" +} + +func (x *Wrapper) GetWrapperKeyId() string { + if x != nil { + return x.WrapperKeyId + } + return "" +} + +func (x *Wrapper) GetCt() []byte { + if x != nil { + return x.Ct + } + return nil +} + +var File_controller_storage_auth_oidc_request_v1_request_proto protoreflect.FileDescriptor + +var file_controller_storage_auth_oidc_request_v1_request_proto_rawDesc = []byte{ + 0x0a, 0x35, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x73, 0x74, 0x6f, + 0x72, 0x61, 0x67, 0x65, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x2f, 0x6f, 0x69, 0x64, 0x63, 0x2f, 0x72, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2f, 0x76, 0x31, 0x2f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x27, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, + 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x61, 0x75, 0x74, 0x68, + 0x2e, 0x6f, 0x69, 0x64, 0x63, 0x2e, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x76, 0x31, + 0x1a, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x73, 0x74, 0x6f, + 0x72, 0x61, 0x67, 0x65, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2f, 0x76, + 0x31, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x1a, 0x2a, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x63, 0x75, + 0x73, 0x74, 0x6f, 0x6d, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x76, 0x31, 0x2f, + 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1e, 0x67, + 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x77, + 0x72, 0x61, 0x70, 0x70, 0x65, 0x72, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xc9, 0x02, + 0x0a, 0x05, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x28, 0x0a, 0x10, 0x74, 0x6f, 0x6b, 0x65, 0x6e, + 0x5f, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0e, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x49, + 0x64, 0x12, 0x4b, 0x0a, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, + 0x18, 0x14, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, + 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, + 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, + 0x6d, 0x70, 0x52, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x53, + 0x0a, 0x0f, 0x65, 0x78, 0x70, 0x69, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x74, 0x69, 0x6d, + 0x65, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, + 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, + 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, + 0x61, 0x6d, 0x70, 0x52, 0x0e, 0x65, 0x78, 0x70, 0x69, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x54, + 0x69, 0x6d, 0x65, 0x12, 0x2c, 0x0a, 0x12, 0x66, 0x69, 0x6e, 0x61, 0x6c, 0x5f, 0x72, 0x65, 0x64, + 0x69, 0x72, 0x65, 0x63, 0x74, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x28, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x10, 0x66, 0x69, 0x6e, 0x61, 0x6c, 0x52, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x55, 0x72, + 0x6c, 0x12, 0x14, 0x0a, 0x05, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x18, 0x32, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x05, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x12, 0x30, 0x0a, 0x14, 0x70, 0x72, 0x6f, 0x76, 0x69, + 0x64, 0x65, 0x72, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, + 0x3c, 0x20, 0x01, 0x28, 0x04, 0x52, 0x12, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x43, + 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x48, 0x61, 0x73, 0x68, 0x22, 0x7b, 0x0a, 0x05, 0x54, 0x6f, 0x6b, + 0x65, 0x6e, 0x12, 0x1d, 0x0a, 0x0a, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, 0x69, 0x64, + 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x49, + 0x64, 0x12, 0x53, 0x0a, 0x0f, 0x65, 0x78, 0x70, 0x69, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, + 0x74, 0x69, 0x6d, 0x65, 0x18, 0x14, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, + 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, + 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, + 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0e, 0x65, 0x78, 0x70, 0x69, 0x72, 0x61, 0x74, 0x69, + 0x6f, 0x6e, 0x54, 0x69, 0x6d, 0x65, 0x22, 0x80, 0x01, 0x0a, 0x07, 0x57, 0x72, 0x61, 0x70, 0x70, + 0x65, 0x72, 0x12, 0x24, 0x0a, 0x0e, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, + 0x64, 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x61, 0x75, 0x74, 0x68, + 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x49, 0x64, 0x12, 0x19, 0x0a, 0x08, 0x73, 0x63, 0x6f, 0x70, + 0x65, 0x5f, 0x69, 0x64, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x63, 0x6f, 0x70, + 0x65, 0x49, 0x64, 0x12, 0x24, 0x0a, 0x0e, 0x77, 0x72, 0x61, 0x70, 0x70, 0x65, 0x72, 0x5f, 0x6b, + 0x65, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x77, 0x72, 0x61, + 0x70, 0x70, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x49, 0x64, 0x12, 0x0e, 0x0a, 0x02, 0x63, 0x74, 0x18, + 0x28, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x02, 0x63, 0x74, 0x42, 0x42, 0x5a, 0x40, 0x67, 0x69, 0x74, + 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, + 0x70, 0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, + 0x6e, 0x61, 0x6c, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x2f, 0x6f, 0x69, 0x64, 0x63, 0x2f, 0x72, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x3b, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x62, 0x06, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_controller_storage_auth_oidc_request_v1_request_proto_rawDescOnce sync.Once + file_controller_storage_auth_oidc_request_v1_request_proto_rawDescData = file_controller_storage_auth_oidc_request_v1_request_proto_rawDesc +) + +func file_controller_storage_auth_oidc_request_v1_request_proto_rawDescGZIP() []byte { + file_controller_storage_auth_oidc_request_v1_request_proto_rawDescOnce.Do(func() { + file_controller_storage_auth_oidc_request_v1_request_proto_rawDescData = protoimpl.X.CompressGZIP(file_controller_storage_auth_oidc_request_v1_request_proto_rawDescData) + }) + return file_controller_storage_auth_oidc_request_v1_request_proto_rawDescData +} + +var file_controller_storage_auth_oidc_request_v1_request_proto_msgTypes = make([]protoimpl.MessageInfo, 3) +var file_controller_storage_auth_oidc_request_v1_request_proto_goTypes = []interface{}{ + (*State)(nil), // 0: controller.storage.auth.oidc.request.v1.State + (*Token)(nil), // 1: controller.storage.auth.oidc.request.v1.Token + (*Wrapper)(nil), // 2: controller.storage.auth.oidc.request.v1.Wrapper + (*timestamp.Timestamp)(nil), // 3: controller.storage.timestamp.v1.Timestamp +} +var file_controller_storage_auth_oidc_request_v1_request_proto_depIdxs = []int32{ + 3, // 0: controller.storage.auth.oidc.request.v1.State.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 3, // 1: controller.storage.auth.oidc.request.v1.State.expiration_time:type_name -> controller.storage.timestamp.v1.Timestamp + 3, // 2: controller.storage.auth.oidc.request.v1.Token.expiration_time:type_name -> controller.storage.timestamp.v1.Timestamp + 3, // [3:3] is the sub-list for method output_type + 3, // [3:3] is the sub-list for method input_type + 3, // [3:3] is the sub-list for extension type_name + 3, // [3:3] is the sub-list for extension extendee + 0, // [0:3] is the sub-list for field type_name +} + +func init() { file_controller_storage_auth_oidc_request_v1_request_proto_init() } +func file_controller_storage_auth_oidc_request_v1_request_proto_init() { + if File_controller_storage_auth_oidc_request_v1_request_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_controller_storage_auth_oidc_request_v1_request_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*State); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_controller_storage_auth_oidc_request_v1_request_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Token); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_controller_storage_auth_oidc_request_v1_request_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Wrapper); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_controller_storage_auth_oidc_request_v1_request_proto_rawDesc, + NumEnums: 0, + NumMessages: 3, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_controller_storage_auth_oidc_request_v1_request_proto_goTypes, + DependencyIndexes: file_controller_storage_auth_oidc_request_v1_request_proto_depIdxs, + MessageInfos: file_controller_storage_auth_oidc_request_v1_request_proto_msgTypes, + }.Build() + File_controller_storage_auth_oidc_request_v1_request_proto = out.File + file_controller_storage_auth_oidc_request_v1_request_proto_rawDesc = nil + file_controller_storage_auth_oidc_request_v1_request_proto_goTypes = nil + file_controller_storage_auth_oidc_request_v1_request_proto_depIdxs = nil +} diff --git a/internal/auth/oidc/request/validate.go b/internal/auth/oidc/request/validate.go new file mode 100644 index 000000000..051b05a3c --- /dev/null +++ b/internal/auth/oidc/request/validate.go @@ -0,0 +1,66 @@ +package request + +import "github.com/hashicorp/boundary/internal/errors" + +// Validate the request.State +func (s *State) Validate() error { + const op = "request.(State).Validate" + if s == nil { + return errors.New(errors.InvalidParameter, op, "missing state") + } + if s.TokenRequestId == "" { + return errors.New(errors.InvalidParameter, op, "missing token request id") + } + if s.CreateTime == nil { + return errors.New(errors.InvalidParameter, op, "missing create time") + } + if s.ExpirationTime == nil { + return errors.New(errors.InvalidParameter, op, "missing expiration time") + } + if s.FinalRedirectUrl == "" { + return errors.New(errors.InvalidParameter, op, "missing final redirect URL") + } + if s.Nonce == "" { + return errors.New(errors.InvalidParameter, op, "missing nonce") + } + if s.ProviderConfigHash == 0 { + return errors.New(errors.InvalidParameter, op, "missing provider config hash") + } + return nil +} + +// Validate the request.Wrapper +func (w *Wrapper) Validate() error { + const op = "request.(Wrapper).Validate" + if w == nil { + return errors.New(errors.InvalidParameter, op, "missing wrapper") + } + if w.AuthMethodId == "" { + return errors.New(errors.InvalidParameter, op, "missing auth method id") + } + if w.ScopeId == "" { + return errors.New(errors.InvalidParameter, op, "missing scope id") + } + if w.WrapperKeyId == "" { + return errors.New(errors.InvalidParameter, op, "missing wrapper key id") + } + if len(w.Ct) == 0 { + return errors.New(errors.InvalidParameter, op, "missing ct") + } + return nil +} + +// Validate the request.Token +func (t *Token) Validate() error { + const op = "request.(Token).Validate" + if t == nil { + return errors.New(errors.InvalidParameter, op, "missing token") + } + if t.RequestId == "" { + return errors.New(errors.InvalidParameter, op, "missing request id") + } + if t.ExpirationTime == nil { + return errors.New(errors.InvalidParameter, op, "missing expiration time") + } + return nil +} diff --git a/internal/auth/oidc/service.go b/internal/auth/oidc/service.go new file mode 100644 index 000000000..082f12a03 --- /dev/null +++ b/internal/auth/oidc/service.go @@ -0,0 +1,244 @@ +package oidc + +import ( + "context" + "crypto/ed25519" + "fmt" + "time" + + "github.com/hashicorp/boundary/internal/auth/oidc/request" + "github.com/hashicorp/boundary/internal/authtoken" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/kms" + wrapping "github.com/hashicorp/go-kms-wrapping" + "github.com/hashicorp/go-kms-wrapping/wrappers/aead" + + "github.com/mr-tron/base58" + "google.golang.org/protobuf/proto" +) + +const ( + // AttemptExpiration defines the TTL for an authentication attempt + AttemptExpiration = 5 * 60 * time.Second + + // FinalRedirectEndpoint is the endpoint that the oidc callback redirect + // client to after the callback is complete. + FinalRedirectEndpoint = "%s/authentication-complete" // TODO jimlambrt 2/2021 get redirect from FE before PR + + // AuthenticationErrorsEndpoint is the endpoint that will returned as the final redirect + // from the callback when there are auth errors + AuthenticationErrorsEndpoint = "%s/authentication-error" // TODO jimlambrt 3/2021 get endpoint from FE before PR + + // CallbackEndpoint is the endpoint for the oidc callback which will be + // included in the auth URL returned when an authen attempted is kicked off. + CallbackEndpoint = "%s/v1/auth-methods/%s:authenticate:callback" // TODO jimlambrt 2/2021 get endpoint from Todd before PR + + // TokenEndpoint is the endpoint the client will poll to see if their + // authentication attempt has completed and if successful, they'll + // retrieve their Boundary token. + TokenEndpoint = "%s/v1/auth-methods/%s:authenticate:token" // TODO jimlambrt 2/2021 get endpoint from Todd before PR +) + +type ( + // OidcRepoFactory is used by "service functions" to create a new oidc repo + OidcRepoFactory func() (*Repository, error) + + // IamRepoFactory is used by "service functions" to create a new iam repo + IamRepoFactory func() (*iam.Repository, error) + + // AuthTokenRepoFactory is used by "service functions" to create a new auth token repo + AuthTokenRepoFactory func() (*authtoken.Repository, error) +) + +// validator defines an optional interface that proto messages can implement +// which will allow encryptMessage to validate them before encryption. +type validator interface { + Validate() error +} + +// encryptMessage will encrypt the message. The encrypted message will be wrapped in a +// request.Wrapper and then encoded into the returned string. This function +// supports encrypting: request.State and request.Token messages. proto Messages should +// implement the validator interface, so they can be validated before encryption. +func encryptMessage(ctx context.Context, wrapper wrapping.Wrapper, am *AuthMethod, m proto.Message) (encodedEncryptedState string, e error) { + const op = "oidc.encryptMessage" + if wrapper == nil { + return "", errors.New(errors.InvalidParameter, op, "missing wrapper") + } + if wrapper.KeyID() == "" { + return "", errors.New(errors.InvalidParameter, op, "missing wrapper key id") + } + if am == nil || am.AuthMethod == nil { + return "", errors.New(errors.InvalidParameter, op, "missing auth method") + } + if am.ScopeId == "" { + return "", errors.New(errors.InvalidParameter, op, "missing auth method scope id") + } + if am.PublicId == "" { + return "", errors.New(errors.InvalidParameter, op, "missing auth method public id") + } + if m == nil { + return "", errors.New(errors.InvalidParameter, op, "missing message to encrypt") + } + switch v := m.(type) { + case *request.State, *request.Token: + default: + return "", errors.New(errors.InvalidParameter, op, fmt.Sprintf("unsupported message type %v for encryption", v)) + } + + v, ok := m.(validator) + if ok { + if err := v.Validate(); err != nil { + return "", errors.Wrap(err, op) + } + } + marshaled, err := proto.Marshal(m) + if err != nil { + return "", errors.Wrap(err, op, errors.WithMsg("unable to marshal message"), errors.WithCode(errors.Encode)) + } + blobInfo, err := wrapper.Encrypt(ctx, marshaled, []byte(fmt.Sprintf("%s%s", am.PublicId, am.ScopeId))) + if err != nil { + return "", errors.New(errors.Encrypt, op, "unable to encrypt message", errors.WithWrap(err)) + } + marshaledBlob, err := proto.Marshal(blobInfo) + if err != nil { + return "", errors.Wrap(err, op, errors.WithMsg("unable to marshal blob"), errors.WithCode(errors.Encode)) + } + wrapped := &request.Wrapper{ + AuthMethodId: am.PublicId, + ScopeId: am.ScopeId, + WrapperKeyId: wrapper.KeyID(), + Ct: marshaledBlob, + } + if err := wrapped.Validate(); err != nil { + return "", errors.Wrap(err, op) + } + marshaledEncryptedSt, err := proto.Marshal(wrapped) + if err != nil { + return "", errors.New(errors.Unknown, op, "unable to marshal encrypted message", errors.WithWrap(err)) + } + + return base58.FastBase58Encoding(marshaledEncryptedSt), nil +} + +// decryptMessage will decrypt messages that were previously encrypted with oidc.encryptMessage(...) +// The returned messageBytes can be marshaled via proto.Marshal into the appropriate message. +func decryptMessage(ctx context.Context, wrappingWrapper wrapping.Wrapper, wrappedRequest *request.Wrapper) (messageBytes []byte, e error) { + const op = "oidc.decryptMessage" + if wrappedRequest == nil { + return nil, errors.New(errors.InvalidParameter, op, "missing wrapped request") + } + if wrappingWrapper == nil { + return nil, errors.New(errors.InvalidParameter, op, "missing wrapping wrapper") + } + var blobInfo wrapping.EncryptedBlobInfo + if err := proto.Unmarshal(wrappedRequest.Ct, &blobInfo); err != nil { + return nil, errors.New(errors.Unknown, op, "unable to marshal blob info", errors.WithWrap(err)) + } + + decryptedMsg, err := wrappingWrapper.Decrypt(ctx, &blobInfo, []byte(fmt.Sprintf("%s%s", wrappedRequest.AuthMethodId, wrappedRequest.ScopeId))) + if err != nil { + return nil, errors.New(errors.Decrypt, op, "unable to decrypt message", errors.WithWrap(err)) + } + return decryptedMsg, nil +} + +// unwrapMessage does just that, it unwraps the encoded request.Wrapper proto message +func unwrapMessage(ctx context.Context, encodedWrappedMsg string) (*request.Wrapper, error) { + const op = "" + decoded, err := base58.FastBase58Decoding(encodedWrappedMsg) + if err != nil { + return nil, errors.New(errors.Unknown, op, "unable to decode message", errors.WithWrap(err)) + } + var wrapper request.Wrapper + if err := proto.Unmarshal(decoded, &wrapper); err != nil { + return nil, errors.New(errors.Unknown, op, "unable to marshal encoded/encrypted message", errors.WithWrap(err)) + } + return &wrapper, nil +} + +// requestWrappingWrapper finds the wrapping wrapper to use when encrypting/decrypting +// both a Request.State and Request.Token. +// +// It first checks the cache of derived wrappers. If it's not found in the cache, it +// generates a key based on the scope's oidc DEK, using the scopeId and authMethodId +// as salt and info for derivation, creates a wrapper for the new key, adds that wrapper +// to the cache and the returns the wrapper for the new key. +// +// It supports the WithKeyId(...) option which allows you to specify which oidc DEK +// to use vs the default of just using the latest version of the DEK. +func requestWrappingWrapper(ctx context.Context, k *kms.Kms, scopeId, authMethodId string, opt ...Option) (wrapping.Wrapper, error) { + const op = "oidc.(Repository).oidcWrapper" + if k == nil { + return nil, errors.New(errors.InvalidParameter, op, "missing kms") + } + if scopeId == "" { + return nil, errors.New(errors.InvalidParameter, op, "missing scope id") + } + if authMethodId == "" { + return nil, errors.New(errors.InvalidParameter, op, "missing auth method id") + } + opts := getOpts(opt...) + // get a specific oidcWrapper using the WithKeyId(...) option + oidcWrapper, err := k.GetWrapper(ctx, scopeId, kms.KeyPurposeOidc, kms.WithKeyId(opts.withKeyId)) + if err != nil { + return nil, errors.Wrap(err, op, errors.WithMsg("unable to get oidc wrapper")) + } + + // What derived key are we looking for? + keyId := derivedKeyId(derivedKeyPurposeState, oidcWrapper.KeyID(), authMethodId) + derivedWrapper, ok := k.GetDerivedPurposeCache().Load(keyId) + if ok { + return derivedWrapper.(*aead.Wrapper), nil + } + + // okay, I guess we need to derive a new key for this combo of oidcWrapper and authMethodId + reader, err := kms.NewDerivedReader(oidcWrapper, 32, []byte(authMethodId), []byte(scopeId)) + if err != nil { + return nil, errors.Wrap(err, op) + } + privKey, _, err := ed25519.GenerateKey(reader) + if err != nil { + return nil, errors.New(errors.Encrypt, op, "unable to generate key", errors.WithWrap(err)) + } + wrapper := aead.NewWrapper(nil) + if _, err := wrapper.SetConfig(map[string]string{ + "key_id": keyId, + }); err != nil { + return nil, errors.Wrap(err, op, errors.WithMsg(fmt.Sprintf("error setting config on aead wrapper in auth method %s", authMethodId))) + } + if err := wrapper.SetAESGCMKeyBytes(privKey); err != nil { + return nil, errors.Wrap(err, op, errors.WithMsg(fmt.Sprintf("error setting key bytes on aead wrapper in auth method %s", authMethodId))) + } + // store the derived key in our cache + k.GetDerivedPurposeCache().Store(keyId, wrapper) + + return wrapper, nil +} + +// derivedKeyId returns a key that represents the derived key +func derivedKeyId(purpose derivedKeyPurpose, wrapperKeyId, authMethodId string) string { + return fmt.Sprintf("%s.%s.%s", purpose.String(), wrapperKeyId, authMethodId) +} + +// derivedKeyPurpose represents the purpose of the derived key. +// +// In the future, this could be moved to the kms package +// and exported for others to use. +type derivedKeyPurpose uint + +const ( + derivedKeyPurposeUnknown = iota + derivedKeyPurposeState +) + +// String returns a representative string for the key's purpose +func (k derivedKeyPurpose) String() string { + switch k { + case derivedKeyPurposeState: + return "oidc_state" + default: + return "oidc_unknown" + } +} diff --git a/internal/auth/oidc/service_callback.go b/internal/auth/oidc/service_callback.go new file mode 100644 index 000000000..235568f7d --- /dev/null +++ b/internal/auth/oidc/service_callback.go @@ -0,0 +1,221 @@ +package oidc + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/golang/protobuf/proto" + "github.com/hashicorp/boundary/internal/auth/oidc/request" + "github.com/hashicorp/boundary/internal/authtoken" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/cap/oidc" +) + +// Callback is an oidc domain service function for processing a successful OIDC +// Authentication Response from an IdP oidc callback. On success, it returns a +// final redirect URL for the response to the IdP. +// +// Callback can return several errors including errors.Forbidden for requests +// with non-unique states (which are replays) +// +// For more info on a successful OIDC Authentication Response see: +// https://openid.net/specs/openid-connect-core-1_0.html#AuthResponse +// +// The service operation includes: +// +// * Decrypt the state which has been encrypted with the OIDC DEK. If decryption +// fails, and error is returned. Decrypted state payload includes the +// token_request_id, nonce and final_redirect_url. +// +// * Exchange the callbackCodeParameter for provider tokens and validate the +// tokens. Call UserInfo endpoint using access token. +// +// * Use oidc.(Repository).upsertAccount to create/update account using ID +// Tokens claims. The "sub" claim as external ID and setting email and full name +// for the account. +// +// * Use iam.(Repository).LookupUserWithLogin(...) look up the iam.User matching +// the Account. +// +// * Use the authtoken.(Repository).CreateAuthToken(...) to create a pending +// auth token for the authenticated user. +func Callback( + ctx context.Context, + oidcRepoFn OidcRepoFactory, + iamRepoFn IamRepoFactory, + atRepoFn AuthTokenRepoFactory, + authMethodId, + state, code string) (finalRedirect string, e error) { + const op = "oidc.Callback" + if oidcRepoFn == nil { + return "", errors.New(errors.InvalidParameter, op, "missing oidc repository function") + } + if iamRepoFn == nil { + return "", errors.New(errors.InvalidParameter, op, "missing iam repository function") + } + if atRepoFn == nil { + return "", errors.New(errors.InvalidParameter, op, "missing auth token repository function") + } + if authMethodId == "" { + return "", errors.New(errors.InvalidParameter, op, "missing auth method id") + } + if state == "" { + return "", errors.New(errors.InvalidParameter, op, "missing state") + } + if code == "" { + return "", errors.New(errors.InvalidParameter, op, "missing code") + } + + // One comment before we begin, Callback orchestrates repositories which + // execute order dependent database transactions: create an account, + // perhaps create an iam.User, and if everything is successful it will + // create a pending token. Any of these independent transactions could + // fail, but we don't need to undo a previous successful transactions + // in the Callback. The sequentially order transactions all leave the + // database is a consistent state, even if subsequent transactions fail. + // Future requests will not be hampered by previous unsuccessful Callback + // operations. + + r, err := oidcRepoFn() + if err != nil { + return "", errors.Wrap(err, op) + } + am, err := r.lookupAuthMethod(ctx, authMethodId) + if err != nil { + return "", errors.Wrap(err, op) + } + if am == nil { + return "", errors.New(errors.RecordNotFound, op, fmt.Sprintf("auth method %s not found", authMethodId)) + } + + // state is a proto request.Wrapper msg, which contains a cipher text field, so + // we need the derived wrapper that was used to encrypt it. + requestWrapper, err := requestWrappingWrapper(ctx, r.kms, am.ScopeId, authMethodId) + if err != nil { + return "", errors.Wrap(err, op) + } + stateWrapper, err := unwrapMessage(ctx, state) + if err != nil { + return "", errors.Wrap(err, op) + } + stateBytes, err := decryptMessage(ctx, requestWrapper, stateWrapper) + if err != nil { + return "", errors.Wrap(err, op) + } + var reqState request.State + if err := proto.Unmarshal(stateBytes, &reqState); err != nil { + return "", errors.New(errors.Unknown, op, "unable to unmarshal request state", errors.WithWrap(err)) + } + + // get the provider from the cache (if possible). FYI: "get" just does the right thing + // about comparing the cache with the auth method from the db. + provider, err := providerCache().get(ctx, am) + if err != nil { + return "", errors.Wrap(err, op) + } + + // if auth method is inactive, we don't allow inflight requests to finish if the + // auth method's config has changed since the request was kicked off. + hash, err := provider.ConfigHash() + if err != nil { + return "", errors.New(errors.Unknown, op, "unable to get provider config hash", errors.WithWrap(err)) + } + if reqState.ProviderConfigHash != hash && am.OperationalState == string(InactiveState) { + return "", errors.New(errors.AuthMethodInactive, op, "auth method configuration changed during in-flight authentication attempt") + } + + // before proceeding, make sure the request hasn't timed out + if time.Now().After(reqState.ExpirationTime.Timestamp.AsTime()) { + return "", errors.New(errors.AuthAttemptExpired, op, "request state has expired") + } + + // now, we need to prep for the token exchange with the oidc provider, which + // means sort of re-creating the orig oidc.Request with enough info to properly + // validate the ID Token + opts := []oidc.Option{ + oidc.WithState(state), + oidc.WithNonce(reqState.Nonce), + } + switch { + case am.MaxAge == -1: + opts = append(opts, oidc.WithMaxAge(0)) + case am.MaxAge > 0: + opts = append(opts, oidc.WithMaxAge(uint(am.MaxAge))) + default: + } + if len(am.AudClaims) > 0 { + opts = append(opts, oidc.WithAudiences(am.AudClaims...)) + } + if strings.TrimSpace(am.ApiUrl) == "" { + return "", errors.New(errors.InvalidParameter, op, "empty api URL") + } + oidcRequest, err := oidc.NewRequest(AttemptExpiration, fmt.Sprintf(CallbackEndpoint, am.ApiUrl, am.PublicId), opts...) + if err != nil { + return "", errors.New(errors.Unknown, op, "unable to create oidc request for token exchange", errors.WithWrap(err)) + } + tk, err := provider.Exchange(ctx, oidcRequest, state, code) + if err != nil { + return "", errors.New(errors.Unknown, op, "unable to complete exchange with oidc provider", errors.WithWrap(err)) + } + + // okay, now we need some claims from both the ID Token and userinfo, so we can + // upsert an auth account + idTkClaims := map[string]interface{}{} // intentionally, NOT nil for call to upsertAccount(...) + userInfoClaims := map[string]interface{}{} // intentionally, NOT nil for call to upsertAccount(...) + + if err := tk.IDToken().Claims(&idTkClaims); err != nil { + return "", errors.New(errors.Unknown, op, "unable to parse ID Token claims", errors.WithWrap(err)) + } + + userInfoTokenSource := tk.StaticTokenSource() + if userInfoTokenSource != nil { + sub, ok := idTkClaims["sub"].(string) + if !ok { + return "", errors.New(errors.Unknown, op, "subject is not present in ID Token, which should not be possible") + } + if err := provider.UserInfo(ctx, userInfoTokenSource, sub, &userInfoClaims); err != nil { + return "", errors.New(errors.Unknown, op, "unable to get user info from provider", errors.WithWrap(err)) + } + } + + acct, err := r.upsertAccount(ctx, am, idTkClaims, userInfoClaims) + if err != nil { + return "", errors.Wrap(err, op) + } + + // before searching for the iam.User associated with the account, + // we need to see if this particular auth method is allowed to + // autovivify users for the scope. + iamRepo, err := iamRepoFn() + if err != nil { + return "", errors.Wrap(err, op) + } + + scope, err := iamRepo.LookupScope(ctx, am.ScopeId) + if err != nil { + return "", errors.Wrap(err, op, errors.WithMsg("unable to lookup account scope: "+scope.PublicId)) + } + + user, err := iamRepo.LookupUserWithLogin(ctx, acct.PublicId) + if err != nil { + return "", errors.Wrap(err, op) + } + + // wow, we're getting close. we just need to create a pending token for this + // successful authentication process, so it can be retrieved by the polling client + // that initialed the authentication attempt. + tokenRepo, err := atRepoFn() + if err != nil { + return "", errors.Wrap(err, op) + } + if _, err := tokenRepo.CreateAuthToken(ctx, user, acct.PublicId, authtoken.WithPublicId(reqState.TokenRequestId), authtoken.WithStatus(authtoken.PendingStatus)); err != nil { + if errors.Match(errors.T(errors.NotUnique), err) { + return "", errors.New(errors.Forbidden, op, "not a unique request", errors.WithWrap(err)) + } + return "", errors.Wrap(err, op) + } + // tada! we can return a final redirect URL for the successful authentication. + return reqState.FinalRedirectUrl, nil +} diff --git a/internal/auth/oidc/service_callback_test.go b/internal/auth/oidc/service_callback_test.go new file mode 100644 index 000000000..29925a0ca --- /dev/null +++ b/internal/auth/oidc/service_callback_test.go @@ -0,0 +1,604 @@ +package oidc + +import ( + "context" + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "net/url" + "testing" + "time" + + "github.com/hashicorp/boundary/internal/auth/oidc/store" + authStore "github.com/hashicorp/boundary/internal/auth/store" + "github.com/hashicorp/boundary/internal/authtoken" + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/iam" + iamStore "github.com/hashicorp/boundary/internal/iam/store" + + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/oplog" + "github.com/hashicorp/cap/oidc" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_Callback(t *testing.T) { + // DO NOT run these tests under t.Parallel(), there be dragons because of dependencies on the + // Database and TestProvider state + ctx := context.Background() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + rootWrapper := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, rootWrapper) + + // some standard factories for unit tests which + // are used in the Callback(...) call + iamRepoFn := func() (*iam.Repository, error) { + return iam.NewRepository(rw, rw, kmsCache) + } + repoFn := func() (*Repository, error) { + return NewRepository(rw, rw, kmsCache) + } + atRepoFn := func() (*authtoken.Repository, error) { + return authtoken.NewRepository(rw, rw, kmsCache) + } + atRepo, err := atRepoFn() + require.NoError(t, err) + + iamRepo := iam.TestRepo(t, conn, rootWrapper) + org, _ := iam.TestScopes(t, iamRepo) + + databaseWrapper, err := kmsCache.GetWrapper(ctx, org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + + // a very simple test mock controller, that simply responds with a 200 OK to every + // request. + testController := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + w.WriteHeader(200) + })) + defer testController.Close() + + // test provider for the tests (see the oidc package docs for more info) + // it will provide discovery, JWKs, a token endpoint, etc for these tests. + tp := oidc.StartTestProvider(t) + tpCert, err := ParseCertificates(tp.CACert()) + require.NoError(t, err) + _, _, tpAlg, _ := tp.SigningKeys() + + // a reusable test authmethod for the unit tests + testAuthMethod := TestAuthMethod(t, conn, databaseWrapper, org.PublicId, ActivePublicState, + "alice-rp", "fido", + WithCertificates(tpCert...), + WithSigningAlgs(Alg(tpAlg)), + WithIssuer(TestConvertToUrls(t, tp.Addr())[0]), + WithApiUrl(TestConvertToUrls(t, testController.URL)[0])) + + // set this as the primary so users will be created on first login + iam.TestSetPrimaryAuthMethod(t, iamRepo, org, testAuthMethod.PublicId) + + // a reusable oidc.Provider for the tests + testProvider, err := convertToProvider(ctx, testAuthMethod) + require.NoError(t, err) + testConfigHash, err := testProvider.ConfigHash() + require.NoError(t, err) + + // a reusable token request id for the tests. + testTokenRequestId, err := authtoken.NewAuthTokenId() + require.NoError(t, err) + + // usuable nonce for the unit tests + testNonce := "nonce" + + // define the audiences the test provider will accept for the unit tests. + tp.SetCustomAudience("foo", "alice-rp") + + // define a second test auth method, which is in an InactiveState for the unit tests. + org2, _ := iam.TestScopes(t, iam.TestRepo(t, conn, rootWrapper)) + databaseWrapper2, err := kmsCache.GetWrapper(ctx, org2.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + testAuthMethod2 := TestAuthMethod(t, conn, databaseWrapper2, org2.PublicId, InactiveState, + "alice-rp", "fido", + WithAudClaims("foo"), + WithMaxAge(-1), // oidc library has a 1 min leeway + WithCertificates(tpCert...), + WithSigningAlgs(Alg(tpAlg)), + WithIssuer(TestConvertToUrls(t, tp.Addr())[0]), + WithApiUrl(TestConvertToUrls(t, testController.URL)[0])) + // define a second test provider based on the inactive test auth method + testProvider2, err := convertToProvider(ctx, testAuthMethod2) + require.NoError(t, err) + testConfigHash2, err := testProvider2.ConfigHash() + require.NoError(t, err) + + tests := []struct { + name string + setup func() // provide a simple way to do some prework before the test. + oidcRepoFn OidcRepoFactory // returns a new oidc repo + iamRepoFn IamRepoFactory // returns a new iam repo + atRepoFn AuthTokenRepoFactory // returns a new auth token repo + am *AuthMethod // the authmethod for the test + state string // state parameter for test provider and Callback(...) + code string // code parameter for test provider and Callback(...) + wantSubject string // sub claim from id token + wantInfoName string // name claim from userinfo + wantInfoEmail string // email claim from userinfo + wantFinalRedirect string // final redirect from Callback(...) + wantErrMatch *errors.Template // error template to match + wantErrContains string // error string should contain + }{ + { + name: "simple", // must remain the first test + oidcRepoFn: repoFn, + iamRepoFn: iamRepoFn, + atRepoFn: atRepoFn, + am: testAuthMethod, + state: testState(t, testAuthMethod, kmsCache, testTokenRequestId, 2000*time.Second, "https://testcontroler.com/hi-alice", testConfigHash, testNonce), + code: "simple", + wantFinalRedirect: "https://testcontroler.com/hi-alice", + wantSubject: "simple@example.com", + wantInfoName: "alice doe-eve", + wantInfoEmail: "alice@example.com", + }, + { + name: "dup", // must follow "simple" unit test + oidcRepoFn: repoFn, + iamRepoFn: iamRepoFn, + atRepoFn: atRepoFn, + am: testAuthMethod, + state: testState(t, testAuthMethod, kmsCache, testTokenRequestId, 2000*time.Second, "https://testcontroler.com/hi-alice", testConfigHash, testNonce), + code: "simple", + wantFinalRedirect: "https://testcontroler.com/hi-alice", + wantSubject: "dup@example.com", + wantInfoName: "alice doe-eve", + wantInfoEmail: "alice@example.com", + }, + { + name: "inactive-valid", + setup: func() { + acct := TestAccount(t, conn, testAuthMethod2, "inactive-valid@example.com") + _ = iam.TestUser(t, iamRepo, org2.PublicId, iam.WithAccountIds(acct.PublicId)) + }, + oidcRepoFn: repoFn, + iamRepoFn: iamRepoFn, + atRepoFn: atRepoFn, + am: testAuthMethod2, + state: testState(t, testAuthMethod2, kmsCache, testTokenRequestId, 2000*time.Second, "https://testcontroler.com/hi-alice", testConfigHash2, testNonce), + code: "simple", + wantFinalRedirect: "https://testcontroler.com/hi-alice", + wantSubject: "inactive-valid@example.com", + wantInfoName: "alice doe-eve", + wantInfoEmail: "alice@example.com", + }, + { + name: "missing-oidc-repo-fn", + iamRepoFn: iamRepoFn, + atRepoFn: atRepoFn, + am: testAuthMethod, + state: testState(t, testAuthMethod, kmsCache, testTokenRequestId, 2000*time.Second, "https://testcontroler.com/hi-alice", testConfigHash, testNonce), + code: "simple", + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "missing oidc repository", + }, + { + name: "missing-iam-repo-fn", + oidcRepoFn: repoFn, + atRepoFn: atRepoFn, + am: testAuthMethod, + state: testState(t, testAuthMethod, kmsCache, testTokenRequestId, 2000*time.Second, "https://testcontroler.com/hi-alice", testConfigHash, testNonce), + code: "simple", + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "missing iam repository", + }, + { + name: "missing-at-repo-fn", + oidcRepoFn: repoFn, + iamRepoFn: iamRepoFn, + am: testAuthMethod, + state: testState(t, testAuthMethod, kmsCache, testTokenRequestId, 2000*time.Second, "https://testcontroler.com/hi-alice", testConfigHash, testNonce), + code: "simple", + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "missing auth token repository", + }, + { + name: "missing-state", + oidcRepoFn: repoFn, + iamRepoFn: iamRepoFn, + atRepoFn: atRepoFn, + am: testAuthMethod, + code: "simple", + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "missing state", + }, + { + name: "missing-code", + oidcRepoFn: repoFn, + iamRepoFn: iamRepoFn, + atRepoFn: atRepoFn, + am: testAuthMethod, + state: testState(t, testAuthMethod, kmsCache, testTokenRequestId, 2000*time.Second, "https://testcontroler.com/hi-alice", testConfigHash, testNonce), + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "missing code", + }, + { + name: "missing-auth-method-id", + oidcRepoFn: repoFn, + iamRepoFn: iamRepoFn, + atRepoFn: atRepoFn, + am: func() *AuthMethod { am := AllocAuthMethod(); return &am }(), + state: testState(t, testAuthMethod, kmsCache, testTokenRequestId, 2000*time.Second, "https://testcontroler.com/hi-alice", testConfigHash, testNonce), + code: "simple", + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "missing auth method", + }, + { + name: "bad-auth-method-id", + oidcRepoFn: repoFn, + iamRepoFn: iamRepoFn, + atRepoFn: atRepoFn, + am: func() *AuthMethod { am := AllocAuthMethod(); am.PublicId = "not-valid"; return &am }(), + state: testState(t, testAuthMethod, kmsCache, testTokenRequestId, 2000*time.Second, "https://testcontroler.com/hi-alice", testConfigHash, testNonce), + code: "simple", + wantErrMatch: errors.T(errors.RecordNotFound), + wantErrContains: "auth method not-valid not found", + }, + { + name: "bad-state-encoding", + oidcRepoFn: repoFn, + iamRepoFn: iamRepoFn, + atRepoFn: atRepoFn, + am: testAuthMethod, + state: "unable to decode message", + code: "simple", + wantErrMatch: errors.T(errors.Unknown), + wantErrContains: "unable to decode message", + }, + { + name: "unable-to-decrypt", + oidcRepoFn: repoFn, + iamRepoFn: iamRepoFn, + atRepoFn: atRepoFn, + am: testAuthMethod, + state: testState(t, testAuthMethod2, kmsCache, testTokenRequestId, 2000*time.Second, "https://testcontroler.com/hi-alice", testConfigHash, testNonce), + code: "simple", + wantErrMatch: errors.T(errors.Decrypt), + wantErrContains: "unable to decrypt message", + }, + { + name: "inactive-with-config-change", + oidcRepoFn: repoFn, + iamRepoFn: iamRepoFn, + atRepoFn: atRepoFn, + am: testAuthMethod2, + state: testState(t, testAuthMethod2, kmsCache, testTokenRequestId, 2000*time.Second, "https://testcontroler.com/hi-alice", 1, testNonce), + code: "simple", + wantErrMatch: errors.T(errors.AuthMethodInactive), + wantErrContains: "configuration changed during in-flight authentication attempt", + }, + { + name: "expired-attempt", + oidcRepoFn: repoFn, + iamRepoFn: iamRepoFn, + atRepoFn: atRepoFn, + am: testAuthMethod, + state: testState(t, testAuthMethod, kmsCache, testTokenRequestId, -20*time.Second, "https://testcontroler.com/hi-alice", testConfigHash, testNonce), + code: "simple", + wantErrMatch: errors.T(errors.AuthAttemptExpired), + wantErrContains: "request state has expired", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + + // start with no tokens in the db + _, err := rw.Exec(ctx, "delete from auth_token", nil) + require.NoError(err) + // start with no users in the db + excludeUsers := []interface{}{"u_anon", "u_auth", "u_recovery"} + _, err = rw.Exec(ctx, "delete from iam_user where public_id not in(?, ?, ?)", excludeUsers) + require.NoError(err) + // start with no oplog entries + _, err = rw.Exec(ctx, "delete from oplog_entry", nil) + require.NoError(err) + + if tt.setup != nil { + tt.setup() + } + + // the test provider is stateful, so we need to configure + // it for this unit test. + tp.SetExpectedAuthNonce(testNonce) + if tt.am != nil { + tp.SetClientCreds(tt.am.ClientId, tt.am.ClientSecret) + tpAllowedRedirect := fmt.Sprintf(CallbackEndpoint, tt.am.ApiUrl, tt.am.PublicId) + tp.SetAllowedRedirectURIs([]string{tpAllowedRedirect}) + } + if tt.code != "" { + tp.SetExpectedAuthCode(tt.code) + } + if tt.state != "" { + tp.SetExpectedState(tt.state) + } + tp.SetExpectedSubject(tt.wantSubject) + + info := map[string]interface{}{} + if tt.wantSubject != "" { + info["sub"] = tt.wantSubject + } + if tt.wantInfoEmail != "" { + info["email"] = tt.wantInfoEmail + } + if tt.wantInfoName != "" { + info["name"] = tt.wantInfoName + } + if len(info) > 0 { + tp.SetUserInfoReply(info) + } + + gotRedirect, err := Callback(ctx, + tt.oidcRepoFn, + tt.iamRepoFn, + tt.atRepoFn, + tt.am.PublicId, + tt.state, + tt.code, + ) + if tt.wantErrMatch != nil { + require.Error(err) + assert.Empty(gotRedirect) + assert.Truef(errors.Match(tt.wantErrMatch, err), "want err code: %q got: %q", tt.wantErrMatch.Code, err) + assert.Contains(err.Error(), tt.wantErrContains) + + // make sure there are no tokens in the db.. + var tokens []authtoken.AuthToken + err := rw.SearchWhere(ctx, &tokens, "1=?", []interface{}{1}) + require.NoError(err) + assert.Equal(0, len(tokens)) + + // make sure there weren't any oplog entries written. + var entries []oplog.Entry + err = rw.SearchWhere(ctx, &entries, "1=?", []interface{}{1}) + require.NoError(err) + require.Equalf(0, len(entries), "should not have found oplog entry for %s", tt.am.PublicId) + return + } + require.NoError(err) + assert.Equal(tt.wantFinalRedirect, gotRedirect) + + // make sure a pending token was created. + var tokens []authtoken.AuthToken + err = rw.SearchWhere(ctx, &tokens, "1=?", []interface{}{1}) + require.NoError(err) + require.Equal(1, len(tokens)) + tk, err := atRepo.LookupAuthToken(ctx, tokens[0].PublicId) + require.NoError(err) + assert.Equal(tk.Status, string(authtoken.PendingStatus)) + + // make sure the account was updated properly + var acct Account + err = rw.LookupWhere(ctx, &acct, "auth_method_id = ? and subject = ?", tt.am.PublicId, tt.wantSubject) + require.NoError(err) + assert.Equal(tt.wantInfoEmail, acct.Email) + assert.Equal(tt.wantInfoName, acct.FullName) + assert.Equal(tk.AuthAccountId, acct.PublicId) + + // make sure the token is properly assoc with the + // logged in user + var users []iam.User + err = rw.SearchWhere(ctx, &users, "public_id not in(?, ?, ?)", excludeUsers) + require.NoError(err) + require.Equal(1, len(users)) + assert.Equal(tk.IamUserId, users[0].PublicId) + + // check the oplog entries. + var entries []oplog.Entry + err = rw.SearchWhere(ctx, &entries, "1=?", []interface{}{1}) + require.NoError(err) + oplogWrapper, err := kmsCache.GetWrapper(ctx, tt.am.ScopeId, kms.KeyPurposeOplog) + require.NoError(err) + types, err := oplog.NewTypeCatalog( + oplog.Type{Interface: new(store.Account), Name: "auth_oidc_account"}, + oplog.Type{Interface: new(iamStore.User), Name: "iam_user"}, + oplog.Type{Interface: new(authStore.Account), Name: "auth_account"}, + ) + require.NoError(err) + + cnt := 0 + foundAcct, foundOidcAcct := false, false + for _, e := range entries { + cnt += 1 + e.Cipherer = oplogWrapper + err := e.DecryptData(ctx) + require.NoError(err) + msgs, err := e.UnmarshalData(types) + require.NoError(err) + for _, m := range msgs { + switch m.TypeName { + case "auth_oidc_account": + foundOidcAcct = true + t.Log("test: ", tt.name, "found oidc: ", m) + case "iam_user": + t.Log("test: ", tt.name, "found iam user: ", m) + case "auth_account": + foundAcct = true + t.Log("test: ", tt.name, "found acct", m) + } + } + } + assert.Truef(foundAcct, "expected to find auth account oplog entry") + assert.Truef(foundOidcAcct, "expected to find auth oidc account oplog entry") + }) + } + t.Run("replay-attack-with-dup-state", func(t *testing.T) { + // a test to ensure that replays of duplicate states + // are rejected and produce an appropriate error. + + assert, require := assert.New(t), require.New(t) + + // start with no tokens in the db + _, err := rw.Exec(ctx, "delete from auth_token", nil) + require.NoError(err) + // start with no users in the db + excludeUsers := []interface{}{"u_anon", "u_auth", "u_recovery"} + _, err = rw.Exec(ctx, "delete from iam_user where public_id not in(?, ?, ?)", excludeUsers) + require.NoError(err) + // start with no oplog entries + _, err = rw.Exec(ctx, "delete from oplog_entry", nil) + require.NoError(err) + + // prime the test provider's state for the test + tp.SetClientCreds(testAuthMethod.ClientId, testAuthMethod.ClientSecret) + tpAllowedRedirect := fmt.Sprintf(CallbackEndpoint, testController.URL, testAuthMethod.PublicId) + tp.SetAllowedRedirectURIs([]string{tpAllowedRedirect}) + state := testState(t, testAuthMethod, kmsCache, testTokenRequestId, 20*time.Second, "https://testcontroler.com/hi-alice", testConfigHash, testNonce) + tp.SetExpectedAuthCode("simple") + tp.SetExpectedState(state) + + wantSubject := "replay-attack-with-dup-state@example.com" + tp.SetExpectedSubject(wantSubject) + + tp.SetUserInfoReply(map[string]interface{}{"sub": wantSubject}) + tp.SetExpectedAuthNonce(testNonce) + + // the first request should succeed. + gotRedirect, err := Callback(ctx, + repoFn, + iamRepoFn, + atRepoFn, + testAuthMethod.PublicId, + state, + "simple", + ) + require.NoError(err) + require.NotNil(gotRedirect) + + // the replay should raise an error. + gotRedirect2, err := Callback(ctx, + repoFn, + iamRepoFn, + atRepoFn, + testAuthMethod.PublicId, + state, + "simple", + ) + require.Error(err) + require.Empty(gotRedirect2) + assert.Truef(errors.Match(errors.T(errors.Forbidden), err), "want err code: %q got: %q", errors.InvalidParameter, err) + assert.Contains(err.Error(), "not a unique request") + }) +} + +// Test_StartAuth_to_Callback will test if we can successfully +// test StartAuth(...) through Callback(...) +func Test_StartAuth_to_Callback(t *testing.T) { + t.Run("startAuth-to-Callback", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + ctx := context.Background() + + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + // start with no tokens in the db + _, err := rw.Exec(ctx, "delete from auth_token", nil) + require.NoError(err) + // start with no users in the db + excludeUsers := []interface{}{"u_anon", "u_auth", "u_recovery"} + _, err = rw.Exec(ctx, "delete from iam_user where public_id not in(?, ?, ?)", excludeUsers) + require.NoError(err) + // start with no oplog entries + _, err = rw.Exec(ctx, "delete from oplog_entry", nil) + require.NoError(err) + + rootWrapper := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, rootWrapper) + + // func pointers for the test controller. + iamRepoFn := func() (*iam.Repository, error) { + return iam.NewRepository(rw, rw, kmsCache) + } + repoFn := func() (*Repository, error) { + return NewRepository(rw, rw, kmsCache) + } + atRepoFn := func() (*authtoken.Repository, error) { + return authtoken.NewRepository(rw, rw, kmsCache) + } + atRepo, err := atRepoFn() + require.NoError(err) + + controller := startTestControllerSrv(t, repoFn, iamRepoFn, atRepoFn) + + iamRepo := iam.TestRepo(t, conn, rootWrapper) + org, _ := iam.TestScopes(t, iamRepo) + + databaseWrapper, err := kmsCache.GetWrapper(ctx, org.PublicId, kms.KeyPurposeDatabase) + require.NoError(err) + + // the testing OIDC provider (it's a functional fake, see the package docs) + tp := oidc.StartTestProvider(t) + tpCert, err := ParseCertificates(tp.CACert()) + require.NoError(err) + _, _, tpAlg, _ := tp.SigningKeys() + + endToEndAuthMethod := TestAuthMethod(t, conn, databaseWrapper, org.PublicId, ActivePublicState, + "end-to-end-rp", "fido", + WithCertificates(tpCert...), + WithSigningAlgs(Alg(tpAlg)), + WithIssuer(TestConvertToUrls(t, tp.Addr())[0]), + WithApiUrl(TestConvertToUrls(t, controller.Addr())[0])) + + // need the updated org version, so we can set the primary auth method id + org, _ = iamRepo.LookupScope(ctx, org.PublicId) + require.NoError(err) + iam.TestSetPrimaryAuthMethod(t, iamRepo, org, endToEndAuthMethod.PublicId) + + // the test controller is stateful and needs to know what auth method id + // it's suppose to operate on + controller.SetAuthMethodId(endToEndAuthMethod.PublicId) + + authUrl, _, _, err := StartAuth(ctx, repoFn, endToEndAuthMethod.PublicId) + require.NoError(err) + + authParams, err := url.ParseQuery(authUrl.RawQuery) + require.NoError(err) + require.Equal(1, len(authParams["nonce"])) + require.Equal(1, len(authParams["state"])) + + // the TestProvider is stateful and needs to be configured for the upcoming requests. + tp.SetExpectedState(authParams["state"][0]) + tp.SetExpectedAuthNonce(authParams["nonce"][0]) + tp.SetExpectedAuthCode("simple") + tp.SetClientCreds(endToEndAuthMethod.ClientId, endToEndAuthMethod.ClientSecret) + tpAllowedRedirect := controller.CallbackUrl() + tp.SetAllowedRedirectURIs([]string{tpAllowedRedirect}) + + client := tp.HTTPClient() + // this http request will: + // * 1) go to the TestProvider, where auth/authz will be faked. + // * 2) TestProvider will send 302 back to the client for callback + // * 3) 302 redirected to testControllerSrv's callback handler + // * 4) callback handler will exchange the token with the TestProvider + // * 5) callback will send 302 back to client with final redirect + // * 6) 302 redirected to testControllerSrv's final redirect handler. + // * 7) final redirect handler sends "Congratulations" msg back to client. + // + // If this succeeds, then the service functions of StartAuth(...) and + // Callback(...) are successfully working together. + resp, err := client.Get(authUrl.String()) + require.NoError(err) + defer resp.Body.Close() + contents, err := ioutil.ReadAll(resp.Body) + require.NoError(err) + require.Containsf(string(contents), "Congratulations", "expected \"Congratulations\" on successful oidc authentication and got: %s", string(contents)) + + // check to make sure there's a pending token, after the successful callback + var tokens []authtoken.AuthToken + err = rw.SearchWhere(ctx, &tokens, "1=?", []interface{}{1}) + require.NoError(err) + require.Equal(1, len(tokens)) + tk, err := atRepo.LookupAuthToken(ctx, tokens[0].PublicId) + require.NoError(err) + assert.Equal(tk.Status, string(authtoken.PendingStatus)) + }) +} diff --git a/internal/auth/oidc/service_start_auth.go b/internal/auth/oidc/service_start_auth.go new file mode 100644 index 000000000..3b54d87ba --- /dev/null +++ b/internal/auth/oidc/service_start_auth.go @@ -0,0 +1,137 @@ +package oidc + +import ( + "context" + "fmt" + "net/url" + "time" + + "github.com/hashicorp/boundary/internal/auth/oidc/request" + "github.com/hashicorp/boundary/internal/authtoken" + "github.com/hashicorp/boundary/internal/db/timestamp" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/cap/oidc" + "google.golang.org/protobuf/types/known/timestamppb" +) + +// StartAuth accepts a request to start an OIDC authentication/authorization +// attempt. It returns two URLs and a tokenId. authUrl is an OIDC authorization +// request URL. The authUrl includes a "state" parameter which is encrypted and +// has a payload which includes (among other things) the final redirect +// (calculated from the clientInfo), a token_request_id, and nonce. The tokenUrl +// is the URL theclient can use to retrieve the results of the user's OIDC +// authentication attempt. The tokenId is an encrypted payload for the POST +// request to the tokenUrl. +// +// If the auth method is in an InactiveState, then an error is returned. +// +// Options supported: +// +// WithRoundTripPayload(string) provides an option for a client roundtrip +// payload. This payload will be added to the final redirect as a query +// parameter. +func StartAuth(ctx context.Context, oidcRepoFn OidcRepoFactory, authMethodId string, opt ...Option) (authUrl *url.URL, tokenUrl *url.URL, tokenId string, e error) { + const op = "oidc.StartAuth" + if authMethodId == "" { + return nil, nil, "", errors.New(errors.InvalidParameter, op, "missing auth method id") + } + if oidcRepoFn == nil { + return nil, nil, "", errors.New(errors.InvalidParameter, op, "missing oidc repo function") + } + r, err := oidcRepoFn() + if err != nil { + return nil, nil, "", errors.Wrap(err, op) + } + am, err := r.lookupAuthMethod(ctx, authMethodId) + if err != nil { + return nil, nil, "", errors.Wrap(err, op) + } + if am == nil { + return nil, nil, "", errors.New(errors.RecordNotFound, op, fmt.Sprintf("auth method %s not found", authMethodId)) + } + if am.OperationalState == string(InactiveState) { + return nil, nil, "", errors.New(errors.AuthMethodInactive, op, "not allowed to start authentication attempt") + } + + // get the provider from the cache (if possible) + provider, err := providerCache().get(ctx, am) + if err != nil { + return nil, nil, "", errors.Wrap(err, op) + } + callbackRedirect := fmt.Sprintf(CallbackEndpoint, am.GetApiUrl(), authMethodId) + + opts := getOpts(opt...) + finalRedirect := fmt.Sprintf(FinalRedirectEndpoint, am.GetApiUrl()) + if opts.withRoundtripPayload != "" { + u := make(url.Values) + u.Add("roundtrip_payload", opts.withRoundtripPayload) + finalRedirect = fmt.Sprintf("%s?%s", finalRedirect, u.Encode()) + } + now := time.Now() + createTime := timestamppb.New(now.Truncate(time.Second)) + exp := timestamppb.New(now.Add(AttemptExpiration).Truncate(time.Second)) + tokenRequestId, err := authtoken.NewAuthTokenId() + if err != nil { + return nil, nil, "", errors.Wrap(err, op) + } + nonce, err := oidc.NewID() + if err != nil { + return nil, nil, "", errors.New(errors.Unknown, op, "unable to generate nonce", errors.WithWrap(err)) + } + hash, err := provider.ConfigHash() + if err != nil { + return nil, nil, "", errors.New(errors.Unknown, op, "unable to get provider config hash", errors.WithWrap(err)) + } + st := &request.State{ + TokenRequestId: tokenRequestId, + CreateTime: ×tamp.Timestamp{Timestamp: createTime}, + ExpirationTime: ×tamp.Timestamp{Timestamp: exp}, + FinalRedirectUrl: finalRedirect, + Nonce: nonce, + ProviderConfigHash: hash, + } + + requestWrapper, err := requestWrappingWrapper(ctx, r.kms, am.ScopeId, authMethodId) + if err != nil { + return nil, nil, "", errors.Wrap(err, op) + } + encodedEncryptedSt, err := encryptMessage(ctx, requestWrapper, am, st) + if err != nil { + return nil, nil, "", errors.Wrap(err, op) + } + // a bare min oidc.Request needed for the provider.AuthURL(...) call. We've intentionally not populated + // things like Audiences, because this oidc.Request isn't cached and not intended for use in future legs + // of the authen flow. + oidcReq, err := oidc.NewRequest( + AttemptExpiration, + callbackRedirect, + oidc.WithState(string(encodedEncryptedSt)), + oidc.WithNonce(nonce)) + if err != nil { + return nil, nil, "", errors.New(errors.Unknown, op, "unable to create oidc request", errors.WithWrap(err)) + } + + u, err := provider.AuthURL(ctx, oidcReq) + if err != nil { + return nil, nil, "", errors.New(errors.Unknown, op, "unable to get auth url from provider", errors.WithWrap(err)) + } + authUrl, err = url.Parse(u) + if err != nil { + return nil, nil, "", errors.New(errors.Unknown, op, "unable to parse auth url", errors.WithWrap(err)) + } + + t := &request.Token{ + RequestId: tokenRequestId, + ExpirationTime: ×tamp.Timestamp{Timestamp: exp}, + } + encodedEncryptedTk, err := encryptMessage(ctx, requestWrapper, am, t) + if err != nil { + return nil, nil, "", errors.Wrap(err, op) + } + tokenUrl, err = url.Parse(fmt.Sprintf(TokenEndpoint, am.GetApiUrl(), authMethodId)) + if err != nil { + return nil, nil, "", errors.New(errors.Unknown, op, "unable to generate token URL", errors.WithWrap(err)) + } + + return authUrl, tokenUrl, encodedEncryptedTk, nil +} diff --git a/internal/auth/oidc/service_start_auth_test.go b/internal/auth/oidc/service_start_auth_test.go new file mode 100644 index 000000000..abfa89e8e --- /dev/null +++ b/internal/auth/oidc/service_start_auth_test.go @@ -0,0 +1,216 @@ +package oidc + +import ( + "context" + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "testing" + "time" + + "github.com/hashicorp/boundary/internal/auth/oidc/request" + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/cap/oidc" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" +) + +func Test_StartAuth(t *testing.T) { + t.Parallel() + ctx := context.Background() + tp := oidc.StartTestProvider(t) + _, _, tpAlg, _ := tp.SigningKeys() + tpCert, err := ParseCertificates(tp.CACert()) + require.NoError(t, err) + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + rootWrapper := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, rootWrapper) + testController := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + w.WriteHeader(200) + })) + defer testController.Close() + + repoFn := func() (*Repository, error) { + return NewRepository(rw, rw, kmsCache) + } + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, rootWrapper)) + databaseWrapper, err := kmsCache.GetWrapper(ctx, org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + testAuthMethod := TestAuthMethod( + t, conn, databaseWrapper, org.PublicId, ActivePublicState, + "test-rp", "fido", + WithIssuer(TestConvertToUrls(t, tp.Addr())[0]), + WithApiUrl(TestConvertToUrls(t, testController.URL)[0]), + WithSigningAlgs(Alg(tpAlg)), + WithCertificates(tpCert...), + ) + + testAuthMethodInactive := TestAuthMethod( + t, conn, databaseWrapper, org.PublicId, InactiveState, + "test-rp2", "fido", + WithIssuer(TestConvertToUrls(t, tp.Addr())[0]), + WithApiUrl(TestConvertToUrls(t, testController.URL)[0]), + WithSigningAlgs(Alg(tpAlg)), + WithCertificates(tpCert...), + ) + + stdSetup := func(am *AuthMethod, repoFn OidcRepoFactory, apiSrv *httptest.Server) (a *AuthMethod, allowedRedirect string) { + // update the allowed redirects for the TestProvider + tpAllowedRedirect := fmt.Sprintf(CallbackEndpoint, apiSrv.URL, am.PublicId) + tp.SetAllowedRedirectURIs([]string{tpAllowedRedirect}) + r, err := repoFn() + require.NoError(t, err) + // update the test's auth method, now that we've added a new callback + am, err = r.lookupAuthMethod(ctx, am.PublicId) + require.NoError(t, err) + require.NotNil(t, am) + return am, tpAllowedRedirect + } + tests := []struct { + name string + repoFn OidcRepoFactory + apiSrv *httptest.Server + authMethod *AuthMethod + roundTripper string + setup func(am *AuthMethod, repoFn OidcRepoFactory, apiSrv *httptest.Server) (*AuthMethod, string) + wantErrMatch *errors.Template + wantErrContains string + }{ + { + name: "simple", + repoFn: repoFn, + apiSrv: testController, + authMethod: testAuthMethod, + setup: stdSetup, + }, + { + name: "simple-with-roundtripper", + repoFn: repoFn, + apiSrv: testController, + authMethod: testAuthMethod, + setup: stdSetup, + roundTripper: `{"alice": "hi bob", "eve": "chuckles says hi alice"}`, + }, + { + name: "inactive", + repoFn: repoFn, + apiSrv: testController, + authMethod: testAuthMethodInactive, + wantErrMatch: errors.T(errors.AuthMethodInactive), + wantErrContains: "not allowed to start authentication attempt", + setup: stdSetup, + }, + { + name: "missing-repo-func", + repoFn: nil, + apiSrv: testController, + authMethod: testAuthMethodInactive, + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "missing oidc repo function", + }, + { + name: "missing-authmethod-id", + repoFn: repoFn, + apiSrv: testController, + authMethod: func() *AuthMethod { am := AllocAuthMethod(); return &am }(), + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "missing auth method id", + }, + { + name: "bad-authmethod-id", + repoFn: repoFn, + apiSrv: testController, + authMethod: func() *AuthMethod { am := AllocAuthMethod(); am.PublicId = "not-valid"; return &am }(), + wantErrMatch: errors.T(errors.RecordNotFound), + wantErrContains: "auth method not-valid not found:", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + var allowedRedirect string + if tt.setup != nil { + tt.authMethod, allowedRedirect = tt.setup(tt.authMethod, tt.repoFn, tt.apiSrv) + } + + now := time.Now() + authUrl, tokenUrl, tokenId, err := StartAuth(ctx, tt.repoFn, tt.authMethod.PublicId, WithRoundtripPayload(tt.roundTripper)) + if tt.wantErrMatch != nil { + require.Error(err) + assert.Nil(authUrl) + assert.Nil(tokenUrl) + assert.Truef(errors.Match(tt.wantErrMatch, err), "want err code: %q got: %q", tt.wantErrMatch, err) + assert.Contains(err.Error(), tt.wantErrContains) + return + } + require.NoError(err) + require.NotNil(authUrl) + require.NotNil(tokenUrl) + + authParams, err := url.ParseQuery(authUrl.RawQuery) + require.NoError(err) + assert.Equal(authParams["response_type"], []string{"code"}) + assert.Equal(authParams["scope"], []string{"openid"}) + assert.Equal(authParams["response_type"], []string{"code"}) + assert.Equal(authParams["redirect_uri"], []string{allowedRedirect}) + assert.Equal(authParams["client_id"], []string{tt.authMethod.ClientId}) + require.Equal(1, len(authParams["nonce"])) + require.Equal(1, len(authParams["state"])) + + // verify the state in the authUrl can be decrypted and it's correct + state := authParams["state"][0] + wrappedStReq, err := unwrapMessage(ctx, state) + require.NoError(err) + repo, err := tt.repoFn() + require.NoError(err) + wrappingWrapper, err := requestWrappingWrapper(ctx, repo.kms, wrappedStReq.ScopeId, wrappedStReq.AuthMethodId) + require.NoError(err) + decryptedBytes, err := decryptMessage(ctx, wrappingWrapper, wrappedStReq) + require.NoError(err) + var reqState request.State + err = proto.Unmarshal(decryptedBytes, &reqState) + require.NoError(err) + assert.NotNil(reqState.CreateTime) + assert.NotEmpty(reqState.FinalRedirectUrl) + switch tt.roundTripper != "" { + case true: + u := make(url.Values) + u.Add("roundtrip_payload", tt.roundTripper) + assert.Equal(fmt.Sprintf(FinalRedirectEndpoint, tt.apiSrv.URL)+"?"+u.Encode(), reqState.FinalRedirectUrl) + assert.Contains(reqState.FinalRedirectUrl, u.Encode()) + default: + assert.Equal(fmt.Sprintf(FinalRedirectEndpoint, tt.apiSrv.URL), reqState.FinalRedirectUrl) + } + assert.Equal(authParams["nonce"][0], reqState.Nonce) + + assert.WithinDuration(reqState.CreateTime.Timestamp.AsTime(), now, 1*time.Second) + assert.WithinDuration(reqState.ExpirationTime.Timestamp.AsTime(), now.Add(AttemptExpiration), 1*time.Second) + + p, err := convertToProvider(ctx, tt.authMethod) + require.NoError(err) + configHash, err := p.ConfigHash() + require.NoError(err) + assert.Equal(configHash, reqState.ProviderConfigHash) + + // verify the token_id in the tokenUrl can be decrypted and it's correct + wrappedTkReq, err := unwrapMessage(ctx, tokenId) + require.NoError(err) + wrappingWrapper, err = requestWrappingWrapper(ctx, repo.kms, wrappedTkReq.ScopeId, wrappedTkReq.AuthMethodId) + require.NoError(err) + decryptedBytes, err = decryptMessage(ctx, wrappingWrapper, wrappedTkReq) + require.NoError(err) + var reqToken request.Token + err = proto.Unmarshal(decryptedBytes, &reqToken) + require.NoError(err) + + assert.Equal(reqState.TokenRequestId, reqToken.RequestId) + assert.Equal(reqState.ExpirationTime, reqToken.ExpirationTime) + }) + } +} diff --git a/internal/auth/oidc/service_test.go b/internal/auth/oidc/service_test.go new file mode 100644 index 000000000..8cdf7c901 --- /dev/null +++ b/internal/auth/oidc/service_test.go @@ -0,0 +1,265 @@ +package oidc + +import ( + "context" + "testing" + "time" + + "github.com/golang/protobuf/ptypes" + "github.com/hashicorp/boundary/internal/auth/oidc/request" + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/db/timestamp" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/kms" + wrapping "github.com/hashicorp/go-kms-wrapping" + "github.com/hashicorp/go-kms-wrapping/wrappers/aead" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" +) + +// Test_encryptState_decryptState are unit tests for both encryptState(...) and decryptState(...) +func Test_encryptMessage_decryptMessage(t *testing.T) { + ctx := context.Background() + conn, _ := db.TestSetup(t, "postgres") + rootWrapper := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, rootWrapper) + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, rootWrapper)) + databaseWrapper, err := kmsCache.GetWrapper(ctx, org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + testAuthMethod := TestAuthMethod(t, conn, databaseWrapper, org.PublicId, ActivePrivateState, "alice-rp", "fido", WithApiUrl(TestConvertToUrls(t, "https://www.alice.com/callback")[0]), WithSigningAlgs(RS256)) + + now := time.Now() + createTime, err := ptypes.TimestampProto(now.Truncate(time.Second)) + require.NoError(t, err) + exp, err := ptypes.TimestampProto(now.Add(AttemptExpiration).Truncate(time.Second)) + require.NoError(t, err) + + tests := []struct { + name string + wrapper wrapping.Wrapper + authMethod *AuthMethod + message proto.Message + wantErrMatch *errors.Template + wantErrContains string + }{ + { + name: "valid-request-state", + wrapper: db.TestWrapper(t), + authMethod: testAuthMethod, + message: &request.State{ + TokenRequestId: "test-token-request-id", + CreateTime: ×tamp.Timestamp{Timestamp: createTime}, + ExpirationTime: ×tamp.Timestamp{Timestamp: exp}, + Nonce: "test-nonce", + FinalRedirectUrl: "www.alice.com/final", + ProviderConfigHash: 100, + }, + }, + { + name: "valid-request-token", + wrapper: db.TestWrapper(t), + authMethod: testAuthMethod, + message: &request.Token{ + RequestId: "test-token-request-id", + ExpirationTime: ×tamp.Timestamp{Timestamp: exp}, + }, + }, + { + name: "missing-wrapper", + wrapper: nil, + authMethod: testAuthMethod, + message: &request.State{ + TokenRequestId: "test-token-request-id", + CreateTime: ×tamp.Timestamp{Timestamp: createTime}, + ExpirationTime: ×tamp.Timestamp{Timestamp: exp}, + Nonce: "test-nonce", + FinalRedirectUrl: "www.alice.com/final", + ProviderConfigHash: 100, + }, + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "missing wrapper", + }, + { + name: "missing-auth-method", + wrapper: db.TestWrapper(t), + message: &request.State{ + TokenRequestId: "test-token-request-id", + CreateTime: ×tamp.Timestamp{Timestamp: createTime}, + ExpirationTime: ×tamp.Timestamp{Timestamp: exp}, + Nonce: "test-nonce", + FinalRedirectUrl: "www.alice.com/final", + ProviderConfigHash: 100, + }, + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "missing auth method", + }, + { + name: "missing-req-state", + wrapper: db.TestWrapper(t), + authMethod: testAuthMethod, + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "missing message", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + encrypted, err := encryptMessage(ctx, tt.wrapper, tt.authMethod, tt.message) + if tt.wantErrMatch != nil { + require.Error(err) + assert.Truef(errors.Match(tt.wantErrMatch, err), "want err code: %q got: %q", tt.wantErrMatch, err) + assert.Empty(encrypted) + if tt.wantErrContains != "" { + assert.Contains(err.Error(), tt.wantErrContains) + } + return + } + require.NoError(err) + assert.NotEmpty(encrypted) + + wrappedMsg, err := unwrapMessage(ctx, encrypted) + assert.Equalf(tt.authMethod.PublicId, wrappedMsg.AuthMethodId, "expected auth method %s and got: %s", tt.authMethod.PublicId, wrappedMsg.AuthMethodId) + assert.Equalf(tt.authMethod.ScopeId, wrappedMsg.ScopeId, "expected scope id %s and got: %s", tt.authMethod.ScopeId, wrappedMsg.ScopeId) + + require.NoError(err) + reqBytes, err := decryptMessage(ctx, tt.wrapper, wrappedMsg) + require.NoError(err) + + var msg proto.Message + switch v := tt.message.(type) { + case *request.State: + msg = &request.State{} + case *request.Token: + msg = &request.Token{} + default: + assert.Fail("unsupported message type: %v", v) + } + err = proto.Unmarshal(reqBytes, msg) + require.NoError(err) + assert.True(proto.Equal(tt.message, msg)) + }) + } + t.Run("decryptState-bad-parameter-tests", func(t *testing.T) { + tests := []struct { + name string + wrapper wrapping.Wrapper + wrappedMsg *request.Wrapper + wantErrMatch *errors.Template + wantErrContains string + }{ + { + name: "missing-wrapper", + wrappedMsg: &request.Wrapper{}, + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "missing wrapping wrapper", + }, + { + name: "missing-encrypted-state", + wrapper: db.TestWrapper(t), + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "missing wrapped request", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert := assert.New(t) + _, err = decryptMessage(ctx, tt.wrapper, tt.wrappedMsg) + assert.Truef(errors.Match(tt.wantErrMatch, err), "want err code: %q got: %q", tt.wantErrMatch, err) + assert.Contains(err.Error(), tt.wantErrContains) + }) + } + }) +} + +func Test_requestWrappingWrapper(t *testing.T) { + t.Parallel() + ctx := context.Background() + conn, _ := db.TestSetup(t, "postgres") + rootWrapper := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, rootWrapper) + + rw := db.New(conn) + repo, err := NewRepository(rw, rw, kmsCache) + require.NoError(t, err) + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, rootWrapper)) + databaseWrapper, err := kmsCache.GetWrapper(ctx, org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + testAuthMethod := TestAuthMethod(t, conn, databaseWrapper, org.PublicId, ActivePrivateState, "alice-rp", "fido", WithApiUrl(TestConvertToUrls(t, "https://alice.com/callback")[0]), WithSigningAlgs(RS256)) + + oidcWrapper, err := kmsCache.GetWrapper(ctx, org.PublicId, kms.KeyPurposeOidc) + require.NoError(t, err) + + tests := []struct { + name string + setupFn func() (string, string) + opt []Option + wantErrMatch *errors.Template + }{ + { + name: "simple-valid", + setupFn: func() (string, string) { return org.PublicId, testAuthMethod.PublicId }, + }, + { + name: "missing-scope", + setupFn: func() (string, string) { return "", testAuthMethod.PublicId }, + wantErrMatch: errors.T(errors.InvalidParameter), + }, + { + name: "missing-auth-method-id", + setupFn: func() (string, string) { return org.PublicId, "" }, + wantErrMatch: errors.T(errors.InvalidParameter), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + scopeId, authMethodId := tt.setupFn() + + wantKeyId := derivedKeyId(derivedKeyPurposeState, oidcWrapper.KeyID(), authMethodId) + kmsCache.GetDerivedPurposeCache().Delete(wantKeyId) + + reqWrapper, err := requestWrappingWrapper(ctx, repo.kms, scopeId, authMethodId, tt.opt...) + if tt.wantErrMatch != nil { + require.Error(err) + assert.Empty(reqWrapper) + cachedWrapper, found := kmsCache.GetDerivedPurposeCache().Load(wantKeyId) + assert.False(found) + assert.Empty(cachedWrapper) + return + } + require.NoError(err) + assert.NotEmpty(requestWrappingWrapper) + assert.Equalf(wantKeyId, reqWrapper.KeyID(), "expected key id %s and got: %s", wantKeyId, reqWrapper.KeyID()) + assert.Equalf(wrapping.AEAD, reqWrapper.Type(), "expected type %s and got: %s", wrapping.AEAD, reqWrapper.Type()) + assert.NotEmpty(reqWrapper.(*aead.Wrapper).GetKeyBytes()) + + cachedWrapper, found := kmsCache.GetDerivedPurposeCache().Load(wantKeyId) + require.True(found) + require.NotEmpty(cachedWrapper) + assert.Equal(reqWrapper, cachedWrapper) + + dupWrapper, err := requestWrappingWrapper(ctx, repo.kms, scopeId, authMethodId, tt.opt...) + require.NoError(err) + require.NotEmpty(dupWrapper) + assert.Equal(reqWrapper, dupWrapper) + }) + } +} + +func Test_derivedKeyPurpose_String(t *testing.T) { + tests := []struct { + purpose derivedKeyPurpose + want string + }{ + {100, "oidc_unknown"}, + {derivedKeyPurposeUnknown, "oidc_unknown"}, + {derivedKeyPurposeState, "oidc_state"}, + } + for _, tt := range tests { + t.Run(tt.purpose.String(), func(t *testing.T) { + assert.Equalf(t, tt.want, tt.purpose.String(), "wanted %s and got: %s", tt.want, tt.purpose.String()) + }) + } +} diff --git a/internal/auth/oidc/service_token_request.go b/internal/auth/oidc/service_token_request.go new file mode 100644 index 000000000..b23b02526 --- /dev/null +++ b/internal/auth/oidc/service_token_request.go @@ -0,0 +1,85 @@ +package oidc + +import ( + "context" + "time" + + "github.com/hashicorp/boundary/internal/auth/oidc/request" + "github.com/hashicorp/boundary/internal/authtoken" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/kms" + "google.golang.org/protobuf/proto" +) + +// TokenRequest is an oidc domain service function for processing a token +// request from a Boundary client. Token requests are the result of a Boundary +// client polling the tokenUrl they received via StartAuth. On success, it +// returns Boundary token. +// +// * Decrypt the tokenRequestId. If encryption fails, it returns an error. +// +// * Use the authtoken.(Repository).IssueAuthToken to issue the request id's +// token and mark it as issued in the repo. If the token is already issue, an +// error is returned. +func TokenRequest(ctx context.Context, kms *kms.Kms, atRepoFn AuthTokenRepoFactory, tokenRequestId string) (*authtoken.AuthToken, error) { + const op = "oidc.TokenRequest" + if kms == nil { + return nil, errors.New(errors.InvalidParameter, op, "missing kms") + } + if atRepoFn == nil { + return nil, errors.New(errors.InvalidParameter, op, "missing auth token repo function") + } + + reqTkWrapper, err := unwrapMessage(ctx, tokenRequestId) + if err != nil { + return nil, errors.Wrap(err, op) + } + if reqTkWrapper.ScopeId == "" { + return nil, errors.New(errors.InvalidParameter, op, "request token id wrapper missing scope id") + } + if reqTkWrapper.AuthMethodId == "" { + return nil, errors.New(errors.InvalidParameter, op, "request token id wrapper missing auth method id") + } + + // tokenRequestId is a proto request.Wrapper, which contains a cipher text field, + // so we need the derived wrapper that was used to encrypt it. + requestWrapper, err := requestWrappingWrapper(ctx, kms, reqTkWrapper.ScopeId, reqTkWrapper.AuthMethodId) + if err != nil { + return nil, errors.Wrap(err, op) + } + reqTkBytes, err := decryptMessage(ctx, requestWrapper, reqTkWrapper) + if err != nil { + return nil, errors.Wrap(err, op) + } + var reqTk request.Token + if err := proto.Unmarshal(reqTkBytes, &reqTk); err != nil { + return nil, errors.New(errors.Unknown, op, "unable to unmarshal request token", errors.WithWrap(err)) + } + + if reqTk.ExpirationTime == nil { + return nil, errors.New(errors.InvalidParameter, op, "missing request token id expiration time") + } + + // before proceeding, make sure the request hasn't timed out + if time.Now().After(reqTk.ExpirationTime.Timestamp.AsTime()) { + return nil, errors.New(errors.AuthAttemptExpired, op, "request token id has expired") + } + + tokenRepo, err := atRepoFn() + if err != nil { + return nil, errors.Wrap(err, op) + } + authTk, err := tokenRepo.IssueAuthToken(ctx, reqTk.RequestId) + if err != nil { + if errors.Match(errors.T(errors.RecordNotFound), err) { + // We don't have it -- at least not yet. So don't mark it as an + // error, but nothing is returned. + return nil, nil + } + return nil, errors.Wrap(err, op) + } + if authTk.Token == "" { + return nil, errors.New(errors.Internal, op, "issued token is missing") + } + return authTk, nil +} diff --git a/internal/auth/oidc/service_token_request_test.go b/internal/auth/oidc/service_token_request_test.go new file mode 100644 index 000000000..cf39c0393 --- /dev/null +++ b/internal/auth/oidc/service_token_request_test.go @@ -0,0 +1,298 @@ +package oidc + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/golang/protobuf/proto" + "github.com/golang/protobuf/ptypes" + "github.com/hashicorp/boundary/internal/auth/oidc/request" + "github.com/hashicorp/boundary/internal/authtoken" + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/db/timestamp" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/kms" + "github.com/mr-tron/base58" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_TokenRequest(t *testing.T) { + t.Parallel() + ctx := context.Background() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + rootWrapper := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, rootWrapper) + + iamRepo := iam.TestRepo(t, conn, rootWrapper) + org, _ := iam.TestScopes(t, iamRepo) + orgDatabaseWrapper, err := kmsCache.GetWrapper(ctx, org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + + atRepoFn := func() (*authtoken.Repository, error) { + r, err := authtoken.NewRepository(rw, rw, kmsCache) + require.NoError(t, err) + return r, nil + } + testAtRepo, err := authtoken.NewRepository(rw, rw, kmsCache) + require.NoError(t, err) + + // a reusable test authmethod for the unit tests + testAuthMethod := TestAuthMethod(t, conn, orgDatabaseWrapper, org.PublicId, ActivePublicState, + "alice-rp", "fido", + WithSigningAlgs(Alg(RS256)), + WithIssuer(TestConvertToUrls(t, "https://alice.com")[0]), + WithApiUrl(TestConvertToUrls(t, "https://alice.com/callback")[0])) + + testAcct := TestAccount(t, conn, testAuthMethod, "alice") + testUser := iam.TestUser(t, iamRepo, org.PublicId, iam.WithAccountIds(testAcct.PublicId)) + + testRequestWrapper, err := requestWrappingWrapper(ctx, kmsCache, testAuthMethod.ScopeId, testAuthMethod.PublicId) + require.NoError(t, err) + + tests := []struct { + name string + kms *kms.Kms + atRepoFn AuthTokenRepoFactory + tokenRequest string + wantNil bool + wantErrMatch *errors.Template + wantErrContains string + }{ + { + name: "missing-kms", + atRepoFn: atRepoFn, + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "missing kms", + }, + { + name: "missing-repoFn", + kms: kmsCache, + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "missing auth token repo function", + }, + { + name: "bad-wrapper", + kms: kmsCache, + atRepoFn: atRepoFn, + tokenRequest: "bad-wrapper", + wantErrMatch: errors.T(errors.Unknown), + wantErrContains: "unable to decode message", + }, + { + name: "missing-wrapper-scope-id", + kms: kmsCache, + atRepoFn: atRepoFn, + tokenRequest: func() string { + w := request.Wrapper{ + AuthMethodId: testAuthMethod.PublicId, + } + b, err := proto.Marshal(&w) + require.NoError(t, err) + return base58.Encode(b) + }(), + wantErrMatch: errors.T(errors.Unknown), + wantErrContains: "missing scope id", + }, + { + name: "missing-wrapper-auth-method-id", + kms: kmsCache, + atRepoFn: atRepoFn, + tokenRequest: func() string { + w := request.Wrapper{ + ScopeId: testAuthMethod.ScopeId, + } + b, err := proto.Marshal(&w) + require.NoError(t, err) + return base58.Encode(b) + }(), + wantErrMatch: errors.T(errors.Unknown), + wantErrContains: "missing auth method id", + }, + { + name: "dek-not-found", + kms: kms.TestKms(t, conn, db.TestWrapper(t)), + atRepoFn: atRepoFn, + tokenRequest: func() string { + tokenPublicId, err := authtoken.NewAuthTokenId() + require.NoError(t, err) + TestPendingToken(t, testAtRepo, testUser, testAcct, tokenPublicId) + return TestTokenRequestId(t, testAuthMethod, kmsCache, 200*time.Second, tokenPublicId) + }(), + wantErrMatch: errors.T(errors.Unknown), + wantErrContains: "unable to get oidc wrapper", + }, + { + name: "expired", + kms: kmsCache, + atRepoFn: atRepoFn, + tokenRequest: func() string { + tokenPublicId, err := authtoken.NewAuthTokenId() + require.NoError(t, err) + TestPendingToken(t, testAtRepo, testUser, testAcct, tokenPublicId) + return TestTokenRequestId(t, testAuthMethod, kmsCache, 0, tokenPublicId) + }(), + wantErrMatch: errors.T(errors.Unknown), + wantErrContains: "request token id has expired", + }, + { + name: "atRepoFn-error", + kms: kmsCache, + atRepoFn: func() (*authtoken.Repository, error) { + return nil, errors.New(errors.Unknown, "test op", "atRepoFn-error") + }, + tokenRequest: func() string { + tokenPublicId, err := authtoken.NewAuthTokenId() + require.NoError(t, err) + TestPendingToken(t, testAtRepo, testUser, testAcct, tokenPublicId) + return TestTokenRequestId(t, testAuthMethod, kmsCache, 200*time.Second, tokenPublicId) + }(), + wantErrMatch: errors.T(errors.Unknown), + wantErrContains: "atRepoFn-error", + }, + { + name: "error-unmarshal", + kms: kmsCache, + atRepoFn: atRepoFn, + tokenRequest: func() string { + blobInfo, err := testRequestWrapper.Encrypt(ctx, []byte("not-valid-request-token"), []byte(fmt.Sprintf("%s%s", testAuthMethod.PublicId, testAuthMethod.ScopeId))) + require.NoError(t, err) + marshaledBlob, err := proto.Marshal(blobInfo) + w := request.Wrapper{ + ScopeId: testAuthMethod.ScopeId, + AuthMethodId: testAuthMethod.PublicId, + WrapperKeyId: testRequestWrapper.KeyID(), + Ct: marshaledBlob, + } + require.NoError(t, err) + b, err := proto.Marshal(&w) + require.NoError(t, err) + return base58.Encode(b) + }(), + wantErrMatch: errors.T(errors.Unknown), + wantErrContains: "unable to unmarshal request token", + }, + { + name: "error-missing-exp", + kms: kmsCache, + atRepoFn: atRepoFn, + tokenRequest: func() string { + tokenPublicId, err := authtoken.NewAuthTokenId() + require.NoError(t, err) + reqTk := request.Token{ + RequestId: tokenPublicId, + } + marshaledReqTk, err := proto.Marshal(&reqTk) + require.NoError(t, err) + blobInfo, err := testRequestWrapper.Encrypt(ctx, marshaledReqTk, []byte(fmt.Sprintf("%s%s", testAuthMethod.PublicId, testAuthMethod.ScopeId))) + require.NoError(t, err) + marshaledBlob, err := proto.Marshal(blobInfo) + w := request.Wrapper{ + ScopeId: testAuthMethod.ScopeId, + AuthMethodId: testAuthMethod.PublicId, + WrapperKeyId: testRequestWrapper.KeyID(), + Ct: marshaledBlob, + } + require.NoError(t, err) + b, err := proto.Marshal(&w) + require.NoError(t, err) + return base58.Encode(b) + }(), + wantErrMatch: errors.T(errors.Unknown), + wantErrContains: "missing request token id expiration", + }, + { + name: "error-missing-request-id", + kms: kmsCache, + atRepoFn: atRepoFn, + tokenRequest: func() string { + exp, err := ptypes.TimestampProto(time.Now().Add(AttemptExpiration).Truncate(time.Second)) + require.NoError(t, err) + reqTk := request.Token{ + ExpirationTime: ×tamp.Timestamp{Timestamp: exp}, + } + marshaledReqTk, err := proto.Marshal(&reqTk) + require.NoError(t, err) + blobInfo, err := testRequestWrapper.Encrypt(ctx, marshaledReqTk, []byte(fmt.Sprintf("%s%s", testAuthMethod.PublicId, testAuthMethod.ScopeId))) + require.NoError(t, err) + marshaledBlob, err := proto.Marshal(blobInfo) + w := request.Wrapper{ + ScopeId: testAuthMethod.ScopeId, + AuthMethodId: testAuthMethod.PublicId, + WrapperKeyId: testRequestWrapper.KeyID(), + Ct: marshaledBlob, + } + require.NoError(t, err) + b, err := proto.Marshal(&w) + require.NoError(t, err) + return base58.Encode(b) + }(), + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "missing token request id", + }, + { + name: "error-issuing-token-forbidden-code", + kms: kmsCache, + atRepoFn: atRepoFn, + tokenRequest: func() string { + exp, err := ptypes.TimestampProto(time.Now().Add(AttemptExpiration).Truncate(time.Second)) + require.NoError(t, err) + reqTk := request.Token{ + RequestId: "not-a-valid-id", + ExpirationTime: ×tamp.Timestamp{Timestamp: exp}, + } + marshaledReqTk, err := proto.Marshal(&reqTk) + require.NoError(t, err) + blobInfo, err := testRequestWrapper.Encrypt(ctx, marshaledReqTk, []byte(fmt.Sprintf("%s%s", testAuthMethod.PublicId, testAuthMethod.ScopeId))) + require.NoError(t, err) + marshaledBlob, err := proto.Marshal(blobInfo) + w := request.Wrapper{ + ScopeId: testAuthMethod.ScopeId, + AuthMethodId: testAuthMethod.PublicId, + WrapperKeyId: testRequestWrapper.KeyID(), + Ct: marshaledBlob, + } + require.NoError(t, err) + b, err := proto.Marshal(&w) + require.NoError(t, err) + return base58.Encode(b) + }(), + wantNil: true, + }, + { + name: "success", + kms: kmsCache, + atRepoFn: atRepoFn, + tokenRequest: func() string { + tokenPublicId, err := authtoken.NewAuthTokenId() + require.NoError(t, err) + TestPendingToken(t, testAtRepo, testUser, testAcct, tokenPublicId) + return TestTokenRequestId(t, testAuthMethod, kmsCache, 200*time.Second, tokenPublicId) + }(), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + gotTk, err := TokenRequest(ctx, tt.kms, tt.atRepoFn, tt.tokenRequest) + if tt.wantErrMatch != nil { + require.Error(err) + assert.Truef(errors.Match(tt.wantErrMatch, err), "wanted %q and got: %+v", tt.wantErrMatch.Code, err) + if tt.wantErrContains != "" { + assert.Contains(err.Error(), tt.wantErrContains) + } + return + } + require.NoError(err) + if tt.wantNil { + assert.Empty(gotTk) + } else { + assert.NotEmpty(gotTk) + } + }) + } +} diff --git a/internal/auth/oidc/signing_alg.go b/internal/auth/oidc/signing_alg.go new file mode 100644 index 000000000..a88988b3e --- /dev/null +++ b/internal/auth/oidc/signing_alg.go @@ -0,0 +1,115 @@ +package oidc + +import ( + "fmt" + + "github.com/hashicorp/boundary/internal/auth/oidc/store" + "github.com/hashicorp/boundary/internal/errors" + "google.golang.org/protobuf/proto" +) + +// Alg represents asymmetric signing algorithms +type Alg string + +const ( + // JOSE asymmetric signing algorithm values as defined by RFC 7518. + // + // See: https://tools.ietf.org/html/rfc7518#section-3.1 + RS256 Alg = "RS256" // RSASSA-PKCS-v1.5 using SHA-256 + RS384 Alg = "RS384" // RSASSA-PKCS-v1.5 using SHA-384 + RS512 Alg = "RS512" // RSASSA-PKCS-v1.5 using SHA-512 + ES256 Alg = "ES256" // ECDSA using P-256 and SHA-256 + ES384 Alg = "ES384" // ECDSA using P-384 and SHA-384 + ES512 Alg = "ES512" // ECDSA using P-521 and SHA-512 + PS256 Alg = "PS256" // RSASSA-PSS using SHA256 and MGF1-SHA256 + PS384 Alg = "PS384" // RSASSA-PSS using SHA384 and MGF1-SHA384 + PS512 Alg = "PS512" // RSASSA-PSS using SHA512 and MGF1-SHA512 + EdDSA Alg = "EdDSA" +) + +var supportedAlgorithms = map[Alg]bool{ + RS256: true, + RS384: true, + RS512: true, + ES256: true, + ES384: true, + ES512: true, + PS256: true, + PS384: true, + PS512: true, + EdDSA: true, +} + +// defaultSigningAlgTableName defines the default table name for a SigningAlg +const defaultSigningAlgTableName = "auth_oidc_signing_alg" + +// SigningAlg defines an signing algorithm supported by an OIDC auth method. +// It is assigned to an OIDC AuthMethod and updates/deletes to that AuthMethod +// are cascaded to its SigningAlgs. SigningAlgs are value objects of an AuthMethod, +// therefore there's no need for oplog metadata, since only the AuthMethod will have +// metadata because it's the root aggregate. +type SigningAlg struct { + *store.SigningAlg + tableName string +} + +// NewSigningAlg creates a new in memory signing alg assigned to an OIDC +// AuthMethod. It supports no options. +func NewSigningAlg(authMethodId string, alg Alg) (*SigningAlg, error) { + const op = "oidc.NewSigningAlg" + s := &SigningAlg{ + SigningAlg: &store.SigningAlg{ + OidcMethodId: authMethodId, + Alg: string(alg), + }, + } + if err := s.validate(op); err != nil { + return nil, err // intentionally not wrapped + } + return s, nil +} + +// SupportedAlgorithm returns true iff the provided algorithm is supported +// by boundary. +func SupportedAlgorithm(a Alg) bool { + return supportedAlgorithms[a] +} + +// validate the SigningAlg. On success, it will return nil. +func (s *SigningAlg) validate(caller errors.Op) error { + if s.OidcMethodId == "" { + return errors.New(errors.InvalidParameter, caller, "missing oidc auth method id") + } + if _, ok := supportedAlgorithms[Alg(s.Alg)]; !ok { + return errors.New(errors.InvalidParameter, caller, fmt.Sprintf("unsupported signing algorithm: %s", s.Alg)) + } + return nil +} + +// AllocSigningAlg makes an empty one in memory +func AllocSigningAlg() SigningAlg { + return SigningAlg{ + SigningAlg: &store.SigningAlg{}, + } +} + +// Clone a SigningAlg +func (s *SigningAlg) Clone() *SigningAlg { + cp := proto.Clone(s.SigningAlg) + return &SigningAlg{ + SigningAlg: cp.(*store.SigningAlg), + } +} + +// TableName returns the table name. +func (s *SigningAlg) TableName() string { + if s.tableName != "" { + return s.tableName + } + return defaultSigningAlgTableName +} + +// SetTableName sets the table name. +func (s *SigningAlg) SetTableName(n string) { + s.tableName = n +} diff --git a/internal/auth/oidc/signing_alg_test.go b/internal/auth/oidc/signing_alg_test.go new file mode 100644 index 000000000..a863b75b7 --- /dev/null +++ b/internal/auth/oidc/signing_alg_test.go @@ -0,0 +1,281 @@ +package oidc + +import ( + "context" + "testing" + + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/kms" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" +) + +func TestSigningAlg_Create(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, wrapper) + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + rw := db.New(conn) + + databaseWrapper, err := kmsCache.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + + testAuthMethod := TestAuthMethod(t, conn, databaseWrapper, org.PublicId, InactiveState, "alice_rp", "my-dogs-name", + WithIssuer(TestConvertToUrls(t, "https://alice.com")[0]), WithApiUrl(TestConvertToUrls(t, "https://api.com")[0])) + + type args struct { + authMethodId string + alg Alg + } + tests := []struct { + name string + args args + want *SigningAlg + wantErr bool + wantIsErr errors.Code + create bool + wantCreateErr bool + wantCreateIsErr errors.Code + }{ + { + name: "valid", + args: args{ + authMethodId: testAuthMethod.PublicId, + alg: RS256, + }, + create: true, + want: func() *SigningAlg { + want := AllocSigningAlg() + want.OidcMethodId = testAuthMethod.PublicId + want.Alg = string(RS256) + return &want + }(), + }, + { + name: "dup", // must follow "valid" test. Alg must be be unique for an OidcMethodId + args: args{ + authMethodId: testAuthMethod.PublicId, + alg: RS256, + }, + create: true, + want: func() *SigningAlg { + want := AllocSigningAlg() + want.OidcMethodId = testAuthMethod.PublicId + want.Alg = string(RS256) + return &want + }(), + wantCreateErr: true, + wantCreateIsErr: errors.NotUnique, + }, + { + name: "empty-auth-method", + args: args{ + authMethodId: "", + alg: RS256, + }, + wantErr: true, + wantIsErr: errors.InvalidParameter, + }, + { + name: "empty-alg", + args: args{ + authMethodId: testAuthMethod.PublicId, + alg: "", + }, + wantErr: true, + wantIsErr: errors.InvalidParameter, + }, + { + name: "supported-alg", + args: args{ + authMethodId: testAuthMethod.PublicId, + alg: Alg("EVE256"), // The unsupported evesdropper 256 curve + }, + wantErr: true, + wantIsErr: errors.InvalidParameter, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + got, err := NewSigningAlg(tt.args.authMethodId, tt.args.alg) + if tt.wantErr { + require.Error(err) + assert.True(errors.Match(errors.T(tt.wantIsErr), err)) + return + } + require.NoError(err) + assert.Equal(tt.want, got) + if tt.create { + ctx := context.Background() + err = rw.Create(ctx, got) + if tt.wantCreateErr { + assert.Error(err) + assert.True(errors.Match(errors.T(tt.wantCreateIsErr), err)) + return + } else { + assert.NoError(err) + } + found := AllocSigningAlg() + require.NoError(rw.LookupWhere(ctx, &found, "oidc_method_id = ? and signing_alg_name = ?", tt.args.authMethodId, string(tt.args.alg))) + assert.Equal(got, &found) + } + }) + } +} + +func TestSigningAlg_Delete(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, wrapper) + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + rw := db.New(conn) + + databaseWrapper, err := kmsCache.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + + testAuthMethod := + TestAuthMethod( + t, + conn, + databaseWrapper, + org.PublicId, + InactiveState, + "alice_rp", + "my-dogs-name", + WithIssuer(TestConvertToUrls(t, "https://alice.com")[0]), + WithApiUrl(TestConvertToUrls(t, "https://api.com")[0]), + WithSigningAlgs(RS256)) // seed an extra callback url to just make sure the delete only gets the right num of rows + + testResource := func(authMethodId string, signingAlg Alg) *SigningAlg { + c, err := NewSigningAlg(authMethodId, signingAlg) + require.NoError(t, err) + return c + } + tests := []struct { + name string + SigningAlg *SigningAlg + wantRowsDeleted int + overrides func(*SigningAlg) + wantErr bool + wantErrMsg string + }{ + { + name: "valid", + SigningAlg: testResource(testAuthMethod.PublicId, RS384), + wantErr: false, + wantRowsDeleted: 1, + }, + { + name: "bad-OidcMethodId", + SigningAlg: testResource(testAuthMethod.PublicId, RS512), + overrides: func(c *SigningAlg) { c.OidcMethodId = "bad-id" }, + wantErr: false, + wantRowsDeleted: 0, + }, + { + name: "bad-Url", + SigningAlg: testResource(testAuthMethod.PublicId, ES256), + overrides: func(c *SigningAlg) { c.Alg = "bad-alg" }, + wantErr: false, + wantRowsDeleted: 0, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + ctx := context.Background() + cp := tt.SigningAlg.Clone() + require.NoError(rw.Create(ctx, &cp)) + + if tt.overrides != nil { + tt.overrides(cp) + } + deletedRows, err := rw.Delete(ctx, &cp) + if tt.wantErr { + require.Error(err) + return + } + require.NoError(err) + if tt.wantRowsDeleted == 0 { + assert.Equal(tt.wantRowsDeleted, deletedRows) + return + } + assert.Equal(tt.wantRowsDeleted, deletedRows) + found := AllocSigningAlg() + err = rw.LookupWhere(ctx, &found, "oidc_method_id = ? and signing_alg_name = ?", tt.SigningAlg.OidcMethodId, tt.SigningAlg.String()) + assert.Truef(errors.IsNotFoundError(err), "unexpected error: %s", err.Error()) + }) + } +} + +func TestSigningAlg_Clone(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, wrapper) + + t.Run("valid", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + databaseWrapper, err := kmsCache.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase) + require.NoError(err) + m := TestAuthMethod(t, conn, databaseWrapper, org.PublicId, InactiveState, "alice_rp", "my-dogs-name", + WithIssuer(TestConvertToUrls(t, "https://alice.com")[0]), WithApiUrl(TestConvertToUrls(t, "https://api.com")[0])) + orig, err := NewSigningAlg(m.PublicId, RS256) + require.NoError(err) + cp := orig.Clone() + assert.True(proto.Equal(cp.SigningAlg, orig.SigningAlg)) + }) + t.Run("not-equal", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + databaseWrapper, err := kmsCache.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase) + require.NoError(err) + m := TestAuthMethod(t, conn, databaseWrapper, org.PublicId, InactiveState, "alice_rp", "my-dogs-name", + WithIssuer(TestConvertToUrls(t, "https://alice.com")[0]), WithApiUrl(TestConvertToUrls(t, "https://api.com")[0])) + orig, err := NewSigningAlg(m.PublicId, ES256) + require.NoError(err) + orig2, err := NewSigningAlg(m.PublicId, ES384) + require.NoError(err) + + cp := orig.Clone() + assert.True(!proto.Equal(cp.SigningAlg, orig2.SigningAlg)) + }) +} + +func TestSigningAlg_SetTableName(t *testing.T) { + t.Parallel() + defaultTableName := defaultSigningAlgTableName + tests := []struct { + name string + setNameTo string + want string + }{ + { + name: "new-name", + setNameTo: "new-name", + want: "new-name", + }, + { + name: "reset to default", + setNameTo: "", + want: defaultTableName, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + def := AllocSigningAlg() + require.Equal(defaultTableName, def.TableName()) + m := AllocSigningAlg() + m.SetTableName(tt.setNameTo) + assert.Equal(tt.want, m.TableName()) + }) + } +} diff --git a/internal/auth/oidc/state.go b/internal/auth/oidc/state.go new file mode 100644 index 000000000..f558b70e3 --- /dev/null +++ b/internal/auth/oidc/state.go @@ -0,0 +1,21 @@ +package oidc + +// AuthMethodState defines the possible states for an oidc auth method +type AuthMethodState string + +const ( + UnknownState AuthMethodState = "unknown" + InactiveState AuthMethodState = "inactive" + ActivePrivateState AuthMethodState = "active-private" + ActivePublicState AuthMethodState = "active-public" +) + +func validState(s string) bool { + st := AuthMethodState(s) + switch st { + case InactiveState, ActivePrivateState, ActivePublicState: + return true + default: + return false + } +} diff --git a/internal/auth/oidc/state_test.go b/internal/auth/oidc/state_test.go new file mode 100644 index 000000000..b7aa04b75 --- /dev/null +++ b/internal/auth/oidc/state_test.go @@ -0,0 +1,24 @@ +package oidc + +import "testing" + +func Test_validState(t *testing.T) { + tests := []struct { + s string + want bool + }{ + {"bad", false}, + {"", false}, + {"unknown", false}, + {"inactive", true}, + {"active-private", true}, + {"active-public", true}, + } + for _, tt := range tests { + t.Run(tt.s, func(t *testing.T) { + if got := validState(tt.s); got != tt.want { + t.Errorf("validState() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/internal/auth/oidc/store/oidc.pb.go b/internal/auth/oidc/store/oidc.pb.go new file mode 100644 index 000000000..d011fd1e9 --- /dev/null +++ b/internal/auth/oidc/store/oidc.pb.go @@ -0,0 +1,932 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.26.0 +// protoc v3.12.4 +// source: controller/storage/auth/oidc/store/v1/oidc.proto + +// Package store provides protobufs for storing types in the password package. + +package store + +import ( + _ "github.com/golang/protobuf/ptypes/wrappers" + timestamp "github.com/hashicorp/boundary/internal/db/timestamp" + _ "github.com/hashicorp/boundary/internal/gen/controller/protooptions" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// AuthMethod represents an OIDC auth method. +type AuthMethod struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // @inject_tag: `gorm:"primary_key"` + PublicId string `protobuf:"bytes,10,opt,name=public_id,json=publicId,proto3" json:"public_id,omitempty" gorm:"primary_key"` + // The create_time is set by the database. + // @inject_tag: `gorm:"default:current_timestamp"` + CreateTime *timestamp.Timestamp `protobuf:"bytes,20,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty" gorm:"default:current_timestamp"` + // The update_time is set by the database. + // @inject_tag: `gorm:"default:current_timestamp"` + UpdateTime *timestamp.Timestamp `protobuf:"bytes,30,opt,name=update_time,json=updateTime,proto3" json:"update_time,omitempty" gorm:"default:current_timestamp"` + // name is optional. If set, it must be unique within scope_id. + // @inject_tag: `gorm:"default:null"` + Name string `protobuf:"bytes,40,opt,name=name,proto3" json:"name,omitempty" gorm:"default:null"` + // description is optional. + // @inject_tag: `gorm:"default:null"` + Description string `protobuf:"bytes,50,opt,name=description,proto3" json:"description,omitempty" gorm:"default:null"` + // The scope_id of the owning scope. Must be set. + // @inject_tag: `gorm:"not_null"` + ScopeId string `protobuf:"bytes,60,opt,name=scope_id,json=scopeId,proto3" json:"scope_id,omitempty" gorm:"not_null"` + // @inject_tag: `gorm:"default:null"` + Version uint32 `protobuf:"varint,70,opt,name=version,proto3" json:"version,omitempty" gorm:"default:null"` + // is_primary_auth_method is a read-only output field which indicates if the + // auth method is set as the scope's primary auth method. + // @inject_tag: `gorm:"-"` + IsPrimaryAuthMethod bool `protobuf:"varint,75,opt,name=is_primary_auth_method,json=isPrimaryAuthMethod,proto3" json:"is_primary_auth_method,omitempty" gorm:"-"` + // operational_state is the current state of the auth_oidc_method (inactive, + // active-private, or active-public). + // @inject_tag: `gorm:"column:state;not_null"` + OperationalState string `protobuf:"bytes,80,opt,name=operational_state,json=operationalState,proto3" json:"operational_state,omitempty" gorm:"column:state;not_null"` + // disable_discovered_config_validation is a flag that when set to true + // indicates the AuthMethod config was not validated against the IdP's + // discovery info document when it was updated or its operational_state was + // changed. + // @inject_tag: `gorm:"not_null"` + DisableDiscoveredConfigValidation bool `protobuf:"varint,89,opt,name=disable_discovered_config_validation,json=disableDiscoveredConfigValidation,proto3" json:"disable_discovered_config_validation,omitempty" gorm:"not_null"` + // issuer is the OIDC Discovery URL without any .well-known component + // @inject_tag: `gorm:"default:null"` + Issuer string `protobuf:"bytes,90,opt,name=issuer,proto3" json:"issuer,omitempty" gorm:"default:null"` + // client_id is the OIDC client identifier + // @inject_tag: `gorm:"not_null"` + ClientId string `protobuf:"bytes,100,opt,name=client_id,json=clientId,proto3" json:"client_id,omitempty" gorm:"not_null"` + // ct_client_secret is the encrypted OIDC client secret which is stored in the db. + // @inject_tag: `gorm:"column:client_secret;not_null" wrapping:"ct,client_secret"` + CtClientSecret []byte `protobuf:"bytes,110,opt,name=ct_client_secret,json=ctClientSecret,proto3" json:"ct_client_secret,omitempty" gorm:"column:client_secret;not_null" wrapping:"ct,client_secret"` + // client_secret is the unencrypted OIDC client secret which is not stored in the database. + // @inject_tag: `gorm:"-" wrapping:"pt,client_secret"` + ClientSecret string `protobuf:"bytes,120,opt,name=client_secret,json=clientSecret,proto3" json:"client_secret,omitempty" gorm:"-" wrapping:"pt,client_secret"` + // client_secret_hmac is a sha256-hmac of the unencrypted client_secret that + // is returned from the API for read. It is recalculated everytime the raw + // client_secret is updated. + // @inject_tag: `gorm:"not_null"` + ClientSecretHmac string `protobuf:"bytes,130,opt,name=client_secret_hmac,json=clientSecretHmac,proto3" json:"client_secret_hmac,omitempty" gorm:"not_null"` + // key_id is the key ID that was used for the encryption operation. It can be + // used to identify a specific version of the key needed to decrypt the value, + // which is useful for caching purposes. + // @inject_tag: `gorm:"not_null"` + KeyId string `protobuf:"bytes,140,opt,name=key_id,json=keyId,proto3" json:"key_id,omitempty" gorm:"not_null"` + // max_age is the allowed elapsed time in seconds since the last time the user + // was actively authenticated by the OIDC provider. -1 indicates the user + // should be re-authenticated immediately and would represent the zero value + // for max age based on the oidc spec. + // @inject_tag: `gorm:"default:null"` + MaxAge int32 `protobuf:"varint,150,opt,name=max_age,json=maxAge,proto3" json:"max_age,omitempty" gorm:"default:null"` + // api_url is the URLs prefix at which the boundary api is reachable. This value + // is used by the auth method in the different steps of the auth flow. + // @inject_tag: `gorm:"default:null"` + ApiUrl string `protobuf:"bytes,160,opt,name=api_url,json=apiUrl,proto3" json:"api_url,omitempty" gorm:"default:null"` + // signing_algs are the signing algorithms allowed for an oidc auth method. + // These are Value Objects that will be stored as SigningAlg messages, and are + // operated on as a complete set. + // @inject_tag: `gorm:"-"` + SigningAlgs []string `protobuf:"bytes,170,rep,name=signing_algs,json=signingAlgs,proto3" json:"signing_algs,omitempty" gorm:"-"` + // aud_claims are the audience claims for a specific oidc auth method. These + // are Value Objects that will be stored as AudClaim messages, and are + // operatated on as a complete set. + // @inject_tag: `gorm:"-"` + AudClaims []string `protobuf:"bytes,180,rep,name=aud_claims,json=audClaims,proto3" json:"aud_claims,omitempty" gorm:"-"` + // certificates are optional PEM encoded x509 certificates that can be + // used as trust anchors when connecting to an OIDC provider. These are Value + // Objects that will be stored as Certificate messages, and are operatated on + // as a complete set. + // @inject_tag: `gorm:"-"` + Certificates []string `protobuf:"bytes,190,rep,name=certificates,proto3" json:"certificates,omitempty" gorm:"-"` +} + +func (x *AuthMethod) Reset() { + *x = AuthMethod{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_storage_auth_oidc_store_v1_oidc_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *AuthMethod) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AuthMethod) ProtoMessage() {} + +func (x *AuthMethod) ProtoReflect() protoreflect.Message { + mi := &file_controller_storage_auth_oidc_store_v1_oidc_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AuthMethod.ProtoReflect.Descriptor instead. +func (*AuthMethod) Descriptor() ([]byte, []int) { + return file_controller_storage_auth_oidc_store_v1_oidc_proto_rawDescGZIP(), []int{0} +} + +func (x *AuthMethod) GetPublicId() string { + if x != nil { + return x.PublicId + } + return "" +} + +func (x *AuthMethod) GetCreateTime() *timestamp.Timestamp { + if x != nil { + return x.CreateTime + } + return nil +} + +func (x *AuthMethod) GetUpdateTime() *timestamp.Timestamp { + if x != nil { + return x.UpdateTime + } + return nil +} + +func (x *AuthMethod) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *AuthMethod) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *AuthMethod) GetScopeId() string { + if x != nil { + return x.ScopeId + } + return "" +} + +func (x *AuthMethod) GetVersion() uint32 { + if x != nil { + return x.Version + } + return 0 +} + +func (x *AuthMethod) GetIsPrimaryAuthMethod() bool { + if x != nil { + return x.IsPrimaryAuthMethod + } + return false +} + +func (x *AuthMethod) GetOperationalState() string { + if x != nil { + return x.OperationalState + } + return "" +} + +func (x *AuthMethod) GetDisableDiscoveredConfigValidation() bool { + if x != nil { + return x.DisableDiscoveredConfigValidation + } + return false +} + +func (x *AuthMethod) GetIssuer() string { + if x != nil { + return x.Issuer + } + return "" +} + +func (x *AuthMethod) GetClientId() string { + if x != nil { + return x.ClientId + } + return "" +} + +func (x *AuthMethod) GetCtClientSecret() []byte { + if x != nil { + return x.CtClientSecret + } + return nil +} + +func (x *AuthMethod) GetClientSecret() string { + if x != nil { + return x.ClientSecret + } + return "" +} + +func (x *AuthMethod) GetClientSecretHmac() string { + if x != nil { + return x.ClientSecretHmac + } + return "" +} + +func (x *AuthMethod) GetKeyId() string { + if x != nil { + return x.KeyId + } + return "" +} + +func (x *AuthMethod) GetMaxAge() int32 { + if x != nil { + return x.MaxAge + } + return 0 +} + +func (x *AuthMethod) GetApiUrl() string { + if x != nil { + return x.ApiUrl + } + return "" +} + +func (x *AuthMethod) GetSigningAlgs() []string { + if x != nil { + return x.SigningAlgs + } + return nil +} + +func (x *AuthMethod) GetAudClaims() []string { + if x != nil { + return x.AudClaims + } + return nil +} + +func (x *AuthMethod) GetCertificates() []string { + if x != nil { + return x.Certificates + } + return nil +} + +// Account represents an OIDC account +// the scope_id column is not included here as it is used only to ensure +// data integrity in the database between iam users and auth methods. +type Account struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // @inject_tag: `gorm:"primary_key"` + PublicId string `protobuf:"bytes,10,opt,name=public_id,json=publicId,proto3" json:"public_id,omitempty" gorm:"primary_key"` + // The create_time is set by the database. + // @inject_tag: `gorm:"default:current_timestamp"` + CreateTime *timestamp.Timestamp `protobuf:"bytes,20,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty" gorm:"default:current_timestamp"` + // The update_time is set by the database. + // @inject_tag: `gorm:"default:current_timestamp"` + UpdateTime *timestamp.Timestamp `protobuf:"bytes,30,opt,name=update_time,json=updateTime,proto3" json:"update_time,omitempty" gorm:"default:current_timestamp"` + // name is optional. If set, it must be unique within scope_id. + // @inject_tag: `gorm:"default:null"` + Name string `protobuf:"bytes,40,opt,name=name,proto3" json:"name,omitempty" gorm:"default:null"` + // description is optional. + // @inject_tag: `gorm:"default:null"` + Description string `protobuf:"bytes,50,opt,name=description,proto3" json:"description,omitempty" gorm:"default:null"` + // @inject_tag: `gorm:"default:null"` + Version uint32 `protobuf:"varint,60,opt,name=version,proto3" json:"version,omitempty" gorm:"default:null"` + // auth_method_id is the fk to the account's auth method. + // @inject_tag: `gorm:"not_null"` + AuthMethodId string `protobuf:"bytes,70,opt,name=auth_method_id,json=authMethodId,proto3" json:"auth_method_id,omitempty" gorm:"not_null"` + // issuer is a case sensitive URL that maps to the OIDC iss claim + // @inject_tag: `gorm:"not_null"` + Issuer string `protobuf:"bytes,80,opt,name=issuer,proto3" json:"issuer,omitempty" gorm:"not_null"` + // subject is a case sensitive string that maps to the OIDC sub claim. + // @inject_tag: `gorm:"not_null"` + Subject string `protobuf:"bytes,90,opt,name=subject,proto3" json:"subject,omitempty" gorm:"not_null"` + // full_name is a string that maps to the OIDC name claim + // @inject_tag: `gorm:"default:null"` + FullName string `protobuf:"bytes,100,opt,name=full_name,json=fullName,proto3" json:"full_name,omitempty" gorm:"default:null"` + // email is a string that maps to the OIDC email claim. + // @inject_tag: `gorm:"default:null"` + Email string `protobuf:"bytes,110,opt,name=email,proto3" json:"email,omitempty" gorm:"default:null"` +} + +func (x *Account) Reset() { + *x = Account{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_storage_auth_oidc_store_v1_oidc_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Account) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Account) ProtoMessage() {} + +func (x *Account) ProtoReflect() protoreflect.Message { + mi := &file_controller_storage_auth_oidc_store_v1_oidc_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Account.ProtoReflect.Descriptor instead. +func (*Account) Descriptor() ([]byte, []int) { + return file_controller_storage_auth_oidc_store_v1_oidc_proto_rawDescGZIP(), []int{1} +} + +func (x *Account) GetPublicId() string { + if x != nil { + return x.PublicId + } + return "" +} + +func (x *Account) GetCreateTime() *timestamp.Timestamp { + if x != nil { + return x.CreateTime + } + return nil +} + +func (x *Account) GetUpdateTime() *timestamp.Timestamp { + if x != nil { + return x.UpdateTime + } + return nil +} + +func (x *Account) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Account) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *Account) GetVersion() uint32 { + if x != nil { + return x.Version + } + return 0 +} + +func (x *Account) GetAuthMethodId() string { + if x != nil { + return x.AuthMethodId + } + return "" +} + +func (x *Account) GetIssuer() string { + if x != nil { + return x.Issuer + } + return "" +} + +func (x *Account) GetSubject() string { + if x != nil { + return x.Subject + } + return "" +} + +func (x *Account) GetFullName() string { + if x != nil { + return x.FullName + } + return "" +} + +func (x *Account) GetEmail() string { + if x != nil { + return x.Email + } + return "" +} + +// SigningAlg entries are the signing algorithms allowed for an oidc auth method. +type SigningAlg struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // @inject_tag: `gorm:"primary_key"` + OidcMethodId string `protobuf:"bytes,10,opt,name=oidc_method_id,json=oidcMethodId,proto3" json:"oidc_method_id,omitempty" gorm:"primary_key"` + // alg is an enum from the auth_oidc_signing_alg_enm table + // @inject_tag: `gorm:"primary_key;column:signing_alg_name"` + Alg string `protobuf:"bytes,20,opt,name=alg,proto3" json:"alg,omitempty" gorm:"primary_key;column:signing_alg_name"` + // The create_time is set by the database. + // @inject_tag: `gorm:"default:current_timestamp"` + CreateTime *timestamp.Timestamp `protobuf:"bytes,30,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty" gorm:"default:current_timestamp"` +} + +func (x *SigningAlg) Reset() { + *x = SigningAlg{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_storage_auth_oidc_store_v1_oidc_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SigningAlg) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SigningAlg) ProtoMessage() {} + +func (x *SigningAlg) ProtoReflect() protoreflect.Message { + mi := &file_controller_storage_auth_oidc_store_v1_oidc_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SigningAlg.ProtoReflect.Descriptor instead. +func (*SigningAlg) Descriptor() ([]byte, []int) { + return file_controller_storage_auth_oidc_store_v1_oidc_proto_rawDescGZIP(), []int{2} +} + +func (x *SigningAlg) GetOidcMethodId() string { + if x != nil { + return x.OidcMethodId + } + return "" +} + +func (x *SigningAlg) GetAlg() string { + if x != nil { + return x.Alg + } + return "" +} + +func (x *SigningAlg) GetCreateTime() *timestamp.Timestamp { + if x != nil { + return x.CreateTime + } + return nil +} + +// AudClaim entries are the audience claims for a specific oidc auth method. +type AudClaim struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // @inject_tag: `gorm:"primary_key"` + OidcMethodId string `protobuf:"bytes,10,opt,name=oidc_method_id,json=oidcMethodId,proto3" json:"oidc_method_id,omitempty" gorm:"primary_key"` + // aud is an allowed audience claim for id_tokens + // @inject_tag: `gorm:"primary_key;column:aud_claim""` + Aud string `protobuf:"bytes,20,opt,name=aud,proto3" json:"aud,omitempty" gorm:"primary_key;column:aud_claim"` + // The create_time is set by the database. + // @inject_tag: `gorm:"default:current_timestamp"` + CreateTime *timestamp.Timestamp `protobuf:"bytes,30,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty" gorm:"default:current_timestamp"` +} + +func (x *AudClaim) Reset() { + *x = AudClaim{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_storage_auth_oidc_store_v1_oidc_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *AudClaim) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AudClaim) ProtoMessage() {} + +func (x *AudClaim) ProtoReflect() protoreflect.Message { + mi := &file_controller_storage_auth_oidc_store_v1_oidc_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AudClaim.ProtoReflect.Descriptor instead. +func (*AudClaim) Descriptor() ([]byte, []int) { + return file_controller_storage_auth_oidc_store_v1_oidc_proto_rawDescGZIP(), []int{3} +} + +func (x *AudClaim) GetOidcMethodId() string { + if x != nil { + return x.OidcMethodId + } + return "" +} + +func (x *AudClaim) GetAud() string { + if x != nil { + return x.Aud + } + return "" +} + +func (x *AudClaim) GetCreateTime() *timestamp.Timestamp { + if x != nil { + return x.CreateTime + } + return nil +} + +// Certificate entries are optional PEM encoded x509 certificates that can be +// used as trust anchors when connecting to an OIDC provider. +type Certificate struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // @inject_tag: `gorm:"primary_key"` + OidcMethodId string `protobuf:"bytes,10,opt,name=oidc_method_id,json=oidcMethodId,proto3" json:"oidc_method_id,omitempty" gorm:"primary_key"` + // certificate is a PEM encoded x509 + // @inject_tag: `gorm:"column:certificate;primary_key"` + Cert string `protobuf:"bytes,20,opt,name=cert,proto3" json:"cert,omitempty" gorm:"column:certificate;primary_key"` + // The create_time is set by the database. + // @inject_tag: `gorm:"default:current_timestamp"` + CreateTime *timestamp.Timestamp `protobuf:"bytes,30,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty" gorm:"default:current_timestamp"` +} + +func (x *Certificate) Reset() { + *x = Certificate{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_storage_auth_oidc_store_v1_oidc_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Certificate) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Certificate) ProtoMessage() {} + +func (x *Certificate) ProtoReflect() protoreflect.Message { + mi := &file_controller_storage_auth_oidc_store_v1_oidc_proto_msgTypes[4] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Certificate.ProtoReflect.Descriptor instead. +func (*Certificate) Descriptor() ([]byte, []int) { + return file_controller_storage_auth_oidc_store_v1_oidc_proto_rawDescGZIP(), []int{4} +} + +func (x *Certificate) GetOidcMethodId() string { + if x != nil { + return x.OidcMethodId + } + return "" +} + +func (x *Certificate) GetCert() string { + if x != nil { + return x.Cert + } + return "" +} + +func (x *Certificate) GetCreateTime() *timestamp.Timestamp { + if x != nil { + return x.CreateTime + } + return nil +} + +var File_controller_storage_auth_oidc_store_v1_oidc_proto protoreflect.FileDescriptor + +var file_controller_storage_auth_oidc_store_v1_oidc_proto_rawDesc = []byte{ + 0x0a, 0x30, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x73, 0x74, 0x6f, + 0x72, 0x61, 0x67, 0x65, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x2f, 0x6f, 0x69, 0x64, 0x63, 0x2f, 0x73, + 0x74, 0x6f, 0x72, 0x65, 0x2f, 0x76, 0x31, 0x2f, 0x6f, 0x69, 0x64, 0x63, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x12, 0x25, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, + 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x2e, 0x6f, 0x69, 0x64, 0x63, + 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x2e, 0x76, 0x31, 0x1a, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, + 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2f, 0x74, 0x69, + 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2f, 0x76, 0x31, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, + 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x2a, 0x63, 0x6f, 0x6e, 0x74, + 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5f, 0x6f, 0x70, + 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x76, 0x31, 0x2f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x77, 0x72, 0x61, 0x70, 0x70, 0x65, 0x72, 0x73, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xc9, 0x09, 0x0a, 0x0a, 0x41, 0x75, 0x74, 0x68, 0x4d, + 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, + 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, + 0x49, 0x64, 0x12, 0x4b, 0x0a, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, + 0x65, 0x18, 0x14, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, + 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, + 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, + 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, + 0x4b, 0x0a, 0x0b, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x1e, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, + 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, + 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, + 0x52, 0x0a, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x24, 0x0a, 0x04, + 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x28, 0x20, 0x01, 0x28, 0x09, 0x42, 0x10, 0xc2, 0xdd, 0x29, 0x0c, + 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x52, 0x04, 0x6e, 0x61, + 0x6d, 0x65, 0x12, 0x40, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, + 0x6e, 0x18, 0x32, 0x20, 0x01, 0x28, 0x09, 0x42, 0x1e, 0xc2, 0xdd, 0x29, 0x1a, 0x0a, 0x0b, 0x44, + 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0b, 0x64, 0x65, 0x73, 0x63, + 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, + 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x19, 0x0a, 0x08, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x5f, 0x69, 0x64, + 0x18, 0x3c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x49, 0x64, 0x12, + 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x46, 0x20, 0x01, 0x28, 0x0d, + 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x33, 0x0a, 0x16, 0x69, 0x73, 0x5f, + 0x70, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x6d, 0x65, 0x74, + 0x68, 0x6f, 0x64, 0x18, 0x4b, 0x20, 0x01, 0x28, 0x08, 0x52, 0x13, 0x69, 0x73, 0x50, 0x72, 0x69, + 0x6d, 0x61, 0x72, 0x79, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x2b, + 0x0a, 0x11, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x5f, 0x73, 0x74, + 0x61, 0x74, 0x65, 0x18, 0x50, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x6f, 0x70, 0x65, 0x72, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x4f, 0x0a, 0x24, 0x64, + 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x65, + 0x64, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x5f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, + 0x69, 0x6f, 0x6e, 0x18, 0x59, 0x20, 0x01, 0x28, 0x08, 0x52, 0x21, 0x64, 0x69, 0x73, 0x61, 0x62, + 0x6c, 0x65, 0x44, 0x69, 0x73, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x65, 0x64, 0x43, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x37, 0x0a, 0x06, + 0x69, 0x73, 0x73, 0x75, 0x65, 0x72, 0x18, 0x5a, 0x20, 0x01, 0x28, 0x09, 0x42, 0x1f, 0xc2, 0xdd, + 0x29, 0x1b, 0x0a, 0x06, 0x49, 0x73, 0x73, 0x75, 0x65, 0x72, 0x12, 0x11, 0x61, 0x74, 0x74, 0x72, + 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x69, 0x73, 0x73, 0x75, 0x65, 0x72, 0x52, 0x06, 0x69, + 0x73, 0x73, 0x75, 0x65, 0x72, 0x12, 0x41, 0x0a, 0x09, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, + 0x69, 0x64, 0x18, 0x64, 0x20, 0x01, 0x28, 0x09, 0x42, 0x24, 0xc2, 0xdd, 0x29, 0x20, 0x0a, 0x08, + 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x14, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, + 0x75, 0x74, 0x65, 0x73, 0x2e, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x52, 0x08, + 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x28, 0x0a, 0x10, 0x63, 0x74, 0x5f, 0x63, + 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x18, 0x6e, 0x20, 0x01, + 0x28, 0x0c, 0x52, 0x0e, 0x63, 0x74, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x63, 0x72, + 0x65, 0x74, 0x12, 0x51, 0x0a, 0x0d, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x73, 0x65, 0x63, + 0x72, 0x65, 0x74, 0x18, 0x78, 0x20, 0x01, 0x28, 0x09, 0x42, 0x2c, 0xc2, 0xdd, 0x29, 0x28, 0x0a, + 0x0c, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x12, 0x18, 0x61, + 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, + 0x5f, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x52, 0x0c, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x53, + 0x65, 0x63, 0x72, 0x65, 0x74, 0x12, 0x2d, 0x0a, 0x12, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, + 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x5f, 0x68, 0x6d, 0x61, 0x63, 0x18, 0x82, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x10, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, + 0x48, 0x6d, 0x61, 0x63, 0x12, 0x16, 0x0a, 0x06, 0x6b, 0x65, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x8c, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6b, 0x65, 0x79, 0x49, 0x64, 0x12, 0x3a, 0x0a, 0x07, + 0x6d, 0x61, 0x78, 0x5f, 0x61, 0x67, 0x65, 0x18, 0x96, 0x01, 0x20, 0x01, 0x28, 0x05, 0x42, 0x20, + 0xc2, 0xdd, 0x29, 0x1c, 0x0a, 0x06, 0x4d, 0x61, 0x78, 0x41, 0x67, 0x65, 0x12, 0x12, 0x61, 0x74, + 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x6d, 0x61, 0x78, 0x5f, 0x61, 0x67, 0x65, + 0x52, 0x06, 0x6d, 0x61, 0x78, 0x41, 0x67, 0x65, 0x12, 0x41, 0x0a, 0x07, 0x61, 0x70, 0x69, 0x5f, + 0x75, 0x72, 0x6c, 0x18, 0xa0, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x27, 0xc2, 0xdd, 0x29, 0x23, + 0x0a, 0x06, 0x41, 0x70, 0x69, 0x55, 0x72, 0x6c, 0x12, 0x19, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, + 0x75, 0x74, 0x65, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x5f, 0x75, 0x72, 0x6c, 0x5f, 0x70, 0x72, 0x65, + 0x66, 0x69, 0x78, 0x52, 0x06, 0x61, 0x70, 0x69, 0x55, 0x72, 0x6c, 0x12, 0x54, 0x0a, 0x0c, 0x73, + 0x69, 0x67, 0x6e, 0x69, 0x6e, 0x67, 0x5f, 0x61, 0x6c, 0x67, 0x73, 0x18, 0xaa, 0x01, 0x20, 0x03, + 0x28, 0x09, 0x42, 0x30, 0xc2, 0xdd, 0x29, 0x2c, 0x0a, 0x0b, 0x53, 0x69, 0x67, 0x6e, 0x69, 0x6e, + 0x67, 0x41, 0x6c, 0x67, 0x73, 0x12, 0x1d, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, + 0x73, 0x2e, 0x73, 0x69, 0x67, 0x6e, 0x69, 0x6e, 0x67, 0x5f, 0x61, 0x6c, 0x67, 0x6f, 0x72, 0x69, + 0x74, 0x68, 0x6d, 0x73, 0x52, 0x0b, 0x73, 0x69, 0x67, 0x6e, 0x69, 0x6e, 0x67, 0x41, 0x6c, 0x67, + 0x73, 0x12, 0x4d, 0x0a, 0x0a, 0x61, 0x75, 0x64, 0x5f, 0x63, 0x6c, 0x61, 0x69, 0x6d, 0x73, 0x18, + 0xb4, 0x01, 0x20, 0x03, 0x28, 0x09, 0x42, 0x2d, 0xc2, 0xdd, 0x29, 0x29, 0x0a, 0x09, 0x41, 0x75, + 0x64, 0x43, 0x6c, 0x61, 0x69, 0x6d, 0x73, 0x12, 0x1c, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, + 0x74, 0x65, 0x73, 0x2e, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x5f, 0x61, 0x75, 0x64, 0x69, + 0x65, 0x6e, 0x63, 0x65, 0x73, 0x52, 0x09, 0x61, 0x75, 0x64, 0x43, 0x6c, 0x61, 0x69, 0x6d, 0x73, + 0x12, 0x50, 0x0a, 0x0c, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x73, + 0x18, 0xbe, 0x01, 0x20, 0x03, 0x28, 0x09, 0x42, 0x2b, 0xc2, 0xdd, 0x29, 0x27, 0x0a, 0x0c, 0x43, + 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x73, 0x12, 0x17, 0x61, 0x74, 0x74, + 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x69, 0x64, 0x70, 0x5f, 0x63, 0x61, 0x5f, 0x63, + 0x65, 0x72, 0x74, 0x73, 0x52, 0x0c, 0x63, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, + 0x65, 0x73, 0x22, 0xcd, 0x03, 0x0a, 0x07, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x1b, + 0x0a, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x08, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x49, 0x64, 0x12, 0x4b, 0x0a, 0x0b, 0x63, + 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x14, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, + 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, + 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x63, 0x72, + 0x65, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x4b, 0x0a, 0x0b, 0x75, 0x70, 0x64, 0x61, + 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, + 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, + 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, + 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x75, 0x70, 0x64, 0x61, 0x74, + 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x24, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x28, 0x20, + 0x01, 0x28, 0x09, 0x42, 0x10, 0xc2, 0xdd, 0x29, 0x0c, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, + 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x40, 0x0a, 0x0b, 0x64, + 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x32, 0x20, 0x01, 0x28, 0x09, + 0x42, 0x1e, 0xc2, 0xdd, 0x29, 0x1a, 0x0a, 0x0b, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, + 0x69, 0x6f, 0x6e, 0x12, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, + 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x18, 0x0a, + 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x3c, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, + 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x24, 0x0a, 0x0e, 0x61, 0x75, 0x74, 0x68, 0x5f, + 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x46, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0c, 0x61, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x49, 0x64, 0x12, 0x16, 0x0a, + 0x06, 0x69, 0x73, 0x73, 0x75, 0x65, 0x72, 0x18, 0x50, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x69, + 0x73, 0x73, 0x75, 0x65, 0x72, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, + 0x18, 0x5a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x12, + 0x1b, 0x0a, 0x09, 0x66, 0x75, 0x6c, 0x6c, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x64, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x08, 0x66, 0x75, 0x6c, 0x6c, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, + 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x18, 0x6e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x6d, 0x61, + 0x69, 0x6c, 0x22, 0x91, 0x01, 0x0a, 0x0a, 0x53, 0x69, 0x67, 0x6e, 0x69, 0x6e, 0x67, 0x41, 0x6c, + 0x67, 0x12, 0x24, 0x0a, 0x0e, 0x6f, 0x69, 0x64, 0x63, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, + 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x6f, 0x69, 0x64, 0x63, 0x4d, + 0x65, 0x74, 0x68, 0x6f, 0x64, 0x49, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x6c, 0x67, 0x18, 0x14, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x61, 0x6c, 0x67, 0x12, 0x4b, 0x0a, 0x0b, 0x63, 0x72, 0x65, + 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, + 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, + 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, + 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x63, 0x72, 0x65, 0x61, + 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x22, 0x8f, 0x01, 0x0a, 0x08, 0x41, 0x75, 0x64, 0x43, 0x6c, + 0x61, 0x69, 0x6d, 0x12, 0x24, 0x0a, 0x0e, 0x6f, 0x69, 0x64, 0x63, 0x5f, 0x6d, 0x65, 0x74, 0x68, + 0x6f, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x6f, 0x69, 0x64, + 0x63, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x49, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x75, 0x64, + 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x61, 0x75, 0x64, 0x12, 0x4b, 0x0a, 0x0b, 0x63, + 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, + 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, + 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x63, 0x72, + 0x65, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x22, 0x94, 0x01, 0x0a, 0x0b, 0x43, 0x65, 0x72, + 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, 0x24, 0x0a, 0x0e, 0x6f, 0x69, 0x64, 0x63, + 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0c, 0x6f, 0x69, 0x64, 0x63, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x49, 0x64, 0x12, 0x12, + 0x0a, 0x04, 0x63, 0x65, 0x72, 0x74, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x63, 0x65, + 0x72, 0x74, 0x12, 0x4b, 0x0a, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, + 0x65, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, + 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, + 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, + 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x42, + 0x3e, 0x5a, 0x3c, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, + 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, + 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x2f, 0x6f, + 0x69, 0x64, 0x63, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x3b, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x62, + 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_controller_storage_auth_oidc_store_v1_oidc_proto_rawDescOnce sync.Once + file_controller_storage_auth_oidc_store_v1_oidc_proto_rawDescData = file_controller_storage_auth_oidc_store_v1_oidc_proto_rawDesc +) + +func file_controller_storage_auth_oidc_store_v1_oidc_proto_rawDescGZIP() []byte { + file_controller_storage_auth_oidc_store_v1_oidc_proto_rawDescOnce.Do(func() { + file_controller_storage_auth_oidc_store_v1_oidc_proto_rawDescData = protoimpl.X.CompressGZIP(file_controller_storage_auth_oidc_store_v1_oidc_proto_rawDescData) + }) + return file_controller_storage_auth_oidc_store_v1_oidc_proto_rawDescData +} + +var file_controller_storage_auth_oidc_store_v1_oidc_proto_msgTypes = make([]protoimpl.MessageInfo, 5) +var file_controller_storage_auth_oidc_store_v1_oidc_proto_goTypes = []interface{}{ + (*AuthMethod)(nil), // 0: controller.storage.auth.oidc.store.v1.AuthMethod + (*Account)(nil), // 1: controller.storage.auth.oidc.store.v1.Account + (*SigningAlg)(nil), // 2: controller.storage.auth.oidc.store.v1.SigningAlg + (*AudClaim)(nil), // 3: controller.storage.auth.oidc.store.v1.AudClaim + (*Certificate)(nil), // 4: controller.storage.auth.oidc.store.v1.Certificate + (*timestamp.Timestamp)(nil), // 5: controller.storage.timestamp.v1.Timestamp +} +var file_controller_storage_auth_oidc_store_v1_oidc_proto_depIdxs = []int32{ + 5, // 0: controller.storage.auth.oidc.store.v1.AuthMethod.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 5, // 1: controller.storage.auth.oidc.store.v1.AuthMethod.update_time:type_name -> controller.storage.timestamp.v1.Timestamp + 5, // 2: controller.storage.auth.oidc.store.v1.Account.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 5, // 3: controller.storage.auth.oidc.store.v1.Account.update_time:type_name -> controller.storage.timestamp.v1.Timestamp + 5, // 4: controller.storage.auth.oidc.store.v1.SigningAlg.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 5, // 5: controller.storage.auth.oidc.store.v1.AudClaim.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 5, // 6: controller.storage.auth.oidc.store.v1.Certificate.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 7, // [7:7] is the sub-list for method output_type + 7, // [7:7] is the sub-list for method input_type + 7, // [7:7] is the sub-list for extension type_name + 7, // [7:7] is the sub-list for extension extendee + 0, // [0:7] is the sub-list for field type_name +} + +func init() { file_controller_storage_auth_oidc_store_v1_oidc_proto_init() } +func file_controller_storage_auth_oidc_store_v1_oidc_proto_init() { + if File_controller_storage_auth_oidc_store_v1_oidc_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_controller_storage_auth_oidc_store_v1_oidc_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*AuthMethod); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_controller_storage_auth_oidc_store_v1_oidc_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Account); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_controller_storage_auth_oidc_store_v1_oidc_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*SigningAlg); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_controller_storage_auth_oidc_store_v1_oidc_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*AudClaim); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_controller_storage_auth_oidc_store_v1_oidc_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Certificate); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_controller_storage_auth_oidc_store_v1_oidc_proto_rawDesc, + NumEnums: 0, + NumMessages: 5, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_controller_storage_auth_oidc_store_v1_oidc_proto_goTypes, + DependencyIndexes: file_controller_storage_auth_oidc_store_v1_oidc_proto_depIdxs, + MessageInfos: file_controller_storage_auth_oidc_store_v1_oidc_proto_msgTypes, + }.Build() + File_controller_storage_auth_oidc_store_v1_oidc_proto = out.File + file_controller_storage_auth_oidc_store_v1_oidc_proto_rawDesc = nil + file_controller_storage_auth_oidc_store_v1_oidc_proto_goTypes = nil + file_controller_storage_auth_oidc_store_v1_oidc_proto_depIdxs = nil +} diff --git a/internal/auth/oidc/testing.go b/internal/auth/oidc/testing.go new file mode 100644 index 000000000..714583fd2 --- /dev/null +++ b/internal/auth/oidc/testing.go @@ -0,0 +1,469 @@ +package oidc + +import ( + "context" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "fmt" + "io/ioutil" + "log" + "math/big" + "net" + "net/http" + "net/http/httptest" + "net/url" + "sort" + "sync" + "testing" + "time" + + "github.com/golang/protobuf/ptypes" + "github.com/hashicorp/boundary/internal/auth/oidc/request" + "github.com/hashicorp/boundary/internal/authtoken" + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/db/timestamp" + "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/cap/oidc" + wrapping "github.com/hashicorp/go-kms-wrapping" + "github.com/jinzhu/gorm" + "github.com/stretchr/testify/require" +) + +// TestAuthMethod creates a test oidc auth method. WithName, WithDescription, +// WithMaxAge, WithApiUrl, WithIssuer, WithCertificates, WithAudClaims, and +// WithSigningAlgs options are supported. +func TestAuthMethod( + t *testing.T, + conn *gorm.DB, + databaseWrapper wrapping.Wrapper, + scopeId string, + state AuthMethodState, + clientId string, + clientSecret ClientSecret, + opt ...Option) *AuthMethod { + t.Helper() + opts := getOpts(opt...) + require := require.New(t) + rw := db.New(conn) + ctx := context.Background() + + authMethod, err := NewAuthMethod(scopeId, clientId, clientSecret, opt...) + require.NoError(err) + id, err := newAuthMethodId() + require.NoError(err) + authMethod.PublicId = id + err = authMethod.encrypt(ctx, databaseWrapper) + require.NoError(err) + err = rw.Create(ctx, authMethod) + require.NoError(err) + + if len(opts.withAudClaims) > 0 { + newAudClaims := make([]interface{}, 0, len(opts.withAudClaims)) + for _, a := range opts.withAudClaims { + aud, err := NewAudClaim(authMethod.PublicId, a) + require.NoError(err) + newAudClaims = append(newAudClaims, aud) + } + err := rw.CreateItems(ctx, newAudClaims) + require.NoError(err) + require.Equal(len(opts.withAudClaims), len(authMethod.AudClaims)) + } + if len(opts.withCertificates) > 0 { + newCerts := make([]interface{}, 0, len(opts.withCertificates)) + for _, c := range opts.withCertificates { + pem, err := EncodeCertificates(c) + require.NoError(err) + cert, err := NewCertificate(authMethod.PublicId, pem[0]) + require.NoError(err) + newCerts = append(newCerts, cert) + } + err := rw.CreateItems(ctx, newCerts) + require.NoError(err) + require.Equal(len(opts.withCertificates), len(authMethod.Certificates)) + } + if len(opts.withSigningAlgs) > 0 { + newAlgs := make([]interface{}, 0, len(opts.withSigningAlgs)) + for _, a := range opts.withSigningAlgs { + alg, err := NewSigningAlg(authMethod.PublicId, a) + require.NoError(err) + newAlgs = append(newAlgs, alg) + } + err := rw.CreateItems(ctx, newAlgs) + require.NoError(err) + require.Equal(len(opts.withSigningAlgs), len(authMethod.SigningAlgs)) + } + + authMethod.OperationalState = string(state) + rowsUpdated, err := rw.Update(ctx, authMethod, []string{"OperationalState"}, nil) + require.NoError(err) + require.True(rowsUpdated == 0 || rowsUpdated == 1) + + return authMethod +} + +// TestSortAuthMethods will sort the provided auth methods by public id and it +// will sort each auth method's embedded value objects (algs, auds, certs, +// callbacks) +func TestSortAuthMethods(t *testing.T, methods []*AuthMethod) { + // sort them by public id first... + sort.Slice(methods, func(a, b int) bool { + return methods[a].PublicId < methods[b].PublicId + }) + + // sort all the embedded value objects... + for _, am := range methods { + sort.Slice(am.SigningAlgs, func(a, b int) bool { + return am.SigningAlgs[a] < am.SigningAlgs[b] + }) + sort.Slice(am.AudClaims, func(a, b int) bool { + return am.AudClaims[a] < am.AudClaims[b] + }) + sort.Slice(am.Certificates, func(a, b int) bool { + return am.Certificates[a] < am.Certificates[b] + }) + } +} + +// TestAccount creates a test oidc auth account. +func TestAccount(t *testing.T, conn *gorm.DB, am *AuthMethod, subject string, opt ...Option) *Account { + t.Helper() + require := require.New(t) + rw := db.New(conn) + ctx := context.Background() + + u, err := url.Parse(am.GetIssuer()) + require.NoError(err) + opt = append(opt, WithIssuer(u)) + a, err := NewAccount(am.PublicId, subject, opt...) + require.NoError(err) + + id, err := newAccountId(am.GetPublicId(), am.Issuer, subject) + require.NoError(err) + a.PublicId = id + + require.NoError(rw.Create(ctx, a)) + return a +} + +// TestConvertToUrls will convert URL string representations to a slice of +// *url.URL +func TestConvertToUrls(t *testing.T, urls ...string) []*url.URL { + t.Helper() + require := require.New(t) + require.NotEmpty(urls) + var convertedUrls []*url.URL + for _, u := range urls { + parsed, err := url.Parse(u) + require.NoError(err) + require.Contains([]string{"http", "https"}, parsed.Scheme) + convertedUrls = append(convertedUrls, parsed) + } + return convertedUrls +} + +// testGenerateCA will generate a test x509 CA cert, along with it encoded in a +// PEM format. +func testGenerateCA(t *testing.T, hosts ...string) (*x509.Certificate, string) { + t.Helper() + require := require.New(t) + + priv, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) + require.NoError(err) + + // ECDSA, ED25519 and RSA subject keys should have the DigitalSignature + // KeyUsage bits set in the x509.Certificate template + keyUsage := x509.KeyUsageDigitalSignature + + validFor := 2 * time.Minute + notBefore := time.Now() + notAfter := notBefore.Add(validFor) + + serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) + serialNumber, err := rand.Int(rand.Reader, serialNumberLimit) + require.NoError(err) + + template := x509.Certificate{ + SerialNumber: serialNumber, + Subject: pkix.Name{ + Organization: []string{"Acme Co"}, + }, + NotBefore: notBefore, + NotAfter: notAfter, + + KeyUsage: keyUsage, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + } + + for _, h := range hosts { + if ip := net.ParseIP(h); ip != nil { + template.IPAddresses = append(template.IPAddresses, ip) + } else { + template.DNSNames = append(template.DNSNames, h) + } + } + + template.IsCA = true + template.KeyUsage |= x509.KeyUsageCertSign + + derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv) + require.NoError(err) + + c, err := x509.ParseCertificate(derBytes) + require.NoError(err) + + return c, string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes})) +} + +func testProvider(t *testing.T, clientId, clientSecret, allowedRedirectURL string, tp *oidc.TestProvider) *oidc.Provider { + t.Helper() + require := require.New(t) + require.NotEmptyf(clientId, "%s: client id is empty") + require.NotEmptyf(clientSecret, "%s: client secret is empty") + require.NotEmptyf(allowedRedirectURL, "%s: redirect URL is empty") + + tp.SetClientCreds(clientId, clientSecret) + _, _, alg, _ := tp.SigningKeys() + + c1, err := oidc.NewConfig( + tp.Addr(), + clientId, + oidc.ClientSecret(clientSecret), + []oidc.Alg{alg}, + []string{allowedRedirectURL}, + oidc.WithProviderCA(tp.CACert()), + ) + require.NoError(err) + p1, err := oidc.NewProvider(c1) + require.NoError(err) + return p1 +} + +// testState will make a request.State and encrypt/encode within a request.Wrapper. +// the returned string can be used as a parameter for functions like: oidc.Callback +func testState( + t *testing.T, + am *AuthMethod, + kms *kms.Kms, + tokenRequestId string, + expIn time.Duration, + finalRedirect string, + providerHash uint64, + nonce string, +) string { + t.Helper() + ctx := context.Background() + require := require.New(t) + require.NotNil(am) + require.NotNil(kms) + + now := time.Now() + createTime, err := ptypes.TimestampProto(now.Truncate(time.Second)) + require.NoError(err) + exp, err := ptypes.TimestampProto(now.Add(expIn).Truncate(time.Second)) + require.NoError(err) + + st := &request.State{ + TokenRequestId: tokenRequestId, + CreateTime: ×tamp.Timestamp{Timestamp: createTime}, + ExpirationTime: ×tamp.Timestamp{Timestamp: exp}, + FinalRedirectUrl: finalRedirect, + Nonce: nonce, + ProviderConfigHash: providerHash, + } + requestWrapper, err := requestWrappingWrapper(ctx, kms, am.ScopeId, am.PublicId) + require.NoError(err) + encodedEncryptedSt, err := encryptMessage(ctx, requestWrapper, am, st) + require.NoError(err) + return encodedEncryptedSt +} + +// TestTokenRequestId will make a request.Token and encrypt/encode within a request.Wrapper. +// the returned string can be used as a parameter for functions like: oidc.TokenRequest +func TestTokenRequestId( + t *testing.T, + am *AuthMethod, + kms *kms.Kms, + expIn time.Duration, + tokenPublicId string, +) string { + t.Helper() + ctx := context.Background() + require := require.New(t) + + now := time.Now() + exp, err := ptypes.TimestampProto(now.Add(expIn).Truncate(time.Second)) + require.NoError(err) + + reqTk := &request.Token{ + RequestId: tokenPublicId, + ExpirationTime: ×tamp.Timestamp{Timestamp: exp}, + } + requestWrapper, err := requestWrappingWrapper(ctx, kms, am.ScopeId, am.PublicId) + require.NoError(err) + + encodedEncryptedReqTk, err := encryptMessage(ctx, requestWrapper, am, reqTk) + require.NoError(err) + return encodedEncryptedReqTk +} + +// TestPendingToken will create a pending auth token for the tokenRequestId (aka public id) +func TestPendingToken( + t *testing.T, + tokenRepo *authtoken.Repository, + user *iam.User, + acct *Account, + tokenRequestId string, +) *authtoken.AuthToken { + t.Helper() + ctx := context.Background() + require := require.New(t) + tk, err := tokenRepo.CreateAuthToken(ctx, user, acct.PublicId, authtoken.WithPublicId(tokenRequestId), authtoken.WithStatus(authtoken.PendingStatus)) + require.NoError(err) + return tk +} + +// testControllerSrv is a test http server that supports the following +// endpoints: +// +// * /v1/auth-methods/:authenticate:callback +// +// * /authentication-complete +// +// * /authentication-error" +type testControllerSrv struct { + mu sync.RWMutex + + authMethodId string + oidcRepoFn OidcRepoFactory + iamRepoFn IamRepoFactory + atRepoFn AuthTokenRepoFactory + httpServer *httptest.Server + t *testing.T +} + +// startTestControllerSrv returns a running testControllerSrv +func startTestControllerSrv(t *testing.T, oidcRepoFn OidcRepoFactory, iamRepoFn IamRepoFactory, atRepoFn AuthTokenRepoFactory) *testControllerSrv { + require := require.New(t) + require.NotNil(t) + t.Helper() + // the repo functions are allowed to be nil + s := &testControllerSrv{ + t: t, + oidcRepoFn: oidcRepoFn, + iamRepoFn: iamRepoFn, + atRepoFn: atRepoFn, + } + s.httpServer = httptest.NewServer(s) + s.httpServer.Config.ErrorLog = log.New(ioutil.Discard, "", 0) + s.t.Cleanup(s.Stop) + return s +} + +// SetAuthMethodId will set the auth method id used for various +// operations +func (s *testControllerSrv) SetAuthMethodId(authMethodId string) { + s.t.Helper() + s.mu.Lock() + defer s.mu.Unlock() + s.authMethodId = authMethodId +} + +// AuthMethodId returns the auth method id currently being used +// for various operations +func (s *testControllerSrv) AuthMethodId() string { + s.t.Helper() + s.mu.RLock() + defer s.mu.RUnlock() + return s.authMethodId +} + +// Addr returns the scheme.host.port of the running srv. +func (s *testControllerSrv) Addr() string { + s.t.Helper() + return s.httpServer.URL +} + +// FinalRedirectUrl returns the final redirect url for the srv, +// including scheme.host.port and path +func (s *testControllerSrv) FinalRedirectUrl() string { + s.t.Helper() + return fmt.Sprintf(FinalRedirectEndpoint, s.Addr()) +} + +// CallbackUrl returns the final callback url for the srv, +// including scheme.host.port and path +func (s *testControllerSrv) CallbackUrl() string { + s.t.Helper() + require := require.New(s.t) + require.NotEmptyf(s.authMethodId, "auth method id was missing") + return fmt.Sprintf(CallbackEndpoint, s.Addr(), s.authMethodId) +} + +// ServeHTTP satisfies the http.Handler interface +func (s *testControllerSrv) ServeHTTP(w http.ResponseWriter, req *http.Request) { + s.t.Helper() + require := require.New(s.t) + switch req.URL.Path { + case fmt.Sprintf("/v1/auth-methods/%s:authenticate:callback", s.authMethodId): + err := req.ParseForm() + require.NoErrorf(err, "%s: internal error: %w", "callback", err) + state := req.FormValue("state") + code := req.FormValue("code") + error := req.FormValue("error") + + switch { + case error != "": + errorRedirectTo := fmt.Sprintf("%s/%s?error=%s", s.Addr(), AuthenticationErrorsEndpoint, error) + s.t.Log("auth error: ", errorRedirectTo) + http.RedirectHandler(errorRedirectTo, 301) + default: + redirectTo, err := Callback(context.Background(), + s.oidcRepoFn, + s.iamRepoFn, + s.atRepoFn, + s.authMethodId, + state, + code, + ) + if err != nil { + errorRedirectTo := fmt.Sprintf("%s/%s?error=%s", s.Addr(), AuthenticationErrorsEndpoint, url.QueryEscape(err.Error())) + s.t.Log("callback error: ", errorRedirectTo) + http.RedirectHandler(errorRedirectTo, 302) + return + } + s.t.Log("callback success: ", redirectTo) + http.Redirect(w, req, redirectTo, http.StatusFound) + // http.RedirectHandler(redirectTo, 302) + } + return + case "/authentication-complete": + s.t.Log("authentication-complete") + _, err := w.Write([]byte("Successful authentication. Congratulations!")) + require.NoErrorf(err, "%s: internal error: %w", req.URL.Path, err) + return + case "/authentication-error": + s.t.Log("authentication-error") + err := req.ParseForm() + require.NoErrorf(err, "%s: internal error: %w", "callback", err) + error := req.FormValue("error") + _, err = w.Write([]byte(fmt.Sprintf("authentication error: %s", error))) + require.NoErrorf(err, "%s: internal error: %w", req.URL.Path, err) + default: + s.t.Log("path not found: ", req.URL.Path) + w.WriteHeader(http.StatusNotFound) + return + } +} + +// Stop stops the running testControllerSrv. This is called as a test clean up function +// which is initialized when starting the srv +func (s *testControllerSrv) Stop() { + s.t.Helper() + s.httpServer.Close() +} diff --git a/internal/auth/password/authmethod.go b/internal/auth/password/authmethod.go index 5ddce1224..3a5b8dd8f 100644 --- a/internal/auth/password/authmethod.go +++ b/internal/auth/password/authmethod.go @@ -43,7 +43,8 @@ func NewAuthMethod(scopeId string, opt ...Option) (*AuthMethod, error) { return a, nil } -func (a *AuthMethod) clone() *AuthMethod { +// Clone simply clones the AuthMethod +func (a *AuthMethod) Clone() *AuthMethod { cp := proto.Clone(a.AuthMethod) return &AuthMethod{ AuthMethod: cp.(*store.AuthMethod), @@ -74,3 +75,19 @@ func (a *AuthMethod) oplog(op oplog.OpType) oplog.Metadata { } return metadata } + +// authMethodView provides a simple way to read an AuthMethod with its +// IsPrimaryAuthMethod field set. By definition, it's used only for reading +// AuthMethods. +type authMethodView struct { + *store.AuthMethod + tableName string +} + +// TableName returns the view name. +func (a *authMethodView) TableName() string { + if a.tableName != "" { + return a.tableName + } + return "auth_password_method_with_is_primary" +} diff --git a/internal/auth/password/options.go b/internal/auth/password/options.go index eefd90d1a..25da1c10d 100644 --- a/internal/auth/password/options.go +++ b/internal/auth/password/options.go @@ -14,14 +14,16 @@ type Option func(*options) // options = how options are represented type options struct { - withName string - withDescription string - withLoginName string - withLimit int - withConfig Configuration - withPublicId string - password string - withPassword bool + withName string + withDescription string + withLoginName string + withLimit int + withConfig Configuration + withPublicId string + password string + withPassword bool + withOrderByCreateTime bool + ascending bool } func getDefaultOptions() options { @@ -81,3 +83,12 @@ func WithConfiguration(config Configuration) Option { o.withConfig = config } } + +// WithOrderByCreateTime provides an option to specify ordering by the +// CreateTime field. +func WithOrderByCreateTime(ascending bool) Option { + return func(o *options) { + o.withOrderByCreateTime = true + o.ascending = ascending + } +} diff --git a/internal/auth/password/options_test.go b/internal/auth/password/options_test.go index 649eaf3c7..ba191cbd1 100644 --- a/internal/auth/password/options_test.go +++ b/internal/auth/password/options_test.go @@ -57,4 +57,12 @@ func Test_GetOpts(t *testing.T) { testOpts.withConfig = c assert.Equal(t, opts, testOpts) }) + t.Run("WithOrderByCreateTime", func(t *testing.T) { + assert := assert.New(t) + opts := getOpts(WithOrderByCreateTime(true)) + testOpts := getDefaultOptions() + testOpts.withOrderByCreateTime = true + testOpts.ascending = true + assert.Equal(opts, testOpts) + }) } diff --git a/internal/auth/password/repository_authmethod.go b/internal/auth/password/repository_authmethod.go index a5451cf88..07792bdb2 100644 --- a/internal/auth/password/repository_authmethod.go +++ b/internal/auth/password/repository_authmethod.go @@ -36,7 +36,7 @@ func (r *Repository) CreateAuthMethod(ctx context.Context, m *AuthMethod, opt .. if m.PublicId != "" { return nil, errors.New(errors.InvalidParameter, op, "public id not empty") } - m = m.clone() + m = m.Clone() opts := getOpts(opt...) @@ -81,7 +81,7 @@ func (r *Repository) CreateAuthMethod(ctx context.Context, m *AuthMethod, opt .. if err := w.Create(ctx, newArgon2Conf, db.WithOplog(oplogWrapper, c.oplog(oplog.OpType_OP_TYPE_CREATE))); err != nil { return errors.Wrap(err, op, errors.WithMsg("unable to create argon conf")) } - newAuthMethod = m.clone() + newAuthMethod = m.Clone() if err := w.Create(ctx, newAuthMethod, db.WithOplog(oplogWrapper, m.oplog(oplog.OpType_OP_TYPE_CREATE))); err != nil { return errors.Wrap(err, op, errors.WithMsg("unable to create auth method")) } @@ -105,31 +105,17 @@ func (r *Repository) LookupAuthMethod(ctx context.Context, publicId string, _ .. if publicId == "" { return nil, errors.New(errors.InvalidPublicId, op, "missing public id") } - a := allocAuthMethod() - a.PublicId = publicId - if err := r.reader.LookupByPublicId(ctx, &a); err != nil { - if errors.IsNotFoundError(err) { - return nil, nil - } - return nil, errors.Wrap(err, op, errors.WithMsg(fmt.Sprintf("failed for %s", publicId))) - } - return &a, nil + return r.lookupAuthMethod(ctx, publicId) } -// ListAuthMethods returns a slice of AuthMethods for the scopeId. WithLimit is the only option supported. +// ListAuthMethods returns a slice of AuthMethods for the scopeId. WithLimit and +// WithOrder options are the only option supported. func (r *Repository) ListAuthMethods(ctx context.Context, scopeIds []string, opt ...Option) ([]*AuthMethod, error) { const op = "password.(Repository).ListAuthMethods" if len(scopeIds) == 0 { return nil, errors.New(errors.InvalidParameter, op, "missing scope id") } - opts := getOpts(opt...) - limit := r.defaultLimit - if opts.withLimit != 0 { - // non-zero signals an override of the default limit for the repo. - limit = opts.withLimit - } - var authMethods []*AuthMethod - err := r.reader.SearchWhere(ctx, &authMethods, "scope_id in (?)", []interface{}{scopeIds}, db.WithLimit(limit)) + authMethods, err := r.getAuthMethods(ctx, "", scopeIds, opt...) if err != nil { return nil, errors.Wrap(err, op) } @@ -159,7 +145,7 @@ func (r *Repository) DeleteAuthMethod(ctx context.Context, scopeId, publicId str db.ExpBackoff{}, func(_ db.Reader, w db.Writer) (err error) { metadata := am.oplog(oplog.OpType_OP_TYPE_DELETE) - dAc := am.clone() + dAc := am.Clone() rowsDeleted, err = w.Delete(ctx, dAc, db.WithOplog(oplogWrapper, metadata)) if err != nil { return errors.Wrap(err, op) @@ -203,8 +189,8 @@ func (r *Repository) UpdateAuthMethod(ctx context.Context, authMethod *AuthMetho } for _, f := range fieldMaskPaths { switch { - case strings.EqualFold("name", f): - case strings.EqualFold("description", f): + case strings.EqualFold("Name", f): + case strings.EqualFold("Description", f): case strings.EqualFold("MinLoginNameLength", f): case strings.EqualFold("MinPasswordLength", f): default: @@ -232,13 +218,13 @@ func (r *Repository) UpdateAuthMethod(ctx context.Context, authMethod *AuthMetho errors.WithMsg("unable to get oplog wrapper")) } - upAuthMethod := authMethod.clone() + upAuthMethod := authMethod.Clone() var rowsUpdated int _, err = r.writer.DoTx( ctx, db.StdRetryCnt, db.ExpBackoff{}, - func(_ db.Reader, w db.Writer) error { + func(reader db.Reader, w db.Writer) error { dbOpts := []db.Option{ db.WithOplog(oplogWrapper, upAuthMethod.oplog(oplog.OpType_OP_TYPE_UPDATE)), db.WithVersion(&version), @@ -257,6 +243,21 @@ func (r *Repository) UpdateAuthMethod(ctx context.Context, authMethod *AuthMetho if rowsUpdated > 1 { return errors.New(errors.MultipleRecords, op, "more than 1 resource would have been updated") } + // we need a new repo, that's using the same reader/writer as this TxHandler + txRepo := &Repository{ + reader: reader, + writer: w, + kms: r.kms, + // intentionally not setting the defaultLimit, so we'll get all + // the account ids without a limit + } + upAuthMethod, err = txRepo.lookupAuthMethod(ctx, upAuthMethod.PublicId) + if err != nil { + return errors.Wrap(err, op, errors.WithMsg("unable to lookup auth method after update")) + } + if upAuthMethod == nil { + return errors.New(errors.RecordNotFound, op, "unable to lookup auth method after update") + } return nil }, ) @@ -268,3 +269,81 @@ func (r *Repository) UpdateAuthMethod(ctx context.Context, authMethod *AuthMetho } return upAuthMethod, rowsUpdated, nil } + +// lookupAuthMethod will lookup a single auth method +func (r *Repository) lookupAuthMethod(ctx context.Context, authMethodId string, opt ...Option) (*AuthMethod, error) { + const op = "oidc.(Repository).lookupAuthMethod" + var err error + ams, err := r.getAuthMethods(ctx, authMethodId, nil, opt...) + if err != nil { + return nil, errors.Wrap(err, op) + } + switch { + case len(ams) == 0: + return nil, nil // not an error to return no rows for a "lookup" + case len(ams) > 1: + return nil, errors.New(errors.NotSpecificIntegrity, op, fmt.Sprintf("%s matched more than 1 ", authMethodId)) + default: + return ams[0], nil + } +} + +// getAuthMethods allows the caller to either lookup a specific AuthMethod via +// its id or search for a set AuthMethods within a set of scopes. Passing both +// scopeIds and a authMethodId is an error. The WithLimit and +// WithOrderByCreateTime options are supported and all other options are +// ignored. +// +// The AuthMethod returned has its IsPrimaryAuthMethod bool set. +// +// When no record is found it returns nil, nil +func (r *Repository) getAuthMethods(ctx context.Context, authMethodId string, scopeIds []string, opt ...Option) ([]*AuthMethod, error) { + const op = "password.(Repository).getAuthMethods" + if authMethodId == "" && len(scopeIds) == 0 { + return nil, errors.New(errors.InvalidParameter, op, "missing search criteria: both auth method id and Scope IDs are empty") + } + if authMethodId != "" && len(scopeIds) > 0 { + return nil, errors.New(errors.InvalidParameter, op, "searching for both an auth method id and Scope IDs is not supported") + } + + dbArgs := []db.Option{} + opts := getOpts(opt...) + limit := r.defaultLimit + if opts.withLimit != 0 { + // non-zero signals an override of the default limit for the repo. + limit = opts.withLimit + } + dbArgs = append(dbArgs, db.WithLimit(limit)) + + if opts.withOrderByCreateTime { + if opts.ascending { + dbArgs = append(dbArgs, db.WithOrder("create_time asc")) + } else { + dbArgs = append(dbArgs, db.WithOrder("create_time")) + } + } + + var args []interface{} + var where []string + switch { + case authMethodId != "": + where, args = append(where, "public_id = ?"), append(args, authMethodId) + default: + where, args = append(where, "scope_id in(?)"), append(args, scopeIds) + } + + var views []*authMethodView + err := r.reader.SearchWhere(ctx, &views, strings.Join(where, " and "), args, dbArgs...) + if err != nil { + return nil, errors.Wrap(err, op) + } + + if len(views) == 0 { // we're done if nothing is found. + return nil, nil + } + authMethods := make([]*AuthMethod, 0, len(views)) + for _, am := range views { + authMethods = append(authMethods, &AuthMethod{AuthMethod: am.AuthMethod}) + } + return authMethods, nil +} diff --git a/internal/auth/password/repository_authmethod_test.go b/internal/auth/password/repository_authmethod_test.go index ceec82ae4..e43a8e9cb 100644 --- a/internal/auth/password/repository_authmethod_test.go +++ b/internal/auth/password/repository_authmethod_test.go @@ -209,7 +209,7 @@ func TestRepository_CreateAuthMethod_DupeNames(t *testing.T) { Name: "test-name-repo", }, } - in2 := in.clone() + in2 := in.Clone() in.ScopeId = org1.GetPublicId() got, err := repo.CreateAuthMethod(context.Background(), in) @@ -411,7 +411,7 @@ func TestRepository_ListAuthMethods(t *testing.T) { { name: "Scope with no auth methods", in: []string{noAuthMethodOrg.GetPublicId()}, - want: []*AuthMethod{}, + want: nil, }, { name: "With populated scope id", @@ -456,12 +456,25 @@ func TestRepository_ListAuthMethods_Multiple_Scopes(t *testing.T) { for i := 0; i < 10; i++ { o := iam.TestOrg(t, iamRepo) scopeIds = append(scopeIds, o.GetPublicId()) - TestAuthMethods(t, conn, o.GetPublicId(), numPerScope) + ams := TestAuthMethods(t, conn, o.GetPublicId(), numPerScope) + iam.TestSetPrimaryAuthMethod(t, iam.TestRepo(t, conn, wrapper), o, ams[0].PublicId) total += numPerScope } - got, err := repo.ListAuthMethods(context.Background(), scopeIds) + got, err := repo.ListAuthMethods(context.Background(), scopeIds, WithOrderByCreateTime(true)) require.NoError(t, err) assert.Equal(t, total, len(got)) + found := map[string]struct{}{} + for _, am := range got { + switch { + case am.Clone().ScopeId == "global": + assert.Equalf(t, false, am.IsPrimaryAuthMethod, "expected the the global auth method to not be primary") + default: + if _, ok := found[am.ScopeId]; !ok { + found[am.ScopeId] = struct{}{} + assert.Equalf(t, true, am.IsPrimaryAuthMethod, "expected the first auth method created for the scope %s to be primary", am.ScopeId) + } + } + } } func TestRepository_ListAuthMethods_Limits(t *testing.T) { @@ -473,6 +486,8 @@ func TestRepository_ListAuthMethods_Limits(t *testing.T) { o, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) authMethodCount := 10 ams := TestAuthMethods(t, conn, o.GetPublicId(), authMethodCount) + primaryAuthMethod := ams[0] + iam.TestSetPrimaryAuthMethod(t, iam.TestRepo(t, conn, wrapper), o, primaryAuthMethod.PublicId) tests := []struct { name string @@ -481,12 +496,14 @@ func TestRepository_ListAuthMethods_Limits(t *testing.T) { wantLen int }{ { - name: "With no limits", - wantLen: authMethodCount, + name: "With no limits", + wantLen: authMethodCount, + listOpts: []Option{WithOrderByCreateTime(true)}, }, { name: "With repo limit", repoOpts: []Option{WithLimit(3)}, + listOpts: []Option{WithOrderByCreateTime(true)}, wantLen: 3, }, { @@ -496,24 +513,24 @@ func TestRepository_ListAuthMethods_Limits(t *testing.T) { }, { name: "With List limit", - listOpts: []Option{WithLimit(3)}, + listOpts: []Option{WithLimit(3), WithOrderByCreateTime(true)}, wantLen: 3, }, { name: "With negative List limit", - listOpts: []Option{WithLimit(-1)}, + listOpts: []Option{WithLimit(-1), WithOrderByCreateTime(true)}, wantLen: authMethodCount, }, { name: "With repo smaller than list limit", repoOpts: []Option{WithLimit(2)}, - listOpts: []Option{WithLimit(6)}, + listOpts: []Option{WithLimit(6), WithOrderByCreateTime(true)}, wantLen: 6, }, { name: "With repo larger than list limit", repoOpts: []Option{WithLimit(6)}, - listOpts: []Option{WithLimit(2)}, + listOpts: []Option{WithLimit(2), WithOrderByCreateTime(true)}, wantLen: 2, }, } @@ -528,6 +545,9 @@ func TestRepository_ListAuthMethods_Limits(t *testing.T) { got, err := repo.ListAuthMethods(context.Background(), []string{ams[0].GetScopeId()}, tt.listOpts...) require.NoError(err) assert.Len(got, tt.wantLen) + if tt.wantLen > 0 { + assert.Equal(true, got[0].IsPrimaryAuthMethod) + } }) } } diff --git a/internal/auth/password/store/password.pb.go b/internal/auth/password/store/password.pb.go index 867802a39..fd523ec71 100644 --- a/internal/auth/password/store/password.pb.go +++ b/internal/auth/password/store/password.pb.go @@ -54,6 +54,10 @@ type AuthMethod struct { MinLoginNameLength uint32 `protobuf:"varint,9,opt,name=min_login_name_length,json=minLoginNameLength,proto3" json:"min_login_name_length,omitempty" gorm:"default:null"` // @inject_tag: `gorm:"default:null"` MinPasswordLength uint32 `protobuf:"varint,10,opt,name=min_password_length,json=minPasswordLength,proto3" json:"min_password_length,omitempty" gorm:"default:null"` + // is_primary_auth_method is a read-only output field which indicates if the + // auth method is set as the scope's primary auth method. + // @inject_tag: `gorm:"-"` + IsPrimaryAuthMethod bool `protobuf:"varint,20,opt,name=is_primary_auth_method,json=isPrimaryAuthMethod,proto3" json:"is_primary_auth_method,omitempty" gorm:"-"` } func (x *AuthMethod) Reset() { @@ -158,6 +162,13 @@ func (x *AuthMethod) GetMinPasswordLength() uint32 { return 0 } +func (x *AuthMethod) GetIsPrimaryAuthMethod() bool { + if x != nil { + return x.IsPrimaryAuthMethod + } + return false +} + type Account struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -363,7 +374,7 @@ var file_controller_storage_auth_password_store_v1_password_proto_rawDesc = []by 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x2a, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x76, 0x31, 0x2f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x22, 0xe2, 0x04, 0x0a, 0x0a, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, + 0x74, 0x6f, 0x22, 0x97, 0x05, 0x0a, 0x0a, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x49, 0x64, 0x12, 0x4b, 0x0a, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x02, 0x20, @@ -376,10 +387,10 @@ var file_controller_storage_auth_password_store_v1_password_proto_rawDesc = []by 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x24, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, - 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x42, 0x10, 0xc2, 0xdd, 0x29, 0x0c, 0x0a, 0x04, 0x6e, 0x61, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x42, 0x10, 0xc2, 0xdd, 0x29, 0x0c, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x40, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, - 0x01, 0x28, 0x09, 0x42, 0x1e, 0xc2, 0xdd, 0x29, 0x1a, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, + 0x01, 0x28, 0x09, 0x42, 0x1e, 0xc2, 0xdd, 0x29, 0x1a, 0x0a, 0x0b, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x19, 0x0a, 0x08, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x06, 0x20, 0x01, @@ -401,50 +412,53 @@ var file_controller_storage_auth_password_store_v1_password_proto_rawDesc = []by 0x6e, 0x67, 0x74, 0x68, 0x12, 0x1e, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x6d, 0x69, 0x6e, 0x5f, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x5f, 0x6c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x52, 0x11, 0x6d, 0x69, 0x6e, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, - 0x64, 0x4c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x22, 0xaf, 0x03, 0x0a, 0x07, 0x41, 0x63, 0x63, 0x6f, - 0x75, 0x6e, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x64, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x49, 0x64, - 0x12, 0x4b, 0x0a, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, - 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, - 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, - 0x70, 0x52, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x4b, 0x0a, - 0x0b, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, - 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, - 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, - 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x24, 0x0a, 0x04, 0x6e, 0x61, - 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x42, 0x10, 0xc2, 0xdd, 0x29, 0x0c, 0x0a, 0x04, - 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, - 0x12, 0x40, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, - 0x05, 0x20, 0x01, 0x28, 0x09, 0x42, 0x1e, 0xc2, 0xdd, 0x29, 0x1a, 0x0a, 0x0b, 0x64, 0x65, 0x73, - 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, - 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, - 0x6f, 0x6e, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x06, 0x20, - 0x01, 0x28, 0x0d, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x24, 0x0a, 0x0e, - 0x61, 0x75, 0x74, 0x68, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x07, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x61, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, - 0x49, 0x64, 0x12, 0x45, 0x0a, 0x0a, 0x6c, 0x6f, 0x67, 0x69, 0x6e, 0x5f, 0x6e, 0x61, 0x6d, 0x65, - 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x42, 0x26, 0xc2, 0xdd, 0x29, 0x22, 0x0a, 0x09, 0x4c, 0x6f, - 0x67, 0x69, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x15, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, - 0x74, 0x65, 0x73, 0x2e, 0x6c, 0x6f, 0x67, 0x69, 0x6e, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x52, 0x09, - 0x6c, 0x6f, 0x67, 0x69, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0x22, 0xb3, 0x01, 0x0a, 0x0a, 0x43, 0x72, - 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x72, 0x69, 0x76, - 0x61, 0x74, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x70, 0x72, - 0x69, 0x76, 0x61, 0x74, 0x65, 0x49, 0x64, 0x12, 0x2e, 0x0a, 0x13, 0x70, 0x61, 0x73, 0x73, 0x77, - 0x6f, 0x72, 0x64, 0x5f, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x41, 0x63, - 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x28, 0x0a, 0x10, 0x70, 0x61, 0x73, 0x73, 0x77, - 0x6f, 0x72, 0x64, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x0e, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x43, 0x6f, 0x6e, 0x66, 0x49, - 0x64, 0x12, 0x2c, 0x0a, 0x12, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x5f, 0x6d, 0x65, - 0x74, 0x68, 0x6f, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x10, 0x70, - 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x49, 0x64, 0x42, - 0x42, 0x5a, 0x40, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, - 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, - 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x2f, 0x70, - 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x3b, 0x73, 0x74, - 0x6f, 0x72, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x64, 0x4c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x12, 0x33, 0x0a, 0x16, 0x69, 0x73, 0x5f, 0x70, 0x72, + 0x69, 0x6d, 0x61, 0x72, 0x79, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, + 0x64, 0x18, 0x14, 0x20, 0x01, 0x28, 0x08, 0x52, 0x13, 0x69, 0x73, 0x50, 0x72, 0x69, 0x6d, 0x61, + 0x72, 0x79, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x22, 0xaf, 0x03, 0x0a, + 0x07, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x75, 0x62, 0x6c, + 0x69, 0x63, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x75, 0x62, + 0x6c, 0x69, 0x63, 0x49, 0x64, 0x12, 0x4b, 0x0a, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, + 0x74, 0x69, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, + 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, + 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, + 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x69, + 0x6d, 0x65, 0x12, 0x4b, 0x0a, 0x0b, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, + 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, + 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x74, 0x69, 0x6d, + 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x76, 0x31, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, + 0x61, 0x6d, 0x70, 0x52, 0x0a, 0x75, 0x70, 0x64, 0x61, 0x74, 0x65, 0x54, 0x69, 0x6d, 0x65, 0x12, + 0x24, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x42, 0x10, 0xc2, + 0xdd, 0x29, 0x0c, 0x0a, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x52, + 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x40, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, + 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x42, 0x1e, 0xc2, 0xdd, 0x29, 0x1a, + 0x0a, 0x0b, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0b, 0x64, + 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, + 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, + 0x6f, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, + 0x6e, 0x12, 0x24, 0x0a, 0x0e, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, + 0x5f, 0x69, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x61, 0x75, 0x74, 0x68, 0x4d, + 0x65, 0x74, 0x68, 0x6f, 0x64, 0x49, 0x64, 0x12, 0x45, 0x0a, 0x0a, 0x6c, 0x6f, 0x67, 0x69, 0x6e, + 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x42, 0x26, 0xc2, 0xdd, 0x29, + 0x22, 0x0a, 0x09, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x15, 0x61, 0x74, + 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x6c, 0x6f, 0x67, 0x69, 0x6e, 0x5f, 0x6e, + 0x61, 0x6d, 0x65, 0x52, 0x09, 0x6c, 0x6f, 0x67, 0x69, 0x6e, 0x4e, 0x61, 0x6d, 0x65, 0x22, 0xb3, + 0x01, 0x0a, 0x0a, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x12, 0x1d, 0x0a, + 0x0a, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x09, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x49, 0x64, 0x12, 0x2e, 0x0a, 0x13, + 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x5f, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, + 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x70, 0x61, 0x73, 0x73, 0x77, + 0x6f, 0x72, 0x64, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x28, 0x0a, 0x10, + 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x5f, 0x69, 0x64, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, + 0x43, 0x6f, 0x6e, 0x66, 0x49, 0x64, 0x12, 0x2c, 0x0a, 0x12, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, + 0x72, 0x64, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x10, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x4d, 0x65, 0x74, 0x68, + 0x6f, 0x64, 0x49, 0x64, 0x42, 0x42, 0x5a, 0x40, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, + 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x62, 0x6f, 0x75, + 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x61, + 0x75, 0x74, 0x68, 0x2f, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x2f, 0x73, 0x74, 0x6f, + 0x72, 0x65, 0x3b, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/internal/auth/subtype.go b/internal/auth/subtype.go index 33ef624b1..39b9d2d89 100644 --- a/internal/auth/subtype.go +++ b/internal/auth/subtype.go @@ -3,7 +3,9 @@ package auth import ( "strings" + "github.com/hashicorp/boundary/internal/auth/oidc" "github.com/hashicorp/boundary/internal/auth/password" + "github.com/hashicorp/boundary/internal/db/timestamp" ) type SubType int @@ -11,22 +13,59 @@ type SubType int const ( UnknownSubtype SubType = iota PasswordSubtype + OidcSubtype ) func (t SubType) String() string { switch t { case PasswordSubtype: return "password" + case OidcSubtype: + return "oidc" } return "unknown" } +// AuthMethod contains the common methods across all the different types of auth methods. +type AuthMethod interface { + GetPublicId() string + GetCreateTime() *timestamp.Timestamp + GetUpdateTime() *timestamp.Timestamp + GetName() string + GetDescription() string + GetScopeId() string + GetVersion() uint32 + GetIsPrimaryAuthMethod() bool +} + +var ( + _ AuthMethod = (*oidc.AuthMethod)(nil) + _ AuthMethod = (*password.AuthMethod)(nil) +) + +type Account interface { + GetPublicId() string + GetCreateTime() *timestamp.Timestamp + GetUpdateTime() *timestamp.Timestamp + GetName() string + GetDescription() string + GetAuthMethodId() string + GetVersion() uint32 +} + +var ( + _ Account = (*oidc.Account)(nil) + _ Account = (*password.Account)(nil) +) + // SubtypeFromType converts a string to a SubType. // returns UnknownSubtype if no SubType with that name is found. func SubtypeFromType(t string) SubType { switch { case strings.EqualFold(strings.TrimSpace(t), PasswordSubtype.String()): return PasswordSubtype + case strings.EqualFold(strings.TrimSpace(t), OidcSubtype.String()): + return OidcSubtype } return UnknownSubtype } @@ -39,6 +78,9 @@ func SubtypeFromId(id string) SubType { case strings.HasPrefix(strings.TrimSpace(id), password.AuthMethodPrefix), strings.HasPrefix(strings.TrimSpace(id), password.AccountPrefix): return PasswordSubtype + case strings.HasPrefix(strings.TrimSpace(id), oidc.AuthMethodPrefix), + strings.HasPrefix(strings.TrimSpace(id), oidc.AccountPrefix): + return OidcSubtype } return UnknownSubtype } diff --git a/internal/auth/subtype_test.go b/internal/auth/subtype_test.go new file mode 100644 index 000000000..557ed5978 --- /dev/null +++ b/internal/auth/subtype_test.go @@ -0,0 +1,67 @@ +package auth + +import ( + "testing" + + "github.com/hashicorp/boundary/internal/auth/oidc" + "github.com/hashicorp/boundary/internal/auth/password" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestSubType(t *testing.T) { + tests := []struct { + subType SubType + id string + wantString string + wantSubtype SubType + }{ + { + subType: UnknownSubtype, + id: "unknownPrefix" + "1234567890", + wantString: "unknown", + wantSubtype: UnknownSubtype, + }, + { + subType: PasswordSubtype, + id: password.AuthMethodPrefix + "1234567890", + wantString: "password", + wantSubtype: PasswordSubtype, + }, + { + subType: PasswordSubtype, + id: password.AccountPrefix + "1234567890", + wantString: "password", + wantSubtype: PasswordSubtype, + }, + { + subType: OidcSubtype, + id: oidc.AuthMethodPrefix + "1234567890", + wantString: "oidc", + wantSubtype: OidcSubtype, + }, + { + subType: OidcSubtype, + id: oidc.AccountPrefix + "1234567890", + wantString: "oidc", + wantSubtype: OidcSubtype, + }, + { + subType: SubType(1000), + id: "unknownPrefix" + "1234567890", + wantString: "unknown", + wantSubtype: UnknownSubtype, + }, + } + for _, tt := range tests { + t.Run(tt.wantString, func(t *testing.T) { + assert, _ := assert.New(t), require.New(t) + s := tt.subType.String() + assert.Equal(tt.wantString, s) + assert.Equal(tt.wantSubtype, SubtypeFromType(s)) + + typ := SubtypeFromId(tt.id) + assert.Equal(tt.wantSubtype, typ) + }) + } +} diff --git a/internal/authtoken/authtoken.go b/internal/authtoken/authtoken.go index 975eed54e..dd7013f06 100644 --- a/internal/authtoken/authtoken.go +++ b/internal/authtoken/authtoken.go @@ -97,7 +97,8 @@ const ( tokenLength = 24 ) -func newAuthTokenId() (string, error) { +// NewAuthTokenId creates a new id for an auth token. +func NewAuthTokenId() (string, error) { const op = "authtoken.newAuthTokenId" id, err := db.NewPublicId(AuthTokenPrefix) if err != nil { @@ -106,18 +107,20 @@ func newAuthTokenId() (string, error) { return id, nil } -// newAuthToken generates a new in-memory token. No options are currently -// supported. -func newAuthToken(_ ...Option) (*AuthToken, error) { +// newAuthToken generates a new in-memory token. The WithStatus option is +// support and all other options are ignored. +func newAuthToken(opt ...Option) (*AuthToken, error) { const op = "authtoken.newAuthToken" token, err := base62.Random(tokenLength) if err != nil { return nil, errors.Wrap(err, op, errors.WithCode(errors.Io)) } + opts := getOpts(opt...) return &AuthToken{ AuthToken: &store.AuthToken{ - Token: fmt.Sprintf("%s%s", TokenValueVersionPrefix, token), + Token: fmt.Sprintf("%s%s", TokenValueVersionPrefix, token), + Status: string(opts.withStatus), }, }, nil } diff --git a/internal/authtoken/authtoken_test.go b/internal/authtoken/authtoken_test.go index c4977b119..ab5c1ae5a 100644 --- a/internal/authtoken/authtoken_test.go +++ b/internal/authtoken/authtoken_test.go @@ -31,7 +31,7 @@ func TestAuthToken_DbUpdate(t *testing.T) { am := password.TestAuthMethods(t, conn, org.GetPublicId(), 1)[0] acct := password.TestAccounts(t, conn, am.GetPublicId(), 1)[0] - newAuthTokId, err := newAuthTokenId() + newAuthTokId, err := NewAuthTokenId() require.NoError(t, err) type args struct { @@ -89,6 +89,14 @@ func TestAuthToken_DbUpdate(t *testing.T) { }, cnt: 1, }, + { + name: "update-status", + args: args{ + fieldMask: []string{"Status"}, + authTok: &store.AuthToken{Status: string(IssuedStatus)}, + }, + cnt: 1, + }, } for _, tt := range tests { @@ -125,7 +133,7 @@ func TestAuthToken_DbCreate(t *testing.T) { createdAuthToken := TestAuthToken(t, conn, kms, org.GetPublicId()) testAuthTokenId := func() string { - id, err := newAuthTokenId() + id, err := NewAuthTokenId() require.NoError(t, err) return id } @@ -147,7 +155,7 @@ func TestAuthToken_DbCreate(t *testing.T) { name: "duplicate-id", in: &store.AuthToken{ PublicId: createdAuthToken.GetPublicId(), - Token: "duplicateid_test", + Token: "duplicate_id_test", AuthAccountId: acct.GetPublicId(), }, wantError: true, @@ -174,7 +182,7 @@ func TestAuthToken_DbCreate(t *testing.T) { func TestAuthToken_DbDelete(t *testing.T) { conn, _ := db.TestSetup(t, "postgres") testAuthTokenId := func() string { - id, err := newAuthTokenId() + id, err := NewAuthTokenId() require.NoError(t, err) return id } diff --git a/internal/authtoken/options.go b/internal/authtoken/options.go index 7683be33e..759e9e6d8 100644 --- a/internal/authtoken/options.go +++ b/internal/authtoken/options.go @@ -29,6 +29,8 @@ type options struct { withTokenTimeToLiveDuration time.Duration withTokenTimeToStaleDuration time.Duration withLimit int + withStatus Status + withPublicId string } func getDefaultOptions() options { @@ -75,3 +77,17 @@ func WithLimit(limit int) Option { } } } + +// WithStatus allows setting of the auth token's Status. +func WithStatus(status Status) Option { + return func(o *options) { + o.withStatus = status + } +} + +// WithPublicId allows the setting of the auth token's public id +func WithPublicId(id string) Option { + return func(o *options) { + o.withPublicId = id + } +} diff --git a/internal/authtoken/options_test.go b/internal/authtoken/options_test.go index f628d7aac..c55e7dc01 100644 --- a/internal/authtoken/options_test.go +++ b/internal/authtoken/options_test.go @@ -34,4 +34,20 @@ func Test_GetOpts(t *testing.T) { testOpts.withTokenTimeToStaleDuration = 1 * time.Hour assert.Equal(opts, testOpts) }) + + t.Run("withStatus", func(t *testing.T) { + assert := assert.New(t) + opts := getOpts(WithStatus(IssuedStatus)) + testOpts := getDefaultOptions() + testOpts.withStatus = IssuedStatus + assert.Equal(opts, testOpts) + }) + + t.Run("WithPublicId", func(t *testing.T) { + assert := assert.New(t) + opts := getOpts(WithPublicId("test-id")) + testOpts := getDefaultOptions() + testOpts.withPublicId = "test-id" + assert.Equal(opts, testOpts) + }) } diff --git a/internal/authtoken/repository.go b/internal/authtoken/repository.go index 66753481e..776e101a0 100644 --- a/internal/authtoken/repository.go +++ b/internal/authtoken/repository.go @@ -54,12 +54,15 @@ func NewRepository(r db.Reader, w db.Writer, kms *kms.Kms, opt ...Option) (*Repo }, nil } -// CreateAuthToken inserts an Auth Token into the repository and returns a new Auth Token. The returned auth token -// contains the auth token value. The provided IAM User ID must be associated to the provided auth account id -// or an error will be returned. All options are ignored. +// CreateAuthToken inserts an Auth Token into the repository and returns a new +// Auth Token. The returned auth token contains the auth token value. The +// provided IAM User ID must be associated to the provided auth account id or an +// error will be returned. The Auth Token will have a Status of "issued". +// The WithStatus and WithPublicId options are supported and all other options +// are ignored. func (r *Repository) CreateAuthToken(ctx context.Context, withIamUser *iam.User, withAuthAccountId string, opt ...Option) (*AuthToken, error) { const op = "authtoken.(Repository).CreateAuthToken" - if withIamUser == nil { + if withIamUser == nil || withIamUser.User == nil { return nil, errors.New(errors.InvalidParameter, op, "missing user") } if withIamUser.GetPublicId() == "" { @@ -68,17 +71,27 @@ func (r *Repository) CreateAuthToken(ctx context.Context, withIamUser *iam.User, if withAuthAccountId == "" { return nil, errors.New(errors.InvalidParameter, op, "missing auth account id") } - at, err := newAuthToken() if err != nil { return nil, errors.Wrap(err, op) } at.AuthAccountId = withAuthAccountId - id, err := newAuthTokenId() - if err != nil { - return nil, errors.Wrap(err, op) + opts := getOpts(opt...) + if opts.withPublicId == "" { + id, err := NewAuthTokenId() + if err != nil { + return nil, errors.Wrap(err, op) + } + opts.withPublicId = id + } + at.PublicId = opts.withPublicId + + switch { + case opts.withStatus != "": + at.Status = string(PendingStatus) + default: + at.Status = string(IssuedStatus) } - at.PublicId = id databaseWrapper, err := r.kms.GetWrapper(ctx, withIamUser.GetScopeId(), kms.KeyPurposeDatabase) if err != nil { @@ -343,3 +356,90 @@ func (r *Repository) DeleteAuthToken(ctx context.Context, id string, opt ...Opti return rowsDeleted, nil } + +// IssueAuthToken will retrieve the "pending" token and update it's status to +// "issued". If the token has already been issued, an error is returned with a +// nil token. If no token is found for the tokenRequestId an error is returned +// with a nil token. +// +// Note: no oplog entries are created for auth token operations (this is intentional). +func (r *Repository) IssueAuthToken(ctx context.Context, tokenRequestId string) (*AuthToken, error) { + const op = "authtoken.(Repository).IssueAuthToken" + if tokenRequestId == "" { + return nil, errors.New(errors.InvalidParameter, op, "missing token request id") + } + + var at *AuthToken + _, err := r.writer.DoTx( + ctx, + db.StdRetryCnt, + db.ExpBackoff{}, + func(reader db.Reader, w db.Writer) error { + at = allocAuthToken() + at.PublicId = tokenRequestId + at.Status = string(IssuedStatus) + // note: no oplog operations are created for auth token operations (this is intentional). + // Setting the ApproximateLastAccessTime to null through using the null mask allows a defined db's + // trigger to set ApproximateLastAccessTime to the commit timestamp. + rowsUpdated, err := w.Update(ctx, at, []string{"Status"}, []string{"ApproximateLastAccessTime"}, db.WithWhere("status = ?", PendingStatus)) + if err != nil { + return errors.Wrap(err, op) + } + if rowsUpdated == 0 { + return errors.New(errors.RecordNotFound, op, "pending auth token not found") + } + if rowsUpdated > 1 { + return errors.New(errors.Internal, op, fmt.Sprintf("should have updated 1 row and we attempted to update %d rows", rowsUpdated)) + } + if at.Status != string(IssuedStatus) { + return errors.New(errors.Internal, op, "updated auth token status != issued") + } + + // we need a new repo, that's using the same reader/writer as this TxHandler + txRepo := Repository{ + reader: reader, + writer: w, + kms: r.kms, + } + at, err = txRepo.LookupAuthToken(ctx, at.PublicId, withTokenValue()) + if err != nil { + return errors.Wrap(err, op) + } + if at == nil { + return errors.New(errors.RecordNotFound, op, "issued auth token not found") + } + return nil + }) + if err != nil { + return nil, err // error already wrapped when raised from r.DoTx(...) + } + return at, nil +} + +// CloseExpiredPendingTokens will close expired pending tokens in the repo. +// This function should called on a periodic basis a Controllers via it's +// "ticker" pattern. +func (r *Repository) CloseExpiredPendingTokens(ctx context.Context) (int, error) { + const op = "authtoken.(Repository).CloseExpiredPendingTokens" + + args := []interface{}{string(FailedStatus), string(PendingStatus)} + const sql = `update auth_token set status = ? where status = ? and now() > expiration_time` + var tokensClosed int + _, err := r.writer.DoTx( + ctx, + db.StdRetryCnt, + db.ExpBackoff{}, + func(_ db.Reader, w db.Writer) error { + var err error + tokensClosed, err = w.Exec(ctx, sql, args) + if err != nil { + return errors.Wrap(err, op) + } + return nil + }, + ) + if err != nil { + return db.NoRowsAffected, err // error already wrapped when raised from r.DoTx(...) + } + return tokensClosed, nil +} diff --git a/internal/authtoken/repository_test.go b/internal/authtoken/repository_test.go index 08a1895dd..7afd53d10 100644 --- a/internal/authtoken/repository_test.go +++ b/internal/authtoken/repository_test.go @@ -9,6 +9,7 @@ import ( "github.com/golang/protobuf/ptypes" "github.com/google/go-cmp/cmp" "github.com/hashicorp/boundary/internal/auth/password" + "github.com/hashicorp/boundary/internal/db/timestamp" "github.com/hashicorp/boundary/internal/kms" "github.com/hashicorp/boundary/internal/oplog" "github.com/stretchr/testify/assert" @@ -188,19 +189,20 @@ func TestRepository_CreateAuthToken(t *testing.T) { org1, _ := iam.TestScopes(t, repo) am := password.TestAuthMethods(t, conn, org1.GetPublicId(), 1)[0] aAcct := password.TestAccounts(t, conn, am.GetPublicId(), 1)[0] - - iamRepo, err := iam.NewRepository(rw, rw, kms) - require.NoError(t, err) - u1, err := iamRepo.LookupUserWithLogin(context.Background(), aAcct.GetPublicId(), iam.WithAutoVivify(true)) - require.NoError(t, err) + iam.TestSetPrimaryAuthMethod(t, repo, org1, am.PublicId) + u1 := iam.TestUser(t, repo, org1.PublicId, iam.WithAccountIds(aAcct.PublicId)) org2, _ := iam.TestScopes(t, repo) u2 := iam.TestUser(t, repo, org2.GetPublicId()) + testId, err := NewAuthTokenId() + require.NoError(t, err) + tests := []struct { name string iamUser *iam.User authAcctId string + opt []Option want *AuthToken wantErr bool }{ @@ -214,6 +216,19 @@ func TestRepository_CreateAuthToken(t *testing.T) { }, }, }, + { + name: "WithPublicId-WithStatus", + iamUser: u1, + authAcctId: aAcct.GetPublicId(), + opt: []Option{WithPublicId(testId), WithStatus(PendingStatus)}, + want: &AuthToken{ + AuthToken: &store.AuthToken{ + AuthAccountId: aAcct.GetPublicId(), + PublicId: testId, + Status: string(PendingStatus), + }, + }, + }, { name: "unconnected-authaccount-user", iamUser: u2, @@ -250,7 +265,7 @@ func TestRepository_CreateAuthToken(t *testing.T) { repo, err := NewRepository(rw, rw, kms) require.NoError(err) require.NotNil(repo) - got, err := repo.CreateAuthToken(context.Background(), tt.iamUser, tt.authAcctId) + got, err := repo.CreateAuthToken(context.Background(), tt.iamUser, tt.authAcctId, tt.opt...) if tt.wantErr { assert.Error(err) assert.Nil(got) @@ -262,6 +277,15 @@ func TestRepository_CreateAuthToken(t *testing.T) { assert.Equal(tt.authAcctId, got.GetAuthAccountId()) assert.Equal(got.CreateTime, got.UpdateTime) assert.Equal(got.CreateTime, got.ApproximateLastAccessTime) + + opts := getOpts(tt.opt...) + if opts.withPublicId != "" { + assert.Equal(opts.withPublicId, got.PublicId) + } + if opts.withStatus != "" { + assert.Equal(string(opts.withStatus), got.Status) + } + // We should find no oplog since tokens are not replicated, so they don't need oplog entries. assert.Error(db.TestVerifyOplog(t, rw, got.GetPublicId(), db.WithOperation(oplog.OpType_OP_TYPE_CREATE))) }) @@ -280,7 +304,7 @@ func TestRepository_LookupAuthToken(t *testing.T) { at.CtToken = nil at.KeyId = "" - badId, err := newAuthTokenId() + badId, err := NewAuthTokenId() require.NoError(t, err) require.NotNil(t, badId) @@ -370,7 +394,7 @@ func TestRepository_ValidateToken(t *testing.T) { require.NoError(t, err) require.NotNil(t, atTime) - badId, err := newAuthTokenId() + badId, err := NewAuthTokenId() require.NoError(t, err) require.NotNil(t, badId) @@ -550,7 +574,7 @@ func TestRepository_DeleteAuthToken(t *testing.T) { repo := iam.TestRepo(t, conn, wrapper) org, _ := iam.TestScopes(t, repo) at := TestAuthToken(t, conn, kms, org.GetPublicId()) - badId, err := newAuthTokenId() + badId, err := NewAuthTokenId() require.NoError(t, err) require.NotNil(t, badId) @@ -685,3 +709,162 @@ func TestRepository_ListAuthTokens_Multiple_Scopes(t *testing.T) { require.NoError(t, err) assert.Equal(t, total, len(got)) } + +func Test_IssuePendingToken(t *testing.T) { + t.Parallel() + ctx := context.Background() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + rootWrapper := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, rootWrapper) + repo, err := NewRepository(rw, rw, kmsCache) + require.NoError(t, err) + + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, rootWrapper)) + + tests := []struct { + name string + tokenRequestId string + wantErrMatch *errors.Template + wantErrContains string + }{ + { + name: "missing-id", + wantErrMatch: errors.T(errors.InvalidParameter), + wantErrContains: "missing token request id", + }, + { + name: "not-found", + tokenRequestId: func() string { + tokenPublicId, err := NewAuthTokenId() + require.NoError(t, err) + tk := TestAuthToken(t, conn, kmsCache, org.PublicId, WithPublicId(tokenPublicId)) + return tk.PublicId + }(), + wantErrMatch: errors.T(errors.RecordNotFound), + wantErrContains: "pending auth token", + }, + { + name: "success", + tokenRequestId: func() string { + tokenPublicId, err := NewAuthTokenId() + require.NoError(t, err) + tk := TestAuthToken(t, conn, kmsCache, org.PublicId, WithStatus(PendingStatus), WithPublicId(tokenPublicId)) + return tk.PublicId + }(), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + tk, err := repo.IssueAuthToken(ctx, tt.tokenRequestId) + if tt.wantErrMatch != nil { + require.Error(err) + assert.Truef(errors.Match(tt.wantErrMatch, err), "wanted %s and got: %+v", tt.wantErrMatch.Code, err) + if tt.wantErrContains != "" { + assert.Contains(err.Error(), tt.wantErrContains) + } + return + } + require.NoError(err) + assert.NotEmpty(tk) + accessTime, err := ptypes.Timestamp(tk.GetApproximateLastAccessTime().GetTimestamp()) + require.NoError(err) + createTime, err := ptypes.Timestamp(tk.GetCreateTime().GetTimestamp()) + require.NoError(err) + assert.True(accessTime.After(createTime), "last access time %q was not after the creation time %q", accessTime, createTime) + }) + } +} + +func Test_CloseExpiredPendingTokens(t *testing.T) { + t.Parallel() + ctx := context.Background() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + rootWrapper := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, rootWrapper) + repo, err := NewRepository(rw, rw, kmsCache) + require.NoError(t, err) + + org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, rootWrapper)) + databaseWrapper, err := kmsCache.GetWrapper(ctx, org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + + createWith := func(cnt int, expIn time.Duration, status Status) { + authMethods := password.TestAuthMethods(t, conn, org.PublicId, 1) + authMethodId := authMethods[0].PublicId + + accts := password.TestAccounts(t, conn, authMethodId, cnt) + for i := 0; i < cnt; i++ { + at := allocAuthToken() + id, err := NewAuthTokenId() + require.NoError(t, err) + at.PublicId = id + exp, err := ptypes.TimestampProto(time.Now().Add(expIn).Truncate(time.Second)) + require.NoError(t, err) + at.ExpirationTime = ×tamp.Timestamp{Timestamp: exp} + at.Status = string(status) + at.AuthAccountId = accts[i].PublicId + at.KeyId = databaseWrapper.KeyID() + at.CtToken = []byte(id) + err = rw.Create(ctx, at) + require.NoError(t, err) + } + } + tests := []struct { + name string + setup func() + wantCnt int + wantErrMatch *errors.Template + wantErrContains string + }{ + { + name: "nada-todo", + }, + { + name: "close-2", + setup: func() { + createWith(2, -10*time.Second, PendingStatus) + createWith(1, 10*time.Second, IssuedStatus) + createWith(1, 10*time.Second, PendingStatus) + }, + wantCnt: 2, + }, + { + name: "close-zero", + setup: func() { + createWith(2, 10*time.Second, PendingStatus) + createWith(1, 10*time.Second, IssuedStatus) + }, + wantCnt: 0, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + + // start with no tokens in the db. + _, err := rw.Exec(ctx, "delete from auth_token", nil) + require.NoError(err) + _, err = rw.Exec(ctx, "delete from auth_account", nil) + require.NoError(err) + + if tt.setup != nil { + tt.setup() + } + tokensClosed, err := repo.CloseExpiredPendingTokens(ctx) + if tt.wantErrMatch != nil { + require.Error(err) + assert.Equal(0, tokensClosed) + assert.Truef(errors.Match(tt.wantErrMatch, err), "wanted %s and got: %s", tt.wantErrMatch.Code, err.Error()) + if tt.wantErrContains != "" { + assert.Contains(err.Error(), tt.wantErrContains) + } + return + } + require.NoError(err) + assert.Equal(tt.wantCnt, tokensClosed) + }) + } +} diff --git a/internal/authtoken/status.go b/internal/authtoken/status.go new file mode 100644 index 000000000..9571c038b --- /dev/null +++ b/internal/authtoken/status.go @@ -0,0 +1,25 @@ +package authtoken + +// Status of the AuthToken. It will default IssuedStatus in the database. +type Status string + +const ( + // UnknownStatus for the token. + UnknownStatus Status = "unknown" + + // PendingStatus means that the token has been created but it pending while + // waiting to be issued. + PendingStatus Status = "auth token pending" + + // IssuedStatus means the token has been issued. It is a final status for the + // token. + IssuedStatus Status = "token issued" + + // FailedStatus means the token is in a failed status before it was issued and + // this is a final status. + FailedStatus Status = "authentication failed" + + // SystemErrorStatus means that the system encountered an error before + // issuing the token. This is a final status. + SystemErrorStatus Status = "system error" +) diff --git a/internal/authtoken/store/authtoken.pb.go b/internal/authtoken/store/authtoken.pb.go index 4038738c1..129df2c36 100644 --- a/internal/authtoken/store/authtoken.pb.go +++ b/internal/authtoken/store/authtoken.pb.go @@ -68,6 +68,10 @@ type AuthToken struct { // which is useful for caching purposes. // @inject_tag: `gorm:"not_null"` KeyId string `protobuf:"bytes,14,opt,name=key_id,json=keyId,proto3" json:"key_id,omitempty" gorm:"not_null"` + // status is the auth token's status. It will default of "token issued" in the + // database. + // @inject_tag: `gorm:"default:null"` + Status string `protobuf:"bytes,15,opt,name=status,proto3" json:"status,omitempty" gorm:"default:null"` } func (x *AuthToken) Reset() { @@ -186,6 +190,13 @@ func (x *AuthToken) GetKeyId() string { return "" } +func (x *AuthToken) GetStatus() string { + if x != nil { + return x.Status + } + return "" +} + var File_controller_storage_authtoken_store_v1_authtoken_proto protoreflect.FileDescriptor var file_controller_storage_authtoken_store_v1_authtoken_proto_rawDesc = []byte{ @@ -198,7 +209,7 @@ var file_controller_storage_authtoken_store_v1_authtoken_proto_rawDesc = []byte{ 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2f, 0x76, 0x31, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, - 0xd5, 0x04, 0x0a, 0x09, 0x41, 0x75, 0x74, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x1b, 0x0a, + 0xed, 0x04, 0x0a, 0x09, 0x41, 0x75, 0x74, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x49, 0x64, 0x12, 0x4b, 0x0a, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, @@ -235,11 +246,13 @@ var file_controller_storage_authtoken_store_v1_authtoken_proto_rawDesc = []byte{ 0x12, 0x1e, 0x0a, 0x0b, 0x69, 0x61, 0x6d, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x69, 0x61, 0x6d, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x15, 0x0a, 0x06, 0x6b, 0x65, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x05, 0x6b, 0x65, 0x79, 0x49, 0x64, 0x42, 0x3e, 0x5a, 0x3c, 0x67, 0x69, 0x74, 0x68, 0x75, - 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, - 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, - 0x6c, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x2f, 0x73, 0x74, 0x6f, 0x72, - 0x65, 0x3b, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x52, 0x05, 0x6b, 0x65, 0x79, 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, + 0x73, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x42, + 0x3e, 0x5a, 0x3c, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, + 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, + 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x74, 0x6f, + 0x6b, 0x65, 0x6e, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x3b, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x62, + 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/internal/authtoken/testing.go b/internal/authtoken/testing.go index a9faa24c6..f7df12caa 100644 --- a/internal/authtoken/testing.go +++ b/internal/authtoken/testing.go @@ -12,7 +12,7 @@ import ( "github.com/stretchr/testify/require" ) -func TestAuthToken(t *testing.T, conn *gorm.DB, kms *kms.Kms, scopeId string) *AuthToken { +func TestAuthToken(t *testing.T, conn *gorm.DB, kms *kms.Kms, scopeId string, opt ...Option) *AuthToken { t.Helper() authMethod := password.TestAuthMethods(t, conn, scopeId, 1)[0] // auth account is only used to join auth method to user. @@ -24,13 +24,12 @@ func TestAuthToken(t *testing.T, conn *gorm.DB, kms *kms.Kms, scopeId string) *A iamRepo, err := iam.NewRepository(rw, rw, kms) require.NoError(t, err) - u, err := iamRepo.LookupUserWithLogin(ctx, acct.GetPublicId(), iam.WithAutoVivify(true)) - require.NoError(t, err) + u := iam.TestUser(t, iamRepo, scopeId, iam.WithAccountIds(acct.PublicId)) repo, err := NewRepository(rw, rw, kms) require.NoError(t, err) - at, err := repo.CreateAuthToken(ctx, u, acct.GetPublicId()) + at, err := repo.CreateAuthToken(ctx, u, acct.GetPublicId(), opt...) require.NoError(t, err) return at } diff --git a/internal/cmd/base/base.go b/internal/cmd/base/base.go index 3df7a6174..c374cb524 100644 --- a/internal/cmd/base/base.go +++ b/internal/cmd/base/base.go @@ -42,6 +42,8 @@ const ( EnvTokenName = "BOUNDARY_TOKEN_NAME" EnvKeyringType = "BOUNDARY_KEYRING_TYPE" envRecoveryConfig = "BOUNDARY_RECOVERY_CONFIG" + + StoredTokenName = "HashiCorp Boundary Auth Token" ) // reRemoveWhitespace is a regular expression for stripping whitespace from @@ -221,7 +223,7 @@ func (c *Command) Client(opt ...Option) (*api.Client, error) { case c.FlagToken != "": c.client.SetToken(c.FlagToken) - case c.client.Token() == "": + case c.client.Token() == "" && strings.ToLower(c.FlagKeyringType) != "none": keyringType, tokenName, err := c.DiscoverKeyringTokenInfo() if err != nil { return nil, err @@ -319,8 +321,11 @@ func (c *Command) ReadTokenFromKeyring(keyringType, tokenName string) *authtoken var err error switch keyringType { + case "none": + return nil + case "wincred", "keychain": - token, err = zkeyring.Get("HashiCorp Boundary Auth Token", tokenName) + token, err = zkeyring.Get(StoredTokenName, tokenName) if err != nil { if err == zkeyring.ErrNotFound { c.UI.Error("No saved credential found, continuing without") diff --git a/internal/cmd/base/format.go b/internal/cmd/base/format.go index fb7b02c8d..77b4d07c0 100644 --- a/internal/cmd/base/format.go +++ b/internal/cmd/base/format.go @@ -201,7 +201,7 @@ func (c *Command) PrintApiError(in *api.Error, contextStr string) { continue } output = append(output, - fmt.Sprintf(" Name: -%s", strings.ReplaceAll(field.Name, "_", "-")), + fmt.Sprintf(" Name: -%s", strings.ReplaceAll(strings.TrimPrefix(field.Name, "attributes."), "_", "-")), fmt.Sprintf(" Error: %s", field.Description), ) } diff --git a/internal/cmd/base/initial_resources.go b/internal/cmd/base/initial_resources.go index 1398d7c3c..8c55d9e58 100644 --- a/internal/cmd/base/initial_resources.go +++ b/internal/cmd/base/initial_resources.go @@ -71,7 +71,7 @@ func (b *Server) CreateInitialLoginRole(ctx context.Context) (*iam.Role, error) return role, nil } -func (b *Server) CreateInitialAuthMethod(ctx context.Context) (*password.AuthMethod, *iam.User, error) { +func (b *Server) CreateInitialPasswordAuthMethod(ctx context.Context) (*password.AuthMethod, *iam.User, error) { rw := db.New(b.Database) kmsRepo, err := kms.NewRepository(rw, rw) @@ -94,14 +94,14 @@ func (b *Server) CreateInitialAuthMethod(ctx context.Context) (*password.AuthMet return nil, nil, fmt.Errorf("error creating password repo: %w", err) } authMethod, err := password.NewAuthMethod(scope.Global.String(), - password.WithName("Generated global scope initial auth method"), - password.WithDescription("Provides initial administrative authentication into Boundary"), + password.WithName("Generated global scope initial password auth method"), + password.WithDescription("Provides initial administrative and unprivileged authentication into Boundary"), ) if err != nil { return nil, nil, fmt.Errorf("error creating new in memory auth method: %w", err) } - if b.DevAuthMethodId == "" { - b.DevAuthMethodId, err = db.NewPublicId(password.AuthMethodPrefix) + if b.DevPasswordAuthMethodId == "" { + b.DevPasswordAuthMethodId, err = db.NewPublicId(password.AuthMethodPrefix) if err != nil { return nil, nil, fmt.Errorf("error generating initial auth method id: %w", err) } @@ -118,28 +118,41 @@ func (b *Server) CreateInitialAuthMethod(ctx context.Context) (*password.AuthMet }() am, err := pwRepo.CreateAuthMethod(cancelCtx, authMethod, - password.WithPublicId(b.DevAuthMethodId)) + password.WithPublicId(b.DevPasswordAuthMethodId)) if err != nil { return nil, nil, fmt.Errorf("error saving auth method to the db: %w", err) } - b.InfoKeys = append(b.InfoKeys, "generated auth method id") - b.Info["generated auth method id"] = b.DevAuthMethodId + b.InfoKeys = append(b.InfoKeys, "generated password auth method id") + b.Info["generated password auth method id"] = b.DevPasswordAuthMethodId + + // we'll designate the initial password auth method as the primary auth + // method id for the global scope, which means the auth method will create + // users on first login. Otherwise, the operator would have to create both + // a password account and a user associated with the new account, before + // users could successfully login. + iamRepo, err := iam.NewRepository(rw, rw, kmsCache) + if err != nil { + return nil, nil, fmt.Errorf("unable to create iam repo: %w", err) + } + globalScope, err := iamRepo.LookupScope(ctx, scope.Global.String()) + if err != nil { + return nil, nil, fmt.Errorf("unable to lookup global scope: %w", err) + } + globalScope.PrimaryAuthMethodId = am.PublicId + if _, _, err := iamRepo.UpdateScope(ctx, globalScope, globalScope.Version, []string{"PrimaryAuthMethodId"}); err != nil { + return nil, nil, fmt.Errorf("unable to set primary auth method for global scope: %w", err) + } createUser := func(loginName, loginPassword, userId string, admin bool) (*iam.User, error) { // Create the dev admin user if loginName == "" { - b.DevLoginName, err = base62.Random(10) - if err != nil { - return nil, fmt.Errorf("unable to generate login name: %w", err) - } - loginName = strings.ToLower(b.DevLoginName) + return nil, fmt.Errorf("empty login name") } if loginPassword == "" { - b.DevPassword, err = base62.Random(20) - if err != nil { - return nil, fmt.Errorf("unable to generate password: %w", err) - } - loginPassword = b.DevPassword + return nil, fmt.Errorf("empty login name") + } + if userId == "" { + return nil, fmt.Errorf("empty user id") } typeStr := "admin" if !admin { @@ -150,7 +163,7 @@ func (b *Server) CreateInitialAuthMethod(ctx context.Context) (*password.AuthMet acct, err := password.NewAccount(am.PublicId, password.WithLoginName(loginName)) if err != nil { - return nil, fmt.Errorf("error creating new in memory auth account: %w", err) + return nil, fmt.Errorf("error creating new in memory password auth account: %w", err) } acct, err = pwRepo.CreateAccount(cancelCtx, scope.Global.String(), acct, password.WithPassword(loginPassword)) if err != nil { @@ -161,17 +174,10 @@ func (b *Server) CreateInitialAuthMethod(ctx context.Context) (*password.AuthMet iamRepo, err := iam.NewRepository(rw, rw, kmsCache, iam.WithRandomReader(b.SecureRandomReader)) if err != nil { - return nil, fmt.Errorf("unable to create repo for org id: %w", err) + return nil, fmt.Errorf("unable to create iam repo: %w", err) } // Create a new user and associate it with the account - if userId == "" { - b.DevUserId, err = db.NewPublicId(iam.UserPrefix) - if err != nil { - return nil, fmt.Errorf("error generating initial user id: %w", err) - } - userId = b.DevUserId - } opts := []iam.Option{ iam.WithPublicId(userId), } @@ -220,12 +226,34 @@ func (b *Server) CreateInitialAuthMethod(ctx context.Context) (*password.AuthMet return u, nil } - if b.DevUnprivilegedLoginName != "" { - unprivUser, err := createUser(b.DevUnprivilegedLoginName, b.DevUnprivilegedPassword, b.DevUnprivilegedUserId, false) + switch { + case b.DevUnprivilegedLoginName == "", + b.DevUnprivilegedPassword == "", + b.DevUnprivilegedUserId == "": + default: + _, err := createUser(b.DevUnprivilegedLoginName, b.DevUnprivilegedPassword, b.DevUnprivilegedUserId, false) if err != nil { return nil, nil, err } - b.DevUnprivilegedUserId = unprivUser.GetPublicId() + } + if b.DevLoginName == "" { + b.DevLoginName, err = base62.Random(10) + if err != nil { + return nil, nil, fmt.Errorf("unable to generate login name: %w", err) + } + b.DevLoginName = strings.ToLower(b.DevLoginName) + } + if b.DevPassword == "" { + b.DevPassword, err = base62.Random(20) + if err != nil { + return nil, nil, fmt.Errorf("unable to generate password: %w", err) + } + } + if b.DevUserId == "" { + b.DevUserId, err = db.NewPublicId(iam.UserPrefix) + if err != nil { + return nil, nil, fmt.Errorf("error generating initial user id: %w", err) + } } u, err := createUser(b.DevLoginName, b.DevPassword, b.DevUserId, true) if err != nil { diff --git a/internal/cmd/base/servers.go b/internal/cmd/base/servers.go index 45a22ad98..239415feb 100644 --- a/internal/cmd/base/servers.go +++ b/internal/cmd/base/servers.go @@ -69,7 +69,8 @@ type Server struct { Listeners []*ServerListener - DevAuthMethodId string + DevPasswordAuthMethodId string + DevOidcAuthMethodId string DevLoginName string DevPassword string DevUserId string @@ -550,7 +551,7 @@ func (b *Server) CreateDevDatabase(ctx context.Context, dialect string, opt ...O return nil } - if _, _, err := b.CreateInitialAuthMethod(ctx); err != nil { + if _, _, err := b.CreateInitialPasswordAuthMethod(ctx); err != nil { return err } diff --git a/internal/cmd/commands.go b/internal/cmd/commands.go index 81c544390..2670a466d 100644 --- a/internal/cmd/commands.go +++ b/internal/cmd/commands.go @@ -60,6 +60,11 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) { Command: base.NewCommand(ui), }, nil }, + "authenticate oidc": func() (cli.Command, error) { + return &authenticate.OidcCommand{ + Command: base.NewCommand(ui), + }, nil + }, "accounts": func() (cli.Command, error) { return &accountscmd.Command{ @@ -108,6 +113,12 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) { Func: "create", }, nil }, + "accounts create oidc": func() (cli.Command, error) { + return &accountscmd.OidcCommand{ + Command: base.NewCommand(ui), + Func: "create", + }, nil + }, "accounts update": func() (cli.Command, error) { return &accountscmd.Command{ Command: base.NewCommand(ui), @@ -120,6 +131,12 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) { Func: "update", }, nil }, + "accounts update oidc": func() (cli.Command, error) { + return &accountscmd.OidcCommand{ + Command: base.NewCommand(ui), + Func: "update", + }, nil + }, "auth-methods": func() (cli.Command, error) { return &authmethodscmd.Command{ @@ -156,6 +173,12 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) { Func: "create", }, nil }, + "auth-methods create oidc": func() (cli.Command, error) { + return &authmethodscmd.OidcCommand{ + Command: base.NewCommand(ui), + Func: "create", + }, nil + }, "auth-methods update": func() (cli.Command, error) { return &authmethodscmd.Command{ Command: base.NewCommand(ui), @@ -168,6 +191,18 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) { Func: "update", }, nil }, + "auth-methods update oidc": func() (cli.Command, error) { + return &authmethodscmd.OidcCommand{ + Command: base.NewCommand(ui), + Func: "update", + }, nil + }, + "auth-methods change-state oidc": func() (cli.Command, error) { + return &authmethodscmd.OidcCommand{ + Command: base.NewCommand(ui), + Func: "change-state", + }, nil + }, "auth-tokens": func() (cli.Command, error) { return &authtokenscmd.Command{ diff --git a/internal/cmd/commands/accountscmd/accounts.gen.go b/internal/cmd/commands/accountscmd/accounts.gen.go index 8f52205a4..4640e84c4 100644 --- a/internal/cmd/commands/accountscmd/accounts.gen.go +++ b/internal/cmd/commands/accountscmd/accounts.gen.go @@ -202,7 +202,7 @@ func (c *Command) Run(args []string) int { } - if ok := extraFlagsHandlingFunc(c, &opts); !ok { + if ok := extraFlagsHandlingFunc(c, f, &opts); !ok { return base.CommandUserError } @@ -316,7 +316,7 @@ var ( extraActionsFlagsMapFunc = func() map[string][]string { return nil } extraSynopsisFunc = func(*Command) string { return "" } extraFlagsFunc = func(*Command, *base.FlagSets, *base.FlagSet) {} - extraFlagsHandlingFunc = func(*Command, *[]accounts.Option) bool { return true } + extraFlagsHandlingFunc = func(*Command, *base.FlagSets, *[]accounts.Option) bool { return true } executeExtraActions = func(_ *Command, inResult api.GenericResult, inErr error, _ *accounts.Client, _ uint32, _ []accounts.Option) (api.GenericResult, error) { return inResult, inErr } diff --git a/internal/cmd/commands/accountscmd/funcs.go b/internal/cmd/commands/accountscmd/funcs.go index aa1d371f2..eeb431be4 100644 --- a/internal/cmd/commands/accountscmd/funcs.go +++ b/internal/cmd/commands/accountscmd/funcs.go @@ -115,7 +115,7 @@ func extraFlagsFuncImpl(c *Command, _ *base.FlagSets, f *base.FlagSet) { } } -func extraFlagsHandlingFuncImpl(c *Command, opts *[]accounts.Option) bool { +func extraFlagsHandlingFuncImpl(c *Command, _ *base.FlagSets, opts *[]accounts.Option) bool { if strutil.StrListContains(flagsMap[c.Func], "password") && c.flagPassword == "" { fmt.Print("Password is not set as flag, please enter it now (will be hidden): ") value, err := password.Read(os.Stdin) diff --git a/internal/cmd/commands/accountscmd/oidc_accounts.gen.go b/internal/cmd/commands/accountscmd/oidc_accounts.gen.go new file mode 100644 index 000000000..672c5aa34 --- /dev/null +++ b/internal/cmd/commands/accountscmd/oidc_accounts.gen.go @@ -0,0 +1,240 @@ +// Code generated by "make api"; DO NOT EDIT. +package accountscmd + +import ( + "errors" + "fmt" + + "github.com/hashicorp/boundary/api" + "github.com/hashicorp/boundary/api/accounts" + "github.com/hashicorp/boundary/internal/cmd/base" + "github.com/hashicorp/boundary/internal/cmd/common" + "github.com/hashicorp/boundary/sdk/strutil" + "github.com/mitchellh/cli" + "github.com/posener/complete" +) + +func initOidcFlags() { + flagsOnce.Do(func() { + extraFlags := extraOidcActionsFlagsMapFunc() + for k, v := range extraFlags { + flagsOidcMap[k] = append(flagsOidcMap[k], v...) + } + }) +} + +var ( + _ cli.Command = (*OidcCommand)(nil) + _ cli.CommandAutocomplete = (*OidcCommand)(nil) +) + +type OidcCommand struct { + *base.Command + + Func string + + plural string + + extraOidcCmdVars +} + +func (c *OidcCommand) AutocompleteArgs() complete.Predictor { + initOidcFlags() + return complete.PredictAnything +} + +func (c *OidcCommand) AutocompleteFlags() complete.Flags { + initOidcFlags() + return c.Flags().Completions() +} + +func (c *OidcCommand) Synopsis() string { + if extra := extraOidcSynopsisFunc(c); extra != "" { + return extra + } + + synopsisStr := "account" + + synopsisStr = fmt.Sprintf("%s %s", "oidc-type", synopsisStr) + + return common.SynopsisFunc(c.Func, synopsisStr) +} + +func (c *OidcCommand) Help() string { + initOidcFlags() + + var helpStr string + helpMap := common.HelpMap("account") + + switch c.Func { + default: + + helpStr = c.extraOidcHelpFunc(helpMap) + } + + // Keep linter from complaining if we don't actually generate code using it + _ = helpMap + return helpStr +} + +var flagsOidcMap = map[string][]string{ + + "create": {"auth-method-id", "name", "description"}, + + "update": {"id", "name", "description", "version"}, +} + +func (c *OidcCommand) Flags() *base.FlagSets { + if len(flagsOidcMap[c.Func]) == 0 { + return c.FlagSet(base.FlagSetNone) + } + + set := c.FlagSet(base.FlagSetHTTP | base.FlagSetClient | base.FlagSetOutputFormat) + f := set.NewFlagSet("Command Options") + common.PopulateCommonFlags(c.Command, f, "oidc-type account", flagsOidcMap[c.Func]) + + extraOidcFlagsFunc(c, set, f) + + return set +} + +func (c *OidcCommand) Run(args []string) int { + initOidcFlags() + + switch c.Func { + case "": + return cli.RunResultHelp + } + + c.plural = "oidc-type account" + switch c.Func { + case "list": + c.plural = "oidc-type accounts" + } + + f := c.Flags() + + if err := f.Parse(args); err != nil { + c.PrintCliError(err) + return base.CommandUserError + } + + if strutil.StrListContains(flagsOidcMap[c.Func], "id") && c.FlagId == "" { + c.PrintCliError(errors.New("ID is required but not passed in via -id")) + return base.CommandUserError + } + + var opts []accounts.Option + + if strutil.StrListContains(flagsOidcMap[c.Func], "auth-method-id") { + switch c.Func { + case "create": + if c.FlagAuthMethodId == "" { + c.PrintCliError(errors.New("AuthMethod ID must be passed in via -auth-method-id or BOUNDARY_AUTH_METHOD_ID")) + return base.CommandUserError + } + } + } + + client, err := c.Client() + if err != nil { + c.PrintCliError(fmt.Errorf("Error creating API client: %s", err.Error())) + return base.CommandCliError + } + accountsClient := accounts.NewClient(client) + + switch c.FlagName { + case "": + case "null": + opts = append(opts, accounts.DefaultName()) + default: + opts = append(opts, accounts.WithName(c.FlagName)) + } + + switch c.FlagDescription { + case "": + case "null": + opts = append(opts, accounts.DefaultDescription()) + default: + opts = append(opts, accounts.WithDescription(c.FlagDescription)) + } + + if c.FlagFilter != "" { + opts = append(opts, accounts.WithFilter(c.FlagFilter)) + } + + var version uint32 + + switch c.Func { + case "update": + switch c.FlagVersion { + case 0: + opts = append(opts, accounts.WithAutomaticVersioning(true)) + default: + version = uint32(c.FlagVersion) + } + } + + if ok := extraOidcFlagsHandlingFunc(c, f, &opts); !ok { + return base.CommandUserError + } + + var result api.GenericResult + + switch c.Func { + + case "create": + result, err = accountsClient.Create(c.Context, c.FlagAuthMethodId, opts...) + + case "update": + result, err = accountsClient.Update(c.Context, c.FlagId, version, opts...) + + } + + result, err = executeExtraOidcActions(c, result, err, accountsClient, version, opts) + + if err != nil { + if apiErr := api.AsServerError(err); apiErr != nil { + c.PrintApiError(apiErr, fmt.Sprintf("Error from controller when performing %s on %s", c.Func, c.plural)) + return base.CommandApiError + } + c.PrintCliError(fmt.Errorf("Error trying to %s %s: %s", c.Func, c.plural, err.Error())) + return base.CommandCliError + } + + output, err := printCustomOidcActionOutput(c) + if err != nil { + c.PrintCliError(err) + return base.CommandUserError + } + if output { + return base.CommandSuccess + } + + switch c.Func { + } + + item := result.GetItem().(*accounts.Account) + switch base.Format(c.UI) { + case "table": + c.UI.Output(printItemTable(item)) + + case "json": + if ok := c.PrintJsonItem(result, item); !ok { + return base.CommandCliError + } + } + + return base.CommandSuccess +} + +var ( + extraOidcActionsFlagsMapFunc = func() map[string][]string { return nil } + extraOidcSynopsisFunc = func(*OidcCommand) string { return "" } + extraOidcFlagsFunc = func(*OidcCommand, *base.FlagSets, *base.FlagSet) {} + extraOidcFlagsHandlingFunc = func(*OidcCommand, *base.FlagSets, *[]accounts.Option) bool { return true } + executeExtraOidcActions = func(_ *OidcCommand, inResult api.GenericResult, inErr error, _ *accounts.Client, _ uint32, _ []accounts.Option) (api.GenericResult, error) { + return inResult, inErr + } + printCustomOidcActionOutput = func(*OidcCommand) (bool, error) { return false, nil } +) diff --git a/internal/cmd/commands/accountscmd/oidc_funcs.go b/internal/cmd/commands/accountscmd/oidc_funcs.go new file mode 100644 index 000000000..d67f3338a --- /dev/null +++ b/internal/cmd/commands/accountscmd/oidc_funcs.go @@ -0,0 +1,105 @@ +package accountscmd + +import ( + "github.com/hashicorp/boundary/api/accounts" + "github.com/hashicorp/boundary/internal/cmd/base" +) + +const ( + subjectFlagName = "subject" + issuerFlagName = "issuer" +) + +func init() { + extraOidcActionsFlagsMapFunc = extraOidcActionsFlagsMapFuncImpl + extraOidcFlagsFunc = extraOidcFlagsFuncImpl + extraOidcFlagsHandlingFunc = extraOidcFlagsHandlingFuncImpl +} + +func extraOidcActionsFlagsMapFuncImpl() map[string][]string { + return map[string][]string{ + "create": {subjectFlagName, issuerFlagName}, + } +} + +type extraOidcCmdVars struct { + flagIssuer string + flagSubject string +} + +func (c *OidcCommand) extraOidcHelpFunc(helpMap map[string]func() string) string { + var helpStr string + switch c.Func { + case "create": + helpStr = base.WrapForHelpText([]string{ + "Usage: boundary accounts create oidc [options] [args]", + "", + " Create a oidc-type account. Example:", + "", + ` $ boundary accounts create oidc -subject "prodops" -description "Oidc account for ProdOps"`, + "", + "", + }) + + case "update": + helpStr = base.WrapForHelpText([]string{ + "Usage: boundary accounts update oidc [options] [args]", + "", + " Update a oidc-type account given its ID. Example:", + "", + ` $ boundary accounts update oidc -id acctoidc_1234567890 -name "devops" -description "Oidc account for DevOps"`, + "", + "", + }) + } + return helpStr + c.Flags().Help() +} + +func extraOidcFlagsFuncImpl(c *OidcCommand, set *base.FlagSets, f *base.FlagSet) { + f = set.NewFlagSet("Oidc Account Options") + + for _, name := range flagsOidcMap[c.Func] { + switch name { + case issuerFlagName: + f.StringVar(&base.StringVar{ + Name: issuerFlagName, + Target: &c.flagIssuer, + Usage: "The issuer for the account.", + }) + case subjectFlagName: + f.StringVar(&base.StringVar{ + Name: subjectFlagName, + Target: &c.flagSubject, + Usage: "The subject for this account on the OIDC provider.", + }) + } + } +} + +func extraOidcFlagsHandlingFuncImpl(c *OidcCommand, _ *base.FlagSets, opts *[]accounts.Option) bool { + switch c.flagSubject { + case "null", "": + if c.Func == "create" { + c.UI.Error("Subject must be passed in via -subject") + return false + } + default: + if c.Func != "create" { + c.UI.Error("-subject can only be set when creating an oidc account") + return false + } + *opts = append(*opts, accounts.WithOidcAccountSubject(c.flagSubject)) + } + + switch c.flagIssuer { + case "": + case "null": + default: + if c.Func != "create" { + c.UI.Error("-issuer can only be set when creating an oidc account") + return false + } + *opts = append(*opts, accounts.WithOidcAccountIssuer(c.flagIssuer)) + } + return true +} diff --git a/internal/cmd/commands/accountscmd/password_accounts.gen.go b/internal/cmd/commands/accountscmd/password_accounts.gen.go index 8dbb9c99a..9077f5dff 100644 --- a/internal/cmd/commands/accountscmd/password_accounts.gen.go +++ b/internal/cmd/commands/accountscmd/password_accounts.gen.go @@ -175,7 +175,7 @@ func (c *PasswordCommand) Run(args []string) int { } } - if ok := extraPasswordFlagsHandlingFunc(c, &opts); !ok { + if ok := extraPasswordFlagsHandlingFunc(c, f, &opts); !ok { return base.CommandUserError } @@ -232,7 +232,7 @@ var ( extraPasswordActionsFlagsMapFunc = func() map[string][]string { return nil } extraPasswordSynopsisFunc = func(*PasswordCommand) string { return "" } extraPasswordFlagsFunc = func(*PasswordCommand, *base.FlagSets, *base.FlagSet) {} - extraPasswordFlagsHandlingFunc = func(*PasswordCommand, *[]accounts.Option) bool { return true } + extraPasswordFlagsHandlingFunc = func(*PasswordCommand, *base.FlagSets, *[]accounts.Option) bool { return true } executeExtraPasswordActions = func(_ *PasswordCommand, inResult api.GenericResult, inErr error, _ *accounts.Client, _ uint32, _ []accounts.Option) (api.GenericResult, error) { return inResult, inErr } diff --git a/internal/cmd/commands/accountscmd/password_funcs.go b/internal/cmd/commands/accountscmd/password_funcs.go index 0df7f8890..c4984925c 100644 --- a/internal/cmd/commands/accountscmd/password_funcs.go +++ b/internal/cmd/commands/accountscmd/password_funcs.go @@ -78,7 +78,7 @@ func extraPasswordFlagsFuncImpl(c *PasswordCommand, set *base.FlagSets, f *base. } } -func extraPasswordFlagsHandlingFuncImpl(c *PasswordCommand, opts *[]accounts.Option) bool { +func extraPasswordFlagsHandlingFuncImpl(c *PasswordCommand, _ *base.FlagSets, opts *[]accounts.Option) bool { if c.Func == "create" && c.flagLoginName == "" { c.UI.Error("Login Name must be passed in via -login-name") return false diff --git a/internal/cmd/commands/authenticate/funcs.go b/internal/cmd/commands/authenticate/funcs.go new file mode 100644 index 000000000..ffd3f808a --- /dev/null +++ b/internal/cmd/commands/authenticate/funcs.go @@ -0,0 +1,105 @@ +package authenticate + +import ( + "encoding/base64" + "encoding/json" + "fmt" + "time" + + "github.com/hashicorp/boundary/api/authmethods" + "github.com/hashicorp/boundary/api/authtokens" + "github.com/hashicorp/boundary/internal/cmd/base" + nkeyring "github.com/jefferai/keyring" + zkeyring "github.com/zalando/go-keyring" +) + +func saveAndOrPrintToken(c *base.Command, result *authmethods.AuthenticateResult) int { + token := new(authtokens.AuthToken) + if err := json.Unmarshal(result.GetRawAttributes(), token); err != nil { + c.PrintCliError(fmt.Errorf("Error trying to decode response as an auth token: %w", err)) + return base.CommandCliError + } + + switch base.Format(c.UI) { + case "table": + c.UI.Output(base.WrapForHelpText([]string{ + "", + "Authentication information:", + fmt.Sprintf(" Account ID: %s", token.AccountId), + fmt.Sprintf(" Auth Method ID: %s", token.AuthMethodId), + fmt.Sprintf(" Expiration Time: %s", token.ExpirationTime.Local().Format(time.RFC1123)), + fmt.Sprintf(" User ID: %s", token.UserId), + })) + + case "json": + if ok := c.PrintJsonItem(&dummyGenericResponse{ + item: token, + response: result.GetResponse(), + }, token); !ok { + return base.CommandCliError + } + return base.CommandSuccess + } + + var gotErr bool + keyringType, tokenName, err := c.DiscoverKeyringTokenInfo() + if err != nil { + c.UI.Error(fmt.Sprintf("Error fetching keyring information: %s", err)) + gotErr = true + } else if keyringType != "none" && + tokenName != "none" && + keyringType != "" && + tokenName != "" { + marshaled, err := json.Marshal(token) + if err != nil { + c.UI.Error(fmt.Sprintf("Error marshaling auth token to save to keyring: %s", err)) + gotErr = true + } else { + switch keyringType { + case "wincred", "keychain": + if err := zkeyring.Set(base.StoredTokenName, tokenName, base64.RawStdEncoding.EncodeToString(marshaled)); err != nil { + c.UI.Error(fmt.Sprintf("Error saving auth token to %q keyring: %s", keyringType, err)) + gotErr = true + } + + default: + krConfig := nkeyring.Config{ + LibSecretCollectionName: "login", + PassPrefix: "HashiCorp_Boundary", + AllowedBackends: []nkeyring.BackendType{nkeyring.BackendType(keyringType)}, + } + + kr, err := nkeyring.Open(krConfig) + if err != nil { + c.UI.Error(fmt.Sprintf("Error opening %q keyring: %s", keyringType, err)) + gotErr = true + break + } + + if err := kr.Set(nkeyring.Item{ + Key: tokenName, + Data: []byte(base64.RawStdEncoding.EncodeToString(marshaled)), + }); err != nil { + c.UI.Error(fmt.Sprintf("Error storing token in %q keyring: %s", keyringType, err)) + gotErr = true + break + } + } + + if !gotErr { + c.UI.Output("\nThe token was successfully stored in the chosen keyring and is not displayed here.") + } + } + } + + switch { + case gotErr: + c.UI.Warn(fmt.Sprintf("The token was not successfully saved to a system keyring. The token is:\n\n%s\n\nIt must be manually passed in via the BOUNDARY_TOKEN env var or -token flag. Storing the token can also be disabled via -keyring-type=none.", token.Token)) + case c.FlagKeyringType == "none": + c.UI.Warn("\nStoring the token in a keyring was disabled. The token is:") + c.UI.Output(token.Token) + c.UI.Warn("Please be sure to store it safely!") + } + + return base.CommandSuccess +} diff --git a/internal/cmd/commands/authenticate/oidc.go b/internal/cmd/commands/authenticate/oidc.go new file mode 100644 index 000000000..aa9f4156d --- /dev/null +++ b/internal/cmd/commands/authenticate/oidc.go @@ -0,0 +1,156 @@ +package authenticate + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + "sync" + "time" + + "github.com/hashicorp/boundary/api" + "github.com/hashicorp/boundary/api/authmethods" + "github.com/hashicorp/boundary/internal/cmd/base" + "github.com/hashicorp/cap/util" + "github.com/mitchellh/cli" + "github.com/mitchellh/go-wordwrap" + "github.com/posener/complete" +) + +var ( + _ cli.Command = (*OidcCommand)(nil) + _ cli.CommandAutocomplete = (*OidcCommand)(nil) +) + +type OidcCommand struct { + *base.Command +} + +func (c *OidcCommand) Synopsis() string { + return wordwrap.WrapString("Invoke the OIDC auth method to authenticate with Boundary", base.TermWidth) +} + +func (c *OidcCommand) Help() string { + return base.WrapForHelpText([]string{ + "Usage: boundary authenticate oidc [options] [args]", + "", + " Invoke the OIDC auth method to authenticate the Boundary CLI:", + "", + ` $ boundary authenticate oidc -auth-method-id amoidc_1234567890`, + "", + "", + }) + c.Flags().Help() +} + +func (c *OidcCommand) Flags() *base.FlagSets { + set := c.FlagSet(base.FlagSetHTTP | base.FlagSetClient | base.FlagSetOutputFormat) + f := set.NewFlagSet("Command Options") + + f.StringVar(&base.StringVar{ + Name: "auth-method-id", + EnvVar: "BOUNDARY_AUTH_METHOD_ID", + Target: &c.FlagAuthMethodId, + Usage: "The auth-method resource to use for the operation", + }) + + return set +} + +func (c *OidcCommand) AutocompleteArgs() complete.Predictor { + return complete.PredictAnything +} + +func (c *OidcCommand) AutocompleteFlags() complete.Flags { + return c.Flags().Completions() +} + +func (c *OidcCommand) Run(args []string) int { + f := c.Flags() + + if err := f.Parse(args); err != nil { + c.PrintCliError(err) + return base.CommandUserError + } + + switch { + case c.FlagAuthMethodId == "": + c.PrintCliError(errors.New("Auth method ID must be provided via -auth-method-id")) + return base.CommandUserError + } + + client, err := c.Client(base.WithNoTokenScope(), base.WithNoTokenValue()) + if err != nil { + c.PrintCliError(fmt.Errorf("Error creating API client: %w", err)) + return base.CommandCliError + } + aClient := authmethods.NewClient(client) + + result, err := aClient.Authenticate(c.Context, c.FlagAuthMethodId, "start", nil) + if err != nil { + if apiErr := api.AsServerError(err); apiErr != nil { + c.PrintApiError(apiErr, "Error from controller when performing authentication start") + return base.CommandApiError + } + c.PrintCliError(fmt.Errorf("Error trying to perform authentication start: %w", err)) + return base.CommandCliError + } + + startResp := new(authmethods.OidcAuthMethodAuthenticateStartResponse) + if err := json.Unmarshal(result.GetRawAttributes(), startResp); err != nil { + c.PrintCliError(fmt.Errorf("Error trying to decode authenticate start response: %w", err)) + return base.CommandCliError + } + + c.UI.Output("Opening returned authentication URL in your browser...") + if err := util.OpenURL(startResp.AuthUrl); err != nil { + c.UI.Error(fmt.Errorf("Unable to open authentication URL in browser: %w", err).Error()) + c.UI.Warn("Please open the following URL manually in your web browser:") + c.UI.Output(startResp.AuthUrl) + } + + var watchCode int + wg := new(sync.WaitGroup) + wg.Add(1) + go func() { + defer wg.Done() + for { + select { + case <-c.Context.Done(): + c.PrintCliError(errors.New("Command canceled.")) + watchCode = base.CommandCliError + return + + case <-time.After(1500 * time.Millisecond): + result, err = aClient.Authenticate(c.Context, c.FlagAuthMethodId, "token", map[string]interface{}{ + "token_id": startResp.TokenId, + }) + if err != nil { + if apiErr := api.AsServerError(err); apiErr != nil { + c.PrintApiError(apiErr, "Error from controller when performing authentication token fetch") + watchCode = base.CommandApiError + return + } + c.PrintCliError(fmt.Errorf("Error trying to perform authentication token fetch: %w", err)) + watchCode = base.CommandCliError + return + } + if result.GetResponse().StatusCode() == http.StatusNoContent { + // Nothing yet -- circle around. + continue + } + return + } + } + }() + wg.Wait() + + if watchCode != 0 { + return watchCode + } + if result == nil { + c.PrintCliError(errors.New("After watching for token, no response was found.")) + return base.CommandCliError + } + + return saveAndOrPrintToken(c.Command, result) +} diff --git a/internal/cmd/commands/authenticate/password.go b/internal/cmd/commands/authenticate/password.go index ea05108aa..4af7f36eb 100644 --- a/internal/cmd/commands/authenticate/password.go +++ b/internal/cmd/commands/authenticate/password.go @@ -1,24 +1,18 @@ package authenticate import ( - "encoding/base64" - "encoding/json" "errors" "fmt" "os" "strings" - "time" "github.com/hashicorp/boundary/api" "github.com/hashicorp/boundary/api/authmethods" - "github.com/hashicorp/boundary/api/authtokens" "github.com/hashicorp/boundary/internal/cmd/base" "github.com/hashicorp/vault/sdk/helper/password" - nkeyring "github.com/jefferai/keyring" "github.com/mitchellh/cli" "github.com/mitchellh/go-wordwrap" "github.com/posener/complete" - zkeyring "github.com/zalando/go-keyring" ) var ( @@ -27,9 +21,8 @@ var ( ) var ( - envPassword = "BOUNDARY_AUTHENTICATE_PASSWORD_PASSWORD" - envLoginName = "BOUNDARY_AUTHENTICATE_PASSWORD_LOGIN_NAME" - envAuthMethodId = "BOUNDARY_AUTHENTICATE_AUTH_METHOD_ID" + envPassword = "BOUNDARY_AUTHENTICATE_PASSWORD_PASSWORD" + envLoginName = "BOUNDARY_AUTHENTICATE_PASSWORD_LOGIN_NAME" ) type PasswordCommand struct { @@ -91,6 +84,21 @@ func (c *PasswordCommand) AutocompleteFlags() complete.Flags { return c.Flags().Completions() } +type dummyGenericResponse struct { + item interface{} + response *api.Response +} + +var _ api.GenericResult = (*dummyGenericResponse)(nil) + +func (d *dummyGenericResponse) GetItem() interface{} { + return d.item +} + +func (d *dummyGenericResponse) GetResponse() *api.Response { + return d.response +} + func (c *PasswordCommand) Run(args []string) int { f := c.Flags() @@ -125,9 +133,6 @@ func (c *PasswordCommand) Run(args []string) int { return base.CommandCliError } - // note: Authenticate() calls SetToken() under the hood to set the - // auth bearer on the client so we do not need to do anything with the - // returned token after this call, so we ignore it result, err := authmethods.NewClient(client).Authenticate(c.Context, c.FlagAuthMethodId, "login", map[string]interface{}{ "login_name": c.flagLoginName, @@ -142,76 +147,5 @@ func (c *PasswordCommand) Run(args []string) int { return base.CommandCliError } - token := result.GetItem().(*authtokens.AuthToken) - switch base.Format(c.UI) { - case "table": - c.UI.Output(base.WrapForHelpText([]string{ - "", - "Authentication information:", - fmt.Sprintf(" Account ID: %s", token.AccountId), - fmt.Sprintf(" Auth Method ID: %s", token.AuthMethodId), - fmt.Sprintf(" Expiration Time: %s", token.ExpirationTime.Local().Format(time.RFC1123)), - fmt.Sprintf(" Token: %s", token.Token), - fmt.Sprintf(" User ID: %s", token.UserId), - })) - - case "json": - if ok := c.PrintJsonItem(result, token); !ok { - return base.CommandCliError - } - return base.CommandSuccess - } - - var gotErr bool - keyringType, tokenName, err := c.DiscoverKeyringTokenInfo() - if err != nil { - c.UI.Error(fmt.Sprintf("Error fetching keyring information: %s", err)) - gotErr = true - } else if keyringType != "none" && - tokenName != "none" && - keyringType != "" && - tokenName != "" { - marshaled, err := json.Marshal(token) - if err != nil { - c.UI.Error(fmt.Sprintf("Error marshaling auth token to save to keyring: %s", err)) - gotErr = true - } else { - switch keyringType { - case "wincred", "keychain": - if err := zkeyring.Set("HashiCorp Boundary Auth Token", tokenName, base64.RawStdEncoding.EncodeToString(marshaled)); err != nil { - c.UI.Error(fmt.Sprintf("Error saving auth token to %q keyring: %s", keyringType, err)) - gotErr = true - } - - default: - krConfig := nkeyring.Config{ - LibSecretCollectionName: "login", - PassPrefix: "HashiCorp_Boundary", - AllowedBackends: []nkeyring.BackendType{nkeyring.BackendType(keyringType)}, - } - - kr, err := nkeyring.Open(krConfig) - if err != nil { - c.UI.Error(fmt.Sprintf("Error opening %q keyring: %s", keyringType, err)) - gotErr = true - break - } - - if err := kr.Set(nkeyring.Item{ - Key: tokenName, - Data: []byte(base64.RawStdEncoding.EncodeToString(marshaled)), - }); err != nil { - c.UI.Error(fmt.Sprintf("Error storing token in %q keyring: %s", keyringType, err)) - gotErr = true - break - } - } - } - } - - if gotErr { - c.UI.Warn("The token printed above must be manually passed in via the BOUNDARY_TOKEN env var or -token flag. Storing the token can also be disabled via -keyring-type=none.") - } - - return base.CommandSuccess + return saveAndOrPrintToken(c.Command, result) } diff --git a/internal/cmd/commands/authmethodscmd/authmethods.gen.go b/internal/cmd/commands/authmethodscmd/authmethods.gen.go index a17e3af3d..427582169 100644 --- a/internal/cmd/commands/authmethodscmd/authmethods.gen.go +++ b/internal/cmd/commands/authmethodscmd/authmethods.gen.go @@ -169,7 +169,7 @@ func (c *Command) Run(args []string) int { var version uint32 - if ok := extraFlagsHandlingFunc(c, &opts); !ok { + if ok := extraFlagsHandlingFunc(c, f, &opts); !ok { return base.CommandUserError } @@ -283,7 +283,7 @@ var ( extraActionsFlagsMapFunc = func() map[string][]string { return nil } extraSynopsisFunc = func(*Command) string { return "" } extraFlagsFunc = func(*Command, *base.FlagSets, *base.FlagSet) {} - extraFlagsHandlingFunc = func(*Command, *[]authmethods.Option) bool { return true } + extraFlagsHandlingFunc = func(*Command, *base.FlagSets, *[]authmethods.Option) bool { return true } executeExtraActions = func(_ *Command, inResult api.GenericResult, inErr error, _ *authmethods.Client, _ uint32, _ []authmethods.Option) (api.GenericResult, error) { return inResult, inErr } diff --git a/internal/cmd/commands/authmethodscmd/oidc_authmethods.gen.go b/internal/cmd/commands/authmethodscmd/oidc_authmethods.gen.go new file mode 100644 index 000000000..bf6963daa --- /dev/null +++ b/internal/cmd/commands/authmethodscmd/oidc_authmethods.gen.go @@ -0,0 +1,255 @@ +// Code generated by "make api"; DO NOT EDIT. +package authmethodscmd + +import ( + "errors" + "fmt" + + "github.com/hashicorp/boundary/api" + "github.com/hashicorp/boundary/api/authmethods" + "github.com/hashicorp/boundary/internal/cmd/base" + "github.com/hashicorp/boundary/internal/cmd/common" + "github.com/hashicorp/boundary/sdk/strutil" + "github.com/mitchellh/cli" + "github.com/posener/complete" +) + +func initOidcFlags() { + flagsOnce.Do(func() { + extraFlags := extraOidcActionsFlagsMapFunc() + for k, v := range extraFlags { + flagsOidcMap[k] = append(flagsOidcMap[k], v...) + } + }) +} + +var ( + _ cli.Command = (*OidcCommand)(nil) + _ cli.CommandAutocomplete = (*OidcCommand)(nil) +) + +type OidcCommand struct { + *base.Command + + Func string + + plural string + + extraOidcCmdVars +} + +func (c *OidcCommand) AutocompleteArgs() complete.Predictor { + initOidcFlags() + return complete.PredictAnything +} + +func (c *OidcCommand) AutocompleteFlags() complete.Flags { + initOidcFlags() + return c.Flags().Completions() +} + +func (c *OidcCommand) Synopsis() string { + if extra := extraOidcSynopsisFunc(c); extra != "" { + return extra + } + + synopsisStr := "auth method" + + synopsisStr = fmt.Sprintf("%s %s", "oidc-type", synopsisStr) + + return common.SynopsisFunc(c.Func, synopsisStr) +} + +func (c *OidcCommand) Help() string { + initOidcFlags() + + var helpStr string + helpMap := common.HelpMap("auth method") + + switch c.Func { + default: + + helpStr = c.extraOidcHelpFunc(helpMap) + } + + // Keep linter from complaining if we don't actually generate code using it + _ = helpMap + return helpStr +} + +var flagsOidcMap = map[string][]string{ + + "create": {"scope-id", "name", "description"}, + + "update": {"id", "name", "description", "version"}, +} + +func (c *OidcCommand) Flags() *base.FlagSets { + if len(flagsOidcMap[c.Func]) == 0 { + return c.FlagSet(base.FlagSetNone) + } + + set := c.FlagSet(base.FlagSetHTTP | base.FlagSetClient | base.FlagSetOutputFormat) + f := set.NewFlagSet("Command Options") + common.PopulateCommonFlags(c.Command, f, "oidc-type auth method", flagsOidcMap[c.Func]) + + extraOidcFlagsFunc(c, set, f) + + return set +} + +func (c *OidcCommand) Run(args []string) int { + initOidcFlags() + + switch c.Func { + case "": + return cli.RunResultHelp + } + + c.plural = "oidc-type auth method" + switch c.Func { + case "list": + c.plural = "oidc-type auth methods" + } + + f := c.Flags() + + if err := f.Parse(args); err != nil { + c.PrintCliError(err) + return base.CommandUserError + } + + if strutil.StrListContains(flagsOidcMap[c.Func], "id") && c.FlagId == "" { + c.PrintCliError(errors.New("ID is required but not passed in via -id")) + return base.CommandUserError + } + + var opts []authmethods.Option + + if strutil.StrListContains(flagsOidcMap[c.Func], "scope-id") { + switch c.Func { + case "create": + if c.FlagScopeId == "" { + c.PrintCliError(errors.New("Scope ID must be passed in via -scope-id or BOUNDARY_SCOPE_ID")) + return base.CommandUserError + } + } + } + + client, err := c.Client() + if err != nil { + c.PrintCliError(fmt.Errorf("Error creating API client: %s", err.Error())) + return base.CommandCliError + } + authmethodsClient := authmethods.NewClient(client) + + switch c.FlagName { + case "": + case "null": + opts = append(opts, authmethods.DefaultName()) + default: + opts = append(opts, authmethods.WithName(c.FlagName)) + } + + switch c.FlagDescription { + case "": + case "null": + opts = append(opts, authmethods.DefaultDescription()) + default: + opts = append(opts, authmethods.WithDescription(c.FlagDescription)) + } + + switch c.FlagRecursive { + case true: + opts = append(opts, authmethods.WithRecursive(true)) + } + + if c.FlagFilter != "" { + opts = append(opts, authmethods.WithFilter(c.FlagFilter)) + } + + var version uint32 + + switch c.Func { + + case "update": + switch c.FlagVersion { + case 0: + opts = append(opts, authmethods.WithAutomaticVersioning(true)) + default: + version = uint32(c.FlagVersion) + } + + case "change-state": + switch c.FlagVersion { + case 0: + opts = append(opts, authmethods.WithAutomaticVersioning(true)) + default: + version = uint32(c.FlagVersion) + } + + } + + if ok := extraOidcFlagsHandlingFunc(c, f, &opts); !ok { + return base.CommandUserError + } + + var result api.GenericResult + + switch c.Func { + + case "create": + result, err = authmethodsClient.Create(c.Context, "oidc", c.FlagScopeId, opts...) + + case "update": + result, err = authmethodsClient.Update(c.Context, c.FlagId, version, opts...) + + } + + result, err = executeExtraOidcActions(c, result, err, authmethodsClient, version, opts) + + if err != nil { + if apiErr := api.AsServerError(err); apiErr != nil { + c.PrintApiError(apiErr, fmt.Sprintf("Error from controller when performing %s on %s", c.Func, c.plural)) + return base.CommandApiError + } + c.PrintCliError(fmt.Errorf("Error trying to %s %s: %s", c.Func, c.plural, err.Error())) + return base.CommandCliError + } + + output, err := printCustomOidcActionOutput(c) + if err != nil { + c.PrintCliError(err) + return base.CommandUserError + } + if output { + return base.CommandSuccess + } + + switch c.Func { + } + + item := result.GetItem().(*authmethods.AuthMethod) + switch base.Format(c.UI) { + case "table": + c.UI.Output(printItemTable(item)) + + case "json": + if ok := c.PrintJsonItem(result, item); !ok { + return base.CommandCliError + } + } + + return base.CommandSuccess +} + +var ( + extraOidcActionsFlagsMapFunc = func() map[string][]string { return nil } + extraOidcSynopsisFunc = func(*OidcCommand) string { return "" } + extraOidcFlagsFunc = func(*OidcCommand, *base.FlagSets, *base.FlagSet) {} + extraOidcFlagsHandlingFunc = func(*OidcCommand, *base.FlagSets, *[]authmethods.Option) bool { return true } + executeExtraOidcActions = func(_ *OidcCommand, inResult api.GenericResult, inErr error, _ *authmethods.Client, _ uint32, _ []authmethods.Option) (api.GenericResult, error) { + return inResult, inErr + } + printCustomOidcActionOutput = func(*OidcCommand) (bool, error) { return false, nil } +) diff --git a/internal/cmd/commands/authmethodscmd/oidc_funcs.go b/internal/cmd/commands/authmethodscmd/oidc_funcs.go new file mode 100644 index 000000000..48236b5bc --- /dev/null +++ b/internal/cmd/commands/authmethodscmd/oidc_funcs.go @@ -0,0 +1,251 @@ +package authmethodscmd + +import ( + "fmt" + "strconv" + + "github.com/hashicorp/boundary/api" + "github.com/hashicorp/boundary/api/authmethods" + "github.com/hashicorp/boundary/internal/cmd/base" +) + +func init() { + extraOidcActionsFlagsMapFunc = extraOidcActionsFlagsMapFuncImpl + extraOidcFlagsFunc = extraOidcFlagsFuncImpl + extraOidcFlagsHandlingFunc = extraOidcFlagHandlingFuncImpl + executeExtraOidcActions = executeExtraOidcActionsImpl +} + +type extraOidcCmdVars struct { + flagState string + flagIssuer string + flagClientId string + flagClientSecret string + flagMaxAgeSeconds string + flagApiUrlPrefix string + flagSigningAlgorithms []string + flagIdpCaCerts []string + flagAllowedAudiences []string + flagDisableDiscoveredConfigValidation bool + flagDryRun bool +} + +const ( + idFlagName = "id" + issuerFlagName = "issuer" + clientIdFlagName = "client-id" + clientSecretFlagName = "client-secret" + maxAgeFlagName = "max-age" + signingAlgorithmFlagName = "signing-algorithm" + apiUrlPrefixFlagName = "api-url-prefix" + caCertFlagName = "idp-ca-cert" + allowedAudienceFlagName = "allowed-audience" + stateFlagName = "state" + disableDiscoveredConfigValidationFlagName = "disable-discovered-config-validation" + dryRunFlagName = "dry-run" +) + +func extraOidcActionsFlagsMapFuncImpl() map[string][]string { + flags := map[string][]string{ + "create": { + issuerFlagName, + clientIdFlagName, + clientSecretFlagName, + maxAgeFlagName, + signingAlgorithmFlagName, + apiUrlPrefixFlagName, + caCertFlagName, + allowedAudienceFlagName, + }, + "change-state": { + idFlagName, + stateFlagName, + disableDiscoveredConfigValidationFlagName, + }, + } + flags["update"] = append(flags["create"], disableDiscoveredConfigValidationFlagName, dryRunFlagName) + return flags +} + +func extraOidcFlagsFuncImpl(c *OidcCommand, set *base.FlagSets, _ *base.FlagSet) { + f := set.NewFlagSet("OIDC Auth Method Options") + + for _, name := range flagsOidcMap[c.Func] { + switch name { + case issuerFlagName: + f.StringVar(&base.StringVar{ + Name: issuerFlagName, + Target: &c.flagIssuer, + Usage: "The provider's Issuer URL.", + }) + case clientIdFlagName: + f.StringVar(&base.StringVar{ + Name: clientIdFlagName, + Target: &c.flagClientId, + Usage: "The OAuth 2.0 Client Identifier this auth method should use with the provider.", + }) + case clientSecretFlagName: + f.StringVar(&base.StringVar{ + Name: clientSecretFlagName, + Target: &c.flagClientSecret, + Usage: "The corresponding client secret.", + }) + case maxAgeFlagName: + f.StringVar(&base.StringVar{ + Name: maxAgeFlagName, + Target: &c.flagMaxAgeSeconds, + Usage: `The OIDC "max_age" parameter sent to the provider.`, + }) + case signingAlgorithmFlagName: + f.StringSliceVar(&base.StringSliceVar{ + Name: signingAlgorithmFlagName, + Target: &c.flagSigningAlgorithms, + Usage: "The allowed signing algorithm. May be specified multiple times for multiple values.", + }) + case apiUrlPrefixFlagName: + f.StringVar(&base.StringVar{ + Name: apiUrlPrefixFlagName, + Target: &c.flagApiUrlPrefix, + Usage: "The URL prefix used by the OIDC provider in the authentication flow.", + }) + case caCertFlagName: + f.StringSliceVar(&base.StringSliceVar{ + Name: caCertFlagName, + Target: &c.flagIdpCaCerts, + Usage: "Optional PEM-encoded X.509 CA certificate that can be used as trust anchors when connecting to an OIDC provider. May be specified multiple times.", + }) + case allowedAudienceFlagName: + f.StringSliceVar(&base.StringSliceVar{ + Name: allowedAudienceFlagName, + Target: &c.flagAllowedAudiences, + Usage: `The acceptable audience ("aud") claim. May be specified multiple times.`, + }) + case stateFlagName: + f.StringVar(&base.StringVar{ + Name: stateFlagName, + Target: &c.flagState, + Usage: "The desired operational state of the auth method.", + }) + case disableDiscoveredConfigValidationFlagName: + f.BoolVar(&base.BoolVar{ + Name: disableDiscoveredConfigValidationFlagName, + Target: &c.flagDisableDiscoveredConfigValidation, + Usage: "Disable validating the given parameters against configuration from the authorization server's discovery URL. This must be specified every time there is an update or state change; not specifying it is equivalent to setting it to false.", + }) + case dryRunFlagName: + f.BoolVar(&base.BoolVar{ + Name: dryRunFlagName, + Target: &c.flagDryRun, + Usage: "Performs all completeness and validation checks with any newly-provided values without persisting the changes.", + }) + } + } +} + +func (c *OidcCommand) extraOidcHelpFunc(helpMap map[string]func() string) string { + var helpStr string + switch c.Func { + case "create": + helpStr = base.WrapForHelpText([]string{ + "Usage: boundary auth-methods create oidc [options] [args]", + "", + " Create an oidc-type auth method. Example:", + "", + ` $ boundary auth-methods create oidc -name prodops -description "Oidc auth-method for ProdOps"`, + "", + "", + }) + + case "update": + helpStr = base.WrapForHelpText([]string{ + "Usage: boundary auth-methods update oidc [options] [args]", + "", + " Update an oidc-type auth method given its ID. Example:", + "", + ` $ boundary auth-methods update oidc -id amoidc_1234567890 -name "devops" -description "Oidc auth-method for DevOps"`, + "", + "", + }) + } + return helpStr + c.Flags().Help() +} + +func extraOidcFlagHandlingFuncImpl(c *OidcCommand, f *base.FlagSets, opts *[]authmethods.Option) bool { + switch c.flagIssuer { + case "": + case "null": + *opts = append(*opts, authmethods.DefaultOidcAuthMethodIssuer()) + default: + *opts = append(*opts, authmethods.WithOidcAuthMethodIssuer(c.flagIssuer)) + } + switch c.flagClientId { + case "": + case "null": + *opts = append(*opts, authmethods.DefaultOidcAuthMethodClientId()) + default: + *opts = append(*opts, authmethods.WithOidcAuthMethodClientId(c.flagClientId)) + } + switch c.flagClientSecret { + case "": + case "null": + *opts = append(*opts, authmethods.DefaultOidcAuthMethodClientSecret()) + default: + *opts = append(*opts, authmethods.WithOidcAuthMethodClientSecret(c.flagClientSecret)) + } + switch c.flagMaxAgeSeconds { + case "": + case "null": + *opts = append(*opts, authmethods.DefaultOidcAuthMethodMaxAge()) + default: + val, err := strconv.ParseUint(c.flagMaxAgeSeconds, 10, 32) + if err != nil { + c.UI.Error(fmt.Sprintf("Error parsing %q: %s", c.flagMaxAgeSeconds, err)) + return false + } + *opts = append(*opts, authmethods.WithOidcAuthMethodMaxAge(uint32(val))) + } + switch c.flagApiUrlPrefix { + case "": + case "null": + *opts = append(*opts, authmethods.DefaultOidcAuthMethodApiUrlPrefix()) + default: + *opts = append(*opts, authmethods.WithOidcAuthMethodApiUrlPrefix(c.flagApiUrlPrefix)) + } + switch { + case len(c.flagSigningAlgorithms) == 0: + case len(c.flagSigningAlgorithms) == 1 && c.flagSigningAlgorithms[0] == "null": + *opts = append(*opts, authmethods.DefaultOidcAuthMethodSigningAlgorithms()) + default: + *opts = append(*opts, authmethods.WithOidcAuthMethodSigningAlgorithms(c.flagSigningAlgorithms)) + } + switch { + case len(c.flagIdpCaCerts) == 0: + case len(c.flagIdpCaCerts) == 1 && c.flagIdpCaCerts[0] == "null": + *opts = append(*opts, authmethods.DefaultOidcAuthMethodIdpCaCerts()) + default: + *opts = append(*opts, authmethods.WithOidcAuthMethodIdpCaCerts(c.flagIdpCaCerts)) + } + switch { + case len(c.flagAllowedAudiences) == 0: + case len(c.flagAllowedAudiences) == 1 && c.flagAllowedAudiences[0] == "null": + *opts = append(*opts, authmethods.DefaultOidcAuthMethodAllowedAudiences()) + default: + *opts = append(*opts, authmethods.WithOidcAuthMethodAllowedAudiences(c.flagAllowedAudiences)) + } + if c.flagDisableDiscoveredConfigValidation { + *opts = append(*opts, authmethods.WithOidcAuthMethodDisableDiscoveredConfigValidation(c.flagDisableDiscoveredConfigValidation)) + } + if c.flagDryRun { + *opts = append(*opts, authmethods.WithOidcAuthMethodDryRun(c.flagDryRun)) + } + + return true +} + +func executeExtraOidcActionsImpl(c *OidcCommand, origResult api.GenericResult, origError error, amClient *authmethods.Client, version uint32, opts []authmethods.Option) (api.GenericResult, error) { + switch c.Func { + case "change-state": + return amClient.ChangeState(c.Context, c.FlagId, version, c.flagState, opts...) + } + return origResult, origError +} diff --git a/internal/cmd/commands/authmethodscmd/password_authmethods.gen.go b/internal/cmd/commands/authmethodscmd/password_authmethods.gen.go index a9e980368..1e7d448c0 100644 --- a/internal/cmd/commands/authmethodscmd/password_authmethods.gen.go +++ b/internal/cmd/commands/authmethodscmd/password_authmethods.gen.go @@ -180,7 +180,7 @@ func (c *PasswordCommand) Run(args []string) int { } } - if ok := extraPasswordFlagsHandlingFunc(c, &opts); !ok { + if ok := extraPasswordFlagsHandlingFunc(c, f, &opts); !ok { return base.CommandUserError } @@ -237,7 +237,7 @@ var ( extraPasswordActionsFlagsMapFunc = func() map[string][]string { return nil } extraPasswordSynopsisFunc = func(*PasswordCommand) string { return "" } extraPasswordFlagsFunc = func(*PasswordCommand, *base.FlagSets, *base.FlagSet) {} - extraPasswordFlagsHandlingFunc = func(*PasswordCommand, *[]authmethods.Option) bool { return true } + extraPasswordFlagsHandlingFunc = func(*PasswordCommand, *base.FlagSets, *[]authmethods.Option) bool { return true } executeExtraPasswordActions = func(_ *PasswordCommand, inResult api.GenericResult, inErr error, _ *authmethods.Client, _ uint32, _ []authmethods.Option) (api.GenericResult, error) { return inResult, inErr } diff --git a/internal/cmd/commands/authmethodscmd/password_funcs.go b/internal/cmd/commands/authmethodscmd/password_funcs.go index b9f917297..1f3b7a916 100644 --- a/internal/cmd/commands/authmethodscmd/password_funcs.go +++ b/internal/cmd/commands/authmethodscmd/password_funcs.go @@ -75,7 +75,7 @@ func extraPasswordFlagsFuncImpl(c *PasswordCommand, set *base.FlagSets, f *base. } } -func extraPasswordFlagHandlingFuncImpl(c *PasswordCommand, opts *[]authmethods.Option) bool { +func extraPasswordFlagHandlingFuncImpl(c *PasswordCommand, _ *base.FlagSets, opts *[]authmethods.Option) bool { var attributes map[string]interface{} addAttribute := func(name string, value interface{}) { if attributes == nil { diff --git a/internal/cmd/commands/authtokenscmd/authtokens.gen.go b/internal/cmd/commands/authtokenscmd/authtokens.gen.go index 204d36f08..807be1b9b 100644 --- a/internal/cmd/commands/authtokenscmd/authtokens.gen.go +++ b/internal/cmd/commands/authtokenscmd/authtokens.gen.go @@ -165,7 +165,7 @@ func (c *Command) Run(args []string) int { var version uint32 - if ok := extraFlagsHandlingFunc(c, &opts); !ok { + if ok := extraFlagsHandlingFunc(c, f, &opts); !ok { return base.CommandUserError } @@ -279,7 +279,7 @@ var ( extraActionsFlagsMapFunc = func() map[string][]string { return nil } extraSynopsisFunc = func(*Command) string { return "" } extraFlagsFunc = func(*Command, *base.FlagSets, *base.FlagSet) {} - extraFlagsHandlingFunc = func(*Command, *[]authtokens.Option) bool { return true } + extraFlagsHandlingFunc = func(*Command, *base.FlagSets, *[]authtokens.Option) bool { return true } executeExtraActions = func(_ *Command, inResult api.GenericResult, inErr error, _ *authtokens.Client, _ uint32, _ []authtokens.Option) (api.GenericResult, error) { return inResult, inErr } diff --git a/internal/cmd/commands/database/funcs.go b/internal/cmd/commands/database/funcs.go index b2a8c0975..37bf801c7 100644 --- a/internal/cmd/commands/database/funcs.go +++ b/internal/cmd/commands/database/funcs.go @@ -67,6 +67,17 @@ func migrateDatabase(ctx context.Context, ui cli.Ui, dialect, u string, requireF if base.Format(ui) == "table" { ui.Info("Migrations successfully run.") } + migrationLogs, err := schema.GetMigrationLog(ctx, dBase) + if err != nil { + ui.Error(fmt.Errorf("Error retrieving database migration logs: %w", err).Error()) + return unlock, 2 + } + if len(migrationLogs) > 0 && base.Format(ui) == "table" { + ui.Info("Migration Logs...") + for _, e := range migrationLogs { + ui.Info(e.Entry) + } + } return unlock, 0 } diff --git a/internal/cmd/commands/database/init.go b/internal/cmd/commands/database/init.go index 3b9527be2..6b049d8b0 100644 --- a/internal/cmd/commands/database/init.go +++ b/internal/cmd/commands/database/init.go @@ -318,14 +318,14 @@ func (c *InitCommand) Run(args []string) (retCode int) { // Use an easy name, at least c.srv.DevLoginName = "admin" - am, user, err := c.srv.CreateInitialAuthMethod(c.Context) + am, user, err := c.srv.CreateInitialPasswordAuthMethod(c.Context) if err != nil { c.UI.Error(fmt.Errorf("Error creating initial auth method and user: %w", err).Error()) return base.CommandCliError } authMethodInfo := &AuthInfo{ - AuthMethodId: c.srv.DevAuthMethodId, + AuthMethodId: c.srv.DevPasswordAuthMethodId, AuthMethodName: am.Name, LoginName: c.srv.DevLoginName, Password: c.srv.DevPassword, diff --git a/internal/cmd/commands/dev/dev.go b/internal/cmd/commands/dev/dev.go index f16b974a1..5f9c65333 100644 --- a/internal/cmd/commands/dev/dev.go +++ b/internal/cmd/commands/dev/dev.go @@ -1,23 +1,30 @@ package dev import ( + "context" + "crypto/ed25519" "fmt" "net" + "net/url" "runtime" "strings" + "github.com/hashicorp/boundary/internal/auth/oidc" "github.com/hashicorp/boundary/internal/auth/password" "github.com/hashicorp/boundary/internal/cmd/base" "github.com/hashicorp/boundary/internal/cmd/config" + "github.com/hashicorp/boundary/internal/db" "github.com/hashicorp/boundary/internal/docker" "github.com/hashicorp/boundary/internal/host/static" "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/kms" "github.com/hashicorp/boundary/internal/servers/controller" "github.com/hashicorp/boundary/internal/servers/controller/handlers" "github.com/hashicorp/boundary/internal/servers/worker" "github.com/hashicorp/boundary/internal/target" "github.com/hashicorp/boundary/internal/types/scope" "github.com/hashicorp/boundary/sdk/strutil" + capoidc "github.com/hashicorp/cap/oidc" "github.com/mitchellh/cli" "github.com/posener/complete" ) @@ -39,6 +46,8 @@ type Command struct { controller *controller.Controller worker *worker.Worker + oidcSetup oidcSetup + flagLogLevel string flagLogFormat string flagCombineLogs bool @@ -274,11 +283,12 @@ func (c *Command) Run(args []string) int { c.UI.Error("Invalid ID suffix, must be exactly 10 characters") return base.CommandUserError } - if !handlers.ValidId("abc", "abc_"+c.flagIdSuffix) { + if !handlers.ValidId(handlers.Id("abc_"+c.flagIdSuffix), "abc") { c.UI.Error("Invalid ID suffix, must be in the set A-Za-z0-9") return base.CommandUserError } - c.DevAuthMethodId = fmt.Sprintf("%s_%s", password.AuthMethodPrefix, c.flagIdSuffix) + c.DevPasswordAuthMethodId = fmt.Sprintf("%s_%s", password.AuthMethodPrefix, c.flagIdSuffix) + c.DevOidcAuthMethodId = fmt.Sprintf("%s_%s", oidc.AuthMethodPrefix, c.flagIdSuffix) c.DevUserId = fmt.Sprintf("%s_%s", iam.UserPrefix, c.flagIdSuffix) c.DevUnprivilegedUserId = "u_" + strutil.Reverse(strings.TrimPrefix(c.DevUserId, "u_")) c.DevOrgId = fmt.Sprintf("%s_%s", scope.Org.Prefix(), c.flagIdSuffix) @@ -452,6 +462,11 @@ func (c *Command) Run(args []string) int { } } + if err := c.startDevOidcAuthMethod(); err != nil { + c.UI.Error(fmt.Errorf("Error starting dev OIDC auth method: %w", err).Error()) + return base.CommandCliError + } + c.PrintInfo(c.UI) c.ReleaseLogGate() @@ -532,3 +547,277 @@ func (c *Command) Run(args []string) int { return base.CommandSuccess } + +type oidcSetup struct { + clientId string + clientSecret oidc.ClientSecret + oidcPort int + callbackPort string + hostAddr string + authMethod *oidc.AuthMethod + pubKey []byte + privKey []byte + testProvider *capoidc.TestProvider + createUnpriv bool + callbackUrl *url.URL +} + +func (c *Command) startDevOidcAuthMethod() error { + var err error + + if c.DevOidcAuthMethodId == "" { + c.DevOidcAuthMethodId, err = db.NewPublicId(oidc.AuthMethodPrefix) + if err != nil { + return fmt.Errorf("error generating initial oidc auth method id: %w", err) + } + } + c.InfoKeys = append(c.InfoKeys, "generated oidc auth method id") + c.Info["generated oidc auth method id"] = c.DevOidcAuthMethodId + + switch { + case c.DevUnprivilegedLoginName == "", + c.DevUnprivilegedPassword == "", + c.DevUnprivilegedUserId == "": + + default: + c.oidcSetup.createUnpriv = true + } + + // Trawl through the listeners and find the api listener so we can use the + // same host name/IP + { + for _, lnConfig := range c.Config.Listeners { + purpose := strings.ToLower(lnConfig.Purpose[0]) + if purpose != "api" { + continue + } + c.oidcSetup.hostAddr, c.oidcSetup.callbackPort, err = net.SplitHostPort(lnConfig.Address) + if err != nil { + if strings.Contains(err.Error(), "missing port") { + c.oidcSetup.hostAddr = lnConfig.Address + // Use the default API port in the callback + c.oidcSetup.callbackPort = "9200" + } else { + return fmt.Errorf("error splitting host/port: %w", err) + } + } + } + if c.oidcSetup.hostAddr == "" { + return fmt.Errorf("could not determine address to use for built-in oidc dev listener") + } + } + + // Find an available port -- allocate one, then close the listener, and + // re-use it. This is a sort of hacky way to get around the chicken and egg + // of the auth method needing to know the discovery URL and the test + // provider needing to know the callback URL. + l, err := net.Listen("tcp", fmt.Sprintf("%s:0", c.oidcSetup.hostAddr)) + if err != nil { + return fmt.Errorf("error finding port for oidc test provider: %w", err) + } + c.oidcSetup.oidcPort = l.(*net.TCPListener).Addr().(*net.TCPAddr).Port + if err := l.Close(); err != nil { + return fmt.Errorf("error closing initial test port: %w", err) + } + c.oidcSetup.callbackUrl, err = url.Parse(fmt.Sprintf("http://%s:%s", c.oidcSetup.hostAddr, c.oidcSetup.callbackPort)) + if err != nil { + return fmt.Errorf("error parsing oidc test provider callback url: %w", err) + } + + // Generate initial IDs/keys + { + c.oidcSetup.clientId, err = capoidc.NewID() + if err != nil { + return fmt.Errorf("unable to generate client id: %w", err) + } + clientSecret, err := capoidc.NewID() + if err != nil { + return fmt.Errorf("unable to generate client secret: %w", err) + } + c.oidcSetup.clientSecret = oidc.ClientSecret(clientSecret) + c.oidcSetup.pubKey, c.oidcSetup.privKey, err = ed25519.GenerateKey(nil) + if err != nil { + return fmt.Errorf("unable to generate signing key: %w", err) + } + } + + // Create the subject information and testing provider + { + logger, err := capoidc.NewTestingLogger(c.Logger.Named("dev-oidc")) + if err != nil { + return fmt.Errorf("unable to create logger: %w", err) + } + + subInfo := map[string]*capoidc.TestSubject{ + c.DevLoginName: { + Password: c.DevPassword, + UserInfo: map[string]interface{}{ + "email": "admin@localhost", + "name": "Admin User", + }, + }, + } + if c.oidcSetup.createUnpriv { + subInfo[c.DevUnprivilegedLoginName] = &capoidc.TestSubject{ + Password: c.DevUnprivilegedPassword, + UserInfo: map[string]interface{}{ + "email": "user@localhost", + "name": "Unprivileged User", + }, + } + } + + clientSecret := string(c.oidcSetup.clientSecret) + + c.oidcSetup.testProvider = capoidc.StartTestProvider( + logger, + capoidc.WithNoTLS(), + capoidc.WithTestHost(c.oidcSetup.hostAddr), + capoidc.WithTestPort(c.oidcSetup.oidcPort), + capoidc.WithTestDefaults(&capoidc.TestProviderDefaults{ + CustomClaims: map[string]interface{}{ + "mode": "dev", + }, + SubjectInfo: subInfo, + SigningKey: &capoidc.TestSigningKey{ + PrivKey: ed25519.PrivateKey(c.oidcSetup.privKey), + PubKey: ed25519.PublicKey(c.oidcSetup.pubKey), + Alg: capoidc.EdDSA, + }, + AllowedRedirectURIs: []string{fmt.Sprintf("%s/v1/auth-methods/%s:authenticate:callback", c.oidcSetup.callbackUrl.String(), c.DevOidcAuthMethodId)}, + ClientID: &c.oidcSetup.clientId, + ClientSecret: &clientSecret, + })) + + c.ShutdownFuncs = append(c.ShutdownFuncs, func() error { + c.oidcSetup.testProvider.Stop() + return nil + }) + } + + // Create auth method and link accounts + { + c.oidcSetup.authMethod, err = c.createInitialOidcAuthMethod() + if err != nil { + return fmt.Errorf("error creating initial oidc auth method: %w", err) + } + } + + return nil +} + +func (c *Command) createInitialOidcAuthMethod() (*oidc.AuthMethod, error) { + rw := db.New(c.Database) + + kmsRepo, err := kms.NewRepository(rw, rw) + if err != nil { + return nil, fmt.Errorf("error creating kms repository: %w", err) + } + kmsCache, err := kms.NewKms(kmsRepo, kms.WithLogger(c.Logger.Named("kms"))) + if err != nil { + return nil, fmt.Errorf("error creating kms cache: %w", err) + } + if err := kmsCache.AddExternalWrappers( + kms.WithRootWrapper(c.RootKms), + ); err != nil { + return nil, fmt.Errorf("error adding config keys to kms: %w", err) + } + + discoveryUrl, err := url.Parse(fmt.Sprintf("http://%s:%d", c.oidcSetup.hostAddr, c.oidcSetup.oidcPort)) + if err != nil { + return nil, fmt.Errorf("error parsing oidc test provider address: %w", err) + } + + // Create the auth method + oidcRepo, err := oidc.NewRepository(rw, rw, kmsCache) + if err != nil { + return nil, fmt.Errorf("error creating oidc repo: %w", err) + } + + authMethod, err := oidc.NewAuthMethod( + scope.Global.String(), + c.oidcSetup.clientId, + c.oidcSetup.clientSecret, + oidc.WithName("Generated global scope initial oidc auth method"), + oidc.WithDescription("Provides initial administrative and unprivileged authentication into Boundary"), + oidc.WithIssuer(discoveryUrl), + oidc.WithApiUrl(c.oidcSetup.callbackUrl), + oidc.WithSigningAlgs(oidc.EdDSA), + oidc.WithOperationalState(oidc.ActivePublicState)) + if err != nil { + return nil, fmt.Errorf("error creating new in memory oidc auth method: %w", err) + } + if c.DevOidcAuthMethodId == "" { + c.DevOidcAuthMethodId, err = db.NewPublicId(oidc.AuthMethodPrefix) + if err != nil { + return nil, fmt.Errorf("error generating initial oidc auth method id: %w", err) + } + } + + cancelCtx, cancel := context.WithCancel(c.Context) + defer cancel() + go func() { + select { + case <-c.ShutdownCh: + cancel() + case <-cancelCtx.Done(): + } + }() + + c.oidcSetup.authMethod, err = oidcRepo.CreateAuthMethod( + cancelCtx, + authMethod, + oidc.WithPublicId(c.DevOidcAuthMethodId)) + if err != nil { + return nil, fmt.Errorf("error saving oidc auth method to the db: %w", err) + } + + // Create accounts + { + createAndLinkAccount := func(loginName, userId, typ string) error { + acct, err := oidc.NewAccount( + c.oidcSetup.authMethod.GetPublicId(), + loginName, + oidc.WithDescription(fmt.Sprintf("Initial %s OIDC account", typ)), + ) + if err != nil { + return fmt.Errorf("error generating %s oidc account: %w", typ, err) + } + acct, err = oidcRepo.CreateAccount( + cancelCtx, + c.oidcSetup.authMethod.GetScopeId(), + acct, + ) + if err != nil { + return fmt.Errorf("error creating %s oidc account: %w", typ, err) + } + + // Link accounts to existing user + iamRepo, err := iam.NewRepository(rw, rw, kmsCache) + if err != nil { + return fmt.Errorf("unable to create iam repo: %w", err) + } + + u, _, err := iamRepo.LookupUser(cancelCtx, userId) + if err != nil { + return fmt.Errorf("error looking up %s user: %w", typ, err) + } + if _, err = iamRepo.AddUserAccounts(cancelCtx, u.GetPublicId(), u.GetVersion(), []string{acct.GetPublicId()}); err != nil { + return fmt.Errorf("error associating initial %s user with account: %w", typ, err) + } + + return nil + } + + if err := createAndLinkAccount(c.DevLoginName, c.DevUserId, "admin"); err != nil { + return nil, err + } + if c.oidcSetup.createUnpriv { + if err := createAndLinkAccount(c.DevUnprivilegedLoginName, c.DevUnprivilegedUserId, "unprivileged"); err != nil { + return nil, err + } + } + } + + return nil, nil +} diff --git a/internal/cmd/commands/groupscmd/funcs.go b/internal/cmd/commands/groupscmd/funcs.go index b69ff240b..97bbf7f80 100644 --- a/internal/cmd/commands/groupscmd/funcs.go +++ b/internal/cmd/commands/groupscmd/funcs.go @@ -105,7 +105,7 @@ func extraFlagsFuncImpl(c *Command, _ *base.FlagSets, f *base.FlagSet) { } } -func extraFlagsHandlingFuncImpl(c *Command, opts *[]groups.Option) bool { +func extraFlagsHandlingFuncImpl(c *Command, _ *base.FlagSets, opts *[]groups.Option) bool { switch c.Func { case "add-members", "remove-members": if len(c.flagMembers) == 0 { diff --git a/internal/cmd/commands/groupscmd/groups.gen.go b/internal/cmd/commands/groupscmd/groups.gen.go index ddf26b74c..0d3b3e110 100644 --- a/internal/cmd/commands/groupscmd/groups.gen.go +++ b/internal/cmd/commands/groupscmd/groups.gen.go @@ -237,7 +237,7 @@ func (c *Command) Run(args []string) int { } - if ok := extraFlagsHandlingFunc(c, &opts); !ok { + if ok := extraFlagsHandlingFunc(c, f, &opts); !ok { return base.CommandUserError } @@ -357,7 +357,7 @@ var ( extraActionsFlagsMapFunc = func() map[string][]string { return nil } extraSynopsisFunc = func(*Command) string { return "" } extraFlagsFunc = func(*Command, *base.FlagSets, *base.FlagSet) {} - extraFlagsHandlingFunc = func(*Command, *[]groups.Option) bool { return true } + extraFlagsHandlingFunc = func(*Command, *base.FlagSets, *[]groups.Option) bool { return true } executeExtraActions = func(_ *Command, inResult api.GenericResult, inErr error, _ *groups.Client, _ uint32, _ []groups.Option) (api.GenericResult, error) { return inResult, inErr } diff --git a/internal/cmd/commands/hostcatalogscmd/hostcatalogs.gen.go b/internal/cmd/commands/hostcatalogscmd/hostcatalogs.gen.go index 4cf61aba7..5ba1cbeb4 100644 --- a/internal/cmd/commands/hostcatalogscmd/hostcatalogs.gen.go +++ b/internal/cmd/commands/hostcatalogscmd/hostcatalogs.gen.go @@ -169,7 +169,7 @@ func (c *Command) Run(args []string) int { var version uint32 - if ok := extraFlagsHandlingFunc(c, &opts); !ok { + if ok := extraFlagsHandlingFunc(c, f, &opts); !ok { return base.CommandUserError } @@ -283,7 +283,7 @@ var ( extraActionsFlagsMapFunc = func() map[string][]string { return nil } extraSynopsisFunc = func(*Command) string { return "" } extraFlagsFunc = func(*Command, *base.FlagSets, *base.FlagSet) {} - extraFlagsHandlingFunc = func(*Command, *[]hostcatalogs.Option) bool { return true } + extraFlagsHandlingFunc = func(*Command, *base.FlagSets, *[]hostcatalogs.Option) bool { return true } executeExtraActions = func(_ *Command, inResult api.GenericResult, inErr error, _ *hostcatalogs.Client, _ uint32, _ []hostcatalogs.Option) (api.GenericResult, error) { return inResult, inErr } diff --git a/internal/cmd/commands/hostcatalogscmd/static_hostcatalogs.gen.go b/internal/cmd/commands/hostcatalogscmd/static_hostcatalogs.gen.go index 8e77b2b01..12faa906f 100644 --- a/internal/cmd/commands/hostcatalogscmd/static_hostcatalogs.gen.go +++ b/internal/cmd/commands/hostcatalogscmd/static_hostcatalogs.gen.go @@ -178,7 +178,7 @@ func (c *StaticCommand) Run(args []string) int { } } - if ok := extraStaticFlagsHandlingFunc(c, &opts); !ok { + if ok := extraStaticFlagsHandlingFunc(c, f, &opts); !ok { return base.CommandUserError } @@ -235,7 +235,7 @@ var ( extraStaticActionsFlagsMapFunc = func() map[string][]string { return nil } extraStaticSynopsisFunc = func(*StaticCommand) string { return "" } extraStaticFlagsFunc = func(*StaticCommand, *base.FlagSets, *base.FlagSet) {} - extraStaticFlagsHandlingFunc = func(*StaticCommand, *[]hostcatalogs.Option) bool { return true } + extraStaticFlagsHandlingFunc = func(*StaticCommand, *base.FlagSets, *[]hostcatalogs.Option) bool { return true } executeExtraStaticActions = func(_ *StaticCommand, inResult api.GenericResult, inErr error, _ *hostcatalogs.Client, _ uint32, _ []hostcatalogs.Option) (api.GenericResult, error) { return inResult, inErr } diff --git a/internal/cmd/commands/hostscmd/hosts.gen.go b/internal/cmd/commands/hostscmd/hosts.gen.go index f05ed727f..27f8e7c27 100644 --- a/internal/cmd/commands/hostscmd/hosts.gen.go +++ b/internal/cmd/commands/hostscmd/hosts.gen.go @@ -180,7 +180,7 @@ func (c *Command) Run(args []string) int { var version uint32 - if ok := extraFlagsHandlingFunc(c, &opts); !ok { + if ok := extraFlagsHandlingFunc(c, f, &opts); !ok { return base.CommandUserError } @@ -294,7 +294,7 @@ var ( extraActionsFlagsMapFunc = func() map[string][]string { return nil } extraSynopsisFunc = func(*Command) string { return "" } extraFlagsFunc = func(*Command, *base.FlagSets, *base.FlagSet) {} - extraFlagsHandlingFunc = func(*Command, *[]hosts.Option) bool { return true } + extraFlagsHandlingFunc = func(*Command, *base.FlagSets, *[]hosts.Option) bool { return true } executeExtraActions = func(_ *Command, inResult api.GenericResult, inErr error, _ *hosts.Client, _ uint32, _ []hosts.Option) (api.GenericResult, error) { return inResult, inErr } diff --git a/internal/cmd/commands/hostscmd/static_funcs.go b/internal/cmd/commands/hostscmd/static_funcs.go index ffa421076..0ccc51c75 100644 --- a/internal/cmd/commands/hostscmd/static_funcs.go +++ b/internal/cmd/commands/hostscmd/static_funcs.go @@ -68,7 +68,7 @@ func extraStaticFlagsFuncImpl(c *StaticCommand, set *base.FlagSets, _ *base.Flag } } -func extraStaticFlagsHandlingFuncImpl(c *StaticCommand, opts *[]hosts.Option) bool { +func extraStaticFlagsHandlingFuncImpl(c *StaticCommand, _ *base.FlagSets, opts *[]hosts.Option) bool { if c.Func == "create" && c.flagAddress == "" { c.UI.Error("Address must be provided via -address") return false diff --git a/internal/cmd/commands/hostscmd/static_hosts.gen.go b/internal/cmd/commands/hostscmd/static_hosts.gen.go index 602ef3442..0c53acedb 100644 --- a/internal/cmd/commands/hostscmd/static_hosts.gen.go +++ b/internal/cmd/commands/hostscmd/static_hosts.gen.go @@ -175,7 +175,7 @@ func (c *StaticCommand) Run(args []string) int { } } - if ok := extraStaticFlagsHandlingFunc(c, &opts); !ok { + if ok := extraStaticFlagsHandlingFunc(c, f, &opts); !ok { return base.CommandUserError } @@ -232,7 +232,7 @@ var ( extraStaticActionsFlagsMapFunc = func() map[string][]string { return nil } extraStaticSynopsisFunc = func(*StaticCommand) string { return "" } extraStaticFlagsFunc = func(*StaticCommand, *base.FlagSets, *base.FlagSet) {} - extraStaticFlagsHandlingFunc = func(*StaticCommand, *[]hosts.Option) bool { return true } + extraStaticFlagsHandlingFunc = func(*StaticCommand, *base.FlagSets, *[]hosts.Option) bool { return true } executeExtraStaticActions = func(_ *StaticCommand, inResult api.GenericResult, inErr error, _ *hosts.Client, _ uint32, _ []hosts.Option) (api.GenericResult, error) { return inResult, inErr } diff --git a/internal/cmd/commands/hostsetscmd/funcs.go b/internal/cmd/commands/hostsetscmd/funcs.go index 773b8351d..dffb0c4c7 100644 --- a/internal/cmd/commands/hostsetscmd/funcs.go +++ b/internal/cmd/commands/hostsetscmd/funcs.go @@ -137,7 +137,7 @@ func extraFlagsFuncImpl(c *Command, _ *base.FlagSets, f *base.FlagSet) { } } -func extraFlagsHandlingFuncImpl(c *Command, opts *[]hostsets.Option) bool { +func extraFlagsHandlingFuncImpl(c *Command, _ *base.FlagSets, opts *[]hostsets.Option) bool { switch c.Func { case "add-hosts", "remove-hosts": if len(c.flagHosts) == 0 { diff --git a/internal/cmd/commands/hostsetscmd/hostsets.gen.go b/internal/cmd/commands/hostsetscmd/hostsets.gen.go index 47e80bbc1..5a19c07bf 100644 --- a/internal/cmd/commands/hostsetscmd/hostsets.gen.go +++ b/internal/cmd/commands/hostsetscmd/hostsets.gen.go @@ -210,7 +210,7 @@ func (c *Command) Run(args []string) int { } - if ok := extraFlagsHandlingFunc(c, &opts); !ok { + if ok := extraFlagsHandlingFunc(c, f, &opts); !ok { return base.CommandUserError } @@ -324,7 +324,7 @@ var ( extraActionsFlagsMapFunc = func() map[string][]string { return nil } extraSynopsisFunc = func(*Command) string { return "" } extraFlagsFunc = func(*Command, *base.FlagSets, *base.FlagSet) {} - extraFlagsHandlingFunc = func(*Command, *[]hostsets.Option) bool { return true } + extraFlagsHandlingFunc = func(*Command, *base.FlagSets, *[]hostsets.Option) bool { return true } executeExtraActions = func(_ *Command, inResult api.GenericResult, inErr error, _ *hostsets.Client, _ uint32, _ []hostsets.Option) (api.GenericResult, error) { return inResult, inErr } diff --git a/internal/cmd/commands/hostsetscmd/static_hostsets.gen.go b/internal/cmd/commands/hostsetscmd/static_hostsets.gen.go index 99d77cdc4..6a2c382f4 100644 --- a/internal/cmd/commands/hostsetscmd/static_hostsets.gen.go +++ b/internal/cmd/commands/hostsetscmd/static_hostsets.gen.go @@ -173,7 +173,7 @@ func (c *StaticCommand) Run(args []string) int { } } - if ok := extraStaticFlagsHandlingFunc(c, &opts); !ok { + if ok := extraStaticFlagsHandlingFunc(c, f, &opts); !ok { return base.CommandUserError } @@ -230,7 +230,7 @@ var ( extraStaticActionsFlagsMapFunc = func() map[string][]string { return nil } extraStaticSynopsisFunc = func(*StaticCommand) string { return "" } extraStaticFlagsFunc = func(*StaticCommand, *base.FlagSets, *base.FlagSet) {} - extraStaticFlagsHandlingFunc = func(*StaticCommand, *[]hostsets.Option) bool { return true } + extraStaticFlagsHandlingFunc = func(*StaticCommand, *base.FlagSets, *[]hostsets.Option) bool { return true } executeExtraStaticActions = func(_ *StaticCommand, inResult api.GenericResult, inErr error, _ *hostsets.Client, _ uint32, _ []hostsets.Option) (api.GenericResult, error) { return inResult, inErr } diff --git a/internal/cmd/commands/rolescmd/funcs.go b/internal/cmd/commands/rolescmd/funcs.go index 6d5f9c136..74a52adaf 100644 --- a/internal/cmd/commands/rolescmd/funcs.go +++ b/internal/cmd/commands/rolescmd/funcs.go @@ -167,7 +167,7 @@ func extraFlagsFuncImpl(c *Command, _ *base.FlagSets, f *base.FlagSet) { } } -func extraFlagsHandlingFuncImpl(c *Command, opts *[]roles.Option) bool { +func extraFlagsHandlingFuncImpl(c *Command, _ *base.FlagSets, opts *[]roles.Option) bool { switch c.flagGrantScopeId { case "": case "null": diff --git a/internal/cmd/commands/rolescmd/roles.gen.go b/internal/cmd/commands/rolescmd/roles.gen.go index d0f4bec90..4d240ff9f 100644 --- a/internal/cmd/commands/rolescmd/roles.gen.go +++ b/internal/cmd/commands/rolescmd/roles.gen.go @@ -261,7 +261,7 @@ func (c *Command) Run(args []string) int { } - if ok := extraFlagsHandlingFunc(c, &opts); !ok { + if ok := extraFlagsHandlingFunc(c, f, &opts); !ok { return base.CommandUserError } @@ -381,7 +381,7 @@ var ( extraActionsFlagsMapFunc = func() map[string][]string { return nil } extraSynopsisFunc = func(*Command) string { return "" } extraFlagsFunc = func(*Command, *base.FlagSets, *base.FlagSet) {} - extraFlagsHandlingFunc = func(*Command, *[]roles.Option) bool { return true } + extraFlagsHandlingFunc = func(*Command, *base.FlagSets, *[]roles.Option) bool { return true } executeExtraActions = func(_ *Command, inResult api.GenericResult, inErr error, _ *roles.Client, _ uint32, _ []roles.Option) (api.GenericResult, error) { return inResult, inErr } diff --git a/internal/cmd/commands/scopescmd/funcs.go b/internal/cmd/commands/scopescmd/funcs.go index 4660b9122..33d1ac081 100644 --- a/internal/cmd/commands/scopescmd/funcs.go +++ b/internal/cmd/commands/scopescmd/funcs.go @@ -9,6 +9,12 @@ import ( "github.com/hashicorp/boundary/internal/cmd/base" ) +const ( + flagPrimaryAuthMethodIdName = "primary-auth-method-id" + flagSkipAdminRoleCreationName = "skip-admin-role-creation" + flagSkipDefaultRoleCreationName = "skip-default-role-creation" +) + func init() { extraActionsFlagsMapFunc = extraActionsFlagsMapFuncImpl extraFlagsFunc = extraFlagsFuncImpl @@ -17,41 +23,52 @@ func init() { func extraActionsFlagsMapFuncImpl() map[string][]string { return map[string][]string{ - "create": {"skip-admin-role-creation", "skip-default-role-creation"}, + "create": {flagSkipAdminRoleCreationName, flagSkipDefaultRoleCreationName}, + "update": {flagPrimaryAuthMethodIdName}, } } type extraCmdVars struct { flagSkipAdminRoleCreation bool flagSkipDefaultRoleCreation bool + flagPrimaryAuthMethodId string } func extraFlagsFuncImpl(c *Command, set *base.FlagSets, f *base.FlagSet) { for _, name := range flagsMap[c.Func] { switch name { - case "skip-admin-role-creation": + case flagSkipAdminRoleCreationName: f.BoolVar(&base.BoolVar{ - Name: "skip-admin-role-creation", + Name: flagSkipAdminRoleCreationName, Target: &c.flagSkipAdminRoleCreation, Usage: "If set, a role granting the current user access to administer the newly-created scope will not automatically be created", }) - case "skip-default-role-creation": + case flagSkipDefaultRoleCreationName: f.BoolVar(&base.BoolVar{ - Name: "skip-default-role-creation", + Name: flagSkipDefaultRoleCreationName, Target: &c.flagSkipDefaultRoleCreation, Usage: "If set, a role granting the anonymous user access to log into auth methods and a few other actions within the newly-created scope will not automatically be created", }) + case flagPrimaryAuthMethodIdName: + f.StringVar(&base.StringVar{ + Name: flagPrimaryAuthMethodIdName, + Target: &c.flagPrimaryAuthMethodId, + Usage: "If set, the primary auth method id for the scope. A primary auth method is allowed to create users on first login and is also used as a source for account full name and email for a scope's users", + }) } } } -func extraFlagsHandlingFuncImpl(c *Command, opts *[]scopes.Option) bool { +func extraFlagsHandlingFuncImpl(c *Command, _ *base.FlagSets, opts *[]scopes.Option) bool { if c.flagSkipAdminRoleCreation { *opts = append(*opts, scopes.WithSkipAdminRoleCreation(c.flagSkipAdminRoleCreation)) } if c.flagSkipDefaultRoleCreation { *opts = append(*opts, scopes.WithSkipDefaultRoleCreation(c.flagSkipDefaultRoleCreation)) } + if c.flagPrimaryAuthMethodId != "" { + *opts = append(*opts, scopes.WithPrimaryAuthMethodId(c.flagPrimaryAuthMethodId)) + } return true } @@ -94,6 +111,11 @@ func (c *Command) printListTable(items []*scopes.Scope) string { fmt.Sprintf(" Description: %s", item.Description), ) } + if item.PrimaryAuthMethodId != "" { + output = append(output, + fmt.Sprintf(" PrimaryAuthMethodId: %s", item.PrimaryAuthMethodId), + ) + } if len(item.AuthorizedActions) > 0 { output = append(output, " Authorized Actions:", @@ -119,6 +141,9 @@ func printItemTable(in *scopes.Scope) string { if in.Description != "" { nonAttributeMap["Description"] = in.Description } + if in.PrimaryAuthMethodId != "" { + nonAttributeMap[flagPrimaryAuthMethodIdName] = in.PrimaryAuthMethodId + } maxLength := base.MaxAttributesLength(nonAttributeMap, nil, nil) diff --git a/internal/cmd/commands/scopescmd/scopes.gen.go b/internal/cmd/commands/scopescmd/scopes.gen.go index 66a91ea26..0d15fb770 100644 --- a/internal/cmd/commands/scopescmd/scopes.gen.go +++ b/internal/cmd/commands/scopescmd/scopes.gen.go @@ -211,7 +211,7 @@ func (c *Command) Run(args []string) int { } } - if ok := extraFlagsHandlingFunc(c, &opts); !ok { + if ok := extraFlagsHandlingFunc(c, f, &opts); !ok { return base.CommandUserError } @@ -331,7 +331,7 @@ var ( extraActionsFlagsMapFunc = func() map[string][]string { return nil } extraSynopsisFunc = func(*Command) string { return "" } extraFlagsFunc = func(*Command, *base.FlagSets, *base.FlagSet) {} - extraFlagsHandlingFunc = func(*Command, *[]scopes.Option) bool { return true } + extraFlagsHandlingFunc = func(*Command, *base.FlagSets, *[]scopes.Option) bool { return true } executeExtraActions = func(_ *Command, inResult api.GenericResult, inErr error, _ *scopes.Client, _ uint32, _ []scopes.Option) (api.GenericResult, error) { return inResult, inErr } diff --git a/internal/cmd/commands/sessionscmd/sessions.gen.go b/internal/cmd/commands/sessionscmd/sessions.gen.go index 7a16e39e6..ca6fec876 100644 --- a/internal/cmd/commands/sessionscmd/sessions.gen.go +++ b/internal/cmd/commands/sessionscmd/sessions.gen.go @@ -169,7 +169,7 @@ func (c *Command) Run(args []string) int { } } - if ok := extraFlagsHandlingFunc(c, &opts); !ok { + if ok := extraFlagsHandlingFunc(c, f, &opts); !ok { return base.CommandUserError } @@ -254,7 +254,7 @@ var ( extraActionsFlagsMapFunc = func() map[string][]string { return nil } extraSynopsisFunc = func(*Command) string { return "" } extraFlagsFunc = func(*Command, *base.FlagSets, *base.FlagSet) {} - extraFlagsHandlingFunc = func(*Command, *[]sessions.Option) bool { return true } + extraFlagsHandlingFunc = func(*Command, *base.FlagSets, *[]sessions.Option) bool { return true } executeExtraActions = func(_ *Command, inResult api.GenericResult, inErr error, _ *sessions.Client, _ uint32, _ []sessions.Option) (api.GenericResult, error) { return inResult, inErr } diff --git a/internal/cmd/commands/targetscmd/funcs.go b/internal/cmd/commands/targetscmd/funcs.go index 031840db6..7ed2df69d 100644 --- a/internal/cmd/commands/targetscmd/funcs.go +++ b/internal/cmd/commands/targetscmd/funcs.go @@ -203,7 +203,7 @@ func extraFlagsFuncImpl(c *Command, _ *base.FlagSets, f *base.FlagSet) { } } -func extraFlagsHandlingFuncImpl(c *Command, opts *[]targets.Option) bool { +func extraFlagsHandlingFuncImpl(c *Command, _ *base.FlagSets, opts *[]targets.Option) bool { // This is custom logic because of the authorized-session handling. If we // support all resources to be looked up by name + scope info we can // eventually graduate this out to the main template. diff --git a/internal/cmd/commands/targetscmd/targets.gen.go b/internal/cmd/commands/targetscmd/targets.gen.go index bce3b013f..880a35238 100644 --- a/internal/cmd/commands/targetscmd/targets.gen.go +++ b/internal/cmd/commands/targetscmd/targets.gen.go @@ -216,7 +216,7 @@ func (c *Command) Run(args []string) int { } - if ok := extraFlagsHandlingFunc(c, &opts); !ok { + if ok := extraFlagsHandlingFunc(c, f, &opts); !ok { return base.CommandUserError } @@ -330,7 +330,7 @@ var ( extraActionsFlagsMapFunc = func() map[string][]string { return nil } extraSynopsisFunc = func(*Command) string { return "" } extraFlagsFunc = func(*Command, *base.FlagSets, *base.FlagSet) {} - extraFlagsHandlingFunc = func(*Command, *[]targets.Option) bool { return true } + extraFlagsHandlingFunc = func(*Command, *base.FlagSets, *[]targets.Option) bool { return true } executeExtraActions = func(_ *Command, inResult api.GenericResult, inErr error, _ *targets.Client, _ uint32, _ []targets.Option) (api.GenericResult, error) { return inResult, inErr } diff --git a/internal/cmd/commands/targetscmd/tcp_funcs.go b/internal/cmd/commands/targetscmd/tcp_funcs.go index f54785557..57dce71c7 100644 --- a/internal/cmd/commands/targetscmd/tcp_funcs.go +++ b/internal/cmd/commands/targetscmd/tcp_funcs.go @@ -91,7 +91,7 @@ func extraTcpFlagsFuncImpl(c *TcpCommand, set *base.FlagSets, f *base.FlagSet) { } } -func extraTcpFlagsHandlingFuncImpl(c *TcpCommand, opts *[]targets.Option) bool { +func extraTcpFlagsHandlingFuncImpl(c *TcpCommand, _ *base.FlagSets, opts *[]targets.Option) bool { switch c.flagDefaultPort { case "": case "null": diff --git a/internal/cmd/commands/targetscmd/tcp_targets.gen.go b/internal/cmd/commands/targetscmd/tcp_targets.gen.go index 55f199804..a8ba7237d 100644 --- a/internal/cmd/commands/targetscmd/tcp_targets.gen.go +++ b/internal/cmd/commands/targetscmd/tcp_targets.gen.go @@ -180,7 +180,7 @@ func (c *TcpCommand) Run(args []string) int { } } - if ok := extraTcpFlagsHandlingFunc(c, &opts); !ok { + if ok := extraTcpFlagsHandlingFunc(c, f, &opts); !ok { return base.CommandUserError } @@ -237,7 +237,7 @@ var ( extraTcpActionsFlagsMapFunc = func() map[string][]string { return nil } extraTcpSynopsisFunc = func(*TcpCommand) string { return "" } extraTcpFlagsFunc = func(*TcpCommand, *base.FlagSets, *base.FlagSet) {} - extraTcpFlagsHandlingFunc = func(*TcpCommand, *[]targets.Option) bool { return true } + extraTcpFlagsHandlingFunc = func(*TcpCommand, *base.FlagSets, *[]targets.Option) bool { return true } executeExtraTcpActions = func(_ *TcpCommand, inResult api.GenericResult, inErr error, _ *targets.Client, _ uint32, _ []targets.Option) (api.GenericResult, error) { return inResult, inErr } diff --git a/internal/cmd/commands/userscmd/funcs.go b/internal/cmd/commands/userscmd/funcs.go index 57fc3a091..3644f9040 100644 --- a/internal/cmd/commands/userscmd/funcs.go +++ b/internal/cmd/commands/userscmd/funcs.go @@ -104,7 +104,7 @@ func extraFlagsFuncImpl(c *Command, _ *base.FlagSets, f *base.FlagSet) { } } -func extraFlagsHandlingFuncImpl(c *Command, opts *[]users.Option) bool { +func extraFlagsHandlingFuncImpl(c *Command, _ *base.FlagSets, opts *[]users.Option) bool { switch c.Func { case "add-accounts", "remove-accounts": if len(c.flagAccounts) == 0 { diff --git a/internal/cmd/commands/userscmd/users.gen.go b/internal/cmd/commands/userscmd/users.gen.go index 8a158c558..f0bf6c42c 100644 --- a/internal/cmd/commands/userscmd/users.gen.go +++ b/internal/cmd/commands/userscmd/users.gen.go @@ -237,7 +237,7 @@ func (c *Command) Run(args []string) int { } - if ok := extraFlagsHandlingFunc(c, &opts); !ok { + if ok := extraFlagsHandlingFunc(c, f, &opts); !ok { return base.CommandUserError } @@ -357,7 +357,7 @@ var ( extraActionsFlagsMapFunc = func() map[string][]string { return nil } extraSynopsisFunc = func(*Command) string { return "" } extraFlagsFunc = func(*Command, *base.FlagSets, *base.FlagSet) {} - extraFlagsHandlingFunc = func(*Command, *[]users.Option) bool { return true } + extraFlagsHandlingFunc = func(*Command, *base.FlagSets, *[]users.Option) bool { return true } executeExtraActions = func(_ *Command, inResult api.GenericResult, inErr error, _ *users.Client, _ uint32, _ []users.Option) (api.GenericResult, error) { return inResult, inErr } diff --git a/internal/cmd/gencli/input.go b/internal/cmd/gencli/input.go index 181d350a2..3a9b7720b 100644 --- a/internal/cmd/gencli/input.go +++ b/internal/cmd/gencli/input.go @@ -93,6 +93,20 @@ var inputStructs = map[string][]*cmdInfo{ HasDescription: true, VersionedActions: []string{"update"}, }, + { + ResourceType: resource.Account.String(), + Pkg: "accounts", + StdActions: []string{"create", "update"}, + SubActionPrefix: "oidc", + HasExtraCommandVars: true, + SkipNormalHelp: true, + HasExtraHelpFunc: true, + HasId: true, + HasName: true, + Container: "AuthMethod", + HasDescription: true, + VersionedActions: []string{"update"}, + }, }, "authmethods": { { @@ -119,6 +133,21 @@ var inputStructs = map[string][]*cmdInfo{ VersionedActions: []string{"update"}, NeedsSubTypeInCreate: true, }, + { + ResourceType: resource.AuthMethod.String(), + Pkg: "authmethods", + StdActions: []string{"create", "update"}, + SubActionPrefix: "oidc", + HasExtraCommandVars: true, + SkipNormalHelp: true, + HasExtraHelpFunc: true, + HasId: true, + HasName: true, + HasDescription: true, + Container: "Scope", + VersionedActions: []string{"update", "change-state"}, + NeedsSubTypeInCreate: true, + }, }, "authtokens": { { diff --git a/internal/cmd/gencli/templates.go b/internal/cmd/gencli/templates.go index 054966163..29df4bf3f 100644 --- a/internal/cmd/gencli/templates.go +++ b/internal/cmd/gencli/templates.go @@ -323,7 +323,7 @@ func (c *{{ camelCase .SubActionPrefix }}Command) Run(args []string) int { } {{ end }} - if ok := extra{{ camelCase .SubActionPrefix }}FlagsHandlingFunc(c, &opts); !ok { + if ok := extra{{ camelCase .SubActionPrefix }}FlagsHandlingFunc(c, f, &opts); !ok { return base.CommandUserError } @@ -455,7 +455,7 @@ var ( extra{{ camelCase .SubActionPrefix }}ActionsFlagsMapFunc = func() map[string][]string { return nil } extra{{ camelCase .SubActionPrefix }}SynopsisFunc = func(*{{ camelCase .SubActionPrefix }}Command) string { return "" } extra{{ camelCase .SubActionPrefix }}FlagsFunc = func(*{{ camelCase .SubActionPrefix }}Command, *base.FlagSets, *base.FlagSet) {} - extra{{ camelCase .SubActionPrefix }}FlagsHandlingFunc = func(*{{ camelCase .SubActionPrefix }}Command, *[]{{ .Pkg }}.Option) bool { return true } + extra{{ camelCase .SubActionPrefix }}FlagsHandlingFunc = func(*{{ camelCase .SubActionPrefix }}Command, *base.FlagSets, *[]{{ .Pkg }}.Option) bool { return true } executeExtra{{ camelCase .SubActionPrefix }}Actions = func(_ *{{ camelCase .SubActionPrefix }}Command, inResult api.GenericResult, inErr error, _ *{{ .Pkg }}.Client, _ uint32, _ []{{ .Pkg }}.Option) (api.GenericResult, error) { return inResult, inErr } diff --git a/internal/db/read_writer.go b/internal/db/read_writer.go index e8461c476..78249a9b3 100644 --- a/internal/db/read_writer.go +++ b/internal/db/read_writer.go @@ -120,6 +120,13 @@ type Writer interface { // supported. Exec(ctx context.Context, sql string, values []interface{}, opt ...Option) (int, error) + // Query will run the raw query and return the *sql.Rows results. Query will + // operate within the context of any ongoing transaction for the db.Writer. The + // caller must close the returned *sql.Rows. Query can/should be used in + // combination with ScanRows. Query is included in the Writer interface + // so callers can execute updates and inserts with returning values. + Query(ctx context.Context, sql string, values []interface{}, opt ...Option) (*sql.Rows, error) + // GetTicket returns an oplog ticket for the aggregate root of "i" which can // be used to WriteOplogEntryWith for that aggregate root. GetTicket(i interface{}) (*store.Ticket, error) diff --git a/internal/db/schema/manager.go b/internal/db/schema/manager.go index 71dd2aa86..5422f870f 100644 --- a/internal/db/schema/manager.go +++ b/internal/db/schema/manager.go @@ -6,6 +6,7 @@ import ( "database/sql" "fmt" "io" + "time" "github.com/hashicorp/boundary/internal/db/schema/postgres" "github.com/hashicorp/boundary/internal/errors" @@ -38,16 +39,25 @@ type driver interface { // the underlying boundary database schema. // Manager is not thread safe. type Manager struct { - db *sql.DB - driver driver - dialect string + db *sql.DB + driver driver + dialect string + migrationStates map[string]migrationState } // NewManager creates a new schema manager. An error is returned // if the provided dialect is unrecognized or if the passed in db is unreachable. -func NewManager(ctx context.Context, dialect string, db *sql.DB) (*Manager, error) { +func NewManager(ctx context.Context, dialect string, db *sql.DB, opt ...Option) (*Manager, error) { const op = "schema.NewManager" dbM := Manager{db: db, dialect: dialect} + opts := getOpts(opt...) + if opts.withMigrationStates != nil { + dbM.migrationStates = opts.withMigrationStates + } else { + // intentionally set it to the reference, so changes to the global var + // will be reflected in the manager instance + dbM.migrationStates = migrationStates + } switch dialect { case "postgres": var err error @@ -144,7 +154,11 @@ func (b *Manager) RollForward(ctx context.Context) error { return errors.Wrap(err, op) } defer func() { - b.driver.Unlock(ctx) + if err := b.driver.Unlock(ctx); err != nil { + // I'm not sure this is ideal, but we have to rollback the current + // transaction if we're unable to release the lock + panic(errors.Wrap(err, op)) + } }() curVersion, _, dirty, err := b.driver.CurrentState(ctx) @@ -156,7 +170,7 @@ func (b *Manager) RollForward(ctx context.Context) error { return errors.New(errors.NotSpecificIntegrity, op, fmt.Sprintf("schema is dirty with version %d", curVersion)) } - if err = b.runMigrations(ctx, newStatementProvider(b.dialect, curVersion)); err != nil { + if err = b.runMigrations(ctx, newStatementProvider(b.dialect, curVersion, WithMigrationStates(b.migrationStates))); err != nil { return errors.Wrap(err, op) } return nil @@ -200,3 +214,48 @@ func (b *Manager) runMigrations(ctx context.Context, qp *statementProvider) erro } return nil } + +// LogEntry represents a log entry generated during migrations. +type LogEntry struct { + Id int + MigrationVersion string + CreateTime time.Time + Entry string +} + +// GetMigrationLog will retrieve the migration logs from the db for the last +// migration. Once it's read the entries, it will delete them from the database. +// The WithDeleteLog option is supported and will remove all log entries when provided. +func GetMigrationLog(ctx context.Context, d *sql.DB, opt ...Option) ([]LogEntry, error) { + const op = "schema.GetMigrationLog" + const sql = "select id, create_time, migration_version, entry from log_migration where migration_version in (select max(version) from boundary_schema_version)" + if d == nil { + return nil, errors.New(errors.InvalidParameter, op, "missing sql db") + } + rows, err := d.QueryContext(ctx, sql) + if err != nil { + return nil, errors.Wrap(err, op) + } + defer rows.Close() + + var entries []LogEntry + for rows.Next() { + var e LogEntry + if err := rows.Scan(&e.Id, &e.CreateTime, &e.MigrationVersion, &e.Entry); err != nil { + return nil, errors.Wrap(err, op) + } + entries = append(entries, e) + } + if rows.Err() != nil { + return nil, errors.Wrap(err, op) + } + opts := getOpts(opt...) + if opts.withDeleteLog { + // this truncate could change to a delete if FKs are needed in the future + _, err = d.ExecContext(ctx, "truncate log_migration") + if err != nil { + return nil, errors.Wrap(err, op) + } + } + return entries, nil +} diff --git a/internal/db/schema/manager_test.go b/internal/db/schema/manager_test.go index 720743229..7337ccf08 100644 --- a/internal/db/schema/manager_test.go +++ b/internal/db/schema/manager_test.go @@ -3,6 +3,7 @@ package schema import ( "context" "database/sql" + "sort" "strings" "testing" @@ -237,6 +238,92 @@ func TestManager_SharedLock(t *testing.T) { assert.Error(t, m2.ExclusiveLock(ctx)) } +func Test_GetMigrationLog(t *testing.T) { + t.Parallel() + ctx := context.Background() + c, u, _, err := docker.StartDbInDocker("postgres") + require.NoError(t, err) + t.Cleanup(func() { + require.NoError(t, c()) + }) + d, err := sql.Open("postgres", u) + require.NoError(t, err) + m, err := NewManager(ctx, "postgres", d) + require.NoError(t, err) + require.NoError(t, m.RollForward(ctx)) + + const insert = `insert into log_migration(entry) values ($1)` + createEntries := func(entries ...string) { + for _, e := range entries { + _, err := d.Exec(insert, e) + require.NoError(t, err) + } + } + tests := []struct { + name string + d *sql.DB + setup func() + withDeleteLog bool + wantEntries []string + wantErrMatch *errors.Template + }{ + { + name: "simple", + d: d, + setup: func() { createEntries("alice", "eve", "bob") }, + wantEntries: []string{"alice", "eve", "bob"}, + }, + { + name: "with-delete-log", + d: d, + setup: func() { createEntries("alice", "eve", "bob") }, + withDeleteLog: true, + wantEntries: []string{"alice", "eve", "bob"}, + }, + { + name: "missing-sql-DB", + wantErrMatch: errors.T(errors.InvalidParameter), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + + // start with no log entries + _, err := d.ExecContext(ctx, "truncate log_migration") + require.NoError(err) + + if tt.setup != nil { + tt.setup() + } + gotLog, err := GetMigrationLog(ctx, tt.d, WithDeleteLog(tt.withDeleteLog)) + if tt.wantErrMatch != nil { + require.Error(err) + assert.Truef(errors.Match(tt.wantErrMatch, err), "expected error with code: %s and got err: %q", tt.wantErrMatch.Code, err) + return + } + require.NoError(err) + var got []string + for _, e := range gotLog { + got = append(got, e.Entry) + } + sort.Strings(got) + sort.Strings(tt.wantEntries) + assert.Equal(tt.wantEntries, got) + + row := d.QueryRowContext(ctx, "select count(*) from log_migration") + require.NoError(row.Err()) + var cnt int + require.NoError(row.Scan(&cnt)) + if tt.withDeleteLog { + assert.Equal(0, cnt) + } else { + assert.Equal(len(gotLog), cnt) + } + }) + } +} + // Creates a new migrationState only with the versions <= the provided maxVer func createPartialMigrationState(om migrationState, maxVer int) migrationState { nState := migrationState{ diff --git a/internal/db/schema/migrations/postgres/2/100_pass.up.sql b/internal/db/schema/migrations/postgres/2/100_pass.up.sql new file mode 100644 index 000000000..5f3a2136f --- /dev/null +++ b/internal/db/schema/migrations/postgres/2/100_pass.up.sql @@ -0,0 +1,28 @@ +begin; + +-- auth_password_method_with_is_primary is useful for reading a password auth +-- method with a bool to determine if it's the scope's primary auth method. +create view auth_password_method_with_is_primary as +select + case when s.primary_auth_method_id is not null then + true + else false end + as is_primary_auth_method, + am.public_id, + am.scope_id, + am.password_conf_id, + am.name, + am.description, + am.create_time, + am.update_time, + am.version, + am.min_login_name_length, + am.min_password_length +from + auth_password_method am + left outer join iam_scope s on am.public_id = s.primary_auth_method_id; +comment on view auth_password_method_with_is_primary is +'password auth method with an is_primary_auth_method bool'; + + +commit; diff --git a/internal/db/schema/migrations/postgres/2/80_log.up.sql b/internal/db/schema/migrations/postgres/2/80_log.up.sql new file mode 100644 index 000000000..0cd2b745a --- /dev/null +++ b/internal/db/schema/migrations/postgres/2/80_log.up.sql @@ -0,0 +1,50 @@ +begin; + +-- log_migration entries represent logs generated during migrations +create table log_migration( + id bigint generated always as identity primary key, + migration_version bigint not null, -- cannot declare FK since the table is truncated during runtime + create_time wt_timestamp, + entry text not null +); +comment on table log_migration is +'log_migration entries are logging output from databaes migrations'; + +-- log_migration triggers +create trigger + default_create_time_column +before +insert on log_migration + for each row execute procedure default_create_time(); + +create trigger + immutable_columns +before +update on log_migration + for each row execute procedure immutable_columns('id', 'migration_version', 'create_time', 'entry'); + + +-- log_migration_version() defines a function to be used in a "before update" +-- trigger for log_migrations entries. Its intent: set the log_migration +-- version column to the current migration version. +create or replace function + log_migration_version() + returns trigger +as $$ + declare current_version bigint; + begin + select max(version) from boundary_schema_version into current_version; + new.migration_version = current_version; + return new; + end; +$$ language plpgsql; +comment on function log_migration_version() is +'log_migration_version will set the log_migration entries to the current migration version'; + +create trigger + migration_version_column +before +insert on log_migration + for each row execute procedure log_migration_version(); + + commit; diff --git a/internal/db/schema/migrations/postgres/2/83_oidc_enm.up.sql b/internal/db/schema/migrations/postgres/2/83_oidc_enm.up.sql new file mode 100644 index 000000000..31443b7fb --- /dev/null +++ b/internal/db/schema/migrations/postgres/2/83_oidc_enm.up.sql @@ -0,0 +1,70 @@ +begin; + +-- auth_oidc_method_state_enum entries define the possible oidc auth method +-- states. +create table auth_oidc_method_state_enm ( + name text primary key + constraint name_only_predefined_oidc_method_states_allowed + check ( + name in ('inactive', 'active-private', 'active-public') + ) +); + +-- populate the values of auth_oidc_method_state_enm +insert into auth_oidc_method_state_enm(name) + values + ('inactive'), + ('active-private'), + ('active-public'); + + -- define the immutable fields for auth_oidc_method_state_enm (all of them) +create trigger + immutable_columns +before +update on auth_oidc_method_state_enm + for each row execute procedure immutable_columns('name'); + + +-- auth_oidc_signing_alg entries define the supported oidc auth method +-- signing algorithms. +create table auth_oidc_signing_alg_enm ( + name text primary key + constraint only_predefined_auth_oidc_signing_algs_allowed + check ( + name in ( + 'RS256', + 'RS384', + 'RS512', + 'ES256', + 'ES384', + 'ES512', + 'PS256', + 'PS384', + 'PS512', + 'EdDSA') + ) +); + +-- populate the values of auth_oidc_signing_alg +insert into auth_oidc_signing_alg_enm (name) + values + ('RS256'), + ('RS384'), + ('RS512'), + ('ES256'), + ('ES384'), + ('ES512'), + ('PS256'), + ('PS384'), + ('PS512'), + ('EdDSA') + ; + + -- define the immutable fields for auth_oidc_signing_alg (all of them) +create trigger + immutable_columns +before +update on auth_oidc_signing_alg_enm + for each row execute procedure immutable_columns('name'); + +commit; diff --git a/internal/db/schema/migrations/postgres/2/84_oidc.up.sql b/internal/db/schema/migrations/postgres/2/84_oidc.up.sql new file mode 100644 index 000000000..cdc04f835 --- /dev/null +++ b/internal/db/schema/migrations/postgres/2/84_oidc.up.sql @@ -0,0 +1,422 @@ +begin; + +-- auth_oidc_method entries are the current oidc auth methods configured for +-- existing scopes. +create table auth_oidc_method ( + public_id wt_public_id + primary key, + scope_id wt_scope_id + not null, + name wt_name, + description wt_description, + create_time wt_timestamp, + update_time wt_timestamp, + version wt_version, + state text not null + constraint auth_oidc_method_state_enm_fkey + references auth_oidc_method_state_enm(name) + on delete restrict + on update cascade, + disable_discovered_config_validation bool not null default false, + api_url wt_url, -- an address prefix at which the boundary api is reachable. + issuer wt_url, + client_id text -- oidc client identifier issued by the oidc provider. + constraint client_id_not_empty + check(length(trim(client_id)) > 0), + client_secret bytea, -- encrypted oidc client secret issued by the oidc provider. + client_secret_hmac text + constraint client_secret_hmac_not_empty + check(length(trim(client_secret_hmac)) > 0), + key_id wt_private_id not null -- key used to encrypt entries via wrapping wrapper. + constraint kms_database_key_version_fkey + references kms_database_key_version(private_id) + on delete restrict + on update cascade, + constraint key_id_not_empty + check(length(trim(key_id)) > 0), + max_age int -- the allowable elapsed time in secs since the last time the user was authenticated. A value -1 basically forces the IdP to re-authenticate the End-User. Zero is not a valid value. + constraint max_age_not_equal_zero + check(max_age != 0) + constraint max_age_not_less_then_negative_one + check(max_age >= -1), + constraint auth_method_fkey + foreign key (scope_id, public_id) + references auth_method (scope_id, public_id) + on delete cascade + on update cascade, + constraint auth_oidc_method_scope_id_name_uq + unique(scope_id, name), + constraint auth_oidc_method_scope_id_public_id_uq + unique(scope_id, public_id), + constraint auth_oidc_method_scope_id_issuer_client_id_unique + unique(scope_id, issuer, client_id) -- a client_id must be unique for a provider within a scope. +); +comment on table auth_oidc_method is +'auth_oidc_method entries are the current oidc auth methods configured for existing scopes.'; + +-- auth_oidc_signing_alg entries are the signing algorithms allowed for an oidc +-- auth method. There must be at least one allowed alg for each oidc auth method. +create table auth_oidc_signing_alg ( + create_time wt_timestamp, + oidc_method_id wt_public_id + constraint auth_oidc_method_fkey + references auth_oidc_method(public_id) + on delete cascade + on update cascade, + signing_alg_name text + constraint auth_oidc_signing_alg_enm_fkey + references auth_oidc_signing_alg_enm(name) + on delete restrict + on update cascade, + primary key(oidc_method_id, signing_alg_name) +); +comment on table auth_oidc_signing_alg is +'auth_oidc_signing_alg entries are the signing algorithms allowed for an oidc auth method. There must be at least one allowed alg for each oidc auth method'; + +-- auth_oidc_aud_claim entries are the audience claims for a specific oidc auth +-- method. There can be 0 or more for each parent oidc auth method. If an auth +-- method has any aud claims, an ID token must contain one of them to be valid. +create table auth_oidc_aud_claim ( + create_time wt_timestamp, + oidc_method_id wt_public_id + constraint auth_oidc_method_fkey + references auth_oidc_method(public_id) + on delete cascade + on update cascade, + aud_claim text not null + constraint aud_claim_must_not_be_empty + check(length(trim(aud_claim)) > 0) + constraint aud_claim_must_be_less_than_1024_chars + check(length(trim(aud_claim)) < 1024), + primary key(oidc_method_id, aud_claim) +); +comment on table auth_oidc_aud_claim is +'auth_oidc_aud_claim entries are the audience claims for a specific oidc auth method. There can be 0 or more for each parent oidc auth method. If an auth method has any aud claims, an ID token must contain one of them to be valid.'; + + +-- auth_oidc_certificate entries are optional PEM encoded x509 certificates. +-- Each entry is a single certificate. An oidc auth method may have 0 or more +-- of these optional x509s. If an auth method has any cert entries, they are +-- used as trust anchors when connecting to the auth method's oidc provider +-- (instead of the host system's cert chain). +create table auth_oidc_certificate ( + create_time wt_timestamp, + oidc_method_id wt_public_id + constraint auth_oidc_method_fkey + references auth_oidc_method(public_id) + on delete cascade + on update cascade, + certificate bytea not null, + primary key(oidc_method_id, certificate) +); +comment on table auth_oidc_certificate is +'auth_oidc_certificate entries are optional PEM encoded x509 certificates. Each entry is a single certificate. An oidc auth method may have 0 or more of these optional x509s. If an auth method has any cert entries, they are used as trust anchors when connecting to the auth methods oidc provider (instead of the host system cert chain)'; + + +-- auth_oidc_account entries are subtypes of auth_account and represent an +-- oidc account. +create table auth_oidc_account ( + public_id wt_public_id + primary key, + auth_method_id wt_public_id + not null, + -- NOTE(mgaffney): The scope_id type is not wt_scope_id because the domain + -- check is executed before the insert trigger which retrieves the scope_id + -- causing an insert to fail. + scope_id text not null, + name wt_name, + description wt_description, + create_time wt_timestamp, + update_time wt_timestamp, + version wt_version, + issuer wt_url not null, -- case-sensitive URL that maps to an id_token's iss claim, + subject text not null -- case-sensitive string that maps to an id_token's sub claim + constraint subject_must_not_be_empty + check ( + length(trim(subject)) > 0 + ) + constraint subject_must_be_less_than_256_chars + check( + length(trim(subject)) <= 255 -- length limit per OIDC spec + ), + full_name wt_full_name, -- may be null and maps to an id_token's name claim + email wt_email, -- may be null and maps to the id_token's email claim + constraint auth_oidc_method_fkey + foreign key (scope_id, auth_method_id) + references auth_oidc_method (scope_id, public_id) + on delete cascade + on update cascade, + constraint auth_account_fkey + foreign key (scope_id, auth_method_id, public_id) + references auth_account (scope_id, auth_method_id, public_id) + on delete cascade + on update cascade, + constraint auth_oidc_account_auth_method_id_name_uq + unique(auth_method_id, name), + -- ############################################################### + -- any change to this constraints name must be aligned with the + -- acctUpsertQuery const in internal/auth/oidc/query.go + -- ############################################################### + constraint auth_oidc_account_auth_method_id_issuer_subject_uq + unique(auth_method_id, issuer, subject), -- subject must be unique for a provider within specific auth method + constraint auth_oidc_account_auth_method_id_public_id_uq + unique(auth_method_id, public_id) +); +comment on table auth_oidc_method is +'auth_oidc_account entries are subtypes of auth_account and represent an oidc account.'; + +-- auth_oidc_method column triggers +create trigger + insert_auth_method_subtype +before insert on auth_oidc_method + for each row execute procedure insert_auth_method_subtype(); + +create trigger + update_time_column +before +update on auth_oidc_method + for each row execute procedure update_time_column(); + +create trigger + immutable_columns +before +update on auth_oidc_method + for each row execute procedure immutable_columns('public_id', 'scope_id', 'create_time'); + +create trigger + default_create_time_column +before +insert on auth_oidc_method + for each row execute procedure default_create_time(); + +create trigger + update_version_column +after update on auth_oidc_method + for each row execute procedure update_version_column(); + +-- active_auth_oidc_method_must_be_complete() defines a function to be used in +-- a "before update" trigger for auth_oidc_method entries. Its intent: prevent +-- incomplete oidc methods from transitioning out of the "inactive" state. +create or replace function + active_auth_oidc_method_must_be_complete() + returns trigger +as $$ + begin + -- validate signing alg + if old.state = 'inactive' and new.state != 'inactive' then + perform + from + auth_oidc_method am + join auth_oidc_signing_alg alg on am.public_id = alg.oidc_method_id + where + new.public_id = am.public_id; + if not found then + raise exception 'an incomplete oidc auth method must remain inactive'; + end if; + -- validate issuer + case + when new.issuer != old.issuer then + if length(trim(new.issuer)) = 0 then + raise exception 'empty issuer: an incomplete oidc auth method must remain inactive'; + end if; + when new.issuer = old.issuer then + if length(trim(old.issuer)) = 0 then + raise exception 'empty issuer: an incomplete oidc auth method must remain inactive'; + end if; + else + end case; + -- validate client_id + case + when new.client_id != old.client_id then + if length(trim(new.client_id)) = 0 then + raise exception 'empty client_id: an incomplete oidc auth method must remain inactive'; + end if; + when new.client_id = old.client_id then + if length(trim(old.client_id)) = 0 then + raise exception 'empty client_id: an incomplete oidc auth method must remain inactive'; + end if; + else + end case; + -- validate client_secret + case + when new.client_secret != old.client_secret then + if length(new.client_secret) = 0 then + raise exception 'empty client_secret: an incomplete oidc auth method must remain inactive'; + end if; + when new.client_secret = old.client_secret then + if length(old.client_secret) = 0 then + raise exception 'empty client_secret: an incomplete oidc auth method must remain inactive'; + end if; + else + end case; + + + end if; + return new; + end; +$$ language plpgsql; +comment on function active_auth_oidc_method_must_be_complete() is +'active_auth_oidc_method_must_be_complete() will raise an error if the oidc auth method is not complete'; + +create trigger + update_active_auth_oidc_method_must_be_complete +before +update on auth_oidc_method + for each row execute procedure active_auth_oidc_method_must_be_complete(); + +-- new_auth_oidc_method_must_be_inactive() defines a function to be used in +-- a "before insert" trigger for auth_oidc_method entries. Its intent: +-- only allow "inactive" auth methods to be inserted. Why? there's no way +-- you can insert an entry that's anything but incomplete, since we have a +-- chicken/egg problem: you need the auth method id to create the required +-- signing algs value objects. +create or replace function + new_auth_oidc_method_must_be_inactive() + returns trigger +as $$ + begin + if new.state != 'inactive' then + raise exception 'an incomplete oidc method must be inactive'; + end if; + end; +$$ language plpgsql; +comment on function new_auth_oidc_method_must_be_inactive() is +'new_auth_oidc_method_must_be_inactive ensures that new incomplete oidc auth methods must remain inactive'; + +create trigger + new_auth_oidc_method_must_be_inactive +before +insert on auth_oidc_method + for each row execute procedure active_auth_oidc_method_must_be_complete(); + +-- auth_oidc_account column triggers +create trigger + update_time_column +before +update on auth_oidc_account + for each row execute procedure update_time_column(); + +create trigger + immutable_columns +before +update on auth_oidc_account + for each row execute procedure immutable_columns('public_id', 'auth_method_id', 'scope_id', 'create_time', 'issuer', 'subject'); + +create trigger + default_create_time_column +before +insert on auth_oidc_account + for each row execute procedure default_create_time(); + +create trigger + update_version_column +after update on auth_oidc_account + for each row execute procedure update_version_column(); + + +-- insert_auth_oidc_account_subtype is intended as a before insert +-- trigger on auth_oidc_account. Its purpose is to insert a base +-- auth_account for new oidc accounts. It's a bit different than the +-- standard trigger for this, because it will have conflicting PKs +-- and we just want to "do nothing" on those conflicts, deferring the +-- raising on an error to insert into the auth_oidc_account table. +-- this is all necessary because of we're using predictable public ids +-- for oidc accounts. +create or replace function + insert_auth_oidc_account_subtype() + returns trigger +as $$ +begin + select auth_method.scope_id + into new.scope_id + from auth_method + where auth_method.public_id = new.auth_method_id; + + insert into auth_account + (public_id, auth_method_id, scope_id) + values + (new.public_id, new.auth_method_id, new.scope_id) + on conflict do nothing; + + return new; +end; + $$ language plpgsql; + +create trigger + insert_auth_oidc_account_subtype +before insert on auth_oidc_account + for each row execute procedure insert_auth_oidc_account_subtype(); + +-- triggers for auth_oidc_method children tables: auth_oidc_aud_claim, +-- auth_oidc_certificate, auth_oidc_signing_alg + + +-- on_delete_active_auth_oidc_method_must_be_complete() defines a function +-- to be used in an "after delete" trigger for auth_oidc_signing_alg +-- Its intent: prevent deletes that would result in an "active" oidc +-- auth method which is incomplete. +create or replace function + on_delete_active_auth_oidc_method_must_be_complete() + returns trigger +as $$ +declare am_state text; +declare alg_cnt int; + begin + select + am.state, + count(alg.oidc_method_id) as alg_cnt + from + auth_oidc_method am + left outer join auth_oidc_signing_alg alg on am.public_id = alg.oidc_method_id + where + new.oidc_method_id = am.public_id + group by am.public_id + into am_state, alg_cnt; + + if not found then + return new; -- auth method was deleted, so we're done + end if; + + if am_state != inactive then + case + when alg_cnt = 0 then + raise exception 'delete would have resulted in an incomplete active oidc auth method with no signing algorithms'; + end case; + end if; + + return new; + end; +$$ language plpgsql; +comment on function on_delete_active_auth_oidc_method_must_be_complete() is +'on_delete_active_auth_oidc_method_must_be_complete() will raise an error if the oidc auth method is not complete after a delete on algs'; + +create trigger + default_create_time_column +before +insert on auth_oidc_aud_claim + for each row execute procedure default_create_time(); + +create trigger + default_create_time_column +before +insert on auth_oidc_certificate + for each row execute procedure default_create_time(); + +create trigger + default_create_time_column +before +insert on auth_oidc_signing_alg + for each row execute procedure default_create_time(); + +create trigger + on_delete_active_auth_oidc_method_must_be_complete +after +delete on auth_oidc_signing_alg + for each row execute procedure on_delete_active_auth_oidc_method_must_be_complete(); + +insert into oplog_ticket (name, version) +values + ('auth_oidc_method', 1), -- auth method is the root aggregate itself and all of its value objects. + ('auth_oidc_account', 1); + +commit; diff --git a/internal/db/schema/migrations/postgres/2/85_authtoken.up.sql b/internal/db/schema/migrations/postgres/2/85_authtoken.up.sql new file mode 100644 index 000000000..51c14600a --- /dev/null +++ b/internal/db/schema/migrations/postgres/2/85_authtoken.up.sql @@ -0,0 +1,48 @@ +begin; + +-- auth_token_status_enm entries define the possible auth token +-- states. +create table auth_token_status_enm ( + name text primary key + constraint name_only_predefined_auth_token_states_allowed + check ( + name in ('auth token pending','token issued', 'authentication failed', 'system error') + ) +); + +-- populate the values of auth_token_status_enm +insert into auth_token_status_enm(name) + values + ('auth token pending'), + ('token issued'), + ('authentication failed'), + ('system error'); + + +-- add the state column with a default to the auth_token table. +alter table auth_token +add column status text +not null +default 'token issued' -- safest default +references auth_token_status_enm(name) + on update cascade + on delete restrict; + + +create or replace view auth_token_account as + select at.public_id, + at.token, + at.auth_account_id, + at.create_time, + at.update_time, + at.approximate_last_access_time, + at.expiration_time, + aa.scope_id, + aa.iam_user_id, + aa.auth_method_id, + at.status + from auth_token as at + inner join auth_account as aa + on at.auth_account_id = aa.public_id; + +commit; \ No newline at end of file diff --git a/internal/db/schema/migrations/postgres/2/86_iam.up.sql b/internal/db/schema/migrations/postgres/2/86_iam.up.sql new file mode 100644 index 000000000..ffdb76b76 --- /dev/null +++ b/internal/db/schema/migrations/postgres/2/86_iam.up.sql @@ -0,0 +1,70 @@ +begin; + +-- add the primary_auth_method_id which determines which auth_method is +-- designated as for "account info" in the user's scope. It also determines +-- which auth method is allowed to auto viviify users. +alter table iam_scope +add column primary_auth_method_id wt_public_id -- allowed to be null and is mutable of course. +constraint auth_method_fkey +references auth_method(public_id) + on update cascade + on delete set null; + +-- establish a compond fk, but there's no cascading of deletes or updates, since +-- we only want to cascade changes to the primary_auth_method_id portion of +-- the compond fk and that is handled in a separate fk declaration. +alter table iam_scope +add constraint auth_method + foreign key (public_id, primary_auth_method_id) + references auth_method(scope_id, public_id); + +-- iam_user_acct_info provides account info for users by determining which +-- auth_method is designated as for "account info" in the user's scope via the +-- scope's primary_auth_method_id. Every sub-type of auth_account must be +-- added to this view's union. +create view iam_acct_info as +select + aa.iam_user_id, + oa.subject as login_name, + oa.full_name as full_name, + oa.email as email +from + iam_scope s, + auth_account aa, + auth_oidc_account oa +where + aa.public_id = oa.public_id and + aa.auth_method_id = s.primary_auth_method_id +union +select + aa.iam_user_id, + pa.login_name, + '' as full_name, + '' as email +from + iam_scope s, + auth_account aa, + auth_password_account pa +where + aa.public_id = pa.public_id and + aa.auth_method_id = s.primary_auth_method_id; + +-- iam_user_acct_info provides a simple way to retrieve entries that include +-- both the iam_user fields with an outer join to the user's account info. +create view iam_user_acct_info as +select + u.public_id, + u.scope_id, + u.name, + u.description, + u.create_time, + u.update_time, + u.version, + i.login_name, + i.full_name, + i.email +from + iam_user u +left outer join iam_acct_info i on u.public_id = i.iam_user_id; + +commit; \ No newline at end of file diff --git a/internal/db/schema/migrations/postgres/2/87_iam.up.sql b/internal/db/schema/migrations/postgres/2/87_iam.up.sql new file mode 100644 index 000000000..e94d73f7e --- /dev/null +++ b/internal/db/schema/migrations/postgres/2/87_iam.up.sql @@ -0,0 +1,69 @@ +begin; + + +-- the intent of this update statement: set the primary auth method for scopes +-- that only have a single auth_password_method, since currently there are only +-- auth_password_methods in boundary. Before this release all +-- auth_password_methods were "basically" primary auth methods and would create +-- an iam_user on first login. +with single_authmethod (scope_id, public_id) as ( + select + am.scope_id, + am.public_id + from + auth_password_method am, + (select + scope_id, + count(public_id) as cnt + from + auth_password_method + group by scope_id) as singles + where + am.scope_id = singles.scope_id and + singles.cnt = 1 +) +update + iam_scope +set + primary_auth_method_id = p.public_id +from + single_authmethod p +where p.scope_id = iam_scope.public_id; + + +-- the intent of the insert with select statement: log the scopes that have more +-- than 1 auth method and therefore cannot have their primary auth method +-- automatically set for them. +with many_authmethod (scope_id, authmethod_cnt) as ( + select + am.scope_id, + many.cnt + from + auth_password_method am, + (select + scope_id, + count(public_id) as cnt + from + auth_password_method + group by scope_id) as many + where + am.scope_id = many.scope_id and + many.cnt > 1 +) +insert into log_migration(entry) +select + distinct concat( + 'unable to set primary_auth_method for ', + public_id, + ' there were ', + m.authmethod_cnt, + ' password auth methods for that scope.' + ) as entry +from + iam_scope s, + many_authmethod m +where + s.primary_auth_method_id is null and + s.public_id = m.scope_id; + +commit; diff --git a/internal/db/schema/migrations/postgres/2/87_iam_test.go b/internal/db/schema/migrations/postgres/2/87_iam_test.go new file mode 100644 index 000000000..0cbe0b8cb --- /dev/null +++ b/internal/db/schema/migrations/postgres/2/87_iam_test.go @@ -0,0 +1,110 @@ +package migration + +import ( + "context" + "database/sql" + "testing" + + "github.com/hashicorp/boundary/internal/auth/password" + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/db/schema" + "github.com/hashicorp/boundary/internal/docker" + "github.com/hashicorp/boundary/internal/iam" + "github.com/jinzhu/gorm" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_PrimaryAuthMethodChanges(t *testing.T) { + t.Parallel() + const priorMigration = 2086 + const primaryAuthMethodMigration = 2087 + t.Run("migrate-store", func(t *testing.T) { + // this is a sequential test which relies on: + // 1) initializing the db using a migration up to the "priorMigration" + // + // 2) seeding the database with some test scopes and auth methods + // + // 3) running the migrations that sets the primary auth methods for + // existing scopes which only have one auth method. + // + // 4) asserting some bits about the state of the db. + assert, require := assert.New(t), require.New(t) + dialect := "postgres" + ctx := context.Background() + + c, u, _, err := docker.StartDbInDocker(dialect) + require.NoError(err) + t.Cleanup(func() { + require.NoError(c()) + }) + d, err := sql.Open(dialect, u) + require.NoError(err) + + // migration to the prior migration (before the one we want to test) + oState := schema.TestCloneMigrationStates(t) + nState := schema.TestCreatePartialMigrationState(oState["postgres"], priorMigration) + oState["postgres"] = nState + + m, err := schema.NewManager(ctx, dialect, d, schema.WithMigrationStates(oState)) + require.NoError(err) + + assert.NoError(m.RollForward(ctx)) + state, err := m.CurrentState(ctx) + require.NoError(err) + assert.Equal(priorMigration, state.DatabaseSchemaVersion) + assert.False(state.Dirty) + + // okay, now we can seed the database with test data + conn, err := gorm.Open(dialect, u) + require.NoError(err) + rootWrapper := db.TestWrapper(t) + iamRepo := iam.TestRepo(t, conn, rootWrapper) + org, _ := iam.TestScopes(t, iamRepo) + _ = password.TestAuthMethods(t, conn, org.PublicId, 3) + org2, _ := iam.TestScopes(t, iamRepo) + _ = password.TestAuthMethods(t, conn, org2.PublicId, 2) + + org3, _ := iam.TestScopes(t, iamRepo) + org3AuthMethods := password.TestAuthMethods(t, conn, org3.PublicId, 1) + org4, _ := iam.TestScopes(t, iamRepo) + org4AuthMethods := password.TestAuthMethods(t, conn, org4.PublicId, 1) + + // now we're ready for the migration we want to test. + oState = schema.TestCloneMigrationStates(t) + nState = schema.TestCreatePartialMigrationState(oState["postgres"], primaryAuthMethodMigration) + oState["postgres"] = nState + + m, err = schema.NewManager(ctx, dialect, d, schema.WithMigrationStates(oState)) + require.NoError(err) + + assert.NoError(m.RollForward(ctx)) + state, err = m.CurrentState(ctx) + require.NoError(err) + assert.Equal(primaryAuthMethodMigration, state.DatabaseSchemaVersion) + assert.False(state.Dirty) + + t.Log("current migration version: ", state.DatabaseSchemaVersion) + entries, err := schema.GetMigrationLog(ctx, d) + require.NoError(err) + for _, e := range entries { + t.Logf("%+v", e) + } + assert.Equalf(2, len(entries), "expected 2 scopes without a primary auth method and got: %d", len(entries)) + + scope1, err := iamRepo.LookupScope(ctx, org.PublicId) + require.NoError(err) + assert.Empty(scope1.PrimaryAuthMethodId) + scope2, err := iamRepo.LookupScope(ctx, org2.PublicId) + require.NoError(err) + assert.Empty(scope2.PrimaryAuthMethodId) + + scope3, err := iamRepo.LookupScope(ctx, org3.PublicId) + require.NoError(err) + assert.Equal(org3AuthMethods[0].PublicId, scope3.PrimaryAuthMethodId) + + scope4, err := iamRepo.LookupScope(ctx, org4.PublicId) + require.NoError(err) + assert.Equal(org4AuthMethods[0].PublicId, scope4.PrimaryAuthMethodId) + }) +} diff --git a/internal/db/schema/migrations/postgres/2/90_auth.up.sql b/internal/db/schema/migrations/postgres/2/90_auth.up.sql new file mode 100644 index 000000000..b92a1ae89 --- /dev/null +++ b/internal/db/schema/migrations/postgres/2/90_auth.up.sql @@ -0,0 +1,97 @@ +begin; + +-- By adding the name column to the base auth method type, the database can +-- ensure that auth method names are unique across all sub types. +alter table auth_method + add column name wt_name; + +alter table auth_method + add constraint auth_method_scope_id_name_uq + unique (scope_id, name); + + +-- the intent of this statement is to update the base type's name with the +-- existing password auth method names. +update auth_method +set name = pw.name +from + auth_password_method pw +where + auth_method.public_id = pw.public_id and + pw.name is not null; + +-- insert_auth_method_subtype() is a replacement of the function definition in +-- migration 07_auth.up.sql This new definition also inserts the sub type's name +-- into the base type. The name column must be on the base type, so the database +-- can ensure that auth method names are unique across all sub types. +create or replace function + insert_auth_method_subtype() + returns trigger +as $$ +begin + insert into auth_method + (public_id, scope_id, name) + values + (new.public_id, new.scope_id, new.name); + return new; +end; +$$ language plpgsql; +comment on function insert_auth_method_subtype() is +'insert_auth_method_subtype() inserts sub type name into the base type auth method table'; + +-- update_auth_method_subtype() is a new function intended to be used in "before +-- update" triggers for all auth method sub types. It's purpose is to ensure +-- that the name column is syncronized between the sub and base auth method +-- types. The name column must be on the base type, so the database can ensure +-- that auth method names are unique across all sub types. +create or replace function + update_auth_method_subtype() + returns trigger +as $$ +begin + update auth_method set name = new.name where public_id = new.public_id and new.name != name; + return new; +end; +$$ language plpgsql; +comment on function update_auth_method_subtype() is +'update_auth_method_subtype() will update base auth method type name column with new values from sub type'; + +create trigger + update_auth_method_subtype +before update on auth_oidc_method + for each row execute procedure update_auth_method_subtype(); + + +create trigger + update_auth_method_subtype +before update on auth_password_method + for each row execute procedure update_auth_method_subtype(); + + +-- delete_auth_method_subtype() is an after trigger function for subytypes of +-- auth_method +create or replace function + delete_auth_method_subtype() + returns trigger +as $$ +begin + delete from auth_method + where public_id = old.public_id; + return null; -- results are ignore since this is an after trigger. +end; +$$ language plpgsql; +comment on function delete_auth_method_subtype is +'delete_auth_method_subtype() is an after trigger function for subytypes of auth_method'; + +create trigger + delete_auth_method_subtype +after delete on auth_oidc_method + for each row execute procedure delete_auth_method_subtype(); + + +create trigger + delete_auth_method_subtype +after delete on auth_password_method + for each row execute procedure delete_auth_method_subtype(); + +commit; diff --git a/internal/db/schema/migrations/postgres/2/90_auth_test.go b/internal/db/schema/migrations/postgres/2/90_auth_test.go new file mode 100644 index 000000000..39c5ae2c0 --- /dev/null +++ b/internal/db/schema/migrations/postgres/2/90_auth_test.go @@ -0,0 +1,114 @@ +package migration + +import ( + "context" + "testing" + + "github.com/hashicorp/boundary/internal/auth/oidc" + "github.com/hashicorp/boundary/internal/auth/password" + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/kms" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_AuthMethodSubtypes(t *testing.T) { + t.Parallel() + assert, require := assert.New(t), require.New(t) + ctx := context.Background() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + rootWrapper := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, rootWrapper) + iamRepo := iam.TestRepo(t, conn, rootWrapper) + org, _ := iam.TestScopes(t, iamRepo) + oidcRepo, err := oidc.NewRepository(rw, rw, kmsCache) + require.NoError(err) + + // test oidc subtype insert + oidcAm, err := oidc.NewAuthMethod(org.PublicId, "alice-rp", "fido", oidc.WithName("alice")) + require.NoError(err) + oidcAm, err = oidcRepo.CreateAuthMethod(ctx, oidcAm) + require.NoError(err) + oidcParent, err := findParent(t, rw, oidcAm.PublicId) + require.NoError(err) + assert.Equal(oidcAm.Name, oidcParent.Name) + + // test oidc subtype update + updatedOidc := oidcAm.Clone() + updatedOidc.Name = "eve's least favorite" + updatedOidc, _, err = oidcRepo.UpdateAuthMethod(ctx, oidcAm, oidcAm.Version, []string{"Name"}) + require.NoError(err) + assert.Equal(updatedOidc.Name, oidcAm.Name) + oidcParent, err = findParent(t, rw, updatedOidc.PublicId) + require.NoError(err) + assert.Equal(updatedOidc.Name, oidcParent.Name) + + // test password subtype insert + pw, err := password.NewAuthMethod(org.PublicId, password.WithName("eve's favorite")) + require.NoError(err) + passRepo, err := password.NewRepository(rw, rw, kmsCache) + require.NoError(err) + pw, err = passRepo.CreateAuthMethod(ctx, pw) + require.NoError(err) + pwParent, err := findParent(t, rw, pw.PublicId) + require.NoError(err) + require.Equal(pwParent.PublicId, pw.PublicId) + assert.Equal("eve's favorite", pwParent.Name) + + // test password subtype update + updatedPw := pw.Clone() + updatedPw.Name = "new name" + updatedPw, _, err = passRepo.UpdateAuthMethod(ctx, updatedPw, updatedPw.Version, []string{"Name"}) + require.NoError(err) + pwParent, err = findParent(t, rw, updatedPw.PublicId) + require.NoError(err) + require.Equal(pwParent.PublicId, updatedPw.PublicId) + assert.Equal(updatedPw.Name, pwParent.Name) + + // // test non-unique names across subtypes + notUnique := updatedPw.Clone() + notUnique.Name = updatedOidc.Name + _, _, err = passRepo.UpdateAuthMethod(ctx, notUnique, notUnique.Version, []string{"Name"}) + require.Error(err) + assert.Truef(errors.Match(errors.T(errors.NotUnique), err), "expected error code %s and got error: %q", errors.NotUnique, err) + + // test password subtype delete + _, err = passRepo.DeleteAuthMethod(ctx, updatedPw.ScopeId, updatedPw.PublicId) + require.NoError(err) + pwParent, err = findParent(t, rw, updatedPw.PublicId) + require.Error(err) + assert.Truef(errors.Match(errors.T(errors.RecordNotFound), err), "expected error code %s and got error: %q", errors.RecordNotFound, err) + assert.Nil(pwParent) + + // test oidc subtype delete + _, err = oidcRepo.DeleteAuthMethod(ctx, updatedOidc.PublicId) + require.NoError(err) + oidcParent, err = findParent(t, rw, updatedOidc.PublicId) + assert.Truef(errors.Match(errors.T(errors.RecordNotFound), err), "expected error code %s and got error: %q", errors.RecordNotFound, err) + assert.Nil(oidcParent) +} + +type parent struct { + PublicId string + ScopeId string + Name string +} + +func (p *parent) GetPublicId() string { return p.PublicId } +func (p *parent) TableName() string { return "auth_method" } + +func findParent(t *testing.T, r db.Reader, authMethodId string) (*parent, error) { + ctx := context.Background() + t.Helper() + p := &parent{ + PublicId: authMethodId, + } + err := r.LookupByPublicId(ctx, p) + if err != nil { + return nil, err + } + return p, nil +} diff --git a/internal/db/schema/migrations/postgres/2/95_oidc.up.sql b/internal/db/schema/migrations/postgres/2/95_oidc.up.sql new file mode 100644 index 000000000..c9c15df8f --- /dev/null +++ b/internal/db/schema/migrations/postgres/2/95_oidc.up.sql @@ -0,0 +1,47 @@ +begin; + +-- oidc_auth_method_with_value_obj is useful for reading an oidc auth method +-- with its associated value objects (algs, auds, certs) as columns +-- with | delimited values. The use of the postgres string_agg(...) to +-- aggregate the value objects into a column works because we are only pulling +-- in one column from the associated tables and that value is part of the +-- primary key and unique. This view will make things like recursive listing of +-- oidc auth methods fairly straightforward to implement for the oidc repo. +-- The view also includes an is_primary_auth_method bool +create view oidc_auth_method_with_value_obj as +select + case when s.primary_auth_method_id is not null then + true + else false end + as is_primary_auth_method, + am.public_id, + am.scope_id, + am.name, + am.description, + am.create_time, + am.update_time, + am.version, + am.state, + am.api_url, + am.disable_discovered_config_validation, + am.issuer, + am.client_id, + am.client_secret, + am.client_secret_hmac, + am.key_id, + am.max_age, + -- the string_agg(..) column will be null if there are no associated value objects + string_agg(distinct alg.signing_alg_name, '|') as algs, + string_agg(distinct aud.aud_claim, '|') as auds, + string_agg(distinct cert.certificate, '|') as certs +from + auth_oidc_method am + left outer join iam_scope s on am.public_id = s.primary_auth_method_id + left outer join auth_oidc_signing_alg alg on am.public_id = alg.oidc_method_id + left outer join auth_oidc_aud_claim aud on am.public_id = aud.oidc_method_id + left outer join auth_oidc_certificate cert on am.public_id = cert.oidc_method_id +group by am.public_id, is_primary_auth_method; -- there can be only one public_id + is_primary_auth_method, so group by isn't a problem. +comment on view oidc_auth_method_with_value_obj is +'oidc auth method with its associated value objects (algs, auds, certs) as columns with | delimited values'; + +commit; \ No newline at end of file diff --git a/internal/db/schema/options.go b/internal/db/schema/options.go new file mode 100644 index 000000000..386365192 --- /dev/null +++ b/internal/db/schema/options.go @@ -0,0 +1,37 @@ +package schema + +// getOpts - iterate the inbound Options and return a struct. +func getOpts(opt ...Option) options { + opts := getDefaultOptions() + for _, o := range opt { + o(&opts) + } + return opts +} + +// Option - how Options are passed as arguments. +type Option func(*options) + +// options = how options are represented +type options struct { + withMigrationStates map[string]migrationState + withDeleteLog bool +} + +func getDefaultOptions() options { + return options{} +} + +// WithMigrationStates provides an optional migration states. +func WithMigrationStates(states map[string]migrationState) Option { + return func(o *options) { + o.withMigrationStates = states + } +} + +// WithDeleteLog provides an option to specify the deletion of log entries. +func WithDeleteLog(del bool) Option { + return func(o *options) { + o.withDeleteLog = del + } +} diff --git a/internal/db/schema/options_test.go b/internal/db/schema/options_test.go new file mode 100644 index 000000000..7f9d46b2a --- /dev/null +++ b/internal/db/schema/options_test.go @@ -0,0 +1,29 @@ +package schema + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +// Test_GetOpts provides unit tests for GetOpts and all the options +func Test_GetOpts(t *testing.T) { + t.Parallel() + t.Run("WithMigrationStates", func(t *testing.T) { + assert := assert.New(t) + oState := TestCloneMigrationStates(t) + nState := TestCreatePartialMigrationState(oState["postgres"], 8) + oState["postgres"] = nState + opts := getOpts(WithMigrationStates(oState)) + testOpts := getDefaultOptions() + testOpts.withMigrationStates = oState + assert.Equal(opts, testOpts) + }) + t.Run("WithDeleteLog", func(t *testing.T) { + assert := assert.New(t) + opts := getOpts(WithDeleteLog(true)) + testOpts := getDefaultOptions() + testOpts.withDeleteLog = true + assert.Equal(opts, testOpts) + }) +} diff --git a/internal/db/schema/postgres/postgres.go b/internal/db/schema/postgres/postgres.go index c3020a1f1..69ed9c72d 100644 --- a/internal/db/schema/postgres/postgres.go +++ b/internal/db/schema/postgres/postgres.go @@ -222,6 +222,12 @@ func (p *Postgres) Run(ctx context.Context, migration io.Reader, version int) er } } + // set the version first, so logs will be associated with this new version. + // if there's an error, it will get rollback + if err := p.setVersion(ctx, version, false); err != nil { + return errors.Wrap(err, op) + } + if _, err := extr.ExecContext(ctx, query); err != nil { if rollbackErr := rollback(); rollbackErr != nil { err = multierror.Append(err, rollbackErr) @@ -235,7 +241,7 @@ func (p *Postgres) Run(ctx context.Context, migration io.Reader, version int) er line, col, lineColOK = computeLineFromPos(query, int(pos)) } } - message := fmt.Sprintf("migration failed") + message := "migration failed" if lineColOK { message = fmt.Sprintf("%s (column %d)", message, col) } @@ -247,9 +253,6 @@ func (p *Postgres) Run(ctx context.Context, migration io.Reader, version int) er } return errors.Wrap(err, op, errors.WithMsg(fmt.Sprintf("migration failed: %s", migr))) } - if err := p.setVersion(ctx, version, false); err != nil { - return errors.Wrap(err, op) - } return nil } diff --git a/internal/db/schema/postgres/postgres_test.go b/internal/db/schema/postgres/postgres_test.go index 70fa6023a..b56c8010f 100644 --- a/internal/db/schema/postgres/postgres_test.go +++ b/internal/db/schema/postgres/postgres_test.go @@ -136,7 +136,7 @@ func TestCurrentState_NoVersionTable(t *testing.T) { } }() // Drop the version table so calls to CurrentState don't rely on that - d.drop(ctx) + require.NoError(t, d.drop(ctx)) v, alreadyRan, dirt, err := d.CurrentState(ctx) assert.NoError(t, err) @@ -166,7 +166,7 @@ func TestCurrentState_ToManyTables(t *testing.T) { }() // Create the most recent table - d.EnsureVersionTable(ctx) + require.NoError(t, d.EnsureVersionTable(ctx)) // Create the legacy version of the table. oldTableCreate := `create table if not exists schema_migrations (version bigint primary key, dirty boolean not null)` @@ -364,6 +364,12 @@ func TestPostgres_Lock(t *testing.T) { if err != nil { t.Fatal(err) } + + // make sure we call call Unlock in an idempotent manner. + err = ps.Unlock(ctx) + if err != nil { + t.Fatal(err) + } }) } diff --git a/internal/db/schema/postgres_migration.gen.go b/internal/db/schema/postgres_migration.gen.go index 45c2d59f7..7404e92a4 100644 --- a/internal/db/schema/postgres_migration.gen.go +++ b/internal/db/schema/postgres_migration.gen.go @@ -4,7 +4,7 @@ package schema func init() { migrationStates["postgres"] = migrationState{ - binarySchemaVersion: 1003, + binarySchemaVersion: 2100, upMigrations: map[int][]byte{ 1: []byte(` create domain wt_public_id as text @@ -4986,6 +4986,887 @@ create trigger kms_version_column before insert on kms_oidc_key_version for each row execute procedure kms_version_column('oidc_key_id'); +`), + 2100: []byte(` +-- auth_password_method_with_is_primary is useful for reading a password auth +-- method with a bool to determine if it's the scope's primary auth method. +create view auth_password_method_with_is_primary as +select + case when s.primary_auth_method_id is not null then + true + else false end + as is_primary_auth_method, + am.public_id, + am.scope_id, + am.password_conf_id, + am.name, + am.description, + am.create_time, + am.update_time, + am.version, + am.min_login_name_length, + am.min_password_length +from + auth_password_method am + left outer join iam_scope s on am.public_id = s.primary_auth_method_id; +comment on view auth_password_method_with_is_primary is +'password auth method with an is_primary_auth_method bool'; +`), + 2080: []byte(` +-- log_migration entries represent logs generated during migrations +create table log_migration( + id bigint generated always as identity primary key, + migration_version bigint not null, -- cannot declare FK since the table is truncated during runtime + create_time wt_timestamp, + entry text not null +); +comment on table log_migration is +'log_migration entries are logging output from databaes migrations'; + +-- log_migration triggers +create trigger + default_create_time_column +before +insert on log_migration + for each row execute procedure default_create_time(); + +create trigger + immutable_columns +before +update on log_migration + for each row execute procedure immutable_columns('id', 'migration_version', 'create_time', 'entry'); + + +-- log_migration_version() defines a function to be used in a "before update" +-- trigger for log_migrations entries. Its intent: set the log_migration +-- version column to the current migration version. +create or replace function + log_migration_version() + returns trigger +as $$ + declare current_version bigint; + begin + select max(version) from boundary_schema_version into current_version; + new.migration_version = current_version; + return new; + end; +$$ language plpgsql; +comment on function log_migration_version() is +'log_migration_version will set the log_migration entries to the current migration version'; + +create trigger + migration_version_column +before +insert on log_migration + for each row execute procedure log_migration_version(); +`), + 2083: []byte(` +-- auth_oidc_method_state_enum entries define the possible oidc auth method +-- states. +create table auth_oidc_method_state_enm ( + name text primary key + constraint name_only_predefined_oidc_method_states_allowed + check ( + name in ('inactive', 'active-private', 'active-public') + ) +); + +-- populate the values of auth_oidc_method_state_enm +insert into auth_oidc_method_state_enm(name) + values + ('inactive'), + ('active-private'), + ('active-public'); + + -- define the immutable fields for auth_oidc_method_state_enm (all of them) +create trigger + immutable_columns +before +update on auth_oidc_method_state_enm + for each row execute procedure immutable_columns('name'); + + +-- auth_oidc_signing_alg entries define the supported oidc auth method +-- signing algorithms. +create table auth_oidc_signing_alg_enm ( + name text primary key + constraint only_predefined_auth_oidc_signing_algs_allowed + check ( + name in ( + 'RS256', + 'RS384', + 'RS512', + 'ES256', + 'ES384', + 'ES512', + 'PS256', + 'PS384', + 'PS512', + 'EdDSA') + ) +); + +-- populate the values of auth_oidc_signing_alg +insert into auth_oidc_signing_alg_enm (name) + values + ('RS256'), + ('RS384'), + ('RS512'), + ('ES256'), + ('ES384'), + ('ES512'), + ('PS256'), + ('PS384'), + ('PS512'), + ('EdDSA') + ; + + -- define the immutable fields for auth_oidc_signing_alg (all of them) +create trigger + immutable_columns +before +update on auth_oidc_signing_alg_enm + for each row execute procedure immutable_columns('name'); +`), + 2084: []byte(` +-- auth_oidc_method entries are the current oidc auth methods configured for +-- existing scopes. +create table auth_oidc_method ( + public_id wt_public_id + primary key, + scope_id wt_scope_id + not null, + name wt_name, + description wt_description, + create_time wt_timestamp, + update_time wt_timestamp, + version wt_version, + state text not null + constraint auth_oidc_method_state_enm_fkey + references auth_oidc_method_state_enm(name) + on delete restrict + on update cascade, + disable_discovered_config_validation bool not null default false, + api_url wt_url, -- an address prefix at which the boundary api is reachable. + issuer wt_url, + client_id text -- oidc client identifier issued by the oidc provider. + constraint client_id_not_empty + check(length(trim(client_id)) > 0), + client_secret bytea, -- encrypted oidc client secret issued by the oidc provider. + client_secret_hmac text + constraint client_secret_hmac_not_empty + check(length(trim(client_secret_hmac)) > 0), + key_id wt_private_id not null -- key used to encrypt entries via wrapping wrapper. + constraint kms_database_key_version_fkey + references kms_database_key_version(private_id) + on delete restrict + on update cascade, + constraint key_id_not_empty + check(length(trim(key_id)) > 0), + max_age int -- the allowable elapsed time in secs since the last time the user was authenticated. A value -1 basically forces the IdP to re-authenticate the End-User. Zero is not a valid value. + constraint max_age_not_equal_zero + check(max_age != 0) + constraint max_age_not_less_then_negative_one + check(max_age >= -1), + constraint auth_method_fkey + foreign key (scope_id, public_id) + references auth_method (scope_id, public_id) + on delete cascade + on update cascade, + constraint auth_oidc_method_scope_id_name_uq + unique(scope_id, name), + constraint auth_oidc_method_scope_id_public_id_uq + unique(scope_id, public_id), + constraint auth_oidc_method_scope_id_issuer_client_id_unique + unique(scope_id, issuer, client_id) -- a client_id must be unique for a provider within a scope. +); +comment on table auth_oidc_method is +'auth_oidc_method entries are the current oidc auth methods configured for existing scopes.'; + +-- auth_oidc_signing_alg entries are the signing algorithms allowed for an oidc +-- auth method. There must be at least one allowed alg for each oidc auth method. +create table auth_oidc_signing_alg ( + create_time wt_timestamp, + oidc_method_id wt_public_id + constraint auth_oidc_method_fkey + references auth_oidc_method(public_id) + on delete cascade + on update cascade, + signing_alg_name text + constraint auth_oidc_signing_alg_enm_fkey + references auth_oidc_signing_alg_enm(name) + on delete restrict + on update cascade, + primary key(oidc_method_id, signing_alg_name) +); +comment on table auth_oidc_signing_alg is +'auth_oidc_signing_alg entries are the signing algorithms allowed for an oidc auth method. There must be at least one allowed alg for each oidc auth method'; + +-- auth_oidc_aud_claim entries are the audience claims for a specific oidc auth +-- method. There can be 0 or more for each parent oidc auth method. If an auth +-- method has any aud claims, an ID token must contain one of them to be valid. +create table auth_oidc_aud_claim ( + create_time wt_timestamp, + oidc_method_id wt_public_id + constraint auth_oidc_method_fkey + references auth_oidc_method(public_id) + on delete cascade + on update cascade, + aud_claim text not null + constraint aud_claim_must_not_be_empty + check(length(trim(aud_claim)) > 0) + constraint aud_claim_must_be_less_than_1024_chars + check(length(trim(aud_claim)) < 1024), + primary key(oidc_method_id, aud_claim) +); +comment on table auth_oidc_aud_claim is +'auth_oidc_aud_claim entries are the audience claims for a specific oidc auth method. There can be 0 or more for each parent oidc auth method. If an auth method has any aud claims, an ID token must contain one of them to be valid.'; + + +-- auth_oidc_certificate entries are optional PEM encoded x509 certificates. +-- Each entry is a single certificate. An oidc auth method may have 0 or more +-- of these optional x509s. If an auth method has any cert entries, they are +-- used as trust anchors when connecting to the auth method's oidc provider +-- (instead of the host system's cert chain). +create table auth_oidc_certificate ( + create_time wt_timestamp, + oidc_method_id wt_public_id + constraint auth_oidc_method_fkey + references auth_oidc_method(public_id) + on delete cascade + on update cascade, + certificate bytea not null, + primary key(oidc_method_id, certificate) +); +comment on table auth_oidc_certificate is +'auth_oidc_certificate entries are optional PEM encoded x509 certificates. Each entry is a single certificate. An oidc auth method may have 0 or more of these optional x509s. If an auth method has any cert entries, they are used as trust anchors when connecting to the auth methods oidc provider (instead of the host system cert chain)'; + + +-- auth_oidc_account entries are subtypes of auth_account and represent an +-- oidc account. +create table auth_oidc_account ( + public_id wt_public_id + primary key, + auth_method_id wt_public_id + not null, + -- NOTE(mgaffney): The scope_id type is not wt_scope_id because the domain + -- check is executed before the insert trigger which retrieves the scope_id + -- causing an insert to fail. + scope_id text not null, + name wt_name, + description wt_description, + create_time wt_timestamp, + update_time wt_timestamp, + version wt_version, + issuer wt_url not null, -- case-sensitive URL that maps to an id_token's iss claim, + subject text not null -- case-sensitive string that maps to an id_token's sub claim + constraint subject_must_not_be_empty + check ( + length(trim(subject)) > 0 + ) + constraint subject_must_be_less_than_256_chars + check( + length(trim(subject)) <= 255 -- length limit per OIDC spec + ), + full_name wt_full_name, -- may be null and maps to an id_token's name claim + email wt_email, -- may be null and maps to the id_token's email claim + constraint auth_oidc_method_fkey + foreign key (scope_id, auth_method_id) + references auth_oidc_method (scope_id, public_id) + on delete cascade + on update cascade, + constraint auth_account_fkey + foreign key (scope_id, auth_method_id, public_id) + references auth_account (scope_id, auth_method_id, public_id) + on delete cascade + on update cascade, + constraint auth_oidc_account_auth_method_id_name_uq + unique(auth_method_id, name), + -- ############################################################### + -- any change to this constraints name must be aligned with the + -- acctUpsertQuery const in internal/auth/oidc/query.go + -- ############################################################### + constraint auth_oidc_account_auth_method_id_issuer_subject_uq + unique(auth_method_id, issuer, subject), -- subject must be unique for a provider within specific auth method + constraint auth_oidc_account_auth_method_id_public_id_uq + unique(auth_method_id, public_id) +); +comment on table auth_oidc_method is +'auth_oidc_account entries are subtypes of auth_account and represent an oidc account.'; + +-- auth_oidc_method column triggers +create trigger + insert_auth_method_subtype +before insert on auth_oidc_method + for each row execute procedure insert_auth_method_subtype(); + +create trigger + update_time_column +before +update on auth_oidc_method + for each row execute procedure update_time_column(); + +create trigger + immutable_columns +before +update on auth_oidc_method + for each row execute procedure immutable_columns('public_id', 'scope_id', 'create_time'); + +create trigger + default_create_time_column +before +insert on auth_oidc_method + for each row execute procedure default_create_time(); + +create trigger + update_version_column +after update on auth_oidc_method + for each row execute procedure update_version_column(); + +-- active_auth_oidc_method_must_be_complete() defines a function to be used in +-- a "before update" trigger for auth_oidc_method entries. Its intent: prevent +-- incomplete oidc methods from transitioning out of the "inactive" state. +create or replace function + active_auth_oidc_method_must_be_complete() + returns trigger +as $$ + begin + -- validate signing alg + if old.state = 'inactive' and new.state != 'inactive' then + perform + from + auth_oidc_method am + join auth_oidc_signing_alg alg on am.public_id = alg.oidc_method_id + where + new.public_id = am.public_id; + if not found then + raise exception 'an incomplete oidc auth method must remain inactive'; + end if; + -- validate issuer + case + when new.issuer != old.issuer then + if length(trim(new.issuer)) = 0 then + raise exception 'empty issuer: an incomplete oidc auth method must remain inactive'; + end if; + when new.issuer = old.issuer then + if length(trim(old.issuer)) = 0 then + raise exception 'empty issuer: an incomplete oidc auth method must remain inactive'; + end if; + else + end case; + -- validate client_id + case + when new.client_id != old.client_id then + if length(trim(new.client_id)) = 0 then + raise exception 'empty client_id: an incomplete oidc auth method must remain inactive'; + end if; + when new.client_id = old.client_id then + if length(trim(old.client_id)) = 0 then + raise exception 'empty client_id: an incomplete oidc auth method must remain inactive'; + end if; + else + end case; + -- validate client_secret + case + when new.client_secret != old.client_secret then + if length(new.client_secret) = 0 then + raise exception 'empty client_secret: an incomplete oidc auth method must remain inactive'; + end if; + when new.client_secret = old.client_secret then + if length(old.client_secret) = 0 then + raise exception 'empty client_secret: an incomplete oidc auth method must remain inactive'; + end if; + else + end case; + + + end if; + return new; + end; +$$ language plpgsql; +comment on function active_auth_oidc_method_must_be_complete() is +'active_auth_oidc_method_must_be_complete() will raise an error if the oidc auth method is not complete'; + +create trigger + update_active_auth_oidc_method_must_be_complete +before +update on auth_oidc_method + for each row execute procedure active_auth_oidc_method_must_be_complete(); + +-- new_auth_oidc_method_must_be_inactive() defines a function to be used in +-- a "before insert" trigger for auth_oidc_method entries. Its intent: +-- only allow "inactive" auth methods to be inserted. Why? there's no way +-- you can insert an entry that's anything but incomplete, since we have a +-- chicken/egg problem: you need the auth method id to create the required +-- signing algs value objects. +create or replace function + new_auth_oidc_method_must_be_inactive() + returns trigger +as $$ + begin + if new.state != 'inactive' then + raise exception 'an incomplete oidc method must be inactive'; + end if; + end; +$$ language plpgsql; +comment on function new_auth_oidc_method_must_be_inactive() is +'new_auth_oidc_method_must_be_inactive ensures that new incomplete oidc auth methods must remain inactive'; + +create trigger + new_auth_oidc_method_must_be_inactive +before +insert on auth_oidc_method + for each row execute procedure active_auth_oidc_method_must_be_complete(); + +-- auth_oidc_account column triggers +create trigger + update_time_column +before +update on auth_oidc_account + for each row execute procedure update_time_column(); + +create trigger + immutable_columns +before +update on auth_oidc_account + for each row execute procedure immutable_columns('public_id', 'auth_method_id', 'scope_id', 'create_time', 'issuer', 'subject'); + +create trigger + default_create_time_column +before +insert on auth_oidc_account + for each row execute procedure default_create_time(); + +create trigger + update_version_column +after update on auth_oidc_account + for each row execute procedure update_version_column(); + + +-- insert_auth_oidc_account_subtype is intended as a before insert +-- trigger on auth_oidc_account. Its purpose is to insert a base +-- auth_account for new oidc accounts. It's a bit different than the +-- standard trigger for this, because it will have conflicting PKs +-- and we just want to "do nothing" on those conflicts, deferring the +-- raising on an error to insert into the auth_oidc_account table. +-- this is all necessary because of we're using predictable public ids +-- for oidc accounts. +create or replace function + insert_auth_oidc_account_subtype() + returns trigger +as $$ +begin + select auth_method.scope_id + into new.scope_id + from auth_method + where auth_method.public_id = new.auth_method_id; + + insert into auth_account + (public_id, auth_method_id, scope_id) + values + (new.public_id, new.auth_method_id, new.scope_id) + on conflict do nothing; + + return new; +end; + $$ language plpgsql; + +create trigger + insert_auth_oidc_account_subtype +before insert on auth_oidc_account + for each row execute procedure insert_auth_oidc_account_subtype(); + +-- triggers for auth_oidc_method children tables: auth_oidc_aud_claim, +-- auth_oidc_certificate, auth_oidc_signing_alg + + +-- on_delete_active_auth_oidc_method_must_be_complete() defines a function +-- to be used in an "after delete" trigger for auth_oidc_signing_alg +-- Its intent: prevent deletes that would result in an "active" oidc +-- auth method which is incomplete. +create or replace function + on_delete_active_auth_oidc_method_must_be_complete() + returns trigger +as $$ +declare am_state text; +declare alg_cnt int; + begin + select + am.state, + count(alg.oidc_method_id) as alg_cnt + from + auth_oidc_method am + left outer join auth_oidc_signing_alg alg on am.public_id = alg.oidc_method_id + where + new.oidc_method_id = am.public_id + group by am.public_id + into am_state, alg_cnt; + + if not found then + return new; -- auth method was deleted, so we're done + end if; + + if am_state != inactive then + case + when alg_cnt = 0 then + raise exception 'delete would have resulted in an incomplete active oidc auth method with no signing algorithms'; + end case; + end if; + + return new; + end; +$$ language plpgsql; +comment on function on_delete_active_auth_oidc_method_must_be_complete() is +'on_delete_active_auth_oidc_method_must_be_complete() will raise an error if the oidc auth method is not complete after a delete on algs'; + +create trigger + default_create_time_column +before +insert on auth_oidc_aud_claim + for each row execute procedure default_create_time(); + +create trigger + default_create_time_column +before +insert on auth_oidc_certificate + for each row execute procedure default_create_time(); + +create trigger + default_create_time_column +before +insert on auth_oidc_signing_alg + for each row execute procedure default_create_time(); + +create trigger + on_delete_active_auth_oidc_method_must_be_complete +after +delete on auth_oidc_signing_alg + for each row execute procedure on_delete_active_auth_oidc_method_must_be_complete(); + +insert into oplog_ticket (name, version) +values + ('auth_oidc_method', 1), -- auth method is the root aggregate itself and all of its value objects. + ('auth_oidc_account', 1); +`), + 2085: []byte(` +-- auth_token_status_enm entries define the possible auth token +-- states. +create table auth_token_status_enm ( + name text primary key + constraint name_only_predefined_auth_token_states_allowed + check ( + name in ('auth token pending','token issued', 'authentication failed', 'system error') + ) +); + +-- populate the values of auth_token_status_enm +insert into auth_token_status_enm(name) + values + ('auth token pending'), + ('token issued'), + ('authentication failed'), + ('system error'); + + +-- add the state column with a default to the auth_token table. +alter table auth_token +add column status text +not null +default 'token issued' -- safest default +references auth_token_status_enm(name) + on update cascade + on delete restrict; + + +create or replace view auth_token_account as + select at.public_id, + at.token, + at.auth_account_id, + at.create_time, + at.update_time, + at.approximate_last_access_time, + at.expiration_time, + aa.scope_id, + aa.iam_user_id, + aa.auth_method_id, + at.status + from auth_token as at + inner join auth_account as aa + on at.auth_account_id = aa.public_id; +`), + 2086: []byte(` +-- add the primary_auth_method_id which determines which auth_method is +-- designated as for "account info" in the user's scope. It also determines +-- which auth method is allowed to auto viviify users. +alter table iam_scope +add column primary_auth_method_id wt_public_id -- allowed to be null and is mutable of course. +constraint auth_method_fkey +references auth_method(public_id) + on update cascade + on delete set null; + +-- establish a compond fk, but there's no cascading of deletes or updates, since +-- we only want to cascade changes to the primary_auth_method_id portion of +-- the compond fk and that is handled in a separate fk declaration. +alter table iam_scope +add constraint auth_method + foreign key (public_id, primary_auth_method_id) + references auth_method(scope_id, public_id); + +-- iam_user_acct_info provides account info for users by determining which +-- auth_method is designated as for "account info" in the user's scope via the +-- scope's primary_auth_method_id. Every sub-type of auth_account must be +-- added to this view's union. +create view iam_acct_info as +select + aa.iam_user_id, + oa.subject as login_name, + oa.full_name as full_name, + oa.email as email +from + iam_scope s, + auth_account aa, + auth_oidc_account oa +where + aa.public_id = oa.public_id and + aa.auth_method_id = s.primary_auth_method_id +union +select + aa.iam_user_id, + pa.login_name, + '' as full_name, + '' as email +from + iam_scope s, + auth_account aa, + auth_password_account pa +where + aa.public_id = pa.public_id and + aa.auth_method_id = s.primary_auth_method_id; + +-- iam_user_acct_info provides a simple way to retrieve entries that include +-- both the iam_user fields with an outer join to the user's account info. +create view iam_user_acct_info as +select + u.public_id, + u.scope_id, + u.name, + u.description, + u.create_time, + u.update_time, + u.version, + i.login_name, + i.full_name, + i.email +from + iam_user u +left outer join iam_acct_info i on u.public_id = i.iam_user_id; +`), + 2087: []byte(` +-- the intent of this update statement: set the primary auth method for scopes +-- that only have a single auth_password_method, since currently there are only +-- auth_password_methods in boundary. Before this release all +-- auth_password_methods were "basically" primary auth methods and would create +-- an iam_user on first login. +with single_authmethod (scope_id, public_id) as ( + select + am.scope_id, + am.public_id + from + auth_password_method am, + (select + scope_id, + count(public_id) as cnt + from + auth_password_method + group by scope_id) as singles + where + am.scope_id = singles.scope_id and + singles.cnt = 1 +) +update + iam_scope +set + primary_auth_method_id = p.public_id +from + single_authmethod p +where p.scope_id = iam_scope.public_id; + + +-- the intent of the insert with select statement: log the scopes that have more +-- than 1 auth method and therefore cannot have their primary auth method +-- automatically set for them. +with many_authmethod (scope_id, authmethod_cnt) as ( + select + am.scope_id, + many.cnt + from + auth_password_method am, + (select + scope_id, + count(public_id) as cnt + from + auth_password_method + group by scope_id) as many + where + am.scope_id = many.scope_id and + many.cnt > 1 +) +insert into log_migration(entry) +select + distinct concat( + 'unable to set primary_auth_method for ', + public_id, + ' there were ', + m.authmethod_cnt, + ' password auth methods for that scope.' + ) as entry +from + iam_scope s, + many_authmethod m +where + s.primary_auth_method_id is null and + s.public_id = m.scope_id; +`), + 2090: []byte(` +-- By adding the name column to the base auth method type, the database can +-- ensure that auth method names are unique across all sub types. +alter table auth_method + add column name wt_name; + +alter table auth_method + add constraint auth_method_scope_id_name_uq + unique (scope_id, name); + + +-- the intent of this statement is to update the base type's name with the +-- existing password auth method names. +update auth_method +set name = pw.name +from + auth_password_method pw +where + auth_method.public_id = pw.public_id and + pw.name is not null; + +-- insert_auth_method_subtype() is a replacement of the function definition in +-- migration 07_auth.up.sql This new definition also inserts the sub type's name +-- into the base type. The name column must be on the base type, so the database +-- can ensure that auth method names are unique across all sub types. +create or replace function + insert_auth_method_subtype() + returns trigger +as $$ +begin + insert into auth_method + (public_id, scope_id, name) + values + (new.public_id, new.scope_id, new.name); + return new; +end; +$$ language plpgsql; +comment on function insert_auth_method_subtype() is +'insert_auth_method_subtype() inserts sub type name into the base type auth method table'; + +-- update_auth_method_subtype() is a new function intended to be used in "before +-- update" triggers for all auth method sub types. It's purpose is to ensure +-- that the name column is syncronized between the sub and base auth method +-- types. The name column must be on the base type, so the database can ensure +-- that auth method names are unique across all sub types. +create or replace function + update_auth_method_subtype() + returns trigger +as $$ +begin + update auth_method set name = new.name where public_id = new.public_id and new.name != name; + return new; +end; +$$ language plpgsql; +comment on function update_auth_method_subtype() is +'update_auth_method_subtype() will update base auth method type name column with new values from sub type'; + +create trigger + update_auth_method_subtype +before update on auth_oidc_method + for each row execute procedure update_auth_method_subtype(); + + +create trigger + update_auth_method_subtype +before update on auth_password_method + for each row execute procedure update_auth_method_subtype(); + + +-- delete_auth_method_subtype() is an after trigger function for subytypes of +-- auth_method +create or replace function + delete_auth_method_subtype() + returns trigger +as $$ +begin + delete from auth_method + where public_id = old.public_id; + return null; -- results are ignore since this is an after trigger. +end; +$$ language plpgsql; +comment on function delete_auth_method_subtype is +'delete_auth_method_subtype() is an after trigger function for subytypes of auth_method'; + +create trigger + delete_auth_method_subtype +after delete on auth_oidc_method + for each row execute procedure delete_auth_method_subtype(); + + +create trigger + delete_auth_method_subtype +after delete on auth_password_method + for each row execute procedure delete_auth_method_subtype(); +`), + 2095: []byte(` +-- oidc_auth_method_with_value_obj is useful for reading an oidc auth method +-- with its associated value objects (algs, auds, certs) as columns +-- with | delimited values. The use of the postgres string_agg(...) to +-- aggregate the value objects into a column works because we are only pulling +-- in one column from the associated tables and that value is part of the +-- primary key and unique. This view will make things like recursive listing of +-- oidc auth methods fairly straightforward to implement for the oidc repo. +-- The view also includes an is_primary_auth_method bool +create view oidc_auth_method_with_value_obj as +select + case when s.primary_auth_method_id is not null then + true + else false end + as is_primary_auth_method, + am.public_id, + am.scope_id, + am.name, + am.description, + am.create_time, + am.update_time, + am.version, + am.state, + am.api_url, + am.disable_discovered_config_validation, + am.issuer, + am.client_id, + am.client_secret, + am.client_secret_hmac, + am.key_id, + am.max_age, + -- the string_agg(..) column will be null if there are no associated value objects + string_agg(distinct alg.signing_alg_name, '|') as algs, + string_agg(distinct aud.aud_claim, '|') as auds, + string_agg(distinct cert.certificate, '|') as certs +from + auth_oidc_method am + left outer join iam_scope s on am.public_id = s.primary_auth_method_id + left outer join auth_oidc_signing_alg alg on am.public_id = alg.oidc_method_id + left outer join auth_oidc_aud_claim aud on am.public_id = aud.oidc_method_id + left outer join auth_oidc_certificate cert on am.public_id = cert.oidc_method_id +group by am.public_id, is_primary_auth_method; -- there can be only one public_id + is_primary_auth_method, so group by isn't a problem. +comment on view oidc_auth_method_with_value_obj is +'oidc auth method with its associated value objects (algs, auds, certs) as columns with | delimited values'; `), }, } diff --git a/internal/db/schema/schema.go b/internal/db/schema/schema.go index 3ac796acb..9e1d53225 100644 --- a/internal/db/schema/schema.go +++ b/internal/db/schema/schema.go @@ -9,8 +9,8 @@ import ( // MigrateStore executes the migrations needed to initialize the store. It // returns true if migrations actually ran; false if the database is already current -// or if there was an error. -func MigrateStore(ctx context.Context, dialect string, url string) (bool, error) { +// or if there was an error. Supports the WithMigrationStates(...) option. +func MigrateStore(ctx context.Context, dialect string, url string, opt ...Option) (bool, error) { const op = "schema.MigrateStore" d, err := sql.Open(dialect, url) @@ -18,7 +18,7 @@ func MigrateStore(ctx context.Context, dialect string, url string) (bool, error) return false, errors.Wrap(err, op) } - sMan, err := NewManager(ctx, dialect, d) + sMan, err := NewManager(ctx, dialect, d, opt...) if err != nil { return false, errors.Wrap(err, op) } diff --git a/internal/db/schema/schema_test.go b/internal/db/schema/schema_test.go index a877350a1..e921c9060 100644 --- a/internal/db/schema/schema_test.go +++ b/internal/db/schema/schema_test.go @@ -2,6 +2,7 @@ package schema import ( "context" + "database/sql" "testing" "github.com/hashicorp/boundary/internal/docker" @@ -41,3 +42,33 @@ func TestMigrateStore(t *testing.T) { assert.NoError(t, err) assert.False(t, ran) } + +func Test_MigrateStore_WithMigrationStates(t *testing.T) { + assert, require := assert.New(t), require.New(t) + dialect := "postgres" + ctx := context.Background() + + c, u, _, err := docker.StartDbInDocker(dialect) + require.NoError(err) + t.Cleanup(func() { + require.NoError(c()) + }) + d, err := sql.Open(dialect, u) + require.NoError(err) + + // migration to the prior migration (before the one we want to test) + oState := TestCloneMigrationStates(t) + nState := TestCreatePartialMigrationState(oState["postgres"], 8) + oState["postgres"] = nState + + ran, err := MigrateStore(ctx, dialect, u, WithMigrationStates(oState)) + assert.NoError(err) + assert.True(ran) + + m, err := NewManager(ctx, dialect, d, WithMigrationStates(oState)) + require.NoError(err) + state, err := m.CurrentState(ctx) + require.NoError(err) + assert.Equal(8, state.DatabaseSchemaVersion) + assert.False(state.Dirty) +} diff --git a/internal/db/schema/state.go b/internal/db/schema/state.go index f2af40a24..1dc1e3845 100644 --- a/internal/db/schema/state.go +++ b/internal/db/schema/state.go @@ -15,8 +15,15 @@ type migrationState struct { // migrationStates is populated by the generated migration code with the key being the dialect. var migrationStates = make(map[string]migrationState) -func getUpMigration(dialect string) map[int][]byte { - ms, ok := migrationStates[dialect] +func getUpMigration(dialect string, opt ...Option) map[int][]byte { + opts := getOpts(opt...) + var ms migrationState + var ok bool + if opts.withMigrationStates != nil { + ms, ok = opts.withMigrationStates[dialect] + } else { + ms, ok = migrationStates[dialect] + } if !ok { return nil } @@ -32,3 +39,20 @@ func BinarySchemaVersion(dialect string) int { } return ms.binarySchemaVersion } + +func cloneMigrationStates(states map[string]migrationState) map[string]migrationState { + nStates := map[string]migrationState{} + for k, s := range states { + newState := migrationState{ + binarySchemaVersion: s.binarySchemaVersion, + upMigrations: map[int][]byte{}, + } + for v, up := range s.upMigrations { + cp := make([]byte, len(up)) + copy(cp, up) + newState.upMigrations[v] = cp + } + nStates[k] = newState + } + return nStates +} diff --git a/internal/db/schema/statement.go b/internal/db/schema/statement.go index 28cf57840..63ce8028e 100644 --- a/internal/db/schema/statement.go +++ b/internal/db/schema/statement.go @@ -13,10 +13,10 @@ type statementProvider struct { up map[int][]byte } -func newStatementProvider(dialect string, curVer int) *statementProvider { +func newStatementProvider(dialect string, curVer int, opt ...Option) *statementProvider { const op = "schema.newStatementProvider" qp := statementProvider{pos: -1} - qp.up = getUpMigration(dialect) + qp.up = getUpMigration(dialect, opt...) for k := range qp.up { qp.versions = append(qp.versions, k) } diff --git a/internal/db/schema/testing.go b/internal/db/schema/testing.go new file mode 100644 index 000000000..38edea0a8 --- /dev/null +++ b/internal/db/schema/testing.go @@ -0,0 +1,27 @@ +package schema + +import ( + "testing" +) + +// Creates a new migrationState only with the versions <= the provided maxVer +func TestCreatePartialMigrationState(om migrationState, maxVer int) migrationState { + nState := migrationState{ + upMigrations: make(map[int][]byte), + } + for k := range om.upMigrations { + if k > maxVer { + // Don't store any versions past our test version. + continue + } + nState.upMigrations[k] = om.upMigrations[k] + if nState.binarySchemaVersion < k { + nState.binarySchemaVersion = k + } + } + return nState +} + +func TestCloneMigrationStates(t *testing.T) map[string]migrationState { + return cloneMigrationStates(migrationStates) +} diff --git a/internal/errors/code.go b/internal/errors/code.go index 9fcae80cd..43464e8ba 100644 --- a/internal/errors/code.go +++ b/internal/errors/code.go @@ -37,6 +37,9 @@ const ( TooShort Code = 113 // TooShort represents an error that means the provided input is not meeting minimum length requirements AccountAlreadyAssociated Code = 114 // AccountAlreadyAssociated represents an attempt to associate an account failed since it was already associated. + AuthAttemptExpired Code = 198 // AuthAttemptExpired represents an expired authentication attempt + AuthMethodInactive Code = 199 // AuthMethodInactive represents an error that means the auth method is not active. + // PasswordTooShort results from attempting to set a password which is to short. PasswordTooShort Code = 200 @@ -74,6 +77,7 @@ const ( ColumnNotFound Code = 1102 // ColumnNotFound represent that a column was not found in the underlying db MaxRetries Code = 1103 // MaxRetries represent that a db Tx hit max retires allowed Exception Code = 1104 // Exception represent that an underlying db exception was raised + VersionMismatch Code = 1105 // VersionMismatch represents the update version and the db version for an entry do not match. // Migration setup errors are codes 2000-2999 MigrationIntegrity Code = 2000 // MigrationIntegrity represents an error with the generated migration related code diff --git a/internal/errors/code_test.go b/internal/errors/code_test.go index a58a07dcf..8499adc9a 100644 --- a/internal/errors/code_test.go +++ b/internal/errors/code_test.go @@ -112,6 +112,16 @@ func TestCode_Both_String_Info(t *testing.T) { c: Forbidden, want: Forbidden, }, + { + name: "AuthMethodInactive", + c: AuthMethodInactive, + want: AuthMethodInactive, + }, + { + name: "AuthAttemptExpired", + c: AuthAttemptExpired, + want: AuthAttemptExpired, + }, { name: "PasswordTooShort", c: PasswordTooShort, @@ -197,6 +207,11 @@ func TestCode_Both_String_Info(t *testing.T) { c: Exception, want: Exception, }, + { + name: "VersionMismatch", + c: VersionMismatch, + want: VersionMismatch, + }, { name: "MultipleRecords", c: MultipleRecords, diff --git a/internal/errors/info.go b/internal/errors/info.go index e3a8d22f0..b0990dacd 100644 --- a/internal/errors/info.go +++ b/internal/errors/info.go @@ -72,6 +72,14 @@ var errorCodeInfo = map[Code]Info{ Message: "too short", Kind: Integrity, }, + AuthMethodInactive: { + Message: "authentication method is inactive", + Kind: State, + }, + AuthAttemptExpired: { + Message: "authentication attempt has expired", + Kind: State, + }, AccountAlreadyAssociated: { Message: "account already associated with another user", Kind: Parameter, @@ -160,6 +168,10 @@ var errorCodeInfo = map[Code]Info{ Message: "db exception", Kind: Integrity, }, + VersionMismatch: { + Message: "version mismatch", + Kind: Integrity, + }, MaxRetries: { Message: "too many retries", Kind: Transaction, diff --git a/internal/errors/kind.go b/internal/errors/kind.go index 1cb36ff1c..ca8d5c482 100644 --- a/internal/errors/kind.go +++ b/internal/errors/kind.go @@ -12,6 +12,7 @@ const ( Transaction Encryption Encoding + State External ) @@ -25,6 +26,7 @@ func (e Kind) String() string { Transaction: "db transaction issue", Encryption: "encryption issue", Encoding: "encoding issue", + State: "state violation", External: "external system issue", }[e] } diff --git a/internal/gen/controller.swagger.json b/internal/gen/controller.swagger.json index f13b730d2..edd7c51f3 100644 --- a/internal/gen/controller.swagger.json +++ b/internal/gen/controller.swagger.json @@ -328,9 +328,9 @@ "operationId": "AuthMethodService_Authenticate", "responses": { "200": { - "description": "", + "description": "A successful response.", "schema": { - "$ref": "#/definitions/controller.api.resources.authtokens.v1.AuthToken" + "$ref": "#/definitions/controller.api.services.v1.AuthenticateResponse" } } }, @@ -475,6 +475,39 @@ ] } }, + "/v1/auth-methods/{id}:change-state": { + "post": { + "summary": "Changes the state of an OIDC AuthMethod", + "operationId": "AuthMethodService_ChangeState", + "responses": { + "200": { + "description": "", + "schema": { + "$ref": "#/definitions/controller.api.resources.authmethods.v1.AuthMethod" + } + } + }, + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "type": "string" + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/controller.api.services.v1.ChangeStateRequest" + } + } + ], + "tags": [ + "controller.api.services.v1.AuthMethodService" + ] + } + }, "/v1/auth-tokens": { "get": { "summary": "Lists all Auth Tokens.", @@ -2572,6 +2605,11 @@ "type": "object", "description": "The attributes that are applicable for the specific Auth Method type." }, + "is_primary": { + "type": "boolean", + "description": "Output only. Whether this auth method is the primary auth method for it's scope.\nTo change this value update the primary_auth_method_id field on the scope.", + "readOnly": true + }, "authorized_actions": { "type": "array", "items": { @@ -3158,6 +3196,10 @@ "type": "string", "description": "The type of the resource." }, + "primary_auth_method_id": { + "type": "string", + "title": "The ID of the primary auth method for this scope. A primary auth method\nis allowed to vivify users when new accounts are created and is the source for the users account info" + }, "authorized_actions": { "type": "array", "items": { @@ -3777,7 +3819,7 @@ }, "token_type": { "type": "string", - "description": "This can be \"cookie\" or \"token\". If not provided, \"token\" will be used. \"cookie\" activates a split-cookie method where the token is split partially between http-only and regular cookies in order to keep it safe from rogue JS in the browser." + "description": "This can be \"cookie\" or \"token\". If not provided, \"token\" will be used. \"cookie\" activates a split-cookie method where the token is split partially between http-only and regular cookies in order\nto keep it safe from rogue JS in the browser." }, "credentials": { "type": "object", @@ -3805,7 +3847,7 @@ }, "token_type": { "type": "string", - "description": "This can be \"cookie\" or \"token\". If not provided, \"token\" will be used. \"cookie\" activates a split-cookie method where the token is split partially between http-only and regular cookies in order to keep it safe from rogue JS in the browser." + "description": "This can be \"cookie\" or \"token\". If not provided, \"token\" will be used. \"cookie\" activates a split-cookie method where the token is split partially between http-only and regular cookies in order\nto keep it safe from rogue JS in the browser." }, "credentials": { "type": "object", @@ -3824,11 +3866,13 @@ "controller.api.services.v1.AuthenticateResponse": { "type": "object", "properties": { - "item": { - "$ref": "#/definitions/controller.api.resources.authtokens.v1.AuthToken" + "attributes": { + "type": "object", + "description": "Valid keys and values depend on the type of Auth Method as well as the command." }, - "token_type": { - "type": "string" + "command": { + "type": "string", + "description": "The command that was performed." } } }, @@ -3912,6 +3956,31 @@ } } }, + "controller.api.services.v1.ChangeStateRequest": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "version": { + "type": "integer", + "format": "int64", + "description": "Version is used to ensure this resource has not changed.\nThe mutation will fail if the version does not match the latest known good version." + }, + "attributes": { + "type": "object", + "description": "The attributes specific to this auth method's state." + } + } + }, + "controller.api.services.v1.ChangeStateResponse": { + "type": "object", + "properties": { + "item": { + "$ref": "#/definitions/controller.api.resources.authmethods.v1.AuthMethod" + } + } + }, "controller.api.services.v1.CreateAccountResponse": { "type": "object", "properties": { diff --git a/internal/gen/controller/api/resources/accounts/account.pb.go b/internal/gen/controller/api/resources/accounts/account.pb.go index 771bf2b96..f29c54375 100644 --- a/internal/gen/controller/api/resources/accounts/account.pb.go +++ b/internal/gen/controller/api/resources/accounts/account.pb.go @@ -165,6 +165,7 @@ func (x *Account) GetAuthorizedActions() []string { return nil } +// Attributes associated only with Accounts with type "password". type PasswordAccountAttributes struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -222,6 +223,84 @@ func (x *PasswordAccountAttributes) GetPassword() *wrappers.StringValue { return nil } +// Attributes associated only with Accounts with type "oidc". +type OidcAccountAttributes struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // issuer is a case sensitive URL that maps to the OIDC iss claim. + // This value is immutable after creation time. + Issuer string `protobuf:"bytes,80,opt,name=issuer,proto3" json:"issuer,omitempty"` + // subject is a case sensitive string that maps to the OIDC sub claim. + // This value is immutable after creation time. + Subject string `protobuf:"bytes,90,opt,name=subject,proto3" json:"subject,omitempty"` + // Output only. full_name is a string that maps to the OIDC name claim + FullName string `protobuf:"bytes,100,opt,name=full_name,proto3" json:"full_name,omitempty"` + // Output only. email is a string that maps to the OIDC email claim. + Email string `protobuf:"bytes,110,opt,name=email,proto3" json:"email,omitempty"` +} + +func (x *OidcAccountAttributes) Reset() { + *x = OidcAccountAttributes{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_api_resources_accounts_v1_account_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *OidcAccountAttributes) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OidcAccountAttributes) ProtoMessage() {} + +func (x *OidcAccountAttributes) ProtoReflect() protoreflect.Message { + mi := &file_controller_api_resources_accounts_v1_account_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OidcAccountAttributes.ProtoReflect.Descriptor instead. +func (*OidcAccountAttributes) Descriptor() ([]byte, []int) { + return file_controller_api_resources_accounts_v1_account_proto_rawDescGZIP(), []int{2} +} + +func (x *OidcAccountAttributes) GetIssuer() string { + if x != nil { + return x.Issuer + } + return "" +} + +func (x *OidcAccountAttributes) GetSubject() string { + if x != nil { + return x.Subject + } + return "" +} + +func (x *OidcAccountAttributes) GetFullName() string { + if x != nil { + return x.FullName + } + return "" +} + +func (x *OidcAccountAttributes) GetEmail() string { + if x != nil { + return x.Email + } + return "" +} + var File_controller_api_resources_accounts_v1_account_proto protoreflect.FileDescriptor var file_controller_api_resources_accounts_v1_account_proto_rawDesc = []byte{ @@ -252,12 +331,12 @@ var file_controller_api_resources_accounts_v1_account_proto_rawDesc = []byte{ 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x14, 0xa0, 0xda, 0x29, 0x01, 0xc2, 0xdd, 0x29, 0x0c, 0x0a, 0x04, 0x6e, 0x61, - 0x6d, 0x65, 0x12, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x62, + 0x6d, 0x65, 0x12, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x62, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x28, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x22, 0xa0, 0xda, 0x29, 0x01, 0xc2, 0xdd, 0x29, 0x1a, 0x0a, 0x0b, 0x64, 0x65, 0x73, - 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, + 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0b, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x3e, 0x0a, 0x0c, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x32, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, @@ -290,13 +369,21 @@ var file_controller_api_resources_accounts_v1_account_proto_rawDesc = []byte{ 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x04, 0xa0, 0xda, 0x29, 0x01, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, - 0x72, 0x64, 0x42, 0x57, 0x5a, 0x55, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, - 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, - 0x61, 0x72, 0x79, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x67, 0x65, 0x6e, - 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x61, 0x70, 0x69, 0x2f, - 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2f, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, - 0x74, 0x73, 0x3b, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x33, + 0x72, 0x64, 0x22, 0x89, 0x01, 0x0a, 0x15, 0x4f, 0x69, 0x64, 0x63, 0x41, 0x63, 0x63, 0x6f, 0x75, + 0x6e, 0x74, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0x1c, 0x0a, 0x06, + 0x69, 0x73, 0x73, 0x75, 0x65, 0x72, 0x18, 0x50, 0x20, 0x01, 0x28, 0x09, 0x42, 0x04, 0xa0, 0xda, + 0x29, 0x01, 0x52, 0x06, 0x69, 0x73, 0x73, 0x75, 0x65, 0x72, 0x12, 0x1e, 0x0a, 0x07, 0x73, 0x75, + 0x62, 0x6a, 0x65, 0x63, 0x74, 0x18, 0x5a, 0x20, 0x01, 0x28, 0x09, 0x42, 0x04, 0xa0, 0xda, 0x29, + 0x01, 0x52, 0x07, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x66, 0x75, + 0x6c, 0x6c, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x64, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x66, + 0x75, 0x6c, 0x6c, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x6d, 0x61, 0x69, + 0x6c, 0x18, 0x6e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x6d, 0x61, 0x69, 0x6c, 0x42, 0x57, + 0x5a, 0x55, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, + 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, + 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x63, 0x6f, 0x6e, + 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x72, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x73, 0x2f, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x3b, 0x61, + 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -311,23 +398,24 @@ func file_controller_api_resources_accounts_v1_account_proto_rawDescGZIP() []byt return file_controller_api_resources_accounts_v1_account_proto_rawDescData } -var file_controller_api_resources_accounts_v1_account_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_controller_api_resources_accounts_v1_account_proto_msgTypes = make([]protoimpl.MessageInfo, 3) var file_controller_api_resources_accounts_v1_account_proto_goTypes = []interface{}{ (*Account)(nil), // 0: controller.api.resources.accounts.v1.Account (*PasswordAccountAttributes)(nil), // 1: controller.api.resources.accounts.v1.PasswordAccountAttributes - (*scopes.ScopeInfo)(nil), // 2: controller.api.resources.scopes.v1.ScopeInfo - (*wrappers.StringValue)(nil), // 3: google.protobuf.StringValue - (*timestamp.Timestamp)(nil), // 4: google.protobuf.Timestamp - (*_struct.Struct)(nil), // 5: google.protobuf.Struct + (*OidcAccountAttributes)(nil), // 2: controller.api.resources.accounts.v1.OidcAccountAttributes + (*scopes.ScopeInfo)(nil), // 3: controller.api.resources.scopes.v1.ScopeInfo + (*wrappers.StringValue)(nil), // 4: google.protobuf.StringValue + (*timestamp.Timestamp)(nil), // 5: google.protobuf.Timestamp + (*_struct.Struct)(nil), // 6: google.protobuf.Struct } var file_controller_api_resources_accounts_v1_account_proto_depIdxs = []int32{ - 2, // 0: controller.api.resources.accounts.v1.Account.scope:type_name -> controller.api.resources.scopes.v1.ScopeInfo - 3, // 1: controller.api.resources.accounts.v1.Account.name:type_name -> google.protobuf.StringValue - 3, // 2: controller.api.resources.accounts.v1.Account.description:type_name -> google.protobuf.StringValue - 4, // 3: controller.api.resources.accounts.v1.Account.created_time:type_name -> google.protobuf.Timestamp - 4, // 4: controller.api.resources.accounts.v1.Account.updated_time:type_name -> google.protobuf.Timestamp - 5, // 5: controller.api.resources.accounts.v1.Account.attributes:type_name -> google.protobuf.Struct - 3, // 6: controller.api.resources.accounts.v1.PasswordAccountAttributes.password:type_name -> google.protobuf.StringValue + 3, // 0: controller.api.resources.accounts.v1.Account.scope:type_name -> controller.api.resources.scopes.v1.ScopeInfo + 4, // 1: controller.api.resources.accounts.v1.Account.name:type_name -> google.protobuf.StringValue + 4, // 2: controller.api.resources.accounts.v1.Account.description:type_name -> google.protobuf.StringValue + 5, // 3: controller.api.resources.accounts.v1.Account.created_time:type_name -> google.protobuf.Timestamp + 5, // 4: controller.api.resources.accounts.v1.Account.updated_time:type_name -> google.protobuf.Timestamp + 6, // 5: controller.api.resources.accounts.v1.Account.attributes:type_name -> google.protobuf.Struct + 4, // 6: controller.api.resources.accounts.v1.PasswordAccountAttributes.password:type_name -> google.protobuf.StringValue 7, // [7:7] is the sub-list for method output_type 7, // [7:7] is the sub-list for method input_type 7, // [7:7] is the sub-list for extension type_name @@ -365,6 +453,18 @@ func file_controller_api_resources_accounts_v1_account_proto_init() { return nil } } + file_controller_api_resources_accounts_v1_account_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*OidcAccountAttributes); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } } type x struct{} out := protoimpl.TypeBuilder{ @@ -372,7 +472,7 @@ func file_controller_api_resources_accounts_v1_account_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_controller_api_resources_accounts_v1_account_proto_rawDesc, NumEnums: 0, - NumMessages: 2, + NumMessages: 3, NumExtensions: 0, NumServices: 0, }, diff --git a/internal/gen/controller/api/resources/authmethods/auth_method.pb.go b/internal/gen/controller/api/resources/authmethods/auth_method.pb.go index 55e926c7d..265044bb2 100644 --- a/internal/gen/controller/api/resources/authmethods/auth_method.pb.go +++ b/internal/gen/controller/api/resources/authmethods/auth_method.pb.go @@ -52,6 +52,9 @@ type AuthMethod struct { Type string `protobuf:"bytes,90,opt,name=type,proto3" json:"type,omitempty"` // The attributes that are applicable for the specific Auth Method type. Attributes *_struct.Struct `protobuf:"bytes,100,opt,name=attributes,proto3" json:"attributes,omitempty"` + // Output only. Whether this auth method is the primary auth method for it's scope. + // To change this value update the primary_auth_method_id field on the scope. + IsPrimary bool `protobuf:"varint,110,opt,name=is_primary,proto3" json:"is_primary,omitempty"` // Output only. The available actions on this resource for this user. AuthorizedActions []string `protobuf:"bytes,300,rep,name=authorized_actions,proto3" json:"authorized_actions,omitempty"` // Output only. The authorized actions for the scope's collections. @@ -160,6 +163,13 @@ func (x *AuthMethod) GetAttributes() *_struct.Struct { return nil } +func (x *AuthMethod) GetIsPrimary() bool { + if x != nil { + return x.IsPrimary + } + return false +} + func (x *AuthMethod) GetAuthorizedActions() []string { if x != nil { return x.AuthorizedActions @@ -174,6 +184,7 @@ func (x *AuthMethod) GetAuthorizedCollectionActions() map[string]*_struct.ListVa return nil } +// The attributes of a password typed auth method. type PasswordAuthMethodAttributes struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -231,6 +242,435 @@ func (x *PasswordAuthMethodAttributes) GetMinPasswordLength() uint32 { return 0 } +// The attributes of an OIDC typed auth method. +type OidcAuthMethodAttributes struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Output only. The state of the auth method. Will be "inactive", + // "active-private", or "active-public". + State string `protobuf:"bytes,10,opt,name=state,proto3" json:"state,omitempty"` + // The issuer URL. Boundary expects only the schema, host, and port and will + // strip off ".well-known/openid-configuration" if present. This will be + // used for configuration discovery as well as for validation of the "iss" + // claim. + Issuer *wrappers.StringValue `protobuf:"bytes,20,opt,name=issuer,proto3" json:"issuer,omitempty"` + // An OAuth 2.0 Client Identifier valid at the Authorization Server. + ClientId *wrappers.StringValue `protobuf:"bytes,30,opt,name=client_id,proto3" json:"client_id,omitempty"` + // Input only. The client's secret. + ClientSecret *wrappers.StringValue `protobuf:"bytes,40,opt,name=client_secret,proto3" json:"client_secret,omitempty"` + // Output only. The HMAC'd value of the clients secret to indicate whether + // the client secret has changed. + ClientSecretHmac string `protobuf:"bytes,50,opt,name=client_secret_hmac,proto3" json:"client_secret_hmac,omitempty"` + // The OIDC "max_age" parameter sent to the Authorization Server indicating + // a maximum acceptable time in seconds since the user's last authentication + // before requiring the user to reauthenticate. 0 indicates an immediate + // need to reauthenticate. + MaxAge *wrappers.UInt32Value `protobuf:"bytes,60,opt,name=max_age,proto3" json:"max_age,omitempty"` + // The signing algorithms allowed for the auth method. + SigningAlgorithms []string `protobuf:"bytes,70,rep,name=signing_algorithms,proto3" json:"signing_algorithms,omitempty"` + // The prefix that should be used for any URLs needed during the + // authentication flow. This includes the callback URL, the token retrieval + // URL, and the redirection URL used by the OIDC Authorization Server. + ApiUrlPrefix *wrappers.StringValue `protobuf:"bytes,80,opt,name=api_url_prefix,proto3" json:"api_url_prefix,omitempty"` + // Output only. The callback URL that should be configured on the + // Authorization Server to use during the authentication flow. + CallbackUrl string `protobuf:"bytes,90,opt,name=callback_url,proto3" json:"callback_url,omitempty"` + // Optional PEM-encoded X.509 CA certificates that can be used as trust anchors + // when connecting to an OIDC provider. + IdpCaCerts []string `protobuf:"bytes,100,rep,name=idp_ca_certs,proto3" json:"idp_ca_certs,omitempty"` + // Allowed audience claims for this auth method. + AllowedAudiences []string `protobuf:"bytes,110,rep,name=allowed_audiences,proto3" json:"allowed_audiences,omitempty"` + // If the Authorization Server's discovered configuration contains values + // that do not match the configuration set on this auth method, this can be + // set to force the local configuration to override the discovered values. + // This is useful in the case of slow-changing or incorrect values announced + // by the Authorization Server. NOTE: For safety, not providing this value + // will be treated the same as "false"; that is, every time the method is + // updated or the state is changed, this value must be set to "true" or it + // will be disabled. + DisableDiscoveredConfigValidation bool `protobuf:"varint,120,opt,name=disable_discovered_config_validation,proto3" json:"disable_discovered_config_validation,omitempty"` + // dry_run, when set on an update request, indicates that the changes should + // not be persisted. Boundary will still perform the normal checks to confirm + // the auth method is complete and validated against the discovered config. + // This value will also be set on the returned resource when set in the request + // along with the updated fields applied to the resource (but not persisted) as + // a result of the update request. + DryRun bool `protobuf:"varint,130,opt,name=dry_run,proto3" json:"dry_run,omitempty"` +} + +func (x *OidcAuthMethodAttributes) Reset() { + *x = OidcAuthMethodAttributes{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_api_resources_authmethods_v1_auth_method_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *OidcAuthMethodAttributes) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OidcAuthMethodAttributes) ProtoMessage() {} + +func (x *OidcAuthMethodAttributes) ProtoReflect() protoreflect.Message { + mi := &file_controller_api_resources_authmethods_v1_auth_method_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OidcAuthMethodAttributes.ProtoReflect.Descriptor instead. +func (*OidcAuthMethodAttributes) Descriptor() ([]byte, []int) { + return file_controller_api_resources_authmethods_v1_auth_method_proto_rawDescGZIP(), []int{2} +} + +func (x *OidcAuthMethodAttributes) GetState() string { + if x != nil { + return x.State + } + return "" +} + +func (x *OidcAuthMethodAttributes) GetIssuer() *wrappers.StringValue { + if x != nil { + return x.Issuer + } + return nil +} + +func (x *OidcAuthMethodAttributes) GetClientId() *wrappers.StringValue { + if x != nil { + return x.ClientId + } + return nil +} + +func (x *OidcAuthMethodAttributes) GetClientSecret() *wrappers.StringValue { + if x != nil { + return x.ClientSecret + } + return nil +} + +func (x *OidcAuthMethodAttributes) GetClientSecretHmac() string { + if x != nil { + return x.ClientSecretHmac + } + return "" +} + +func (x *OidcAuthMethodAttributes) GetMaxAge() *wrappers.UInt32Value { + if x != nil { + return x.MaxAge + } + return nil +} + +func (x *OidcAuthMethodAttributes) GetSigningAlgorithms() []string { + if x != nil { + return x.SigningAlgorithms + } + return nil +} + +func (x *OidcAuthMethodAttributes) GetApiUrlPrefix() *wrappers.StringValue { + if x != nil { + return x.ApiUrlPrefix + } + return nil +} + +func (x *OidcAuthMethodAttributes) GetCallbackUrl() string { + if x != nil { + return x.CallbackUrl + } + return "" +} + +func (x *OidcAuthMethodAttributes) GetIdpCaCerts() []string { + if x != nil { + return x.IdpCaCerts + } + return nil +} + +func (x *OidcAuthMethodAttributes) GetAllowedAudiences() []string { + if x != nil { + return x.AllowedAudiences + } + return nil +} + +func (x *OidcAuthMethodAttributes) GetDisableDiscoveredConfigValidation() bool { + if x != nil { + return x.DisableDiscoveredConfigValidation + } + return false +} + +func (x *OidcAuthMethodAttributes) GetDryRun() bool { + if x != nil { + return x.DryRun + } + return false +} + +// The structure of the OIDC authenticate start response, in the JSON object +type OidcAuthMethodAuthenticateStartResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // The returned authentication URL + AuthUrl string `protobuf:"bytes,10,opt,name=auth_url,proto3" json:"auth_url,omitempty"` + // The returned token URL + TokenUrl string `protobuf:"bytes,20,opt,name=token_url,proto3" json:"token_url,omitempty"` + // The returned token ID + TokenId string `protobuf:"bytes,30,opt,name=token_id,proto3" json:"token_id,omitempty"` +} + +func (x *OidcAuthMethodAuthenticateStartResponse) Reset() { + *x = OidcAuthMethodAuthenticateStartResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_api_resources_authmethods_v1_auth_method_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *OidcAuthMethodAuthenticateStartResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OidcAuthMethodAuthenticateStartResponse) ProtoMessage() {} + +func (x *OidcAuthMethodAuthenticateStartResponse) ProtoReflect() protoreflect.Message { + mi := &file_controller_api_resources_authmethods_v1_auth_method_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OidcAuthMethodAuthenticateStartResponse.ProtoReflect.Descriptor instead. +func (*OidcAuthMethodAuthenticateStartResponse) Descriptor() ([]byte, []int) { + return file_controller_api_resources_authmethods_v1_auth_method_proto_rawDescGZIP(), []int{3} +} + +func (x *OidcAuthMethodAuthenticateStartResponse) GetAuthUrl() string { + if x != nil { + return x.AuthUrl + } + return "" +} + +func (x *OidcAuthMethodAuthenticateStartResponse) GetTokenUrl() string { + if x != nil { + return x.TokenUrl + } + return "" +} + +func (x *OidcAuthMethodAuthenticateStartResponse) GetTokenId() string { + if x != nil { + return x.TokenId + } + return "" +} + +// The structure of OIDC callback request parameters +type OidcAuthMethodAuthenticateCallbackRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // The returned code + Code string `protobuf:"bytes,10,opt,name=code,proto3" json:"code,omitempty"` + // The returned state + State string `protobuf:"bytes,20,opt,name=state,proto3" json:"state,omitempty"` + // Error parameters, if they are returned + Error string `protobuf:"bytes,30,opt,name=error,proto3" json:"error,omitempty"` + ErrorDescription string `protobuf:"bytes,40,opt,name=error_description,proto3" json:"error_description,omitempty"` + ErrorUri string `protobuf:"bytes,50,opt,name=error_uri,proto3" json:"error_uri,omitempty"` +} + +func (x *OidcAuthMethodAuthenticateCallbackRequest) Reset() { + *x = OidcAuthMethodAuthenticateCallbackRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_api_resources_authmethods_v1_auth_method_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *OidcAuthMethodAuthenticateCallbackRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OidcAuthMethodAuthenticateCallbackRequest) ProtoMessage() {} + +func (x *OidcAuthMethodAuthenticateCallbackRequest) ProtoReflect() protoreflect.Message { + mi := &file_controller_api_resources_authmethods_v1_auth_method_proto_msgTypes[4] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OidcAuthMethodAuthenticateCallbackRequest.ProtoReflect.Descriptor instead. +func (*OidcAuthMethodAuthenticateCallbackRequest) Descriptor() ([]byte, []int) { + return file_controller_api_resources_authmethods_v1_auth_method_proto_rawDescGZIP(), []int{4} +} + +func (x *OidcAuthMethodAuthenticateCallbackRequest) GetCode() string { + if x != nil { + return x.Code + } + return "" +} + +func (x *OidcAuthMethodAuthenticateCallbackRequest) GetState() string { + if x != nil { + return x.State + } + return "" +} + +func (x *OidcAuthMethodAuthenticateCallbackRequest) GetError() string { + if x != nil { + return x.Error + } + return "" +} + +func (x *OidcAuthMethodAuthenticateCallbackRequest) GetErrorDescription() string { + if x != nil { + return x.ErrorDescription + } + return "" +} + +func (x *OidcAuthMethodAuthenticateCallbackRequest) GetErrorUri() string { + if x != nil { + return x.ErrorUri + } + return "" +} + +// The structure of OIDC callback response parameters +type OidcAuthMethodAuthenticateCallbackResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // The final redirection URL + FinalRedirectUrl string `protobuf:"bytes,10,opt,name=final_redirect_url,proto3" json:"final_redirect_url,omitempty"` +} + +func (x *OidcAuthMethodAuthenticateCallbackResponse) Reset() { + *x = OidcAuthMethodAuthenticateCallbackResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_api_resources_authmethods_v1_auth_method_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *OidcAuthMethodAuthenticateCallbackResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OidcAuthMethodAuthenticateCallbackResponse) ProtoMessage() {} + +func (x *OidcAuthMethodAuthenticateCallbackResponse) ProtoReflect() protoreflect.Message { + mi := &file_controller_api_resources_authmethods_v1_auth_method_proto_msgTypes[5] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OidcAuthMethodAuthenticateCallbackResponse.ProtoReflect.Descriptor instead. +func (*OidcAuthMethodAuthenticateCallbackResponse) Descriptor() ([]byte, []int) { + return file_controller_api_resources_authmethods_v1_auth_method_proto_rawDescGZIP(), []int{5} +} + +func (x *OidcAuthMethodAuthenticateCallbackResponse) GetFinalRedirectUrl() string { + if x != nil { + return x.FinalRedirectUrl + } + return "" +} + +// The structure of OIDC token request parameters +type OidcAuthMethodAuthenticateTokenRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // The ID of the pending token + TokenId string `protobuf:"bytes,10,opt,name=token_id,proto3" json:"token_id,omitempty"` +} + +func (x *OidcAuthMethodAuthenticateTokenRequest) Reset() { + *x = OidcAuthMethodAuthenticateTokenRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_api_resources_authmethods_v1_auth_method_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *OidcAuthMethodAuthenticateTokenRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OidcAuthMethodAuthenticateTokenRequest) ProtoMessage() {} + +func (x *OidcAuthMethodAuthenticateTokenRequest) ProtoReflect() protoreflect.Message { + mi := &file_controller_api_resources_authmethods_v1_auth_method_proto_msgTypes[6] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OidcAuthMethodAuthenticateTokenRequest.ProtoReflect.Descriptor instead. +func (*OidcAuthMethodAuthenticateTokenRequest) Descriptor() ([]byte, []int) { + return file_controller_api_resources_authmethods_v1_auth_method_proto_rawDescGZIP(), []int{6} +} + +func (x *OidcAuthMethodAuthenticateTokenRequest) GetTokenId() string { + if x != nil { + return x.TokenId + } + return "" +} + var File_controller_api_resources_authmethods_v1_auth_method_proto protoreflect.FileDescriptor var file_controller_api_resources_authmethods_v1_auth_method_proto_rawDesc = []byte{ @@ -252,7 +692,7 @@ var file_controller_api_resources_authmethods_v1_auth_method_proto_rawDesc = []b 0x6f, 0x74, 0x6f, 0x1a, 0x2a, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x76, 0x31, 0x2f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, - 0xd1, 0x06, 0x0a, 0x0a, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x0e, + 0xf1, 0x06, 0x0a, 0x0a, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x5f, 0x69, 0x64, 0x12, 0x43, 0x0a, 0x05, 0x73, 0x63, @@ -263,13 +703,13 @@ var file_controller_api_resources_authmethods_v1_auth_method_proto_rawDesc = []b 0x46, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x28, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x14, 0xa0, 0xda, 0x29, - 0x01, 0xc2, 0xdd, 0x29, 0x0c, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x04, 0x6e, 0x61, 0x6d, + 0x01, 0xc2, 0xdd, 0x29, 0x0c, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x04, 0x4e, 0x61, 0x6d, 0x65, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x62, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x32, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x22, 0xa0, 0xda, 0x29, 0x01, 0xc2, 0xdd, 0x29, 0x1a, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, - 0x6e, 0x12, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0b, + 0x6e, 0x12, 0x0b, 0x44, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x3e, 0x0a, 0x0c, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x3c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, @@ -285,7 +725,9 @@ var file_controller_api_resources_authmethods_v1_auth_method_proto_rawDesc = []b 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x42, 0x04, 0xa0, 0xda, 0x29, 0x01, 0x52, 0x0a, 0x61, 0x74, - 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0x2f, 0x0a, 0x12, 0x61, 0x75, 0x74, 0x68, + 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0x1e, 0x0a, 0x0a, 0x69, 0x73, 0x5f, 0x70, + 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x18, 0x6e, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x69, 0x73, + 0x5f, 0x70, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x12, 0x2f, 0x0a, 0x12, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x5f, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0xac, 0x02, 0x20, 0x03, 0x28, 0x09, 0x52, 0x12, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x5f, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x9b, 0x01, 0x0a, 0x1d, 0x61, 0x75, @@ -321,13 +763,110 @@ var file_controller_api_resources_authmethods_v1_auth_method_proto_rawDesc = []b 0x6e, 0x5f, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x5f, 0x6c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x12, 0x11, 0x4d, 0x69, 0x6e, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x4c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x52, 0x13, 0x6d, 0x69, 0x6e, 0x5f, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, - 0x72, 0x64, 0x5f, 0x6c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x42, 0x5d, 0x5a, 0x5b, 0x67, 0x69, 0x74, - 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, - 0x70, 0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, - 0x6e, 0x61, 0x6c, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, - 0x65, 0x72, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, - 0x2f, 0x61, 0x75, 0x74, 0x68, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x3b, 0x61, 0x75, 0x74, - 0x68, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x72, 0x64, 0x5f, 0x6c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x22, 0xa3, 0x08, 0x0a, 0x18, 0x4f, 0x69, + 0x64, 0x63, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x41, 0x74, 0x74, 0x72, + 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, + 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x59, 0x0a, 0x06, + 0x69, 0x73, 0x73, 0x75, 0x65, 0x72, 0x18, 0x14, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, + 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, + 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x23, 0xa0, 0xda, 0x29, 0x01, + 0xc2, 0xdd, 0x29, 0x1b, 0x0a, 0x11, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, + 0x2e, 0x69, 0x73, 0x73, 0x75, 0x65, 0x72, 0x12, 0x06, 0x49, 0x73, 0x73, 0x75, 0x65, 0x72, 0x52, + 0x06, 0x69, 0x73, 0x73, 0x75, 0x65, 0x72, 0x12, 0x64, 0x0a, 0x09, 0x63, 0x6c, 0x69, 0x65, 0x6e, + 0x74, 0x5f, 0x69, 0x64, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, + 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, + 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x28, 0xa0, 0xda, 0x29, 0x01, 0xc2, 0xdd, + 0x29, 0x20, 0x0a, 0x14, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x63, + 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x12, 0x08, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, + 0x49, 0x64, 0x52, 0x09, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x64, 0x12, 0x74, 0x0a, + 0x0d, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x18, 0x28, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, + 0x75, 0x65, 0x42, 0x30, 0xa0, 0xda, 0x29, 0x01, 0xc2, 0xdd, 0x29, 0x28, 0x0a, 0x18, 0x61, 0x74, + 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, + 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x12, 0x0c, 0x43, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x53, 0x65, + 0x63, 0x72, 0x65, 0x74, 0x52, 0x0d, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x73, 0x65, 0x63, + 0x72, 0x65, 0x74, 0x12, 0x2e, 0x0a, 0x12, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x73, 0x65, + 0x63, 0x72, 0x65, 0x74, 0x5f, 0x68, 0x6d, 0x61, 0x63, 0x18, 0x32, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x12, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x73, 0x65, 0x63, 0x72, 0x65, 0x74, 0x5f, 0x68, + 0x6d, 0x61, 0x63, 0x12, 0x5c, 0x0a, 0x07, 0x6d, 0x61, 0x78, 0x5f, 0x61, 0x67, 0x65, 0x18, 0x3c, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x55, 0x49, 0x6e, 0x74, 0x33, 0x32, 0x56, 0x61, 0x6c, + 0x75, 0x65, 0x42, 0x24, 0xa0, 0xda, 0x29, 0x01, 0xc2, 0xdd, 0x29, 0x1c, 0x0a, 0x12, 0x61, 0x74, + 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x6d, 0x61, 0x78, 0x5f, 0x61, 0x67, 0x65, + 0x12, 0x06, 0x4d, 0x61, 0x78, 0x41, 0x67, 0x65, 0x52, 0x07, 0x6d, 0x61, 0x78, 0x5f, 0x61, 0x67, + 0x65, 0x12, 0x64, 0x0a, 0x12, 0x73, 0x69, 0x67, 0x6e, 0x69, 0x6e, 0x67, 0x5f, 0x61, 0x6c, 0x67, + 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d, 0x73, 0x18, 0x46, 0x20, 0x03, 0x28, 0x09, 0x42, 0x34, 0xa0, + 0xda, 0x29, 0x01, 0xc2, 0xdd, 0x29, 0x2c, 0x0a, 0x1d, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, + 0x74, 0x65, 0x73, 0x2e, 0x73, 0x69, 0x67, 0x6e, 0x69, 0x6e, 0x67, 0x5f, 0x61, 0x6c, 0x67, 0x6f, + 0x72, 0x69, 0x74, 0x68, 0x6d, 0x73, 0x12, 0x0b, 0x53, 0x69, 0x67, 0x6e, 0x69, 0x6e, 0x67, 0x41, + 0x6c, 0x67, 0x73, 0x52, 0x12, 0x73, 0x69, 0x67, 0x6e, 0x69, 0x6e, 0x67, 0x5f, 0x61, 0x6c, 0x67, + 0x6f, 0x72, 0x69, 0x74, 0x68, 0x6d, 0x73, 0x12, 0x71, 0x0a, 0x0e, 0x61, 0x70, 0x69, 0x5f, 0x75, + 0x72, 0x6c, 0x5f, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x18, 0x50, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, + 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x2b, 0xa0, + 0xda, 0x29, 0x01, 0xc2, 0xdd, 0x29, 0x23, 0x0a, 0x19, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, + 0x74, 0x65, 0x73, 0x2e, 0x61, 0x70, 0x69, 0x5f, 0x75, 0x72, 0x6c, 0x5f, 0x70, 0x72, 0x65, 0x66, + 0x69, 0x78, 0x12, 0x06, 0x41, 0x70, 0x69, 0x55, 0x72, 0x6c, 0x52, 0x0e, 0x61, 0x70, 0x69, 0x5f, + 0x75, 0x72, 0x6c, 0x5f, 0x70, 0x72, 0x65, 0x66, 0x69, 0x78, 0x12, 0x22, 0x0a, 0x0c, 0x63, 0x61, + 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x5a, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0c, 0x63, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x5f, 0x75, 0x72, 0x6c, 0x12, 0x53, + 0x0a, 0x0c, 0x69, 0x64, 0x70, 0x5f, 0x63, 0x61, 0x5f, 0x63, 0x65, 0x72, 0x74, 0x73, 0x18, 0x64, + 0x20, 0x03, 0x28, 0x09, 0x42, 0x2f, 0xa0, 0xda, 0x29, 0x01, 0xc2, 0xdd, 0x29, 0x27, 0x0a, 0x17, + 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x69, 0x64, 0x70, 0x5f, 0x63, + 0x61, 0x5f, 0x63, 0x65, 0x72, 0x74, 0x73, 0x12, 0x0c, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, + 0x63, 0x61, 0x74, 0x65, 0x73, 0x52, 0x0c, 0x69, 0x64, 0x70, 0x5f, 0x63, 0x61, 0x5f, 0x63, 0x65, + 0x72, 0x74, 0x73, 0x12, 0x5f, 0x0a, 0x11, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x5f, 0x61, + 0x75, 0x64, 0x69, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x18, 0x6e, 0x20, 0x03, 0x28, 0x09, 0x42, 0x31, + 0xa0, 0xda, 0x29, 0x01, 0xc2, 0xdd, 0x29, 0x29, 0x0a, 0x1c, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, + 0x75, 0x74, 0x65, 0x73, 0x2e, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x5f, 0x61, 0x75, 0x64, + 0x69, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x12, 0x09, 0x41, 0x75, 0x64, 0x43, 0x6c, 0x61, 0x69, 0x6d, + 0x73, 0x52, 0x11, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x5f, 0x61, 0x75, 0x64, 0x69, 0x65, + 0x6e, 0x63, 0x65, 0x73, 0x12, 0x58, 0x0a, 0x24, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, 0x65, 0x5f, + 0x64, 0x69, 0x73, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x65, 0x64, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x5f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x78, 0x20, 0x01, + 0x28, 0x08, 0x42, 0x04, 0xa0, 0xda, 0x29, 0x01, 0x52, 0x24, 0x64, 0x69, 0x73, 0x61, 0x62, 0x6c, + 0x65, 0x5f, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x65, 0x64, 0x5f, 0x63, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x5f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1f, + 0x0a, 0x07, 0x64, 0x72, 0x79, 0x5f, 0x72, 0x75, 0x6e, 0x18, 0x82, 0x01, 0x20, 0x01, 0x28, 0x08, + 0x42, 0x04, 0xa0, 0xda, 0x29, 0x01, 0x52, 0x07, 0x64, 0x72, 0x79, 0x5f, 0x72, 0x75, 0x6e, 0x22, + 0x7f, 0x0a, 0x27, 0x4f, 0x69, 0x64, 0x63, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, + 0x64, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x53, 0x74, 0x61, + 0x72, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x61, 0x75, + 0x74, 0x68, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x61, 0x75, + 0x74, 0x68, 0x5f, 0x75, 0x72, 0x6c, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x5f, + 0x75, 0x72, 0x6c, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x74, 0x6f, 0x6b, 0x65, 0x6e, + 0x5f, 0x75, 0x72, 0x6c, 0x12, 0x1a, 0x0a, 0x08, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x5f, 0x69, 0x64, + 0x18, 0x1e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x5f, 0x69, 0x64, + 0x22, 0xb7, 0x01, 0x0a, 0x29, 0x4f, 0x69, 0x64, 0x63, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, + 0x68, 0x6f, 0x64, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x43, + 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, + 0x0a, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x63, 0x6f, + 0x64, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x14, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, + 0x72, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x2c, + 0x0a, 0x11, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, + 0x69, 0x6f, 0x6e, 0x18, 0x28, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x65, 0x72, 0x72, 0x6f, 0x72, + 0x5f, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1c, 0x0a, 0x09, + 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x75, 0x72, 0x69, 0x18, 0x32, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x09, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x5f, 0x75, 0x72, 0x69, 0x22, 0x5c, 0x0a, 0x2a, 0x4f, 0x69, + 0x64, 0x63, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x41, 0x75, 0x74, 0x68, + 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x43, 0x61, 0x6c, 0x6c, 0x62, 0x61, 0x63, 0x6b, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x2e, 0x0a, 0x12, 0x66, 0x69, 0x6e, 0x61, + 0x6c, 0x5f, 0x72, 0x65, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x0a, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x66, 0x69, 0x6e, 0x61, 0x6c, 0x5f, 0x72, 0x65, 0x64, 0x69, + 0x72, 0x65, 0x63, 0x74, 0x5f, 0x75, 0x72, 0x6c, 0x22, 0x44, 0x0a, 0x26, 0x4f, 0x69, 0x64, 0x63, + 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, + 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x0a, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x5f, 0x69, 0x64, 0x42, 0x5d, + 0x5a, 0x5b, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, + 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, + 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x63, 0x6f, 0x6e, + 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x72, 0x65, 0x73, 0x6f, + 0x75, 0x72, 0x63, 0x65, 0x73, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, + 0x73, 0x3b, 0x61, 0x75, 0x74, 0x68, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x62, 0x06, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -342,31 +881,42 @@ func file_controller_api_resources_authmethods_v1_auth_method_proto_rawDescGZIP( return file_controller_api_resources_authmethods_v1_auth_method_proto_rawDescData } -var file_controller_api_resources_authmethods_v1_auth_method_proto_msgTypes = make([]protoimpl.MessageInfo, 3) +var file_controller_api_resources_authmethods_v1_auth_method_proto_msgTypes = make([]protoimpl.MessageInfo, 8) var file_controller_api_resources_authmethods_v1_auth_method_proto_goTypes = []interface{}{ - (*AuthMethod)(nil), // 0: controller.api.resources.authmethods.v1.AuthMethod - (*PasswordAuthMethodAttributes)(nil), // 1: controller.api.resources.authmethods.v1.PasswordAuthMethodAttributes - nil, // 2: controller.api.resources.authmethods.v1.AuthMethod.AuthorizedCollectionActionsEntry - (*scopes.ScopeInfo)(nil), // 3: controller.api.resources.scopes.v1.ScopeInfo - (*wrappers.StringValue)(nil), // 4: google.protobuf.StringValue - (*timestamp.Timestamp)(nil), // 5: google.protobuf.Timestamp - (*_struct.Struct)(nil), // 6: google.protobuf.Struct - (*_struct.ListValue)(nil), // 7: google.protobuf.ListValue + (*AuthMethod)(nil), // 0: controller.api.resources.authmethods.v1.AuthMethod + (*PasswordAuthMethodAttributes)(nil), // 1: controller.api.resources.authmethods.v1.PasswordAuthMethodAttributes + (*OidcAuthMethodAttributes)(nil), // 2: controller.api.resources.authmethods.v1.OidcAuthMethodAttributes + (*OidcAuthMethodAuthenticateStartResponse)(nil), // 3: controller.api.resources.authmethods.v1.OidcAuthMethodAuthenticateStartResponse + (*OidcAuthMethodAuthenticateCallbackRequest)(nil), // 4: controller.api.resources.authmethods.v1.OidcAuthMethodAuthenticateCallbackRequest + (*OidcAuthMethodAuthenticateCallbackResponse)(nil), // 5: controller.api.resources.authmethods.v1.OidcAuthMethodAuthenticateCallbackResponse + (*OidcAuthMethodAuthenticateTokenRequest)(nil), // 6: controller.api.resources.authmethods.v1.OidcAuthMethodAuthenticateTokenRequest + nil, // 7: controller.api.resources.authmethods.v1.AuthMethod.AuthorizedCollectionActionsEntry + (*scopes.ScopeInfo)(nil), // 8: controller.api.resources.scopes.v1.ScopeInfo + (*wrappers.StringValue)(nil), // 9: google.protobuf.StringValue + (*timestamp.Timestamp)(nil), // 10: google.protobuf.Timestamp + (*_struct.Struct)(nil), // 11: google.protobuf.Struct + (*wrappers.UInt32Value)(nil), // 12: google.protobuf.UInt32Value + (*_struct.ListValue)(nil), // 13: google.protobuf.ListValue } var file_controller_api_resources_authmethods_v1_auth_method_proto_depIdxs = []int32{ - 3, // 0: controller.api.resources.authmethods.v1.AuthMethod.scope:type_name -> controller.api.resources.scopes.v1.ScopeInfo - 4, // 1: controller.api.resources.authmethods.v1.AuthMethod.name:type_name -> google.protobuf.StringValue - 4, // 2: controller.api.resources.authmethods.v1.AuthMethod.description:type_name -> google.protobuf.StringValue - 5, // 3: controller.api.resources.authmethods.v1.AuthMethod.created_time:type_name -> google.protobuf.Timestamp - 5, // 4: controller.api.resources.authmethods.v1.AuthMethod.updated_time:type_name -> google.protobuf.Timestamp - 6, // 5: controller.api.resources.authmethods.v1.AuthMethod.attributes:type_name -> google.protobuf.Struct - 2, // 6: controller.api.resources.authmethods.v1.AuthMethod.authorized_collection_actions:type_name -> controller.api.resources.authmethods.v1.AuthMethod.AuthorizedCollectionActionsEntry - 7, // 7: controller.api.resources.authmethods.v1.AuthMethod.AuthorizedCollectionActionsEntry.value:type_name -> google.protobuf.ListValue - 8, // [8:8] is the sub-list for method output_type - 8, // [8:8] is the sub-list for method input_type - 8, // [8:8] is the sub-list for extension type_name - 8, // [8:8] is the sub-list for extension extendee - 0, // [0:8] is the sub-list for field type_name + 8, // 0: controller.api.resources.authmethods.v1.AuthMethod.scope:type_name -> controller.api.resources.scopes.v1.ScopeInfo + 9, // 1: controller.api.resources.authmethods.v1.AuthMethod.name:type_name -> google.protobuf.StringValue + 9, // 2: controller.api.resources.authmethods.v1.AuthMethod.description:type_name -> google.protobuf.StringValue + 10, // 3: controller.api.resources.authmethods.v1.AuthMethod.created_time:type_name -> google.protobuf.Timestamp + 10, // 4: controller.api.resources.authmethods.v1.AuthMethod.updated_time:type_name -> google.protobuf.Timestamp + 11, // 5: controller.api.resources.authmethods.v1.AuthMethod.attributes:type_name -> google.protobuf.Struct + 7, // 6: controller.api.resources.authmethods.v1.AuthMethod.authorized_collection_actions:type_name -> controller.api.resources.authmethods.v1.AuthMethod.AuthorizedCollectionActionsEntry + 9, // 7: controller.api.resources.authmethods.v1.OidcAuthMethodAttributes.issuer:type_name -> google.protobuf.StringValue + 9, // 8: controller.api.resources.authmethods.v1.OidcAuthMethodAttributes.client_id:type_name -> google.protobuf.StringValue + 9, // 9: controller.api.resources.authmethods.v1.OidcAuthMethodAttributes.client_secret:type_name -> google.protobuf.StringValue + 12, // 10: controller.api.resources.authmethods.v1.OidcAuthMethodAttributes.max_age:type_name -> google.protobuf.UInt32Value + 9, // 11: controller.api.resources.authmethods.v1.OidcAuthMethodAttributes.api_url_prefix:type_name -> google.protobuf.StringValue + 13, // 12: controller.api.resources.authmethods.v1.AuthMethod.AuthorizedCollectionActionsEntry.value:type_name -> google.protobuf.ListValue + 13, // [13:13] is the sub-list for method output_type + 13, // [13:13] is the sub-list for method input_type + 13, // [13:13] is the sub-list for extension type_name + 13, // [13:13] is the sub-list for extension extendee + 0, // [0:13] is the sub-list for field type_name } func init() { file_controller_api_resources_authmethods_v1_auth_method_proto_init() } @@ -399,6 +949,66 @@ func file_controller_api_resources_authmethods_v1_auth_method_proto_init() { return nil } } + file_controller_api_resources_authmethods_v1_auth_method_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*OidcAuthMethodAttributes); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_controller_api_resources_authmethods_v1_auth_method_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*OidcAuthMethodAuthenticateStartResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_controller_api_resources_authmethods_v1_auth_method_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*OidcAuthMethodAuthenticateCallbackRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_controller_api_resources_authmethods_v1_auth_method_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*OidcAuthMethodAuthenticateCallbackResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_controller_api_resources_authmethods_v1_auth_method_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*OidcAuthMethodAuthenticateTokenRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } } type x struct{} out := protoimpl.TypeBuilder{ @@ -406,7 +1016,7 @@ func file_controller_api_resources_authmethods_v1_auth_method_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_controller_api_resources_authmethods_v1_auth_method_proto_rawDesc, NumEnums: 0, - NumMessages: 3, + NumMessages: 8, NumExtensions: 0, NumServices: 0, }, diff --git a/internal/gen/controller/api/resources/scopes/scope.pb.go b/internal/gen/controller/api/resources/scopes/scope.pb.go index 7a78108e4..2c8e8d569 100644 --- a/internal/gen/controller/api/resources/scopes/scope.pb.go +++ b/internal/gen/controller/api/resources/scopes/scope.pb.go @@ -133,6 +133,9 @@ type Scope struct { Version uint32 `protobuf:"varint,80,opt,name=version,proto3" json:"version,omitempty"` // The type of the resource. Type string `protobuf:"bytes,90,opt,name=type,proto3" json:"type,omitempty"` + // The ID of the primary auth method for this scope. A primary auth method + // is allowed to vivify users when new accounts are created and is the source for the users account info + PrimaryAuthMethodId *wrappers.StringValue `protobuf:"bytes,100,opt,name=primary_auth_method_id,proto3" json:"primary_auth_method_id,omitempty"` // Output only. The available actions on this resource for this user. AuthorizedActions []string `protobuf:"bytes,300,rep,name=authorized_actions,proto3" json:"authorized_actions,omitempty"` // Output only. The authorized actions for the scope's collections. @@ -234,6 +237,13 @@ func (x *Scope) GetType() string { return "" } +func (x *Scope) GetPrimaryAuthMethodId() *wrappers.StringValue { + if x != nil { + return x.PrimaryAuthMethodId + } + return nil +} + func (x *Scope) GetAuthorizedActions() []string { if x != nil { return x.AuthorizedActions @@ -274,7 +284,7 @@ var file_controller_api_resources_scopes_v1_scope_proto_rawDesc = []byte{ 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x28, 0x0a, 0x0f, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x5f, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x5f, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x5f, 0x69, - 0x64, 0x22, 0x83, 0x06, 0x0a, 0x05, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, + 0x64, 0x22, 0x91, 0x07, 0x0a, 0x05, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x5f, 0x69, 0x64, 0x12, 0x43, 0x0a, 0x05, 0x73, 0x63, 0x6f, 0x70, 0x65, @@ -303,32 +313,41 @@ var file_controller_api_resources_scopes_v1_scope_proto_rawDesc = []byte{ 0x74, 0x65, 0x64, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x50, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x5a, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x2f, 0x0a, 0x12, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, - 0x69, 0x7a, 0x65, 0x64, 0x5f, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0xac, 0x02, 0x20, - 0x03, 0x28, 0x09, 0x52, 0x12, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x5f, - 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x91, 0x01, 0x0a, 0x1d, 0x61, 0x75, 0x74, 0x68, + 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x8b, 0x01, 0x0a, 0x16, 0x70, 0x72, 0x69, 0x6d, 0x61, + 0x72, 0x79, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x5f, 0x69, + 0x64, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, + 0x56, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x35, 0xa0, 0xda, 0x29, 0x01, 0xc2, 0xdd, 0x29, 0x2d, 0x0a, + 0x16, 0x70, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x6d, 0x65, + 0x74, 0x68, 0x6f, 0x64, 0x5f, 0x69, 0x64, 0x12, 0x13, 0x50, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, + 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x49, 0x64, 0x52, 0x16, 0x70, 0x72, + 0x69, 0x6d, 0x61, 0x72, 0x79, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, + 0x64, 0x5f, 0x69, 0x64, 0x12, 0x2f, 0x0a, 0x12, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, + 0x65, 0x64, 0x5f, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0xac, 0x02, 0x20, 0x03, 0x28, + 0x09, 0x52, 0x12, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x5f, 0x61, 0x63, + 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x91, 0x01, 0x0a, 0x1d, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, + 0x69, 0x7a, 0x65, 0x64, 0x5f, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, + 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0xb6, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x4a, + 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, + 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x73, + 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, + 0x69, 0x7a, 0x65, 0x64, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x41, 0x63, + 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x1d, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x5f, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, - 0x6e, 0x5f, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0xb6, 0x02, 0x20, 0x03, 0x28, 0x0b, - 0x32, 0x4a, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, - 0x69, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x73, 0x63, 0x6f, 0x70, - 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x2e, 0x41, 0x75, 0x74, 0x68, - 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, - 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x1d, 0x61, 0x75, - 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x5f, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, - 0x69, 0x6f, 0x6e, 0x5f, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x1a, 0x6a, 0x0a, 0x20, 0x41, - 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, - 0x69, 0x6f, 0x6e, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, - 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, - 0x79, 0x12, 0x30, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, - 0x75, 0x66, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, - 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x53, 0x5a, 0x51, 0x67, 0x69, 0x74, 0x68, 0x75, - 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, - 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, - 0x6c, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, - 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2f, 0x73, - 0x63, 0x6f, 0x70, 0x65, 0x73, 0x3b, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x62, 0x06, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x33, + 0x6e, 0x5f, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x1a, 0x6a, 0x0a, 0x20, 0x41, 0x75, 0x74, + 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, + 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, + 0x30, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, + 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, + 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x53, 0x5a, 0x51, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, + 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x62, 0x6f, + 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, + 0x67, 0x65, 0x6e, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x61, + 0x70, 0x69, 0x2f, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2f, 0x73, 0x63, 0x6f, + 0x70, 0x65, 0x73, 0x3b, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x33, } var ( @@ -358,13 +377,14 @@ var file_controller_api_resources_scopes_v1_scope_proto_depIdxs = []int32{ 3, // 2: controller.api.resources.scopes.v1.Scope.description:type_name -> google.protobuf.StringValue 4, // 3: controller.api.resources.scopes.v1.Scope.created_time:type_name -> google.protobuf.Timestamp 4, // 4: controller.api.resources.scopes.v1.Scope.updated_time:type_name -> google.protobuf.Timestamp - 2, // 5: controller.api.resources.scopes.v1.Scope.authorized_collection_actions:type_name -> controller.api.resources.scopes.v1.Scope.AuthorizedCollectionActionsEntry - 5, // 6: controller.api.resources.scopes.v1.Scope.AuthorizedCollectionActionsEntry.value:type_name -> google.protobuf.ListValue - 7, // [7:7] is the sub-list for method output_type - 7, // [7:7] is the sub-list for method input_type - 7, // [7:7] is the sub-list for extension type_name - 7, // [7:7] is the sub-list for extension extendee - 0, // [0:7] is the sub-list for field type_name + 3, // 5: controller.api.resources.scopes.v1.Scope.primary_auth_method_id:type_name -> google.protobuf.StringValue + 2, // 6: controller.api.resources.scopes.v1.Scope.authorized_collection_actions:type_name -> controller.api.resources.scopes.v1.Scope.AuthorizedCollectionActionsEntry + 5, // 7: controller.api.resources.scopes.v1.Scope.AuthorizedCollectionActionsEntry.value:type_name -> google.protobuf.ListValue + 8, // [8:8] is the sub-list for method output_type + 8, // [8:8] is the sub-list for method input_type + 8, // [8:8] is the sub-list for extension type_name + 8, // [8:8] is the sub-list for extension extendee + 0, // [0:8] is the sub-list for field type_name } func init() { file_controller_api_resources_scopes_v1_scope_proto_init() } diff --git a/internal/gen/controller/api/services/auth_method_service.pb.go b/internal/gen/controller/api/services/auth_method_service.pb.go index f0d073093..ab44d48d9 100644 --- a/internal/gen/controller/api/services/auth_method_service.pb.go +++ b/internal/gen/controller/api/services/auth_method_service.pb.go @@ -527,18 +527,22 @@ func (*DeleteAuthMethodResponse) Descriptor() ([]byte, []int) { return file_controller_api_services_v1_auth_method_service_proto_rawDescGZIP(), []int{9} } -// The layout of the struct for "credentials" field in AuthenticateRequest. This message isn't directly referenced anywhere but is used here to define the expected field names and types. -type PasswordCredentials struct { +// Attributes specific to changing the state of an oidc auth method. +type OidcChangeStateAttributes struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - LoginName string `protobuf:"bytes,1,opt,name=login_name,proto3" json:"login_name,omitempty"` - Password string `protobuf:"bytes,2,opt,name=password,proto3" json:"password,omitempty"` + // state must be `inactive`, `active-private`, or `active-public` + State string `protobuf:"bytes,1,opt,name=state,proto3" json:"state,omitempty"` + // This flag is only useful for an oidc auth method. It should not be used + // unless the oidc provider's config is incorrectly set and is stopping the + // activation of this auth method. + OverrideOidcDiscoveryUrlConfig bool `protobuf:"varint,2,opt,name=override_oidc_discovery_url_config,proto3" json:"override_oidc_discovery_url_config,omitempty"` } -func (x *PasswordCredentials) Reset() { - *x = PasswordCredentials{} +func (x *OidcChangeStateAttributes) Reset() { + *x = OidcChangeStateAttributes{} if protoimpl.UnsafeEnabled { mi := &file_controller_api_services_v1_auth_method_service_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -546,13 +550,13 @@ func (x *PasswordCredentials) Reset() { } } -func (x *PasswordCredentials) String() string { +func (x *OidcChangeStateAttributes) String() string { return protoimpl.X.MessageStringOf(x) } -func (*PasswordCredentials) ProtoMessage() {} +func (*OidcChangeStateAttributes) ProtoMessage() {} -func (x *PasswordCredentials) ProtoReflect() protoreflect.Message { +func (x *OidcChangeStateAttributes) ProtoReflect() protoreflect.Message { mi := &file_controller_api_services_v1_auth_method_service_proto_msgTypes[10] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -564,25 +568,306 @@ func (x *PasswordCredentials) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use PasswordCredentials.ProtoReflect.Descriptor instead. -func (*PasswordCredentials) Descriptor() ([]byte, []int) { +// Deprecated: Use OidcChangeStateAttributes.ProtoReflect.Descriptor instead. +func (*OidcChangeStateAttributes) Descriptor() ([]byte, []int) { return file_controller_api_services_v1_auth_method_service_proto_rawDescGZIP(), []int{10} } -func (x *PasswordCredentials) GetLoginName() string { +func (x *OidcChangeStateAttributes) GetState() string { + if x != nil { + return x.State + } + return "" +} + +func (x *OidcChangeStateAttributes) GetOverrideOidcDiscoveryUrlConfig() bool { + if x != nil { + return x.OverrideOidcDiscoveryUrlConfig + } + return false +} + +type ChangeStateRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + // Version is used to ensure this resource has not changed. + // The mutation will fail if the version does not match the latest known good version. + Version uint32 `protobuf:"varint,2,opt,name=version,proto3" json:"version,omitempty"` + // The attributes specific to this auth method's state. + Attributes *_struct.Struct `protobuf:"bytes,4,opt,name=attributes,proto3" json:"attributes,omitempty"` +} + +func (x *ChangeStateRequest) Reset() { + *x = ChangeStateRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_api_services_v1_auth_method_service_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ChangeStateRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ChangeStateRequest) ProtoMessage() {} + +func (x *ChangeStateRequest) ProtoReflect() protoreflect.Message { + mi := &file_controller_api_services_v1_auth_method_service_proto_msgTypes[11] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ChangeStateRequest.ProtoReflect.Descriptor instead. +func (*ChangeStateRequest) Descriptor() ([]byte, []int) { + return file_controller_api_services_v1_auth_method_service_proto_rawDescGZIP(), []int{11} +} + +func (x *ChangeStateRequest) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *ChangeStateRequest) GetVersion() uint32 { + if x != nil { + return x.Version + } + return 0 +} + +func (x *ChangeStateRequest) GetAttributes() *_struct.Struct { + if x != nil { + return x.Attributes + } + return nil +} + +type ChangeStateResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Item *authmethods.AuthMethod `protobuf:"bytes,1,opt,name=item,proto3" json:"item,omitempty"` +} + +func (x *ChangeStateResponse) Reset() { + *x = ChangeStateResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_api_services_v1_auth_method_service_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ChangeStateResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ChangeStateResponse) ProtoMessage() {} + +func (x *ChangeStateResponse) ProtoReflect() protoreflect.Message { + mi := &file_controller_api_services_v1_auth_method_service_proto_msgTypes[12] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ChangeStateResponse.ProtoReflect.Descriptor instead. +func (*ChangeStateResponse) Descriptor() ([]byte, []int) { + return file_controller_api_services_v1_auth_method_service_proto_rawDescGZIP(), []int{12} +} + +func (x *ChangeStateResponse) GetItem() *authmethods.AuthMethod { + if x != nil { + return x.Item + } + return nil +} + +// The layout of the struct for "attributes" field in AuthenticateRequest for a password type. This message isn't directly referenced anywhere but is used here to define the expected field names and +// types. +type PasswordLoginAttributes struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + LoginName string `protobuf:"bytes,1,opt,name=login_name,proto3" json:"login_name,omitempty"` + Password string `protobuf:"bytes,2,opt,name=password,proto3" json:"password,omitempty"` +} + +func (x *PasswordLoginAttributes) Reset() { + *x = PasswordLoginAttributes{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_api_services_v1_auth_method_service_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *PasswordLoginAttributes) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PasswordLoginAttributes) ProtoMessage() {} + +func (x *PasswordLoginAttributes) ProtoReflect() protoreflect.Message { + mi := &file_controller_api_services_v1_auth_method_service_proto_msgTypes[13] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PasswordLoginAttributes.ProtoReflect.Descriptor instead. +func (*PasswordLoginAttributes) Descriptor() ([]byte, []int) { + return file_controller_api_services_v1_auth_method_service_proto_rawDescGZIP(), []int{13} +} + +func (x *PasswordLoginAttributes) GetLoginName() string { if x != nil { return x.LoginName } return "" } -func (x *PasswordCredentials) GetPassword() string { +func (x *PasswordLoginAttributes) GetPassword() string { if x != nil { return x.Password } return "" } +// The layout of the struct for "attributes" field in AuthenticateRequest for a oidc type's start command. This message isn't directly referenced anywhere but is used here to define the expected field +// names and types. +type OidcStartAttributes struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // An object which will be marshaled as JSON and roundtripped in the token command call. + RoundtripPayload *_struct.Struct `protobuf:"bytes,1,opt,name=roundtrip_payload,proto3" json:"roundtrip_payload,omitempty"` + // Cached marshaled payload. This is not ingressed from the client; anything found will be thrown out. + CachedRoundtripPayload string `protobuf:"bytes,2,opt,name=cached_roundtrip_payload,json=cachedRoundtripPayload,proto3" json:"cached_roundtrip_payload,omitempty"` +} + +func (x *OidcStartAttributes) Reset() { + *x = OidcStartAttributes{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_api_services_v1_auth_method_service_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *OidcStartAttributes) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OidcStartAttributes) ProtoMessage() {} + +func (x *OidcStartAttributes) ProtoReflect() protoreflect.Message { + mi := &file_controller_api_services_v1_auth_method_service_proto_msgTypes[14] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OidcStartAttributes.ProtoReflect.Descriptor instead. +func (*OidcStartAttributes) Descriptor() ([]byte, []int) { + return file_controller_api_services_v1_auth_method_service_proto_rawDescGZIP(), []int{14} +} + +func (x *OidcStartAttributes) GetRoundtripPayload() *_struct.Struct { + if x != nil { + return x.RoundtripPayload + } + return nil +} + +func (x *OidcStartAttributes) GetCachedRoundtripPayload() string { + if x != nil { + return x.CachedRoundtripPayload + } + return "" +} + +// The layout of the struct for "attributes" field in AuthenticateRequest for a +// oidc type's token command. This message isn't directly referenced anywhere +// but is used here to define the expected field names and types. +type OidcTokenAttributes struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // The token id for the request. This is the token id that was returned from + // the oidc type's start command + TokenId string `protobuf:"bytes,1,opt,name=token_id,json=tokenId,proto3" json:"token_id,omitempty"` +} + +func (x *OidcTokenAttributes) Reset() { + *x = OidcTokenAttributes{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_api_services_v1_auth_method_service_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *OidcTokenAttributes) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OidcTokenAttributes) ProtoMessage() {} + +func (x *OidcTokenAttributes) ProtoReflect() protoreflect.Message { + mi := &file_controller_api_services_v1_auth_method_service_proto_msgTypes[15] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OidcTokenAttributes.ProtoReflect.Descriptor instead. +func (*OidcTokenAttributes) Descriptor() ([]byte, []int) { + return file_controller_api_services_v1_auth_method_service_proto_rawDescGZIP(), []int{15} +} + +func (x *OidcTokenAttributes) GetTokenId() string { + if x != nil { + return x.TokenId + } + return "" +} + type AuthenticateRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -590,7 +875,8 @@ type AuthenticateRequest struct { // The ID of the Auth Method in the system that should be used for authentication. AuthMethodId string `protobuf:"bytes,1,opt,name=auth_method_id,proto3" json:"auth_method_id,omitempty"` - // This can be "cookie" or "token". If not provided, "token" will be used. "cookie" activates a split-cookie method where the token is split partially between http-only and regular cookies in order to keep it safe from rogue JS in the browser. + // This can be "cookie" or "token". If not provided, "token" will be used. "cookie" activates a split-cookie method where the token is split partially between http-only and regular cookies in order + // to keep it safe from rogue JS in the browser. TokenType string `protobuf:"bytes,2,opt,name=token_type,proto3" json:"token_type,omitempty"` // Deprecated; use "attributes" instead. // @@ -605,7 +891,7 @@ type AuthenticateRequest struct { func (x *AuthenticateRequest) Reset() { *x = AuthenticateRequest{} if protoimpl.UnsafeEnabled { - mi := &file_controller_api_services_v1_auth_method_service_proto_msgTypes[11] + mi := &file_controller_api_services_v1_auth_method_service_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -618,7 +904,7 @@ func (x *AuthenticateRequest) String() string { func (*AuthenticateRequest) ProtoMessage() {} func (x *AuthenticateRequest) ProtoReflect() protoreflect.Message { - mi := &file_controller_api_services_v1_auth_method_service_proto_msgTypes[11] + mi := &file_controller_api_services_v1_auth_method_service_proto_msgTypes[16] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -631,7 +917,7 @@ func (x *AuthenticateRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use AuthenticateRequest.ProtoReflect.Descriptor instead. func (*AuthenticateRequest) Descriptor() ([]byte, []int) { - return file_controller_api_services_v1_auth_method_service_proto_rawDescGZIP(), []int{11} + return file_controller_api_services_v1_auth_method_service_proto_rawDescGZIP(), []int{16} } func (x *AuthenticateRequest) GetAuthMethodId() string { @@ -675,14 +961,16 @@ type AuthenticateResponse struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Item *authtokens.AuthToken `protobuf:"bytes,1,opt,name=item,proto3" json:"item,omitempty"` - TokenType string `protobuf:"bytes,2,opt,name=token_type,proto3" json:"token_type,omitempty"` + // Valid keys and values depend on the type of Auth Method as well as the command. + Attributes *_struct.Struct `protobuf:"bytes,4,opt,name=attributes,proto3" json:"attributes,omitempty"` + // The command that was performed. + Command string `protobuf:"bytes,5,opt,name=command,proto3" json:"command,omitempty"` } func (x *AuthenticateResponse) Reset() { *x = AuthenticateResponse{} if protoimpl.UnsafeEnabled { - mi := &file_controller_api_services_v1_auth_method_service_proto_msgTypes[12] + mi := &file_controller_api_services_v1_auth_method_service_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -695,7 +983,7 @@ func (x *AuthenticateResponse) String() string { func (*AuthenticateResponse) ProtoMessage() {} func (x *AuthenticateResponse) ProtoReflect() protoreflect.Message { - mi := &file_controller_api_services_v1_auth_method_service_proto_msgTypes[12] + mi := &file_controller_api_services_v1_auth_method_service_proto_msgTypes[17] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -708,19 +996,19 @@ func (x *AuthenticateResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use AuthenticateResponse.ProtoReflect.Descriptor instead. func (*AuthenticateResponse) Descriptor() ([]byte, []int) { - return file_controller_api_services_v1_auth_method_service_proto_rawDescGZIP(), []int{12} + return file_controller_api_services_v1_auth_method_service_proto_rawDescGZIP(), []int{17} } -func (x *AuthenticateResponse) GetItem() *authtokens.AuthToken { +func (x *AuthenticateResponse) GetAttributes() *_struct.Struct { if x != nil { - return x.Item + return x.Attributes } return nil } -func (x *AuthenticateResponse) GetTokenType() string { +func (x *AuthenticateResponse) GetCommand() string { if x != nil { - return x.TokenType + return x.Command } return "" } @@ -732,7 +1020,8 @@ type AuthenticateLoginRequest struct { // The ID of the Auth Method in the system that should be used for authentication. AuthMethodId string `protobuf:"bytes,1,opt,name=auth_method_id,proto3" json:"auth_method_id,omitempty"` - // This can be "cookie" or "token". If not provided, "token" will be used. "cookie" activates a split-cookie method where the token is split partially between http-only and regular cookies in order to keep it safe from rogue JS in the browser. + // This can be "cookie" or "token". If not provided, "token" will be used. "cookie" activates a split-cookie method where the token is split partially between http-only and regular cookies in order + // to keep it safe from rogue JS in the browser. TokenType string `protobuf:"bytes,2,opt,name=token_type,proto3" json:"token_type,omitempty"` // Credentials are passed to the Auth Method; the valid keys and values depend on the type of Auth Method. Credentials *_struct.Struct `protobuf:"bytes,3,opt,name=credentials,proto3" json:"credentials,omitempty"` @@ -741,7 +1030,7 @@ type AuthenticateLoginRequest struct { func (x *AuthenticateLoginRequest) Reset() { *x = AuthenticateLoginRequest{} if protoimpl.UnsafeEnabled { - mi := &file_controller_api_services_v1_auth_method_service_proto_msgTypes[13] + mi := &file_controller_api_services_v1_auth_method_service_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -754,7 +1043,7 @@ func (x *AuthenticateLoginRequest) String() string { func (*AuthenticateLoginRequest) ProtoMessage() {} func (x *AuthenticateLoginRequest) ProtoReflect() protoreflect.Message { - mi := &file_controller_api_services_v1_auth_method_service_proto_msgTypes[13] + mi := &file_controller_api_services_v1_auth_method_service_proto_msgTypes[18] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -767,7 +1056,7 @@ func (x *AuthenticateLoginRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use AuthenticateLoginRequest.ProtoReflect.Descriptor instead. func (*AuthenticateLoginRequest) Descriptor() ([]byte, []int) { - return file_controller_api_services_v1_auth_method_service_proto_rawDescGZIP(), []int{13} + return file_controller_api_services_v1_auth_method_service_proto_rawDescGZIP(), []int{18} } func (x *AuthenticateLoginRequest) GetAuthMethodId() string { @@ -803,7 +1092,7 @@ type AuthenticateLoginResponse struct { func (x *AuthenticateLoginResponse) Reset() { *x = AuthenticateLoginResponse{} if protoimpl.UnsafeEnabled { - mi := &file_controller_api_services_v1_auth_method_service_proto_msgTypes[14] + mi := &file_controller_api_services_v1_auth_method_service_proto_msgTypes[19] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -816,7 +1105,7 @@ func (x *AuthenticateLoginResponse) String() string { func (*AuthenticateLoginResponse) ProtoMessage() {} func (x *AuthenticateLoginResponse) ProtoReflect() protoreflect.Message { - mi := &file_controller_api_services_v1_auth_method_service_proto_msgTypes[14] + mi := &file_controller_api_services_v1_auth_method_service_proto_msgTypes[19] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -829,7 +1118,7 @@ func (x *AuthenticateLoginResponse) ProtoReflect() protoreflect.Message { // Deprecated: Use AuthenticateLoginResponse.ProtoReflect.Descriptor instead. func (*AuthenticateLoginResponse) Descriptor() ([]byte, []int) { - return file_controller_api_services_v1_auth_method_service_proto_rawDescGZIP(), []int{14} + return file_controller_api_services_v1_auth_method_service_proto_rawDescGZIP(), []int{19} } func (x *AuthenticateLoginResponse) GetItem() *authtokens.AuthToken { @@ -927,151 +1216,199 @@ var file_controller_api_services_v1_auth_method_service_proto_rawDesc = []byte{ 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x22, 0x1a, 0x0a, 0x18, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, - 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x51, 0x0a, 0x13, 0x50, 0x61, 0x73, - 0x73, 0x77, 0x6f, 0x72, 0x64, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73, - 0x12, 0x1e, 0x0a, 0x0a, 0x6c, 0x6f, 0x67, 0x69, 0x6e, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x6c, 0x6f, 0x67, 0x69, 0x6e, 0x5f, 0x6e, 0x61, 0x6d, 0x65, - 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0xef, 0x01, 0x0a, - 0x13, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x0e, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x6d, 0x65, 0x74, - 0x68, 0x6f, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x61, 0x75, - 0x74, 0x68, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x5f, 0x69, 0x64, 0x12, 0x1e, 0x0a, 0x0a, - 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x0a, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x12, 0x3d, 0x0a, 0x0b, - 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x81, 0x01, 0x0a, 0x19, 0x4f, 0x69, + 0x64, 0x63, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x41, 0x74, 0x74, + 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x4e, 0x0a, + 0x22, 0x6f, 0x76, 0x65, 0x72, 0x72, 0x69, 0x64, 0x65, 0x5f, 0x6f, 0x69, 0x64, 0x63, 0x5f, 0x64, + 0x69, 0x73, 0x63, 0x6f, 0x76, 0x65, 0x72, 0x79, 0x5f, 0x75, 0x72, 0x6c, 0x5f, 0x63, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x22, 0x6f, 0x76, 0x65, 0x72, 0x72, + 0x69, 0x64, 0x65, 0x5f, 0x6f, 0x69, 0x64, 0x63, 0x5f, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x76, 0x65, + 0x72, 0x79, 0x5f, 0x75, 0x72, 0x6c, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x77, 0x0a, + 0x12, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x02, 0x69, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x37, 0x0a, + 0x0a, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x42, 0x02, 0x18, 0x01, 0x52, 0x0b, - 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73, 0x12, 0x37, 0x0a, 0x0a, 0x61, - 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x17, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, - 0x66, 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x52, 0x0a, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, - 0x75, 0x74, 0x65, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x18, - 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x22, 0x7d, - 0x0a, 0x14, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x45, 0x0a, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x31, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, - 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, - 0x61, 0x75, 0x74, 0x68, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x75, - 0x74, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x12, 0x1e, 0x0a, - 0x0a, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x0a, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x22, 0x9d, 0x01, - 0x0a, 0x18, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x4c, 0x6f, - 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x0e, 0x61, 0x75, - 0x74, 0x68, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x0e, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x5f, - 0x69, 0x64, 0x12, 0x1e, 0x0a, 0x0a, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x5f, 0x74, 0x79, 0x70, 0x65, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x5f, 0x74, 0x79, - 0x70, 0x65, 0x12, 0x39, 0x0a, 0x0b, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, - 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, + 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x52, 0x0a, 0x61, 0x74, 0x74, 0x72, + 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x22, 0x5e, 0x0a, 0x13, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, + 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x47, 0x0a, + 0x04, 0x69, 0x74, 0x65, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x33, 0x2e, 0x63, 0x6f, + 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x72, 0x65, 0x73, + 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x6d, 0x65, 0x74, 0x68, 0x6f, + 0x64, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, + 0x52, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x22, 0x55, 0x0a, 0x17, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, + 0x72, 0x64, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, + 0x73, 0x12, 0x1e, 0x0a, 0x0a, 0x6c, 0x6f, 0x67, 0x69, 0x6e, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x6c, 0x6f, 0x67, 0x69, 0x6e, 0x5f, 0x6e, 0x61, 0x6d, + 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x22, 0x96, 0x01, + 0x0a, 0x13, 0x4f, 0x69, 0x64, 0x63, 0x53, 0x74, 0x61, 0x72, 0x74, 0x41, 0x74, 0x74, 0x72, 0x69, + 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0x45, 0x0a, 0x11, 0x72, 0x6f, 0x75, 0x6e, 0x64, 0x74, 0x72, + 0x69, 0x70, 0x5f, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x17, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, + 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x52, 0x11, 0x72, 0x6f, 0x75, 0x6e, 0x64, + 0x74, 0x72, 0x69, 0x70, 0x5f, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x12, 0x38, 0x0a, 0x18, + 0x63, 0x61, 0x63, 0x68, 0x65, 0x64, 0x5f, 0x72, 0x6f, 0x75, 0x6e, 0x64, 0x74, 0x72, 0x69, 0x70, + 0x5f, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x16, + 0x63, 0x61, 0x63, 0x68, 0x65, 0x64, 0x52, 0x6f, 0x75, 0x6e, 0x64, 0x74, 0x72, 0x69, 0x70, 0x50, + 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x22, 0x30, 0x0a, 0x13, 0x4f, 0x69, 0x64, 0x63, 0x54, 0x6f, + 0x6b, 0x65, 0x6e, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0x19, 0x0a, + 0x08, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x07, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x49, 0x64, 0x22, 0xef, 0x01, 0x0a, 0x13, 0x41, 0x75, 0x74, + 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x12, 0x26, 0x0a, 0x0e, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x5f, + 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x6d, + 0x65, 0x74, 0x68, 0x6f, 0x64, 0x5f, 0x69, 0x64, 0x12, 0x1e, 0x0a, 0x0a, 0x74, 0x6f, 0x6b, 0x65, + 0x6e, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x74, 0x6f, + 0x6b, 0x65, 0x6e, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x12, 0x3d, 0x0a, 0x0b, 0x63, 0x72, 0x65, 0x64, + 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, + 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, + 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x42, 0x02, 0x18, 0x01, 0x52, 0x0b, 0x63, 0x72, 0x65, 0x64, + 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73, 0x12, 0x37, 0x0a, 0x0a, 0x61, 0x74, 0x74, 0x72, 0x69, + 0x62, 0x75, 0x74, 0x65, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, + 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, + 0x72, 0x75, 0x63, 0x74, 0x52, 0x0a, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, + 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x07, 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x22, 0x87, 0x01, 0x0a, 0x14, 0x41, + 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x37, 0x0a, 0x0a, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, + 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, - 0x52, 0x0b, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73, 0x22, 0x82, 0x01, - 0x0a, 0x19, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x4c, 0x6f, - 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x45, 0x0a, 0x04, 0x69, - 0x74, 0x65, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x31, 0x2e, 0x63, 0x6f, 0x6e, 0x74, - 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, - 0x72, 0x63, 0x65, 0x73, 0x2e, 0x61, 0x75, 0x74, 0x68, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x2e, - 0x76, 0x31, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x52, 0x04, 0x69, 0x74, - 0x65, 0x6d, 0x12, 0x1e, 0x0a, 0x0a, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x5f, 0x74, 0x79, 0x70, 0x65, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x5f, 0x74, 0x79, - 0x70, 0x65, 0x32, 0xbf, 0x0b, 0x0a, 0x11, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, - 0x64, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0xb8, 0x01, 0x0a, 0x0d, 0x47, 0x65, 0x74, - 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x30, 0x2e, 0x63, 0x6f, 0x6e, - 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, - 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x75, 0x74, 0x68, 0x4d, - 0x65, 0x74, 0x68, 0x6f, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x31, 0x2e, 0x63, - 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, - 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x75, 0x74, - 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, - 0x42, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1d, 0x12, 0x15, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x75, 0x74, - 0x68, 0x2d, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x62, 0x04, - 0x69, 0x74, 0x65, 0x6d, 0x92, 0x41, 0x1c, 0x12, 0x1a, 0x47, 0x65, 0x74, 0x73, 0x20, 0x61, 0x20, - 0x73, 0x69, 0x6e, 0x67, 0x6c, 0x65, 0x20, 0x41, 0x75, 0x74, 0x68, 0x20, 0x4d, 0x65, 0x74, 0x68, - 0x6f, 0x64, 0x2e, 0x12, 0xb0, 0x01, 0x0a, 0x0f, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x75, 0x74, 0x68, - 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x12, 0x32, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, - 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, - 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, - 0x68, 0x6f, 0x64, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x33, 0x2e, 0x63, 0x6f, - 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, - 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x41, 0x75, 0x74, - 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x22, 0x34, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x12, 0x12, 0x10, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x75, - 0x74, 0x68, 0x2d, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x92, 0x41, 0x19, 0x12, 0x17, 0x4c, - 0x69, 0x73, 0x74, 0x73, 0x20, 0x61, 0x6c, 0x6c, 0x20, 0x41, 0x75, 0x74, 0x68, 0x20, 0x4d, 0x65, - 0x74, 0x68, 0x6f, 0x64, 0x73, 0x2e, 0x12, 0xc5, 0x01, 0x0a, 0x10, 0x43, 0x72, 0x65, 0x61, 0x74, - 0x65, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x33, 0x2e, 0x63, 0x6f, - 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, - 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, - 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x34, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, - 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, - 0x65, 0x61, 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x46, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1e, 0x22, 0x10, - 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x2d, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, - 0x3a, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x62, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x92, 0x41, 0x1f, 0x12, - 0x1d, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x73, 0x20, 0x61, 0x20, 0x73, 0x69, 0x6e, 0x67, 0x6c, - 0x65, 0x20, 0x41, 0x75, 0x74, 0x68, 0x20, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x2e, 0x12, 0xc4, - 0x01, 0x0a, 0x10, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, - 0x68, 0x6f, 0x64, 0x12, 0x33, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, + 0x52, 0x0a, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0x18, 0x0a, 0x07, + 0x63, 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, + 0x6f, 0x6d, 0x6d, 0x61, 0x6e, 0x64, 0x4a, 0x04, 0x08, 0x01, 0x10, 0x02, 0x4a, 0x04, 0x08, 0x02, + 0x10, 0x03, 0x52, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x52, 0x0a, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x5f, + 0x74, 0x79, 0x70, 0x65, 0x22, 0x9d, 0x01, 0x0a, 0x18, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, + 0x69, 0x63, 0x61, 0x74, 0x65, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x26, 0x0a, 0x0e, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, + 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x61, 0x75, 0x74, 0x68, 0x5f, + 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x5f, 0x69, 0x64, 0x12, 0x1e, 0x0a, 0x0a, 0x74, 0x6f, 0x6b, + 0x65, 0x6e, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x74, + 0x6f, 0x6b, 0x65, 0x6e, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x12, 0x39, 0x0a, 0x0b, 0x63, 0x72, 0x65, + 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, + 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, + 0x2e, 0x53, 0x74, 0x72, 0x75, 0x63, 0x74, 0x52, 0x0b, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, + 0x69, 0x61, 0x6c, 0x73, 0x22, 0x82, 0x01, 0x0a, 0x19, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, + 0x69, 0x63, 0x61, 0x74, 0x65, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x45, 0x0a, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x31, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, + 0x69, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x2e, 0x61, 0x75, 0x74, 0x68, + 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x54, 0x6f, + 0x6b, 0x65, 0x6e, 0x52, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x12, 0x1e, 0x0a, 0x0a, 0x74, 0x6f, 0x6b, + 0x65, 0x6e, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x74, + 0x6f, 0x6b, 0x65, 0x6e, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x32, 0x8b, 0x0d, 0x0a, 0x11, 0x41, 0x75, + 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, + 0xb8, 0x01, 0x0a, 0x0d, 0x47, 0x65, 0x74, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, + 0x64, 0x12, 0x30, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, + 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x47, + 0x65, 0x74, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x1a, 0x31, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, - 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, - 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x34, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, - 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, - 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, - 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x45, - 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x23, 0x32, 0x15, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x75, 0x74, 0x68, - 0x2d, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x3a, 0x04, 0x69, - 0x74, 0x65, 0x6d, 0x62, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x92, 0x41, 0x19, 0x12, 0x17, 0x55, 0x70, - 0x64, 0x61, 0x74, 0x65, 0x73, 0x20, 0x61, 0x6e, 0x20, 0x41, 0x75, 0x74, 0x68, 0x20, 0x4d, 0x65, - 0x74, 0x68, 0x6f, 0x64, 0x2e, 0x12, 0xb6, 0x01, 0x0a, 0x10, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, + 0x2e, 0x47, 0x65, 0x74, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x42, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1d, 0x12, 0x15, + 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x2d, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, + 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x62, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x92, 0x41, 0x1c, 0x12, 0x1a, + 0x47, 0x65, 0x74, 0x73, 0x20, 0x61, 0x20, 0x73, 0x69, 0x6e, 0x67, 0x6c, 0x65, 0x20, 0x41, 0x75, + 0x74, 0x68, 0x20, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x2e, 0x12, 0xb0, 0x01, 0x0a, 0x0f, 0x4c, + 0x69, 0x73, 0x74, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x12, 0x32, + 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, + 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, + 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x33, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, + 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, + 0x4c, 0x69, 0x73, 0x74, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x34, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x12, 0x12, + 0x10, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x2d, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, + 0x73, 0x92, 0x41, 0x19, 0x12, 0x17, 0x4c, 0x69, 0x73, 0x74, 0x73, 0x20, 0x61, 0x6c, 0x6c, 0x20, + 0x41, 0x75, 0x74, 0x68, 0x20, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x2e, 0x12, 0xc5, 0x01, + 0x0a, 0x10, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, + 0x6f, 0x64, 0x12, 0x33, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, + 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, + 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x34, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, + 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, + 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4d, + 0x65, 0x74, 0x68, 0x6f, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x46, 0x82, + 0xd3, 0xe4, 0x93, 0x02, 0x1e, 0x22, 0x10, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x2d, + 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x3a, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x62, 0x04, 0x69, + 0x74, 0x65, 0x6d, 0x92, 0x41, 0x1f, 0x12, 0x1d, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x73, 0x20, + 0x61, 0x20, 0x73, 0x69, 0x6e, 0x67, 0x6c, 0x65, 0x20, 0x41, 0x75, 0x74, 0x68, 0x20, 0x4d, 0x65, + 0x74, 0x68, 0x6f, 0x64, 0x2e, 0x12, 0xc4, 0x01, 0x0a, 0x10, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x33, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, - 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x75, + 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x34, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, - 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, - 0x65, 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x37, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x17, 0x2a, 0x15, 0x2f, + 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x64, + 0x61, 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x45, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x23, 0x32, 0x15, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x2d, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x2f, - 0x7b, 0x69, 0x64, 0x7d, 0x92, 0x41, 0x17, 0x12, 0x15, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x73, - 0x20, 0x61, 0x6e, 0x20, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0xfd, - 0x01, 0x0a, 0x0c, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, - 0x2f, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, - 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x75, 0x74, - 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x30, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, - 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x75, - 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x22, 0x89, 0x01, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x39, 0x22, 0x2e, 0x2f, 0x76, 0x31, - 0x2f, 0x61, 0x75, 0x74, 0x68, 0x2d, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x2f, 0x7b, 0x61, - 0x75, 0x74, 0x68, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x5f, 0x69, 0x64, 0x7d, 0x3a, 0x61, - 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x3a, 0x01, 0x2a, 0x62, 0x04, - 0x69, 0x74, 0x65, 0x6d, 0x92, 0x41, 0x47, 0x12, 0x45, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, - 0x69, 0x63, 0x61, 0x74, 0x65, 0x20, 0x61, 0x20, 0x75, 0x73, 0x65, 0x72, 0x20, 0x74, 0x6f, 0x20, - 0x61, 0x6e, 0x20, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x72, 0x65, 0x74, - 0x72, 0x69, 0x65, 0x76, 0x65, 0x20, 0x61, 0x6e, 0x20, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, - 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x2e, 0x12, 0xf3, - 0x01, 0x0a, 0x11, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x4c, - 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x34, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, - 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, - 0x31, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x4c, 0x6f, - 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x35, 0x2e, 0x63, 0x6f, 0x6e, - 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, - 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, - 0x63, 0x61, 0x74, 0x65, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x22, 0x71, 0x88, 0x02, 0x01, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x3f, 0x22, 0x34, 0x2f, 0x76, - 0x31, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x2d, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x2f, 0x7b, - 0x61, 0x75, 0x74, 0x68, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x5f, 0x69, 0x64, 0x7d, 0x3a, - 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x3a, 0x6c, 0x6f, 0x67, - 0x69, 0x6e, 0x3a, 0x01, 0x2a, 0x62, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x92, 0x41, 0x26, 0x12, 0x24, - 0x44, 0x65, 0x70, 0x72, 0x65, 0x63, 0x61, 0x74, 0x65, 0x64, 0x3a, 0x20, 0x55, 0x73, 0x65, 0x20, - 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x20, 0x69, 0x6e, 0x73, - 0x74, 0x65, 0x61, 0x64, 0x42, 0x4d, 0x5a, 0x4b, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, - 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x62, 0x6f, 0x75, - 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x67, - 0x65, 0x6e, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x61, 0x70, - 0x69, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x3b, 0x73, 0x65, 0x72, 0x76, 0x69, - 0x63, 0x65, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x7b, 0x69, 0x64, 0x7d, 0x3a, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x62, 0x04, 0x69, 0x74, 0x65, 0x6d, + 0x92, 0x41, 0x19, 0x12, 0x17, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x73, 0x20, 0x61, 0x6e, 0x20, + 0x41, 0x75, 0x74, 0x68, 0x20, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x2e, 0x12, 0xb6, 0x01, 0x0a, + 0x10, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, + 0x64, 0x12, 0x33, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, + 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x44, + 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x34, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, + 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, + 0x2e, 0x76, 0x31, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x41, 0x75, 0x74, 0x68, 0x4d, 0x65, + 0x74, 0x68, 0x6f, 0x64, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x37, 0x82, 0xd3, + 0xe4, 0x93, 0x02, 0x17, 0x2a, 0x15, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x2d, 0x6d, + 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x92, 0x41, 0x17, 0x12, 0x15, + 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x73, 0x20, 0x61, 0x6e, 0x20, 0x41, 0x75, 0x74, 0x68, 0x4d, + 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0xcf, 0x01, 0x0a, 0x0b, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, + 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x2e, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, + 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, + 0x76, 0x31, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2f, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, + 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, + 0x76, 0x31, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x5f, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x2d, 0x22, 0x22, + 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x2d, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, + 0x2f, 0x7b, 0x69, 0x64, 0x7d, 0x3a, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x2d, 0x73, 0x74, 0x61, + 0x74, 0x65, 0x3a, 0x01, 0x2a, 0x62, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x92, 0x41, 0x29, 0x12, 0x27, + 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x20, 0x74, 0x68, 0x65, 0x20, 0x73, 0x74, 0x61, 0x74, + 0x65, 0x20, 0x6f, 0x66, 0x20, 0x61, 0x6e, 0x20, 0x4f, 0x49, 0x44, 0x43, 0x20, 0x41, 0x75, 0x74, + 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0xf7, 0x01, 0x0a, 0x0c, 0x41, 0x75, 0x74, 0x68, + 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x12, 0x2f, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, + 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, + 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, + 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x30, 0x2e, 0x63, 0x6f, 0x6e, 0x74, + 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, + 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, + 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x83, 0x01, 0x82, 0xd3, + 0xe4, 0x93, 0x02, 0x33, 0x22, 0x2e, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x2d, 0x6d, + 0x65, 0x74, 0x68, 0x6f, 0x64, 0x73, 0x2f, 0x7b, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x6d, 0x65, 0x74, + 0x68, 0x6f, 0x64, 0x5f, 0x69, 0x64, 0x7d, 0x3a, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, + 0x63, 0x61, 0x74, 0x65, 0x3a, 0x01, 0x2a, 0x92, 0x41, 0x47, 0x12, 0x45, 0x41, 0x75, 0x74, 0x68, + 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x20, 0x61, 0x20, 0x75, 0x73, 0x65, 0x72, 0x20, + 0x74, 0x6f, 0x20, 0x61, 0x6e, 0x20, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x20, 0x61, 0x6e, 0x64, 0x20, + 0x72, 0x65, 0x74, 0x72, 0x69, 0x65, 0x76, 0x65, 0x20, 0x61, 0x6e, 0x20, 0x61, 0x75, 0x74, 0x68, + 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x74, 0x6f, 0x6b, 0x65, 0x6e, + 0x2e, 0x12, 0xf3, 0x01, 0x0a, 0x11, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, + 0x74, 0x65, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x34, 0x2e, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, + 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, + 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, + 0x65, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x35, 0x2e, + 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x73, + 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x75, 0x74, 0x68, 0x65, + 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x71, 0x88, 0x02, 0x01, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x3f, 0x22, + 0x34, 0x2f, 0x76, 0x31, 0x2f, 0x61, 0x75, 0x74, 0x68, 0x2d, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, + 0x73, 0x2f, 0x7b, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x5f, 0x69, + 0x64, 0x7d, 0x3a, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x3a, + 0x6c, 0x6f, 0x67, 0x69, 0x6e, 0x3a, 0x01, 0x2a, 0x62, 0x04, 0x69, 0x74, 0x65, 0x6d, 0x92, 0x41, + 0x26, 0x12, 0x24, 0x44, 0x65, 0x70, 0x72, 0x65, 0x63, 0x61, 0x74, 0x65, 0x64, 0x3a, 0x20, 0x55, + 0x73, 0x65, 0x20, 0x41, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x20, + 0x69, 0x6e, 0x73, 0x74, 0x65, 0x61, 0x64, 0x42, 0x4d, 0x5a, 0x4b, 0x67, 0x69, 0x74, 0x68, 0x75, + 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, + 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, + 0x6c, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, + 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x3b, 0x73, 0x65, + 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -1086,7 +1423,7 @@ func file_controller_api_services_v1_auth_method_service_proto_rawDescGZIP() []b return file_controller_api_services_v1_auth_method_service_proto_rawDescData } -var file_controller_api_services_v1_auth_method_service_proto_msgTypes = make([]protoimpl.MessageInfo, 15) +var file_controller_api_services_v1_auth_method_service_proto_msgTypes = make([]protoimpl.MessageInfo, 20) var file_controller_api_services_v1_auth_method_service_proto_goTypes = []interface{}{ (*GetAuthMethodRequest)(nil), // 0: controller.api.services.v1.GetAuthMethodRequest (*GetAuthMethodResponse)(nil), // 1: controller.api.services.v1.GetAuthMethodResponse @@ -1098,48 +1435,58 @@ var file_controller_api_services_v1_auth_method_service_proto_goTypes = []interf (*UpdateAuthMethodResponse)(nil), // 7: controller.api.services.v1.UpdateAuthMethodResponse (*DeleteAuthMethodRequest)(nil), // 8: controller.api.services.v1.DeleteAuthMethodRequest (*DeleteAuthMethodResponse)(nil), // 9: controller.api.services.v1.DeleteAuthMethodResponse - (*PasswordCredentials)(nil), // 10: controller.api.services.v1.PasswordCredentials - (*AuthenticateRequest)(nil), // 11: controller.api.services.v1.AuthenticateRequest - (*AuthenticateResponse)(nil), // 12: controller.api.services.v1.AuthenticateResponse - (*AuthenticateLoginRequest)(nil), // 13: controller.api.services.v1.AuthenticateLoginRequest - (*AuthenticateLoginResponse)(nil), // 14: controller.api.services.v1.AuthenticateLoginResponse - (*authmethods.AuthMethod)(nil), // 15: controller.api.resources.authmethods.v1.AuthMethod - (*field_mask.FieldMask)(nil), // 16: google.protobuf.FieldMask - (*_struct.Struct)(nil), // 17: google.protobuf.Struct - (*authtokens.AuthToken)(nil), // 18: controller.api.resources.authtokens.v1.AuthToken + (*OidcChangeStateAttributes)(nil), // 10: controller.api.services.v1.OidcChangeStateAttributes + (*ChangeStateRequest)(nil), // 11: controller.api.services.v1.ChangeStateRequest + (*ChangeStateResponse)(nil), // 12: controller.api.services.v1.ChangeStateResponse + (*PasswordLoginAttributes)(nil), // 13: controller.api.services.v1.PasswordLoginAttributes + (*OidcStartAttributes)(nil), // 14: controller.api.services.v1.OidcStartAttributes + (*OidcTokenAttributes)(nil), // 15: controller.api.services.v1.OidcTokenAttributes + (*AuthenticateRequest)(nil), // 16: controller.api.services.v1.AuthenticateRequest + (*AuthenticateResponse)(nil), // 17: controller.api.services.v1.AuthenticateResponse + (*AuthenticateLoginRequest)(nil), // 18: controller.api.services.v1.AuthenticateLoginRequest + (*AuthenticateLoginResponse)(nil), // 19: controller.api.services.v1.AuthenticateLoginResponse + (*authmethods.AuthMethod)(nil), // 20: controller.api.resources.authmethods.v1.AuthMethod + (*field_mask.FieldMask)(nil), // 21: google.protobuf.FieldMask + (*_struct.Struct)(nil), // 22: google.protobuf.Struct + (*authtokens.AuthToken)(nil), // 23: controller.api.resources.authtokens.v1.AuthToken } var file_controller_api_services_v1_auth_method_service_proto_depIdxs = []int32{ - 15, // 0: controller.api.services.v1.GetAuthMethodResponse.item:type_name -> controller.api.resources.authmethods.v1.AuthMethod - 15, // 1: controller.api.services.v1.ListAuthMethodsResponse.items:type_name -> controller.api.resources.authmethods.v1.AuthMethod - 15, // 2: controller.api.services.v1.CreateAuthMethodRequest.item:type_name -> controller.api.resources.authmethods.v1.AuthMethod - 15, // 3: controller.api.services.v1.CreateAuthMethodResponse.item:type_name -> controller.api.resources.authmethods.v1.AuthMethod - 15, // 4: controller.api.services.v1.UpdateAuthMethodRequest.item:type_name -> controller.api.resources.authmethods.v1.AuthMethod - 16, // 5: controller.api.services.v1.UpdateAuthMethodRequest.update_mask:type_name -> google.protobuf.FieldMask - 15, // 6: controller.api.services.v1.UpdateAuthMethodResponse.item:type_name -> controller.api.resources.authmethods.v1.AuthMethod - 17, // 7: controller.api.services.v1.AuthenticateRequest.credentials:type_name -> google.protobuf.Struct - 17, // 8: controller.api.services.v1.AuthenticateRequest.attributes:type_name -> google.protobuf.Struct - 18, // 9: controller.api.services.v1.AuthenticateResponse.item:type_name -> controller.api.resources.authtokens.v1.AuthToken - 17, // 10: controller.api.services.v1.AuthenticateLoginRequest.credentials:type_name -> google.protobuf.Struct - 18, // 11: controller.api.services.v1.AuthenticateLoginResponse.item:type_name -> controller.api.resources.authtokens.v1.AuthToken - 0, // 12: controller.api.services.v1.AuthMethodService.GetAuthMethod:input_type -> controller.api.services.v1.GetAuthMethodRequest - 2, // 13: controller.api.services.v1.AuthMethodService.ListAuthMethods:input_type -> controller.api.services.v1.ListAuthMethodsRequest - 4, // 14: controller.api.services.v1.AuthMethodService.CreateAuthMethod:input_type -> controller.api.services.v1.CreateAuthMethodRequest - 6, // 15: controller.api.services.v1.AuthMethodService.UpdateAuthMethod:input_type -> controller.api.services.v1.UpdateAuthMethodRequest - 8, // 16: controller.api.services.v1.AuthMethodService.DeleteAuthMethod:input_type -> controller.api.services.v1.DeleteAuthMethodRequest - 11, // 17: controller.api.services.v1.AuthMethodService.Authenticate:input_type -> controller.api.services.v1.AuthenticateRequest - 13, // 18: controller.api.services.v1.AuthMethodService.AuthenticateLogin:input_type -> controller.api.services.v1.AuthenticateLoginRequest - 1, // 19: controller.api.services.v1.AuthMethodService.GetAuthMethod:output_type -> controller.api.services.v1.GetAuthMethodResponse - 3, // 20: controller.api.services.v1.AuthMethodService.ListAuthMethods:output_type -> controller.api.services.v1.ListAuthMethodsResponse - 5, // 21: controller.api.services.v1.AuthMethodService.CreateAuthMethod:output_type -> controller.api.services.v1.CreateAuthMethodResponse - 7, // 22: controller.api.services.v1.AuthMethodService.UpdateAuthMethod:output_type -> controller.api.services.v1.UpdateAuthMethodResponse - 9, // 23: controller.api.services.v1.AuthMethodService.DeleteAuthMethod:output_type -> controller.api.services.v1.DeleteAuthMethodResponse - 12, // 24: controller.api.services.v1.AuthMethodService.Authenticate:output_type -> controller.api.services.v1.AuthenticateResponse - 14, // 25: controller.api.services.v1.AuthMethodService.AuthenticateLogin:output_type -> controller.api.services.v1.AuthenticateLoginResponse - 19, // [19:26] is the sub-list for method output_type - 12, // [12:19] is the sub-list for method input_type - 12, // [12:12] is the sub-list for extension type_name - 12, // [12:12] is the sub-list for extension extendee - 0, // [0:12] is the sub-list for field type_name + 20, // 0: controller.api.services.v1.GetAuthMethodResponse.item:type_name -> controller.api.resources.authmethods.v1.AuthMethod + 20, // 1: controller.api.services.v1.ListAuthMethodsResponse.items:type_name -> controller.api.resources.authmethods.v1.AuthMethod + 20, // 2: controller.api.services.v1.CreateAuthMethodRequest.item:type_name -> controller.api.resources.authmethods.v1.AuthMethod + 20, // 3: controller.api.services.v1.CreateAuthMethodResponse.item:type_name -> controller.api.resources.authmethods.v1.AuthMethod + 20, // 4: controller.api.services.v1.UpdateAuthMethodRequest.item:type_name -> controller.api.resources.authmethods.v1.AuthMethod + 21, // 5: controller.api.services.v1.UpdateAuthMethodRequest.update_mask:type_name -> google.protobuf.FieldMask + 20, // 6: controller.api.services.v1.UpdateAuthMethodResponse.item:type_name -> controller.api.resources.authmethods.v1.AuthMethod + 22, // 7: controller.api.services.v1.ChangeStateRequest.attributes:type_name -> google.protobuf.Struct + 20, // 8: controller.api.services.v1.ChangeStateResponse.item:type_name -> controller.api.resources.authmethods.v1.AuthMethod + 22, // 9: controller.api.services.v1.OidcStartAttributes.roundtrip_payload:type_name -> google.protobuf.Struct + 22, // 10: controller.api.services.v1.AuthenticateRequest.credentials:type_name -> google.protobuf.Struct + 22, // 11: controller.api.services.v1.AuthenticateRequest.attributes:type_name -> google.protobuf.Struct + 22, // 12: controller.api.services.v1.AuthenticateResponse.attributes:type_name -> google.protobuf.Struct + 22, // 13: controller.api.services.v1.AuthenticateLoginRequest.credentials:type_name -> google.protobuf.Struct + 23, // 14: controller.api.services.v1.AuthenticateLoginResponse.item:type_name -> controller.api.resources.authtokens.v1.AuthToken + 0, // 15: controller.api.services.v1.AuthMethodService.GetAuthMethod:input_type -> controller.api.services.v1.GetAuthMethodRequest + 2, // 16: controller.api.services.v1.AuthMethodService.ListAuthMethods:input_type -> controller.api.services.v1.ListAuthMethodsRequest + 4, // 17: controller.api.services.v1.AuthMethodService.CreateAuthMethod:input_type -> controller.api.services.v1.CreateAuthMethodRequest + 6, // 18: controller.api.services.v1.AuthMethodService.UpdateAuthMethod:input_type -> controller.api.services.v1.UpdateAuthMethodRequest + 8, // 19: controller.api.services.v1.AuthMethodService.DeleteAuthMethod:input_type -> controller.api.services.v1.DeleteAuthMethodRequest + 11, // 20: controller.api.services.v1.AuthMethodService.ChangeState:input_type -> controller.api.services.v1.ChangeStateRequest + 16, // 21: controller.api.services.v1.AuthMethodService.Authenticate:input_type -> controller.api.services.v1.AuthenticateRequest + 18, // 22: controller.api.services.v1.AuthMethodService.AuthenticateLogin:input_type -> controller.api.services.v1.AuthenticateLoginRequest + 1, // 23: controller.api.services.v1.AuthMethodService.GetAuthMethod:output_type -> controller.api.services.v1.GetAuthMethodResponse + 3, // 24: controller.api.services.v1.AuthMethodService.ListAuthMethods:output_type -> controller.api.services.v1.ListAuthMethodsResponse + 5, // 25: controller.api.services.v1.AuthMethodService.CreateAuthMethod:output_type -> controller.api.services.v1.CreateAuthMethodResponse + 7, // 26: controller.api.services.v1.AuthMethodService.UpdateAuthMethod:output_type -> controller.api.services.v1.UpdateAuthMethodResponse + 9, // 27: controller.api.services.v1.AuthMethodService.DeleteAuthMethod:output_type -> controller.api.services.v1.DeleteAuthMethodResponse + 12, // 28: controller.api.services.v1.AuthMethodService.ChangeState:output_type -> controller.api.services.v1.ChangeStateResponse + 17, // 29: controller.api.services.v1.AuthMethodService.Authenticate:output_type -> controller.api.services.v1.AuthenticateResponse + 19, // 30: controller.api.services.v1.AuthMethodService.AuthenticateLogin:output_type -> controller.api.services.v1.AuthenticateLoginResponse + 23, // [23:31] is the sub-list for method output_type + 15, // [15:23] is the sub-list for method input_type + 15, // [15:15] is the sub-list for extension type_name + 15, // [15:15] is the sub-list for extension extendee + 0, // [0:15] is the sub-list for field type_name } func init() { file_controller_api_services_v1_auth_method_service_proto_init() } @@ -1269,7 +1616,7 @@ func file_controller_api_services_v1_auth_method_service_proto_init() { } } file_controller_api_services_v1_auth_method_service_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*PasswordCredentials); i { + switch v := v.(*OidcChangeStateAttributes); i { case 0: return &v.state case 1: @@ -1281,7 +1628,7 @@ func file_controller_api_services_v1_auth_method_service_proto_init() { } } file_controller_api_services_v1_auth_method_service_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*AuthenticateRequest); i { + switch v := v.(*ChangeStateRequest); i { case 0: return &v.state case 1: @@ -1293,7 +1640,7 @@ func file_controller_api_services_v1_auth_method_service_proto_init() { } } file_controller_api_services_v1_auth_method_service_proto_msgTypes[12].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*AuthenticateResponse); i { + switch v := v.(*ChangeStateResponse); i { case 0: return &v.state case 1: @@ -1305,7 +1652,7 @@ func file_controller_api_services_v1_auth_method_service_proto_init() { } } file_controller_api_services_v1_auth_method_service_proto_msgTypes[13].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*AuthenticateLoginRequest); i { + switch v := v.(*PasswordLoginAttributes); i { case 0: return &v.state case 1: @@ -1317,6 +1664,66 @@ func file_controller_api_services_v1_auth_method_service_proto_init() { } } file_controller_api_services_v1_auth_method_service_proto_msgTypes[14].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*OidcStartAttributes); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_controller_api_services_v1_auth_method_service_proto_msgTypes[15].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*OidcTokenAttributes); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_controller_api_services_v1_auth_method_service_proto_msgTypes[16].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*AuthenticateRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_controller_api_services_v1_auth_method_service_proto_msgTypes[17].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*AuthenticateResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_controller_api_services_v1_auth_method_service_proto_msgTypes[18].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*AuthenticateLoginRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_controller_api_services_v1_auth_method_service_proto_msgTypes[19].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*AuthenticateLoginResponse); i { case 0: return &v.state @@ -1335,7 +1742,7 @@ func file_controller_api_services_v1_auth_method_service_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_controller_api_services_v1_auth_method_service_proto_rawDesc, NumEnums: 0, - NumMessages: 15, + NumMessages: 20, NumExtensions: 0, NumServices: 1, }, diff --git a/internal/gen/controller/api/services/auth_method_service.pb.gw.go b/internal/gen/controller/api/services/auth_method_service.pb.gw.go index 1807c75cb..ac9cbdfb5 100644 --- a/internal/gen/controller/api/services/auth_method_service.pb.gw.go +++ b/internal/gen/controller/api/services/auth_method_service.pb.gw.go @@ -305,6 +305,74 @@ func local_request_AuthMethodService_DeleteAuthMethod_0(ctx context.Context, mar } +func request_AuthMethodService_ChangeState_0(ctx context.Context, marshaler runtime.Marshaler, client AuthMethodServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq ChangeStateRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + var ( + val string + ok bool + err error + _ = err + ) + + val, ok = pathParams["id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "id") + } + + protoReq.Id, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "id", err) + } + + msg, err := client.ChangeState(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err + +} + +func local_request_AuthMethodService_ChangeState_0(ctx context.Context, marshaler runtime.Marshaler, server AuthMethodServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var protoReq ChangeStateRequest + var metadata runtime.ServerMetadata + + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + + var ( + val string + ok bool + err error + _ = err + ) + + val, ok = pathParams["id"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "id") + } + + protoReq.Id, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "id", err) + } + + msg, err := server.ChangeState(ctx, &protoReq) + return msg, metadata, err + +} + func request_AuthMethodService_Authenticate_0(ctx context.Context, marshaler runtime.Marshaler, client AuthMethodServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq AuthenticateRequest var metadata runtime.ServerMetadata @@ -562,6 +630,29 @@ func RegisterAuthMethodServiceHandlerServer(ctx context.Context, mux *runtime.Se }) + mux.Handle("POST", pattern_AuthMethodService_ChangeState_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/controller.api.services.v1.AuthMethodService/ChangeState") + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_AuthMethodService_ChangeState_0(rctx, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_AuthMethodService_ChangeState_0(ctx, mux, outboundMarshaler, w, req, response_AuthMethodService_ChangeState_0{resp}, mux.GetForwardResponseOptions()...) + + }) + mux.Handle("POST", pattern_AuthMethodService_Authenticate_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() @@ -581,7 +672,7 @@ func RegisterAuthMethodServiceHandlerServer(ctx context.Context, mux *runtime.Se return } - forward_AuthMethodService_Authenticate_0(ctx, mux, outboundMarshaler, w, req, response_AuthMethodService_Authenticate_0{resp}, mux.GetForwardResponseOptions()...) + forward_AuthMethodService_Authenticate_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) @@ -749,6 +840,26 @@ func RegisterAuthMethodServiceHandlerClient(ctx context.Context, mux *runtime.Se }) + mux.Handle("POST", pattern_AuthMethodService_ChangeState_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + rctx, err := runtime.AnnotateContext(ctx, mux, req, "/controller.api.services.v1.AuthMethodService/ChangeState") + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_AuthMethodService_ChangeState_0(rctx, inboundMarshaler, client, req, pathParams) + ctx = runtime.NewServerMetadataContext(ctx, md) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + + forward_AuthMethodService_ChangeState_0(ctx, mux, outboundMarshaler, w, req, response_AuthMethodService_ChangeState_0{resp}, mux.GetForwardResponseOptions()...) + + }) + mux.Handle("POST", pattern_AuthMethodService_Authenticate_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { ctx, cancel := context.WithCancel(req.Context()) defer cancel() @@ -765,7 +876,7 @@ func RegisterAuthMethodServiceHandlerClient(ctx context.Context, mux *runtime.Se return } - forward_AuthMethodService_Authenticate_0(ctx, mux, outboundMarshaler, w, req, response_AuthMethodService_Authenticate_0{resp}, mux.GetForwardResponseOptions()...) + forward_AuthMethodService_Authenticate_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) }) @@ -819,12 +930,12 @@ func (m response_AuthMethodService_UpdateAuthMethod_0) XXX_ResponseBody() interf return response.Item } -type response_AuthMethodService_Authenticate_0 struct { +type response_AuthMethodService_ChangeState_0 struct { proto.Message } -func (m response_AuthMethodService_Authenticate_0) XXX_ResponseBody() interface{} { - response := m.Message.(*AuthenticateResponse) +func (m response_AuthMethodService_ChangeState_0) XXX_ResponseBody() interface{} { + response := m.Message.(*ChangeStateResponse) return response.Item } @@ -848,6 +959,8 @@ var ( pattern_AuthMethodService_DeleteAuthMethod_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2}, []string{"v1", "auth-methods", "id"}, "")) + pattern_AuthMethodService_ChangeState_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2}, []string{"v1", "auth-methods", "id"}, "change-state")) + pattern_AuthMethodService_Authenticate_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2}, []string{"v1", "auth-methods", "auth_method_id"}, "authenticate")) pattern_AuthMethodService_AuthenticateLogin_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 1, 5, 2}, []string{"v1", "auth-methods", "auth_method_id"}, "authenticate:login")) @@ -864,6 +977,8 @@ var ( forward_AuthMethodService_DeleteAuthMethod_0 = runtime.ForwardResponseMessage + forward_AuthMethodService_ChangeState_0 = runtime.ForwardResponseMessage + forward_AuthMethodService_Authenticate_0 = runtime.ForwardResponseMessage forward_AuthMethodService_AuthenticateLogin_0 = runtime.ForwardResponseMessage diff --git a/internal/gen/controller/api/services/auth_method_service_grpc.pb.go b/internal/gen/controller/api/services/auth_method_service_grpc.pb.go index 497e8efde..ab923ba57 100644 --- a/internal/gen/controller/api/services/auth_method_service_grpc.pb.go +++ b/internal/gen/controller/api/services/auth_method_service_grpc.pb.go @@ -44,6 +44,8 @@ type AuthMethodServiceClient interface { // DeleteAuthMethod removes an Auth Method from Boundary. If the Auth Method id // is malformed or not provided an error is returned. DeleteAuthMethod(ctx context.Context, in *DeleteAuthMethodRequest, opts ...grpc.CallOption) (*DeleteAuthMethodResponse, error) + // ChangeState changes the state of an Auth Method from Boundary. + ChangeState(ctx context.Context, in *ChangeStateRequest, opts ...grpc.CallOption) (*ChangeStateResponse, error) // Authenticate validates credentials provided and returns an Auth Token. // Deprecated: use AuthenticateLogin instead. Authenticate(ctx context.Context, in *AuthenticateRequest, opts ...grpc.CallOption) (*AuthenticateResponse, error) @@ -106,6 +108,15 @@ func (c *authMethodServiceClient) DeleteAuthMethod(ctx context.Context, in *Dele return out, nil } +func (c *authMethodServiceClient) ChangeState(ctx context.Context, in *ChangeStateRequest, opts ...grpc.CallOption) (*ChangeStateResponse, error) { + out := new(ChangeStateResponse) + err := c.cc.Invoke(ctx, "/controller.api.services.v1.AuthMethodService/ChangeState", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *authMethodServiceClient) Authenticate(ctx context.Context, in *AuthenticateRequest, opts ...grpc.CallOption) (*AuthenticateResponse, error) { out := new(AuthenticateResponse) err := c.cc.Invoke(ctx, "/controller.api.services.v1.AuthMethodService/Authenticate", in, out, opts...) @@ -155,6 +166,8 @@ type AuthMethodServiceServer interface { // DeleteAuthMethod removes an Auth Method from Boundary. If the Auth Method id // is malformed or not provided an error is returned. DeleteAuthMethod(context.Context, *DeleteAuthMethodRequest) (*DeleteAuthMethodResponse, error) + // ChangeState changes the state of an Auth Method from Boundary. + ChangeState(context.Context, *ChangeStateRequest) (*ChangeStateResponse, error) // Authenticate validates credentials provided and returns an Auth Token. // Deprecated: use AuthenticateLogin instead. Authenticate(context.Context, *AuthenticateRequest) (*AuthenticateResponse, error) @@ -184,6 +197,9 @@ func (UnimplementedAuthMethodServiceServer) UpdateAuthMethod(context.Context, *U func (UnimplementedAuthMethodServiceServer) DeleteAuthMethod(context.Context, *DeleteAuthMethodRequest) (*DeleteAuthMethodResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method DeleteAuthMethod not implemented") } +func (UnimplementedAuthMethodServiceServer) ChangeState(context.Context, *ChangeStateRequest) (*ChangeStateResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method ChangeState not implemented") +} func (UnimplementedAuthMethodServiceServer) Authenticate(context.Context, *AuthenticateRequest) (*AuthenticateResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method Authenticate not implemented") } @@ -293,6 +309,24 @@ func _AuthMethodService_DeleteAuthMethod_Handler(srv interface{}, ctx context.Co return interceptor(ctx, in, info, handler) } +func _AuthMethodService_ChangeState_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ChangeStateRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AuthMethodServiceServer).ChangeState(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/controller.api.services.v1.AuthMethodService/ChangeState", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AuthMethodServiceServer).ChangeState(ctx, req.(*ChangeStateRequest)) + } + return interceptor(ctx, in, info, handler) +} + func _AuthMethodService_Authenticate_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(AuthenticateRequest) if err := dec(in); err != nil { @@ -356,6 +390,10 @@ var AuthMethodService_ServiceDesc = grpc.ServiceDesc{ MethodName: "DeleteAuthMethod", Handler: _AuthMethodService_DeleteAuthMethod_Handler, }, + { + MethodName: "ChangeState", + Handler: _AuthMethodService_ChangeState_Handler, + }, { MethodName: "Authenticate", Handler: _AuthMethodService_Authenticate_Handler, diff --git a/internal/iam/options.go b/internal/iam/options.go index ae0238e6e..236a14322 100644 --- a/internal/iam/options.go +++ b/internal/iam/options.go @@ -20,7 +20,6 @@ type options struct { withName string withDescription string withLimit int - withAutoVivify bool withGrantScopeId string withSkipVetForWrite bool withDisassociate bool @@ -28,6 +27,8 @@ type options struct { withSkipDefaultRoleCreation bool withUserId string withRandomReader io.Reader + withAccountIds []string + withPrimaryAuthMethodId string } func getDefaultOptions() options { @@ -36,7 +37,6 @@ func getDefaultOptions() options { withName: "", withDescription: "", withLimit: 0, - withAutoVivify: false, withGrantScopeId: "", withSkipVetForWrite: false, } @@ -72,14 +72,6 @@ func WithLimit(limit int) Option { } } -// WithAutoVivify provides an option to enable user auto vivification when -// calling repo.LookupUserWithLogin(). -func WithAutoVivify(enable bool) Option { - return func(o *options) { - o.withAutoVivify = enable - } -} - // WithGrantScopeId provides an option to specify the scope ID for grants in // roles. func WithGrantScopeId(id string) Option { @@ -127,9 +119,25 @@ func WithUserId(id string) Option { } } -// WithRandomReader provides and option to specify a random reader. +// WithRandomReader provides an option to specify a random reader. func WithRandomReader(reader io.Reader) Option { return func(o *options) { o.withRandomReader = reader } } + +// WithAccountIds provides an option for specifying account ids to +// add to a user. +func WithAccountIds(id ...string) Option { + return func(o *options) { + o.withAccountIds = id + } +} + +// WithPrimaryAuthMethodId provides an option to specify the +// primary auth method for the scope. +func WithPrimaryAuthMethodId(id string) Option { + return func(o *options) { + o.withPrimaryAuthMethodId = id + } +} diff --git a/internal/iam/options_test.go b/internal/iam/options_test.go index 18824ba1a..5ee5aca85 100644 --- a/internal/iam/options_test.go +++ b/internal/iam/options_test.go @@ -41,19 +41,6 @@ func Test_GetOpts(t *testing.T) { testOpts.withLimit = 1 assert.Equal(opts, testOpts) }) - t.Run("WithAutoVivify", func(t *testing.T) { - assert := assert.New(t) - // test default of false - opts := getOpts() - testOpts := getDefaultOptions() - testOpts.withAutoVivify = false - assert.Equal(opts, testOpts) - - opts = getOpts(WithAutoVivify(true)) - testOpts = getDefaultOptions() - testOpts.withAutoVivify = true - assert.Equal(opts, testOpts) - }) t.Run("WithGrantScopeId", func(t *testing.T) { assert := assert.New(t) opts := getOpts(WithGrantScopeId("o_1234")) @@ -74,4 +61,22 @@ func Test_GetOpts(t *testing.T) { testOpts.withDisassociate = true assert.Equal(opts, testOpts) }) + t.Run("WithAccountIds", func(t *testing.T) { + assert := assert.New(t) + opts := getOpts() + testOpts := getDefaultOptions() + testOpts.withAccountIds = nil + assert.Equal(opts, testOpts) + + opts = getOpts(WithAccountIds("account-1", "account-2")) + testOpts.withAccountIds = []string{"account-1", "account-2"} + assert.Equal(opts, testOpts) + }) + t.Run("WithPrimaryAuthMethodId", func(t *testing.T) { + assert := assert.New(t) + opts := getOpts(WithPrimaryAuthMethodId("test")) + testOpts := getDefaultOptions() + testOpts.withPrimaryAuthMethodId = "test" + assert.Equal(opts, testOpts) + }) } diff --git a/internal/iam/repository.go b/internal/iam/repository.go index 3550caadb..93dcd4ff3 100644 --- a/internal/iam/repository.go +++ b/internal/iam/repository.go @@ -242,7 +242,7 @@ func (r *Repository) delete(ctx context.Context, resource Resource, _ ...Option) func (r *Repository) stdMetadata(ctx context.Context, resource Resource) (oplog.Metadata, error) { const op = "iam.(Repository).stdMetadata" if s, ok := resource.(*Scope); ok { - newScope := allocScope() + newScope := AllocScope() newScope.PublicId = s.PublicId newScope.Type = s.Type if newScope.Type == "" { @@ -251,7 +251,7 @@ func (r *Repository) stdMetadata(ctx context.Context, resource Resource) (oplog. } } switch newScope.Type { - case scope.Org.String(): + case scope.Global.String(), scope.Org.String(): return oplog.Metadata{ "resource-public-id": []string{resource.GetPublicId()}, "scope-id": []string{newScope.PublicId}, diff --git a/internal/iam/repository_group.go b/internal/iam/repository_group.go index aac2e2788..c2a82f8cd 100644 --- a/internal/iam/repository_group.go +++ b/internal/iam/repository_group.go @@ -512,7 +512,6 @@ func groupMemberChanges(ctx context.Context, reader db.Reader, groupId string, u for _, v := range userIds { params = append(params, v) } - // fmt.Println(query, params) rows, err := reader.Query(ctx, query, params) if err != nil { return nil, nil, errors.Wrap(err, op) diff --git a/internal/iam/repository_scope.go b/internal/iam/repository_scope.go index 2b0d694fa..b4ce107fc 100644 --- a/internal/iam/repository_scope.go +++ b/internal/iam/repository_scope.go @@ -386,8 +386,9 @@ func (r *Repository) UpdateScope(ctx context.Context, scope *Scope, version uint var dbMask, nullFields []string dbMask, nullFields = dbcommon.BuildUpdatePaths( map[string]interface{}{ - "name": scope.Name, - "description": scope.Description, + "name": scope.Name, + "description": scope.Description, + "primaryAuthMethodId": scope.PrimaryAuthMethodId, }, fieldMaskPaths, nil, @@ -396,7 +397,6 @@ func (r *Repository) UpdateScope(ctx context.Context, scope *Scope, version uint if len(dbMask) == 0 && len(nullFields) == 0 { return nil, db.NoRowsAffected, errors.E(errors.WithCode(errors.EmptyFieldMask), errors.WithOp(op)) } - resource, rowsUpdated, err := r.update(ctx, scope, version, dbMask, nullFields) if err != nil { if errors.IsUniqueError(err) { @@ -414,7 +414,7 @@ func (r *Repository) LookupScope(ctx context.Context, withPublicId string, _ ... if withPublicId == "" { return nil, errors.New(errors.InvalidParameter, op, "missing public id") } - scope := allocScope() + scope := AllocScope() scope.PublicId = withPublicId if err := r.reader.LookupByPublicId(ctx, &scope); err != nil { if errors.IsNotFoundError(err) { @@ -434,7 +434,7 @@ func (r *Repository) DeleteScope(ctx context.Context, withPublicId string, _ ... if withPublicId == scope.Global.String() { return db.NoRowsAffected, errors.New(errors.InvalidParameter, op, "invalid to delete global scope") } - scope := allocScope() + scope := AllocScope() scope.PublicId = withPublicId rowsDeleted, err := r.delete(ctx, &scope) if err != nil { diff --git a/internal/iam/repository_scope_primary_auth_method_id_test.go b/internal/iam/repository_scope_primary_auth_method_id_test.go new file mode 100644 index 000000000..93df7d7ee --- /dev/null +++ b/internal/iam/repository_scope_primary_auth_method_id_test.go @@ -0,0 +1,52 @@ +package iam_test + +import ( + "context" + "testing" + "time" + + "github.com/hashicorp/boundary/internal/auth/oidc" + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/oplog" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// Test_Repository_UpdateScope_AccountInfoAuthMethodId will simply test updating +// a scope's AccountInfoAuthMethodId. We had to make a separate test in the iam_test +// package to overcome the dreaded diamond dependency problem. +func Test_Repository_UpdateScope_AccountInfoAuthMethodId(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + repo := iam.TestRepo(t, conn, wrapper) + kmsCache := kms.TestKms(t, conn, wrapper) + + t.Run("update-AccountInfoAuthMethodId", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + + org := iam.TestOrg(t, repo) + + databaseWrapper, err := kmsCache.GetWrapper(context.Background(), org.PublicId, kms.KeyPurposeDatabase) + require.NoError(err) + + am := oidc.TestAuthMethod(t, conn, databaseWrapper, org.PublicId, oidc.ActivePublicState, "alice-rp", "fido", oidc.WithSigningAlgs(oidc.RS256), oidc.WithApiUrl(oidc.TestConvertToUrls(t, "https://www.alice.com/callback")[0])) + + org.PrimaryAuthMethodId = am.PublicId + s, updatedRows, err := repo.UpdateScope(context.Background(), org, 1, []string{"PrimaryAuthMethodId"}) + require.NoError(err) + assert.Equal(1, updatedRows) + require.NotNil(s) + assert.Equal(am.PublicId, s.PrimaryAuthMethodId) + + foundScope, err := repo.LookupScope(context.Background(), s.PublicId) + require.NoError(err) + assert.Equal(foundScope.PrimaryAuthMethodId, s.PrimaryAuthMethodId) + + err = db.TestVerifyOplog(t, rw, s.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_UPDATE), db.WithCreateNotBefore(10*time.Second)) + assert.NoError(err) + }) +} diff --git a/internal/iam/repository_scope_test.go b/internal/iam/repository_scope_test.go index ae1ad6242..6a3691763 100644 --- a/internal/iam/repository_scope_test.go +++ b/internal/iam/repository_scope_test.go @@ -442,7 +442,7 @@ func TestRepository_UpdateScope(t *testing.T) { assert.NoError(err) } - foundScope := allocScope() + foundScope := AllocScope() foundScope.PublicId = updatedScope.PublicId where := "public_id = ?" for _, f := range tt.wantNullFields { @@ -520,7 +520,7 @@ func Test_Repository_ListScopes(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) - require.NoError(conn.Where("type = 'org'").Delete(allocScope()).Error) + require.NoError(conn.Where("type = 'org'").Delete(AllocScope()).Error) testOrgs := []*Scope{} for i := 0; i < tt.createCnt; i++ { testOrgs = append(testOrgs, testOrg(t, repo, "", "")) @@ -543,7 +543,7 @@ func TestRepository_ListScopes_Multiple_Scopes(t *testing.T) { wrapper := db.TestWrapper(t) repo := TestRepo(t, conn, wrapper) - require.NoError(t, conn.Where("public_id != 'global'").Delete(allocScope()).Error) + require.NoError(t, conn.Where("public_id != 'global'").Delete(AllocScope()).Error) const numPerScope = 10 var total int diff --git a/internal/iam/repository_test.go b/internal/iam/repository_test.go index d8d33b7e1..128932a32 100644 --- a/internal/iam/repository_test.go +++ b/internal/iam/repository_test.go @@ -307,7 +307,7 @@ func TestRepository_update(t *testing.T) { err = db.TestVerifyOplog(t, rw, org.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_UPDATE), db.WithCreateNotBefore(10*time.Second)) require.NoError(err) - foundResource := allocScope() + foundResource := AllocScope() foundResource.PublicId = updatedResource.GetPublicId() where := "public_id = ?" for _, f := range tt.args.setToNullPaths { diff --git a/internal/iam/repository_user.go b/internal/iam/repository_user.go index 593501fe4..d5d32cd44 100644 --- a/internal/iam/repository_user.go +++ b/internal/iam/repository_user.go @@ -214,13 +214,13 @@ func (r *Repository) ListUsers(ctx context.Context, withScopeIds []string, opt . // LookupUserWithLogin will attempt to lookup the user with a matching // account id and return the user if found. If a user is not found and the -// WithAutoVivify() option is true, then a new iam User will be -// created in the scope of the account, and associated with the +// account's scope is not the PrimaryAuthMethod, then an error is returned. +// If the account's scope is the PrimaryAuthMethod, then a new iam User will be +// created (autovivified) in the scope of the account, and associated with the // account. If a new user is auto vivified, then the WithName and // WithDescription options are supported as well. func (r *Repository) LookupUserWithLogin(ctx context.Context, accountId string, opt ...Option) (*User, error) { const op = "iam.(Repository).LookupUserWithLogin" - opts := getOpts(opt...) if accountId == "" { return nil, errors.New(errors.InvalidParameter, op, "missing account id") } @@ -231,9 +231,6 @@ func (r *Repository) LookupUserWithLogin(ctx context.Context, accountId string, if u != nil { return u, nil } - if !opts.withAutoVivify { - return nil, errors.New(errors.RecordNotFound, op, fmt.Sprintf("user not found for account %s", accountId)) - } acct := allocAccount() acct.PublicId = accountId @@ -242,6 +239,14 @@ func (r *Repository) LookupUserWithLogin(ctx context.Context, accountId string, return nil, errors.Wrap(err, op, errors.WithMsg(fmt.Sprintf("unable to lookup account %s", accountId))) } + allowed, err := r.allowUserAutoVivify(ctx, &acct) + if err != nil { + return nil, errors.Wrap(err, op) + } + if !allowed { + return nil, errors.New(errors.RecordNotFound, op, fmt.Sprintf("user not found for account %s", accountId)) + } + metadata := oplog.Metadata{ "resource-public-id": []string{accountId}, "scope-id": []string{acct.ScopeId}, @@ -306,6 +311,21 @@ func (r *Repository) LookupUserWithLogin(ctx context.Context, accountId string, return obtainedUser, nil } +// allowUserAutoVivify determines if a user can be autovivified based on the account's scope +func (r *Repository) allowUserAutoVivify(ctx context.Context, acct *authAccount) (bool, error) { + const op = "iam.(Repository).allowUserAutoVivify" + if acct == nil { + return false, errors.New(errors.InvalidParameter, op, "missing account") + } + acctScope := AllocScope() + acctScope.PublicId = acct.ScopeId + err := r.reader.LookupByPublicId(context.Background(), &acctScope) + if err != nil { + return false, errors.Wrap(err, op, errors.WithMsg(fmt.Sprintf("unable to lookup account's scope %s", acct.ScopeId))) + } + return acct.AuthMethodId == acctScope.PrimaryAuthMethodId, nil +} + func (r *Repository) getUserWithAccount(ctx context.Context, withAccountId string, _ ...Option) (*User, error) { const op = "iam.(Repository).getUserWithAccount" if withAccountId == "" { diff --git a/internal/iam/repository_user_test.go b/internal/iam/repository_user_test.go index e3172e541..8d7b0ba44 100644 --- a/internal/iam/repository_user_test.go +++ b/internal/iam/repository_user_test.go @@ -571,12 +571,17 @@ func TestRepository_LookupUserWithLogin(t *testing.T) { id := testId(t) org, _ := TestScopes(t, repo) authMethodId := testAuthMethod(t, conn, org.PublicId) + TestSetPrimaryAuthMethod(t, repo, org, authMethodId) newAuthAcct := testAccount(t, conn, org.PublicId, authMethodId, "") - newAuthAcctWithoutVivify := testAccount(t, conn, org.PublicId, authMethodId, "") + + authMethodId2 := testAuthMethod(t, conn, org.PublicId) + newAuthAcctWithoutVivify := testAccount(t, conn, org.PublicId, authMethodId2, "") user := TestUser(t, repo, org.PublicId, WithName("existing-"+id)) - existingAuthAcct := testAccount(t, conn, org.PublicId, authMethodId, user.PublicId) - require.Equal(t, user.PublicId, existingAuthAcct.IamUserId) + existingUserWithAcctWithVivify := testAccount(t, conn, org.PublicId, authMethodId, user.PublicId) + require.Equal(t, user.PublicId, existingUserWithAcctWithVivify.IamUserId) + + existingUserWithAcctNoVivify := testAccount(t, conn, org.PublicId, authMethodId2, user.PublicId) type args struct { withAccountId string @@ -596,7 +601,6 @@ func TestRepository_LookupUserWithLogin(t *testing.T) { args: args{ withAccountId: newAuthAcct.PublicId, opt: []Option{ - WithAutoVivify(true), WithName("valid-" + id), WithDescription("valid-" + id), }, @@ -622,21 +626,19 @@ func TestRepository_LookupUserWithLogin(t *testing.T) { wantErrIs: errors.InvalidParameter, }, { - name: "existing-auth-account", + name: "existing-user-with-account-with-vivify", args: args{ - withAccountId: existingAuthAcct.PublicId, + withAccountId: existingUserWithAcctWithVivify.PublicId, }, wantErr: false, wantName: "existing-" + id, wantUser: user, }, { - name: "existing-auth-account-with-vivify", + name: "existing-user-with-account-no-vivify", args: args{ - withAccountId: existingAuthAcct.PublicId, - opt: []Option{ - WithAutoVivify(true), - }, + withAccountId: existingUserWithAcctNoVivify.PublicId, + opt: []Option{}, }, wantErr: false, wantName: "existing-" + id, @@ -654,9 +656,7 @@ func TestRepository_LookupUserWithLogin(t *testing.T) { name: "bad-auth-account-id-with-vivify", args: args{ withAccountId: id, - opt: []Option{ - WithAutoVivify(true), - }, + opt: []Option{}, }, wantErr: true, wantErrIs: errors.RecordNotFound, diff --git a/internal/iam/scope.go b/internal/iam/scope.go index 6843d3329..6fde2fc83 100644 --- a/internal/iam/scope.go +++ b/internal/iam/scope.go @@ -35,14 +35,14 @@ var ( ) func NewOrg(opt ...Option) (*Scope, error) { - global := allocScope() + global := AllocScope() global.PublicId = "global" return newScope(&global, opt...) } func NewProject(orgPublicId string, opt ...Option) (*Scope, error) { const op = "iam.NewProject" - org := allocScope() + org := AllocScope() org.PublicId = orgPublicId p, err := newScope(&org, opt...) if err != nil { @@ -54,7 +54,8 @@ func NewProject(orgPublicId string, opt ...Option) (*Scope, error) { // newScope creates a new Scope with options: WithName specifies the Scope's // friendly name. WithDescription specifies the scope's description. WithScope // specifies the Scope's parent and must be filled in. The type of the parent is -// used to determine the type of the child. +// used to determine the type of the child. WithPrimaryAuthMethodId specifies +// the primary auth method for the scope func newScope(parent *Scope, opt ...Option) (*Scope, error) { const op = "iam.newScope" if parent == nil || parent.PublicId == "" { @@ -74,17 +75,18 @@ func newScope(parent *Scope, opt ...Option) (*Scope, error) { opts := getOpts(opt...) s := &Scope{ Scope: &store.Scope{ - Type: typ.String(), - Name: opts.withName, - Description: opts.withDescription, - ParentId: parent.PublicId, + Type: typ.String(), + Name: opts.withName, + Description: opts.withDescription, + ParentId: parent.PublicId, + PrimaryAuthMethodId: opts.withPrimaryAuthMethodId, }, } return s, nil } -func allocScope() Scope { +func AllocScope() Scope { return Scope{ Scope: &store.Scope{}, } @@ -131,7 +133,7 @@ func (s *Scope) VetForWrite(ctx context.Context, r db.Reader, opType db.OpType, return errors.New(errors.InvalidParameter, op, `org's parent must be "global"`) } case s.Type == scope.Project.String(): - parentScope := allocScope() + parentScope := AllocScope() parentScope.PublicId = s.ParentId if err := r.LookupByPublicId(ctx, &parentScope, opt...); err != nil { return errors.Wrap(err, op, errors.WithMsg("unable to verify project's org scope")) diff --git a/internal/iam/scope_test.go b/internal/iam/scope_test.go index 75a37bb47..77918d6e6 100644 --- a/internal/iam/scope_test.go +++ b/internal/iam/scope_test.go @@ -147,7 +147,7 @@ func TestScope_GetScope(t *testing.T) { assert.NotNil(projectOrg) assert.Equal(projectOrg.PublicId, proj.ParentId) - p := allocScope() + p := AllocScope() p.PublicId = proj.PublicId p.Type = scope.Project.String() projectOrg, err = p.GetScope(context.Background(), w) @@ -213,7 +213,7 @@ func TestScope_GlobalErrors(t *testing.T) { }) t.Run("creation disallowed at vet time", func(t *testing.T) { // Not allowed to create - s := allocScope() + s := AllocScope() s.Type = scope.Global.String() s.PublicId = "global" err := s.VetForWrite(context.Background(), nil, db.CreateOp) @@ -222,7 +222,7 @@ func TestScope_GlobalErrors(t *testing.T) { }) t.Run("check org parent at vet time", func(t *testing.T) { // Org must have global parent - s := allocScope() + s := AllocScope() s.Type = scope.Org.String() s.PublicId = "o_1234" s.ParentId = "global" @@ -234,7 +234,7 @@ func TestScope_GlobalErrors(t *testing.T) { }) t.Run("not deletable in db", func(t *testing.T) { // Should not be deletable - s := allocScope() + s := AllocScope() s.PublicId = "global" // Add this to validate that we did in fact delete err := w.LookupById(context.Background(), &s) @@ -270,7 +270,7 @@ func TestScope_SetTableName(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) - def := allocScope() + def := AllocScope() require.Equal(defaultTableName, def.TableName()) s := &Scope{ Scope: &store.Scope{}, diff --git a/internal/iam/store/scope.pb.go b/internal/iam/store/scope.pb.go index c3fa6c68e..67b1d8dc6 100644 --- a/internal/iam/store/scope.pb.go +++ b/internal/iam/store/scope.pb.go @@ -51,6 +51,12 @@ type Scope struct { // version allows optimistic locking of the scope // @inject_tag: `gorm:"default:null"` Version uint32 `protobuf:"varint,8,opt,name=version,proto3" json:"version,omitempty" gorm:"default:null"` + // primary_auth_method_id specifies which auth method will be used as a + // source for account information for users within the scope. This auth + // method will also be designated as the auth method which can autovivify + // users. + // @inject_tag: `gorm:"default:null"` + PrimaryAuthMethodId string `protobuf:"bytes,20,opt,name=primary_auth_method_id,json=primaryAuthMethodId,proto3" json:"primary_auth_method_id,omitempty" gorm:"default:null"` } func (x *Scope) Reset() { @@ -141,6 +147,13 @@ func (x *Scope) GetVersion() uint32 { return 0 } +func (x *Scope) GetPrimaryAuthMethodId() string { + if x != nil { + return x.PrimaryAuthMethodId + } + return "" +} + var File_controller_storage_iam_store_v1_scope_proto protoreflect.FileDescriptor var file_controller_storage_iam_store_v1_scope_proto_rawDesc = []byte{ @@ -154,7 +167,7 @@ var file_controller_storage_iam_store_v1_scope_proto_rawDesc = []byte{ 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x2f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x6f, 0x6c, 0x6c, 0x65, 0x72, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2f, 0x76, 0x31, 0x2f, 0x74, 0x69, 0x6d, 0x65, - 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xf1, 0x02, 0x0a, 0x05, + 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xd9, 0x03, 0x0a, 0x05, 0x53, 0x63, 0x6f, 0x70, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x49, 0x64, 0x12, 0x4b, 0x0a, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x74, 0x69, 0x6d, @@ -177,12 +190,18 @@ var file_controller_storage_iam_store_v1_scope_proto_rawDesc = []byte{ 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, - 0x18, 0x08, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x42, - 0x38, 0x5a, 0x36, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, - 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, - 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x69, 0x61, 0x6d, 0x2f, 0x73, 0x74, - 0x6f, 0x72, 0x65, 0x3b, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, - 0x33, + 0x18, 0x08, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, + 0x66, 0x0a, 0x16, 0x70, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, + 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x5f, 0x69, 0x64, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x42, + 0x31, 0xc2, 0xdd, 0x29, 0x2d, 0x0a, 0x13, 0x50, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x41, 0x75, + 0x74, 0x68, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x49, 0x64, 0x12, 0x16, 0x70, 0x72, 0x69, 0x6d, + 0x61, 0x72, 0x79, 0x5f, 0x61, 0x75, 0x74, 0x68, 0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x5f, + 0x69, 0x64, 0x52, 0x13, 0x70, 0x72, 0x69, 0x6d, 0x61, 0x72, 0x79, 0x41, 0x75, 0x74, 0x68, 0x4d, + 0x65, 0x74, 0x68, 0x6f, 0x64, 0x49, 0x64, 0x42, 0x38, 0x5a, 0x36, 0x67, 0x69, 0x74, 0x68, 0x75, + 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x68, 0x61, 0x73, 0x68, 0x69, 0x63, 0x6f, 0x72, 0x70, 0x2f, + 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x61, 0x72, 0x79, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, + 0x6c, 0x2f, 0x69, 0x61, 0x6d, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x3b, 0x73, 0x74, 0x6f, 0x72, + 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/internal/iam/testing.go b/internal/iam/testing.go index 60f2f1e5d..6dd2a0830 100644 --- a/internal/iam/testing.go +++ b/internal/iam/testing.go @@ -39,6 +39,17 @@ func TestRepo(t *testing.T, conn *gorm.DB, rootWrapper wrapping.Wrapper, opt ... return repo } +// TestSetPrimaryAuthMethod will set the PrimaryAuthMethodId for a scope. +func TestSetPrimaryAuthMethod(t *testing.T, repo *Repository, s *Scope, authMethodId string) { + t.Helper() + require := require.New(t) + require.NotEmpty(s) + require.NotEmpty(authMethodId) + s.PrimaryAuthMethodId = authMethodId + _, _, err := repo.UpdateScope(context.Background(), s, s.Version, []string{"PrimaryAuthMethodId"}) + require.NoError(err) +} + // TestScopes creates an org and project suitable for testing. func TestScopes(t *testing.T, repo *Repository, opt ...Option) (org *Scope, prj *Scope) { t.Helper() @@ -121,7 +132,8 @@ func testPublicId(t *testing.T, prefix string) string { return publicId } -// TestUser creates a user suitable for testing. +// TestUser creates a user suitable for testing. Supports the options: +// WithName, WithDescription and WithAccountIds. func TestUser(t *testing.T, repo *Repository, scopeId string, opt ...Option) *User { t.Helper() require := require.New(t) @@ -131,6 +143,11 @@ func TestUser(t *testing.T, repo *Repository, scopeId string, opt ...Option) *Us user, err = repo.CreateUser(context.Background(), user) require.NoError(err) require.NotEmpty(user.PublicId) + opts := getOpts(opt...) + if len(opts.withAccountIds) > 0 { + _, err := repo.AddUserAccounts(context.Background(), user.PublicId, user.Version, opts.withAccountIds) + require.NoError(err) + } return user } diff --git a/internal/kms/kms.go b/internal/kms/kms.go index 7cce24027..d6bafbe8a 100644 --- a/internal/kms/kms.go +++ b/internal/kms/kms.go @@ -2,7 +2,9 @@ package kms import ( "context" + "crypto/sha256" "fmt" + "io" "sync" "github.com/hashicorp/boundary/internal/db" @@ -12,6 +14,7 @@ import ( wrapping "github.com/hashicorp/go-kms-wrapping" "github.com/hashicorp/go-kms-wrapping/wrappers/aead" "github.com/hashicorp/go-kms-wrapping/wrappers/multiwrapper" + "golang.org/x/crypto/hkdf" ) // ExternalWrappers holds wrappers defined outside of Boundary, e.g. in its @@ -57,6 +60,8 @@ type Kms struct { externalScopeCache map[string]*ExternalWrappers externalScopeCacheMutex sync.RWMutex + derivedPurposeCache sync.Map + repo *Repository } @@ -83,6 +88,10 @@ func (k *Kms) GetScopePurposeCache() *sync.Map { return &k.scopePurposeCache } +func (k *Kms) GetDerivedPurposeCache() *sync.Map { + return &k.derivedPurposeCache +} + // AddExternalWrappers allows setting the external keys. // // TODO: If we support more than one, e.g. for encrypting against many in case @@ -151,7 +160,7 @@ func (k *Kms) GetWrapper(ctx context.Context, scopeId string, purpose KeyPurpose } switch purpose { - case KeyPurposeOplog, KeyPurposeDatabase, KeyPurposeTokens, KeyPurposeSessions: + case KeyPurposeOplog, KeyPurposeDatabase, KeyPurposeTokens, KeyPurposeSessions, KeyPurposeOidc: case KeyPurposeUnknown: return nil, errors.New(errors.InvalidParameter, op, "missing key purpose") default: @@ -306,6 +315,8 @@ func (k *Kms) loadDek(ctx context.Context, scopeId string, purpose KeyPurpose, r keys, err = repo.ListTokenKeys(ctx) case KeyPurposeSessions: keys, err = repo.ListSessionKeys(ctx) + case KeyPurposeOidc: + keys, err = repo.ListOidcKeys(ctx) default: return nil, errors.New(errors.InvalidParameter, op, "unknown or invalid DEK purpose specified") } @@ -333,6 +344,8 @@ func (k *Kms) loadDek(ctx context.Context, scopeId string, purpose KeyPurpose, r keyVersions, err = repo.ListTokenKeyVersions(ctx, rootWrapper, keyId, WithOrderByVersion(db.DescendingOrderBy)) case KeyPurposeSessions: keyVersions, err = repo.ListSessionKeyVersions(ctx, rootWrapper, keyId, WithOrderByVersion(db.DescendingOrderBy)) + case KeyPurposeOidc: + keyVersions, err = repo.ListOidcKeyVersions(ctx, rootWrapper, keyId, WithOrderByVersion(db.DescendingOrderBy)) default: return nil, errors.New(errors.InvalidParameter, op, "unknown or invalid DEK purpose specified") } @@ -363,3 +376,41 @@ func (k *Kms) loadDek(ctx context.Context, scopeId string, purpose KeyPurpose, r return multi, nil } + +// DerivedReader returns a reader from which keys can be read, using the +// given wrapper, reader length limit, salt and context info. Salt and info can +// be nil. +// +// Example: +// reader, _ := NewDerivedReader(wrapper, userId, jobId) +// key := ed25519.GenerateKey(reader) +func NewDerivedReader(wrapper wrapping.Wrapper, lenLimit int64, salt, info []byte) (*io.LimitedReader, error) { + const op = "kms.NewDerivedReader" + if wrapper == nil { + return nil, errors.New(errors.InvalidParameter, op, "missing wrapper") + } + if lenLimit < 20 { + return nil, errors.New(errors.InvalidParameter, op, "lenLimit must be >= 20") + } + var aeadWrapper *aead.Wrapper + switch w := wrapper.(type) { + case *multiwrapper.MultiWrapper: + raw := w.WrapperForKeyID("__base__") + var ok bool + if aeadWrapper, ok = raw.(*aead.Wrapper); !ok { + return nil, errors.New(errors.InvalidParameter, op, "unexpected wrapper type from multiwrapper base") + } + case *aead.Wrapper: + if w.GetKeyBytes() == nil { + return nil, errors.New(errors.InvalidParameter, op, "aead wrapper missing bytes") + } + aeadWrapper = w + default: + return nil, errors.New(errors.InvalidParameter, op, "unknown wrapper type") + } + reader := hkdf.New(sha256.New, aeadWrapper.GetKeyBytes(), salt, info) + return &io.LimitedReader{ + R: reader, + N: lenLimit, + }, nil +} diff --git a/internal/kms/kms_test.go b/internal/kms/kms_test.go index 0ce3bb44b..dbd8e964e 100644 --- a/internal/kms/kms_test.go +++ b/internal/kms/kms_test.go @@ -3,12 +3,19 @@ package kms import ( "context" "crypto/rand" + "crypto/sha256" + "io" "testing" "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/errors" "github.com/hashicorp/boundary/internal/types/scope" + wrapping "github.com/hashicorp/go-kms-wrapping" + "github.com/hashicorp/go-kms-wrapping/wrappers/aead" "github.com/hashicorp/go-uuid" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "golang.org/x/crypto/hkdf" ) func TestKms_KeyId(t *testing.T) { @@ -66,3 +73,101 @@ func TestKms_KeyId(t *testing.T) { _, err = kmsCache.GetWrapper(ctx, scope.Global.String(), KeyPurposeDatabase, WithKeyId("foo")) require.Error(err) } + +func TestNewDerivedReader(t *testing.T) { + wrapper := db.TestWrapper(t) + + type args struct { + wrapper wrapping.Wrapper + lenLimit int64 + salt []byte + info []byte + } + tests := []struct { + name string + args args + want *io.LimitedReader + wantErr bool + wantErrCode errors.Code + wantErrContains string + }{ + { + name: "valid-with-salt", + args: args{ + wrapper: wrapper, + lenLimit: 32, + info: nil, + salt: []byte("salt"), + }, + want: &io.LimitedReader{ + R: hkdf.New(sha256.New, wrapper.(*aead.Wrapper).GetKeyBytes(), []byte("salt"), nil), + N: 32, + }, + }, + { + name: "valid-with-salt-info", + args: args{ + wrapper: wrapper, + lenLimit: 32, + info: []byte("info"), + salt: []byte("salt"), + }, + want: &io.LimitedReader{ + R: hkdf.New(sha256.New, wrapper.(*aead.Wrapper).GetKeyBytes(), []byte("salt"), []byte("info")), + N: 32, + }, + }, + { + name: "nil-wrapper", + args: args{ + wrapper: nil, + lenLimit: 10, + info: []byte("info"), + salt: []byte("salt"), + }, + wantErr: true, + wantErrCode: errors.InvalidParameter, + wantErrContains: "missing wrapper", + }, + { + name: "too-short", + args: args{ + wrapper: wrapper, + lenLimit: 10, + info: []byte("info"), + salt: []byte("salt"), + }, + wantErr: true, + wantErrCode: errors.InvalidParameter, + wantErrContains: "lenLimit must be >= 20", + }, + { + name: "wrapper-with-no-bytes", + args: args{ + wrapper: &aead.Wrapper{}, + lenLimit: 32, + info: nil, + salt: []byte("salt"), + }, + wantErr: true, + wantErrCode: errors.InvalidParameter, + wantErrContains: "missing bytes", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + got, err := NewDerivedReader(tt.args.wrapper, tt.args.lenLimit, tt.args.salt, tt.args.info) + if tt.wantErr { + require.Error(err) + assert.Truef(errors.Match(errors.T(errors.InvalidParameter), err), "unexpected error: %s", err) + if tt.wantErrContains != "" { + assert.Contains(err.Error(), tt.wantErrContains) + } + return + } + require.NoError(err) + assert.Equal(tt.want, got) + }) + } +} diff --git a/internal/oplog/oplog_test.go b/internal/oplog/oplog_test.go index afd527042..0c28c00fc 100644 --- a/internal/oplog/oplog_test.go +++ b/internal/oplog/oplog_test.go @@ -334,7 +334,7 @@ func Test_UnmarshalData(t *testing.T) { require.NoError(err) _, err = entry.UnmarshalData(types) require.Error(err) - assert.Equal("oplog.(Entry).UnmarshalData: error removing item from queue: oplog.(Queue).Remove: error getting the TypeName: oplog.(TypeCatalog).Get: type name not found: integrity violation: error #105", err.Error()) + assert.Equal("oplog.(Entry).UnmarshalData: error removing item from queue: oplog.(Queue).Remove: error getting the TypeName: user: oplog.(TypeCatalog).Get: type name not found: integrity violation: error #105", err.Error()) }) } diff --git a/internal/oplog/queue.go b/internal/oplog/queue.go index ed1fc88c0..0ac7fb3d5 100644 --- a/internal/oplog/queue.go +++ b/internal/oplog/queue.go @@ -121,7 +121,7 @@ func (q *Queue) Remove() (proto.Message, OpType, []string, []string, error) { } any, err := q.Catalog.Get(msg.TypeName) if err != nil { - return nil, 0, nil, nil, errors.Wrap(err, op, errors.WithMsg("error getting the TypeName")) + return nil, 0, nil, nil, errors.Wrap(err, op, errors.WithMsg(fmt.Sprintf("error getting the TypeName: %s", msg.TypeName))) } pm := any.(proto.Message) if err = proto.Unmarshal(msg.Value, pm); err != nil { diff --git a/internal/proto/local/controller/api/resources/accounts/v1/account.proto b/internal/proto/local/controller/api/resources/accounts/v1/account.proto index 65e0a600d..304c6a7cb 100644 --- a/internal/proto/local/controller/api/resources/accounts/v1/account.proto +++ b/internal/proto/local/controller/api/resources/accounts/v1/account.proto @@ -19,10 +19,10 @@ message Account { resources.scopes.v1.ScopeInfo scope = 20; // Optional name for identification purposes. - google.protobuf.StringValue name = 30 [(custom_options.v1.generate_sdk_option) = true, (custom_options.v1.mask_mapping) = {this:"name" that: "name"}]; + google.protobuf.StringValue name = 30 [(custom_options.v1.generate_sdk_option) = true, (custom_options.v1.mask_mapping) = {this:"name" that: "Name"}]; // Optional user-set description for identification purposes. - google.protobuf.StringValue description = 40 [(custom_options.v1.generate_sdk_option) = true, (custom_options.v1.mask_mapping) = {this:"description" that: "description"}]; + google.protobuf.StringValue description = 40 [(custom_options.v1.generate_sdk_option) = true, (custom_options.v1.mask_mapping) = {this:"description" that: "Description"}]; // Output only. The time this resource was created. google.protobuf.Timestamp created_time = 50 [json_name="created_time"]; @@ -47,6 +47,7 @@ message Account { repeated string authorized_actions = 300 [json_name="authorized_actions"]; } +// Attributes associated only with Accounts with type "password". message PasswordAccountAttributes { // The login name of this Account. This is unique per Auth Method. string login_name = 10 [json_name="login_name", (custom_options.v1.generate_sdk_option) = true, (custom_options.v1.mask_mapping) = {this:"attributes.login_name" that: "LoginName"}]; @@ -54,3 +55,20 @@ message PasswordAccountAttributes { // The password for this Account. google.protobuf.StringValue password = 20 [(custom_options.v1.generate_sdk_option) = true]; } + +// Attributes associated only with Accounts with type "oidc". +message OidcAccountAttributes { + // issuer is a case sensitive URL that maps to the OIDC iss claim. + // This value is immutable after creation time. + string issuer = 80 [json_name="issuer", (custom_options.v1.generate_sdk_option) = true]; + + // subject is a case sensitive string that maps to the OIDC sub claim. + // This value is immutable after creation time. + string subject = 90 [json_name="subject", (custom_options.v1.generate_sdk_option) = true]; + + // Output only. full_name is a string that maps to the OIDC name claim + string full_name = 100 [json_name="full_name"]; + + // Output only. email is a string that maps to the OIDC email claim. + string email = 110; +} diff --git a/internal/proto/local/controller/api/resources/authmethods/v1/auth_method.proto b/internal/proto/local/controller/api/resources/authmethods/v1/auth_method.proto index ab4133fdd..59b7b49f1 100644 --- a/internal/proto/local/controller/api/resources/authmethods/v1/auth_method.proto +++ b/internal/proto/local/controller/api/resources/authmethods/v1/auth_method.proto @@ -22,10 +22,10 @@ message AuthMethod { resources.scopes.v1.ScopeInfo scope = 30; // Optional name for identification purposes. - google.protobuf.StringValue name = 40 [(custom_options.v1.generate_sdk_option) = true, (custom_options.v1.mask_mapping) = {this:"name" that: "name"}]; + google.protobuf.StringValue name = 40 [(custom_options.v1.generate_sdk_option) = true, (custom_options.v1.mask_mapping) = {this:"name" that: "Name"}]; // Optional user-set description for identification purposes. - google.protobuf.StringValue description = 50 [(custom_options.v1.generate_sdk_option) = true, (custom_options.v1.mask_mapping) = {this:"description" that: "description"}]; + google.protobuf.StringValue description = 50 [(custom_options.v1.generate_sdk_option) = true, (custom_options.v1.mask_mapping) = {this:"description" that: "Description"}]; // Output only. The time this resource was created. google.protobuf.Timestamp created_time = 60 [json_name="created_time"]; @@ -43,6 +43,10 @@ message AuthMethod { // The attributes that are applicable for the specific Auth Method type. google.protobuf.Struct attributes = 100 [(custom_options.v1.generate_sdk_option) = true]; + // Output only. Whether this auth method is the primary auth method for it's scope. + // To change this value update the primary_auth_method_id field on the scope. + bool is_primary = 110 [json_name = "is_primary"]; + // Output only. The available actions on this resource for this user. repeated string authorized_actions = 300 [json_name="authorized_actions"]; @@ -50,10 +54,115 @@ message AuthMethod { map authorized_collection_actions = 310 [json_name="authorized_collection_actions"]; } +// The attributes of a password typed auth method. message PasswordAuthMethodAttributes { // The minimum length allowed for user names for Accounts in this Auth Method. - uint32 min_login_name_length = 10 [json_name="min_login_name_length", (custom_options.v1.generate_sdk_option) = true, (custom_options.v1.mask_mapping) = {this:"attributes.min_login_name_length" that: "MinLoginNameLength"}]; + uint32 min_login_name_length = 10 [json_name="min_login_name_length", (custom_options.v1.generate_sdk_option) = true, (custom_options.v1.mask_mapping) = {this:"attributes.min_login_name_length" that:"MinLoginNameLength"}]; // The minimum length allowed for passwords for Accounts in this Auth Method. - uint32 min_password_length = 20 [json_name="min_password_length", (custom_options.v1.generate_sdk_option) = true, (custom_options.v1.mask_mapping) = {this:"attributes.min_password_length" that: "MinPasswordLength"}]; + uint32 min_password_length = 20 [json_name="min_password_length", (custom_options.v1.generate_sdk_option) = true, (custom_options.v1.mask_mapping) = {this:"attributes.min_password_length" that:"MinPasswordLength"}]; +} + +// The attributes of an OIDC typed auth method. +message OidcAuthMethodAttributes { + // Output only. The state of the auth method. Will be "inactive", + // "active-private", or "active-public". + string state = 10 [json_name="state"]; + + // The issuer URL. Boundary expects only the schema, host, and port and will + // strip off ".well-known/openid-configuration" if present. This will be + // used for configuration discovery as well as for validation of the "iss" + // claim. + google.protobuf.StringValue issuer = 20 [json_name="issuer", (custom_options.v1.generate_sdk_option) = true, (custom_options.v1.mask_mapping) = {this:"attributes.issuer" that:"Issuer"}]; + + // An OAuth 2.0 Client Identifier valid at the Authorization Server. + google.protobuf.StringValue client_id = 30 [json_name="client_id", (custom_options.v1.generate_sdk_option) = true, (custom_options.v1.mask_mapping) = {this:"attributes.client_id" that:"ClientId"}]; + + // Input only. The client's secret. + google.protobuf.StringValue client_secret = 40 [json_name="client_secret", (custom_options.v1.generate_sdk_option) = true, (custom_options.v1.mask_mapping) = {this:"attributes.client_secret" that:"ClientSecret"}]; + + // Output only. The HMAC'd value of the clients secret to indicate whether + // the client secret has changed. + string client_secret_hmac = 50 [json_name="client_secret_hmac"]; + + // The OIDC "max_age" parameter sent to the Authorization Server indicating + // a maximum acceptable time in seconds since the user's last authentication + // before requiring the user to reauthenticate. 0 indicates an immediate + // need to reauthenticate. + google.protobuf.UInt32Value max_age = 60 [json_name="max_age", (custom_options.v1.generate_sdk_option) = true, (custom_options.v1.mask_mapping) = {this:"attributes.max_age" that:"MaxAge"}]; + + // The signing algorithms allowed for the auth method. + repeated string signing_algorithms = 70 [json_name="signing_algorithms", (custom_options.v1.generate_sdk_option) = true, (custom_options.v1.mask_mapping) = {this:"attributes.signing_algorithms" that:"SigningAlgs"}]; + + // The prefix that should be used for any URLs needed during the + // authentication flow. This includes the callback URL, the token retrieval + // URL, and the redirection URL used by the OIDC Authorization Server. + google.protobuf.StringValue api_url_prefix = 80 [json_name="api_url_prefix", (custom_options.v1.generate_sdk_option) = true, (custom_options.v1.mask_mapping) = {this:"attributes.api_url_prefix" that:"ApiUrl"}]; + + // Output only. The callback URL that should be configured on the + // Authorization Server to use during the authentication flow. + string callback_url = 90 [json_name="callback_url"]; + + // Optional PEM-encoded X.509 CA certificates that can be used as trust anchors + // when connecting to an OIDC provider. + repeated string idp_ca_certs = 100 [json_name="idp_ca_certs", (custom_options.v1.generate_sdk_option) = true, (custom_options.v1.mask_mapping) = {this:"attributes.idp_ca_certs" that:"Certificates"}]; + + // Allowed audience claims for this auth method. + repeated string allowed_audiences = 110 [json_name="allowed_audiences", (custom_options.v1.generate_sdk_option) = true, (custom_options.v1.mask_mapping) = {this:"attributes.allowed_audiences" that:"AudClaims"}]; + + // If the Authorization Server's discovered configuration contains values + // that do not match the configuration set on this auth method, this can be + // set to force the local configuration to override the discovered values. + // This is useful in the case of slow-changing or incorrect values announced + // by the Authorization Server. NOTE: For safety, not providing this value + // will be treated the same as "false"; that is, every time the method is + // updated or the state is changed, this value must be set to "true" or it + // will be disabled. + bool disable_discovered_config_validation = 120 [json_name="disable_discovered_config_validation", (custom_options.v1.generate_sdk_option) = true]; + + // dry_run, when set on an update request, indicates that the changes should + // not be persisted. Boundary will still perform the normal checks to confirm + // the auth method is complete and validated against the discovered config. + // This value will also be set on the returned resource when set in the request + // along with the updated fields applied to the resource (but not persisted) as + // a result of the update request. + bool dry_run = 130 [json_name="dry_run", (custom_options.v1.generate_sdk_option) = true]; +} + +// The structure of the OIDC authenticate start response, in the JSON object +message OidcAuthMethodAuthenticateStartResponse { + // The returned authentication URL + string auth_url = 10 [json_name="auth_url"]; + + // The returned token URL + string token_url = 20 [json_name="token_url"]; + + // The returned token ID + string token_id = 30 [json_name="token_id"]; +} + +// The structure of OIDC callback request parameters +message OidcAuthMethodAuthenticateCallbackRequest { + // The returned code + string code = 10 [json_name="code"]; + + // The returned state + string state = 20 [json_name="state"]; + + // Error parameters, if they are returned + string error = 30 [json_name="error"]; + string error_description = 40 [json_name="error_description"]; + string error_uri = 50 [json_name="error_uri"]; +} + +// The structure of OIDC callback response parameters +message OidcAuthMethodAuthenticateCallbackResponse { + // The final redirection URL + string final_redirect_url = 10 [json_name="final_redirect_url"]; +} + +// The structure of OIDC token request parameters +message OidcAuthMethodAuthenticateTokenRequest { + // The ID of the pending token + string token_id = 10 [json_name="token_id"]; } \ No newline at end of file diff --git a/internal/proto/local/controller/api/resources/scopes/v1/scope.proto b/internal/proto/local/controller/api/resources/scopes/v1/scope.proto index 393762cf3..94c4091d8 100644 --- a/internal/proto/local/controller/api/resources/scopes/v1/scope.proto +++ b/internal/proto/local/controller/api/resources/scopes/v1/scope.proto @@ -10,55 +10,60 @@ import "google/protobuf/struct.proto"; import "controller/custom_options/v1/options.proto"; message ScopeInfo { - // Output only. The ID of the Scope. - string id = 1; + // Output only. The ID of the Scope. + string id = 1; - // Output only. The type of the Scope. - string type = 2; + // Output only. The type of the Scope. + string type = 2; - // Output only. The name of the Scope, if any. - string name = 3; + // Output only. The name of the Scope, if any. + string name = 3; - // Output only. The description of the Scope, if any. - string description = 4; + // Output only. The description of the Scope, if any. + string description = 4; - // Output only. The ID of the parent Scope, if any. This field will be empty if this is the "global" scope. - string parent_scope_id = 5 [json_name="parent_scope_id"]; + // Output only. The ID of the parent Scope, if any. This field will be empty if this is the "global" scope. + string parent_scope_id = 5 [json_name = "parent_scope_id"]; } // Scope contains all fields related to a Scope resource message Scope { - // Output only. The ID of the Scope. - string id = 10; + // Output only. The ID of the Scope. + string id = 10; - // The ID of the Scope this resource is in. If this is the "global" Scope this field will be empty. - string scope_id = 20 [json_name="scope_id"]; + // The ID of the Scope this resource is in. If this is the "global" Scope this field will be empty. + string scope_id = 20 [json_name = "scope_id"]; - // Output only. Scope information for this resource. - ScopeInfo scope = 30; + // Output only. Scope information for this resource. + ScopeInfo scope = 30; - // Optional name for identification purposes. - google.protobuf.StringValue name = 40 [(custom_options.v1.generate_sdk_option) = true, (custom_options.v1.mask_mapping) = {this: "name" that: "name"}]; + // Optional name for identification purposes. + google.protobuf.StringValue name = 40 [(custom_options.v1.generate_sdk_option) = true, (custom_options.v1.mask_mapping) = { this: "name" that: "name" }]; - // Optional user-set descripton for identification purposes. - google.protobuf.StringValue description = 50 [(custom_options.v1.generate_sdk_option) = true, (custom_options.v1.mask_mapping) = {this: "description" that: "description"}]; + // Optional user-set descripton for identification purposes. + google.protobuf.StringValue description = 50 [(custom_options.v1.generate_sdk_option) = true, (custom_options.v1.mask_mapping) = { this: "description" that: "description" }]; - // Output only. The time this resource was created. - google.protobuf.Timestamp created_time = 60 [json_name="created_time"]; + // Output only. The time this resource was created. + google.protobuf.Timestamp created_time = 60 [json_name = "created_time"]; - // Output only. The time this resource was last updated. - google.protobuf.Timestamp updated_time = 70 [json_name="updated_time"]; + // Output only. The time this resource was last updated. + google.protobuf.Timestamp updated_time = 70 [json_name = "updated_time"]; - // Version is used in mutation requests, after the initial creation, to ensure this resource has not changed. - // The mutation will fail if the version does not match the latest known good version. - uint32 version = 80; + // Version is used in mutation requests, after the initial creation, to ensure this resource has not changed. + // The mutation will fail if the version does not match the latest known good version. + uint32 version = 80; - // The type of the resource. - string type = 90; + // The type of the resource. + string type = 90; - // Output only. The available actions on this resource for this user. - repeated string authorized_actions = 300 [json_name="authorized_actions"]; + // The ID of the primary auth method for this scope. A primary auth method + // is allowed to vivify users when new accounts are created and is the source for the users account info + google.protobuf.StringValue primary_auth_method_id = 100 + [json_name = "primary_auth_method_id", (custom_options.v1.generate_sdk_option) = true, (custom_options.v1.mask_mapping) = { this: "primary_auth_method_id" that: "PrimaryAuthMethodId" }]; - // Output only. The authorized actions for the scope's collections. - map authorized_collection_actions = 310 [json_name="authorized_collection_actions"]; + // Output only. The available actions on this resource for this user. + repeated string authorized_actions = 300 [json_name = "authorized_actions"]; + + // Output only. The authorized actions for the scope's collections. + map authorized_collection_actions = 310 [json_name = "authorized_collection_actions"]; } diff --git a/internal/proto/local/controller/api/services/v1/auth_method_service.proto b/internal/proto/local/controller/api/services/v1/auth_method_service.proto index 169aabd86..44ad9d921 100644 --- a/internal/proto/local/controller/api/services/v1/auth_method_service.proto +++ b/internal/proto/local/controller/api/services/v1/auth_method_service.proto @@ -14,7 +14,7 @@ import "controller/api/resources/authtokens/v1/authtoken.proto"; service AuthMethodService { // GetAuthMethod returns a stored Auth Method if present. The provided request // must include the Auth Method id. If missing, malformed or referencing a - // non existing resource an error is returned. + // non existing resource an error is returned. rpc GetAuthMethod(GetAuthMethodRequest) returns (GetAuthMethodResponse) { option (google.api.http) = { get: "/v1/auth-methods/{id}" @@ -83,18 +83,28 @@ service AuthMethodService { }; } - // Authenticate validates credentials provided and returns an Auth Token. - // Deprecated: use AuthenticateLogin instead. - rpc Authenticate(AuthenticateRequest) returns (AuthenticateResponse) { + // ChangeState changes the state of an Auth Method from Boundary. + rpc ChangeState(ChangeStateRequest) returns (ChangeStateResponse) { option (google.api.http) = { - post: "/v1/auth-methods/{auth_method_id}:authenticate" + post: "/v1/auth-methods/{id}:change-state" body: "*" response_body: "item" }; + option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { + summary: "Changes the state of an OIDC AuthMethod" + }; + } + + // Authenticate validates credentials provided and returns an Auth Token. + // Deprecated: use AuthenticateLogin instead. + rpc Authenticate(AuthenticateRequest) returns (AuthenticateResponse) { + option (google.api.http) = { + post: "/v1/auth-methods/{auth_method_id}:authenticate" + body: "*" + }; option (grpc.gateway.protoc_gen_openapiv2.options.openapiv2_operation) = { summary: "Authenticate a user to an scope and retrieve an authentication token." }; - } // AuthenticateLogin validates credentials provided for a single-step login @@ -121,9 +131,9 @@ message GetAuthMethodResponse { } message ListAuthMethodsRequest { - string scope_id = 1 [json_name="scope_id"]; - bool recursive = 20 [json_name="recursive"]; - string filter = 30 [json_name="filter"]; + string scope_id = 1 [json_name = "scope_id"]; + bool recursive = 20 [json_name = "recursive"]; + string filter = 30 [json_name = "filter"]; } message ListAuthMethodsResponse { @@ -142,7 +152,7 @@ message CreateAuthMethodResponse { message UpdateAuthMethodRequest { string id = 1; resources.authmethods.v1.AuthMethod item = 2; - google.protobuf.FieldMask update_mask = 3 [json_name="update_mask"]; + google.protobuf.FieldMask update_mask = 3 [json_name = "update_mask"]; } message UpdateAuthMethodResponse { @@ -155,40 +165,89 @@ message DeleteAuthMethodRequest { message DeleteAuthMethodResponse {} -// The layout of the struct for "credentials" field in AuthenticateRequest. This message isn't directly referenced anywhere but is used here to define the expected field names and types. -message PasswordCredentials { - string login_name = 1 [json_name="login_name"]; +// Attributes specific to changing the state of an oidc auth method. +message OidcChangeStateAttributes { + // state must be `inactive`, `active-private`, or `active-public` + string state = 1; + // This flag is only useful for an oidc auth method. It should not be used + // unless the oidc provider's config is incorrectly set and is stopping the + // activation of this auth method. + bool override_oidc_discovery_url_config = 2 [json_name = "override_oidc_discovery_url_config"]; +} + +message ChangeStateRequest { + string id = 1; + // Version is used to ensure this resource has not changed. + // The mutation will fail if the version does not match the latest known good version. + uint32 version = 2; + + // The attributes specific to this auth method's state. + google.protobuf.Struct attributes = 4; +} + +message ChangeStateResponse { + resources.authmethods.v1.AuthMethod item = 1; +} + +// The layout of the struct for "attributes" field in AuthenticateRequest for a password type. This message isn't directly referenced anywhere but is used here to define the expected field names and +// types. +message PasswordLoginAttributes { + string login_name = 1 [json_name = "login_name"]; string password = 2; } +// The layout of the struct for "attributes" field in AuthenticateRequest for a oidc type's start command. This message isn't directly referenced anywhere but is used here to define the expected field +// names and types. +message OidcStartAttributes { + // An object which will be marshaled as JSON and roundtripped in the token command call. + google.protobuf.Struct roundtrip_payload = 1 [json_name = "roundtrip_payload"]; + // Cached marshaled payload. This is not ingressed from the client; anything found will be thrown out. + string cached_roundtrip_payload = 2; +} + +// The layout of the struct for "attributes" field in AuthenticateRequest for a +// oidc type's token command. This message isn't directly referenced anywhere +// but is used here to define the expected field names and types. +message OidcTokenAttributes { + // The token id for the request. This is the token id that was returned from + // the oidc type's start command + string token_id = 1; +} + message AuthenticateRequest { // The ID of the Auth Method in the system that should be used for authentication. - string auth_method_id = 1 [json_name="auth_method_id"]; - // This can be "cookie" or "token". If not provided, "token" will be used. "cookie" activates a split-cookie method where the token is split partially between http-only and regular cookies in order to keep it safe from rogue JS in the browser. - string token_type = 2 [json_name="token_type"]; + string auth_method_id = 1 [json_name = "auth_method_id"]; + // This can be "cookie" or "token". If not provided, "token" will be used. "cookie" activates a split-cookie method where the token is split partially between http-only and regular cookies in order + // to keep it safe from rogue JS in the browser. + string token_type = 2 [json_name = "token_type"]; // Deprecated; use "attributes" instead. - google.protobuf.Struct credentials = 3 [json_name="credentials", deprecated=true]; + google.protobuf.Struct credentials = 3 [json_name = "credentials", deprecated = true]; // Attributes are passed to the Auth Method; the valid keys and values depend on the type of Auth Method as well as the command. - google.protobuf.Struct attributes = 4 [json_name="attributes"]; + google.protobuf.Struct attributes = 4 [json_name = "attributes"]; // The command to perform. - string command = 5 [json_name="command"]; + string command = 5 [json_name = "command"]; } message AuthenticateResponse { - resources.authtokens.v1.AuthToken item = 1; - string token_type = 2 [json_name="token_type"]; + reserved 1, 2; // Old item and token_type + reserved "item", "token_type"; + // Valid keys and values depend on the type of Auth Method as well as the command. + google.protobuf.Struct attributes = 4 [json_name = "attributes"]; + // The command that was performed. + string command = 5 [json_name = "command"]; } message AuthenticateLoginRequest { // The ID of the Auth Method in the system that should be used for authentication. - string auth_method_id = 1 [json_name="auth_method_id"]; - // This can be "cookie" or "token". If not provided, "token" will be used. "cookie" activates a split-cookie method where the token is split partially between http-only and regular cookies in order to keep it safe from rogue JS in the browser. - string token_type = 2 [json_name="token_type"]; + string auth_method_id = 1 [json_name = "auth_method_id"]; + // This can be "cookie" or "token". If not provided, "token" will be used. "cookie" activates a split-cookie method where the token is split partially between http-only and regular cookies in order + // to keep it safe from rogue JS in the browser. + string token_type = 2 [json_name = "token_type"]; // Credentials are passed to the Auth Method; the valid keys and values depend on the type of Auth Method. google.protobuf.Struct credentials = 3; } message AuthenticateLoginResponse { resources.authtokens.v1.AuthToken item = 1; - string token_type = 2 [json_name="token_type"]; + string token_type = 2 [json_name = "token_type"]; } \ No newline at end of file diff --git a/internal/proto/local/controller/storage/auth/oidc/request/v1/request.proto b/internal/proto/local/controller/storage/auth/oidc/request/v1/request.proto new file mode 100644 index 000000000..0dfa7e622 --- /dev/null +++ b/internal/proto/local/controller/storage/auth/oidc/request/v1/request.proto @@ -0,0 +1,76 @@ +syntax = "proto3"; + +// Package store provides protobufs for storing types in the password package. +package controller.storage.auth.oidc.request.v1; +option go_package = "github.com/hashicorp/boundary/internal/auth/oidc/request;request"; + +import "controller/storage/timestamp/v1/timestamp.proto"; +import "controller/custom_options/v1/options.proto"; +import "google/protobuf/wrappers.proto"; + +// State represents an oidc request state. State will be marshaled and then +// wrapped with an EncryptedState before that's marshaled and encrypted. +// State used in two legs of the oidc authentication flow. + +// First, State is used in constructing the authorization URL, in the first +// leg of the authen flow. State represents the unique data used to construct +// an oidc.Request (see: https://github.com/hashicorp/cap/blob/main/oidc/request.go). +// State needs enough information, that when combined with a Boundary oidc auth method, +// a proper oidc.Request can be recreated during the second leg of the authen flow. State +// also needs the provider.ConfigHash() used to from the first leg, so it can verify +// the Boundary's oidc auth method configuration hasn't changed since the authen flow +// began. +message State { + // token_request_id is the id. This id is used by the client to poll for a Boundary + // token, once the final leg of the authen flow is compeleted. The Callback uses this + // id to create a "pending" token for that polling process. + string token_request_id = 10; + + // create_time of the request that started the authentication flow. + timestamp.v1.Timestamp create_time = 20; + + // expiration_time of the authenticaion flow. + timestamp.v1.Timestamp expiration_time = 30; + + // final_redirect_url that will be sent back to the client after the callback + string final_redirect_url = 40; + + // nonce of the request which is used to verify the ID Token in the third leg + // as a way to prevent replay attacks. + // + // See https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest + // and https://openid.net/specs/openid-connect-core-1_0.html#NonceNotes. + string nonce = 50; + + // provider_config_hash can be used to see if the provider's config has changed + // since the request started. + uint64 provider_config_hash = 60; +} + +// Token is the request token that's returned as part of the auth_token_url from +// oidc.StartAuth(...) +message Token { + // request_id for the token. + string request_id = 10; + + // expiration_time of the authenticaion flow. + timestamp.v1.Timestamp expiration_time = 20; +} + +// Wrapper wraps an encrypted cipher text with non-sensitive info +// which allows Boundary to determine how to decrypt +// the wrappered cipher text (ct) field. +message Wrapper { + // auth_method_id is the auth method of the oidc request + string auth_method_id = 10; + + // scope_id is the auth method's scope + string scope_id = 20; + + // wrapper_key_id is the DEK wrapper key id which was used to derive the + // cipher's key + string wrapper_key_id = 30; + + // ct is the encrypted cipher text + bytes ct = 40; +} diff --git a/internal/proto/local/controller/storage/auth/oidc/store/v1/oidc.proto b/internal/proto/local/controller/storage/auth/oidc/store/v1/oidc.proto new file mode 100644 index 000000000..eb02104e6 --- /dev/null +++ b/internal/proto/local/controller/storage/auth/oidc/store/v1/oidc.proto @@ -0,0 +1,204 @@ +syntax = "proto3"; + +// Package store provides protobufs for storing types in the password package. +package controller.storage.auth.oidc.store.v1; +option go_package = "github.com/hashicorp/boundary/internal/auth/oidc/store;store"; + +import "controller/storage/timestamp/v1/timestamp.proto"; +import "controller/custom_options/v1/options.proto"; +import "google/protobuf/wrappers.proto"; + +// AuthMethod represents an OIDC auth method. +message AuthMethod { + // @inject_tag: `gorm:"primary_key"` + string public_id = 10; + + // The create_time is set by the database. + // @inject_tag: `gorm:"default:current_timestamp"` + timestamp.v1.Timestamp create_time = 20; + + // The update_time is set by the database. + // @inject_tag: `gorm:"default:current_timestamp"` + timestamp.v1.Timestamp update_time = 30; + + // name is optional. If set, it must be unique within scope_id. + // @inject_tag: `gorm:"default:null"` + string name = 40 [(custom_options.v1.mask_mapping) = { this: "Name" that: "name" }]; + + // description is optional. + // @inject_tag: `gorm:"default:null"` + string description = 50 [(custom_options.v1.mask_mapping) = { this: "Description" that: "description" }]; + + // The scope_id of the owning scope. Must be set. + // @inject_tag: `gorm:"not_null"` + string scope_id = 60; + + // @inject_tag: `gorm:"default:null"` + uint32 version = 70; + + // is_primary_auth_method is a read-only output field which indicates if the + // auth method is set as the scope's primary auth method. + // @inject_tag: `gorm:"-"` + bool is_primary_auth_method = 75; + + // operational_state is the current state of the auth_oidc_method (inactive, + // active-private, or active-public). + // @inject_tag: `gorm:"column:state;not_null"` + string operational_state = 80; + + // disable_discovered_config_validation is a flag that when set to true + // indicates the AuthMethod config was not validated against the IdP's + // discovery info document when it was updated or its operational_state was + // changed. + // @inject_tag: `gorm:"not_null"` + bool disable_discovered_config_validation = 89; + + // issuer is the OIDC Discovery URL without any .well-known component + // @inject_tag: `gorm:"default:null"` + string issuer = 90 [(custom_options.v1.mask_mapping) = { this: "Issuer" that: "attributes.issuer" }]; + + // client_id is the OIDC client identifier + // @inject_tag: `gorm:"not_null"` + string client_id = 100 [(custom_options.v1.mask_mapping) = { this: "ClientId" that: "attributes.client_id" }]; + + // ct_client_secret is the encrypted OIDC client secret which is stored in the db. + // @inject_tag: `gorm:"column:client_secret;not_null" wrapping:"ct,client_secret"` + bytes ct_client_secret = 110; + + // client_secret is the unencrypted OIDC client secret which is not stored in the database. + // @inject_tag: `gorm:"-" wrapping:"pt,client_secret"` + string client_secret = 120 [(custom_options.v1.mask_mapping) = { this: "ClientSecret" that: "attributes.client_secret" }]; + + // client_secret_hmac is a sha256-hmac of the unencrypted client_secret that + // is returned from the API for read. It is recalculated everytime the raw + // client_secret is updated. + // @inject_tag: `gorm:"not_null"` + string client_secret_hmac = 130; + + // key_id is the key ID that was used for the encryption operation. It can be + // used to identify a specific version of the key needed to decrypt the value, + // which is useful for caching purposes. + // @inject_tag: `gorm:"not_null"` + string key_id = 140; + + // max_age is the allowed elapsed time in seconds since the last time the user + // was actively authenticated by the OIDC provider. -1 indicates the user + // should be re-authenticated immediately and would represent the zero value + // for max age based on the oidc spec. + // @inject_tag: `gorm:"default:null"` + int32 max_age = 150 [(custom_options.v1.mask_mapping) = { this: "MaxAge" that: "attributes.max_age" }]; + + // api_url is the URLs prefix at which the boundary api is reachable. This value + // is used by the auth method in the different steps of the auth flow. + // @inject_tag: `gorm:"default:null"` + string api_url = 160 [(custom_options.v1.mask_mapping) = { this: "ApiUrl" that: "attributes.api_url_prefix" }]; + + // signing_algs are the signing algorithms allowed for an oidc auth method. + // These are Value Objects that will be stored as SigningAlg messages, and are + // operated on as a complete set. + // @inject_tag: `gorm:"-"` + repeated string signing_algs = 170 [(custom_options.v1.mask_mapping) = { this: "SigningAlgs" that: "attributes.signing_algorithms" }]; + + // aud_claims are the audience claims for a specific oidc auth method. These + // are Value Objects that will be stored as AudClaim messages, and are + // operatated on as a complete set. + // @inject_tag: `gorm:"-"` + repeated string aud_claims = 180 [(custom_options.v1.mask_mapping) = { this: "AudClaims" that: "attributes.allowed_audiences" }]; + + // certificates are optional PEM encoded x509 certificates that can be + // used as trust anchors when connecting to an OIDC provider. These are Value + // Objects that will be stored as Certificate messages, and are operatated on + // as a complete set. + // @inject_tag: `gorm:"-"` + repeated string certificates = 190 [(custom_options.v1.mask_mapping) = { this: "Certificates" that: "attributes.idp_ca_certs" }]; +} + +// Account represents an OIDC account +// the scope_id column is not included here as it is used only to ensure +// data integrity in the database between iam users and auth methods. +message Account { + // @inject_tag: `gorm:"primary_key"` + string public_id = 10; + + // The create_time is set by the database. + // @inject_tag: `gorm:"default:current_timestamp"` + timestamp.v1.Timestamp create_time = 20; + + // The update_time is set by the database. + // @inject_tag: `gorm:"default:current_timestamp"` + timestamp.v1.Timestamp update_time = 30; + + // name is optional. If set, it must be unique within scope_id. + // @inject_tag: `gorm:"default:null"` + string name = 40 [(custom_options.v1.mask_mapping) = { this: "Name" that: "name" }]; + + // description is optional. + // @inject_tag: `gorm:"default:null"` + string description = 50 [(custom_options.v1.mask_mapping) = { this: "Description" that: "description" }]; + + // @inject_tag: `gorm:"default:null"` + uint32 version = 60; + + // auth_method_id is the fk to the account's auth method. + // @inject_tag: `gorm:"not_null"` + string auth_method_id = 70; + + // issuer is a case sensitive URL that maps to the OIDC iss claim + // @inject_tag: `gorm:"not_null"` + string issuer = 80; + + // subject is a case sensitive string that maps to the OIDC sub claim. + // @inject_tag: `gorm:"not_null"` + string subject = 90; + + // full_name is a string that maps to the OIDC name claim + // @inject_tag: `gorm:"default:null"` + string full_name = 100; + + // email is a string that maps to the OIDC email claim. + // @inject_tag: `gorm:"default:null"` + string email = 110; +} + +// SigningAlg entries are the signing algorithms allowed for an oidc auth method. +message SigningAlg { + // @inject_tag: `gorm:"primary_key"` + string oidc_method_id = 10; + + // alg is an enum from the auth_oidc_signing_alg_enm table + // @inject_tag: `gorm:"primary_key;column:signing_alg_name"` + string alg = 20; + + // The create_time is set by the database. + // @inject_tag: `gorm:"default:current_timestamp"` + timestamp.v1.Timestamp create_time = 30; +} + +// AudClaim entries are the audience claims for a specific oidc auth method. +message AudClaim { + // @inject_tag: `gorm:"primary_key"` + string oidc_method_id = 10; + + // aud is an allowed audience claim for id_tokens + // @inject_tag: `gorm:"primary_key;column:aud_claim""` + string aud = 20; + + // The create_time is set by the database. + // @inject_tag: `gorm:"default:current_timestamp"` + timestamp.v1.Timestamp create_time = 30; +} + +// Certificate entries are optional PEM encoded x509 certificates that can be +// used as trust anchors when connecting to an OIDC provider. +message Certificate { + // @inject_tag: `gorm:"primary_key"` + string oidc_method_id = 10; + + // certificate is a PEM encoded x509 + // @inject_tag: `gorm:"column:certificate;primary_key"` + string cert = 20; + + // The create_time is set by the database. + // @inject_tag: `gorm:"default:current_timestamp"` + timestamp.v1.Timestamp create_time = 30; +} \ No newline at end of file diff --git a/internal/proto/local/controller/storage/auth/password/store/v1/password.proto b/internal/proto/local/controller/storage/auth/password/store/v1/password.proto index 6f2533dc9..5d1e1e0f1 100644 --- a/internal/proto/local/controller/storage/auth/password/store/v1/password.proto +++ b/internal/proto/local/controller/storage/auth/password/store/v1/password.proto @@ -21,11 +21,11 @@ message AuthMethod { // name is optional. If set, it must be unique within scope_id. // @inject_tag: `gorm:"default:null"` - string name = 4 [(custom_options.v1.mask_mapping) = {this:"name" that: "name"}]; + string name = 4 [(custom_options.v1.mask_mapping) = { this: "Name" that: "name" }]; // description is optional. // @inject_tag: `gorm:"default:null"` - string description = 5 [(custom_options.v1.mask_mapping) = {this:"description" that: "description"}]; + string description = 5 [(custom_options.v1.mask_mapping) = { this: "Description" that: "description" }]; // The scope_id of the owning scope. Must be set. // @inject_tag: `gorm:"not_null"` @@ -38,10 +38,15 @@ message AuthMethod { string password_conf_id = 8; // @inject_tag: `gorm:"default:null"` - uint32 min_login_name_length = 9 [(custom_options.v1.mask_mapping) = {this:"MinLoginNameLength" that: "attributes.min_login_name_length"}]; + uint32 min_login_name_length = 9 [(custom_options.v1.mask_mapping) = { this: "MinLoginNameLength" that: "attributes.min_login_name_length" }]; // @inject_tag: `gorm:"default:null"` - uint32 min_password_length = 10 [(custom_options.v1.mask_mapping) = {this:"MinPasswordLength" that: "attributes.min_password_length"}]; + uint32 min_password_length = 10 [(custom_options.v1.mask_mapping) = { this: "MinPasswordLength" that: "attributes.min_password_length" }]; + + // is_primary_auth_method is a read-only output field which indicates if the + // auth method is set as the scope's primary auth method. + // @inject_tag: `gorm:"-"` + bool is_primary_auth_method = 20; } message Account { @@ -58,11 +63,11 @@ message Account { // name is optional. If set, it must be unique within scope_id. // @inject_tag: `gorm:"default:null"` - string name = 4 [(custom_options.v1.mask_mapping) = {this:"name" that: "name"}]; + string name = 4 [(custom_options.v1.mask_mapping) = { this: "Name" that: "name" }]; // description is optional. // @inject_tag: `gorm:"default:null"` - string description = 5 [(custom_options.v1.mask_mapping) = {this:"description" that: "description"}]; + string description = 5 [(custom_options.v1.mask_mapping) = { this: "Description" that: "description" }]; // @inject_tag: `gorm:"default:null"` uint32 version = 6; @@ -71,7 +76,7 @@ message Account { string auth_method_id = 7; // @inject_tag: `gorm:"not_null"` - string login_name = 8 [(custom_options.v1.mask_mapping) = {this:"LoginName" that: "attributes.login_name"}]; + string login_name = 8 [(custom_options.v1.mask_mapping) = { this: "LoginName" that: "attributes.login_name" }]; // the scope_id column is not included here as it is used only to ensure // data integrity in the database between iam users and auth methods. diff --git a/internal/proto/local/controller/storage/authtoken/store/v1/authtoken.proto b/internal/proto/local/controller/storage/authtoken/store/v1/authtoken.proto index a18f4287f..412748994 100644 --- a/internal/proto/local/controller/storage/authtoken/store/v1/authtoken.proto +++ b/internal/proto/local/controller/storage/authtoken/store/v1/authtoken.proto @@ -6,57 +6,62 @@ option go_package = "github.com/hashicorp/boundary/internal/authtoken/store;stor import "controller/storage/timestamp/v1/timestamp.proto"; message AuthToken { - // public_id is used to access the auth token via an API - // @inject_tag: gorm:"primary_key" - string public_id = 1; + // public_id is used to access the auth token via an API + // @inject_tag: gorm:"primary_key" + string public_id = 1; - // create_time from the RDBMS - // @inject_tag: `gorm:"default:current_timestamp"` - timestamp.v1.Timestamp create_time = 2; + // create_time from the RDBMS + // @inject_tag: `gorm:"default:current_timestamp"` + timestamp.v1.Timestamp create_time = 2; - // update_time from the RDBMS - // @inject_tag: `gorm:"default:current_timestamp"` - timestamp.v1.Timestamp update_time = 3; + // update_time from the RDBMS + // @inject_tag: `gorm:"default:current_timestamp"` + timestamp.v1.Timestamp update_time = 3; - // last_access_time indicates the last time the auth token was used on the boundary API. - // @inject_tag: `gorm:"default:current_timestamp"` - timestamp.v1.Timestamp approximate_last_access_time = 4; + // last_access_time indicates the last time the auth token was used on the boundary API. + // @inject_tag: `gorm:"default:current_timestamp"` + timestamp.v1.Timestamp approximate_last_access_time = 4; - // expiration_time indicates when this session will expire. - // If null a default duration and create_time is used to calculate expiration. - // @inject_tag: `gorm:"default:null"` - timestamp.v1.Timestamp expiration_time = 5; + // expiration_time indicates when this session will expire. + // If null a default duration and create_time is used to calculate expiration. + // @inject_tag: `gorm:"default:null"` + timestamp.v1.Timestamp expiration_time = 5; - // ciphertext token value stored in the database - // @inject_tag: gorm:"column:token;not_null" wrapping:"ct,authtoken_token" - bytes ct_token = 6; + // ciphertext token value stored in the database + // @inject_tag: gorm:"column:token;not_null" wrapping:"ct,authtoken_token" + bytes ct_token = 6; - // plain text version of the decrypted authtoken value - // we are NOT storing this plain-text entry data in the db - // token is the field stored and used by the client - // @inject_tag: gorm:"-" wrapping:"pt,authtoken_token" - string token = 7; + // plain text version of the decrypted authtoken value + // we are NOT storing this plain-text entry data in the db + // token is the field stored and used by the client + // @inject_tag: gorm:"-" wrapping:"pt,authtoken_token" + string token = 7; - // auth_account_id is the public id for the auth account this auth token - // was generated for. - // @inject_tag: `gorm:"default:not_null"` - string auth_account_id = 10; + // auth_account_id is the public id for the auth account this auth token + // was generated for. + // @inject_tag: `gorm:"default:not_null"` + string auth_account_id = 10; - // scope_id is not stored in the backing DB but it derived from the linked to auth account. - // @inject_tag: gorm:"-" - string scope_id = 11; + // scope_id is not stored in the backing DB but it derived from the linked to auth account. + // @inject_tag: gorm:"-" + string scope_id = 11; - // auth_method_id is not stored in the backing DB but it derived from the linked to auth account. - // @inject_tag: gorm:"-" - string auth_method_id = 12; + // auth_method_id is not stored in the backing DB but it derived from the linked to auth account. + // @inject_tag: gorm:"-" + string auth_method_id = 12; - // iam_user_id is not stored in the backing DB but it derived from the linked to auth account. - // @inject_tag: gorm:"-" - string iam_user_id = 13; + // iam_user_id is not stored in the backing DB but it derived from the linked to auth account. + // @inject_tag: gorm:"-" + string iam_user_id = 13; - // key_id is the key ID that was used for the encryption operation. It can be - // used to identify a specific version of the key needed to decrypt the value, - // which is useful for caching purposes. - // @inject_tag: `gorm:"not_null"` - string key_id = 14; + // key_id is the key ID that was used for the encryption operation. It can be + // used to identify a specific version of the key needed to decrypt the value, + // which is useful for caching purposes. + // @inject_tag: `gorm:"not_null"` + string key_id = 14; + + // status is the auth token's status. It will default of "token issued" in the + // database. + // @inject_tag: `gorm:"default:null"` + string status = 15; } \ No newline at end of file diff --git a/internal/proto/local/controller/storage/iam/store/v1/scope.proto b/internal/proto/local/controller/storage/iam/store/v1/scope.proto index 7c8532ee1..13f25d466 100644 --- a/internal/proto/local/controller/storage/iam/store/v1/scope.proto +++ b/internal/proto/local/controller/storage/iam/store/v1/scope.proto @@ -22,7 +22,7 @@ message Scope { // name is the optional friendly name used to // access the Scope via an API // @inject_tag: `gorm:"default:null"` - string name = 4 [(custom_options.v1.mask_mapping) = {this: "name" that: "name"}]; + string name = 4 [(custom_options.v1.mask_mapping) = { this: "name" that: "name" }]; // Type of scope string type = 5; @@ -33,9 +33,16 @@ message Scope { // description of scope // @inject_tag: `gorm:"default:null"` - string description = 7 [(custom_options.v1.mask_mapping) = {this: "description" that: "description"}]; + string description = 7 [(custom_options.v1.mask_mapping) = { this: "description" that: "description" }]; // version allows optimistic locking of the scope // @inject_tag: `gorm:"default:null"` uint32 version = 8; + + // primary_auth_method_id specifies which auth method will be used as a + // source for account information for users within the scope. This auth + // method will also be designated as the auth method which can autovivify + // users. + // @inject_tag: `gorm:"default:null"` + string primary_auth_method_id = 20 [(custom_options.v1.mask_mapping) = { this: "PrimaryAuthMethodId" that: "primary_auth_method_id" }]; } diff --git a/internal/servers/controller/common/common.go b/internal/servers/controller/common/common.go index 1823289bc..0ec1d0e39 100644 --- a/internal/servers/controller/common/common.go +++ b/internal/servers/controller/common/common.go @@ -1,8 +1,8 @@ package common import ( + "github.com/hashicorp/boundary/internal/auth/oidc" "github.com/hashicorp/boundary/internal/auth/password" - "github.com/hashicorp/boundary/internal/authtoken" "github.com/hashicorp/boundary/internal/host/static" "github.com/hashicorp/boundary/internal/iam" "github.com/hashicorp/boundary/internal/servers" @@ -11,8 +11,9 @@ import ( ) type ( - AuthTokenRepoFactory func() (*authtoken.Repository, error) + AuthTokenRepoFactory = oidc.AuthTokenRepoFactory IamRepoFactory func() (*iam.Repository, error) + OidcAuthRepoFactory = oidc.OidcRepoFactory PasswordAuthRepoFactory func() (*password.Repository, error) ServersRepoFactory func() (*servers.Repository, error) StaticRepoFactory func() (*static.Repository, error) diff --git a/internal/servers/controller/controller.go b/internal/servers/controller/controller.go index bffd6a7ba..c34d0fd3c 100644 --- a/internal/servers/controller/controller.go +++ b/internal/servers/controller/controller.go @@ -6,6 +6,7 @@ import ( "fmt" "sync" + "github.com/hashicorp/boundary/internal/auth/oidc" "github.com/hashicorp/boundary/internal/auth/password" "github.com/hashicorp/boundary/internal/authtoken" "github.com/hashicorp/boundary/internal/cmd/config" @@ -40,6 +41,7 @@ type Controller struct { // Repo factory methods AuthTokenRepoFn common.AuthTokenRepoFactory IamRepoFn common.IamRepoFactory + OidcRepoFn common.OidcAuthRepoFactory PasswordAuthRepoFn common.PasswordAuthRepoFactory ServersRepoFn common.ServersRepoFactory SessionRepoFn common.SessionRepoFactory @@ -120,6 +122,9 @@ func New(conf *Config) (*Controller, error) { c.ServersRepoFn = func() (*servers.Repository, error) { return servers.NewRepository(dbase, dbase, c.kms) } + c.OidcRepoFn = func() (*oidc.Repository, error) { + return oidc.NewRepository(dbase, dbase, c.kms) + } c.PasswordAuthRepoFn = func() (*password.Repository, error) { return password.NewRepository(dbase, dbase, c.kms) } @@ -149,6 +154,7 @@ func (c *Controller) Start() error { c.startStatusTicking(c.baseContext) c.startRecoveryNonceCleanupTicking(c.baseContext) c.startTerminateCompletedSessionsTicking(c.baseContext) + c.startCloseExpiredPendingTokens(c.baseContext) c.started.Store(true) return nil diff --git a/internal/servers/controller/handler.go b/internal/servers/controller/handler.go index 51ad8f410..faf502ba8 100644 --- a/internal/servers/controller/handler.go +++ b/internal/servers/controller/handler.go @@ -1,10 +1,13 @@ package controller import ( + "bytes" "context" "encoding/json" "fmt" + "io/ioutil" "net/http" + "net/textproto" "os" "strings" "time" @@ -55,14 +58,15 @@ func (c *Controller) handler(props HandlerProperties) (http.Handler, error) { corsWrappedHandler := wrapHandlerWithCors(mux, props) commonWrappedHandler := wrapHandlerWithCommonFuncs(corsWrappedHandler, c, props) - printablePathCheckHandler := cleanhttp.PrintablePathCheckHandler(commonWrappedHandler, nil) + callbackInterceptingHandler := wrapHandlerWithCallbackInterceptor(commonWrappedHandler, c) + printablePathCheckHandler := cleanhttp.PrintablePathCheckHandler(callbackInterceptingHandler, nil) return printablePathCheckHandler, nil } func handleGrpcGateway(c *Controller, props HandlerProperties) (http.Handler, error) { - // Register*ServiceHandlerServer methods ignore the passed in ctx. Using - // the a context now just in case this changes in the future + // Register*ServiceHandlerServer methods ignore the passed in ctx. Using it + // now however in case this changes in the future. ctx := props.CancelCtx mux := runtime.NewServeMux( runtime.WithMarshalerOption(runtime.MIMEWildcard, &runtime.HTTPBodyMarshaler{ @@ -103,14 +107,14 @@ func handleGrpcGateway(c *Controller, props HandlerProperties) (http.Handler, er if err := services.RegisterHostServiceHandlerServer(ctx, mux, hs); err != nil { return nil, fmt.Errorf("failed to register host service handler: %w", err) } - accts, err := accounts.NewService(c.PasswordAuthRepoFn) + accts, err := accounts.NewService(c.PasswordAuthRepoFn, c.OidcRepoFn) if err != nil { return nil, fmt.Errorf("failed to create account handler service: %w", err) } if err := services.RegisterAccountServiceHandlerServer(ctx, mux, accts); err != nil { return nil, fmt.Errorf("failed to register account service handler: %w", err) } - authMethods, err := authmethods.NewService(c.kms, c.PasswordAuthRepoFn, c.IamRepoFn, c.AuthTokenRepoFn) + authMethods, err := authmethods.NewService(c.kms, c.PasswordAuthRepoFn, c.OidcRepoFn, c.IamRepoFn, c.AuthTokenRepoFn) if err != nil { return nil, fmt.Errorf("failed to create auth method handler service: %w", err) } @@ -321,6 +325,89 @@ func wrapHandlerWithCors(h http.Handler, props HandlerProperties) http.Handler { }) } +type cmdAttrs struct { + Command string `json:"command,omitempty"` + Attributes interface{} `json:"attributes,omitempty"` +} + +func wrapHandlerWithCallbackInterceptor(h http.Handler, c *Controller) http.Handler { + logCallbackErrors := os.Getenv("BOUNDARY_LOG_CALLBACK_ERRORS") != "" + + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + // If this doesn't have a callback suffix on a supported action, serve + // normally + if !strings.HasSuffix(req.URL.Path, ":authenticate:callback") { + h.ServeHTTP(w, req) + return + } + + req.URL.Path = strings.TrimSuffix(req.URL.Path, ":callback") + + // How we get the parameters changes based on the method. Right now only + // GET is supported with query args, but this can support POST with JSON + // or URL-encoded args. In those cases, the MIME type would have to be + // checked; for URL-encoded it'd use ParseForm like Get, and for JSON + // you'd use a json.RawMessage for Attributes consisting of the body. Or + // something very similar to that. + var useForm bool + switch req.Method { + case http.MethodGet: + if err := req.ParseForm(); err != nil { + if logCallbackErrors && c != nil { + c.logger.Trace("callback error", "method", req.Method, "url", req.URL.RequestURI(), "error", err) + } + w.WriteHeader(http.StatusBadRequest) + return + } + useForm = true + } + + attrs := &cmdAttrs{ + Command: "callback", + } + + switch { + case useForm: + if len(req.Form) > 0 { + values := make(map[string]interface{}, len(req.Form)) + // This won't handle repeated values. That's fine, at least for now. + // We can address that if needed, which seems unlikely. + for k := range req.Form { + values[k] = req.Form.Get(k) + } + attrs.Attributes = values + } + + attrBytes, err := json.Marshal(attrs) + if err != nil { + if logCallbackErrors && c != nil { + c.logger.Trace("callback error marshaling json", "method", req.Method, "url", req.URL.RequestURI(), "error", err) + } + w.WriteHeader(http.StatusInternalServerError) + return + } + + // If there is any existing body, close it as we're going to replace + // it. It shouldn't be populated in this code path, but you never + // know. + if req.Body != nil { + if err := req.Body.Close(); err != nil { + if logCallbackErrors && c != nil { + c.logger.Trace("callback error closing original request body", "method", req.Method, "url", req.URL.RequestURI(), "error", err) + } + } + } + bytesReader := bytes.NewReader(attrBytes) + req.Body = ioutil.NopCloser(bytesReader) + req.ContentLength = int64(bytesReader.Len()) + req.Header.Set(textproto.CanonicalMIMEHeaderKey("content-type"), "application/json") + req.Method = http.MethodPost + } + + h.ServeHTTP(w, req) + }) +} + /* func WrapForwardedForHandler(h http.Handler, authorizedAddrs []*sockaddr.SockAddrMarshaler, rejectNotPresent, rejectNonAuthz bool, hopSkips int) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { diff --git a/internal/servers/controller/handler_test.go b/internal/servers/controller/handler_test.go index 000401d5b..868db7617 100644 --- a/internal/servers/controller/handler_test.go +++ b/internal/servers/controller/handler_test.go @@ -2,10 +2,13 @@ package controller import ( "bytes" + "context" "encoding/json" "fmt" "io/ioutil" + "net" "net/http" + "net/url" "strings" "testing" @@ -43,9 +46,9 @@ func TestAuthenticationHandler(t *testing.T) { body := make(map[string]interface{}) require.NoError(t, json.Unmarshal(b, &body)) - require.Contains(t, body, "id") - require.Contains(t, body, "token") - pubId, tok := body["id"].(string), body["token"].(string) + require.Contains(t, body, "attributes") + attrs := body["attributes"].(map[string]interface{}) + pubId, tok := attrs["id"].(string), attrs["token"].(string) assert.NotEmpty(t, pubId) assert.NotEmpty(t, tok) assert.Truef(t, strings.HasPrefix(tok, pubId), "Token: %q, Id: %q", tok, pubId) @@ -64,10 +67,12 @@ func TestAuthenticationHandler(t *testing.T) { body = make(map[string]interface{}) require.NoError(t, json.Unmarshal(b, &body)) - require.Contains(t, body, "id") - require.Contains(t, body, "auth_method_id") - require.Contains(t, body, "user_id") - require.NotContains(t, body, "token") + attrs = body["attributes"].(map[string]interface{}) + + require.Contains(t, attrs, "id") + require.Contains(t, attrs, "auth_method_id") + require.Contains(t, attrs, "user_id") + require.NotContains(t, attrs, "token") cookies := make(map[string]*http.Cookie) for _, c := range resp.Cookies() { @@ -81,7 +86,7 @@ func TestAuthenticationHandler(t *testing.T) { assert.False(t, cookies[handlers.JsVisibleCookieName].HttpOnly) tok = cookies[handlers.JsVisibleCookieName].Value - pubId = body["id"].(string) + pubId = attrs["id"].(string) assert.NotEmpty(t, pubId) assert.Truef(t, strings.HasPrefix(tok, pubId), "Token: %q, Id: %q", tok, pubId) } @@ -98,6 +103,7 @@ func TestHandleImplementedPaths(t *testing.T) { "v1/accounts/someid", "v1/auth-methods", "v1/auth-methods/someid", + "v1/auth-methods/someid:authenticate:callback", "v1/auth-tokens", "v1/auth-tokens/someid", "v1/groups", @@ -213,3 +219,116 @@ func TestHandleImplementedPaths(t *testing.T) { } } } + +func TestCallbackInterceptor(t *testing.T) { + listener, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + defer func() { + // Ignore errors as a normal shutdown will also close the listener when + // the server Shutdown is called. This is just in case. + _ = listener.Close() + }() + + noopHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Body == nil { + w.WriteHeader(http.StatusOK) + return + } + var buf bytes.Buffer + read, err := buf.ReadFrom(r.Body) + require.NoError(t, err) + written, err := w.Write(buf.Bytes()) + require.NoError(t, err) + require.EqualValues(t, read, written) + }) + + server := &http.Server{ + Handler: wrapHandlerWithCallbackInterceptor(noopHandler, nil), + } + + go func() { + if err := server.Serve(listener); err != nil { + if err != http.ErrServerClosed { + require.NoError(t, err) + } + } + }() + + testCases := []struct { + name string + path string + args url.Values + wantJson *cmdAttrs + }{ + { + name: "not callback, no args", + path: "v1/auth-methods/ampw_1234567890:read", + }, + { + name: "not callback, with args", + path: "v1/auth-methods/ampw_1234567890:read", + args: url.Values{ + "state": []string{"fooBar"}, + "token": []string{"barFoo"}, + }, + }, + { + name: "callback, no args", + path: "v1/auth-methods/ampw_1234567890:authenticate:callback", + wantJson: &cmdAttrs{Command: "callback"}, + }, + { + name: "callback, invalid pattern", + path: "v1/auth-methods/ampw_1234567890:read:callback", + }, + { + name: "callback, with args", + path: "v1/auth-methods/ampw_1234567890:authenticate:callback", + args: url.Values{ + "state": []string{"fooBar"}, + "token": []string{"barFoo"}, + }, + wantJson: &cmdAttrs{ + Command: "callback", + Attributes: map[string]interface{}{ + "state": "fooBar", + "token": "barFoo", + }, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + require := require.New(t) + + req, err := http.NewRequest(http.MethodGet, + fmt.Sprintf("http://%s/%s", listener.Addr().String(), tc.path), + nil) + require.NoError(err) + + if tc.args != nil { + req.URL.RawQuery = tc.args.Encode() + } + + resp, err := http.DefaultClient.Do(req) + require.NoError(err) + require.EqualValues(http.StatusOK, resp.StatusCode) + + if tc.wantJson != nil { + require.NotNil(resp.Body) + defer resp.Body.Close() + wantJsonJson, err := json.Marshal(tc.wantJson) + require.NoError(err) + var buf bytes.Buffer + _, err = buf.ReadFrom(resp.Body) + require.NoError(err) + require.Equal(wantJsonJson, buf.Bytes()) + } else { + require.Equal(http.NoBody, resp.Body) + } + }) + } + + require.NoError(t, server.Shutdown(context.Background())) +} diff --git a/internal/servers/controller/handlers/accounts/account_service.go b/internal/servers/controller/handlers/accounts/account_service.go index df26f100d..93154b0a2 100644 --- a/internal/servers/controller/handlers/accounts/account_service.go +++ b/internal/servers/controller/handlers/accounts/account_service.go @@ -3,10 +3,14 @@ package accounts import ( "context" "fmt" + "net/url" + "strings" "github.com/hashicorp/boundary/internal/auth" + "github.com/hashicorp/boundary/internal/auth/oidc" + oidcstore "github.com/hashicorp/boundary/internal/auth/oidc/store" "github.com/hashicorp/boundary/internal/auth/password" - "github.com/hashicorp/boundary/internal/auth/password/store" + pwstore "github.com/hashicorp/boundary/internal/auth/password/store" "github.com/hashicorp/boundary/internal/errors" pb "github.com/hashicorp/boundary/internal/gen/controller/api/resources/accounts" pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services" @@ -19,17 +23,46 @@ import ( "google.golang.org/protobuf/types/known/wrapperspb" ) +const ( + // general auth method field names + versionField = "version" + authMethodIdField = "auth_method_id" + typeField = "type" + attributesField = "attributes" + filterField = "filter" + idField = "id" + + // password field names + loginNameKey = "login_name" + newPasswordField = "new_password" + currentPasswordField = "current_password" + + // oidc field names + issuerField = "attributes.issuer" + subjectField = "attributes.subject" + nameClaimField = "attributes.full_name" + emailClaimField = "attributes.email" +) + var ( - maskManager handlers.MaskManager + pwMaskManager handlers.MaskManager + oidcMaskManager handlers.MaskManager // IdActions contains the set of actions that can be performed on // individual resources - IdActions = action.ActionSet{ - action.Read, - action.Update, - action.Delete, - action.SetPassword, - action.ChangePassword, + IdActions = map[auth.SubType]action.ActionSet{ + auth.PasswordSubtype: { + action.Read, + action.Update, + action.Delete, + action.SetPassword, + action.ChangePassword, + }, + auth.OidcSubtype: { + action.Read, + action.Update, + action.Delete, + }, } // CollectionActions contains the set of actions that can be performed on @@ -42,7 +75,10 @@ var ( func init() { var err error - if maskManager, err = handlers.NewMaskManager(&store.Account{}, &pb.Account{}, &pb.PasswordAccountAttributes{}); err != nil { + if pwMaskManager, err = handlers.NewMaskManager(&pwstore.Account{}, &pb.Account{}, &pb.PasswordAccountAttributes{}); err != nil { + panic(err) + } + if oidcMaskManager, err = handlers.NewMaskManager(&oidcstore.Account{}, &pb.Account{}, &pb.OidcAccountAttributes{}); err != nil { panic(err) } } @@ -51,15 +87,19 @@ func init() { type Service struct { pbs.UnimplementedAccountServiceServer - repoFn common.PasswordAuthRepoFactory + pwRepoFn common.PasswordAuthRepoFactory + oidcRepoFn common.OidcAuthRepoFactory } // NewService returns a user service which handles user related requests to boundary. -func NewService(repo common.PasswordAuthRepoFactory) (Service, error) { - if repo == nil { +func NewService(pwRepo common.PasswordAuthRepoFactory, oidcRepo common.OidcAuthRepoFactory) (Service, error) { + if pwRepo == nil { return Service{}, fmt.Errorf("nil password repository provided") } - return Service{repoFn: repo}, nil + if oidcRepo == nil { + return Service{}, fmt.Errorf("nil oidc repository provided") + } + return Service{pwRepoFn: pwRepo, oidcRepoFn: oidcRepo}, nil } var _ pbs.AccountServiceServer = Service{} @@ -89,7 +129,7 @@ func (s Service) ListAccounts(ctx context.Context, req *pbs.ListAccountsRequest) } for _, item := range ul { item.Scope = authResults.Scope - item.AuthorizedActions = authResults.FetchActionSetForId(ctx, item.Id, IdActions, auth.WithResource(res)).Strings() + item.AuthorizedActions = authResults.FetchActionSetForId(ctx, item.Id, IdActions[auth.SubtypeFromId(item.Id)], auth.WithResource(res)).Strings() if len(item.AuthorizedActions) == 0 { continue } @@ -114,7 +154,7 @@ func (s Service) GetAccount(ctx context.Context, req *pbs.GetAccountRequest) (*p return nil, err } u.Scope = authResults.Scope - u.AuthorizedActions = authResults.FetchActionSetForId(ctx, u.Id, IdActions).Strings() + u.AuthorizedActions = authResults.FetchActionSetForId(ctx, u.Id, IdActions[auth.SubtypeFromId(u.Id)]).Strings() return &pbs.GetAccountResponse{Item: u}, nil } @@ -127,12 +167,12 @@ func (s Service) CreateAccount(ctx context.Context, req *pbs.CreateAccountReques if authResults.Error != nil { return nil, authResults.Error } - u, err := s.createInRepo(ctx, authMeth.GetPublicId(), authResults.Scope.GetId(), req.GetItem()) + u, err := s.createInRepo(ctx, authMeth, req.GetItem()) if err != nil { return nil, err } u.Scope = authResults.Scope - u.AuthorizedActions = authResults.FetchActionSetForId(ctx, u.Id, IdActions).Strings() + u.AuthorizedActions = authResults.FetchActionSetForId(ctx, u.Id, IdActions[auth.SubtypeFromId(req.GetItem().GetAuthMethodId())]).Strings() return &pbs.CreateAccountResponse{Item: u, Uri: fmt.Sprintf("accounts/%s", u.GetId())}, nil } @@ -145,12 +185,12 @@ func (s Service) UpdateAccount(ctx context.Context, req *pbs.UpdateAccountReques if authResults.Error != nil { return nil, authResults.Error } - u, err := s.updateInRepo(ctx, authResults.Scope.GetId(), authMeth.GetPublicId(), req.GetId(), req.GetUpdateMask().GetPaths(), req.GetItem()) + u, err := s.updateInRepo(ctx, authResults.Scope.GetId(), authMeth.GetPublicId(), req) if err != nil { return nil, err } u.Scope = authResults.Scope - u.AuthorizedActions = authResults.FetchActionSetForId(ctx, u.Id, IdActions).Strings() + u.AuthorizedActions = authResults.FetchActionSetForId(ctx, u.Id, IdActions[auth.SubtypeFromId(u.Id)]).Strings() return &pbs.UpdateAccountResponse{Item: u}, nil } @@ -184,7 +224,7 @@ func (s Service) ChangePassword(ctx context.Context, req *pbs.ChangePasswordRequ return nil, err } u.Scope = authResults.Scope - u.AuthorizedActions = authResults.FetchActionSetForId(ctx, u.Id, IdActions).Strings() + u.AuthorizedActions = authResults.FetchActionSetForId(ctx, u.Id, IdActions[auth.SubtypeFromId(u.Id)]).Strings() return &pbs.ChangePasswordResponse{Item: u}, nil } @@ -202,29 +242,50 @@ func (s Service) SetPassword(ctx context.Context, req *pbs.SetPasswordRequest) ( return nil, err } u.Scope = authResults.Scope - u.AuthorizedActions = authResults.FetchActionSetForId(ctx, u.Id, IdActions).Strings() + u.AuthorizedActions = authResults.FetchActionSetForId(ctx, u.Id, IdActions[auth.SubtypeFromId(u.Id)]).Strings() return &pbs.SetPasswordResponse{Item: u}, nil } func (s Service) getFromRepo(ctx context.Context, id string) (*pb.Account, error) { - repo, err := s.repoFn() - if err != nil { - return nil, err - } - u, err := repo.LookupAccount(ctx, id) - if err != nil { - if errors.IsNotFoundError(err) { - return nil, handlers.NotFoundErrorf("Account %q doesn't exist.", id) + var acct auth.Account + switch auth.SubtypeFromId(id) { + case auth.PasswordSubtype: + repo, err := s.pwRepoFn() + if err != nil { + return nil, err } - return nil, err + a, err := repo.LookupAccount(ctx, id) + if err != nil { + if errors.IsNotFoundError(err) { + return nil, handlers.NotFoundErrorf("Account %q doesn't exist.", id) + } + return nil, err + } + acct = a + case auth.OidcSubtype: + repo, err := s.oidcRepoFn() + if err != nil { + return nil, err + } + a, err := repo.LookupAccount(ctx, id) + if err != nil { + if errors.IsNotFoundError(err) { + return nil, handlers.NotFoundErrorf("Account %q doesn't exist.", id) + } + return nil, err + } + acct = a + default: + return nil, handlers.NotFoundErrorf("Unrecognized id.") } - if u == nil { - return nil, handlers.NotFoundErrorf("Account %q doesn't exist.", id) - } - return toProto(u) + return toProto(acct) } -func (s Service) createInRepo(ctx context.Context, authMethodId, scopeId string, item *pb.Account) (*pb.Account, error) { +func (s Service) createPwInRepo(ctx context.Context, am auth.AuthMethod, item *pb.Account) (*password.Account, error) { + const op = "account_service.(Serivce).createPwInRepo" + if item == nil { + return nil, errors.New(errors.InvalidParameter, op, "nil item") + } pwAttrs := &pb.PasswordAccountAttributes{} if err := handlers.StructToProto(item.GetAttributes(), pwAttrs); err != nil { return nil, handlers.InvalidArgumentErrorf("Error in provided request.", @@ -237,11 +298,11 @@ func (s Service) createInRepo(ctx context.Context, authMethodId, scopeId string, if item.GetDescription() != nil { opts = append(opts, password.WithDescription(item.GetDescription().GetValue())) } - a, err := password.NewAccount(authMethodId, opts...) + a, err := password.NewAccount(am.GetPublicId(), opts...) if err != nil { return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to build user for creation: %v.", err) } - repo, err := s.repoFn() + repo, err := s.pwRepoFn() if err != nil { return nil, err } @@ -250,44 +311,102 @@ func (s Service) createInRepo(ctx context.Context, authMethodId, scopeId string, if pwAttrs.GetPassword() != nil { createOpts = append(createOpts, password.WithPassword(pwAttrs.GetPassword().GetValue())) } - out, err := repo.CreateAccount(ctx, scopeId, a, createOpts...) + out, err := repo.CreateAccount(ctx, am.GetScopeId(), a, createOpts...) if err != nil { return nil, fmt.Errorf("unable to create user: %w", err) } if out == nil { return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to create user but no error returned from repository.") } + return out, nil +} + +func (s Service) createOidcInRepo(ctx context.Context, am auth.AuthMethod, item *pb.Account) (*oidc.Account, error) { + const op = "account_service.(Service).createOidcInRepo" + if item == nil { + return nil, errors.New(errors.InvalidParameter, op, "nil item") + } + var opts []oidc.Option + if item.GetName() != nil { + opts = append(opts, oidc.WithName(item.GetName().GetValue())) + } + if item.GetDescription() != nil { + opts = append(opts, oidc.WithDescription(item.GetDescription().GetValue())) + } + attrs := &pb.OidcAccountAttributes{} + if err := handlers.StructToProto(item.GetAttributes(), attrs); err != nil { + return nil, handlers.InvalidArgumentErrorf("Error in provided request.", + map[string]string{"attributes": "Attribute fields do not match the expected format."}) + } + if attrs.GetIssuer() != "" { + u, err := url.Parse(attrs.GetIssuer()) + if err != nil { + return nil, errors.Wrap(err, op, errors.WithMsg("unable to parse issuer"), errors.WithCode(errors.InvalidParameter)) + } + opts = append(opts, oidc.WithIssuer(u)) + } + a, err := oidc.NewAccount(am.GetPublicId(), attrs.GetSubject(), opts...) + if err != nil { + return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to build user for creation: %v.", err) + } + repo, err := s.oidcRepoFn() + if err != nil { + return nil, err + } + + out, err := repo.CreateAccount(ctx, am.GetScopeId(), a) + if err != nil { + return nil, errors.Wrap(err, op, errors.WithMsg("unable to create user")) + } + if out == nil { + return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to create user but no error returned from repository.") + } + return out, nil +} + +func (s Service) createInRepo(ctx context.Context, am auth.AuthMethod, item *pb.Account) (*pb.Account, error) { + const op = "account_service.(Service).createInRepo" + if item == nil { + return nil, errors.New(errors.InvalidParameter, op, "nil item") + } + var out auth.Account + switch auth.SubtypeFromId(am.GetPublicId()) { + case auth.PasswordSubtype: + am, err := s.createPwInRepo(ctx, am, item) + if err != nil { + return nil, err + } + if am == nil { + return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to create auth method but no error returned from repository.") + } + out = am + case auth.OidcSubtype: + am, err := s.createOidcInRepo(ctx, am, item) + if err != nil { + return nil, err + } + if am == nil { + return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to create auth method but no error returned from repository.") + } + out = am + } return toProto(out) } -func (s Service) updateInRepo(ctx context.Context, scopeId, authMethId, id string, mask []string, item *pb.Account) (*pb.Account, error) { - var opts []password.Option - if desc := item.GetDescription(); desc != nil { - opts = append(opts, password.WithDescription(desc.GetValue())) - } - if name := item.GetName(); name != nil { - opts = append(opts, password.WithName(name.GetValue())) - } - u, err := password.NewAccount(authMethId, opts...) +func (s Service) updatePwInRepo(ctx context.Context, scopeId, authMethId, id string, mask []string, item *pb.Account) (*password.Account, error) { + u, err := toStoragePwAccount(authMethId, item) if err != nil { - return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to build auth method for update: %v.", err) + return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to build account for update: %v.", err) } u.PublicId = id - pwAttrs := &pb.PasswordAccountAttributes{} - if err := handlers.StructToProto(item.GetAttributes(), pwAttrs); err != nil { - return nil, handlers.ApiErrorWithCodeAndMessage(codes.InvalidArgument, "Provided attributes don't match expected format.") - } - if pwAttrs.GetLoginName() != "" { - u.LoginName = pwAttrs.GetLoginName() - } version := item.GetVersion() - dbMask := maskManager.Translate(mask) + dbMask := pwMaskManager.Translate(mask) if len(dbMask) == 0 { return nil, handlers.InvalidArgumentErrorf("No valid fields included in the update mask.", map[string]string{"update_mask": "No valid fields provided in the update mask."}) } - repo, err := s.repoFn() + repo, err := s.pwRepoFn() if err != nil { return nil, err } @@ -303,15 +422,85 @@ func (s Service) updateInRepo(ctx context.Context, scopeId, authMethId, id strin if rowsUpdated == 0 { return nil, handlers.NotFoundErrorf("Account %q doesn't exist or incorrect version provided.", id) } + return out, nil +} + +func (s Service) updateOidcInRepo(ctx context.Context, scopeId, amId, id string, mask []string, item *pb.Account) (*oidc.Account, error) { + const op = "account_service.(Service).updateOidcInRepo" + if item == nil { + return nil, errors.New(errors.InvalidParameter, op, "nil account.") + } + u := oidc.AllocAccount() + u.PublicId = id + if item.GetName() != nil { + u.Name = item.GetName().GetValue() + } + if item.GetDescription() != nil { + u.Description = item.GetDescription().GetValue() + } + + version := item.GetVersion() + + dbMask := oidcMaskManager.Translate(mask) + if len(dbMask) == 0 { + return nil, handlers.InvalidArgumentErrorf("No valid fields included in the update mask.", map[string]string{"update_mask": "No valid fields provided in the update mask."}) + } + repo, err := s.oidcRepoFn() + if err != nil { + return nil, err + } + out, rowsUpdated, err := repo.UpdateAccount(ctx, scopeId, u, version, dbMask) + if err != nil { + return nil, fmt.Errorf("unable to update account: %w", err) + } + if rowsUpdated == 0 { + return nil, handlers.NotFoundErrorf("Account %q doesn't exist or incorrect version provided.", id) + } + return out, nil +} + +func (s Service) updateInRepo(ctx context.Context, scopeId, authMethodId string, req *pbs.UpdateAccountRequest) (*pb.Account, error) { + var out auth.Account + switch auth.SubtypeFromId(req.GetId()) { + case auth.PasswordSubtype: + a, err := s.updatePwInRepo(ctx, scopeId, authMethodId, req.GetId(), req.GetUpdateMask().GetPaths(), req.GetItem()) + if err != nil { + return nil, err + } + if a == nil { + return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to update account but no error returned from repository.") + } + out = a + case auth.OidcSubtype: + a, err := s.updateOidcInRepo(ctx, scopeId, authMethodId, req.GetId(), req.GetUpdateMask().GetPaths(), req.GetItem()) + if err != nil { + return nil, err + } + if a == nil { + return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to update account but no error returned from repository.") + } + out = a + } return toProto(out) } func (s Service) deleteFromRepo(ctx context.Context, scopeId, id string) (bool, error) { - repo, err := s.repoFn() - if err != nil { - return false, err + var rows int + var err error + switch auth.SubtypeFromId(id) { + case auth.PasswordSubtype: + repo, iErr := s.pwRepoFn() + if iErr != nil { + return false, iErr + } + rows, err = repo.DeleteAccount(ctx, scopeId, id) + case auth.OidcSubtype: + repo, iErr := s.oidcRepoFn() + if iErr != nil { + return false, iErr + } + rows, err = repo.DeleteAccount(ctx, scopeId, id) } - rows, err := repo.DeleteAccount(ctx, scopeId, id) if err != nil { if errors.IsNotFoundError(err) { return false, nil @@ -322,27 +511,46 @@ func (s Service) deleteFromRepo(ctx context.Context, scopeId, id string) (bool, } func (s Service) listFromRepo(ctx context.Context, authMethodId string) ([]*pb.Account, error) { - repo, err := s.repoFn() - if err != nil { - return nil, err - } - ul, err := repo.ListAccounts(ctx, authMethodId) - if err != nil { - return nil, err - } var outUl []*pb.Account - for _, u := range ul { - ou, err := toProto(u) + switch auth.SubtypeFromId(authMethodId) { + case auth.PasswordSubtype: + pwRepo, err := s.pwRepoFn() if err != nil { return nil, err } - outUl = append(outUl, ou) + pwl, err := pwRepo.ListAccounts(ctx, authMethodId) + if err != nil { + return nil, err + } + for _, u := range pwl { + ou, err := toProto(u) + if err != nil { + return nil, err + } + outUl = append(outUl, ou) + } + case auth.OidcSubtype: + oidcRepo, err := s.oidcRepoFn() + if err != nil { + return nil, err + } + oidcl, err := oidcRepo.ListAccounts(ctx, authMethodId) + if err != nil { + return nil, err + } + for _, u := range oidcl { + ou, err := toProto(u) + if err != nil { + return nil, err + } + outUl = append(outUl, ou) + } } return outUl, nil } func (s Service) changePasswordInRepo(ctx context.Context, scopeId, id string, version uint32, currentPassword, newPassword string) (*pb.Account, error) { - repo, err := s.repoFn() + repo, err := s.pwRepoFn() if err != nil { return nil, err } @@ -367,7 +575,7 @@ func (s Service) changePasswordInRepo(ctx context.Context, scopeId, id string, v } func (s Service) setPasswordInRepo(ctx context.Context, scopeId, id string, version uint32, pw string) (*pb.Account, error) { - repo, err := s.repoFn() + repo, err := s.pwRepoFn() if err != nil { return nil, err } @@ -385,55 +593,88 @@ func (s Service) setPasswordInRepo(ctx context.Context, scopeId, id string, vers return toProto(out) } -func (s Service) parentAndAuthResult(ctx context.Context, id string, a action.Type) (*password.AuthMethod, auth.VerifyResults) { +func (s Service) parentAndAuthResult(ctx context.Context, id string, a action.Type) (auth.AuthMethod, auth.VerifyResults) { res := auth.VerifyResults{} - repo, err := s.repoFn() + pwRepo, err := s.pwRepoFn() + if err != nil { + res.Error = err + return nil, res + } + oidcRepo, err := s.oidcRepoFn() if err != nil { res.Error = err return nil, res } var parentId string - var authMeth *password.AuthMethod opts := []auth.Option{auth.WithType(resource.Account), auth.WithAction(a)} switch a { case action.List, action.Create: parentId = id default: - acct, err := repo.LookupAccount(ctx, id) + switch auth.SubtypeFromId(id) { + case auth.PasswordSubtype: + acct, err := pwRepo.LookupAccount(ctx, id) + if err != nil { + res.Error = err + return nil, res + } + if acct == nil { + res.Error = handlers.NotFoundError() + return nil, res + } + parentId = acct.GetAuthMethodId() + case auth.OidcSubtype: + acct, err := oidcRepo.LookupAccount(ctx, id) + if err != nil { + res.Error = err + return nil, res + } + if acct == nil { + res.Error = handlers.NotFoundError() + return nil, res + } + parentId = acct.GetAuthMethodId() + } + opts = append(opts, auth.WithId(id)) + } + + var authMeth auth.AuthMethod + switch auth.SubtypeFromId(parentId) { + case auth.PasswordSubtype: + am, err := pwRepo.LookupAuthMethod(ctx, parentId) if err != nil { res.Error = err return nil, res } - if acct == nil { + if am == nil { res.Error = handlers.NotFoundError() return nil, res } - parentId = acct.GetAuthMethodId() - opts = append(opts, auth.WithId(id)) - } - - authMeth, err = repo.LookupAuthMethod(ctx, parentId) - if err != nil { - res.Error = err - return nil, res - } - if authMeth == nil { - res.Error = handlers.NotFoundError() - return nil, res + authMeth = am + case auth.OidcSubtype: + am, err := oidcRepo.LookupAuthMethod(ctx, parentId) + if err != nil { + res.Error = err + return nil, res + } + if am == nil { + res.Error = handlers.NotFoundError() + return nil, res + } + authMeth = am } opts = append(opts, auth.WithScopeId(authMeth.GetScopeId()), auth.WithPin(parentId)) return authMeth, auth.Verify(ctx, opts...) } -func toProto(in *password.Account) (*pb.Account, error) { +func toProto(in auth.Account) (*pb.Account, error) { out := pb.Account{ Id: in.GetPublicId(), CreatedTime: in.GetCreateTime().GetTimestamp(), UpdatedTime: in.GetUpdateTime().GetTimestamp(), AuthMethodId: in.GetAuthMethodId(), Version: in.GetVersion(), - Type: auth.PasswordSubtype.String(), } if in.GetDescription() != "" { out.Description = &wrapperspb.StringValue{Value: in.GetDescription()} @@ -441,76 +682,188 @@ func toProto(in *password.Account) (*pb.Account, error) { if in.GetName() != "" { out.Name = &wrapperspb.StringValue{Value: in.GetName()} } - if st, err := handlers.ProtoToStruct(&pb.PasswordAccountAttributes{LoginName: in.GetLoginName()}); err == nil { + switch i := in.(type) { + case *password.Account: + out.Type = auth.PasswordSubtype.String() + st, err := handlers.ProtoToStruct(&pb.PasswordAccountAttributes{LoginName: i.GetLoginName()}) + if err != nil { + return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "failed building password attribute struct: %v", err) + } + out.Attributes = st + case *oidc.Account: + out.Type = auth.OidcSubtype.String() + attrs := &pb.OidcAccountAttributes{ + Issuer: i.GetIssuer(), + Subject: i.GetSubject(), + FullName: i.GetFullName(), + Email: i.GetEmail(), + } + st, err := handlers.ProtoToStruct(attrs) + if err != nil { + return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "failed building oidc attribute struct: %v", err) + } out.Attributes = st - } else { - return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "failed building password attribute struct: %v", err) } return &out, nil } +func toStoragePwAccount(amId string, item *pb.Account) (*password.Account, error) { + const op = "account_service.toStoragePwAccount" + if item == nil { + return nil, errors.New(errors.InvalidParameter, op, "nil account.") + } + var opts []password.Option + if item.GetName() != nil { + opts = append(opts, password.WithName(item.GetName().GetValue())) + } + if item.GetDescription() != nil { + opts = append(opts, password.WithDescription(item.GetDescription().GetValue())) + } + u, err := password.NewAccount(amId, opts...) + if err != nil { + return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to build account for creation: %v.", err) + } + + attrs := &pb.PasswordAccountAttributes{} + if err := handlers.StructToProto(item.GetAttributes(), attrs); err != nil { + return nil, handlers.InvalidArgumentErrorf("Error in provided request.", + map[string]string{attributesField: "Attribute fields do not match the expected format."}) + } + + if attrs.GetLoginName() != "" { + u.LoginName = attrs.GetLoginName() + } + return u, nil +} + // A validateX method should exist for each method above. These methods do not make calls to any backing service but enforce // requirements on the structure of the request. They verify that: // * The path passed in is correctly formatted // * All required parameters are set // * There are no conflicting parameters provided func validateGetRequest(req *pbs.GetAccountRequest) error { - return handlers.ValidateGetRequest(password.AccountPrefix, req, handlers.NoopValidatorFn) + const op = "account_service.validateGetRequest" + if req == nil { + return errors.New(errors.InvalidParameter, op, "nil request") + } + return handlers.ValidateGetRequest(handlers.NoopValidatorFn, req, password.AccountPrefix, oidc.AccountPrefix) } func validateCreateRequest(req *pbs.CreateAccountRequest) error { + const op = "account_service.validateCreateRequest" + if req == nil { + return errors.New(errors.InvalidParameter, op, "nil request") + } return handlers.ValidateCreateRequest(req.GetItem(), func() map[string]string { badFields := map[string]string{} if req.GetItem().GetAuthMethodId() == "" { - badFields["auth_method_id"] = "This field is required." + badFields[authMethodIdField] = "This field is required." } switch auth.SubtypeFromId(req.GetItem().GetAuthMethodId()) { case auth.PasswordSubtype: if req.GetItem().GetType() != "" && req.GetItem().GetType() != auth.PasswordSubtype.String() { - badFields["type"] = "Doesn't match the parent resource's type." + badFields[typeField] = "Doesn't match the parent resource's type." } - pwAttrs := &pb.PasswordAccountAttributes{} - if err := handlers.StructToProto(req.GetItem().GetAttributes(), pwAttrs); err != nil { - badFields["attributes"] = "Attribute fields do not match the expected format." + attrs := &pb.PasswordAccountAttributes{} + if err := handlers.StructToProto(req.GetItem().GetAttributes(), attrs); err != nil { + badFields[attributesField] = "Attribute fields do not match the expected format." } - if pwAttrs.GetLoginName() == "" { - badFields["login_name"] = "This is a required field for this type." + if attrs.GetLoginName() == "" { + badFields[loginNameKey] = "This is a required field for this type." + } + case auth.OidcSubtype: + if req.GetItem().GetType() != "" && req.GetItem().GetType() != auth.OidcSubtype.String() { + badFields[typeField] = "Doesn't match the parent resource's type." + } + attrs := &pb.OidcAccountAttributes{} + if err := handlers.StructToProto(req.GetItem().GetAttributes(), attrs); err != nil { + badFields[attributesField] = "Attribute fields do not match the expected format." + } + if attrs.GetSubject() == "" { + badFields[subjectField] = "This is a required field for this type." + } + if attrs.GetIssuer() != "" { + du, err := url.Parse(attrs.GetIssuer()) + if err != nil { + badFields[issuerField] = fmt.Sprintf("Cannot be parsed as a url. %v", err) + } + if trimmed := strings.TrimSuffix(strings.TrimSuffix(du.RawPath, "/"), "/.well-known/openid-configuration"); trimmed != "" { + badFields[issuerField] = "The path segment of the url should be empty." + } + } + if attrs.GetFullName() != "" { + badFields[nameClaimField] = "This is a read only field." + } + if attrs.GetEmail() != "" { + badFields[emailClaimField] = "This is a read only field." } default: - badFields["auth_method_id"] = "Unknown auth method type from ID." + badFields[authMethodIdField] = "Unknown auth method type from ID." } return badFields }) } func validateUpdateRequest(req *pbs.UpdateAccountRequest) error { - return handlers.ValidateUpdateRequest(password.AccountPrefix, req, req.GetItem(), func() map[string]string { + const op = "account_service.validateUpdateRequest" + if req == nil { + return errors.New(errors.InvalidParameter, op, "nil request") + } + return handlers.ValidateUpdateRequest(req, req.GetItem(), func() map[string]string { badFields := map[string]string{} switch auth.SubtypeFromId(req.GetId()) { case auth.PasswordSubtype: if req.GetItem().GetType() != "" && req.GetItem().GetType() != auth.PasswordSubtype.String() { - badFields["type"] = "Cannot modify the resource type." + badFields[typeField] = "Cannot modify the resource type." } - pwAttrs := &pb.PasswordAccountAttributes{} - if err := handlers.StructToProto(req.GetItem().GetAttributes(), pwAttrs); err != nil { - badFields["attributes"] = "Attribute fields do not match the expected format." + attrs := &pb.PasswordAccountAttributes{} + if err := handlers.StructToProto(req.GetItem().GetAttributes(), attrs); err != nil { + badFields[attributesField] = "Attribute fields do not match the expected format." + } + case auth.OidcSubtype: + if req.GetItem().GetType() != "" && req.GetItem().GetType() != auth.OidcSubtype.String() { + badFields[typeField] = "Cannot modify the resource type." + } + attrs := &pb.OidcAccountAttributes{} + if err := handlers.StructToProto(req.GetItem().GetAttributes(), attrs); err != nil { + badFields[attributesField] = "Attribute fields do not match the expected format." + } + if handlers.MaskContains(req.GetUpdateMask().GetPaths(), subjectField) { + badFields[subjectField] = "Field cannot be updated." + } + if handlers.MaskContains(req.GetUpdateMask().GetPaths(), issuerField) { + badFields[issuerField] = "Field cannot be updated." + } + if handlers.MaskContains(req.GetUpdateMask().GetPaths(), emailClaimField) { + badFields[emailClaimField] = "Field is read only." + } + if handlers.MaskContains(req.GetUpdateMask().GetPaths(), nameClaimField) { + badFields[nameClaimField] = "Field is read only." } } return badFields - }) + }, password.AccountPrefix, oidc.AccountPrefix) } func validateDeleteRequest(req *pbs.DeleteAccountRequest) error { - return handlers.ValidateDeleteRequest(password.AccountPrefix, req, handlers.NoopValidatorFn) + const op = "account_service.validateDeleteRequest" + if req == nil { + return errors.New(errors.InvalidParameter, op, "nil request") + } + return handlers.ValidateDeleteRequest(handlers.NoopValidatorFn, req, password.AccountPrefix, oidc.AccountPrefix) } func validateListRequest(req *pbs.ListAccountsRequest) error { + const op = "account_service.validateListRequest" + if req == nil { + return errors.New(errors.InvalidParameter, op, "nil request") + } badFields := map[string]string{} - if !handlers.ValidId(password.AuthMethodPrefix, req.GetAuthMethodId()) { - badFields["auth_method_id"] = "Invalid formatted identifier." + if !handlers.ValidId(handlers.Id(req.GetAuthMethodId()), password.AuthMethodPrefix, oidc.AuthMethodPrefix) { + badFields[authMethodIdField] = "Invalid formatted identifier." } if _, err := handlers.NewFilter(req.GetFilter()); err != nil { - badFields["filter"] = fmt.Sprintf("This field could not be parsed. %v", err) + badFields[filterField] = fmt.Sprintf("This field could not be parsed. %v", err) } if len(badFields) > 0 { return handlers.InvalidArgumentErrorf("Error in provided request.", badFields) @@ -519,18 +872,22 @@ func validateListRequest(req *pbs.ListAccountsRequest) error { } func validateChangePasswordRequest(req *pbs.ChangePasswordRequest) error { + const op = "account_service.validateChangePasswordRequest" + if req == nil { + return errors.New(errors.InvalidParameter, op, "nil request") + } badFields := map[string]string{} - if !handlers.ValidId(password.AccountPrefix, req.GetId()) { - badFields["id"] = "Improperly formatted identifier." + if !handlers.ValidId(handlers.Id(req.GetId()), password.AccountPrefix) { + badFields[idField] = "Improperly formatted identifier." } if req.GetVersion() == 0 { - badFields["version"] = "Existing resource version is required for an update." + badFields[versionField] = "Existing resource version is required for an update." } if req.GetNewPassword() == "" { - badFields["new_password"] = "This is a required field." + badFields[newPasswordField] = "This is a required field." } if req.GetCurrentPassword() == "" { - badFields["current_password"] = "This is a required field." + badFields[currentPasswordField] = "This is a required field." } if len(badFields) > 0 { return handlers.InvalidArgumentErrorf("Error in provided request.", badFields) @@ -539,12 +896,16 @@ func validateChangePasswordRequest(req *pbs.ChangePasswordRequest) error { } func validateSetPasswordRequest(req *pbs.SetPasswordRequest) error { + const op = "account_service.validateSetPasswordRequest" + if req == nil { + return errors.New(errors.InvalidParameter, op, "nil request") + } badFields := map[string]string{} - if !handlers.ValidId(password.AccountPrefix, req.GetId()) { - badFields["id"] = "Improperly formatted identifier." + if !handlers.ValidId(handlers.Id(req.GetId()), password.AccountPrefix) { + badFields[idField] = "Improperly formatted identifier." } if req.GetVersion() == 0 { - badFields["version"] = "Existing resource version is required for an update." + badFields[versionField] = "Existing resource version is required for an update." } if len(badFields) > 0 { return handlers.InvalidArgumentErrorf("Error in provided request.", badFields) diff --git a/internal/servers/controller/handlers/accounts/account_service_test.go b/internal/servers/controller/handlers/accounts/account_service_test.go index 4cc5f0abb..4c048957c 100644 --- a/internal/servers/controller/handlers/accounts/account_service_test.go +++ b/internal/servers/controller/handlers/accounts/account_service_test.go @@ -1,14 +1,16 @@ package accounts_test import ( + "context" "errors" "fmt" + "sort" "strings" "testing" - "github.com/golang/protobuf/ptypes" "github.com/google/go-cmp/cmp" "github.com/hashicorp/boundary/internal/auth" + "github.com/hashicorp/boundary/internal/auth/oidc" "github.com/hashicorp/boundary/internal/auth/password" "github.com/hashicorp/boundary/internal/authtoken" "github.com/hashicorp/boundary/internal/db" @@ -17,8 +19,10 @@ import ( pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services" "github.com/hashicorp/boundary/internal/iam" "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/servers/controller/common" "github.com/hashicorp/boundary/internal/servers/controller/handlers" "github.com/hashicorp/boundary/internal/servers/controller/handlers/accounts" + "github.com/hashicorp/boundary/internal/types/action" "github.com/hashicorp/boundary/internal/types/scope" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -26,38 +30,134 @@ import ( "google.golang.org/grpc/codes" "google.golang.org/protobuf/testing/protocmp" "google.golang.org/protobuf/types/known/structpb" + "google.golang.org/protobuf/types/known/timestamppb" "google.golang.org/protobuf/types/known/wrapperspb" ) -func TestGet(t *testing.T) { +var ( + pwAuthorizedActions = []string{ + action.Read.String(), + action.Update.String(), + action.Delete.String(), + action.SetPassword.String(), + action.ChangePassword.String(), + } + oidcAuthorizedActions = []string{ + action.Read.String(), + action.Update.String(), + action.Delete.String(), + } +) + +func TestNewService(t *testing.T) { conn, _ := db.TestSetup(t, "postgres") rw := db.New(conn) wrap := db.TestWrapper(t) - kms := kms.TestKms(t, conn, wrap) - repoFn := func() (*password.Repository, error) { - return password.NewRepository(rw, rw, kms) + kmsCache := kms.TestKms(t, conn, wrap) + pwRepoFn := func() (*password.Repository, error) { + return password.NewRepository(rw, rw, kmsCache) } - iamRepoFn := func() (*iam.Repository, error) { - return iam.NewRepository(rw, rw, kms) + oidcRepoFn := func() (*oidc.Repository, error) { + return oidc.NewRepository(rw, rw, kmsCache) } - s, err := accounts.NewService(repoFn) + cases := []struct { + name string + pwRepo common.PasswordAuthRepoFactory + oidcRepo common.OidcAuthRepoFactory + wantErr bool + }{ + { + name: "nil-all", + wantErr: true, + }, + { + name: "nil-pw-repo", + oidcRepo: oidcRepoFn, + wantErr: true, + }, + { + name: "nil-oidc-repo", + pwRepo: pwRepoFn, + wantErr: true, + }, + { + name: "success", + pwRepo: pwRepoFn, + oidcRepo: oidcRepoFn, + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + _, err := accounts.NewService(tc.pwRepo, tc.oidcRepo) + if tc.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} + +func TestGet(t *testing.T) { + ctx := context.Background() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrap := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, wrap) + pwRepoFn := func() (*password.Repository, error) { + return password.NewRepository(rw, rw, kmsCache) + } + oidcRepoFn := func() (*oidc.Repository, error) { + return oidc.NewRepository(rw, rw, kmsCache) + } + iamRepoFn := func() (*iam.Repository, error) { + return iam.NewRepository(rw, rw, kmsCache) + } + + s, err := accounts.NewService(pwRepoFn, oidcRepoFn) require.NoError(t, err, "Couldn't create new auth token service.") org, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrap)) - am := password.TestAuthMethods(t, conn, org.GetPublicId(), 1)[0] - aa := password.TestAccounts(t, conn, am.GetPublicId(), 1)[0] - wireAccount := pb.Account{ - Id: aa.GetPublicId(), - AuthMethodId: aa.GetAuthMethodId(), - CreatedTime: aa.GetCreateTime().GetTimestamp(), - UpdatedTime: aa.GetUpdateTime().GetTimestamp(), + am := password.TestAuthMethods(t, conn, org.GetPublicId(), 1)[0] + pwA := password.TestAccounts(t, conn, am.GetPublicId(), 1)[0] + + pwWireAccount := pb.Account{ + Id: pwA.GetPublicId(), + AuthMethodId: pwA.GetAuthMethodId(), + CreatedTime: pwA.GetCreateTime().GetTimestamp(), + UpdatedTime: pwA.GetUpdateTime().GetTimestamp(), Scope: &scopepb.ScopeInfo{Id: org.GetPublicId(), Type: scope.Org.String(), ParentScopeId: scope.Global.String()}, Version: 1, Type: "password", - Attributes: &structpb.Struct{Fields: map[string]*structpb.Value{"login_name": structpb.NewStringValue(aa.GetLoginName())}}, - AuthorizedActions: []string{"read", "update", "delete", "set-password", "change-password"}, + Attributes: &structpb.Struct{Fields: map[string]*structpb.Value{"login_name": structpb.NewStringValue(pwA.GetLoginName())}}, + AuthorizedActions: pwAuthorizedActions, + } + + databaseWrapper, err := kmsCache.GetWrapper(ctx, org.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + oidcAm := oidc.TestAuthMethod( + t, conn, databaseWrapper, org.PublicId, oidc.ActivePrivateState, + "alice-rp", "fido", + oidc.WithIssuer(oidc.TestConvertToUrls(t, "https://www.alice.com")[0]), + oidc.WithSigningAlgs(oidc.RS256), + oidc.WithApiUrl(oidc.TestConvertToUrls(t, "https://www.alice.com/callback")[0]), + ) + oidcA := oidc.TestAccount(t, conn, oidcAm, "test-subject") + oidcWireAccount := pb.Account{ + Id: oidcA.GetPublicId(), + AuthMethodId: oidcA.GetAuthMethodId(), + CreatedTime: oidcA.GetCreateTime().GetTimestamp(), + UpdatedTime: oidcA.GetUpdateTime().GetTimestamp(), + Scope: &scopepb.ScopeInfo{Id: org.GetPublicId(), Type: scope.Org.String(), ParentScopeId: scope.Global.String()}, + Version: 1, + Type: auth.OidcSubtype.String(), + Attributes: &structpb.Struct{Fields: map[string]*structpb.Value{ + "issuer": structpb.NewStringValue(oidcAm.GetIssuer()), + "subject": structpb.NewStringValue("test-subject"), + }}, + AuthorizedActions: oidcAuthorizedActions, } cases := []struct { @@ -67,16 +167,27 @@ func TestGet(t *testing.T) { err error }{ { - name: "Get an existing account", - req: &pbs.GetAccountRequest{Id: wireAccount.GetId()}, - res: &pbs.GetAccountResponse{Item: &wireAccount}, + name: "Get a password account", + req: &pbs.GetAccountRequest{Id: pwWireAccount.GetId()}, + res: &pbs.GetAccountResponse{Item: &pwWireAccount}, }, { - name: "Get a non existing account", + name: "Get an oidc account", + req: &pbs.GetAccountRequest{Id: oidcWireAccount.GetId()}, + res: &pbs.GetAccountResponse{Item: &oidcWireAccount}, + }, + { + name: "Get a non existing password account", req: &pbs.GetAccountRequest{Id: password.AccountPrefix + "_DoesntExis"}, res: nil, err: handlers.ApiErrorWithCode(codes.NotFound), }, + { + name: "Get a non existing oidc account", + req: &pbs.GetAccountRequest{Id: oidc.AccountPrefix + "_DoesntExis"}, + res: nil, + err: handlers.ApiErrorWithCode(codes.NotFound), + }, { name: "Wrong id prefix", req: &pbs.GetAccountRequest{Id: "j_1234567890"}, @@ -97,20 +208,25 @@ func TestGet(t *testing.T) { if tc.err != nil { require.Error(gErr) assert.True(errors.Is(gErr, tc.err), "GetAccount(%+v) got error %v, wanted %v", tc.req, gErr, tc.err) + return } + require.NoError(gErr) assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "GetAccount(%q) got response %q, wanted %q", tc.req, got, tc.res) }) } } -func TestList(t *testing.T) { +func TestListPassword(t *testing.T) { conn, _ := db.TestSetup(t, "postgres") rw := db.New(conn) wrap := db.TestWrapper(t) kms := kms.TestKms(t, conn, wrap) - repoFn := func() (*password.Repository, error) { + pwRepoFn := func() (*password.Repository, error) { return password.NewRepository(rw, rw, kms) } + oidcRepoFn := func() (*oidc.Repository, error) { + return oidc.NewRepository(rw, rw, kms) + } iamRepoFn := func() (*iam.Repository, error) { return iam.NewRepository(rw, rw, kms) } @@ -130,7 +246,7 @@ func TestList(t *testing.T) { Version: 1, Type: "password", Attributes: &structpb.Struct{Fields: map[string]*structpb.Value{"login_name": structpb.NewStringValue(aa.GetLoginName())}}, - AuthorizedActions: []string{"read", "update", "delete", "set-password", "change-password"}, + AuthorizedActions: pwAuthorizedActions, }) } @@ -145,7 +261,7 @@ func TestList(t *testing.T) { Version: 1, Type: "password", Attributes: &structpb.Struct{Fields: map[string]*structpb.Value{"login_name": structpb.NewStringValue(aa.GetLoginName())}}, - AuthorizedActions: []string{"read", "update", "delete", "set-password", "change-password"}, + AuthorizedActions: pwAuthorizedActions, }) } @@ -200,13 +316,14 @@ func TestList(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) - s, err := accounts.NewService(repoFn) + s, err := accounts.NewService(pwRepoFn, oidcRepoFn) require.NoError(err, "Couldn't create new user service.") got, gErr := s.ListAccounts(auth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId()), tc.req) if tc.err != nil { require.Error(gErr) assert.True(errors.Is(gErr, tc.err), "ListAccounts() with auth method %q got error %v, wanted %v", tc.req, gErr, tc.err) + return } else { require.NoError(gErr) } @@ -215,23 +332,175 @@ func TestList(t *testing.T) { } } -func TestDelete(t *testing.T) { +func TestListOidc(t *testing.T) { + ctx := context.Background() conn, _ := db.TestSetup(t, "postgres") rw := db.New(conn) wrap := db.TestWrapper(t) - kms := kms.TestKms(t, conn, wrap) - repoFn := func() (*password.Repository, error) { - return password.NewRepository(rw, rw, kms) + kmsCache := kms.TestKms(t, conn, wrap) + pwRepoFn := func() (*password.Repository, error) { + return password.NewRepository(rw, rw, kmsCache) + } + oidcRepoFn := func() (*oidc.Repository, error) { + return oidc.NewRepository(rw, rw, kmsCache) } iamRepoFn := func() (*iam.Repository, error) { - return iam.NewRepository(rw, rw, kms) + return iam.NewRepository(rw, rw, kmsCache) + } + + o, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrap)) + databaseWrapper, err := kmsCache.GetWrapper(ctx, o.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + amNoAccounts := oidc.TestAuthMethod(t, conn, databaseWrapper, o.PublicId, oidc.ActivePrivateState, "noAccounts", "fido", + oidc.WithIssuer(oidc.TestConvertToUrls(t, "https://www.noaccounts.com")[0]), oidc.WithSigningAlgs(oidc.RS256), oidc.WithApiUrl(oidc.TestConvertToUrls(t, "https://www.alice.com/callback")[0])) + amSomeAccounts := oidc.TestAuthMethod(t, conn, databaseWrapper, o.PublicId, oidc.ActivePrivateState, "someAccounts", "fido", + oidc.WithIssuer(oidc.TestConvertToUrls(t, "https://www.someaccounts.com")[0]), oidc.WithSigningAlgs(oidc.RS256), oidc.WithApiUrl(oidc.TestConvertToUrls(t, "https://www.alice.com/callback")[0])) + amOtherAccounts := oidc.TestAuthMethod(t, conn, databaseWrapper, o.PublicId, oidc.ActivePrivateState, "otherAccounts", "fido", + oidc.WithIssuer(oidc.TestConvertToUrls(t, "https://www.otheraccounts.com")[0]), oidc.WithSigningAlgs(oidc.RS256), oidc.WithApiUrl(oidc.TestConvertToUrls(t, "https://www.alice.com/callback")[0])) + + var wantSomeAccounts []*pb.Account + for i := 0; i < 3; i++ { + subId := fmt.Sprintf("test-subject%d", i) + aa := oidc.TestAccount(t, conn, amSomeAccounts, subId) + wantSomeAccounts = append(wantSomeAccounts, &pb.Account{ + Id: aa.GetPublicId(), + AuthMethodId: aa.GetAuthMethodId(), + CreatedTime: aa.GetCreateTime().GetTimestamp(), + UpdatedTime: aa.GetUpdateTime().GetTimestamp(), + Scope: &scopepb.ScopeInfo{Id: o.GetPublicId(), Type: scope.Org.String(), ParentScopeId: scope.Global.String()}, + Version: 1, + Type: auth.OidcSubtype.String(), + Attributes: &structpb.Struct{Fields: map[string]*structpb.Value{ + "issuer": structpb.NewStringValue(amSomeAccounts.GetIssuer()), + "subject": structpb.NewStringValue(subId), + }}, + AuthorizedActions: oidcAuthorizedActions, + }) + } + + var wantOtherAccounts []*pb.Account + for i := 0; i < 3; i++ { + subId := fmt.Sprintf("test-subject%d", i) + aa := oidc.TestAccount(t, conn, amOtherAccounts, subId) + wantOtherAccounts = append(wantOtherAccounts, &pb.Account{ + Id: aa.GetPublicId(), + AuthMethodId: aa.GetAuthMethodId(), + CreatedTime: aa.GetCreateTime().GetTimestamp(), + UpdatedTime: aa.GetUpdateTime().GetTimestamp(), + Scope: &scopepb.ScopeInfo{Id: o.GetPublicId(), Type: scope.Org.String(), ParentScopeId: scope.Global.String()}, + Version: 1, + Type: auth.OidcSubtype.String(), + Attributes: &structpb.Struct{Fields: map[string]*structpb.Value{ + "issuer": structpb.NewStringValue(amOtherAccounts.GetIssuer()), + "subject": structpb.NewStringValue(subId), + }}, + AuthorizedActions: oidcAuthorizedActions, + }) + } + + cases := []struct { + name string + req *pbs.ListAccountsRequest + res *pbs.ListAccountsResponse + err error + }{ + { + name: "List Some Accounts", + req: &pbs.ListAccountsRequest{AuthMethodId: amSomeAccounts.GetPublicId()}, + res: &pbs.ListAccountsResponse{Items: wantSomeAccounts}, + }, + { + name: "List Other Accounts", + req: &pbs.ListAccountsRequest{AuthMethodId: amOtherAccounts.GetPublicId()}, + res: &pbs.ListAccountsResponse{Items: wantOtherAccounts}, + }, + { + name: "List No Accounts", + req: &pbs.ListAccountsRequest{AuthMethodId: amNoAccounts.GetPublicId()}, + res: &pbs.ListAccountsResponse{}, + }, + { + name: "Unfound Auth Method", + req: &pbs.ListAccountsRequest{AuthMethodId: oidc.AuthMethodPrefix + "_DoesntExis"}, + err: handlers.ApiErrorWithCode(codes.NotFound), + }, + { + name: "Filter Some Accounts", + req: &pbs.ListAccountsRequest{ + AuthMethodId: amSomeAccounts.GetPublicId(), + Filter: fmt.Sprintf(`"/item/attributes/subject"==%q`, wantSomeAccounts[1].Attributes.AsMap()["subject"]), + }, + res: &pbs.ListAccountsResponse{Items: wantSomeAccounts[1:2]}, + }, + { + name: "Filter All Accounts", + req: &pbs.ListAccountsRequest{ + AuthMethodId: amSomeAccounts.GetPublicId(), + Filter: `"/item/id"=="noaccountmatchesthis"`, + }, + res: &pbs.ListAccountsResponse{}, + }, + { + name: "Filter Bad Format", + req: &pbs.ListAccountsRequest{AuthMethodId: amSomeAccounts.GetPublicId(), Filter: `"//id/"=="bad"`}, + err: handlers.InvalidArgumentErrorf("bad format", nil), + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + s, err := accounts.NewService(pwRepoFn, oidcRepoFn) + require.NoError(err, "Couldn't create new user service.") + + got, gErr := s.ListAccounts(auth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId()), tc.req) + if tc.err != nil { + require.Error(gErr) + assert.True(errors.Is(gErr, tc.err), "ListAccounts() with auth method %q got error %v, wanted %v", tc.req, gErr, tc.err) + return + } else { + require.NoError(gErr) + } + sort.Slice(got.Items, func(i, j int) bool { + return strings.Compare(got.Items[i].GetAttributes().GetFields()["subject"].GetStringValue(), + got.Items[j].GetAttributes().GetFields()["subject"].GetStringValue()) < 0 + }) + assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "ListAccounts() with scope %q got response %q, wanted %q", tc.req, got, tc.res) + }) + } +} + +func TestDelete(t *testing.T) { + ctx := context.Background() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrap := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, wrap) + pwRepoFn := func() (*password.Repository, error) { + return password.NewRepository(rw, rw, kmsCache) + } + oidcRepoFn := func() (*oidc.Repository, error) { + return oidc.NewRepository(rw, rw, kmsCache) + } + iamRepoFn := func() (*iam.Repository, error) { + return iam.NewRepository(rw, rw, kmsCache) } o, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrap)) am1 := password.TestAuthMethods(t, conn, o.GetPublicId(), 1)[0] ac := password.TestAccounts(t, conn, am1.GetPublicId(), 1)[0] - s, err := accounts.NewService(repoFn) + databaseWrapper, err := kmsCache.GetWrapper(ctx, o.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + oidcAm := oidc.TestAuthMethod( + t, conn, databaseWrapper, o.PublicId, oidc.ActivePrivateState, + "alice-rp", "fido", + oidc.WithIssuer(oidc.TestConvertToUrls(t, "https://www.alice.com")[0]), + oidc.WithSigningAlgs(oidc.RS256), + oidc.WithApiUrl(oidc.TestConvertToUrls(t, "https://www.alice.com/callback")[0]), + ) + oidcA := oidc.TestAccount(t, conn, oidcAm, "test-subject") + + s, err := accounts.NewService(pwRepoFn, oidcRepoFn) require.NoError(t, err, "Error when getting new user service.") cases := []struct { @@ -242,19 +511,33 @@ func TestDelete(t *testing.T) { err error }{ { - name: "Delete an existing token", + name: "Delete an existing pw account", req: &pbs.DeleteAccountRequest{ Id: ac.GetPublicId(), }, res: &pbs.DeleteAccountResponse{}, }, { - name: "Delete bad account id", + name: "Delete an existing oidc account", + req: &pbs.DeleteAccountRequest{ + Id: oidcA.GetPublicId(), + }, + res: &pbs.DeleteAccountResponse{}, + }, + { + name: "Delete bad pw account id", req: &pbs.DeleteAccountRequest{ Id: password.AccountPrefix + "_doesntexis", }, err: handlers.ApiErrorWithCode(codes.NotFound), }, + { + name: "Delete bad oidc account id", + req: &pbs.DeleteAccountRequest{ + Id: oidc.AccountPrefix + "_doesntexis", + }, + err: handlers.ApiErrorWithCode(codes.NotFound), + }, { name: "Bad account id formatting", req: &pbs.DeleteAccountRequest{ @@ -282,9 +565,12 @@ func TestDelete_twice(t *testing.T) { wrap := db.TestWrapper(t) rw := db.New(conn) kms := kms.TestKms(t, conn, wrap) - repoFn := func() (*password.Repository, error) { + pwRepoFn := func() (*password.Repository, error) { return password.NewRepository(rw, rw, kms) } + oidcRepoFn := func() (*oidc.Repository, error) { + return oidc.NewRepository(rw, rw, kms) + } iamRepoFn := func() (*iam.Repository, error) { return iam.NewRepository(rw, rw, kms) } @@ -293,7 +579,7 @@ func TestDelete_twice(t *testing.T) { am := password.TestAuthMethods(t, conn, o.GetPublicId(), 1)[0] ac := password.TestAccounts(t, conn, am.GetPublicId(), 1)[0] - s, err := accounts.NewService(repoFn) + s, err := accounts.NewService(pwRepoFn, oidcRepoFn) require.NoError(err, "Error when getting new user service") req := &pbs.DeleteAccountRequest{ Id: ac.GetPublicId(), @@ -305,25 +591,28 @@ func TestDelete_twice(t *testing.T) { assert.True(errors.Is(gErr, handlers.ApiErrorWithCode(codes.NotFound)), "Expected permission denied for the second delete.") } -func TestCreate(t *testing.T) { +func TestCreatePassword(t *testing.T) { conn, _ := db.TestSetup(t, "postgres") rw := db.New(conn) wrap := db.TestWrapper(t) kms := kms.TestKms(t, conn, wrap) - repoFn := func() (*password.Repository, error) { + pwRepoFn := func() (*password.Repository, error) { return password.NewRepository(rw, rw, kms) } + oidcRepoFn := func() (*oidc.Repository, error) { + return oidc.NewRepository(rw, rw, kms) + } iamRepoFn := func() (*iam.Repository, error) { return iam.NewRepository(rw, rw, kms) } - s, err := accounts.NewService(repoFn) + s, err := accounts.NewService(pwRepoFn, oidcRepoFn) require.NoError(t, err, "Error when getting new account service.") o, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrap)) am := password.TestAuthMethods(t, conn, o.GetPublicId(), 1)[0] defaultAccount := password.TestAccounts(t, conn, am.GetPublicId(), 1)[0] - defaultCreated, err := ptypes.Timestamp(defaultAccount.GetCreateTime().GetTimestamp()) + defaultCreated := defaultAccount.GetCreateTime().GetTimestamp() require.NoError(t, err, "Error converting proto to timestamp.") createAttr := func(un, pw string) *structpb.Struct { @@ -363,7 +652,7 @@ func TestCreate(t *testing.T) { Version: 1, Type: "password", Attributes: createAttr("validaccount", ""), - AuthorizedActions: []string{"read", "update", "delete", "set-password", "change-password"}, + AuthorizedActions: pwAuthorizedActions, }, }, }, @@ -383,7 +672,7 @@ func TestCreate(t *testing.T) { Version: 1, Type: "password", Attributes: createAttr("notypedefined", ""), - AuthorizedActions: []string{"read", "update", "delete", "set-password", "change-password"}, + AuthorizedActions: pwAuthorizedActions, }, }, }, @@ -407,7 +696,7 @@ func TestCreate(t *testing.T) { Version: 1, Type: "password", Attributes: createAttr("haspassword", ""), - AuthorizedActions: []string{"read", "update", "delete", "set-password", "change-password"}, + AuthorizedActions: pwAuthorizedActions, }, }, }, @@ -441,7 +730,7 @@ func TestCreate(t *testing.T) { req: &pbs.CreateAccountRequest{ Item: &pb.Account{ AuthMethodId: defaultAccount.GetAuthMethodId(), - CreatedTime: ptypes.TimestampNow(), + CreatedTime: timestamppb.Now(), Type: "password", Attributes: createAttr("nocreatedtime", ""), }, @@ -454,7 +743,7 @@ func TestCreate(t *testing.T) { req: &pbs.CreateAccountRequest{ Item: &pb.Account{ AuthMethodId: defaultAccount.GetAuthMethodId(), - UpdatedTime: ptypes.TimestampNow(), + UpdatedTime: timestamppb.Now(), Type: "password", Attributes: createAttr("noupdatetime", ""), }, @@ -485,13 +774,13 @@ func TestCreate(t *testing.T) { if got != nil { assert.Contains(got.GetUri(), tc.res.Uri) assert.True(strings.HasPrefix(got.GetItem().GetId(), password.AccountPrefix+"_")) - gotCreateTime, err := ptypes.Timestamp(got.GetItem().GetCreatedTime()) + gotCreateTime := got.GetItem().GetCreatedTime() require.NoError(err, "Error converting proto to timestamp.") - gotUpdateTime, err := ptypes.Timestamp(got.GetItem().GetUpdatedTime()) + gotUpdateTime := got.GetItem().GetUpdatedTime() require.NoError(err, "Error converting proto to timestamp.") // Verify it is a user created after the test setup's default user - assert.True(gotCreateTime.After(defaultCreated), "New account should have been created after default user. Was created %v, which is after %v", gotCreateTime, defaultCreated) - assert.True(gotUpdateTime.After(defaultCreated), "New account should have been updated after default user. Was updated %v, which is after %v", gotUpdateTime, defaultCreated) + assert.True(gotCreateTime.AsTime().After(defaultCreated.AsTime()), "New account should have been created after default user. Was created %v, which is after %v", gotCreateTime, defaultCreated) + assert.True(gotUpdateTime.AsTime().After(defaultCreated.AsTime()), "New account should have been updated after default user. Was updated %v, which is after %v", gotUpdateTime, defaultCreated) // Clear all values which are hard to compare against. got.Uri, tc.res.Uri = "", "" @@ -503,21 +792,237 @@ func TestCreate(t *testing.T) { } } -func TestUpdate(t *testing.T) { +func TestCreateOidc(t *testing.T) { + ctx := context.Background() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrap := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, wrap) + pwRepoFn := func() (*password.Repository, error) { + return password.NewRepository(rw, rw, kmsCache) + } + oidcRepoFn := func() (*oidc.Repository, error) { + return oidc.NewRepository(rw, rw, kmsCache) + } + iamRepoFn := func() (*iam.Repository, error) { + return iam.NewRepository(rw, rw, kmsCache) + } + + s, err := accounts.NewService(pwRepoFn, oidcRepoFn) + require.NoError(t, err, "Error when getting new account service.") + + o, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrap)) + databaseWrapper, err := kmsCache.GetWrapper(ctx, o.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + am := oidc.TestAuthMethod( + t, conn, databaseWrapper, o.PublicId, oidc.ActivePrivateState, + "alice-rp", "fido", + oidc.WithIssuer(oidc.TestConvertToUrls(t, "https://www.alice.com")[0]), + oidc.WithSigningAlgs(oidc.RS256), + oidc.WithApiUrl(oidc.TestConvertToUrls(t, "https://www.alice.com/callback")[0]), + ) + + createAttr := func(sid string) *structpb.Struct { + attr := &pb.OidcAccountAttributes{Subject: sid} + ret, err := handlers.ProtoToStruct(attr) + require.NoError(t, err, "Error converting proto to struct.") + return ret + } + + cases := []struct { + name string + req *pbs.CreateAccountRequest + res *pbs.CreateAccountResponse + err error + }{ + { + name: "Create a valid Account", + req: &pbs.CreateAccountRequest{ + Item: &pb.Account{ + AuthMethodId: am.GetPublicId(), + Name: &wrapperspb.StringValue{Value: "name"}, + Description: &wrapperspb.StringValue{Value: "desc"}, + Type: auth.OidcSubtype.String(), + Attributes: createAttr("valid-account"), + }, + }, + res: &pbs.CreateAccountResponse{ + Uri: fmt.Sprintf("accounts/%s_", oidc.AccountPrefix), + Item: &pb.Account{ + AuthMethodId: am.GetPublicId(), + Name: &wrapperspb.StringValue{Value: "name"}, + Description: &wrapperspb.StringValue{Value: "desc"}, + Scope: &scopepb.ScopeInfo{Id: o.GetPublicId(), Type: scope.Org.String(), ParentScopeId: scope.Global.String()}, + Version: 1, + Type: auth.OidcSubtype.String(), + Attributes: func() *structpb.Struct { + a := createAttr("valid-account") + a.Fields["issuer"] = structpb.NewStringValue(am.GetIssuer()) + return a + }(), + AuthorizedActions: oidcAuthorizedActions, + }, + }, + }, + { + name: "Create a valid Account without type defined", + req: &pbs.CreateAccountRequest{ + Item: &pb.Account{ + AuthMethodId: am.GetPublicId(), + Attributes: createAttr("no type defined"), + }, + }, + res: &pbs.CreateAccountResponse{ + Uri: fmt.Sprintf("accounts/%s_", oidc.AccountPrefix), + Item: &pb.Account{ + AuthMethodId: am.GetPublicId(), + Scope: &scopepb.ScopeInfo{Id: o.GetPublicId(), Type: scope.Org.String(), ParentScopeId: scope.Global.String()}, + Version: 1, + Type: auth.OidcSubtype.String(), + Attributes: func() *structpb.Struct { + a := createAttr("no type defined") + a.Fields["issuer"] = structpb.NewStringValue(am.GetIssuer()) + return a + }(), + AuthorizedActions: oidcAuthorizedActions, + }, + }, + }, + { + name: "Create Account With Overwritten Issuer", + req: &pbs.CreateAccountRequest{ + Item: &pb.Account{ + AuthMethodId: am.GetPublicId(), + Name: &wrapperspb.StringValue{Value: "overwritten issuer"}, + Type: auth.OidcSubtype.String(), + Attributes: func() *structpb.Struct { + a := createAttr("overwritten-issuer") + a.Fields["issuer"] = structpb.NewStringValue("https://overwrite.com") + return a + }(), + }, + }, + res: &pbs.CreateAccountResponse{ + Uri: fmt.Sprintf("accounts/%s_", oidc.AccountPrefix), + Item: &pb.Account{ + AuthMethodId: am.GetPublicId(), + Name: &wrapperspb.StringValue{Value: "overwritten issuer"}, + Scope: &scopepb.ScopeInfo{Id: o.GetPublicId(), Type: scope.Org.String(), ParentScopeId: scope.Global.String()}, + Version: 1, + Type: auth.OidcSubtype.String(), + Attributes: func() *structpb.Struct { + a := createAttr("overwritten-issuer") + a.Fields["issuer"] = structpb.NewStringValue("https://overwrite.com") + return a + }(), + AuthorizedActions: oidcAuthorizedActions, + }, + }, + }, + { + name: "Cant specify mismatching type", + req: &pbs.CreateAccountRequest{ + Item: &pb.Account{ + AuthMethodId: am.GetPublicId(), + Type: auth.PasswordSubtype.String(), + Attributes: createAttr("cant-specify-mismatching-type"), + }, + }, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "Can't specify Id", + req: &pbs.CreateAccountRequest{ + Item: &pb.Account{ + AuthMethodId: am.GetPublicId(), + Id: oidc.AccountPrefix + "_notallowed", + Type: auth.OidcSubtype.String(), + Attributes: createAttr("cant-specify-id"), + }, + }, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "Can't specify Created Time", + req: &pbs.CreateAccountRequest{ + Item: &pb.Account{ + AuthMethodId: am.GetPublicId(), + CreatedTime: timestamppb.Now(), + Type: auth.OidcSubtype.String(), + Attributes: createAttr("cant-specify-created-time"), + }, + }, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "Can't specify Update Time", + req: &pbs.CreateAccountRequest{ + Item: &pb.Account{ + AuthMethodId: am.GetPublicId(), + UpdatedTime: timestamppb.Now(), + Type: auth.OidcSubtype.String(), + Attributes: createAttr("cant-specify-update-time"), + }, + }, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "Must specify subject", + req: &pbs.CreateAccountRequest{ + Item: &pb.Account{ + AuthMethodId: am.GetPublicId(), + Type: auth.OidcSubtype.String(), + }, + }, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + got, gErr := s.CreateAccount(auth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId()), tc.req) + if tc.err != nil { + require.Error(gErr) + assert.True(errors.Is(gErr, tc.err), "CreateAccount(%+v) got error %v, wanted %v", tc.req, gErr, tc.err) + return + } + require.NoError(gErr) + if got != nil { + assert.Contains(got.GetUri(), tc.res.Uri) + assert.True(strings.HasPrefix(got.GetItem().GetId(), oidc.AccountPrefix+"_")) + // Clear all values which are hard to compare against. + got.Uri, tc.res.Uri = "", "" + got.Item.Id, tc.res.Item.Id = "", "" + got.Item.CreatedTime, got.Item.UpdatedTime, tc.res.Item.CreatedTime, tc.res.Item.UpdatedTime = nil, nil, nil, nil + } + assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "CreateAccount(%q) got response %q, wanted %q", tc.req, got, tc.res) + }) + } +} + +func TestUpdatePassword(t *testing.T) { conn, _ := db.TestSetup(t, "postgres") rw := db.New(conn) wrap := db.TestWrapper(t) kms := kms.TestKms(t, conn, wrap) - repoFn := func() (*password.Repository, error) { + pwRepoFn := func() (*password.Repository, error) { return password.NewRepository(rw, rw, kms) } + oidcRepoFn := func() (*oidc.Repository, error) { + return oidc.NewRepository(rw, rw, kms) + } iamRepoFn := func() (*iam.Repository, error) { return iam.NewRepository(rw, rw, kms) } o, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrap)) am := password.TestAuthMethods(t, conn, o.GetPublicId(), 1)[0] - tested, err := accounts.NewService(repoFn) + tested, err := accounts.NewService(pwRepoFn, oidcRepoFn) require.NoError(t, err, "Error when getting new auth_method service.") defaultScopeInfo := &scopepb.ScopeInfo{Id: o.GetPublicId(), Type: o.GetType(), ParentScopeId: scope.Global.String()} @@ -578,7 +1083,7 @@ func TestUpdate(t *testing.T) { Type: "password", Attributes: defaultAttributes, Scope: defaultScopeInfo, - AuthorizedActions: []string{"read", "update", "delete", "set-password", "change-password"}, + AuthorizedActions: pwAuthorizedActions, }, }, }, @@ -602,7 +1107,7 @@ func TestUpdate(t *testing.T) { Type: "password", Attributes: defaultAttributes, Scope: defaultScopeInfo, - AuthorizedActions: []string{"read", "update", "delete", "set-password", "change-password"}, + AuthorizedActions: pwAuthorizedActions, }, }, }, @@ -672,7 +1177,7 @@ func TestUpdate(t *testing.T) { Type: "password", Attributes: defaultAttributes, Scope: defaultScopeInfo, - AuthorizedActions: []string{"read", "update", "delete", "set-password", "change-password"}, + AuthorizedActions: pwAuthorizedActions, }, }, }, @@ -696,7 +1201,7 @@ func TestUpdate(t *testing.T) { Type: "password", Attributes: defaultAttributes, Scope: defaultScopeInfo, - AuthorizedActions: []string{"read", "update", "delete", "set-password", "change-password"}, + AuthorizedActions: pwAuthorizedActions, }, }, }, @@ -720,7 +1225,7 @@ func TestUpdate(t *testing.T) { Type: "password", Attributes: defaultAttributes, Scope: defaultScopeInfo, - AuthorizedActions: []string{"read", "update", "delete", "set-password", "change-password"}, + AuthorizedActions: pwAuthorizedActions, }, }, }, @@ -744,7 +1249,7 @@ func TestUpdate(t *testing.T) { Type: "password", Attributes: modifiedAttributes, Scope: defaultScopeInfo, - AuthorizedActions: []string{"read", "update", "delete", "set-password", "change-password"}, + AuthorizedActions: pwAuthorizedActions, }, }, }, @@ -784,7 +1289,7 @@ func TestUpdate(t *testing.T) { Paths: []string{"created_time"}, }, Item: &pb.Account{ - CreatedTime: ptypes.TimestampNow(), + CreatedTime: timestamppb.Now(), }, }, res: nil, @@ -797,7 +1302,7 @@ func TestUpdate(t *testing.T) { Paths: []string{"updated_time"}, }, Item: &pb.Account{ - UpdatedTime: ptypes.TimestampNow(), + UpdatedTime: timestamppb.Now(), }, }, res: nil, @@ -846,14 +1351,397 @@ func TestUpdate(t *testing.T) { if got != nil { assert.NotNilf(tc.res, "Expected UpdateAccount response to be nil, but was %v", got) - gotUpdateTime, err := ptypes.Timestamp(got.GetItem().GetUpdatedTime()) + gotUpdateTime := got.GetItem().GetUpdatedTime() require.NoError(err, "Error converting proto to timestamp") - created, err := ptypes.Timestamp(acc.GetCreatedTime()) + created := acc.GetCreatedTime() require.NoError(err, "Error converting proto to timestamp") // Verify it is a auth_method updated after it was created - assert.True(gotUpdateTime.After(created), "Updated account should have been updated after it's creation. Was updated %v, which is after %v", gotUpdateTime, created) + assert.True(gotUpdateTime.AsTime().After(created.AsTime()), "Updated account should have been updated after it's creation. Was updated %v, which is after %v", gotUpdateTime, created) + + // Clear all values which are hard to compare against. + got.Item.UpdatedTime, tc.res.Item.UpdatedTime = nil, nil + + assert.EqualValues(2, got.Item.Version) + tc.res.Item.Version = 2 + } + assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "UpdateAccount(%q) got response %q, wanted %q", tc.req, got, tc.res) + }) + } +} + +func TestUpdateOidc(t *testing.T) { + ctx := context.Background() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrap := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, wrap) + pwRepoFn := func() (*password.Repository, error) { + return password.NewRepository(rw, rw, kmsCache) + } + oidcRepoFn := func() (*oidc.Repository, error) { + return oidc.NewRepository(rw, rw, kmsCache) + } + iamRepoFn := func() (*iam.Repository, error) { + return iam.NewRepository(rw, rw, kmsCache) + } + + o, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrap)) + + databaseWrapper, err := kmsCache.GetWrapper(ctx, o.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + am := oidc.TestAuthMethod( + t, conn, databaseWrapper, o.PublicId, oidc.ActivePrivateState, + "alice-rp", "fido", + oidc.WithIssuer(oidc.TestConvertToUrls(t, "https://www.alice.com")[0]), + oidc.WithSigningAlgs(oidc.RS256), + oidc.WithApiUrl(oidc.TestConvertToUrls(t, "https://www.alice.com/callback")[0])) + + tested, err := accounts.NewService(pwRepoFn, oidcRepoFn) + require.NoError(t, err, "Error when getting new auth_method service.") + + defaultScopeInfo := &scopepb.ScopeInfo{Id: o.GetPublicId(), Type: o.GetType(), ParentScopeId: scope.Global.String()} + defaultAttributes := &structpb.Struct{Fields: map[string]*structpb.Value{ + "issuer": structpb.NewStringValue("https://www.alice.com"), + "subject": structpb.NewStringValue("test-subject"), + }} + modifiedAttributes := &structpb.Struct{Fields: map[string]*structpb.Value{ + "issuer": structpb.NewStringValue("https://www.changed.com"), + "subject": structpb.NewStringValue("changed"), + }} + + freshAccount := func(t *testing.T) (*oidc.Account, func()) { + t.Helper() + acc := oidc.TestAccount(t, conn, am, "test-subject", oidc.WithName("default"), oidc.WithDescription("default")) + + clean := func() { + _, err := tested.DeleteAccount(auth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId()), + &pbs.DeleteAccountRequest{Id: acc.GetPublicId()}) + require.NoError(t, err) + } + + return acc, clean + } + + cases := []struct { + name string + req *pbs.UpdateAccountRequest + res *pbs.UpdateAccountResponse + err error + }{ + { + name: "Update an Existing AuthMethod", + req: &pbs.UpdateAccountRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"name", "description"}, + }, + Item: &pb.Account{ + Name: &wrapperspb.StringValue{Value: "new"}, + Description: &wrapperspb.StringValue{Value: "desc"}, + Type: auth.OidcSubtype.String(), + }, + }, + res: &pbs.UpdateAccountResponse{ + Item: &pb.Account{ + AuthMethodId: am.GetPublicId(), + Name: &wrapperspb.StringValue{Value: "new"}, + Description: &wrapperspb.StringValue{Value: "desc"}, + Type: auth.OidcSubtype.String(), + Attributes: defaultAttributes, + Scope: defaultScopeInfo, + AuthorizedActions: oidcAuthorizedActions, + }, + }, + }, + { + name: "Multiple Paths in single string", + req: &pbs.UpdateAccountRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"name,description"}, + }, + Item: &pb.Account{ + Name: &wrapperspb.StringValue{Value: "new"}, + Description: &wrapperspb.StringValue{Value: "desc"}, + Type: auth.OidcSubtype.String(), + }, + }, + res: &pbs.UpdateAccountResponse{ + Item: &pb.Account{ + AuthMethodId: am.GetPublicId(), + Name: &wrapperspb.StringValue{Value: "new"}, + Description: &wrapperspb.StringValue{Value: "desc"}, + Type: auth.OidcSubtype.String(), + Attributes: defaultAttributes, + Scope: defaultScopeInfo, + AuthorizedActions: oidcAuthorizedActions, + }, + }, + }, + { + name: "No Update Mask", + req: &pbs.UpdateAccountRequest{ + Item: &pb.Account{ + Name: &wrapperspb.StringValue{Value: "updated name"}, + Description: &wrapperspb.StringValue{Value: "updated desc"}, + Attributes: modifiedAttributes, + }, + }, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "Cant change type", + req: &pbs.UpdateAccountRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"name"}, + }, + Item: &pb.Account{ + Name: &wrapperspb.StringValue{Value: ""}, + Type: "oidc", + }, + }, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "No Paths in Mask", + req: &pbs.UpdateAccountRequest{ + UpdateMask: &field_mask.FieldMask{Paths: []string{}}, + Item: &pb.Account{ + Name: &wrapperspb.StringValue{Value: "updated name"}, + Description: &wrapperspb.StringValue{Value: "updated desc"}, + Attributes: modifiedAttributes, + }, + }, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "Only non-existant paths in Mask", + req: &pbs.UpdateAccountRequest{ + UpdateMask: &field_mask.FieldMask{Paths: []string{"nonexistant_field"}}, + Item: &pb.Account{ + Name: &wrapperspb.StringValue{Value: "updated name"}, + Description: &wrapperspb.StringValue{Value: "updated desc"}, + Attributes: modifiedAttributes, + }, + }, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "Unset Name", + req: &pbs.UpdateAccountRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"name"}, + }, + Item: &pb.Account{ + Description: &wrapperspb.StringValue{Value: "ignored"}, + Attributes: modifiedAttributes, + }, + }, + res: &pbs.UpdateAccountResponse{ + Item: &pb.Account{ + AuthMethodId: am.GetPublicId(), + Description: &wrapperspb.StringValue{Value: "default"}, + Type: auth.OidcSubtype.String(), + Attributes: defaultAttributes, + Scope: defaultScopeInfo, + AuthorizedActions: oidcAuthorizedActions, + }, + }, + }, + { + name: "Update Only Name", + req: &pbs.UpdateAccountRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"name"}, + }, + Item: &pb.Account{ + Name: &wrapperspb.StringValue{Value: "updated"}, + Description: &wrapperspb.StringValue{Value: "ignored"}, + Attributes: modifiedAttributes, + }, + }, + res: &pbs.UpdateAccountResponse{ + Item: &pb.Account{ + AuthMethodId: am.GetPublicId(), + Name: &wrapperspb.StringValue{Value: "updated"}, + Description: &wrapperspb.StringValue{Value: "default"}, + Type: auth.OidcSubtype.String(), + Attributes: defaultAttributes, + Scope: defaultScopeInfo, + AuthorizedActions: oidcAuthorizedActions, + }, + }, + }, + { + name: "Update Only Description", + req: &pbs.UpdateAccountRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"description"}, + }, + Item: &pb.Account{ + Name: &wrapperspb.StringValue{Value: "ignored"}, + Description: &wrapperspb.StringValue{Value: "notignored"}, + Attributes: modifiedAttributes, + }, + }, + res: &pbs.UpdateAccountResponse{ + Item: &pb.Account{ + AuthMethodId: am.GetPublicId(), + Name: &wrapperspb.StringValue{Value: "default"}, + Description: &wrapperspb.StringValue{Value: "notignored"}, + Type: auth.OidcSubtype.String(), + Attributes: defaultAttributes, + Scope: defaultScopeInfo, + AuthorizedActions: oidcAuthorizedActions, + }, + }, + }, + { + name: "Update LoginName", + req: &pbs.UpdateAccountRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"attributes.login_name"}, + }, + Item: &pb.Account{ + Name: &wrapperspb.StringValue{Value: "ignored"}, + Description: &wrapperspb.StringValue{Value: "ignored"}, + Attributes: modifiedAttributes, + }, + }, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "Update a Non Existing Account", + req: &pbs.UpdateAccountRequest{ + Id: password.AccountPrefix + "_DoesntExis", + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"description"}, + }, + Item: &pb.Account{ + Name: &wrapperspb.StringValue{Value: "new"}, + Description: &wrapperspb.StringValue{Value: "desc"}, + }, + }, + err: handlers.ApiErrorWithCode(codes.NotFound), + }, + { + name: "Cant change Id", + req: &pbs.UpdateAccountRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"id"}, + }, + Item: &pb.Account{ + Id: password.AccountPrefix + "_somethinge", + Name: &wrapperspb.StringValue{Value: "new"}, + Description: &wrapperspb.StringValue{Value: "new desc"}, + }, + }, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "Cant specify Created Time", + req: &pbs.UpdateAccountRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"created_time"}, + }, + Item: &pb.Account{ + CreatedTime: timestamppb.Now(), + }, + }, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "Cant specify Updated Time", + req: &pbs.UpdateAccountRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"updated_time"}, + }, + Item: &pb.Account{ + UpdatedTime: timestamppb.Now(), + }, + }, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "Cant specify Type", + req: &pbs.UpdateAccountRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"type"}, + }, + Item: &pb.Account{ + Type: "oidc", + }, + }, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "Update Issuer", + req: &pbs.UpdateAccountRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"attributes.issuer"}, + }, + Item: &pb.Account{ + Name: &wrapperspb.StringValue{Value: "ignored"}, + Attributes: modifiedAttributes, + }, + }, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "Update Subject", + req: &pbs.UpdateAccountRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"attributes.subject"}, + }, + Item: &pb.Account{ + Name: &wrapperspb.StringValue{Value: "ignored"}, + Attributes: modifiedAttributes, + }, + }, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + acc, cleanup := freshAccount(t) + defer cleanup() + + tc.req.Item.Version = 1 + + if tc.req.GetId() == "" { + tc.req.Id = acc.GetPublicId() + } + + if tc.res != nil && tc.res.Item != nil { + tc.res.Item.Id = acc.GetPublicId() + tc.res.Item.CreatedTime = acc.GetCreateTime().GetTimestamp() + } + + got, gErr := tested.UpdateAccount(auth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId()), tc.req) + if tc.err != nil { + require.Error(gErr) + assert.True(errors.Is(gErr, tc.err), "UpdateAccount(%+v) got error %v, wanted %v", tc.req, gErr, tc.err) + } + + if tc.res == nil { + require.Nil(got) + } + + if got != nil { + assert.NotNilf(tc.res, "Expected UpdateAccount response to be nil, but was %v", got) + gotUpdateTime := got.GetItem().GetUpdatedTime() + require.NoError(err, "Error converting proto to timestamp") + + created := acc.GetCreateTime().GetTimestamp() + require.NoError(err, "Error converting proto to timestamp") + + // Verify it is a auth_method updated after it was created + assert.True(gotUpdateTime.AsTime().After(created.AsTime()), "Updated account should have been updated after it's creation. Was updated %v, which is after %v", gotUpdateTime, created) // Clear all values which are hard to compare against. got.Item.UpdatedTime, tc.res.Item.UpdatedTime = nil, nil @@ -871,15 +1759,18 @@ func TestSetPassword(t *testing.T) { rw := db.New(conn) wrap := db.TestWrapper(t) kms := kms.TestKms(t, conn, wrap) - repoFn := func() (*password.Repository, error) { + pwRepoFn := func() (*password.Repository, error) { return password.NewRepository(rw, rw, kms) } + oidcRepoFn := func() (*oidc.Repository, error) { + return oidc.NewRepository(rw, rw, kms) + } iamRepoFn := func() (*iam.Repository, error) { return iam.NewRepository(rw, rw, kms) } o, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrap)) - tested, err := accounts.NewService(repoFn) + tested, err := accounts.NewService(pwRepoFn, oidcRepoFn) require.NoError(t, err, "Error when getting new auth_method service.") createAccount := func(t *testing.T, pw string) *pb.Account { @@ -966,6 +1857,18 @@ func TestSetPassword(t *testing.T) { version: 0, password: "somepassword", }, + { + name: "notfound account id", + accountId: password.AccountPrefix + "_DoesntExis", + version: defaultAcct.GetVersion(), + password: "anewpassword", + }, + { + name: "password to short", + accountId: defaultAcct.GetId(), + version: defaultAcct.GetVersion(), + password: "123", + }, } for _, tt := range badRequestCases { @@ -988,15 +1891,18 @@ func TestChangePassword(t *testing.T) { rw := db.New(conn) wrap := db.TestWrapper(t) kms := kms.TestKms(t, conn, wrap) - repoFn := func() (*password.Repository, error) { + pwRepoFn := func() (*password.Repository, error) { return password.NewRepository(rw, rw, kms) } + oidcRepoFn := func() (*oidc.Repository, error) { + return oidc.NewRepository(rw, rw, kms) + } iamRepoFn := func() (*iam.Repository, error) { return iam.NewRepository(rw, rw, kms) } o, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrap)) - tested, err := accounts.NewService(repoFn) + tested, err := accounts.NewService(pwRepoFn, oidcRepoFn) require.NoError(t, err, "Error when getting new auth_method service.") createAccount := func(t *testing.T, pw string) *pb.Account { @@ -1112,6 +2018,22 @@ func TestChangePassword(t *testing.T) { oldPW: "somepassword", newPW: "somepassword", }, + { + name: "notfound account id", + authMethodId: defaultAcct.GetAuthMethodId(), + accountId: password.AccountPrefix + "_DoesntExis", + version: defaultAcct.GetVersion(), + oldPW: "somepassword", + newPW: "anewpassword", + }, + { + name: "new password to short", + authMethodId: defaultAcct.GetAuthMethodId(), + accountId: defaultAcct.GetId(), + version: defaultAcct.GetVersion(), + oldPW: "somepassword", + newPW: "123", + }, } for _, tt := range badRequestCases { diff --git a/internal/servers/controller/handlers/accounts/validate_test.go b/internal/servers/controller/handlers/accounts/validate_test.go new file mode 100644 index 000000000..ec7a51a58 --- /dev/null +++ b/internal/servers/controller/handlers/accounts/validate_test.go @@ -0,0 +1,253 @@ +package accounts + +import ( + "fmt" + "strings" + "testing" + + "github.com/hashicorp/boundary/internal/auth" + "github.com/hashicorp/boundary/internal/auth/oidc" + "github.com/hashicorp/boundary/internal/auth/password" + pb "github.com/hashicorp/boundary/internal/gen/controller/api/resources/accounts" + pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/fieldmaskpb" + "google.golang.org/protobuf/types/known/structpb" +) + +func fieldError(field, details string) string { + return fmt.Sprintf(`{name: %q, desc: %q}`, field, details) +} + +func TestValidateCreateRequest(t *testing.T) { + cases := []struct { + name string + item *pb.Account + errContains string + }{ + { + name: "unrecognized authmethod prefix", + item: &pb.Account{ + AuthMethodId: "anything_1234567890", + }, + errContains: fieldError(authMethodIdField, "Unknown auth method type from ID."), + }, + { + name: "mismatched pw authmethod oidc type", + item: &pb.Account{ + Type: auth.OidcSubtype.String(), + AuthMethodId: password.AuthMethodPrefix + "_1234567890", + }, + errContains: fieldError(typeField, "Doesn't match the parent resource's type."), + }, + { + name: "mismatched oidc authmethod pw type", + item: &pb.Account{ + Type: auth.PasswordSubtype.String(), + AuthMethodId: oidc.AuthMethodPrefix + "_1234567890", + }, + errContains: fieldError(typeField, "Doesn't match the parent resource's type."), + }, + { + name: "missing oidc subject", + item: &pb.Account{ + Type: auth.OidcSubtype.String(), + AuthMethodId: oidc.AuthMethodPrefix + "_1234567890", + }, + errContains: fieldError(subjectField, "This is a required field for this type."), + }, + { + name: "read only name claim field", + item: &pb.Account{ + Type: auth.OidcSubtype.String(), + AuthMethodId: oidc.AuthMethodPrefix + "_1234567890", + Attributes: &structpb.Struct{Fields: map[string]*structpb.Value{ + "full_name": structpb.NewStringValue("something"), + }}, + }, + errContains: fieldError(nameClaimField, "This is a read only field."), + }, + { + name: "read only email claim field", + item: &pb.Account{ + Type: auth.OidcSubtype.String(), + AuthMethodId: oidc.AuthMethodPrefix + "_1234567890", + Attributes: &structpb.Struct{Fields: map[string]*structpb.Value{ + "email": structpb.NewStringValue("something"), + }}, + }, + errContains: fieldError(emailClaimField, "This is a read only field."), + }, + { + name: "missing login name for password type", + item: &pb.Account{ + Type: auth.PasswordSubtype.String(), + AuthMethodId: password.AuthMethodPrefix + "_1234567890", + }, + errContains: fieldError(loginNameKey, "This is a required field for this type."), + }, + { + name: "bad pw attributes", + item: &pb.Account{ + Type: auth.PasswordSubtype.String(), + AuthMethodId: password.AuthMethodPrefix + "_1234567890", + Attributes: &structpb.Struct{Fields: map[string]*structpb.Value{ + "test": structpb.NewStringValue("something"), + }}, + }, + errContains: fieldError(attributesField, "Attribute fields do not match the expected format."), + }, + { + name: "bad oidc attributes", + item: &pb.Account{ + Type: auth.OidcSubtype.String(), + AuthMethodId: oidc.AuthMethodPrefix + "_1234567890", + Attributes: &structpb.Struct{Fields: map[string]*structpb.Value{ + "test": structpb.NewStringValue("something"), + }}, + }, + errContains: fieldError(attributesField, "Attribute fields do not match the expected format."), + }, + { + name: "no error", + item: &pb.Account{ + Type: auth.PasswordSubtype.String(), + AuthMethodId: password.AuthMethodPrefix + "_1234567890", + Attributes: &structpb.Struct{Fields: map[string]*structpb.Value{ + loginNameKey: structpb.NewStringValue("something"), + }}, + }, + }, + { + name: "no oidc errors", + item: &pb.Account{ + Type: auth.OidcSubtype.String(), + AuthMethodId: oidc.AuthMethodPrefix + "_1234567890", + Attributes: &structpb.Struct{Fields: map[string]*structpb.Value{ + "subject": structpb.NewStringValue("no oidc errors"), + }}, + }, + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + req := &pbs.CreateAccountRequest{Item: tc.item} + err := validateCreateRequest(req) + if tc.errContains == "" { + require.NoError(t, err) + return + } + assert.True(t, strings.Contains(err.Error(), tc.errContains), + "%q wasn't contained in %q", tc.errContains, err.Error()) + }) + } +} + +func TestValidateUpdateRequest(t *testing.T) { + cases := []struct { + name string + req *pbs.UpdateAccountRequest + errContains string + }{ + { + name: "password to oidc change type", + req: &pbs.UpdateAccountRequest{ + Id: password.AccountPrefix + "_1234567890", + Item: &pb.Account{ + Type: auth.OidcSubtype.String(), + }, + }, + errContains: fieldError(typeField, "Cannot modify the resource type."), + }, + { + name: "oidc to password change type", + req: &pbs.UpdateAccountRequest{ + Id: oidc.AccountPrefix + "_1234567890", + Item: &pb.Account{ + Type: auth.PasswordSubtype.String(), + }, + }, + errContains: fieldError(typeField, "Cannot modify the resource type."), + }, + { + name: "password bad attributes", + req: &pbs.UpdateAccountRequest{ + Id: password.AccountPrefix + "_1234567890", + Item: &pb.Account{ + Attributes: &structpb.Struct{Fields: map[string]*structpb.Value{ + "test": structpb.NewStringValue("something"), + }}, + }, + }, + errContains: fieldError(attributesField, "Attribute fields do not match the expected format."), + }, + { + name: "oidc bad attributes", + req: &pbs.UpdateAccountRequest{ + Id: oidc.AccountPrefix + "_1234567890", + Item: &pb.Account{ + Attributes: &structpb.Struct{Fields: map[string]*structpb.Value{ + "test": structpb.NewStringValue("something"), + }}, + }, + }, + errContains: fieldError(attributesField, "Attribute fields do not match the expected format."), + }, + { + name: "no error", + req: &pbs.UpdateAccountRequest{ + Id: oidc.AccountPrefix + "_1234567890", + UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{}}, + Item: &pb.Account{ + Version: 1, + }, + }, + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + err := validateUpdateRequest(tc.req) + if tc.errContains == "" { + require.NoError(t, err) + return + } + assert.True(t, strings.Contains(err.Error(), tc.errContains), + "%q wasn't contained in %q", tc.errContains, err.Error()) + }) + } + + t.Run("oidc read only fields", func(t *testing.T) { + readOnlyFields := []string{ + emailClaimField, + nameClaimField, + } + err := validateUpdateRequest(&pbs.UpdateAccountRequest{ + Id: oidc.AccountPrefix + "1234567890", + UpdateMask: &fieldmaskpb.FieldMask{Paths: readOnlyFields}, + }) + + for _, f := range readOnlyFields { + expected := fieldError(f, "Field is read only.") + assert.True(t, strings.Contains(err.Error(), expected), + "%q wasn't contained in %q", expected, err.Error()) + } + }) + + t.Run("oidc write only at create fields", func(t *testing.T) { + readOnlyFields := []string{ + issuerField, + subjectField, + } + err := validateUpdateRequest(&pbs.UpdateAccountRequest{ + Id: oidc.AccountPrefix + "1234567890", + UpdateMask: &fieldmaskpb.FieldMask{Paths: readOnlyFields}, + }) + + for _, f := range readOnlyFields { + expected := fieldError(f, "Field cannot be updated.") + assert.True(t, strings.Contains(err.Error(), expected), + "%q wasn't contained in %q", expected, err.Error()) + } + }) +} diff --git a/internal/servers/controller/handlers/authmethods/authmethod_service.go b/internal/servers/controller/handlers/authmethods/authmethod_service.go index f39ffb698..2dcdc149e 100644 --- a/internal/servers/controller/handlers/authmethods/authmethod_service.go +++ b/internal/servers/controller/handlers/authmethods/authmethod_service.go @@ -4,18 +4,18 @@ import ( "context" stderrors "errors" "fmt" + "net/url" "strings" "github.com/hashicorp/boundary/internal/auth" + "github.com/hashicorp/boundary/internal/auth/oidc" "github.com/hashicorp/boundary/internal/auth/password" - "github.com/hashicorp/boundary/internal/auth/password/store" "github.com/hashicorp/boundary/internal/authtoken" "github.com/hashicorp/boundary/internal/errors" pb "github.com/hashicorp/boundary/internal/gen/controller/api/resources/authmethods" pba "github.com/hashicorp/boundary/internal/gen/controller/api/resources/authtokens" "github.com/hashicorp/boundary/internal/gen/controller/api/resources/scopes" pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services" - "github.com/hashicorp/boundary/internal/iam" "github.com/hashicorp/boundary/internal/kms" "github.com/hashicorp/boundary/internal/perms" "github.com/hashicorp/boundary/internal/servers/controller/common" @@ -33,21 +33,21 @@ import ( ) const ( - loginNameKey = "login_name" - pwKey = "password" + // general auth method field names + commandField = "command" + versionField = "version" + scopeIdField = "scope_id" + typeField = "type" + attributesField = "attributes" + authMethodIdField = "auth_method_id" + tokenTypeField = "token_type" + isPrimaryField = "is_primary" ) var ( - maskManager handlers.MaskManager - // IdActions contains the set of actions that can be performed on // individual resources - IdActions = action.ActionSet{ - action.Read, - action.Update, - action.Delete, - action.Authenticate, - } + IdActions = make(map[auth.SubType]action.ActionSet) // CollectionActions contains the set of actions that can be performed on // this collection @@ -61,13 +61,6 @@ var ( } ) -func init() { - var err error - if maskManager, err = handlers.NewMaskManager(&store.AuthMethod{}, &pb.AuthMethod{}, &pb.PasswordAuthMethodAttributes{}); err != nil { - panic(err) - } -} - func populateCollectionAuthorizedActions(ctx context.Context, authResults auth.VerifyResults, item *pb.AuthMethod) error { @@ -99,24 +92,31 @@ func populateCollectionAuthorizedActions(ctx context.Context, type Service struct { pbs.UnimplementedAuthMethodServiceServer - kms *kms.Kms - pwRepoFn common.PasswordAuthRepoFactory - iamRepoFn common.IamRepoFactory - atRepoFn common.AuthTokenRepoFactory + kms *kms.Kms + pwRepoFn common.PasswordAuthRepoFactory + oidcRepoFn common.OidcAuthRepoFactory + iamRepoFn common.IamRepoFactory + atRepoFn common.AuthTokenRepoFactory } // NewService returns a auth method service which handles auth method related requests to boundary. -func NewService(kms *kms.Kms, pwRepoFn common.PasswordAuthRepoFactory, iamRepoFn common.IamRepoFactory, atRepoFn common.AuthTokenRepoFactory) (Service, error) { +func NewService(kms *kms.Kms, pwRepoFn common.PasswordAuthRepoFactory, oidcRepoFn common.OidcAuthRepoFactory, iamRepoFn common.IamRepoFactory, atRepoFn common.AuthTokenRepoFactory) (Service, error) { if kms == nil { return Service{}, stderrors.New("nil kms provided") } if pwRepoFn == nil { return Service{}, fmt.Errorf("nil password repository provided") } + if oidcRepoFn == nil { + return Service{}, fmt.Errorf("nil oidc repository provided") + } if iamRepoFn == nil { return Service{}, fmt.Errorf("nil iam repository provided") } - return Service{kms: kms, pwRepoFn: pwRepoFn, iamRepoFn: iamRepoFn, atRepoFn: atRepoFn}, nil + if atRepoFn == nil { + return Service{}, fmt.Errorf("nil auth token repository provided") + } + return Service{kms: kms, pwRepoFn: pwRepoFn, oidcRepoFn: oidcRepoFn, iamRepoFn: iamRepoFn, atRepoFn: atRepoFn}, nil } var _ pbs.AuthMethodServiceServer = Service{} @@ -164,7 +164,7 @@ func (s Service) ListAuthMethods(ctx context.Context, req *pbs.ListAuthMethodsRe for _, item := range ul { item.Scope = scopeInfoMap[item.GetScopeId()] res.ScopeId = item.Scope.Id - item.AuthorizedActions = authResults.FetchActionSetForId(ctx, item.Id, IdActions, auth.WithResource(res)).Strings() + item.AuthorizedActions = authResults.FetchActionSetForId(ctx, item.Id, IdActions[auth.SubtypeFromId(item.Id)], auth.WithResource(res)).Strings() if len(item.AuthorizedActions) == 0 { continue } @@ -192,7 +192,7 @@ func (s Service) GetAuthMethod(ctx context.Context, req *pbs.GetAuthMethodReques return nil, err } u.Scope = authResults.Scope - u.AuthorizedActions = authResults.FetchActionSetForId(ctx, u.Id, IdActions).Strings() + u.AuthorizedActions = authResults.FetchActionSetForId(ctx, u.Id, IdActions[auth.SubtypeFromId(u.Id)]).Strings() if err := populateCollectionAuthorizedActions(ctx, authResults, u); err != nil { return nil, err } @@ -213,7 +213,7 @@ func (s Service) CreateAuthMethod(ctx context.Context, req *pbs.CreateAuthMethod return nil, err } u.Scope = authResults.Scope - u.AuthorizedActions = authResults.FetchActionSetForId(ctx, u.Id, IdActions).Strings() + u.AuthorizedActions = authResults.FetchActionSetForId(ctx, u.Id, IdActions[auth.SubtypeFromId(u.Id)]).Strings() if err := populateCollectionAuthorizedActions(ctx, authResults, u); err != nil { return nil, err } @@ -229,18 +229,39 @@ func (s Service) UpdateAuthMethod(ctx context.Context, req *pbs.UpdateAuthMethod if authResults.Error != nil { return nil, authResults.Error } - u, err := s.updateInRepo(ctx, authResults.Scope.GetId(), req.GetId(), req.GetUpdateMask().GetPaths(), req.GetItem()) + u, err := s.updateInRepo(ctx, authResults.Scope.GetId(), req) if err != nil { return nil, err } u.Scope = authResults.Scope - u.AuthorizedActions = authResults.FetchActionSetForId(ctx, u.Id, IdActions).Strings() + u.AuthorizedActions = authResults.FetchActionSetForId(ctx, u.Id, IdActions[auth.SubtypeFromId(u.Id)]).Strings() if err := populateCollectionAuthorizedActions(ctx, authResults, u); err != nil { return nil, err } return &pbs.UpdateAuthMethodResponse{Item: u}, nil } +// ChangeState implements the interface pbs.AuthMethodServiceServer. +func (s Service) ChangeState(ctx context.Context, req *pbs.ChangeStateRequest) (*pbs.ChangeStateResponse, error) { + if err := validateChangeStateRequest(req); err != nil { + return nil, err + } + authResults := s.authResult(ctx, req.GetId(), action.ChangeState) + if authResults.Error != nil { + return nil, authResults.Error + } + am, err := s.changeStateInRepo(ctx, req) + if err != nil { + return nil, err + } + am.Scope = authResults.Scope + am.AuthorizedActions = authResults.FetchActionSetForId(ctx, am.Id, IdActions[auth.OidcSubtype]).Strings() + if err := populateCollectionAuthorizedActions(ctx, authResults, am); err != nil { + return nil, err + } + return &pbs.ChangeStateResponse{Item: am}, nil +} + // DeleteAuthMethod implements the interface pbs.AuthMethodServiceServer. func (s Service) DeleteAuthMethod(ctx context.Context, req *pbs.DeleteAuthMethodRequest) (*pbs.DeleteAuthMethodResponse, error) { if err := validateDeleteRequest(req); err != nil { @@ -259,24 +280,35 @@ func (s Service) DeleteAuthMethod(ctx context.Context, req *pbs.DeleteAuthMethod // Authenticate implements the interface pbs.AuthenticationServiceServer. func (s Service) Authenticate(ctx context.Context, req *pbs.AuthenticateRequest) (*pbs.AuthenticateResponse, error) { + const op = "authmethod_service.(Service).Authenticate" if err := validateAuthenticateRequest(req); err != nil { return nil, err } + + switch auth.SubtypeFromId(req.GetAuthMethodId()) { + case auth.PasswordSubtype: + if err := validateAuthenticatePasswordRequest(req); err != nil { + return nil, err + } + case auth.OidcSubtype: + if err := validateAuthenticateOidcRequest(req); err != nil { + return nil, err + } + } + authResults := s.authResult(ctx, req.GetAuthMethodId(), action.Authenticate) if authResults.Error != nil { return nil, authResults.Error } - attrs := req.GetAttributes().GetFields() - tok, err := s.authenticateWithRepo(ctx, authResults.Scope.GetId(), req.GetAuthMethodId(), attrs[loginNameKey].GetStringValue(), attrs[pwKey].GetStringValue()) - if err != nil { - return nil, err + + switch auth.SubtypeFromId(req.GetAuthMethodId()) { + case auth.PasswordSubtype: + return s.authenticatePassword(ctx, req, &authResults) + + case auth.OidcSubtype: + return s.authenticateOidc(ctx, req, &authResults) } - res := &perms.Resource{ - ScopeId: authResults.Scope.Id, - Type: resource.AuthToken, - } - tok.AuthorizedActions = authResults.FetchActionSetForId(ctx, tok.Id, authtokens.IdActions, auth.WithResource(res)).Strings() - return &pbs.AuthenticateResponse{Item: tok, TokenType: req.GetTokenType()}, nil + return nil, errors.New(errors.Internal, op, "Invalid auth method subtype not caught in validation function.") } // Deprecated: use Authenticate @@ -289,7 +321,7 @@ func (s Service) AuthenticateLogin(ctx context.Context, req *pbs.AuthenticateLog return nil, authResults.Error } creds := req.GetCredentials().GetFields() - tok, err := s.authenticateWithRepo(ctx, authResults.Scope.GetId(), req.GetAuthMethodId(), creds[loginNameKey].GetStringValue(), creds[pwKey].GetStringValue()) + tok, err := s.authenticateWithPwRepo(ctx, authResults.Scope.GetId(), req.GetAuthMethodId(), creds[loginNameField].GetStringValue(), creds[passwordField].GetStringValue()) if err != nil { return nil, err } @@ -302,34 +334,67 @@ func (s Service) AuthenticateLogin(ctx context.Context, req *pbs.AuthenticateLog } func (s Service) getFromRepo(ctx context.Context, id string) (*pb.AuthMethod, error) { - repo, err := s.pwRepoFn() - if err != nil { - return nil, err + var lookupErr error + var am auth.AuthMethod + switch auth.SubtypeFromId(id) { + case auth.PasswordSubtype: + repo, err := s.pwRepoFn() + if err != nil { + return nil, err + } + am, lookupErr = repo.LookupAuthMethod(ctx, id) + + case auth.OidcSubtype: + repo, err := s.oidcRepoFn() + if err != nil { + return nil, err + } + am, lookupErr = repo.LookupAuthMethod(ctx, id) + + default: + return nil, handlers.NotFoundErrorf("Unrecognized id.") } - u, err := repo.LookupAuthMethod(ctx, id) - if err != nil { - if errors.IsNotFoundError(err) { + + if lookupErr != nil { + if errors.IsNotFoundError(lookupErr) { return nil, handlers.NotFoundErrorf("AuthMethod %q doesn't exist.", id) } - return nil, err + return nil, lookupErr } - if u == nil { + if am == nil { return nil, handlers.NotFoundErrorf("AuthMethod %q doesn't exist.", id) } - return toAuthMethodProto(u) + + return toAuthMethodProto(am) } func (s Service) listFromRepo(ctx context.Context, scopeIds []string) ([]*pb.AuthMethod, error) { - repo, err := s.pwRepoFn() + oidcRepo, err := s.oidcRepoFn() if err != nil { return nil, err } - ul, err := repo.ListAuthMethods(ctx, scopeIds) + ol, err := oidcRepo.ListAuthMethods(ctx, scopeIds) if err != nil { return nil, err } var outUl []*pb.AuthMethod - for _, u := range ul { + for _, u := range ol { + ou, err := toAuthMethodProto(u) + if err != nil { + return nil, err + } + outUl = append(outUl, ou) + } + + repo, err := s.pwRepoFn() + if err != nil { + return nil, err + } + pl, err := repo.ListAuthMethods(ctx, scopeIds) + if err != nil { + return nil, err + } + for _, u := range pl { ou, err := toAuthMethodProto(u) if err != nil { return nil, err @@ -340,147 +405,146 @@ func (s Service) listFromRepo(ctx context.Context, scopeIds []string) ([]*pb.Aut } func (s Service) createInRepo(ctx context.Context, scopeId string, item *pb.AuthMethod) (*pb.AuthMethod, error) { - var opts []password.Option - if item.GetName() != nil { - opts = append(opts, password.WithName(item.GetName().GetValue())) - } - if item.GetDescription() != nil { - opts = append(opts, password.WithDescription(item.GetDescription().GetValue())) - } - u, err := password.NewAuthMethod(scopeId, opts...) - if err != nil { - return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to build auth method for creation: %v.", err) - } - repo, err := s.pwRepoFn() - if err != nil { - return nil, err - } - out, err := repo.CreateAuthMethod(ctx, u) - if err != nil { - return nil, fmt.Errorf("unable to create auth method: %w", err) - } - if out == nil { - return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to create auth method but no error returned from repository.") + var out auth.AuthMethod + switch auth.SubtypeFromType(item.GetType()) { + case auth.PasswordSubtype: + am, err := s.createPwInRepo(ctx, scopeId, item) + if err != nil { + return nil, err + } + if am == nil { + return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to create auth method but no error returned from repository.") + } + out = am + case auth.OidcSubtype: + am, err := s.createOidcInRepo(ctx, scopeId, item) + if err != nil { + return nil, err + } + if am == nil { + return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to create auth method but no error returned from repository.") + } + out = am } return toAuthMethodProto(out) } -func (s Service) updateInRepo(ctx context.Context, scopeId, id string, mask []string, item *pb.AuthMethod) (*pb.AuthMethod, error) { - var opts []password.Option - if desc := item.GetDescription(); desc != nil { - opts = append(opts, password.WithDescription(desc.GetValue())) +func (s Service) updateInRepo(ctx context.Context, scopeId string, req *pbs.UpdateAuthMethodRequest) (*pb.AuthMethod, error) { + const op = "authmethod_service.(Service).updateInRepo" + storageToWire := toAuthMethodProto + var out auth.AuthMethod + switch auth.SubtypeFromId(req.GetId()) { + case auth.PasswordSubtype: + am, err := s.updatePwInRepo(ctx, scopeId, req.GetId(), req.GetUpdateMask().GetPaths(), req.GetItem()) + if err != nil { + return nil, err + } + if am == nil { + return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to update auth method but no error returned from repository.") + } + out = am + case auth.OidcSubtype: + am, dryRun, err := s.updateOidcInRepo(ctx, scopeId, req) + if err != nil { + return nil, err + } + if am == nil { + return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to update auth method but no error returned from repository.") + } + if dryRun { + storageToWire = func(in auth.AuthMethod) (*pb.AuthMethod, error) { + am, err := toAuthMethodProto(in) + if err != nil { + return nil, errors.Wrap(err, op) + } + attrs := &pb.OidcAuthMethodAttributes{} + if err := handlers.StructToProto(am.GetAttributes(), attrs); err != nil { + return nil, errors.Wrap(err, op, errors.WithMsg("can't convert from attribute struct to proto")) + } + attrs.DryRun = true + if am.Attributes, err = handlers.ProtoToStruct(attrs); err != nil { + return nil, errors.Wrap(err, op, errors.WithMsg("can't convert from attribute proto to struct")) + } + return am, nil + } + } + out = am } - if name := item.GetName(); name != nil { - opts = append(opts, password.WithName(name.GetValue())) - } - u, err := password.NewAuthMethod(scopeId, opts...) - if err != nil { - return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to build auth method for update: %v.", err) - } - - pwAttrs := &pb.PasswordAuthMethodAttributes{} - if err := handlers.StructToProto(item.GetAttributes(), pwAttrs); err != nil { - return nil, handlers.InvalidArgumentErrorf("Error in provided request.", - map[string]string{"attributes": "Attribute fields do not match the expected format."}) - } - if pwAttrs.GetMinLoginNameLength() != 0 { - u.MinLoginNameLength = pwAttrs.GetMinLoginNameLength() - } - if pwAttrs.GetMinPasswordLength() != 0 { - u.MinPasswordLength = pwAttrs.GetMinPasswordLength() - } - version := item.GetVersion() - - u.PublicId = id - dbMask := maskManager.Translate(mask) - if len(dbMask) == 0 { - return nil, handlers.InvalidArgumentErrorf("No valid fields included in the update mask.", map[string]string{"update_mask": "No valid fields provided in the update mask."}) - } - repo, err := s.pwRepoFn() - if err != nil { - return nil, err - } - out, rowsUpdated, err := repo.UpdateAuthMethod(ctx, u, version, dbMask) - if err != nil { - return nil, fmt.Errorf("unable to update auth method: %w", err) - } - if rowsUpdated == 0 { - return nil, handlers.NotFoundErrorf("AuthMethod %q doesn't exist or incorrect version provided.", id) - } - return toAuthMethodProto(out) + return storageToWire(out) } func (s Service) deleteFromRepo(ctx context.Context, scopeId, id string) (bool, error) { - repo, err := s.pwRepoFn() - if err != nil { - return false, err + var rows int + var dErr error + switch auth.SubtypeFromId(id) { + case auth.PasswordSubtype: + repo, err := s.pwRepoFn() + if err != nil { + return false, err + } + rows, dErr = repo.DeleteAuthMethod(ctx, scopeId, id) + + case auth.OidcSubtype: + repo, err := s.oidcRepoFn() + if err != nil { + return false, err + } + rows, dErr = repo.DeleteAuthMethod(ctx, id) } - rows, err := repo.DeleteAuthMethod(ctx, scopeId, id) - if err != nil { - if errors.IsNotFoundError(err) { + + if dErr != nil { + if errors.IsNotFoundError(dErr) { return false, nil } - return false, fmt.Errorf("unable to delete auth method: %w", err) + return false, fmt.Errorf("unable to delete auth method: %w", dErr) } + return rows > 0, nil } -func (s Service) authenticateWithRepo(ctx context.Context, scopeId, authMethodId, loginName, pw string) (*pba.AuthToken, error) { - iamRepo, err := s.iamRepoFn() - if err != nil { - return nil, err - } - atRepo, err := s.atRepoFn() - if err != nil { - return nil, err - } - pwRepo, err := s.pwRepoFn() - if err != nil { - return nil, err +func (s Service) changeStateInRepo(ctx context.Context, req *pbs.ChangeStateRequest) (*pb.AuthMethod, error) { + const op = "authmethod_service.(Service).changeStateInRepo" + + switch auth.SubtypeFromId(req.GetId()) { + case auth.OidcSubtype: + repo, err := s.oidcRepoFn() + if err != nil { + return nil, err + } + + attrs := &pbs.OidcChangeStateAttributes{} + if err := handlers.StructToProto(req.GetAttributes(), attrs); err != nil { + return nil, errors.Wrap(err, op, errors.WithMsg("unable to parse attributes")) + } + + var opts []oidc.Option + if attrs.GetOverrideOidcDiscoveryUrlConfig() { + opts = append(opts, oidc.WithForce()) + } + + var am *oidc.AuthMethod + switch oidcStateMap[attrs.GetState()] { + case inactiveState: + am, err = repo.MakeInactive(ctx, req.GetId(), req.GetVersion()) + case privateState: + am, err = repo.MakePrivate(ctx, req.GetId(), req.GetVersion(), opts...) + case publicState: + am, err = repo.MakePublic(ctx, req.GetId(), req.GetVersion(), opts...) + default: + err = errors.New(errors.InvalidParameter, op, fmt.Sprintf("unrecognized state %q", attrs.GetState())) + } + if err != nil { + return nil, err + } + + return toAuthMethodProto(am) } - acct, err := pwRepo.Authenticate(ctx, scopeId, authMethodId, loginName, pw) - if err != nil { - return nil, err - } - if acct == nil { - return nil, handlers.ApiErrorWithCodeAndMessage(codes.Unauthenticated, "Unable to authenticate.") - } - - u, err := iamRepo.LookupUserWithLogin(ctx, acct.GetPublicId(), iam.WithAutoVivify(true)) - if err != nil { - return nil, err - } - tok, err := atRepo.CreateAuthToken(ctx, u, acct.GetPublicId()) - if err != nil { - return nil, err - } - - token, err := authtoken.EncryptToken(ctx, s.kms, scopeId, tok.GetPublicId(), tok.GetToken()) - if err != nil { - return nil, err - } - - tok.Token = tok.GetPublicId() + "_" + token - prot := toAuthTokenProto(tok) - - scp, err := iamRepo.LookupScope(ctx, u.GetScopeId()) - if err != nil { - return nil, err - } - if scp == nil { - return nil, err - } - prot.Scope = &scopes.ScopeInfo{ - Id: scp.GetPublicId(), - Type: scp.GetType(), - ParentScopeId: scp.GetParentId(), - } - - return prot, nil + return nil, errors.New(errors.InvalidParameter, op, "Given auth method type does not support changing state") } func (s Service) authResult(ctx context.Context, id string, a action.Type) auth.VerifyResults { + const op = "authmethods.(Service).authResult" res := auth.VerifyResults{} var parentId string @@ -503,18 +567,42 @@ func (s Service) authResult(ctx context.Context, id string, a action.Type) auth. return res } default: - repo, err := s.pwRepoFn() - if err != nil { - res.Error = err - return res - } - authMeth, err := repo.LookupAuthMethod(ctx, id) - if err != nil { - res.Error = err - return res - } - if authMeth == nil { - res.Error = handlers.NotFoundError() + var authMeth auth.AuthMethod + switch auth.SubtypeFromId(id) { + case auth.PasswordSubtype: + repo, err := s.pwRepoFn() + if err != nil { + res.Error = err + return res + } + am, err := repo.LookupAuthMethod(ctx, id) + if err != nil { + res.Error = err + return res + } + if am == nil { + res.Error = handlers.NotFoundError() + return res + } + authMeth = am + case auth.OidcSubtype: + repo, err := s.oidcRepoFn() + if err != nil { + res.Error = err + return res + } + am, err := repo.LookupAuthMethod(ctx, id) + if err != nil { + res.Error = err + return res + } + if am == nil { + res.Error = handlers.NotFoundError() + return res + } + authMeth = am + default: + res.Error = errors.New(errors.InvalidPublicId, op, "unrecognized auth method type") return res } parentId = authMeth.GetScopeId() @@ -524,14 +612,14 @@ func (s Service) authResult(ctx context.Context, id string, a action.Type) auth. return auth.Verify(ctx, opts...) } -func toAuthMethodProto(in *password.AuthMethod) (*pb.AuthMethod, error) { - out := pb.AuthMethod{ +func toAuthMethodProto(in auth.AuthMethod) (*pb.AuthMethod, error) { + out := &pb.AuthMethod{ Id: in.GetPublicId(), ScopeId: in.GetScopeId(), CreatedTime: in.GetCreateTime().GetTimestamp(), UpdatedTime: in.GetUpdateTime().GetTimestamp(), Version: in.GetVersion(), - Type: auth.PasswordSubtype.String(), + IsPrimary: in.GetIsPrimaryAuthMethod(), } if in.GetDescription() != "" { out.Description = wrapperspb.String(in.GetDescription()) @@ -539,15 +627,52 @@ func toAuthMethodProto(in *password.AuthMethod) (*pb.AuthMethod, error) { if in.GetName() != "" { out.Name = wrapperspb.String(in.GetName()) } - st, err := handlers.ProtoToStruct(&pb.PasswordAuthMethodAttributes{ - MinLoginNameLength: in.GetMinLoginNameLength(), - MinPasswordLength: in.GetMinPasswordLength(), - }) - if err != nil { - return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "failed building password attribute struct: %v", err) + switch i := in.(type) { + case *password.AuthMethod: + out.Type = auth.PasswordSubtype.String() + st, err := handlers.ProtoToStruct(&pb.PasswordAuthMethodAttributes{ + MinLoginNameLength: i.GetMinLoginNameLength(), + MinPasswordLength: i.GetMinPasswordLength(), + }) + if err != nil { + return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "failed building password attribute struct: %v", err) + } + out.Attributes = st + case *oidc.AuthMethod: + out.Type = auth.OidcSubtype.String() + attrs := &pb.OidcAuthMethodAttributes{ + ClientId: wrapperspb.String(i.GetClientId()), + ClientSecretHmac: i.ClientSecretHmac, + IdpCaCerts: i.GetCertificates(), + State: i.GetOperationalState(), + SigningAlgorithms: i.GetSigningAlgs(), + AllowedAudiences: i.GetAudClaims(), + } + if i.DisableDiscoveredConfigValidation { + attrs.DisableDiscoveredConfigValidation = true + } + if i.GetIssuer() != "" { + attrs.Issuer = wrapperspb.String(i.Issuer) + } + if len(i.GetApiUrl()) > 0 { + attrs.ApiUrlPrefix = wrapperspb.String(i.GetApiUrl()) + attrs.CallbackUrl = fmt.Sprintf("%s/v1/auth-methods/%s:authenticate:callback", i.GetApiUrl(), i.GetPublicId()) + } + switch i.GetMaxAge() { + case 0: + case -1: + attrs.MaxAge = wrapperspb.UInt32(0) + default: + attrs.MaxAge = wrapperspb.UInt32(uint32(i.GetMaxAge())) + } + + st, err := handlers.ProtoToStruct(attrs) + if err != nil { + return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "failed building oidc attribute struct: %v", err) + } + out.Attributes = st } - out.Attributes = st - return &out, nil + return out, nil } func toAuthTokenProto(t *authtoken.AuthToken) *pba.AuthToken { @@ -570,57 +695,210 @@ func toAuthTokenProto(t *authtoken.AuthToken) *pba.AuthToken { // * All required parameters are set // * There are no conflicting parameters provided func validateGetRequest(req *pbs.GetAuthMethodRequest) error { - return handlers.ValidateGetRequest(password.AuthMethodPrefix, req, handlers.NoopValidatorFn) + const op = "authmethod_service.validateGetRequest" + if req == nil { + return errors.New(errors.InvalidParameter, op, "nil request") + } + return handlers.ValidateGetRequest(handlers.NoopValidatorFn, req, password.AuthMethodPrefix, oidc.AuthMethodPrefix) } func validateCreateRequest(req *pbs.CreateAuthMethodRequest) error { + const op = "authmethod_service.validateCreateRequest" + if req == nil { + return errors.New(errors.InvalidParameter, op, "nil request") + } return handlers.ValidateCreateRequest(req.GetItem(), func() map[string]string { badFields := map[string]string{} - if !handlers.ValidId(scope.Org.Prefix(), req.GetItem().GetScopeId()) && + if !handlers.ValidId(handlers.Id(req.GetItem().GetScopeId()), scope.Org.Prefix()) && scope.Global.String() != req.GetItem().GetScopeId() { - badFields["scope_id"] = "This field must be 'global' or a valid org scope id." + badFields[scopeIdField] = "This field must be 'global' or a valid org scope id." + } + if req.GetItem().GetIsPrimary() { + badFields[isPrimaryField] = "This field is read only." } switch auth.SubtypeFromType(req.GetItem().GetType()) { case auth.PasswordSubtype: - pwAttrs := &pb.PasswordAuthMethodAttributes{} - if err := handlers.StructToProto(req.GetItem().GetAttributes(), pwAttrs); err != nil { - badFields["attributes"] = "Attribute fields do not match the expected format." + attrs := &pb.PasswordAuthMethodAttributes{} + if err := handlers.StructToProto(req.GetItem().GetAttributes(), attrs); err != nil { + badFields[attributesField] = "Attribute fields do not match the expected format." + } + case auth.OidcSubtype: + attrs := &pb.OidcAuthMethodAttributes{} + if err := handlers.StructToProto(req.GetItem().GetAttributes(), attrs); err != nil { + badFields[attributesField] = "Attribute fields do not match the expected format." + } else { + if attrs.GetIssuer().GetValue() != "" { + iss, err := url.Parse(attrs.GetIssuer().GetValue()) + if err != nil { + badFields[issuerField] = fmt.Sprintf("Cannot be parsed as a url. %v", err) + } + if !strutil.StrListContains([]string{"http", "https"}, iss.Scheme) { + badFields[issuerField] = fmt.Sprintf("Must have schema %q or %q specified", "http", "https") + } + } + if attrs.GetDisableDiscoveredConfigValidation() { + badFields[disableDiscoveredConfigValidationField] = "Field is not allowed at create time." + } + if attrs.GetClientId().GetValue() == "" { + badFields[clientIdField] = "Field required for creating an OIDC auth method." + } + if attrs.GetClientSecret().GetValue() == "" { + badFields[clientSecretField] = "Field required for creating an OIDC auth method." + } + if attrs.GetClientSecretHmac() != "" { + badFields[clientSecretHmacField] = "Field is read only." + } + if attrs.GetState() != "" { + badFields[stateField] = "Field is read only." + } + if attrs.GetCallbackUrl() != "" { + badFields[callbackUrlField] = "Field is read only." + } + if len(attrs.GetSigningAlgorithms()) > 0 { + for _, sa := range attrs.GetSigningAlgorithms() { + if !oidc.SupportedAlgorithm(oidc.Alg(sa)) { + badFields[signingAlgorithmField] = fmt.Sprintf("Contains unsupported algorithm %q", sa) + break + } + } + } + if strings.TrimSpace(attrs.GetApiUrlPrefix().GetValue()) == "" { + // TODO: When we start accepting the address used in the request make this an optional field. + badFields[apiUrlPrefixField] = "This field is required." + } else { + if cu, err := url.Parse(attrs.GetApiUrlPrefix().GetValue()); err != nil || (cu.Scheme != "http" && cu.Scheme != "https") || cu.Host == "" { + badFields[apiUrlPrefixField] = fmt.Sprintf("%q cannot be parsed as a url.", attrs.GetApiUrlPrefix().GetValue()) + } + } + if len(attrs.GetIdpCaCerts()) > 0 { + if _, err := oidc.ParseCertificates(attrs.GetIdpCaCerts()...); err != nil { + badFields[caCertsField] = fmt.Sprintf("Cannot parse CA certificates. %v", err.Error()) + } + } } default: - badFields["type"] = fmt.Sprintf("This is a required field and must be %q.", auth.PasswordSubtype.String()) + badFields[typeField] = fmt.Sprintf("This is a required field and must be %q.", auth.PasswordSubtype.String()) } return badFields }) } func validateUpdateRequest(req *pbs.UpdateAuthMethodRequest) error { - return handlers.ValidateUpdateRequest(password.AuthMethodPrefix, req, req.GetItem(), func() map[string]string { + const op = "authmethod_service.validateUpdateRequest" + if req == nil { + return errors.New(errors.InvalidParameter, op, "nil request") + } + return handlers.ValidateUpdateRequest(req, req.GetItem(), func() map[string]string { badFields := map[string]string{} + if handlers.MaskContains(req.GetUpdateMask().GetPaths(), isPrimaryField) { + badFields[isPrimaryField] = "This field is read only." + } switch auth.SubtypeFromId(req.GetId()) { case auth.PasswordSubtype: if req.GetItem().GetType() != "" && auth.SubtypeFromType(req.GetItem().GetType()) != auth.PasswordSubtype { - badFields["type"] = "Cannot modify the resource type." + badFields[typeField] = "Cannot modify the resource type." } - pwAttrs := &pb.PasswordAuthMethodAttributes{} - if err := handlers.StructToProto(req.GetItem().GetAttributes(), pwAttrs); err != nil { - badFields["attributes"] = "Attribute fields do not match the expected format." + attrs := &pb.PasswordAuthMethodAttributes{} + if err := handlers.StructToProto(req.GetItem().GetAttributes(), attrs); err != nil { + badFields[attributesField] = "Attribute fields do not match the expected format." + } + case auth.OidcSubtype: + if req.GetItem().GetType() != "" && auth.SubtypeFromType(req.GetItem().GetType()) != auth.OidcSubtype { + badFields[typeField] = "Cannot modify the resource type." + } + attrs := &pb.OidcAuthMethodAttributes{} + if err := handlers.StructToProto(req.GetItem().GetAttributes(), attrs); err != nil { + badFields[attributesField] = "Attribute fields do not match the expected format." + } + if attrs.DryRun && attrs.DisableDiscoveredConfigValidation { + badFields[disableDiscoveredConfigValidationField] = "This field cannot be set to true with dry_run." + } + + if handlers.MaskContains(req.GetUpdateMask().GetPaths(), apiUrlPrefixField) { + // TODO: When we start accepting the address used in the request make this an optional field. + if strings.TrimSpace(attrs.GetApiUrlPrefix().GetValue()) == "" { + badFields[apiUrlPrefixField] = "This field should not be set to empty." + } + if cu, err := url.Parse(attrs.GetApiUrlPrefix().GetValue()); err != nil || (cu.Scheme != "http" && cu.Scheme != "https") || cu.Host == "" { + badFields[apiUrlPrefixField] = fmt.Sprintf("%q cannot be parsed as a url.", attrs.GetApiUrlPrefix().GetValue()) + } + } + if handlers.MaskContains(req.GetUpdateMask().GetPaths(), issuerField) { + if attrs.GetIssuer().GetValue() != "" { + iss, err := url.Parse(attrs.GetIssuer().GetValue()) + if err != nil { + badFields[issuerField] = fmt.Sprintf("Cannot be parsed as a url. %v", err) + } + if !strutil.StrListContains([]string{"http", "https"}, iss.Scheme) { + badFields[issuerField] = fmt.Sprintf("Must have schema %q or %q specified", "http", "https") + } + } + } + if handlers.MaskContains(req.GetUpdateMask().GetPaths(), apiUrlPrefixField) { + if attrs.GetApiUrlPrefix().GetValue() != "" { + cu, err := url.Parse(attrs.GetApiUrlPrefix().GetValue()) + if err != nil || cu.Host == "" { + badFields[apiUrlPrefixField] = fmt.Sprintf("%q cannot be parsed as a url.", attrs.GetApiUrlPrefix().GetValue()) + } + if !strutil.StrListContains([]string{"http", "https"}, cu.Scheme) { + badFields[apiUrlPrefixField] = fmt.Sprintf("Must have schema %q or %q specified", "http", "https") + } + } + } + if handlers.MaskContains(req.GetUpdateMask().GetPaths(), clientSecretField) && attrs.GetClientSecret().GetValue() == "" { + badFields[clientSecretField] = "Can change but cannot unset this field." + } + if handlers.MaskContains(req.GetUpdateMask().GetPaths(), clientIdField) && attrs.GetClientId().GetValue() == "" { + badFields[clientIdField] = "Can change but cannot unset this field." + } + + if attrs.GetClientSecretHmac() != "" { + badFields[clientSecretHmacField] = "Field is read only." + } + if attrs.GetState() != "" { + badFields[stateField] = "Field is read only." + } + if attrs.GetCallbackUrl() != "" { + badFields[callbackUrlField] = "Field is read only." + } + + if len(attrs.GetSigningAlgorithms()) > 0 { + for _, sa := range attrs.GetSigningAlgorithms() { + if !oidc.SupportedAlgorithm(oidc.Alg(sa)) { + badFields[signingAlgorithmField] = fmt.Sprintf("Contains unsupported algorithm %q", sa) + break + } + } + } + if len(attrs.GetIdpCaCerts()) > 0 { + if _, err := oidc.ParseCertificates(attrs.GetIdpCaCerts()...); err != nil { + badFields[caCertsField] = fmt.Sprintf("Cannot parse CA certificates. %v", err.Error()) + } } default: badFields["id"] = "Incorrectly formatted identifier." } return badFields - }) + }, password.AuthMethodPrefix, oidc.AuthMethodPrefix) } func validateDeleteRequest(req *pbs.DeleteAuthMethodRequest) error { - return handlers.ValidateDeleteRequest(password.AuthMethodPrefix, req, handlers.NoopValidatorFn) + const op = "authmethod_service.validateDeleteRequest" + if req == nil { + return errors.New(errors.InvalidParameter, op, "nil request") + } + return handlers.ValidateDeleteRequest(handlers.NoopValidatorFn, req, password.AuthMethodPrefix, oidc.AuthMethodPrefix) } func validateListRequest(req *pbs.ListAuthMethodsRequest) error { + const op = "authmethod_service.validateListRequest" + if req == nil { + return errors.New(errors.InvalidParameter, op, "nil request") + } badFields := map[string]string{} - if !handlers.ValidId(scope.Org.Prefix(), req.GetScopeId()) && + if !handlers.ValidId(handlers.Id(req.GetScopeId()), scope.Org.Prefix()) && req.GetScopeId() != scope.Global.String() { - badFields["scope_id"] = "This field must be 'global' or a valid org scope id." + badFields[scopeIdField] = "This field must be 'global' or a valid org scope id." } if _, err := handlers.NewFilter(req.GetFilter()); err != nil { badFields["filter"] = fmt.Sprintf("This field could not be parsed. %v", err) @@ -631,34 +909,27 @@ func validateListRequest(req *pbs.ListAuthMethodsRequest) error { return nil } -func validateAuthenticateRequest(req *pbs.AuthenticateRequest) error { +func validateChangeStateRequest(req *pbs.ChangeStateRequest) error { + const op = "authmethod_service.validateChangeStateRequest" + if req == nil { + return errors.New(errors.InvalidParameter, op, "nil request") + } + if st := auth.SubtypeFromId(req.GetId()); st != auth.OidcSubtype { + return handlers.NotFoundErrorf("This endpoint is only available for the %q Auth Method type.", auth.OidcSubtype.String()) + } badFields := make(map[string]string) - if strings.TrimSpace(req.GetAuthMethodId()) == "" { - badFields["auth_method_id"] = "This is a required field." - } else if !handlers.ValidId(password.AuthMethodPrefix, req.GetAuthMethodId()) { - badFields["auth_method_id"] = "Invalid formatted identifier." + if req.GetVersion() == 0 { + badFields[versionField] = "Resource version is required." } - if req.GetAttributes() == nil && req.GetCredentials() != nil { - // TODO: Eventually, remove this - req.Attributes = req.Credentials + + attrs := &pbs.OidcChangeStateAttributes{} + if err := handlers.StructToProto(req.GetAttributes(), attrs); err != nil { + badFields[attributesField] = "Attribute fields do not match the expected format." } - if req.GetCommand() == "" { - // TODO: Eventually, require a command. For now, fall back to "login" for backwards compat. - req.Command = "login" - } - if req.Command != "login" { - badFields["command"] = "Invalid command for this auth method type." - } - attrs := req.GetAttributes().GetFields() - if _, ok := attrs[loginNameKey]; !ok { - badFields["attributes.login_name"] = "This is a required field." - } - if _, ok := attrs[pwKey]; !ok { - badFields["attributes.password"] = "This is a required field." - } - tType := strings.ToLower(strings.TrimSpace(req.GetTokenType())) - if tType != "" && tType != "token" && tType != "cookie" { - badFields["token_type"] = `The only accepted types are "token" and "cookie".` + switch oidcStateMap[attrs.GetState()] { + case inactiveState, privateState, publicState: + default: + badFields[stateField] = fmt.Sprintf("Only supported values are %q, %q, or %q.", inactiveState.String(), privateState.String(), publicState.String()) } if len(badFields) > 0 { return handlers.InvalidArgumentErrorf("Invalid fields provided in request.", badFields) @@ -666,30 +937,133 @@ func validateAuthenticateRequest(req *pbs.AuthenticateRequest) error { return nil } +func validateAuthenticateRequest(req *pbs.AuthenticateRequest) error { + const op = "authmethod_service.validateAuthenticateRequest" + if req == nil { + return errors.New(errors.InvalidParameter, op, "nil request") + } + + badFields := make(map[string]string) + + if strings.TrimSpace(req.GetAuthMethodId()) == "" { + badFields[authMethodIdField] = "This is a required field." + } else { + st := auth.SubtypeFromId(req.GetAuthMethodId()) + switch st { + case auth.PasswordSubtype, auth.OidcSubtype: + default: + badFields[authMethodIdField] = "Unknown auth method type." + } + } + + if len(badFields) > 0 { + return handlers.InvalidArgumentErrorf("Invalid fields provided in request.", badFields) + } + + return nil +} + // Deprecated; remove when AuthenticateLogin is removed func validateAuthenticateLoginRequest(req *pbs.AuthenticateLoginRequest) error { + const op = "authmethod_service.validateAuthenticateLoginRequest" + if req == nil { + return errors.New(errors.InvalidParameter, op, "nil request") + } badFields := make(map[string]string) if strings.TrimSpace(req.GetAuthMethodId()) == "" { - badFields["auth_method_id"] = "This is a required field." - } else if !handlers.ValidId(password.AuthMethodPrefix, req.GetAuthMethodId()) { - badFields["auth_method_id"] = "Invalid formatted identifier." + badFields[authMethodIdField] = "This is a required field." + } else if !handlers.ValidId(handlers.Id(req.GetAuthMethodId()), password.AuthMethodPrefix) { + badFields[authMethodIdField] = "Invalid formatted identifier." } if req.GetCredentials() == nil { badFields["credentials"] = "This is a required field." } creds := req.GetCredentials().GetFields() - if _, ok := creds[loginNameKey]; !ok { + if _, ok := creds[loginNameField]; !ok { badFields["credentials.login_name"] = "This is a required field." } - if _, ok := creds[pwKey]; !ok { + if _, ok := creds[passwordField]; !ok { badFields["credentials.password"] = "This is a required field." } tType := strings.ToLower(strings.TrimSpace(req.GetTokenType())) if tType != "" && tType != "token" && tType != "cookie" { - badFields["token_type"] = `The only accepted types are "token" and "cookie".` + badFields[tokenTypeField] = `The only accepted types are "token" and "cookie".` } if len(badFields) > 0 { return handlers.InvalidArgumentErrorf("Invalid fields provided in request.", badFields) } return nil } + +func (s Service) convertInternalAuthTokenToApiAuthToken(ctx context.Context, tok *authtoken.AuthToken) (*pba.AuthToken, error) { + const op = "authmethod_service.convertInternalAuthTokenToApiAuthToken" + iamRepo, err := s.iamRepoFn() + if err != nil { + return nil, err + } + if tok == nil { + return nil, errors.New(errors.InvalidParameter, op, "Nil auth token.") + } + if tok.Token == "" { + return nil, errors.New(errors.InvalidParameter, op, "Empty token.") + } + if tok.GetPublicId() == "" { + return nil, errors.New(errors.InvalidParameter, op, "Empty token public ID.") + } + if tok.GetScopeId() == "" { + return nil, errors.New(errors.InvalidParameter, op, "Empty token, scope ID.") + } + token, err := authtoken.EncryptToken(ctx, s.kms, tok.GetScopeId(), tok.GetPublicId(), tok.GetToken()) + if err != nil { + return nil, err + } + + tok.Token = tok.GetPublicId() + "_" + token + prot := toAuthTokenProto(tok) + + scp, err := iamRepo.LookupScope(ctx, tok.GetScopeId()) + if err != nil { + return nil, err + } + if scp == nil { + return nil, err + } + prot.Scope = &scopes.ScopeInfo{ + Id: scp.GetPublicId(), + Type: scp.GetType(), + ParentScopeId: scp.GetParentId(), + } + + return prot, nil +} + +func (s Service) convertToAuthenticateResponse(ctx context.Context, req *pbs.AuthenticateRequest, authResults *auth.VerifyResults, tok *pba.AuthToken) (*pbs.AuthenticateResponse, error) { + const op = "authmethod_service.convertToAuthenticateResponse" + if req == nil { + return nil, errors.New(errors.InvalidParameter, op, "Nil request.") + } + if authResults == nil { + return nil, errors.New(errors.InvalidParameter, op, "Nil auth results.") + } + if authResults.Scope == nil { + return nil, errors.New(errors.InvalidParameter, op, "Nil auth results scope.") + } + if authResults.Scope.Id == "" { + return nil, errors.New(errors.InvalidParameter, op, "Missing auth results scope ID.") + } + if tok == nil { + return nil, errors.New(errors.InvalidParameter, op, "Nil auth token.") + } + res := &perms.Resource{ + ScopeId: authResults.Scope.Id, + Type: resource.AuthToken, + } + tok.AuthorizedActions = authResults.FetchActionSetForId(ctx, tok.Id, authtokens.IdActions, auth.WithResource(res)).Strings() + retAttrs, err := handlers.ProtoToStruct(tok) + if err != nil { + return nil, err + } + retAttrs.GetFields()[tokenTypeField] = structpb.NewStringValue(req.GetTokenType()) + + return &pbs.AuthenticateResponse{Command: req.GetCommand(), Attributes: retAttrs}, nil +} diff --git a/internal/servers/controller/handlers/authmethods/authmethod_service_test.go b/internal/servers/controller/handlers/authmethods/authmethod_service_test.go index c00e10320..06098a1cc 100644 --- a/internal/servers/controller/handlers/authmethods/authmethod_service_test.go +++ b/internal/servers/controller/handlers/authmethods/authmethod_service_test.go @@ -2,16 +2,18 @@ package authmethods_test import ( "context" - "errors" "fmt" + "regexp" "strings" "testing" "github.com/google/go-cmp/cmp" "github.com/hashicorp/boundary/internal/auth" + "github.com/hashicorp/boundary/internal/auth/oidc" "github.com/hashicorp/boundary/internal/auth/password" "github.com/hashicorp/boundary/internal/authtoken" "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/errors" pb "github.com/hashicorp/boundary/internal/gen/controller/api/resources/authmethods" scopepb "github.com/hashicorp/boundary/internal/gen/controller/api/resources/scopes" pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services" @@ -19,8 +21,8 @@ import ( "github.com/hashicorp/boundary/internal/kms" "github.com/hashicorp/boundary/internal/servers/controller/handlers" "github.com/hashicorp/boundary/internal/servers/controller/handlers/authmethods" + "github.com/hashicorp/boundary/internal/types/action" "github.com/hashicorp/boundary/internal/types/scope" - "google.golang.org/genproto/protobuf/field_mask" "google.golang.org/grpc/codes" "google.golang.org/protobuf/testing/protocmp" "google.golang.org/protobuf/types/known/structpb" @@ -36,6 +38,22 @@ const ( testLoginName = "default" ) +var ( + pwAuthorizedActions = []string{ + action.Read.String(), + action.Update.String(), + action.Delete.String(), + action.Authenticate.String(), + } + oidcAuthorizedActions = []string{ + action.Read.String(), + action.Update.String(), + action.Delete.String(), + action.ChangeState.String(), + action.Authenticate.String(), + } +) + var authorizedCollectionActions = map[string]*structpb.ListValue{ "accounts": { Values: []*structpb.Value{ @@ -49,22 +67,26 @@ func TestGet(t *testing.T) { conn, _ := db.TestSetup(t, "postgres") rw := db.New(conn) wrapper := db.TestWrapper(t) - kms := kms.TestKms(t, conn, wrapper) + kmsCache := kms.TestKms(t, conn, wrapper) iamRepoFn := func() (*iam.Repository, error) { return iam.TestRepo(t, conn, wrapper), nil } + oidcRepoFn := func() (*oidc.Repository, error) { + return oidc.NewRepository(rw, rw, kmsCache) + } pwRepoFn := func() (*password.Repository, error) { - return password.NewRepository(rw, rw, kms) + return password.NewRepository(rw, rw, kmsCache) } atRepoFn := func() (*authtoken.Repository, error) { - return authtoken.NewRepository(rw, rw, kms) + return authtoken.NewRepository(rw, rw, kmsCache) } iamRepo := iam.TestRepo(t, conn, wrapper) o, _ := iam.TestScopes(t, iamRepo) am := password.TestAuthMethods(t, conn, o.GetPublicId(), 1)[0] + iam.TestSetPrimaryAuthMethod(t, iamRepo, o, am.GetPublicId()) - wantU := &pb.AuthMethod{ + wantPw := &pb.AuthMethod{ Id: am.GetPublicId(), ScopeId: am.GetScopeId(), CreatedTime: am.CreateTime.GetTimestamp(), @@ -80,7 +102,37 @@ func TestGet(t *testing.T) { Type: o.GetType(), ParentScopeId: scope.Global.String(), }, - AuthorizedActions: []string{"read", "update", "delete", "authenticate"}, + IsPrimary: true, + AuthorizedActions: pwAuthorizedActions, + AuthorizedCollectionActions: authorizedCollectionActions, + } + + databaseWrapper, err := kmsCache.GetWrapper(context.Background(), o.GetPublicId(), kms.KeyPurposeDatabase) + require.NoError(t, err) + oidcam := oidc.TestAuthMethod(t, conn, databaseWrapper, o.GetPublicId(), oidc.InactiveState, "alice_rp", "secret", + oidc.WithIssuer(oidc.TestConvertToUrls(t, "https://alice.com")[0]), oidc.WithApiUrl(oidc.TestConvertToUrls(t, "https://api.com")[0])) + + wantOidc := &pb.AuthMethod{ + Id: oidcam.GetPublicId(), + ScopeId: oidcam.GetScopeId(), + CreatedTime: oidcam.CreateTime.GetTimestamp(), + UpdatedTime: oidcam.UpdateTime.GetTimestamp(), + Type: auth.OidcSubtype.String(), + Attributes: &structpb.Struct{Fields: map[string]*structpb.Value{ + "issuer": structpb.NewStringValue("https://alice.com"), + "client_id": structpb.NewStringValue("alice_rp"), + "client_secret_hmac": structpb.NewStringValue(""), + "state": structpb.NewStringValue(string(oidc.InactiveState)), + "api_url_prefix": structpb.NewStringValue("https://api.com"), + "callback_url": structpb.NewStringValue(fmt.Sprintf(oidc.CallbackEndpoint, "https://api.com", oidcam.GetPublicId())), + }}, + Version: 1, + Scope: &scopepb.ScopeInfo{ + Id: o.GetPublicId(), + Type: o.GetType(), + ParentScopeId: scope.Global.String(), + }, + AuthorizedActions: oidcAuthorizedActions, AuthorizedCollectionActions: authorizedCollectionActions, } @@ -92,10 +144,16 @@ func TestGet(t *testing.T) { err error }{ { - name: "Get an Existing AuthMethod", + name: "Get an Existing PW AuthMethod", scopeId: o.GetPublicId(), req: &pbs.GetAuthMethodRequest{Id: am.GetPublicId()}, - res: &pbs.GetAuthMethodResponse{Item: wantU}, + res: &pbs.GetAuthMethodResponse{Item: wantPw}, + }, + { + name: "Get an Existing OIDC AuthMethod", + scopeId: o.GetPublicId(), + req: &pbs.GetAuthMethodRequest{Id: oidcam.GetPublicId()}, + res: &pbs.GetAuthMethodResponse{Item: wantOidc}, }, { name: "Get a non existant AuthMethod", @@ -123,13 +181,19 @@ func TestGet(t *testing.T) { t.Run(tc.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) - s, err := authmethods.NewService(kms, pwRepoFn, iamRepoFn, atRepoFn) + s, err := authmethods.NewService(kmsCache, pwRepoFn, oidcRepoFn, iamRepoFn, atRepoFn) require.NoError(err, "Couldn't create new auth_method service.") got, gErr := s.GetAuthMethod(auth.DisabledAuthTestContext(iamRepoFn, tc.scopeId), tc.req) if tc.err != nil { require.Error(gErr) assert.True(errors.Is(gErr, tc.err), "GetAuthMethod(%+v) got error %v, wanted %v", tc.req, gErr, tc.err) + return + } + require.NoError(gErr) + if _, ok := got.Item.Attributes.Fields["client_secret_hmac"]; ok { + assert.NotEqual("secret", got.Item.Attributes.Fields["client_secret_hmac"]) + got.Item.Attributes.Fields["client_secret_hmac"] = structpb.NewStringValue("") } assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "GetAuthMethod(%q) got response %q, wanted %q", tc.req, got, tc.res) }) @@ -140,15 +204,18 @@ func TestList(t *testing.T) { conn, _ := db.TestSetup(t, "postgres") rw := db.New(conn) wrapper := db.TestWrapper(t) - kms := kms.TestKms(t, conn, wrapper) + kmsCache := kms.TestKms(t, conn, wrapper) iamRepoFn := func() (*iam.Repository, error) { return iam.TestRepo(t, conn, wrapper), nil } + oidcRepoFn := func() (*oidc.Repository, error) { + return oidc.NewRepository(rw, rw, kmsCache) + } pwRepoFn := func() (*password.Repository, error) { - return password.NewRepository(rw, rw, kms) + return password.NewRepository(rw, rw, kmsCache) } atRepoFn := func() (*authtoken.Repository, error) { - return authtoken.NewRepository(rw, rw, kms) + return authtoken.NewRepository(rw, rw, kmsCache) } iamRepo := iam.TestRepo(t, conn, wrapper) @@ -157,6 +224,33 @@ func TestList(t *testing.T) { oWithOtherAuthMethods, _ := iam.TestScopes(t, iamRepo) var wantSomeAuthMethods []*pb.AuthMethod + databaseWrapper, err := kmsCache.GetWrapper(context.Background(), oWithAuthMethods.GetPublicId(), kms.KeyPurposeDatabase) + require.NoError(t, err) + oidcam := oidc.TestAuthMethod(t, conn, databaseWrapper, oWithAuthMethods.GetPublicId(), oidc.InactiveState, "alice_rp", "secret", + oidc.WithIssuer(oidc.TestConvertToUrls(t, "https://alice.com")[0]), oidc.WithApiUrl(oidc.TestConvertToUrls(t, "https://api.com")[0])) + iam.TestSetPrimaryAuthMethod(t, iamRepo, oWithAuthMethods, oidcam.GetPublicId()) + + wantSomeAuthMethods = append(wantSomeAuthMethods, &pb.AuthMethod{ + Id: oidcam.GetPublicId(), + ScopeId: oWithAuthMethods.GetPublicId(), + CreatedTime: oidcam.GetCreateTime().GetTimestamp(), + UpdatedTime: oidcam.GetUpdateTime().GetTimestamp(), + Scope: &scopepb.ScopeInfo{Id: oWithAuthMethods.GetPublicId(), Type: scope.Org.String(), ParentScopeId: scope.Global.String()}, + Version: 1, + Type: auth.OidcSubtype.String(), + Attributes: &structpb.Struct{Fields: map[string]*structpb.Value{ + "issuer": structpb.NewStringValue("https://alice.com"), + "client_id": structpb.NewStringValue("alice_rp"), + "client_secret_hmac": structpb.NewStringValue(""), + "state": structpb.NewStringValue(string(oidc.InactiveState)), + "api_url_prefix": structpb.NewStringValue("https://api.com"), + "callback_url": structpb.NewStringValue(fmt.Sprintf(oidc.CallbackEndpoint, "https://api.com", oidcam.GetPublicId())), + }}, + IsPrimary: true, + AuthorizedActions: oidcAuthorizedActions, + AuthorizedCollectionActions: authorizedCollectionActions, + }) + for _, am := range password.TestAuthMethods(t, conn, oWithAuthMethods.GetPublicId(), 3) { wantSomeAuthMethods = append(wantSomeAuthMethods, &pb.AuthMethod{ Id: am.GetPublicId(), @@ -170,7 +264,7 @@ func TestList(t *testing.T) { "min_password_length": structpb.NewNumberValue(8), "min_login_name_length": structpb.NewNumberValue(3), }}, - AuthorizedActions: []string{"read", "update", "delete", "authenticate"}, + AuthorizedActions: pwAuthorizedActions, AuthorizedCollectionActions: authorizedCollectionActions, }) } @@ -189,7 +283,7 @@ func TestList(t *testing.T) { "min_password_length": structpb.NewNumberValue(8), "min_login_name_length": structpb.NewNumberValue(3), }}, - AuthorizedActions: []string{"read", "update", "delete", "authenticate"}, + AuthorizedActions: pwAuthorizedActions, AuthorizedCollectionActions: authorizedCollectionActions, }) } @@ -249,15 +343,22 @@ func TestList(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) - s, err := authmethods.NewService(kms, pwRepoFn, iamRepoFn, atRepoFn) + s, err := authmethods.NewService(kmsCache, pwRepoFn, oidcRepoFn, iamRepoFn, atRepoFn) require.NoError(err, "Couldn't create new auth_method service.") got, gErr := s.ListAuthMethods(auth.DisabledAuthTestContext(iamRepoFn, tc.req.GetScopeId()), tc.req) if tc.err != nil { require.Error(gErr) assert.True(errors.Is(gErr, tc.err), "ListAuthMethods() for scope %q got error %v, wanted %v", tc.req.GetScopeId(), gErr, tc.err) - } else { - require.NoError(gErr) + return + } + require.NoError(gErr) + for i, g := range got.Items { + if _, ok := g.Attributes.Fields["client_secret_hmac"]; ok { + assert.NotEqual("secret", g.Attributes.Fields["client_secret_hmac"]) + delete(g.Attributes.Fields, "client_secret_hmac") + delete(tc.res.Items[i].Attributes.Fields, "client_secret_hmac") + } } assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "ListAuthMethods() for scope %q got response %q, wanted %q", tc.req.GetScopeId(), got, tc.res) }) @@ -268,22 +369,30 @@ func TestDelete(t *testing.T) { conn, _ := db.TestSetup(t, "postgres") rw := db.New(conn) wrapper := db.TestWrapper(t) - kms := kms.TestKms(t, conn, wrapper) + kmsCache := kms.TestKms(t, conn, wrapper) iamRepoFn := func() (*iam.Repository, error) { return iam.TestRepo(t, conn, wrapper), nil } + oidcRepoFn := func() (*oidc.Repository, error) { + return oidc.NewRepository(rw, rw, kmsCache) + } pwRepoFn := func() (*password.Repository, error) { - return password.NewRepository(rw, rw, kms) + return password.NewRepository(rw, rw, kmsCache) } atRepoFn := func() (*authtoken.Repository, error) { - return authtoken.NewRepository(rw, rw, kms) + return authtoken.NewRepository(rw, rw, kmsCache) } iamRepo := iam.TestRepo(t, conn, wrapper) o, _ := iam.TestScopes(t, iamRepo) - am := password.TestAuthMethods(t, conn, o.GetPublicId(), 1)[0] + pwam := password.TestAuthMethods(t, conn, o.GetPublicId(), 1)[0] - s, err := authmethods.NewService(kms, pwRepoFn, iamRepoFn, atRepoFn) + databaseWrapper, err := kmsCache.GetWrapper(context.Background(), o.GetPublicId(), kms.KeyPurposeDatabase) + require.NoError(t, err) + oidcam := oidc.TestAuthMethod(t, conn, databaseWrapper, o.GetPublicId(), oidc.InactiveState, "alice_rp", "my-dogs-name", + oidc.WithIssuer(oidc.TestConvertToUrls(t, "https://alice.com")[0]), oidc.WithApiUrl(oidc.TestConvertToUrls(t, "https://api.com")[0])) + + s, err := authmethods.NewService(kmsCache, pwRepoFn, oidcRepoFn, iamRepoFn, atRepoFn) require.NoError(t, err, "Error when getting new auth_method service.") cases := []struct { @@ -293,9 +402,16 @@ func TestDelete(t *testing.T) { err error }{ { - name: "Delete an Existing AuthMethod", + name: "Delete an Existing PW AuthMethod", req: &pbs.DeleteAuthMethodRequest{ - Id: am.GetPublicId(), + Id: pwam.GetPublicId(), + }, + res: &pbs.DeleteAuthMethodResponse{}, + }, + { + name: "Delete an Existing OIDC AuthMethod", + req: &pbs.DeleteAuthMethodRequest{ + Id: oidcam.GetPublicId(), }, res: &pbs.DeleteAuthMethodResponse{}, }, @@ -336,6 +452,9 @@ func TestDelete_twice(t *testing.T) { iamRepoFn := func() (*iam.Repository, error) { return iam.TestRepo(t, conn, wrapper), nil } + oidcRepoFn := func() (*oidc.Repository, error) { + return oidc.NewRepository(rw, rw, kms) + } pwRepoFn := func() (*password.Repository, error) { return password.NewRepository(rw, rw, kms) } @@ -347,7 +466,7 @@ func TestDelete_twice(t *testing.T) { o, _ := iam.TestScopes(t, iamRepo) am := password.TestAuthMethods(t, conn, o.GetPublicId(), 1)[0] - s, err := authmethods.NewService(kms, pwRepoFn, iamRepoFn, atRepoFn) + s, err := authmethods.NewService(kms, pwRepoFn, oidcRepoFn, iamRepoFn, atRepoFn) require.NoError(err, "Error when getting new auth_method service.") req := &pbs.DeleteAuthMethodRequest{ @@ -371,6 +490,9 @@ func TestCreate(t *testing.T) { pwRepoFn := func() (*password.Repository, error) { return password.NewRepository(rw, rw, kms) } + oidcRepoFn := func() (*oidc.Repository, error) { + return oidc.NewRepository(rw, rw, kms) + } atRepoFn := func() (*authtoken.Repository, error) { return authtoken.NewRepository(rw, rw, kms) } @@ -381,19 +503,21 @@ func TestCreate(t *testing.T) { defaultCreated := defaultAm.GetCreateTime().GetTimestamp() cases := []struct { - name string - req *pbs.CreateAuthMethodRequest - res *pbs.CreateAuthMethodResponse - err error + name string + req *pbs.CreateAuthMethodRequest + res *pbs.CreateAuthMethodResponse + idPrefix string + err error }{ { - name: "Create a valid AuthMethod", + name: "Create a valid Password AuthMethod", req: &pbs.CreateAuthMethodRequest{Item: &pb.AuthMethod{ ScopeId: o.GetPublicId(), Name: &wrapperspb.StringValue{Value: "name"}, Description: &wrapperspb.StringValue{Value: "desc"}, Type: "password", }}, + idPrefix: password.AuthMethodPrefix + "_", res: &pbs.CreateAuthMethodResponse{ Uri: fmt.Sprintf("auth-methods/%s_", password.AuthMethodPrefix), Item: &pb.AuthMethod{ @@ -410,19 +534,64 @@ func TestCreate(t *testing.T) { "min_password_length": structpb.NewNumberValue(8), "min_login_name_length": structpb.NewNumberValue(3), }}, - AuthorizedActions: []string{"read", "update", "delete", "authenticate"}, + AuthorizedActions: pwAuthorizedActions, AuthorizedCollectionActions: authorizedCollectionActions, }, }, }, { - name: "Create a global AuthMethod", + name: "Create a valid OIDC AuthMethod", + req: &pbs.CreateAuthMethodRequest{Item: &pb.AuthMethod{ + ScopeId: o.GetPublicId(), + Type: auth.OidcSubtype.String(), + Attributes: &structpb.Struct{Fields: map[string]*structpb.Value{ + "issuer": structpb.NewStringValue("https://example.discovery.url:4821/.well-known/openid-configuration/"), + "client_id": structpb.NewStringValue("someclientid"), + "client_secret": structpb.NewStringValue("secret"), + "api_url_prefix": structpb.NewStringValue("https://callback.prefix:9281/path"), + "allowed_audiences": func() *structpb.Value { + lv, _ := structpb.NewList([]interface{}{"foo", "bar"}) + return structpb.NewListValue(lv) + }(), + }}, + }}, + idPrefix: oidc.AuthMethodPrefix + "_", + res: &pbs.CreateAuthMethodResponse{ + Uri: fmt.Sprintf("auth-methods/%s_", oidc.AuthMethodPrefix), + Item: &pb.AuthMethod{ + Id: defaultAm.GetPublicId(), + ScopeId: o.GetPublicId(), + CreatedTime: defaultAm.GetCreateTime().GetTimestamp(), + UpdatedTime: defaultAm.GetUpdateTime().GetTimestamp(), + Scope: &scopepb.ScopeInfo{Id: o.GetPublicId(), Type: o.GetType(), ParentScopeId: scope.Global.String()}, + Version: 1, + Type: auth.OidcSubtype.String(), + Attributes: &structpb.Struct{Fields: map[string]*structpb.Value{ + "issuer": structpb.NewStringValue("https://example.discovery.url:4821/"), + "client_id": structpb.NewStringValue("someclientid"), + "client_secret_hmac": structpb.NewStringValue(""), + "state": structpb.NewStringValue(string(oidc.InactiveState)), + "api_url_prefix": structpb.NewStringValue("https://callback.prefix:9281/path"), + "callback_url": structpb.NewStringValue(fmt.Sprintf("https://callback.prefix:9281/path/v1/auth-methods/%s_[0-9A-z]*:authenticate:callback", oidc.AuthMethodPrefix)), + "allowed_audiences": func() *structpb.Value { + lv, _ := structpb.NewList([]interface{}{"foo", "bar"}) + return structpb.NewListValue(lv) + }(), + }}, + AuthorizedActions: oidcAuthorizedActions, + AuthorizedCollectionActions: authorizedCollectionActions, + }, + }, + }, + { + name: "Create a global Password AuthMethod", req: &pbs.CreateAuthMethodRequest{Item: &pb.AuthMethod{ ScopeId: scope.Global.String(), Name: &wrapperspb.StringValue{Value: "name"}, Description: &wrapperspb.StringValue{Value: "desc"}, Type: "password", }}, + idPrefix: password.AuthMethodPrefix + "_", res: &pbs.CreateAuthMethodResponse{ Uri: fmt.Sprintf("auth-methods/%s_", password.AuthMethodPrefix), Item: &pb.AuthMethod{ @@ -439,7 +608,42 @@ func TestCreate(t *testing.T) { "min_password_length": structpb.NewNumberValue(8), "min_login_name_length": structpb.NewNumberValue(3), }}, - AuthorizedActions: []string{"read", "update", "delete", "authenticate"}, + AuthorizedActions: pwAuthorizedActions, + AuthorizedCollectionActions: authorizedCollectionActions, + }, + }, + }, + { + name: "Create a global OIDC AuthMethod", + req: &pbs.CreateAuthMethodRequest{Item: &pb.AuthMethod{ + ScopeId: scope.Global.String(), + Type: auth.OidcSubtype.String(), + Attributes: &structpb.Struct{Fields: map[string]*structpb.Value{ + "issuer": structpb.NewStringValue("https://example.discovery.url"), + "api_url_prefix": structpb.NewStringValue("https://api.com"), + "client_id": structpb.NewStringValue("someclientid"), + "client_secret": structpb.NewStringValue("secret"), + }}, + }}, + idPrefix: oidc.AuthMethodPrefix + "_", + res: &pbs.CreateAuthMethodResponse{ + Uri: fmt.Sprintf("auth-methods/%s_", oidc.AuthMethodPrefix), + Item: &pb.AuthMethod{ + Id: defaultAm.GetPublicId(), + ScopeId: scope.Global.String(), + CreatedTime: defaultAm.GetCreateTime().GetTimestamp(), + UpdatedTime: defaultAm.GetUpdateTime().GetTimestamp(), + Scope: &scopepb.ScopeInfo{Id: scope.Global.String(), Type: scope.Global.String(), Name: scope.Global.String(), Description: "Global Scope"}, + Version: 1, + Type: auth.OidcSubtype.String(), + Attributes: &structpb.Struct{Fields: map[string]*structpb.Value{ + "api_url_prefix": structpb.NewStringValue("https://api.com"), + "issuer": structpb.NewStringValue("https://example.discovery.url"), + "client_id": structpb.NewStringValue("someclientid"), + "client_secret_hmac": structpb.NewStringValue(""), + "state": structpb.NewStringValue(string(oidc.InactiveState)), + }}, + AuthorizedActions: oidcAuthorizedActions, AuthorizedCollectionActions: authorizedCollectionActions, }, }, @@ -459,6 +663,7 @@ func TestCreate(t *testing.T) { req: &pbs.CreateAuthMethodRequest{Item: &pb.AuthMethod{ ScopeId: o.GetPublicId(), CreatedTime: timestamppb.Now(), + Type: auth.PasswordSubtype.String(), }}, res: nil, err: handlers.ApiErrorWithCode(codes.InvalidArgument), @@ -468,7 +673,17 @@ func TestCreate(t *testing.T) { req: &pbs.CreateAuthMethodRequest{Item: &pb.AuthMethod{ ScopeId: o.GetPublicId(), UpdatedTime: timestamppb.Now(), - Type: "password", + Type: auth.PasswordSubtype.String(), + }}, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "Can't specify IsPrimary", + req: &pbs.CreateAuthMethodRequest{Item: &pb.AuthMethod{ + ScopeId: o.GetPublicId(), + Type: auth.PasswordSubtype.String(), + IsPrimary: true, }}, res: nil, err: handlers.ApiErrorWithCode(codes.InvalidArgument), @@ -478,7 +693,7 @@ func TestCreate(t *testing.T) { req: &pbs.CreateAuthMethodRequest{Item: &pb.AuthMethod{ ScopeId: o.GetPublicId(), UpdatedTime: timestamppb.Now(), - Type: "password", + Type: auth.PasswordSubtype.String(), }}, res: nil, err: handlers.ApiErrorWithCode(codes.InvalidArgument), @@ -494,12 +709,12 @@ func TestCreate(t *testing.T) { err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, { - name: "Attributes must be valid for type", + name: "Attributes must be valid for password type", req: &pbs.CreateAuthMethodRequest{Item: &pb.AuthMethod{ ScopeId: o.GetPublicId(), Name: &wrapperspb.StringValue{Value: "Attributes must be valid for type"}, Description: &wrapperspb.StringValue{Value: "Attributes must be valid for type"}, - Type: "password", + Type: auth.PasswordSubtype.String(), Attributes: &structpb.Struct{Fields: map[string]*structpb.Value{ "invalid_field": structpb.NewStringValue("invalid_value"), "min_login_name_length": structpb.NewNumberValue(3), @@ -508,27 +723,209 @@ func TestCreate(t *testing.T) { res: nil, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, + { + name: "Attributes must be valid for oidc type", + req: &pbs.CreateAuthMethodRequest{Item: &pb.AuthMethod{ + ScopeId: o.GetPublicId(), + Name: &wrapperspb.StringValue{Value: "Attributes must be valid for type"}, + Description: &wrapperspb.StringValue{Value: "Attributes must be valid for type"}, + Type: auth.OidcSubtype.String(), + Attributes: &structpb.Struct{Fields: map[string]*structpb.Value{ + "login-name": structpb.NewStringValue("invalid_value"), + "password": structpb.NewNumberValue(3), + }}, + }}, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "OIDC AuthMethod Doesn't Require Issuer", + req: &pbs.CreateAuthMethodRequest{Item: &pb.AuthMethod{ + ScopeId: o.GetPublicId(), + Type: auth.OidcSubtype.String(), + Attributes: &structpb.Struct{Fields: map[string]*structpb.Value{ + "api_url_prefix": structpb.NewStringValue("https://api.com"), + "client_id": structpb.NewStringValue("someclientid"), + "client_secret": structpb.NewStringValue("secret"), + }}, + }}, + res: &pbs.CreateAuthMethodResponse{ + Uri: fmt.Sprintf("auth-methods/%s_", oidc.AuthMethodPrefix), + Item: &pb.AuthMethod{ + Id: defaultAm.GetPublicId(), + ScopeId: o.GetPublicId(), + CreatedTime: defaultAm.GetCreateTime().GetTimestamp(), + UpdatedTime: defaultAm.GetUpdateTime().GetTimestamp(), + Scope: &scopepb.ScopeInfo{Id: o.GetPublicId(), Type: o.GetType(), ParentScopeId: scope.Global.String()}, + Version: 1, + Type: auth.OidcSubtype.String(), + Attributes: &structpb.Struct{Fields: map[string]*structpb.Value{ + "api_url_prefix": structpb.NewStringValue("https://api.com"), + "client_id": structpb.NewStringValue("someclientid"), + "client_secret_hmac": structpb.NewStringValue(""), + "state": structpb.NewStringValue(string(oidc.InactiveState)), + }}, + AuthorizedActions: oidcAuthorizedActions, + AuthorizedCollectionActions: authorizedCollectionActions, + }, + }, + }, + { + name: "OIDC AuthMethod Requires ApiUrl", + req: &pbs.CreateAuthMethodRequest{Item: &pb.AuthMethod{ + ScopeId: o.GetPublicId(), + Type: auth.OidcSubtype.String(), + Attributes: &structpb.Struct{Fields: map[string]*structpb.Value{ + "client_id": structpb.NewStringValue("someclientid"), + "client_secret": structpb.NewStringValue("secret"), + }}, + }}, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "OIDC AuthMethod Requires Client Id", + req: &pbs.CreateAuthMethodRequest{Item: &pb.AuthMethod{ + ScopeId: o.GetPublicId(), + Type: auth.OidcSubtype.String(), + Attributes: &structpb.Struct{Fields: map[string]*structpb.Value{ + "api_url_prefix": structpb.NewStringValue("https://api.com"), + "issuer": structpb.NewStringValue("https://example.discovery.url:4821/.well-known/openid-configuration/"), + "client_secret": structpb.NewStringValue("secret"), + }}, + }}, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "OIDC AuthMethod Requires Client Secret", + req: &pbs.CreateAuthMethodRequest{Item: &pb.AuthMethod{ + ScopeId: o.GetPublicId(), + Type: auth.OidcSubtype.String(), + Attributes: &structpb.Struct{Fields: map[string]*structpb.Value{ + "api_url_prefix": structpb.NewStringValue("https://api.com"), + "issuer": structpb.NewStringValue("https://example.discovery.url:4821/.well-known/openid-configuration/"), + "client_id": structpb.NewStringValue("someclientid"), + }}, + }}, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "OIDC AuthMethod cant specify client secret hmac", + req: &pbs.CreateAuthMethodRequest{Item: &pb.AuthMethod{ + ScopeId: o.GetPublicId(), + Type: auth.OidcSubtype.String(), + Attributes: &structpb.Struct{Fields: map[string]*structpb.Value{ + "api_url_prefix": structpb.NewStringValue("https://api.com"), + "issuer": structpb.NewStringValue("https://example.discovery.url:4821/.well-known/openid-configuration/"), + "client_id": structpb.NewStringValue("someclientid"), + "client_secret": structpb.NewStringValue("secret"), + "client_secret_hmac": structpb.NewStringValue("hmac"), + }}, + }}, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "OIDC AuthMethod cant specify state", + req: &pbs.CreateAuthMethodRequest{Item: &pb.AuthMethod{ + ScopeId: o.GetPublicId(), + Type: auth.OidcSubtype.String(), + Attributes: &structpb.Struct{Fields: map[string]*structpb.Value{ + "api_url_prefix": structpb.NewStringValue("https://api.com"), + "issuer": structpb.NewStringValue("https://example.discovery.url:4821/.well-known/openid-configuration/"), + "client_id": structpb.NewStringValue("someclientid"), + "client_secret": structpb.NewStringValue("secret"), + "state": structpb.NewStringValue(string(oidc.InactiveState)), + }}, + }}, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "OIDC AuthMethod Must Match Standard Alg Names", + req: &pbs.CreateAuthMethodRequest{Item: &pb.AuthMethod{ + ScopeId: o.GetPublicId(), + Type: auth.OidcSubtype.String(), + Attributes: &structpb.Struct{Fields: map[string]*structpb.Value{ + "api_url_prefix": structpb.NewStringValue("https://api.com"), + "issuer": structpb.NewStringValue("https://example2.discovery.url:4821"), + "client_id": structpb.NewStringValue("someclientid"), + "client_secret": structpb.NewStringValue("secret"), + "signing_algorithms": func() *structpb.Value { + lv, _ := structpb.NewList([]interface{}{string(oidc.ES256), strings.ToLower(string(oidc.EdDSA))}) + return structpb.NewListValue(lv) + }(), + }}, + }}, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "OIDC AuthMethod API Urls Prefix Format", + req: &pbs.CreateAuthMethodRequest{Item: &pb.AuthMethod{ + ScopeId: o.GetPublicId(), + Type: auth.OidcSubtype.String(), + Attributes: &structpb.Struct{Fields: map[string]*structpb.Value{ + "issuer": structpb.NewStringValue("https://example2.discovery.url:4821"), + "client_id": structpb.NewStringValue("someclientid"), + "client_secret": structpb.NewStringValue("secret"), + "api_url_prefix": structpb.NewStringValue("invalid path"), + }}, + }}, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "OIDC AuthMethod Callback Url Read Only", + req: &pbs.CreateAuthMethodRequest{Item: &pb.AuthMethod{ + ScopeId: o.GetPublicId(), + Type: auth.OidcSubtype.String(), + Attributes: &structpb.Struct{Fields: map[string]*structpb.Value{ + "issuer": structpb.NewStringValue("https://example2.discovery.url:4821"), + "client_id": structpb.NewStringValue("someclientid"), + "client_secret": structpb.NewStringValue("secret"), + "callback_url": structpb.NewStringValue("http://another.url.com:82471"), + }}, + }}, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "OIDC AuthMethod unparseable certificates", + req: &pbs.CreateAuthMethodRequest{Item: &pb.AuthMethod{ + ScopeId: o.GetPublicId(), + Type: auth.OidcSubtype.String(), + Attributes: &structpb.Struct{Fields: map[string]*structpb.Value{ + "api_url_prefix": structpb.NewStringValue("https://api.com"), + "issuer": structpb.NewStringValue("https://example2.discovery.url:4821"), + "client_id": structpb.NewStringValue("someclientid"), + "client_secret": structpb.NewStringValue("secret"), + "idp_ca_certs": func() *structpb.Value { + lv, _ := structpb.NewList([]interface{}{"unparseable"}) + return structpb.NewListValue(lv) + }(), + }}, + }}, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { assert, require := assert.New(t), require.New(t) - s, err := authmethods.NewService(kms, pwRepoFn, iamRepoFn, atRepoFn) + s, err := authmethods.NewService(kms, pwRepoFn, oidcRepoFn, iamRepoFn, atRepoFn) require.NoError(err, "Error when getting new auth_method service.") got, gErr := s.CreateAuthMethod(auth.DisabledAuthTestContext(iamRepoFn, tc.req.GetItem().GetScopeId()), tc.req) if tc.err != nil { require.Error(gErr) assert.True(errors.Is(gErr, tc.err), "CreateAuthMethod(%+v) got error %v, wanted %v", tc.req, gErr, tc.err) + return } + require.NoError(gErr) if tc.res == nil { require.Nil(got) } if got != nil { assert.Contains(got.GetUri(), tc.res.Uri) - assert.True(strings.HasPrefix(got.GetItem().GetId(), password.AuthMethodPrefix+"_")) + assert.True(strings.HasPrefix(got.GetItem().GetId(), tc.idPrefix)) gotCreateTime := got.GetItem().GetCreatedTime() gotUpdateTime := got.GetItem().GetUpdatedTime() + // Verify it is a auth_method created after the test setup's default auth_method assert.True(gotCreateTime.AsTime().After(defaultCreated.AsTime()), "New auth_method should have been created after default auth_method. Was created %v, which is after %v", gotCreateTime, defaultCreated) assert.True(gotUpdateTime.AsTime().After(defaultCreated.AsTime()), "New auth_method should have been updated after default auth_method. Was updated %v, which is after %v", gotUpdateTime, defaultCreated) @@ -537,630 +934,21 @@ func TestCreate(t *testing.T) { got.Uri, tc.res.Uri = "", "" got.Item.Id, tc.res.Item.Id = "", "" got.Item.CreatedTime, got.Item.UpdatedTime, tc.res.Item.CreatedTime, tc.res.Item.UpdatedTime = nil, nil, nil, nil + if _, ok := got.Item.Attributes.Fields["client_secret_hmac"]; ok { + assert.NotEqual(tc.req.Item.Attributes.Fields["client_secret"], got.Item.Attributes.Fields["client_secret_hmac"]) + got.Item.Attributes.Fields["client_secret_hmac"] = structpb.NewStringValue("") + } + if _, ok := got.Item.Attributes.Fields["callback_url"]; ok { + exp := tc.res.Item.Attributes.Fields["callback_url"].GetStringValue() + gVal := got.Item.Attributes.Fields["callback_url"].GetStringValue() + matches, err := regexp.MatchString(exp, gVal) + require.NoError(err) + assert.True(matches, "%q doesn't match %q", gVal, exp) + delete(got.Item.Attributes.Fields, "callback_url") + delete(tc.res.Item.Attributes.Fields, "callback_url") + } } assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "CreateAuthMethod(%q) got response %q, wanted %q", tc.req, got, tc.res) }) } } - -func TestUpdate(t *testing.T) { - conn, _ := db.TestSetup(t, "postgres") - rw := db.New(conn) - wrapper := db.TestWrapper(t) - kms := kms.TestKms(t, conn, wrapper) - iamRepoFn := func() (*iam.Repository, error) { - return iam.TestRepo(t, conn, wrapper), nil - } - pwRepoFn := func() (*password.Repository, error) { - return password.NewRepository(rw, rw, kms) - } - atRepoFn := func() (*authtoken.Repository, error) { - return authtoken.NewRepository(rw, rw, kms) - } - iamRepo := iam.TestRepo(t, conn, wrapper) - - o, _ := iam.TestScopes(t, iamRepo) - tested, err := authmethods.NewService(kms, pwRepoFn, iamRepoFn, atRepoFn) - require.NoError(t, err, "Error when getting new auth_method service.") - - defaultScopeInfo := &scopepb.ScopeInfo{Id: o.GetPublicId(), Type: o.GetType(), ParentScopeId: scope.Global.String()} - - freshAuthMethod := func() (*pb.AuthMethod, func()) { - am, err := tested.CreateAuthMethod(auth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId()), - &pbs.CreateAuthMethodRequest{Item: &pb.AuthMethod{ - ScopeId: o.GetPublicId(), - Name: wrapperspb.String("default"), - Description: wrapperspb.String("default"), - Type: "password", - }}) - require.NoError(t, err) - - clean := func() { - _, err := tested.DeleteAuthMethod(auth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId()), - &pbs.DeleteAuthMethodRequest{Id: am.GetItem().GetId()}) - require.NoError(t, err) - } - - return am.GetItem(), clean - } - - cases := []struct { - name string - req *pbs.UpdateAuthMethodRequest - res *pbs.UpdateAuthMethodResponse - err error - }{ - { - name: "Update an Existing AuthMethod", - req: &pbs.UpdateAuthMethodRequest{ - UpdateMask: &field_mask.FieldMask{ - Paths: []string{"name", "description"}, - }, - Item: &pb.AuthMethod{ - Name: &wrapperspb.StringValue{Value: "new"}, - Description: &wrapperspb.StringValue{Value: "desc"}, - Type: "password", - }, - }, - res: &pbs.UpdateAuthMethodResponse{ - Item: &pb.AuthMethod{ - ScopeId: o.GetPublicId(), - Name: &wrapperspb.StringValue{Value: "new"}, - Description: &wrapperspb.StringValue{Value: "desc"}, - Type: "password", - Attributes: &structpb.Struct{Fields: map[string]*structpb.Value{ - "min_password_length": structpb.NewNumberValue(8), - "min_login_name_length": structpb.NewNumberValue(3), - }}, - Scope: defaultScopeInfo, - AuthorizedActions: []string{"read", "update", "delete", "authenticate"}, - AuthorizedCollectionActions: authorizedCollectionActions, - }, - }, - }, - { - name: "Multiple Paths in single string", - req: &pbs.UpdateAuthMethodRequest{ - UpdateMask: &field_mask.FieldMask{ - Paths: []string{"name,description,type"}, - }, - Item: &pb.AuthMethod{ - Name: &wrapperspb.StringValue{Value: "new"}, - Description: &wrapperspb.StringValue{Value: "desc"}, - Type: "password", - }, - }, - res: &pbs.UpdateAuthMethodResponse{ - Item: &pb.AuthMethod{ - ScopeId: o.GetPublicId(), - Name: &wrapperspb.StringValue{Value: "new"}, - Description: &wrapperspb.StringValue{Value: "desc"}, - Type: "password", - Attributes: &structpb.Struct{Fields: map[string]*structpb.Value{ - "min_password_length": structpb.NewNumberValue(8), - "min_login_name_length": structpb.NewNumberValue(3), - }}, - Scope: defaultScopeInfo, - AuthorizedActions: []string{"read", "update", "delete", "authenticate"}, - AuthorizedCollectionActions: authorizedCollectionActions, - }, - }, - }, - { - name: "No Update Mask", - req: &pbs.UpdateAuthMethodRequest{ - Item: &pb.AuthMethod{ - Name: &wrapperspb.StringValue{Value: "updated name"}, - Description: &wrapperspb.StringValue{Value: "updated desc"}, - }, - }, - err: handlers.ApiErrorWithCode(codes.InvalidArgument), - }, - { - name: "No Paths in Mask", - req: &pbs.UpdateAuthMethodRequest{ - UpdateMask: &field_mask.FieldMask{Paths: []string{}}, - Item: &pb.AuthMethod{ - Name: &wrapperspb.StringValue{Value: "updated name"}, - Description: &wrapperspb.StringValue{Value: "updated desc"}, - }, - }, - err: handlers.ApiErrorWithCode(codes.InvalidArgument), - }, - { - name: "Only non-existant paths in Mask", - req: &pbs.UpdateAuthMethodRequest{ - UpdateMask: &field_mask.FieldMask{Paths: []string{"nonexistant_field"}}, - Item: &pb.AuthMethod{ - Name: &wrapperspb.StringValue{Value: "updated name"}, - Description: &wrapperspb.StringValue{Value: "updated desc"}, - }, - }, - err: handlers.ApiErrorWithCode(codes.InvalidArgument), - }, - { - name: "Cant change type", - req: &pbs.UpdateAuthMethodRequest{ - UpdateMask: &field_mask.FieldMask{Paths: []string{"name", "type"}}, - Item: &pb.AuthMethod{ - Name: &wrapperspb.StringValue{Value: "updated name"}, - Type: "oidc", - }, - }, - err: handlers.ApiErrorWithCode(codes.InvalidArgument), - }, - { - name: "Unset Name", - req: &pbs.UpdateAuthMethodRequest{ - UpdateMask: &field_mask.FieldMask{ - Paths: []string{"name"}, - }, - Item: &pb.AuthMethod{ - Description: &wrapperspb.StringValue{Value: "ignored"}, - }, - }, - res: &pbs.UpdateAuthMethodResponse{ - Item: &pb.AuthMethod{ - ScopeId: o.GetPublicId(), - Description: &wrapperspb.StringValue{Value: "default"}, - Type: "password", - Attributes: &structpb.Struct{Fields: map[string]*structpb.Value{ - "min_password_length": structpb.NewNumberValue(8), - "min_login_name_length": structpb.NewNumberValue(3), - }}, - Scope: defaultScopeInfo, - AuthorizedActions: []string{"read", "update", "delete", "authenticate"}, - AuthorizedCollectionActions: authorizedCollectionActions, - }, - }, - }, - { - name: "Update Only Name", - req: &pbs.UpdateAuthMethodRequest{ - UpdateMask: &field_mask.FieldMask{ - Paths: []string{"name"}, - }, - Item: &pb.AuthMethod{ - Name: &wrapperspb.StringValue{Value: "updated"}, - Description: &wrapperspb.StringValue{Value: "ignored"}, - }, - }, - res: &pbs.UpdateAuthMethodResponse{ - Item: &pb.AuthMethod{ - ScopeId: o.GetPublicId(), - Name: &wrapperspb.StringValue{Value: "updated"}, - Description: &wrapperspb.StringValue{Value: "default"}, - Type: "password", - Attributes: &structpb.Struct{Fields: map[string]*structpb.Value{ - "min_password_length": structpb.NewNumberValue(8), - "min_login_name_length": structpb.NewNumberValue(3), - }}, - Scope: defaultScopeInfo, - AuthorizedActions: []string{"read", "update", "delete", "authenticate"}, - AuthorizedCollectionActions: authorizedCollectionActions, - }, - }, - }, - { - name: "Update Only Description", - req: &pbs.UpdateAuthMethodRequest{ - UpdateMask: &field_mask.FieldMask{ - Paths: []string{"description"}, - }, - Item: &pb.AuthMethod{ - Name: &wrapperspb.StringValue{Value: "ignored"}, - Description: &wrapperspb.StringValue{Value: "notignored"}, - }, - }, - res: &pbs.UpdateAuthMethodResponse{ - Item: &pb.AuthMethod{ - ScopeId: o.GetPublicId(), - Name: &wrapperspb.StringValue{Value: "default"}, - Description: &wrapperspb.StringValue{Value: "notignored"}, - Type: "password", - Attributes: &structpb.Struct{Fields: map[string]*structpb.Value{ - "min_password_length": structpb.NewNumberValue(8), - "min_login_name_length": structpb.NewNumberValue(3), - }}, - Scope: defaultScopeInfo, - AuthorizedActions: []string{"read", "update", "delete", "authenticate"}, - AuthorizedCollectionActions: authorizedCollectionActions, - }, - }, - }, - { - name: "Update a Non Existing AuthMethod", - req: &pbs.UpdateAuthMethodRequest{ - Id: password.AuthMethodPrefix + "_DoesntExis", - UpdateMask: &field_mask.FieldMask{ - Paths: []string{"description"}, - }, - Item: &pb.AuthMethod{ - Name: &wrapperspb.StringValue{Value: "new"}, - Description: &wrapperspb.StringValue{Value: "desc"}, - }, - }, - err: handlers.ApiErrorWithCode(codes.NotFound), - }, - { - name: "Cant change Id", - req: &pbs.UpdateAuthMethodRequest{ - UpdateMask: &field_mask.FieldMask{ - Paths: []string{"id"}, - }, - Item: &pb.AuthMethod{ - Id: password.AuthMethodPrefix + "_somethinge", - Name: &wrapperspb.StringValue{Value: "new"}, - Description: &wrapperspb.StringValue{Value: "new desc"}, - }, - }, - res: nil, - err: handlers.ApiErrorWithCode(codes.InvalidArgument), - }, - { - name: "Cant specify Created Time", - req: &pbs.UpdateAuthMethodRequest{ - UpdateMask: &field_mask.FieldMask{ - Paths: []string{"created_time"}, - }, - Item: &pb.AuthMethod{ - CreatedTime: timestamppb.Now(), - }, - }, - res: nil, - err: handlers.ApiErrorWithCode(codes.InvalidArgument), - }, - { - name: "Cant specify Updated Time", - req: &pbs.UpdateAuthMethodRequest{ - UpdateMask: &field_mask.FieldMask{ - Paths: []string{"updated_time"}, - }, - Item: &pb.AuthMethod{ - UpdatedTime: timestamppb.Now(), - }, - }, - res: nil, - err: handlers.ApiErrorWithCode(codes.InvalidArgument), - }, - { - name: "Cant specify Type", - req: &pbs.UpdateAuthMethodRequest{ - UpdateMask: &field_mask.FieldMask{ - Paths: []string{"type"}, - }, - Item: &pb.AuthMethod{ - Type: "oidc", - }, - }, - res: nil, - err: handlers.ApiErrorWithCode(codes.InvalidArgument), - }, - { - name: "Update login name length", - req: &pbs.UpdateAuthMethodRequest{ - UpdateMask: &field_mask.FieldMask{ - Paths: []string{"attributes.min_login_name_length"}, - }, - Item: &pb.AuthMethod{ - Name: &wrapperspb.StringValue{Value: "ignored"}, - Description: &wrapperspb.StringValue{Value: "ignored"}, - Attributes: &structpb.Struct{Fields: map[string]*structpb.Value{ - "min_login_name_length": structpb.NewNumberValue(42), - "min_password_length": structpb.NewNumberValue(55555), - }}, - }, - }, - res: &pbs.UpdateAuthMethodResponse{ - Item: &pb.AuthMethod{ - ScopeId: o.GetPublicId(), - Name: &wrapperspb.StringValue{Value: "default"}, - Description: &wrapperspb.StringValue{Value: "default"}, - Type: "password", - Attributes: &structpb.Struct{Fields: map[string]*structpb.Value{ - "min_password_length": structpb.NewNumberValue(8), - "min_login_name_length": structpb.NewNumberValue(42), - }}, - Scope: defaultScopeInfo, - AuthorizedActions: []string{"read", "update", "delete", "authenticate"}, - AuthorizedCollectionActions: authorizedCollectionActions, - }, - }, - }, - { - name: "Update password length", - req: &pbs.UpdateAuthMethodRequest{ - UpdateMask: &field_mask.FieldMask{ - Paths: []string{"attributes.min_password_length"}, - }, - Item: &pb.AuthMethod{ - Name: &wrapperspb.StringValue{Value: "ignored"}, - Description: &wrapperspb.StringValue{Value: "ignored"}, - Attributes: &structpb.Struct{Fields: map[string]*structpb.Value{ - "min_login_name_length": structpb.NewNumberValue(5555), - "min_password_length": structpb.NewNumberValue(42), - }}, - }, - }, - res: &pbs.UpdateAuthMethodResponse{ - Item: &pb.AuthMethod{ - ScopeId: o.GetPublicId(), - Name: &wrapperspb.StringValue{Value: "default"}, - Description: &wrapperspb.StringValue{Value: "default"}, - Type: "password", - Attributes: &structpb.Struct{Fields: map[string]*structpb.Value{ - "min_password_length": structpb.NewNumberValue(42), - "min_login_name_length": structpb.NewNumberValue(3), - }}, - Scope: defaultScopeInfo, - AuthorizedActions: []string{"read", "update", "delete", "authenticate"}, - AuthorizedCollectionActions: authorizedCollectionActions, - }, - }, - }, - } - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - assert, require := assert.New(t), require.New(t) - am, cleanup := freshAuthMethod() - defer cleanup() - - tc.req.Item.Version = 1 - - if tc.req.GetId() == "" { - tc.req.Id = am.GetId() - } - - if tc.res != nil && tc.res.Item != nil { - tc.res.Item.Id = am.GetId() - tc.res.Item.CreatedTime = am.GetCreatedTime() - } - - got, gErr := tested.UpdateAuthMethod(auth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId()), tc.req) - if tc.err != nil { - require.Error(gErr) - assert.True(errors.Is(gErr, tc.err), "UpdateAuthMethod(%+v) got error %v, wanted %v", tc.req, gErr, tc.err) - } - - if tc.res == nil { - require.Nil(got) - } - - if got != nil { - assert.NotNilf(tc.res, "Expected UpdateAuthMethod response to be nil, but was %v", got) - - created := am.GetCreatedTime().AsTime() - gotUpdateTime := got.GetItem().GetUpdatedTime().AsTime() - // Verify it is a auth_method updated after it was created - assert.True(gotUpdateTime.After(created), "Updated auth_method should have been updated after it's creation. Was updated %v, which is after %v", gotUpdateTime, created) - - // Clear all values which are hard to compare against. - got.Item.UpdatedTime, tc.res.Item.UpdatedTime = nil, nil - - assert.EqualValues(2, got.Item.Version) - tc.res.Item.Version = 2 - } - assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "UpdateAuthMethod(%q) got response %q, wanted %q", tc.req, got, tc.res) - }) - } -} - -func TestAuthenticate(t *testing.T) { - conn, _ := db.TestSetup(t, "postgres") - rw := db.New(conn) - wrapper := db.TestWrapper(t) - kms := kms.TestKms(t, conn, wrapper) - o, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) - - iamRepoFn := func() (*iam.Repository, error) { - return iam.TestRepo(t, conn, wrapper), nil - } - pwRepoFn := func() (*password.Repository, error) { - return password.NewRepository(rw, rw, kms) - } - atRepoFn := func() (*authtoken.Repository, error) { - return authtoken.NewRepository(rw, rw, kms) - } - am := password.TestAuthMethods(t, conn, o.GetPublicId(), 1)[0] - - acct, err := password.NewAccount(am.GetPublicId(), password.WithLoginName(testLoginName)) - require.NoError(t, err) - - pwRepo, err := pwRepoFn() - require.NoError(t, err) - acct, err = pwRepo.CreateAccount(context.Background(), o.GetPublicId(), acct, password.WithPassword(testPassword)) - require.NoError(t, err) - require.NotNil(t, acct) - - cases := []struct { - name string - request *pbs.AuthenticateRequest - wantType string - wantErr error - }{ - { - name: "basic", - request: &pbs.AuthenticateRequest{ - AuthMethodId: am.GetPublicId(), - TokenType: "token", - Credentials: func() *structpb.Struct { - creds := map[string]*structpb.Value{ - "login_name": {Kind: &structpb.Value_StringValue{StringValue: testLoginName}}, - "password": {Kind: &structpb.Value_StringValue{StringValue: testPassword}}, - } - return &structpb.Struct{Fields: creds} - }(), - }, - wantType: "token", - }, - { - name: "cookie-type", - request: &pbs.AuthenticateRequest{ - AuthMethodId: am.GetPublicId(), - TokenType: "cookie", - Credentials: func() *structpb.Struct { - creds := map[string]*structpb.Value{ - "login_name": {Kind: &structpb.Value_StringValue{StringValue: testLoginName}}, - "password": {Kind: &structpb.Value_StringValue{StringValue: testPassword}}, - } - return &structpb.Struct{Fields: creds} - }(), - }, - wantType: "cookie", - }, - { - name: "no-token-type", - request: &pbs.AuthenticateRequest{ - AuthMethodId: am.GetPublicId(), - Credentials: func() *structpb.Struct { - creds := map[string]*structpb.Value{ - "login_name": {Kind: &structpb.Value_StringValue{StringValue: testLoginName}}, - "password": {Kind: &structpb.Value_StringValue{StringValue: testPassword}}, - } - return &structpb.Struct{Fields: creds} - }(), - }, - }, - { - name: "bad-token-type", - request: &pbs.AuthenticateRequest{ - AuthMethodId: am.GetPublicId(), - TokenType: "email", - Credentials: func() *structpb.Struct { - creds := map[string]*structpb.Value{ - "login_name": {Kind: &structpb.Value_StringValue{StringValue: testLoginName}}, - "password": {Kind: &structpb.Value_StringValue{StringValue: testPassword}}, - } - return &structpb.Struct{Fields: creds} - }(), - }, - wantErr: handlers.ApiErrorWithCode(codes.InvalidArgument), - }, - { - name: "no-authmethod", - request: &pbs.AuthenticateRequest{ - Credentials: func() *structpb.Struct { - creds := map[string]*structpb.Value{ - "login_name": {Kind: &structpb.Value_StringValue{StringValue: testLoginName}}, - "password": {Kind: &structpb.Value_StringValue{StringValue: testPassword}}, - } - return &structpb.Struct{Fields: creds} - }(), - }, - wantErr: handlers.ApiErrorWithCode(codes.InvalidArgument), - }, - { - name: "wrong-password", - request: &pbs.AuthenticateRequest{ - AuthMethodId: am.GetPublicId(), - TokenType: "token", - Credentials: func() *structpb.Struct { - creds := map[string]*structpb.Value{ - "login_name": {Kind: &structpb.Value_StringValue{StringValue: testLoginName}}, - "password": {Kind: &structpb.Value_StringValue{StringValue: "wrong"}}, - } - return &structpb.Struct{Fields: creds} - }(), - }, - wantErr: handlers.ApiErrorWithCode(codes.Unauthenticated), - }, - { - name: "wrong-login-name", - request: &pbs.AuthenticateRequest{ - AuthMethodId: am.GetPublicId(), - TokenType: "token", - Credentials: func() *structpb.Struct { - creds := map[string]*structpb.Value{ - "login_name": {Kind: &structpb.Value_StringValue{StringValue: "wrong"}}, - "password": {Kind: &structpb.Value_StringValue{StringValue: testPassword}}, - } - return &structpb.Struct{Fields: creds} - }(), - }, - wantErr: handlers.ApiErrorWithCode(codes.Unauthenticated), - }, - } - - for _, tc := range cases { - t.Run(tc.name, func(t *testing.T) { - assert, require := assert.New(t), require.New(t) - s, err := authmethods.NewService(kms, pwRepoFn, iamRepoFn, atRepoFn) - require.NoError(err) - - resp, err := s.Authenticate(auth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId()), tc.request) - if tc.wantErr != nil { - assert.Error(err) - assert.Truef(errors.Is(err, tc.wantErr), "Got %#v, wanted %#v", err, tc.wantErr) - return - } - require.NoError(err) - aToken := resp.GetItem() - assert.NotEmpty(aToken.GetId()) - assert.NotEmpty(aToken.GetToken()) - assert.True(strings.HasPrefix(aToken.GetToken(), aToken.GetId())) - assert.Equal(am.GetPublicId(), aToken.GetAuthMethodId()) - assert.Equal(aToken.GetCreatedTime(), aToken.GetUpdatedTime()) - assert.Equal(aToken.GetCreatedTime(), aToken.GetApproximateLastUsedTime()) - assert.Equal(acct.GetPublicId(), aToken.GetAccountId()) - assert.Equal(am.GetPublicId(), aToken.GetAuthMethodId()) - assert.Equal(tc.wantType, resp.GetTokenType()) - }) - } -} - -func TestAuthenticate_AuthAccountConnectedToIamUser(t *testing.T) { - assert, require := assert.New(t), require.New(t) - conn, _ := db.TestSetup(t, "postgres") - rw := db.New(conn) - wrapper := db.TestWrapper(t) - kms := kms.TestKms(t, conn, wrapper) - o, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) - - iamRepoFn := func() (*iam.Repository, error) { - return iam.TestRepo(t, conn, wrapper), nil - } - pwRepoFn := func() (*password.Repository, error) { - return password.NewRepository(rw, rw, kms) - } - atRepoFn := func() (*authtoken.Repository, error) { - return authtoken.NewRepository(rw, rw, kms) - } - - am := password.TestAuthMethods(t, conn, o.GetPublicId(), 1)[0] - acct, err := password.NewAccount(am.GetPublicId(), password.WithLoginName(testLoginName)) - require.NoError(err) - - pwRepo, err := pwRepoFn() - require.NoError(err) - acct, err = pwRepo.CreateAccount(context.Background(), o.GetPublicId(), acct, password.WithPassword(testPassword)) - require.NoError(err) - - // connected to an account. - iamRepo, err := iamRepoFn() - require.NoError(err) - iamUser, err := iamRepo.LookupUserWithLogin(context.Background(), acct.GetPublicId(), iam.WithAutoVivify(true)) - require.NoError(err) - - s, err := authmethods.NewService(kms, pwRepoFn, iamRepoFn, atRepoFn) - require.NoError(err) - resp, err := s.Authenticate(auth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId()), &pbs.AuthenticateRequest{ - AuthMethodId: am.GetPublicId(), - Credentials: func() *structpb.Struct { - creds := map[string]*structpb.Value{ - "login_name": {Kind: &structpb.Value_StringValue{StringValue: testLoginName}}, - "password": {Kind: &structpb.Value_StringValue{StringValue: testPassword}}, - } - return &structpb.Struct{Fields: creds} - }(), - }) - require.NoError(err) - - aToken := resp.GetItem() - assert.Equal(iamUser.GetPublicId(), aToken.GetUserId()) - assert.Equal(am.GetPublicId(), aToken.GetAuthMethodId()) - assert.Equal(acct.GetPublicId(), aToken.GetAccountId()) - - assert.NotEmpty(aToken.GetId()) - assert.NotEmpty(aToken.GetToken()) - assert.True(strings.HasPrefix(aToken.GetToken(), aToken.GetId())) -} diff --git a/internal/servers/controller/handlers/authmethods/oidc.go b/internal/servers/controller/handlers/authmethods/oidc.go new file mode 100644 index 000000000..636bf5ca1 --- /dev/null +++ b/internal/servers/controller/handlers/authmethods/oidc.go @@ -0,0 +1,431 @@ +package authmethods + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "strings" + + "github.com/hashicorp/boundary/internal/auth" + "github.com/hashicorp/boundary/internal/auth/oidc" + oidcstore "github.com/hashicorp/boundary/internal/auth/oidc/store" + "github.com/hashicorp/boundary/internal/errors" + pb "github.com/hashicorp/boundary/internal/gen/controller/api/resources/authmethods" + pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services" + "github.com/hashicorp/boundary/internal/servers/controller/handlers" + "github.com/hashicorp/boundary/internal/types/action" + "google.golang.org/grpc/codes" +) + +const ( + // commands + startCommand = "start" + callbackCommand = "callback" + tokenCommand = "token" + + // token request/response fields + tokenField = "token" + + // field names + issuerField = "attributes.issuer" + clientSecretField = "attributes.client_secret" + clientIdField = "attributes.client_id" + clientSecretHmacField = "attributes.client_secret_hmac" + stateField = "attributes.state" + callbackUrlField = "attributes.callback_url" + apiUrlPrefixField = "attributes.api_url_prefix" + caCertsField = "attributes.ca_certs" + maxAgeField = "attributes.max_age" + signingAlgorithmField = "attributes.signing_algorithms" + disableDiscoveredConfigValidationField = "attributes.disable_discovered_config_validation" + roundtripPayloadAttributesField = "attributes.roundtrip_payload" + codeField = "attributes.code" +) + +var oidcMaskManager handlers.MaskManager + +func init() { + var err error + if oidcMaskManager, err = handlers.NewMaskManager(&oidcstore.AuthMethod{}, &pb.AuthMethod{}, &pb.OidcAuthMethodAttributes{}); err != nil { + panic(err) + } + + IdActions[auth.OidcSubtype] = action.ActionSet{ + action.Read, + action.Update, + action.Delete, + action.ChangeState, + action.Authenticate, + } +} + +type oidcState uint + +const ( + unknownState oidcState = iota + inactiveState + privateState + publicState +) + +var oidcStateMap = map[string]oidcState{ + inactiveState.String(): inactiveState, + privateState.String(): privateState, + publicState.String(): publicState, +} + +func (o oidcState) String() string { + return [...]string{ + "unknown", + "inactive", + "active-private", + "active-public", + }[o] +} + +// createOidcInRepo creates an oidc auth method in a repo and returns the result. +// This method should never return a nil AuthMethod without returning an error. +func (s Service) createOidcInRepo(ctx context.Context, scopeId string, item *pb.AuthMethod) (*oidc.AuthMethod, error) { + u, _, _, err := toStorageOidcAuthMethod(scopeId, item) + if err != nil { + return nil, err + } + repo, err := s.oidcRepoFn() + if err != nil { + return nil, err + } + out, err := repo.CreateAuthMethod(ctx, u) + if err != nil { + return nil, fmt.Errorf("unable to create auth method: %w", err) + } + return out, nil +} + +func (s Service) updateOidcInRepo(ctx context.Context, scopeId string, req *pbs.UpdateAuthMethodRequest) (*oidc.AuthMethod, bool, error) { + item := req.GetItem() + u, dryRun, forced, err := toStorageOidcAuthMethod(scopeId, item) + if err != nil { + return nil, dryRun, err + } + u.PublicId = req.GetId() + + var opts []oidc.Option + if forced { + opts = append(opts, oidc.WithForce()) + } + if dryRun { + opts = append(opts, oidc.WithDryRun()) + } + + version := item.GetVersion() + dbMask := oidcMaskManager.Translate(req.GetUpdateMask().GetPaths()) + if len(dbMask) == 0 { + return nil, dryRun, handlers.InvalidArgumentErrorf("No valid fields included in the update mask.", map[string]string{"update_mask": "No valid fields provided in the update mask."}) + } + + repo, err := s.oidcRepoFn() + if err != nil { + return nil, dryRun, err + } + out, rowsUpdated, err := repo.UpdateAuthMethod(ctx, u, version, dbMask, opts...) + if err != nil { + return nil, dryRun, fmt.Errorf("unable to update auth method: %w", err) + } + if rowsUpdated == 0 && !dryRun && out == nil { + return nil, dryRun, handlers.NotFoundErrorf("AuthMethod %q doesn't exist or incorrect version provided.", req.GetId()) + } + return out, dryRun, nil +} + +func (s Service) authenticateOidc(ctx context.Context, req *pbs.AuthenticateRequest, authResults *auth.VerifyResults) (*pbs.AuthenticateResponse, error) { + const op = "authmethod_service.(Service).authenticateOidc" + if req == nil { + return nil, errors.New(errors.InvalidParameter, op, "Nil request.") + } + if authResults == nil { + return nil, errors.New(errors.InvalidParameter, op, "Nil auth results.") + } + switch req.GetCommand() { + case startCommand: + return s.authenticateOidcStart(ctx, req, authResults) + case callbackCommand: + return s.authenticateOidcCallback(ctx, req, authResults) + case tokenCommand: + return s.authenticateOidcToken(ctx, req, authResults) + } + + return &pbs.AuthenticateResponse{Command: req.GetCommand(), Attributes: nil}, nil +} + +func (s Service) authenticateOidcStart(ctx context.Context, req *pbs.AuthenticateRequest, authResults *auth.VerifyResults) (*pbs.AuthenticateResponse, error) { + const op = "authmethod_service.(Service).authenticateOidcStart" + if req == nil { + return nil, errors.New(errors.InvalidParameter, op, "Nil request.") + } + if authResults == nil { + return nil, errors.New(errors.InvalidParameter, op, "Nil auth results.") + } + + var opts []oidc.Option + if req.GetAttributes() != nil { + attrs := new(pbs.OidcStartAttributes) + if err := handlers.StructToProto(req.GetAttributes(), attrs); err != nil { + return nil, errors.New(errors.InvalidParameter, op, "Error parsing request attributes.") + } + if attrs.GetCachedRoundtripPayload() != "" { + opts = append(opts, oidc.WithRoundtripPayload(attrs.GetCachedRoundtripPayload())) + } + } + + authUrl, tokenUrl, tokenId, err := oidc.StartAuth(ctx, s.oidcRepoFn, req.GetAuthMethodId(), opts...) + if err != nil { + // TODO: Log something + return nil, errors.New(errors.Internal, op, "Error generating parameters for starting the OIDC flow.") + } + + resp := &pb.OidcAuthMethodAuthenticateStartResponse{ + AuthUrl: authUrl.String(), + TokenUrl: tokenUrl.String(), + TokenId: tokenId, + } + + attrs, err := handlers.ProtoToStruct(resp) + if err != nil { + return nil, errors.New(errors.Internal, op, "Error marshaling parameters.") + } + + return &pbs.AuthenticateResponse{Command: req.GetCommand(), Attributes: attrs}, nil +} + +func (s Service) authenticateOidcCallback(ctx context.Context, req *pbs.AuthenticateRequest, authResults *auth.VerifyResults) (*pbs.AuthenticateResponse, error) { + const op = "authmethod_service.(Service).authenticateOidcCallback" + if req == nil { + return nil, errors.New(errors.InvalidParameter, op, "Nil request.") + } + if authResults == nil { + return nil, errors.New(errors.InvalidParameter, op, "Nil auth results.") + } + + attrs := new(pb.OidcAuthMethodAuthenticateCallbackRequest) + // Note that this conversion has already happened in the validate call so we don't expect errors here. + if err := handlers.StructToProto(req.GetAttributes(), attrs, handlers.WithDiscardUnknownFields(true)); err != nil { + return nil, err + } + + finalRedirectUrl, err := oidc.Callback( + ctx, + s.oidcRepoFn, + oidc.IamRepoFactory(s.iamRepoFn), + s.atRepoFn, + req.GetAuthMethodId(), + attrs.GetState(), + attrs.GetCode()) + if err != nil { + // TODO: Log something more meaningful + return nil, errors.New(errors.InvalidParameter, op, "Callback validation failed.", errors.WithWrap(err)) + } + + respAttrs, err := handlers.ProtoToStruct(&pb.OidcAuthMethodAuthenticateCallbackResponse{ + FinalRedirectUrl: finalRedirectUrl, + }) + if err != nil { + return nil, errors.New(errors.Internal, op, "Error marshaling parameters") + } + + return &pbs.AuthenticateResponse{Command: req.GetCommand(), Attributes: respAttrs}, nil +} + +func (s Service) authenticateOidcToken(ctx context.Context, req *pbs.AuthenticateRequest, authResults *auth.VerifyResults) (*pbs.AuthenticateResponse, error) { + const op = "authmethod_service.(Service).authenticateOidcToken" + if req == nil { + return nil, errors.New(errors.InvalidParameter, op, "Nil request.") + } + if authResults == nil { + return nil, errors.New(errors.InvalidParameter, op, "Nil auth results.") + } + if req.GetAttributes() == nil { + return nil, errors.New(errors.InvalidParameter, op, "Nil request attributes.") + } + + attrs := new(pb.OidcAuthMethodAuthenticateTokenRequest) + if err := handlers.StructToProto(req.GetAttributes(), attrs); err != nil { + return nil, errors.New(errors.InvalidParameter, op, "Error parsing request attributes.") + } + if attrs.TokenId == "" { + return nil, errors.New(errors.InvalidParameter, op, "Empty token id request attributes.") + } + + token, err := oidc.TokenRequest(ctx, s.kms, s.atRepoFn, attrs.TokenId) + if err != nil { + // TODO: Log something so we don't lose the error's context and entire msg... + switch { + case errors.Match(errors.T(errors.Forbidden), err): + return nil, errors.Wrap(err, op, errors.WithMsg("Forbidden.")) + case errors.Match(errors.T(errors.AuthAttemptExpired), err): + return nil, errors.Wrap(err, op, errors.WithMsg("Forbidden.")) + default: + return nil, errors.Wrap(err, op, errors.WithMsg("Error generating parameters for token request.")) + } + } + if token == nil { + return nil, nil + } + + responseToken, err := s.convertInternalAuthTokenToApiAuthToken( + ctx, + token, + ) + if err != nil { + return nil, errors.Wrap(err, op) + } + return s.convertToAuthenticateResponse(ctx, req, authResults, responseToken) +} + +func validateAuthenticateOidcRequest(req *pbs.AuthenticateRequest) error { + badFields := make(map[string]string) + + switch req.GetCommand() { + case startCommand: + if req.GetAttributes() != nil { + attrs := new(pbs.OidcStartAttributes) + if err := handlers.StructToProto(req.GetAttributes(), attrs); err != nil { + badFields[attributesField] = "Could not be parsed, or contains invalid fields." + break + } + + // Ensure we pay no attention to cache information provided by the client + attrs.CachedRoundtripPayload = "" + + payload := attrs.GetRoundtripPayload() + if payload == nil { + break + } + m, err := json.Marshal(payload.AsMap()) + if err != nil { + // We don't know what's in this payload so we swallow the + // error, as it could be something sensitive. + badFields[roundtripPayloadAttributesField] = "Unable to marshal given value as JSON." + } else { + // Cache for later + attrs.CachedRoundtripPayload = string(m) + req.Attributes, err = handlers.ProtoToStruct(attrs) + if err != nil { + // TODO: Logging, when we have a logger + return fmt.Errorf("unable to convert map back to proto") + } + } + } + case callbackCommand: + if req.GetAttributes() == nil { + badFields[attributesField] = "No callback attributes provided." + break + } + + attrs := new(pb.OidcAuthMethodAuthenticateCallbackRequest) + if err := handlers.StructToProto(req.GetAttributes(), attrs, handlers.WithDiscardUnknownFields(true)); err != nil { + badFields[attributesField] = "Unable to parse callback request attributes." + break + } + + if attrs.GetError() != "" { + // TODO: Log more info. + return handlers.ApiErrorWithCodeAndMessage(codes.Unauthenticated, "OIDC provider returned an error.") + } + + if attrs.GetCode() == "" { + badFields[codeField] = "Code field not supplied in callback request." + } + + if attrs.GetState() == "" { + badFields[stateField] = "State field not supplied in callback request." + } + + case tokenCommand: + tType := strings.ToLower(strings.TrimSpace(req.GetTokenType())) + if tType != "" && tType != "token" && tType != "cookie" { + badFields[tokenTypeField] = `The only accepted types are "token" and "cookie".` + } + + default: + badFields[commandField] = "Invalid command for this auth method type." + } + + if len(badFields) > 0 { + return handlers.InvalidArgumentErrorf("Invalid fields provided in request.", badFields) + } + return nil +} + +func toStorageOidcAuthMethod(scopeId string, in *pb.AuthMethod) (out *oidc.AuthMethod, dryRun, forced bool, err error) { + const op = "authmethod_service.toStorageOidcAuthMethod" + if in == nil { + return nil, false, false, errors.New(errors.InvalidParameter, op, "nil auth method.") + } + attrs := &pb.OidcAuthMethodAttributes{} + if err := handlers.StructToProto(in.GetAttributes(), attrs); err != nil { + return nil, false, false, handlers.InvalidArgumentErrorf("Error in provided request.", + map[string]string{attributesField: "Attribute fields do not match the expected format."}) + } + clientId := attrs.GetClientId().GetValue() + clientSecret := oidc.ClientSecret(attrs.GetClientSecret().GetValue()) + + var opts []oidc.Option + if in.GetName() != nil { + opts = append(opts, oidc.WithName(in.GetName().GetValue())) + } + if in.GetDescription() != nil { + opts = append(opts, oidc.WithDescription(in.GetDescription().GetValue())) + } + + if iss := strings.TrimSpace(attrs.GetIssuer().GetValue()); iss != "" { + // Strip off everything after and including ".well-known/openid-configuration" + // but leave the "/" attached to the end. + iss = strings.SplitN(iss, ".well-known/", 2)[0] + issuer, err := url.Parse(iss) + if err != nil { + return nil, false, false, errors.Wrap(err, op, errors.WithMsg("cannot parse issuer"), errors.WithCode(errors.InvalidParameter)) + } + opts = append(opts, oidc.WithIssuer(issuer)) + } + if apiUrl := strings.TrimSpace(attrs.GetApiUrlPrefix().GetValue()); apiUrl != "" { + apiU, err := url.Parse(apiUrl) + if err != nil { + return nil, false, false, errors.Wrap(err, op, errors.WithMsg("cannot parse api_url_prefix"), errors.WithCode(errors.InvalidParameter)) + } + opts = append(opts, oidc.WithApiUrl(apiU)) + } + + if attrs.GetMaxAge() != nil { + maxAge := attrs.GetMaxAge().GetValue() + if maxAge == 0 { + opts = append(opts, oidc.WithMaxAge(-1)) + } else { + opts = append(opts, oidc.WithMaxAge(int(maxAge))) + } + } + var signAlgs []oidc.Alg + for _, a := range attrs.GetSigningAlgorithms() { + signAlgs = append(signAlgs, oidc.Alg(a)) + } + if len(signAlgs) > 0 { + opts = append(opts, oidc.WithSigningAlgs(signAlgs...)) + } + if len(attrs.GetAllowedAudiences()) > 0 { + opts = append(opts, oidc.WithAudClaims(attrs.GetAllowedAudiences()...)) + } + + if len(attrs.GetIdpCaCerts()) > 0 { + certs, err := oidc.ParseCertificates(attrs.GetIdpCaCerts()...) + if err != nil { + return nil, false, false, err + } + opts = append(opts, oidc.WithCertificates(certs...)) + } + + u, err := oidc.NewAuthMethod(scopeId, clientId, clientSecret, opts...) + if err != nil { + return nil, false, false, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to build auth method: %v.", err) + } + return u, attrs.GetDryRun(), attrs.GetDisableDiscoveredConfigValidation(), nil +} diff --git a/internal/servers/controller/handlers/authmethods/oidc_test.go b/internal/servers/controller/handlers/authmethods/oidc_test.go new file mode 100644 index 000000000..9ba2d8e5a --- /dev/null +++ b/internal/servers/controller/handlers/authmethods/oidc_test.go @@ -0,0 +1,1513 @@ +package authmethods_test + +import ( + "context" + "crypto/x509" + "fmt" + "net/http" + "net/http/httptest" + "regexp" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/boundary/internal/auth" + "github.com/hashicorp/boundary/internal/auth/oidc" + "github.com/hashicorp/boundary/internal/auth/password" + "github.com/hashicorp/boundary/internal/authtoken" + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/errors" + pb "github.com/hashicorp/boundary/internal/gen/controller/api/resources/authmethods" + scopepb "github.com/hashicorp/boundary/internal/gen/controller/api/resources/scopes" + pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services" + "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/servers/controller/common" + "github.com/hashicorp/boundary/internal/servers/controller/handlers" + "github.com/hashicorp/boundary/internal/servers/controller/handlers/authmethods" + "github.com/hashicorp/boundary/internal/types/scope" + capoidc "github.com/hashicorp/cap/oidc" + wrapping "github.com/hashicorp/go-kms-wrapping" + "github.com/jinzhu/gorm" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/genproto/protobuf/field_mask" + "google.golang.org/grpc/codes" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/testing/protocmp" + "google.golang.org/protobuf/types/known/structpb" + "google.golang.org/protobuf/types/known/timestamppb" + "google.golang.org/protobuf/types/known/wrapperspb" +) + +type setup struct { + ctx context.Context + conn *gorm.DB + rw *db.Db + rootWrapper wrapping.Wrapper + kmsCache *kms.Kms + iamRepo *iam.Repository + iamRepoFn common.IamRepoFactory + oidcRepoFn common.OidcAuthRepoFactory + pwRepoFn common.PasswordAuthRepoFactory + atRepoFn common.AuthTokenRepoFactory + org *iam.Scope + proj *iam.Scope + databaseWrapper wrapping.Wrapper + authMethodService authmethods.Service + testProvider *capoidc.TestProvider + testProviderAlg capoidc.Alg + testProviderCaCert []*x509.Certificate + testController *httptest.Server + authMethod *oidc.AuthMethod + testProviderAllowedRedirect string +} + +func getSetup(t *testing.T) setup { + t.Helper() + require := require.New(t) + var ret setup + var err error + ret.ctx = context.Background() + + ret.conn, _ = db.TestSetup(t, "postgres") + ret.rw = db.New(ret.conn) + ret.rootWrapper = db.TestWrapper(t) + ret.kmsCache = kms.TestKms(t, ret.conn, ret.rootWrapper) + + ret.iamRepo = iam.TestRepo(t, ret.conn, ret.rootWrapper) + ret.iamRepoFn = func() (*iam.Repository, error) { + return ret.iamRepo, nil + } + ret.oidcRepoFn = func() (*oidc.Repository, error) { + return oidc.NewRepository(ret.rw, ret.rw, ret.kmsCache) + } + ret.pwRepoFn = func() (*password.Repository, error) { + return password.NewRepository(ret.rw, ret.rw, ret.kmsCache) + } + ret.atRepoFn = func() (*authtoken.Repository, error) { + return authtoken.NewRepository(ret.rw, ret.rw, ret.kmsCache) + } + + ret.org, ret.proj = iam.TestScopes(t, ret.iamRepo) + ret.databaseWrapper, err = ret.kmsCache.GetWrapper(ret.ctx, ret.org.PublicId, kms.KeyPurposeDatabase) + require.NoError(err) + + ret.authMethodService, err = authmethods.NewService(ret.kmsCache, ret.pwRepoFn, ret.oidcRepoFn, ret.iamRepoFn, ret.atRepoFn) + require.NoError(err) + + ret.testProvider = capoidc.StartTestProvider(t) + _, _, ret.testProviderAlg, _ = ret.testProvider.SigningKeys() + ret.testProviderCaCert, err = oidc.ParseCertificates(ret.testProvider.CACert()) + require.NoError(err) + + ret.testController = httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + w.WriteHeader(200) + })) + t.Cleanup(ret.testController.Close) + + ret.authMethod = oidc.TestAuthMethod( + t, ret.conn, ret.databaseWrapper, ret.org.PublicId, oidc.ActivePublicState, + "test-rp", "fido", + oidc.WithIssuer(oidc.TestConvertToUrls(t, ret.testProvider.Addr())[0]), + oidc.WithApiUrl(oidc.TestConvertToUrls(t, ret.testController.URL)[0]), + oidc.WithSigningAlgs(oidc.Alg(ret.testProviderAlg)), + oidc.WithCertificates(ret.testProviderCaCert...), + ) + + ret.testProviderAllowedRedirect = fmt.Sprintf(oidc.CallbackEndpoint, ret.testController.URL, ret.authMethod.PublicId) + ret.testProvider.SetAllowedRedirectURIs([]string{ret.testProviderAllowedRedirect}) + + r, err := ret.oidcRepoFn() + require.NoError(err) + // update the test's auth method, now that we've added a new callback + ret.authMethod, err = r.LookupAuthMethod(ret.ctx, ret.authMethod.PublicId) + require.NoError(err) + require.NotNil(ret.authMethod) + return ret +} + +func TestUpdate_OIDC(t *testing.T) { + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + kms := kms.TestKms(t, conn, wrapper) + iamRepoFn := func() (*iam.Repository, error) { + return iam.TestRepo(t, conn, wrapper), nil + } + oidcRepoFn := func() (*oidc.Repository, error) { + return oidc.NewRepository(rw, rw, kms) + } + pwRepoFn := func() (*password.Repository, error) { + return password.NewRepository(rw, rw, kms) + } + atRepoFn := func() (*authtoken.Repository, error) { + return authtoken.NewRepository(rw, rw, kms) + } + iamRepo := iam.TestRepo(t, conn, wrapper) + + o, _ := iam.TestScopes(t, iamRepo) + tested, err := authmethods.NewService(kms, pwRepoFn, oidcRepoFn, iamRepoFn, atRepoFn) + require.NoError(t, err, "Error when getting new auth_method service.") + + defaultScopeInfo := &scopepb.ScopeInfo{Id: o.GetPublicId(), Type: o.GetType(), ParentScopeId: scope.Global.String()} + + tp := capoidc.StartTestProvider(t) + tpClientId := "alice-rp" + tpClientSecret := "her-dog's-name" + tp.SetClientCreds(tpClientId, tpClientSecret) + _, _, tpAlg, _ := tp.SigningKeys() + + defaultAttributeFields := func() map[string]*structpb.Value { + return map[string]*structpb.Value{ + "issuer": structpb.NewStringValue(tp.Addr()), + "client_id": structpb.NewStringValue("someclientid"), + "client_secret": structpb.NewStringValue("secret"), + "api_url_prefix": structpb.NewStringValue("http://example.com"), + "idp_ca_certs": func() *structpb.Value { + lv, _ := structpb.NewList([]interface{}{tp.CACert()}) + return structpb.NewListValue(lv) + }(), + "signing_algorithms": func() *structpb.Value { + lv, _ := structpb.NewList([]interface{}{string(tpAlg)}) + return structpb.NewListValue(lv) + }(), + } + } + defaultReadAttributeFields := func() map[string]*structpb.Value { + return map[string]*structpb.Value{ + "issuer": structpb.NewStringValue(tp.Addr()), + "client_id": structpb.NewStringValue("someclientid"), + "client_secret_hmac": structpb.NewStringValue(""), + "state": structpb.NewStringValue(string(oidc.ActivePrivateState)), + "api_url_prefix": structpb.NewStringValue("http://example.com"), + "callback_url": structpb.NewStringValue(fmt.Sprintf("http://example.com/v1/auth-methods/%s_[0-9A-z]*:authenticate:callback", oidc.AuthMethodPrefix)), + "idp_ca_certs": func() *structpb.Value { + lv, _ := structpb.NewList([]interface{}{tp.CACert()}) + return structpb.NewListValue(lv) + }(), + "signing_algorithms": func() *structpb.Value { + lv, _ := structpb.NewList([]interface{}{string(tpAlg)}) + return structpb.NewListValue(lv) + }(), + } + } + + freshAuthMethod := func(t *testing.T) (*pb.AuthMethod, func()) { + ctx := auth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId()) + am, err := tested.CreateAuthMethod(ctx, &pbs.CreateAuthMethodRequest{Item: &pb.AuthMethod{ + ScopeId: o.GetPublicId(), + Name: wrapperspb.String("default"), + Description: wrapperspb.String("default"), + Type: auth.OidcSubtype.String(), + Attributes: &structpb.Struct{ + Fields: defaultAttributeFields(), + }, + }}) + require.NoError(t, err) + + csr, err := tested.ChangeState(ctx, &pbs.ChangeStateRequest{ + Id: am.GetItem().GetId(), + Version: am.GetItem().GetVersion(), + Attributes: &structpb.Struct{ + Fields: map[string]*structpb.Value{"state": structpb.NewStringValue("active-private")}, + }, + }) + require.NoError(t, err) + + clean := func() { + _, err := tested.DeleteAuthMethod(auth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId()), + &pbs.DeleteAuthMethodRequest{Id: am.GetItem().GetId()}) + require.NoError(t, err) + } + return csr.GetItem(), clean + } + + cases := []struct { + name string + req *pbs.UpdateAuthMethodRequest + res *pbs.UpdateAuthMethodResponse + err error + wantErr bool + }{ + { + name: "Update an Existing AuthMethod", + req: &pbs.UpdateAuthMethodRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"name", "description"}, + }, + Item: &pb.AuthMethod{ + Name: &wrapperspb.StringValue{Value: "new"}, + Description: &wrapperspb.StringValue{Value: "desc"}, + Type: auth.OidcSubtype.String(), + }, + }, + res: &pbs.UpdateAuthMethodResponse{ + Item: &pb.AuthMethod{ + ScopeId: o.GetPublicId(), + Name: &wrapperspb.StringValue{Value: "new"}, + Description: &wrapperspb.StringValue{Value: "desc"}, + Type: auth.OidcSubtype.String(), + Attributes: &structpb.Struct{ + Fields: defaultReadAttributeFields(), + }, + Scope: defaultScopeInfo, + AuthorizedActions: oidcAuthorizedActions, + AuthorizedCollectionActions: authorizedCollectionActions, + }, + }, + }, + { + name: "Multiple Paths in single string", + req: &pbs.UpdateAuthMethodRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"name,description,type"}, + }, + Item: &pb.AuthMethod{ + Name: &wrapperspb.StringValue{Value: "new"}, + Description: &wrapperspb.StringValue{Value: "desc"}, + Type: auth.OidcSubtype.String(), + }, + }, + res: &pbs.UpdateAuthMethodResponse{ + Item: &pb.AuthMethod{ + ScopeId: o.GetPublicId(), + Name: &wrapperspb.StringValue{Value: "new"}, + Description: &wrapperspb.StringValue{Value: "desc"}, + Type: auth.OidcSubtype.String(), + Attributes: &structpb.Struct{ + Fields: defaultReadAttributeFields(), + }, + Scope: defaultScopeInfo, + AuthorizedActions: oidcAuthorizedActions, + AuthorizedCollectionActions: authorizedCollectionActions, + }, + }, + }, + { + name: "Update Issuer", + req: &pbs.UpdateAuthMethodRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"attributes.issuer"}, + }, + Item: &pb.AuthMethod{ + Attributes: &structpb.Struct{ + Fields: func() map[string]*structpb.Value { + f := defaultAttributeFields() + f["issuer"] = structpb.NewStringValue("http://localhost:72759/somepath/.well-known/openid-configuration") + f["disable_discovered_config_validation"] = structpb.NewBoolValue(true) + return f + }(), + }, + }, + }, + res: &pbs.UpdateAuthMethodResponse{ + Item: &pb.AuthMethod{ + ScopeId: o.GetPublicId(), + Name: &wrapperspb.StringValue{Value: "default"}, + Description: &wrapperspb.StringValue{Value: "default"}, + Type: auth.OidcSubtype.String(), + Attributes: &structpb.Struct{ + Fields: func() map[string]*structpb.Value { + f := defaultReadAttributeFields() + f["issuer"] = structpb.NewStringValue("http://localhost:72759/somepath/") + f["disable_discovered_config_validation"] = structpb.NewBoolValue(true) + return f + }(), + }, + Scope: defaultScopeInfo, + AuthorizedActions: oidcAuthorizedActions, + AuthorizedCollectionActions: authorizedCollectionActions, + }, + }, + }, + { + name: "No Update Mask", + req: &pbs.UpdateAuthMethodRequest{ + Item: &pb.AuthMethod{ + Name: &wrapperspb.StringValue{Value: "updated name"}, + Description: &wrapperspb.StringValue{Value: "updated desc"}, + }, + }, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "No Paths in Mask", + req: &pbs.UpdateAuthMethodRequest{ + UpdateMask: &field_mask.FieldMask{Paths: []string{}}, + Item: &pb.AuthMethod{ + Name: &wrapperspb.StringValue{Value: "updated name"}, + Description: &wrapperspb.StringValue{Value: "updated desc"}, + }, + }, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "Only non-existant paths in Mask", + req: &pbs.UpdateAuthMethodRequest{ + UpdateMask: &field_mask.FieldMask{Paths: []string{"nonexistant_field"}}, + Item: &pb.AuthMethod{ + Name: &wrapperspb.StringValue{Value: "updated name"}, + Description: &wrapperspb.StringValue{Value: "updated desc"}, + }, + }, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "Cant change type", + req: &pbs.UpdateAuthMethodRequest{ + UpdateMask: &field_mask.FieldMask{Paths: []string{"name", "type"}}, + Item: &pb.AuthMethod{ + Name: &wrapperspb.StringValue{Value: "updated name"}, + Type: auth.PasswordSubtype.String(), + }, + }, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "Cant change is primary", + req: &pbs.UpdateAuthMethodRequest{ + UpdateMask: &field_mask.FieldMask{Paths: []string{"is_primary"}}, + Item: &pb.AuthMethod{ + IsPrimary: true, + }, + }, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "Unset Name", + req: &pbs.UpdateAuthMethodRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"name"}, + }, + Item: &pb.AuthMethod{ + Description: &wrapperspb.StringValue{Value: "ignored"}, + }, + }, + res: &pbs.UpdateAuthMethodResponse{ + Item: &pb.AuthMethod{ + ScopeId: o.GetPublicId(), + Description: &wrapperspb.StringValue{Value: "default"}, + Type: auth.OidcSubtype.String(), + Attributes: &structpb.Struct{ + Fields: defaultReadAttributeFields(), + }, + Scope: defaultScopeInfo, + AuthorizedActions: oidcAuthorizedActions, + AuthorizedCollectionActions: authorizedCollectionActions, + }, + }, + }, + { + name: "Unset Description", + req: &pbs.UpdateAuthMethodRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"description"}, + }, + Item: &pb.AuthMethod{ + Name: &wrapperspb.StringValue{Value: "ignored"}, + }, + }, + res: &pbs.UpdateAuthMethodResponse{ + Item: &pb.AuthMethod{ + ScopeId: o.GetPublicId(), + Name: &wrapperspb.StringValue{Value: "default"}, + Type: auth.OidcSubtype.String(), + Attributes: &structpb.Struct{ + Fields: defaultReadAttributeFields(), + }, + Scope: defaultScopeInfo, + AuthorizedActions: oidcAuthorizedActions, + AuthorizedCollectionActions: authorizedCollectionActions, + }, + }, + }, + { + name: "Update Only Name", + req: &pbs.UpdateAuthMethodRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"name"}, + }, + Item: &pb.AuthMethod{ + Name: &wrapperspb.StringValue{Value: "updated"}, + Description: &wrapperspb.StringValue{Value: "ignored"}, + }, + }, + res: &pbs.UpdateAuthMethodResponse{ + Item: &pb.AuthMethod{ + ScopeId: o.GetPublicId(), + Name: &wrapperspb.StringValue{Value: "updated"}, + Description: &wrapperspb.StringValue{Value: "default"}, + Type: auth.OidcSubtype.String(), + Attributes: &structpb.Struct{ + Fields: defaultReadAttributeFields(), + }, + Scope: defaultScopeInfo, + AuthorizedActions: oidcAuthorizedActions, + AuthorizedCollectionActions: authorizedCollectionActions, + }, + }, + }, + { + name: "Update Only Description", + req: &pbs.UpdateAuthMethodRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"description"}, + }, + Item: &pb.AuthMethod{ + Name: &wrapperspb.StringValue{Value: "ignored"}, + Description: &wrapperspb.StringValue{Value: "notignored"}, + }, + }, + res: &pbs.UpdateAuthMethodResponse{ + Item: &pb.AuthMethod{ + ScopeId: o.GetPublicId(), + Name: &wrapperspb.StringValue{Value: "default"}, + Description: &wrapperspb.StringValue{Value: "notignored"}, + Type: auth.OidcSubtype.String(), + Attributes: &structpb.Struct{ + Fields: defaultReadAttributeFields(), + }, + Scope: defaultScopeInfo, + AuthorizedActions: oidcAuthorizedActions, + AuthorizedCollectionActions: authorizedCollectionActions, + }, + }, + }, + { + name: "Update a Non Existing AuthMethod", + req: &pbs.UpdateAuthMethodRequest{ + Id: password.AuthMethodPrefix + "_DoesntExis", + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"description"}, + }, + Item: &pb.AuthMethod{ + Name: &wrapperspb.StringValue{Value: "new"}, + Description: &wrapperspb.StringValue{Value: "desc"}, + }, + }, + err: handlers.ApiErrorWithCode(codes.NotFound), + }, + { + name: "Cant change Id", + req: &pbs.UpdateAuthMethodRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"id"}, + }, + Item: &pb.AuthMethod{ + Id: password.AuthMethodPrefix + "_somethinge", + Name: &wrapperspb.StringValue{Value: "new"}, + Description: &wrapperspb.StringValue{Value: "new desc"}, + }, + }, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "Cant specify Created Time", + req: &pbs.UpdateAuthMethodRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"created_time"}, + }, + Item: &pb.AuthMethod{ + CreatedTime: timestamppb.Now(), + }, + }, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "Cant specify Updated Time", + req: &pbs.UpdateAuthMethodRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"updated_time"}, + }, + Item: &pb.AuthMethod{ + UpdatedTime: timestamppb.Now(), + }, + }, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "Cant specify Type", + req: &pbs.UpdateAuthMethodRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"type"}, + }, + Item: &pb.AuthMethod{ + Type: auth.OidcSubtype.String(), + }, + }, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "Unset Client Id", + req: &pbs.UpdateAuthMethodRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"attributes.client_id"}, + }, + Item: &pb.AuthMethod{}, + }, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "Unset Client Secret", + req: &pbs.UpdateAuthMethodRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"attributes.client_secret"}, + }, + Item: &pb.AuthMethod{}, + }, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "Unset Signing Algorithms", + req: &pbs.UpdateAuthMethodRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"attributes.signing_algorithms"}, + }, + Item: &pb.AuthMethod{}, + }, + wantErr: true, + }, + { + name: "Set Max Age to zero", + req: &pbs.UpdateAuthMethodRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"attributes.max_age"}, + }, + Item: &pb.AuthMethod{ + Attributes: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "max_age": structpb.NewNumberValue(0), + }, + }, + }, + }, + res: &pbs.UpdateAuthMethodResponse{ + Item: &pb.AuthMethod{ + ScopeId: o.GetPublicId(), + Name: &wrapperspb.StringValue{Value: "default"}, + Description: &wrapperspb.StringValue{Value: "default"}, + Type: auth.OidcSubtype.String(), + Attributes: &structpb.Struct{ + Fields: func() map[string]*structpb.Value { + f := defaultReadAttributeFields() + f["max_age"] = structpb.NewNumberValue(0) + return f + }(), + }, + Scope: defaultScopeInfo, + AuthorizedActions: oidcAuthorizedActions, + AuthorizedCollectionActions: authorizedCollectionActions, + }, + }, + }, + { + name: "Change Max Age", + req: &pbs.UpdateAuthMethodRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"attributes.max_age"}, + }, + Item: &pb.AuthMethod{ + Attributes: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "max_age": structpb.NewNumberValue(4), + }, + }, + }, + }, + res: &pbs.UpdateAuthMethodResponse{ + Item: &pb.AuthMethod{ + ScopeId: o.GetPublicId(), + Name: &wrapperspb.StringValue{Value: "default"}, + Description: &wrapperspb.StringValue{Value: "default"}, + Type: auth.OidcSubtype.String(), + Attributes: &structpb.Struct{ + Fields: func() map[string]*structpb.Value { + f := defaultReadAttributeFields() + f["max_age"] = structpb.NewNumberValue(4) + return f + }(), + }, + Scope: defaultScopeInfo, + AuthorizedActions: oidcAuthorizedActions, + AuthorizedCollectionActions: authorizedCollectionActions, + }, + }, + }, + { + name: "Change Client Id", + req: &pbs.UpdateAuthMethodRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"attributes.client_id"}, + }, + Item: &pb.AuthMethod{ + Attributes: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "client_id": structpb.NewStringValue("new id"), + }, + }, + }, + }, + res: &pbs.UpdateAuthMethodResponse{ + Item: &pb.AuthMethod{ + ScopeId: o.GetPublicId(), + Name: &wrapperspb.StringValue{Value: "default"}, + Description: &wrapperspb.StringValue{Value: "default"}, + Type: auth.OidcSubtype.String(), + Attributes: &structpb.Struct{ + Fields: func() map[string]*structpb.Value { + f := defaultReadAttributeFields() + f["client_id"] = structpb.NewStringValue("new id") + return f + }(), + }, + Scope: defaultScopeInfo, + AuthorizedActions: oidcAuthorizedActions, + AuthorizedCollectionActions: authorizedCollectionActions, + }, + }, + }, + { + name: "Unset Api Url Prefix", + req: &pbs.UpdateAuthMethodRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"attributes.api_url_prefix"}, + }, + Item: &pb.AuthMethod{ + Attributes: &structpb.Struct{}, + }, + }, + wantErr: true, + }, + { + name: "Change Api Url Prefix", + req: &pbs.UpdateAuthMethodRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"attributes.api_url_prefix"}, + }, + Item: &pb.AuthMethod{ + Attributes: &structpb.Struct{ + Fields: func() map[string]*structpb.Value { + f := map[string]*structpb.Value{ + "api_url_prefix": structpb.NewStringValue("https://callback.prefix:9281/path"), + } + return f + }(), + }, + }, + }, + res: &pbs.UpdateAuthMethodResponse{ + Item: &pb.AuthMethod{ + ScopeId: o.GetPublicId(), + Name: &wrapperspb.StringValue{Value: "default"}, + Description: &wrapperspb.StringValue{Value: "default"}, + Type: auth.OidcSubtype.String(), + Attributes: &structpb.Struct{ + Fields: func() map[string]*structpb.Value { + f := defaultReadAttributeFields() + f["api_url_prefix"] = structpb.NewStringValue("https://callback.prefix:9281/path") + f["callback_url"] = structpb.NewStringValue(fmt.Sprintf("https://callback.prefix:9281/path/v1/auth-methods/%s_[0-9A-z]*:authenticate:callback", oidc.AuthMethodPrefix)) + return f + }(), + }, + Scope: defaultScopeInfo, + AuthorizedActions: oidcAuthorizedActions, + AuthorizedCollectionActions: authorizedCollectionActions, + }, + }, + }, + { + name: "Change Allowed Audiences", + req: &pbs.UpdateAuthMethodRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"attributes.allowed_audiences"}, + }, + Item: &pb.AuthMethod{ + Attributes: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "allowed_audiences": func() *structpb.Value { + lv, _ := structpb.NewList([]interface{}{"bar", "foo"}) + return structpb.NewListValue(lv) + }(), + }, + }, + }, + }, + res: &pbs.UpdateAuthMethodResponse{ + Item: &pb.AuthMethod{ + ScopeId: o.GetPublicId(), + Name: &wrapperspb.StringValue{Value: "default"}, + Description: &wrapperspb.StringValue{Value: "default"}, + Type: auth.OidcSubtype.String(), + Attributes: &structpb.Struct{ + Fields: func() map[string]*structpb.Value { + f := defaultReadAttributeFields() + f["allowed_audiences"] = func() *structpb.Value { + lv, _ := structpb.NewList([]interface{}{"bar", "foo"}) + return structpb.NewListValue(lv) + }() + return f + }(), + }, + Scope: defaultScopeInfo, + AuthorizedActions: oidcAuthorizedActions, + AuthorizedCollectionActions: authorizedCollectionActions, + }, + }, + }, + { + name: "Unset Issuer Is Incomplete", + req: &pbs.UpdateAuthMethodRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"attributes.issuer"}, + }, + Item: &pb.AuthMethod{}, + }, + wantErr: true, + }, + { + name: "Unsupported Signing Algorithms", + req: &pbs.UpdateAuthMethodRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"attributes.signing_algorithms"}, + }, + Item: &pb.AuthMethod{ + Attributes: &structpb.Struct{ + Fields: func() map[string]*structpb.Value { + f := defaultAttributeFields() + f["signing_algorithms"] = func() *structpb.Value { + lv, _ := structpb.NewList([]interface{}{string(oidc.EdDSA)}) + return structpb.NewListValue(lv) + }() + return f + }(), + }, + }, + }, + wantErr: true, + }, + { + name: "Forced Unsupported Signing Algorithms", + req: &pbs.UpdateAuthMethodRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"attributes.signing_algorithms"}, + }, + Item: &pb.AuthMethod{ + Attributes: &structpb.Struct{ + Fields: func() map[string]*structpb.Value { + f := defaultAttributeFields() + f["disable_discovered_config_validation"] = structpb.NewBoolValue(true) + f["signing_algorithms"] = func() *structpb.Value { + lv, _ := structpb.NewList([]interface{}{string(oidc.EdDSA)}) + return structpb.NewListValue(lv) + }() + return f + }(), + }, + }, + }, + res: &pbs.UpdateAuthMethodResponse{ + Item: &pb.AuthMethod{ + ScopeId: o.GetPublicId(), + Name: &wrapperspb.StringValue{Value: "default"}, + Description: &wrapperspb.StringValue{Value: "default"}, + Type: auth.OidcSubtype.String(), + Attributes: &structpb.Struct{ + Fields: func() map[string]*structpb.Value { + f := defaultReadAttributeFields() + f["signing_algorithms"] = func() *structpb.Value { + lv, _ := structpb.NewList([]interface{}{string(oidc.EdDSA)}) + return structpb.NewListValue(lv) + }() + f["disable_discovered_config_validation"] = structpb.NewBoolValue(true) + return f + }(), + }, + Scope: defaultScopeInfo, + AuthorizedActions: oidcAuthorizedActions, + AuthorizedCollectionActions: authorizedCollectionActions, + }, + }, + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + am, cleanup := freshAuthMethod(t) + defer cleanup() + + tc.req.Item.Version = am.GetVersion() + + if tc.req.GetId() == "" { + tc.req.Id = am.GetId() + } + + if tc.res != nil && tc.res.Item != nil { + tc.res.Item.Id = am.GetId() + tc.res.Item.CreatedTime = am.GetCreatedTime() + } + + got, gErr := tested.UpdateAuthMethod(auth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId()), tc.req) + // TODO: When handlers move to domain errors remove wantErr and rely errors.Match here. + if tc.err != nil || tc.wantErr { + require.Error(gErr) + if tc.err != nil { + assert.True(errors.Is(gErr, tc.err), "UpdateAuthMethod(%+v) got error %v, wanted %v", tc.req, gErr, tc.err) + } + return + } + require.NoError(gErr) + if tc.res == nil { + require.Nil(got) + } + + if got != nil { + assert.NotNilf(tc.res, "Expected UpdateAuthMethod response to be nil, but was %v", got) + gotUpdateTime := got.GetItem().GetUpdatedTime().AsTime() + created := am.GetCreatedTime().AsTime() + + // Verify it is a auth_method updated after it was created + assert.True(gotUpdateTime.After(created), "Updated auth_method should have been updated after it's creation. Was updated %v, which is after %v", gotUpdateTime, created) + + // Clear all values which are hard to compare against. + if _, ok := got.Item.Attributes.Fields["client_secret_hmac"]; ok { + assert.NotEqual("secret", got.Item.Attributes.Fields["client_secret_hmac"]) + got.Item.Attributes.Fields["client_secret_hmac"] = structpb.NewStringValue("") + } + if _, ok := got.Item.Attributes.Fields["callback_url"]; ok { + exp := tc.res.Item.Attributes.Fields["callback_url"].GetStringValue() + gVal := got.Item.Attributes.Fields["callback_url"].GetStringValue() + matches, err := regexp.MatchString(exp, gVal) + require.NoError(err) + assert.True(matches, "%q doesn't match %q", gVal, exp) + delete(got.Item.Attributes.Fields, "callback_url") + delete(tc.res.Item.Attributes.Fields, "callback_url") + } + + got.Item.UpdatedTime, tc.res.Item.UpdatedTime = nil, nil + + assert.EqualValues(3, got.Item.Version) + tc.res.Item.Version = 3 + } + assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform(), protocmp.SortRepeatedFields(got)), "UpdateAuthMethod(%q) got response %q, wanted %q", tc.req, got, tc.res) + }) + } +} + +func TestUpdate_OIDCDryRun(t *testing.T) { + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, wrapper) + iamRepoFn := func() (*iam.Repository, error) { + return iam.TestRepo(t, conn, wrapper), nil + } + oidcRepoFn := func() (*oidc.Repository, error) { + return oidc.NewRepository(rw, rw, kmsCache) + } + pwRepoFn := func() (*password.Repository, error) { + return password.NewRepository(rw, rw, kmsCache) + } + atRepoFn := func() (*authtoken.Repository, error) { + return authtoken.NewRepository(rw, rw, kmsCache) + } + iamRepo := iam.TestRepo(t, conn, wrapper) + + o, _ := iam.TestScopes(t, iamRepo) + + tp := capoidc.StartTestProvider(t) + tpClientId := "alice-rp" + tpClientSecret := "her-dog's-name" + tp.SetClientCreds(tpClientId, tpClientSecret) + _, _, tpAlg, _ := tp.SigningKeys() + + tpCert, err := oidc.ParseCertificates(tp.CACert()) + require.NoError(t, err) + databaseWrapper, err := kmsCache.GetWrapper(context.Background(), o.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + + am := oidc.TestAuthMethod(t, + conn, databaseWrapper, + o.PublicId, + oidc.ActivePrivateState, + tpClientId, oidc.ClientSecret(tpClientSecret), + oidc.WithName("default"), + oidc.WithDescription("default"), + oidc.WithCertificates(tpCert[0]), + oidc.WithSigningAlgs(oidc.Alg(tpAlg)), + oidc.WithIssuer(oidc.TestConvertToUrls(t, tp.Addr())[0]), + oidc.WithApiUrl(oidc.TestConvertToUrls(t, "http://localhost/")[0]), + ) + wireAuthMethod := &pb.AuthMethod{ + Id: am.GetPublicId(), + ScopeId: am.GetScopeId(), + Scope: &scopepb.ScopeInfo{Id: o.GetPublicId(), Type: o.GetType(), ParentScopeId: scope.Global.String()}, + Name: wrapperspb.String(am.GetName()), + Description: wrapperspb.String(am.GetDescription()), + CreatedTime: am.GetCreateTime().GetTimestamp(), + UpdatedTime: am.GetUpdateTime().GetTimestamp(), + Version: am.GetVersion(), + Type: auth.OidcSubtype.String(), + AuthorizedActions: oidcAuthorizedActions, + AuthorizedCollectionActions: authorizedCollectionActions, + Attributes: &structpb.Struct{Fields: map[string]*structpb.Value{ + "api_url_prefix": structpb.NewStringValue(am.GetApiUrl()), + "callback_url": structpb.NewStringValue(fmt.Sprintf("%s/v1/auth-methods/%s:authenticate:callback", am.GetApiUrl(), am.GetPublicId())), + "client_id": structpb.NewStringValue(am.GetClientId()), + "client_secret_hmac": structpb.NewStringValue(am.GetClientSecretHmac()), + "issuer": structpb.NewStringValue(am.GetIssuer()), + "state": structpb.NewStringValue(am.GetOperationalState()), + "idp_ca_certs": func() *structpb.Value { + lv, _ := structpb.NewList([]interface{}{tp.CACert()}) + return structpb.NewListValue(lv) + }(), + "signing_algorithms": func() *structpb.Value { + lv, _ := structpb.NewList([]interface{}{string(tpAlg)}) + return structpb.NewListValue(lv) + }(), + }}, + } + + tested, err := authmethods.NewService(kmsCache, pwRepoFn, oidcRepoFn, iamRepoFn, atRepoFn) + require.NoError(t, err, "Error when getting new auth_method service.") + cases := []struct { + name string + id string + mask []string + item *pb.AuthMethod + want *pb.AuthMethod + err error + wantErr bool + }{ + { + name: "Update an Existing AuthMethod", + id: am.GetPublicId(), + mask: []string{"name", "description", "attributes.dry_run"}, + item: &pb.AuthMethod{ + Name: wrapperspb.String("updated"), + Description: wrapperspb.String("updated"), + Version: am.GetVersion(), + Attributes: &structpb.Struct{Fields: map[string]*structpb.Value{ + "dry_run": structpb.NewBoolValue(true), + }}, + }, + want: func() *pb.AuthMethod { + c := proto.Clone(wireAuthMethod).(*pb.AuthMethod) + c.Name = wrapperspb.String("updated") + c.Description = wrapperspb.String("updated") + c.GetAttributes().GetFields()["dry_run"] = structpb.NewBoolValue(true) + return c + }(), + }, + { + name: "Update To Make Not Complete", + id: am.GetPublicId(), + mask: []string{"attributes.issuer"}, + item: &pb.AuthMethod{ + Version: am.GetVersion(), + Attributes: &structpb.Struct{Fields: map[string]*structpb.Value{ + "dry_run": structpb.NewBoolValue(true), + }}, + }, + wantErr: true, + }, + { + name: "Update To Make Not Validated", + id: am.GetPublicId(), + mask: []string{"attributes.signing_algorithms"}, + item: &pb.AuthMethod{ + Version: am.GetVersion(), + Attributes: &structpb.Struct{Fields: map[string]*structpb.Value{ + "dry_run": structpb.NewBoolValue(true), + "signing_algorithms": func() *structpb.Value { + lv, _ := structpb.NewList([]interface{}{string(oidc.RS512)}) + return structpb.NewListValue(lv) + }(), + }}, + }, + wantErr: true, + }, + { + name: "Update dry run with force", + id: am.GetPublicId(), + mask: []string{"name"}, + item: &pb.AuthMethod{ + Version: am.GetVersion(), + Attributes: &structpb.Struct{Fields: map[string]*structpb.Value{ + "dry_run": structpb.NewBoolValue(true), + "disable_discovered_config_validation": structpb.NewBoolValue(true), + }}, + }, + wantErr: true, + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + dryUpdated, err := tested.UpdateAuthMethod(auth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId()), + &pbs.UpdateAuthMethodRequest{ + Id: tc.id, + Item: tc.item, + UpdateMask: &field_mask.FieldMask{Paths: tc.mask}, + }) + + // TODO: When handlers move to domain errors remove wantErr and rely errors.Match here. + if tc.err != nil || tc.wantErr { + require.Error(t, err) + if tc.err != nil { + assert.True(t, errors.Is(err, tc.err)) + } + return + } + require.NoError(t, err) + assert.Empty(t, cmp.Diff(dryUpdated.GetItem(), tc.want, protocmp.Transform(), protocmp.SortRepeatedFields(dryUpdated))) + + got, err := tested.GetAuthMethod(auth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId()), &pbs.GetAuthMethodRequest{Id: tc.id}) + require.NoError(t, err) + assert.Empty(t, cmp.Diff(got.GetItem(), wireAuthMethod, protocmp.Transform())) + }) + } +} + +func TestChangeState_OIDC(t *testing.T) { + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + kmsCache := kms.TestKms(t, conn, wrapper) + iamRepoFn := func() (*iam.Repository, error) { + return iam.TestRepo(t, conn, wrapper), nil + } + oidcRepoFn := func() (*oidc.Repository, error) { + return oidc.NewRepository(rw, rw, kmsCache) + } + pwRepoFn := func() (*password.Repository, error) { + return password.NewRepository(rw, rw, kmsCache) + } + atRepoFn := func() (*authtoken.Repository, error) { + return authtoken.NewRepository(rw, rw, kmsCache) + } + iamRepo := iam.TestRepo(t, conn, wrapper) + + o, _ := iam.TestScopes(t, iamRepo) + pwam := password.TestAuthMethods(t, conn, o.GetPublicId(), 1)[0] + + databaseWrapper, err := kmsCache.GetWrapper(context.Background(), o.PublicId, kms.KeyPurposeDatabase) + require.NoError(t, err) + + tp := capoidc.StartTestProvider(t) + tpClientId := "alice-rp" + tpClientSecret := "her-dog's-name" + tp.SetClientCreds(tpClientId, tpClientSecret) + _, _, tpAlg, _ := tp.SigningKeys() + tpCert, err := oidc.ParseCertificates(tp.CACert()) + require.NoError(t, err) + + incompleteAm := oidc.TestAuthMethod(t, conn, databaseWrapper, o.PublicId, "inactive", "client id", "secret", + oidc.WithIssuer(oidc.TestConvertToUrls(t, "https://alice.com")[0]), oidc.WithApiUrl(oidc.TestConvertToUrls(t, "https://api.com")[0])) + oidcam := oidc.TestAuthMethod(t, conn, databaseWrapper, o.PublicId, "inactive", tpClientId, oidc.ClientSecret(tpClientSecret), + oidc.WithIssuer(oidc.TestConvertToUrls(t, tp.Addr())[0]), oidc.WithSigningAlgs(oidc.Alg(tpAlg)), oidc.WithApiUrl(oidc.TestConvertToUrls(t, "https://example.callback:58")[0]), oidc.WithCertificates(tpCert...)) + mismatchedAM := oidc.TestAuthMethod(t, conn, databaseWrapper, o.PublicId, "inactive", "different_client_id", oidc.ClientSecret(tpClientSecret), + oidc.WithIssuer(oidc.TestConvertToUrls(t, tp.Addr())[0]), oidc.WithSigningAlgs(oidc.EdDSA), oidc.WithApiUrl(oidc.TestConvertToUrls(t, "https://example.callback:58")[0]), oidc.WithCertificates(tpCert...)) + + s, err := authmethods.NewService(kmsCache, pwRepoFn, oidcRepoFn, iamRepoFn, atRepoFn) + require.NoError(t, err, "Error when getting new auth_method service.") + + signingAlg := func() *structpb.Value { + lv, err := structpb.NewList([]interface{}{string(tpAlg)}) + require.NoError(t, err) + return structpb.NewListValue(lv) + }() + + certs := func() *structpb.Value { + lv, err := structpb.NewList([]interface{}{tp.CACert()}) + require.NoError(t, err) + return structpb.NewListValue(lv) + }() + + wantTemplate := &pb.AuthMethod{ + Id: oidcam.GetPublicId(), + ScopeId: oidcam.GetScopeId(), + CreatedTime: oidcam.CreateTime.GetTimestamp(), + UpdatedTime: oidcam.UpdateTime.GetTimestamp(), + Type: auth.OidcSubtype.String(), + Attributes: &structpb.Struct{Fields: map[string]*structpb.Value{ + "issuer": structpb.NewStringValue(oidcam.GetIssuer()), + "client_id": structpb.NewStringValue(tpClientId), + "client_secret_hmac": structpb.NewStringValue(""), + "state": structpb.NewStringValue(string(oidc.InactiveState)), + "callback_url": structpb.NewStringValue("https://example.callback:58/v1/auth-methods/amoidc_[0-9A-z]*:authenticate:callback"), + "api_url_prefix": structpb.NewStringValue("https://example.callback:58"), + "signing_algorithms": signingAlg, + "idp_ca_certs": certs, + }}, + Version: 1, + Scope: &scopepb.ScopeInfo{ + Id: o.GetPublicId(), + Type: o.GetType(), + ParentScopeId: scope.Global.String(), + }, + AuthorizedActions: oidcAuthorizedActions, + AuthorizedCollectionActions: authorizedCollectionActions, + } + + toState := func(s string) *structpb.Struct { + return &structpb.Struct{Fields: map[string]*structpb.Value{"state": structpb.NewStringValue(s)}} + } + + // These test cases must be run in this order since these tests rely on the correct versions being provided + cases := []struct { + name string + req *pbs.ChangeStateRequest + res *pbs.ChangeStateResponse + err bool + }{ + { + name: "Password Auth Method", + req: &pbs.ChangeStateRequest{Id: pwam.GetPublicId(), Version: pwam.GetVersion(), Attributes: toState("inactive")}, + err: true, + }, + { + name: "No Version Specified", + req: &pbs.ChangeStateRequest{Id: oidcam.GetPublicId(), Attributes: toState("inactive")}, + err: true, + }, + { + name: "Keep Inactive", + req: &pbs.ChangeStateRequest{Id: oidcam.GetPublicId(), Version: oidcam.GetVersion(), Attributes: toState("inactive")}, + res: &pbs.ChangeStateResponse{Item: func() *pb.AuthMethod { + am := proto.Clone(wantTemplate).(*pb.AuthMethod) + return am + }()}, + }, + { + name: "Make Incomplete Private", + req: &pbs.ChangeStateRequest{Id: incompleteAm.GetPublicId(), Version: oidcam.GetVersion(), Attributes: toState("active-private")}, + err: true, + }, + { + name: "Make Incomplete Public", + req: &pbs.ChangeStateRequest{Id: incompleteAm.GetPublicId(), Version: oidcam.GetVersion(), Attributes: toState("active-public")}, + err: true, + }, + { + name: "Mismatched To Public", + req: &pbs.ChangeStateRequest{ + Id: mismatchedAM.GetPublicId(), + Version: mismatchedAM.GetVersion(), + Attributes: toState("active-public"), + }, + err: true, + }, + { + name: "Force Mismatched To Public", + req: &pbs.ChangeStateRequest{ + Id: mismatchedAM.GetPublicId(), + Version: mismatchedAM.GetVersion(), + Attributes: func() *structpb.Struct { + s := toState("active-public") + s.Fields["override_oidc_discovery_url_config"] = structpb.NewBoolValue(true) + return s + }(), + }, + res: &pbs.ChangeStateResponse{Item: func() *pb.AuthMethod { + am := proto.Clone(wantTemplate).(*pb.AuthMethod) + am.Id = mismatchedAM.PublicId + am.Attributes.Fields["disable_discovered_config_validation"] = structpb.NewBoolValue(true) + am.Attributes.Fields["state"] = structpb.NewStringValue("active-public") + am.Attributes.Fields["client_id"] = structpb.NewStringValue(mismatchedAM.ClientId) + am.Attributes.Fields["signing_algorithms"] = func() *structpb.Value { + lv, _ := structpb.NewList([]interface{}{string(oidc.EdDSA)}) + return structpb.NewListValue(lv) + }() + am.CreatedTime = mismatchedAM.CreateTime.GetTimestamp() + am.Version = 2 + return am + }()}, + }, + { + name: "Make Complete Private", + req: &pbs.ChangeStateRequest{Id: oidcam.GetPublicId(), Version: oidcam.GetVersion(), Attributes: toState("active-private")}, + res: &pbs.ChangeStateResponse{Item: func() *pb.AuthMethod { + am := proto.Clone(wantTemplate).(*pb.AuthMethod) + am.Attributes.Fields["state"] = structpb.NewStringValue("active-private") + am.Version = 2 + return am + }()}, + }, + { + name: "Make Complete Public", + req: &pbs.ChangeStateRequest{Id: oidcam.GetPublicId(), Version: 2, Attributes: toState("active-public")}, + res: &pbs.ChangeStateResponse{Item: func() *pb.AuthMethod { + am := proto.Clone(wantTemplate).(*pb.AuthMethod) + am.Attributes.Fields["state"] = structpb.NewStringValue("active-public") + am.Version = 3 + return am + }()}, + }, + { + name: "Make Complete Inactive", + req: &pbs.ChangeStateRequest{Id: oidcam.GetPublicId(), Version: 3, Attributes: toState("inactive")}, + res: &pbs.ChangeStateResponse{Item: func() *pb.AuthMethod { + am := proto.Clone(wantTemplate).(*pb.AuthMethod) + am.Attributes.Fields["state"] = structpb.NewStringValue("inactive") + am.Version = 4 + return am + }()}, + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + + got, gErr := s.ChangeState(auth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId()), tc.req) + if tc.err { + require.Error(gErr) + return + } + require.NoError(gErr) + if _, ok := got.Item.Attributes.Fields["client_secret_hmac"]; ok { + got.Item.Attributes.Fields["client_secret_hmac"] = structpb.NewStringValue("") + } + if _, ok := got.Item.Attributes.Fields["callback_url"]; ok { + exp := tc.res.Item.Attributes.Fields["callback_url"].GetStringValue() + gVal := got.Item.Attributes.Fields["callback_url"].GetStringValue() + matches, err := regexp.MatchString(exp, gVal) + require.NoError(err) + assert.True(matches, "%q doesn't match %q", gVal, exp) + delete(got.Item.Attributes.Fields, "callback_url") + delete(tc.res.Item.Attributes.Fields, "callback_url") + } + got.Item.UpdatedTime, tc.res.Item.UpdatedTime = nil, nil + + assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "ChangeState() got response %q, wanted %q", got, tc.res) + }) + } +} + +func TestAuthenticate_OIDC_Start(t *testing.T) { + s := getSetup(t) + + cases := []struct { + name string + request *pbs.AuthenticateRequest + wantErr error + }{ + { + name: "no command", + request: &pbs.AuthenticateRequest{ + AuthMethodId: s.authMethod.GetPublicId(), + }, + wantErr: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "bad command", + request: &pbs.AuthenticateRequest{ + Command: "bad", + AuthMethodId: s.authMethod.GetPublicId(), + }, + wantErr: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "no auth method id", + request: &pbs.AuthenticateRequest{ + Command: "start", + }, + wantErr: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "good request, no attributes", + request: &pbs.AuthenticateRequest{ + Command: "start", + AuthMethodId: s.authMethod.GetPublicId(), + }, + }, + // NOTE: We can't really test bad roundtrip payload attributes because + // any type structpb lets us use in creation will be valid for JSON, and + // attempting to force with e.g. json.RawMessage causes structpb to + // error. This is okay; in the real world anything coming in would + // _have_ to be valid JSON because it came in on the JSON API + // successfully! + // + // The below test is really testing that it doesn't error; it won't be + // until a test all the way through the token step that we can verify + // the underlying functionality, at least without some very onerous + // decryption steps. Not worth it; just validate once we perform all + // steps! + { + name: "roundtrip payload attributes", + request: &pbs.AuthenticateRequest{ + Command: "start", + AuthMethodId: s.authMethod.GetPublicId(), + Attributes: func() *structpb.Struct { + ret, err := structpb.NewStruct(map[string]interface{}{ + "roundtrip_payload": map[string]interface{}{ + "foo": "bar", + "baz": true, + }, + }) + require.NoError(t, err) + return ret + }(), + }, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + got, err := s.authMethodService.Authenticate(auth.DisabledAuthTestContext(s.iamRepoFn, s.org.GetPublicId()), tc.request) + if tc.wantErr != nil { + assert.Error(err) + assert.Truef(errors.Is(err, tc.wantErr), "Got %#v, wanted %#v", err, tc.wantErr) + return + } + require.NoError(err) + require.Equal(got.GetCommand(), "start") + // We can't really compare directly as a lot of the values contain + // random data, so just verify existence + require.NotNil(got.GetAttributes()) + m := got.GetAttributes().AsMap() + require.NotNil(m) + require.Contains(m, "auth_url") + require.NotEmpty(m["auth_url"]) + require.Contains(m, "token_url") + require.NotEmpty(m["token_url"]) + require.Contains(m, "token_id") + require.NotEmpty(m["token_id"]) + }) + } +} + +func TestAuthenticate_OIDC_Token(t *testing.T) { + s := getSetup(t) + testAtRepo, err := authtoken.NewRepository(s.rw, s.rw, s.kmsCache) + require.NoError(t, err) + + // a reusable test authmethod for the unit tests + testAuthMethod := oidc.TestAuthMethod(t, s.conn, s.databaseWrapper, s.org.PublicId, oidc.ActivePublicState, + "alice-rp", "fido", + oidc.WithIssuer(oidc.TestConvertToUrls(t, "https://alice.com")[0]), + oidc.WithSigningAlgs(oidc.Alg(oidc.RS256)), + oidc.WithApiUrl(oidc.TestConvertToUrls(t, "https://alice.com/callback")[0])) + + testAcct := oidc.TestAccount(t, s.conn, testAuthMethod, "alice") + testUser := iam.TestUser(t, s.iamRepo, s.org.PublicId, iam.WithAccountIds(testAcct.PublicId)) + + cases := []struct { + name string + request *pbs.AuthenticateRequest + wantErr error + wantErrMatch *errors.Template + }{ + { + name: "no command", + request: &pbs.AuthenticateRequest{ + AuthMethodId: s.authMethod.GetPublicId(), + }, + wantErr: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "bad command", + request: &pbs.AuthenticateRequest{ + Command: "bad", + AuthMethodId: s.authMethod.GetPublicId(), + }, + wantErr: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "no auth method id", + request: &pbs.AuthenticateRequest{ + Command: "token", + }, + wantErr: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "no attributes", + request: &pbs.AuthenticateRequest{ + Command: "token", + AuthMethodId: s.authMethod.GetPublicId(), + }, + wantErrMatch: errors.T(errors.InvalidParameter), + }, + { + name: "success", + request: &pbs.AuthenticateRequest{ + Command: "token", + AuthMethodId: s.authMethod.GetPublicId(), + Attributes: func() *structpb.Struct { + ret, err := structpb.NewStruct(map[string]interface{}{ + "token_id": func() string { + tokenPublicId, err := authtoken.NewAuthTokenId() + require.NoError(t, err) + oidc.TestPendingToken(t, testAtRepo, testUser, testAcct, tokenPublicId) + return oidc.TestTokenRequestId(t, testAuthMethod, s.kmsCache, 200*time.Second, tokenPublicId) + }(), + }) + require.NoError(t, err) + return ret + }(), + }, + }, + { + name: "expired", + request: &pbs.AuthenticateRequest{ + Command: "token", + AuthMethodId: s.authMethod.GetPublicId(), + Attributes: func() *structpb.Struct { + ret, err := structpb.NewStruct(map[string]interface{}{ + "token_id": func() string { + tokenPublicId, err := authtoken.NewAuthTokenId() + require.NoError(t, err) + oidc.TestPendingToken(t, testAtRepo, testUser, testAcct, tokenPublicId) + return oidc.TestTokenRequestId(t, testAuthMethod, s.kmsCache, -20*time.Second, tokenPublicId) + }(), + }) + require.NoError(t, err) + return ret + }(), + }, + wantErrMatch: errors.T(errors.AuthAttemptExpired), + }, + { + name: "invalid-token-id-not-encoded-encrypted-proto", + request: &pbs.AuthenticateRequest{ + Command: "token", + AuthMethodId: s.authMethod.GetPublicId(), + Attributes: func() *structpb.Struct { + ret, err := structpb.NewStruct(map[string]interface{}{ + "token_id": "bad-token-id", + }) + require.NoError(t, err) + return ret + }(), + }, + wantErrMatch: errors.T(errors.Unknown), + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + got, err := s.authMethodService.Authenticate(auth.DisabledAuthTestContext(s.iamRepoFn, s.org.GetPublicId()), tc.request) + if tc.wantErr != nil { + require.Error(err) + assert.Truef(errors.Is(err, tc.wantErr), "Got %#v, wanted %#v", err, tc.wantErr) + return + } + if tc.wantErrMatch != nil { + require.Error(err) + assert.Truef(errors.Match(tc.wantErrMatch, err), "Got %#v, wanted %#v", err, tc.wantErrMatch.Code) + return + } + require.NoError(err) + require.Equal(got.GetCommand(), "token") + require.NotNil(got.GetAttributes()) + m := got.GetAttributes().AsMap() + require.NotNil(m) + require.Contains(m, "token") + require.NotEmpty(m["token"]) + }) + } +} diff --git a/internal/servers/controller/handlers/authmethods/password.go b/internal/servers/controller/handlers/authmethods/password.go new file mode 100644 index 000000000..ef6253061 --- /dev/null +++ b/internal/servers/controller/handlers/authmethods/password.go @@ -0,0 +1,202 @@ +package authmethods + +import ( + "context" + "fmt" + "strings" + + "github.com/hashicorp/boundary/internal/auth" + "github.com/hashicorp/boundary/internal/auth/password" + pwstore "github.com/hashicorp/boundary/internal/auth/password/store" + "github.com/hashicorp/boundary/internal/errors" + pb "github.com/hashicorp/boundary/internal/gen/controller/api/resources/authmethods" + pba "github.com/hashicorp/boundary/internal/gen/controller/api/resources/authtokens" + pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services" + "github.com/hashicorp/boundary/internal/servers/controller/handlers" + "github.com/hashicorp/boundary/internal/types/action" + "google.golang.org/grpc/codes" +) + +const ( + // password field names + loginNameField = "login_name" + passwordField = "password" + loginCommand = "login" +) + +var pwMaskManager handlers.MaskManager + +func init() { + var err error + if pwMaskManager, err = handlers.NewMaskManager(&pwstore.AuthMethod{}, &pb.AuthMethod{}, &pb.PasswordAuthMethodAttributes{}); err != nil { + panic(err) + } + + IdActions[auth.PasswordSubtype] = action.ActionSet{ + action.Read, + action.Update, + action.Delete, + action.Authenticate, + } +} + +// createPwInRepo creates a password auth method in a repo and returns the result. +// This method should never return a nil AuthMethod without returning an error. +func (s Service) createPwInRepo(ctx context.Context, scopeId string, item *pb.AuthMethod) (*password.AuthMethod, error) { + u, err := toStoragePwAuthMethod(scopeId, item) + if err != nil { + return nil, err + } + repo, err := s.pwRepoFn() + if err != nil { + return nil, err + } + out, err := repo.CreateAuthMethod(ctx, u) + if err != nil { + return nil, fmt.Errorf("unable to create auth method: %w", err) + } + return out, err +} + +func (s Service) updatePwInRepo(ctx context.Context, scopeId, id string, mask []string, item *pb.AuthMethod) (*password.AuthMethod, error) { + u, err := toStoragePwAuthMethod(scopeId, item) + if err != nil { + return nil, err + } + + version := item.GetVersion() + u.PublicId = id + + dbMask := pwMaskManager.Translate(mask) + if len(dbMask) == 0 { + return nil, handlers.InvalidArgumentErrorf("No valid fields included in the update mask.", map[string]string{"update_mask": "No valid fields provided in the update mask."}) + } + + repo, err := s.pwRepoFn() + if err != nil { + return nil, err + } + out, rowsUpdated, err := repo.UpdateAuthMethod(ctx, u, version, dbMask) + if err != nil { + return nil, fmt.Errorf("unable to update auth method: %w", err) + } + if rowsUpdated == 0 { + return nil, handlers.NotFoundErrorf("AuthMethod %q doesn't exist or incorrect version provided.", id) + } + return out, nil +} + +func (s Service) authenticatePassword(ctx context.Context, req *pbs.AuthenticateRequest, authResults *auth.VerifyResults) (*pbs.AuthenticateResponse, error) { + reqAttrs := req.GetAttributes().GetFields() + tok, err := s.authenticateWithPwRepo(ctx, authResults.Scope.GetId(), req.GetAuthMethodId(), reqAttrs[loginNameField].GetStringValue(), reqAttrs[passwordField].GetStringValue()) + if err != nil { + return nil, err + } + return s.convertToAuthenticateResponse(ctx, req, authResults, tok) +} + +func (s Service) authenticateWithPwRepo(ctx context.Context, scopeId, authMethodId, loginName, pw string) (*pba.AuthToken, error) { + iamRepo, err := s.iamRepoFn() + if err != nil { + return nil, err + } + atRepo, err := s.atRepoFn() + if err != nil { + return nil, err + } + pwRepo, err := s.pwRepoFn() + if err != nil { + return nil, err + } + + acct, err := pwRepo.Authenticate(ctx, scopeId, authMethodId, loginName, pw) + if err != nil { + return nil, err + } + if acct == nil { + return nil, handlers.ApiErrorWithCodeAndMessage(codes.Unauthenticated, "Unable to authenticate.") + } + + u, err := iamRepo.LookupUserWithLogin(ctx, acct.GetPublicId()) + if err != nil { + return nil, err + } + tok, err := atRepo.CreateAuthToken(ctx, u, acct.GetPublicId()) + if err != nil { + return nil, err + } + + return s.convertInternalAuthTokenToApiAuthToken( + ctx, + tok, + ) +} + +func validateAuthenticatePasswordRequest(req *pbs.AuthenticateRequest) error { + badFields := make(map[string]string) + + if req.GetAttributes() == nil && req.GetCredentials() != nil { + // TODO: Eventually, remove this + req.Attributes = req.Credentials + } + if req.GetAttributes() == nil || req.GetAttributes().GetFields() == nil { + badFields["attributes"] = "This is a required field." + // Return early because we need non-nil values in the rest of the check. + return handlers.InvalidArgumentErrorf("Invalid fields provided in request.", badFields) + } + + attrs := req.GetAttributes().GetFields() + if _, ok := attrs[loginNameField]; !ok { + badFields["attributes.login_name"] = "This is a required field." + } + if _, ok := attrs[passwordField]; !ok { + badFields["attributes.password"] = "This is a required field." + } + if req.GetCommand() == "" { + // TODO: Eventually, require a command. For now, fall back to "login" for backwards compat. + req.Command = loginCommand + } + if req.Command != loginCommand { + badFields[commandField] = "Invalid command for this auth method type." + } + tType := strings.ToLower(strings.TrimSpace(req.GetTokenType())) + if tType != "" && tType != "token" && tType != "cookie" { + badFields[tokenTypeField] = `The only accepted types are "token" and "cookie".` + } + + if len(badFields) > 0 { + return handlers.InvalidArgumentErrorf("Invalid fields provided in request.", badFields) + } + return nil +} + +func toStoragePwAuthMethod(scopeId string, item *pb.AuthMethod) (*password.AuthMethod, error) { + const op = "authmethod_service.toStoragePwAuthMethod" + if item == nil { + return nil, errors.New(errors.InvalidParameter, op, "nil auth method.") + } + var opts []password.Option + if item.GetName() != nil { + opts = append(opts, password.WithName(item.GetName().GetValue())) + } + if item.GetDescription() != nil { + opts = append(opts, password.WithDescription(item.GetDescription().GetValue())) + } + u, err := password.NewAuthMethod(scopeId, opts...) + if err != nil { + return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to build auth method for creation: %v.", err) + } + + pwAttrs := &pb.PasswordAuthMethodAttributes{} + if err := handlers.StructToProto(item.GetAttributes(), pwAttrs); err != nil { + return nil, handlers.InvalidArgumentErrorf("Error in provided request.", + map[string]string{attributesField: "Attribute fields do not match the expected format."}) + } + if pwAttrs.GetMinLoginNameLength() != 0 { + u.MinLoginNameLength = pwAttrs.GetMinLoginNameLength() + } + if pwAttrs.GetMinPasswordLength() != 0 { + u.MinPasswordLength = pwAttrs.GetMinPasswordLength() + } + return u, nil +} diff --git a/internal/servers/controller/handlers/authmethods/password_test.go b/internal/servers/controller/handlers/authmethods/password_test.go new file mode 100644 index 000000000..62fbfb6f3 --- /dev/null +++ b/internal/servers/controller/handlers/authmethods/password_test.go @@ -0,0 +1,846 @@ +package authmethods_test + +import ( + "context" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/boundary/internal/auth" + "github.com/hashicorp/boundary/internal/auth/oidc" + "github.com/hashicorp/boundary/internal/auth/password" + "github.com/hashicorp/boundary/internal/authtoken" + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/errors" + pb "github.com/hashicorp/boundary/internal/gen/controller/api/resources/authmethods" + authtokenpb "github.com/hashicorp/boundary/internal/gen/controller/api/resources/authtokens" + scopepb "github.com/hashicorp/boundary/internal/gen/controller/api/resources/scopes" + pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services" + "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/servers/controller/handlers" + "github.com/hashicorp/boundary/internal/servers/controller/handlers/authmethods" + "github.com/hashicorp/boundary/internal/types/scope" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/genproto/protobuf/field_mask" + "google.golang.org/grpc/codes" + "google.golang.org/protobuf/testing/protocmp" + "google.golang.org/protobuf/types/known/structpb" + "google.golang.org/protobuf/types/known/timestamppb" + "google.golang.org/protobuf/types/known/wrapperspb" +) + +func TestUpdate_Password(t *testing.T) { + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + kms := kms.TestKms(t, conn, wrapper) + iamRepoFn := func() (*iam.Repository, error) { + return iam.TestRepo(t, conn, wrapper), nil + } + oidcRepoFn := func() (*oidc.Repository, error) { + return oidc.NewRepository(rw, rw, kms) + } + pwRepoFn := func() (*password.Repository, error) { + return password.NewRepository(rw, rw, kms) + } + atRepoFn := func() (*authtoken.Repository, error) { + return authtoken.NewRepository(rw, rw, kms) + } + iamRepo := iam.TestRepo(t, conn, wrapper) + + o, _ := iam.TestScopes(t, iamRepo) + tested, err := authmethods.NewService(kms, pwRepoFn, oidcRepoFn, iamRepoFn, atRepoFn) + require.NoError(t, err, "Error when getting new auth_method service.") + + defaultScopeInfo := &scopepb.ScopeInfo{Id: o.GetPublicId(), Type: o.GetType(), ParentScopeId: scope.Global.String()} + + freshAuthMethod := func() (*pb.AuthMethod, func()) { + am, err := tested.CreateAuthMethod(auth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId()), + &pbs.CreateAuthMethodRequest{Item: &pb.AuthMethod{ + ScopeId: o.GetPublicId(), + Name: wrapperspb.String("default"), + Description: wrapperspb.String("default"), + Type: "password", + }}) + require.NoError(t, err) + + clean := func() { + _, err := tested.DeleteAuthMethod(auth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId()), + &pbs.DeleteAuthMethodRequest{Id: am.GetItem().GetId()}) + require.NoError(t, err) + } + + return am.GetItem(), clean + } + + cases := []struct { + name string + req *pbs.UpdateAuthMethodRequest + res *pbs.UpdateAuthMethodResponse + err error + }{ + { + name: "Update an Existing AuthMethod", + req: &pbs.UpdateAuthMethodRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"name", "description"}, + }, + Item: &pb.AuthMethod{ + Name: &wrapperspb.StringValue{Value: "new"}, + Description: &wrapperspb.StringValue{Value: "desc"}, + Type: "password", + }, + }, + res: &pbs.UpdateAuthMethodResponse{ + Item: &pb.AuthMethod{ + ScopeId: o.GetPublicId(), + Name: &wrapperspb.StringValue{Value: "new"}, + Description: &wrapperspb.StringValue{Value: "desc"}, + Type: "password", + Attributes: &structpb.Struct{Fields: map[string]*structpb.Value{ + "min_password_length": structpb.NewNumberValue(8), + "min_login_name_length": structpb.NewNumberValue(3), + }}, + Scope: defaultScopeInfo, + AuthorizedActions: pwAuthorizedActions, + AuthorizedCollectionActions: authorizedCollectionActions, + }, + }, + }, + { + name: "Multiple Paths in single string", + req: &pbs.UpdateAuthMethodRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"name,description,type"}, + }, + Item: &pb.AuthMethod{ + Name: &wrapperspb.StringValue{Value: "new"}, + Description: &wrapperspb.StringValue{Value: "desc"}, + Type: "password", + }, + }, + res: &pbs.UpdateAuthMethodResponse{ + Item: &pb.AuthMethod{ + ScopeId: o.GetPublicId(), + Name: &wrapperspb.StringValue{Value: "new"}, + Description: &wrapperspb.StringValue{Value: "desc"}, + Type: "password", + Attributes: &structpb.Struct{Fields: map[string]*structpb.Value{ + "min_password_length": structpb.NewNumberValue(8), + "min_login_name_length": structpb.NewNumberValue(3), + }}, + Scope: defaultScopeInfo, + AuthorizedActions: pwAuthorizedActions, + AuthorizedCollectionActions: authorizedCollectionActions, + }, + }, + }, + { + name: "No Update Mask", + req: &pbs.UpdateAuthMethodRequest{ + Item: &pb.AuthMethod{ + Name: &wrapperspb.StringValue{Value: "updated name"}, + Description: &wrapperspb.StringValue{Value: "updated desc"}, + }, + }, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "No Paths in Mask", + req: &pbs.UpdateAuthMethodRequest{ + UpdateMask: &field_mask.FieldMask{Paths: []string{}}, + Item: &pb.AuthMethod{ + Name: &wrapperspb.StringValue{Value: "updated name"}, + Description: &wrapperspb.StringValue{Value: "updated desc"}, + }, + }, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "Only non-existant paths in Mask", + req: &pbs.UpdateAuthMethodRequest{ + UpdateMask: &field_mask.FieldMask{Paths: []string{"nonexistant_field"}}, + Item: &pb.AuthMethod{ + Name: &wrapperspb.StringValue{Value: "updated name"}, + Description: &wrapperspb.StringValue{Value: "updated desc"}, + }, + }, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "Cant change type", + req: &pbs.UpdateAuthMethodRequest{ + UpdateMask: &field_mask.FieldMask{Paths: []string{"name", "type"}}, + Item: &pb.AuthMethod{ + Name: &wrapperspb.StringValue{Value: "updated name"}, + Type: "oidc", + }, + }, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "Cant change is primary", + req: &pbs.UpdateAuthMethodRequest{ + UpdateMask: &field_mask.FieldMask{Paths: []string{"is_primary"}}, + Item: &pb.AuthMethod{ + IsPrimary: true, + }, + }, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "Unset Name", + req: &pbs.UpdateAuthMethodRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"name"}, + }, + Item: &pb.AuthMethod{ + Description: &wrapperspb.StringValue{Value: "ignored"}, + }, + }, + res: &pbs.UpdateAuthMethodResponse{ + Item: &pb.AuthMethod{ + ScopeId: o.GetPublicId(), + Description: &wrapperspb.StringValue{Value: "default"}, + Type: "password", + Attributes: &structpb.Struct{Fields: map[string]*structpb.Value{ + "min_password_length": structpb.NewNumberValue(8), + "min_login_name_length": structpb.NewNumberValue(3), + }}, + Scope: defaultScopeInfo, + AuthorizedActions: pwAuthorizedActions, + AuthorizedCollectionActions: authorizedCollectionActions, + }, + }, + }, + { + name: "Update Only Name", + req: &pbs.UpdateAuthMethodRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"name"}, + }, + Item: &pb.AuthMethod{ + Name: &wrapperspb.StringValue{Value: "updated"}, + Description: &wrapperspb.StringValue{Value: "ignored"}, + }, + }, + res: &pbs.UpdateAuthMethodResponse{ + Item: &pb.AuthMethod{ + ScopeId: o.GetPublicId(), + Name: &wrapperspb.StringValue{Value: "updated"}, + Description: &wrapperspb.StringValue{Value: "default"}, + Type: "password", + Attributes: &structpb.Struct{Fields: map[string]*structpb.Value{ + "min_password_length": structpb.NewNumberValue(8), + "min_login_name_length": structpb.NewNumberValue(3), + }}, + Scope: defaultScopeInfo, + AuthorizedActions: pwAuthorizedActions, + AuthorizedCollectionActions: authorizedCollectionActions, + }, + }, + }, + { + name: "Update Only Description", + req: &pbs.UpdateAuthMethodRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"description"}, + }, + Item: &pb.AuthMethod{ + Name: &wrapperspb.StringValue{Value: "ignored"}, + Description: &wrapperspb.StringValue{Value: "notignored"}, + }, + }, + res: &pbs.UpdateAuthMethodResponse{ + Item: &pb.AuthMethod{ + ScopeId: o.GetPublicId(), + Name: &wrapperspb.StringValue{Value: "default"}, + Description: &wrapperspb.StringValue{Value: "notignored"}, + Type: "password", + Attributes: &structpb.Struct{Fields: map[string]*structpb.Value{ + "min_password_length": structpb.NewNumberValue(8), + "min_login_name_length": structpb.NewNumberValue(3), + }}, + Scope: defaultScopeInfo, + AuthorizedActions: pwAuthorizedActions, + AuthorizedCollectionActions: authorizedCollectionActions, + }, + }, + }, + { + name: "Update a Non Existing AuthMethod", + req: &pbs.UpdateAuthMethodRequest{ + Id: password.AuthMethodPrefix + "_DoesntExis", + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"description"}, + }, + Item: &pb.AuthMethod{ + Name: &wrapperspb.StringValue{Value: "new"}, + Description: &wrapperspb.StringValue{Value: "desc"}, + }, + }, + err: handlers.ApiErrorWithCode(codes.NotFound), + }, + { + name: "Cant change Id", + req: &pbs.UpdateAuthMethodRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"id"}, + }, + Item: &pb.AuthMethod{ + Id: password.AuthMethodPrefix + "_somethinge", + Name: &wrapperspb.StringValue{Value: "new"}, + Description: &wrapperspb.StringValue{Value: "new desc"}, + }, + }, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "Cant specify Created Time", + req: &pbs.UpdateAuthMethodRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"created_time"}, + }, + Item: &pb.AuthMethod{ + CreatedTime: timestamppb.Now(), + }, + }, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "Cant specify Updated Time", + req: &pbs.UpdateAuthMethodRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"updated_time"}, + }, + Item: &pb.AuthMethod{ + UpdatedTime: timestamppb.Now(), + }, + }, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "Cant specify Type", + req: &pbs.UpdateAuthMethodRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"type"}, + }, + Item: &pb.AuthMethod{ + Type: "oidc", + }, + }, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "Update login name length", + req: &pbs.UpdateAuthMethodRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"attributes.min_login_name_length"}, + }, + Item: &pb.AuthMethod{ + Name: &wrapperspb.StringValue{Value: "ignored"}, + Description: &wrapperspb.StringValue{Value: "ignored"}, + Attributes: &structpb.Struct{Fields: map[string]*structpb.Value{ + "min_login_name_length": structpb.NewNumberValue(42), + "min_password_length": structpb.NewNumberValue(55555), + }}, + }, + }, + res: &pbs.UpdateAuthMethodResponse{ + Item: &pb.AuthMethod{ + ScopeId: o.GetPublicId(), + Name: &wrapperspb.StringValue{Value: "default"}, + Description: &wrapperspb.StringValue{Value: "default"}, + Type: "password", + Attributes: &structpb.Struct{Fields: map[string]*structpb.Value{ + "min_password_length": structpb.NewNumberValue(8), + "min_login_name_length": structpb.NewNumberValue(42), + }}, + Scope: defaultScopeInfo, + AuthorizedActions: pwAuthorizedActions, + AuthorizedCollectionActions: authorizedCollectionActions, + }, + }, + }, + { + name: "Update password length", + req: &pbs.UpdateAuthMethodRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"attributes.min_password_length"}, + }, + Item: &pb.AuthMethod{ + Name: &wrapperspb.StringValue{Value: "ignored"}, + Description: &wrapperspb.StringValue{Value: "ignored"}, + Attributes: &structpb.Struct{Fields: map[string]*structpb.Value{ + "min_login_name_length": structpb.NewNumberValue(5555), + "min_password_length": structpb.NewNumberValue(42), + }}, + }, + }, + res: &pbs.UpdateAuthMethodResponse{ + Item: &pb.AuthMethod{ + ScopeId: o.GetPublicId(), + Name: &wrapperspb.StringValue{Value: "default"}, + Description: &wrapperspb.StringValue{Value: "default"}, + Type: "password", + Attributes: &structpb.Struct{Fields: map[string]*structpb.Value{ + "min_password_length": structpb.NewNumberValue(42), + "min_login_name_length": structpb.NewNumberValue(3), + }}, + Scope: defaultScopeInfo, + AuthorizedActions: pwAuthorizedActions, + AuthorizedCollectionActions: authorizedCollectionActions, + }, + }, + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + am, cleanup := freshAuthMethod() + defer cleanup() + + tc.req.Item.Version = 1 + + if tc.req.GetId() == "" { + tc.req.Id = am.GetId() + } + + if tc.res != nil && tc.res.Item != nil { + tc.res.Item.Id = am.GetId() + tc.res.Item.CreatedTime = am.GetCreatedTime() + } + + got, gErr := tested.UpdateAuthMethod(auth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId()), tc.req) + if tc.err != nil { + require.Error(gErr) + assert.True(errors.Is(gErr, tc.err), "UpdateAuthMethod(%+v) got error %v, wanted %v", tc.req, gErr, tc.err) + } + + if tc.res == nil { + require.Nil(got) + } + + if got != nil { + assert.NotNilf(tc.res, "Expected UpdateAuthMethod response to be nil, but was %v", got) + + created := am.GetCreatedTime().AsTime() + gotUpdateTime := got.GetItem().GetUpdatedTime().AsTime() + // Verify it is a auth_method updated after it was created + assert.True(gotUpdateTime.After(created), "Updated auth_method should have been updated after it's creation. Was updated %v, which is after %v", gotUpdateTime, created) + + // Clear all values which are hard to compare against. + got.Item.UpdatedTime, tc.res.Item.UpdatedTime = nil, nil + + assert.EqualValues(2, got.Item.Version) + tc.res.Item.Version = 2 + } + assert.Empty(cmp.Diff(got, tc.res, protocmp.Transform()), "UpdateAuthMethod(%q) got response %q, wanted %q", tc.req, got, tc.res) + }) + } +} + +func TestAuthenticate_Password(t *testing.T) { + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + kms := kms.TestKms(t, conn, wrapper) + o, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + + iamRepoFn := func() (*iam.Repository, error) { + return iam.TestRepo(t, conn, wrapper), nil + } + oidcRepoFn := func() (*oidc.Repository, error) { + return oidc.NewRepository(rw, rw, kms) + } + pwRepoFn := func() (*password.Repository, error) { + return password.NewRepository(rw, rw, kms) + } + atRepoFn := func() (*authtoken.Repository, error) { + return authtoken.NewRepository(rw, rw, kms) + } + am := password.TestAuthMethods(t, conn, o.GetPublicId(), 1)[0] + + iam.TestSetPrimaryAuthMethod(t, iam.TestRepo(t, conn, wrapper), o, am.PublicId) + + acct, err := password.NewAccount(am.GetPublicId(), password.WithLoginName(testLoginName)) + require.NoError(t, err) + + pwRepo, err := pwRepoFn() + require.NoError(t, err) + acct, err = pwRepo.CreateAccount(context.Background(), o.GetPublicId(), acct, password.WithPassword(testPassword)) + require.NoError(t, err) + require.NotNil(t, acct) + + cases := []struct { + name string + request *pbs.AuthenticateRequest + wantType string + wantErr error + }{ + { + name: "basic", + request: &pbs.AuthenticateRequest{ + AuthMethodId: am.GetPublicId(), + TokenType: "token", + Credentials: func() *structpb.Struct { + creds := map[string]*structpb.Value{ + "login_name": {Kind: &structpb.Value_StringValue{StringValue: testLoginName}}, + "password": {Kind: &structpb.Value_StringValue{StringValue: testPassword}}, + } + return &structpb.Struct{Fields: creds} + }(), + }, + wantType: "token", + }, + { + name: "cookie-type", + request: &pbs.AuthenticateRequest{ + AuthMethodId: am.GetPublicId(), + TokenType: "cookie", + Credentials: func() *structpb.Struct { + creds := map[string]*structpb.Value{ + "login_name": {Kind: &structpb.Value_StringValue{StringValue: testLoginName}}, + "password": {Kind: &structpb.Value_StringValue{StringValue: testPassword}}, + } + return &structpb.Struct{Fields: creds} + }(), + }, + wantType: "cookie", + }, + { + name: "no-token-type", + request: &pbs.AuthenticateRequest{ + AuthMethodId: am.GetPublicId(), + Credentials: func() *structpb.Struct { + creds := map[string]*structpb.Value{ + "login_name": {Kind: &structpb.Value_StringValue{StringValue: testLoginName}}, + "password": {Kind: &structpb.Value_StringValue{StringValue: testPassword}}, + } + return &structpb.Struct{Fields: creds} + }(), + }, + }, + { + name: "bad-token-type", + request: &pbs.AuthenticateRequest{ + AuthMethodId: am.GetPublicId(), + TokenType: "email", + Credentials: func() *structpb.Struct { + creds := map[string]*structpb.Value{ + "login_name": {Kind: &structpb.Value_StringValue{StringValue: testLoginName}}, + "password": {Kind: &structpb.Value_StringValue{StringValue: testPassword}}, + } + return &structpb.Struct{Fields: creds} + }(), + }, + wantErr: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "no-authmethod", + request: &pbs.AuthenticateRequest{ + Credentials: func() *structpb.Struct { + creds := map[string]*structpb.Value{ + "login_name": {Kind: &structpb.Value_StringValue{StringValue: testLoginName}}, + "password": {Kind: &structpb.Value_StringValue{StringValue: testPassword}}, + } + return &structpb.Struct{Fields: creds} + }(), + }, + wantErr: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "wrong-password", + request: &pbs.AuthenticateRequest{ + AuthMethodId: am.GetPublicId(), + TokenType: "token", + Credentials: func() *structpb.Struct { + creds := map[string]*structpb.Value{ + "login_name": {Kind: &structpb.Value_StringValue{StringValue: testLoginName}}, + "password": {Kind: &structpb.Value_StringValue{StringValue: "wrong"}}, + } + return &structpb.Struct{Fields: creds} + }(), + }, + wantErr: handlers.ApiErrorWithCode(codes.Unauthenticated), + }, + { + name: "wrong-login-name", + request: &pbs.AuthenticateRequest{ + AuthMethodId: am.GetPublicId(), + TokenType: "token", + Credentials: func() *structpb.Struct { + creds := map[string]*structpb.Value{ + "login_name": {Kind: &structpb.Value_StringValue{StringValue: "wrong"}}, + "password": {Kind: &structpb.Value_StringValue{StringValue: testPassword}}, + } + return &structpb.Struct{Fields: creds} + }(), + }, + wantErr: handlers.ApiErrorWithCode(codes.Unauthenticated), + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + s, err := authmethods.NewService(kms, pwRepoFn, oidcRepoFn, iamRepoFn, atRepoFn) + require.NoError(err) + + resp, err := s.Authenticate(auth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId()), tc.request) + if tc.wantErr != nil { + assert.Error(err) + assert.Truef(errors.Is(err, tc.wantErr), "Got %#v, wanted %#v", err, tc.wantErr) + return + } + require.NoError(err) + aToken := &authtokenpb.AuthToken{} + require.NoError(handlers.StructToProto(resp.GetAttributes(), aToken, handlers.WithDiscardUnknownFields(true))) + assert.NotEmpty(aToken.GetId()) + assert.NotEmpty(aToken.GetToken()) + assert.True(strings.HasPrefix(aToken.GetToken(), aToken.GetId())) + assert.Equal(am.GetPublicId(), aToken.GetAuthMethodId()) + assert.Equal(aToken.GetCreatedTime(), aToken.GetUpdatedTime()) + assert.Equal(aToken.GetCreatedTime(), aToken.GetApproximateLastUsedTime()) + assert.Equal(acct.GetPublicId(), aToken.GetAccountId()) + assert.Equal(am.GetPublicId(), aToken.GetAuthMethodId()) + assert.Equal(tc.wantType, resp.GetAttributes().GetFields()["token_type"].GetStringValue()) + }) + } +} + +func TestAuthenticateLogin_Password(t *testing.T) { + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + kms := kms.TestKms(t, conn, wrapper) + o, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + + iamRepoFn := func() (*iam.Repository, error) { + return iam.TestRepo(t, conn, wrapper), nil + } + oidcRepoFn := func() (*oidc.Repository, error) { + return oidc.NewRepository(rw, rw, kms) + } + pwRepoFn := func() (*password.Repository, error) { + return password.NewRepository(rw, rw, kms) + } + atRepoFn := func() (*authtoken.Repository, error) { + return authtoken.NewRepository(rw, rw, kms) + } + am := password.TestAuthMethods(t, conn, o.GetPublicId(), 1)[0] + + iam.TestSetPrimaryAuthMethod(t, iam.TestRepo(t, conn, wrapper), o, am.PublicId) + + acct, err := password.NewAccount(am.GetPublicId(), password.WithLoginName(testLoginName)) + require.NoError(t, err) + + pwRepo, err := pwRepoFn() + require.NoError(t, err) + acct, err = pwRepo.CreateAccount(context.Background(), o.GetPublicId(), acct, password.WithPassword(testPassword)) + require.NoError(t, err) + require.NotNil(t, acct) + + cases := []struct { + name string + request *pbs.AuthenticateLoginRequest + wantType string + wantErr error + }{ + { + name: "basic", + request: &pbs.AuthenticateLoginRequest{ + AuthMethodId: am.GetPublicId(), + TokenType: "token", + Credentials: func() *structpb.Struct { + creds := map[string]*structpb.Value{ + "login_name": {Kind: &structpb.Value_StringValue{StringValue: testLoginName}}, + "password": {Kind: &structpb.Value_StringValue{StringValue: testPassword}}, + } + return &structpb.Struct{Fields: creds} + }(), + }, + wantType: "token", + }, + { + name: "cookie-type", + request: &pbs.AuthenticateLoginRequest{ + AuthMethodId: am.GetPublicId(), + TokenType: "cookie", + Credentials: func() *structpb.Struct { + creds := map[string]*structpb.Value{ + "login_name": {Kind: &structpb.Value_StringValue{StringValue: testLoginName}}, + "password": {Kind: &structpb.Value_StringValue{StringValue: testPassword}}, + } + return &structpb.Struct{Fields: creds} + }(), + }, + wantType: "cookie", + }, + { + name: "no-token-type", + request: &pbs.AuthenticateLoginRequest{ + AuthMethodId: am.GetPublicId(), + Credentials: func() *structpb.Struct { + creds := map[string]*structpb.Value{ + "login_name": {Kind: &structpb.Value_StringValue{StringValue: testLoginName}}, + "password": {Kind: &structpb.Value_StringValue{StringValue: testPassword}}, + } + return &structpb.Struct{Fields: creds} + }(), + }, + }, + { + name: "bad-token-type", + request: &pbs.AuthenticateLoginRequest{ + AuthMethodId: am.GetPublicId(), + TokenType: "email", + Credentials: func() *structpb.Struct { + creds := map[string]*structpb.Value{ + "login_name": {Kind: &structpb.Value_StringValue{StringValue: testLoginName}}, + "password": {Kind: &structpb.Value_StringValue{StringValue: testPassword}}, + } + return &structpb.Struct{Fields: creds} + }(), + }, + wantErr: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "no-authmethod", + request: &pbs.AuthenticateLoginRequest{ + Credentials: func() *structpb.Struct { + creds := map[string]*structpb.Value{ + "login_name": {Kind: &structpb.Value_StringValue{StringValue: testLoginName}}, + "password": {Kind: &structpb.Value_StringValue{StringValue: testPassword}}, + } + return &structpb.Struct{Fields: creds} + }(), + }, + wantErr: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, + { + name: "wrong-password", + request: &pbs.AuthenticateLoginRequest{ + AuthMethodId: am.GetPublicId(), + TokenType: "token", + Credentials: func() *structpb.Struct { + creds := map[string]*structpb.Value{ + "login_name": {Kind: &structpb.Value_StringValue{StringValue: testLoginName}}, + "password": {Kind: &structpb.Value_StringValue{StringValue: "wrong"}}, + } + return &structpb.Struct{Fields: creds} + }(), + }, + wantErr: handlers.ApiErrorWithCode(codes.Unauthenticated), + }, + { + name: "wrong-login-name", + request: &pbs.AuthenticateLoginRequest{ + AuthMethodId: am.GetPublicId(), + TokenType: "token", + Credentials: func() *structpb.Struct { + creds := map[string]*structpb.Value{ + "login_name": {Kind: &structpb.Value_StringValue{StringValue: "wrong"}}, + "password": {Kind: &structpb.Value_StringValue{StringValue: testPassword}}, + } + return &structpb.Struct{Fields: creds} + }(), + }, + wantErr: handlers.ApiErrorWithCode(codes.Unauthenticated), + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + s, err := authmethods.NewService(kms, pwRepoFn, oidcRepoFn, iamRepoFn, atRepoFn) + require.NoError(err) + + resp, err := s.AuthenticateLogin(auth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId()), tc.request) + if tc.wantErr != nil { + assert.Error(err) + assert.Truef(errors.Is(err, tc.wantErr), "Got %#v, wanted %#v", err, tc.wantErr) + return + } + require.NoError(err) + aToken := resp.GetItem() + assert.NotEmpty(aToken.GetId()) + assert.NotEmpty(aToken.GetToken()) + assert.True(strings.HasPrefix(aToken.GetToken(), aToken.GetId())) + assert.Equal(am.GetPublicId(), aToken.GetAuthMethodId()) + assert.Equal(aToken.GetCreatedTime(), aToken.GetUpdatedTime()) + assert.Equal(aToken.GetCreatedTime(), aToken.GetApproximateLastUsedTime()) + assert.Equal(acct.GetPublicId(), aToken.GetAccountId()) + assert.Equal(am.GetPublicId(), aToken.GetAuthMethodId()) + assert.Equal(tc.wantType, resp.GetTokenType()) + }) + } +} + +func TestAuthenticate_AuthAccountConnectedToIamUser_Password(t *testing.T) { + assert, require := assert.New(t), require.New(t) + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + kms := kms.TestKms(t, conn, wrapper) + o, _ := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + + iamRepoFn := func() (*iam.Repository, error) { + return iam.TestRepo(t, conn, wrapper), nil + } + oidcRepoFn := func() (*oidc.Repository, error) { + return oidc.NewRepository(rw, rw, kms) + } + pwRepoFn := func() (*password.Repository, error) { + return password.NewRepository(rw, rw, kms) + } + atRepoFn := func() (*authtoken.Repository, error) { + return authtoken.NewRepository(rw, rw, kms) + } + + am := password.TestAuthMethods(t, conn, o.GetPublicId(), 1)[0] + acct, err := password.NewAccount(am.GetPublicId(), password.WithLoginName(testLoginName)) + require.NoError(err) + + pwRepo, err := pwRepoFn() + require.NoError(err) + acct, err = pwRepo.CreateAccount(context.Background(), o.GetPublicId(), acct, password.WithPassword(testPassword)) + require.NoError(err) + + // connected to an account. + iamRepo, err := iamRepoFn() + require.NoError(err) + iam.TestUser(t, iamRepo, am.ScopeId, iam.WithAccountIds(acct.PublicId)) + iamUser, err := iamRepo.LookupUserWithLogin(context.Background(), acct.GetPublicId()) + require.NoError(err) + + s, err := authmethods.NewService(kms, pwRepoFn, oidcRepoFn, iamRepoFn, atRepoFn) + require.NoError(err) + resp, err := s.Authenticate(auth.DisabledAuthTestContext(iamRepoFn, o.GetPublicId()), &pbs.AuthenticateRequest{ + AuthMethodId: am.GetPublicId(), + Credentials: func() *structpb.Struct { + creds := map[string]*structpb.Value{ + "login_name": {Kind: &structpb.Value_StringValue{StringValue: testLoginName}}, + "password": {Kind: &structpb.Value_StringValue{StringValue: testPassword}}, + } + return &structpb.Struct{Fields: creds} + }(), + }) + require.NoError(err) + + aToken := &authtokenpb.AuthToken{} + require.NoError(handlers.StructToProto(resp.GetAttributes(), aToken, handlers.WithDiscardUnknownFields(true))) + assert.Equal(iamUser.GetPublicId(), aToken.GetUserId()) + assert.Equal(am.GetPublicId(), aToken.GetAuthMethodId()) + assert.Equal(acct.GetPublicId(), aToken.GetAccountId()) + + assert.NotEmpty(aToken.GetId()) + assert.NotEmpty(aToken.GetToken()) + assert.True(strings.HasPrefix(aToken.GetToken(), aToken.GetId())) +} diff --git a/internal/servers/controller/handlers/authtokens/authtoken_service.go b/internal/servers/controller/handlers/authtokens/authtoken_service.go index 671b82b29..051918d49 100644 --- a/internal/servers/controller/handlers/authtokens/authtoken_service.go +++ b/internal/servers/controller/handlers/authtokens/authtoken_service.go @@ -257,16 +257,16 @@ func toProto(in *authtoken.AuthToken) *pb.AuthToken { // * All required parameters are set // * There are no conflicting parameters provided func validateGetRequest(req *pbs.GetAuthTokenRequest) error { - return handlers.ValidateGetRequest(authtoken.AuthTokenPrefix, req, handlers.NoopValidatorFn) + return handlers.ValidateGetRequest(handlers.NoopValidatorFn, req, authtoken.AuthTokenPrefix) } func validateDeleteRequest(req *pbs.DeleteAuthTokenRequest) error { - return handlers.ValidateDeleteRequest(authtoken.AuthTokenPrefix, req, handlers.NoopValidatorFn) + return handlers.ValidateDeleteRequest(handlers.NoopValidatorFn, req, authtoken.AuthTokenPrefix) } func validateListRequest(req *pbs.ListAuthTokensRequest) error { badFields := map[string]string{} - if !handlers.ValidId(scope.Org.Prefix(), req.GetScopeId()) && + if !handlers.ValidId(handlers.Id(req.GetScopeId()), scope.Org.Prefix()) && req.GetScopeId() != scope.Global.String() { badFields["scope_id"] = "This field must be 'global' or a valid org scope id." } diff --git a/internal/servers/controller/handlers/groups/group_service.go b/internal/servers/controller/handlers/groups/group_service.go index 7298fead4..7652c6f7a 100644 --- a/internal/servers/controller/handlers/groups/group_service.go +++ b/internal/servers/controller/handlers/groups/group_service.go @@ -482,14 +482,14 @@ func toProto(in *iam.Group, members []*iam.GroupMember) *pb.Group { // * All required parameters are set // * There are no conflicting parameters provided func validateGetRequest(req *pbs.GetGroupRequest) error { - return handlers.ValidateGetRequest(iam.GroupPrefix, req, handlers.NoopValidatorFn) + return handlers.ValidateGetRequest(handlers.NoopValidatorFn, req, iam.GroupPrefix) } func validateCreateRequest(req *pbs.CreateGroupRequest) error { return handlers.ValidateCreateRequest(req.GetItem(), func() map[string]string { badFields := map[string]string{} - if !handlers.ValidId(scope.Org.Prefix(), req.GetItem().GetScopeId()) && - !handlers.ValidId(scope.Project.Prefix(), req.GetItem().GetScopeId()) && + if !handlers.ValidId(handlers.Id(req.GetItem().GetScopeId()), scope.Org.Prefix()) && + !handlers.ValidId(handlers.Id(req.GetItem().GetScopeId()), scope.Project.Prefix()) && scope.Global.String() != req.GetItem().GetScopeId() { badFields["scope_id"] = "This field is missing or improperly formatted." } @@ -498,17 +498,17 @@ func validateCreateRequest(req *pbs.CreateGroupRequest) error { } func validateUpdateRequest(req *pbs.UpdateGroupRequest) error { - return handlers.ValidateUpdateRequest(iam.GroupPrefix, req, req.GetItem(), handlers.NoopValidatorFn) + return handlers.ValidateUpdateRequest(req, req.GetItem(), handlers.NoopValidatorFn, iam.GroupPrefix) } func validateDeleteRequest(req *pbs.DeleteGroupRequest) error { - return handlers.ValidateDeleteRequest(iam.GroupPrefix, req, handlers.NoopValidatorFn) + return handlers.ValidateDeleteRequest(handlers.NoopValidatorFn, req, iam.GroupPrefix) } func validateListRequest(req *pbs.ListGroupsRequest) error { badFields := map[string]string{} - if !handlers.ValidId(scope.Org.Prefix(), req.GetScopeId()) && - !handlers.ValidId(scope.Project.Prefix(), req.GetScopeId()) && + if !handlers.ValidId(handlers.Id(req.GetScopeId()), scope.Org.Prefix()) && + !handlers.ValidId(handlers.Id(req.GetScopeId()), scope.Project.Prefix()) && req.GetScopeId() != scope.Global.String() { badFields["scope_id"] = "Incorrectly formatted identifier." } @@ -523,7 +523,7 @@ func validateListRequest(req *pbs.ListGroupsRequest) error { func validateAddGroupMembersRequest(req *pbs.AddGroupMembersRequest) error { badFields := map[string]string{} - if !handlers.ValidId(iam.GroupPrefix, req.GetId()) { + if !handlers.ValidId(handlers.Id(req.GetId()), iam.GroupPrefix) { badFields["id"] = "Incorrectly formatted identifier." } if req.GetVersion() == 0 { @@ -533,7 +533,7 @@ func validateAddGroupMembersRequest(req *pbs.AddGroupMembersRequest) error { badFields["member_ids"] = "Must be non-empty." } for _, id := range req.GetMemberIds() { - if !handlers.ValidId(iam.UserPrefix, id) { + if !handlers.ValidId(handlers.Id(id), iam.UserPrefix) { badFields["member_ids"] = fmt.Sprintf("Must only contain valid user ids but found %q.", id) break } @@ -550,14 +550,14 @@ func validateAddGroupMembersRequest(req *pbs.AddGroupMembersRequest) error { func validateSetGroupMembersRequest(req *pbs.SetGroupMembersRequest) error { badFields := map[string]string{} - if !handlers.ValidId(iam.GroupPrefix, req.GetId()) { + if !handlers.ValidId(handlers.Id(req.GetId()), iam.GroupPrefix) { badFields["id"] = "Incorrectly formatted identifier." } if req.GetVersion() == 0 { badFields["version"] = "Required field." } for _, id := range req.GetMemberIds() { - if !handlers.ValidId(iam.UserPrefix, id) { + if !handlers.ValidId(handlers.Id(id), iam.UserPrefix) { badFields["member_ids"] = fmt.Sprintf("Must only contain valid user ids but found %q.", id) break } @@ -574,7 +574,7 @@ func validateSetGroupMembersRequest(req *pbs.SetGroupMembersRequest) error { func validateRemoveGroupMembersRequest(req *pbs.RemoveGroupMembersRequest) error { badFields := map[string]string{} - if !handlers.ValidId(iam.GroupPrefix, req.GetId()) { + if !handlers.ValidId(handlers.Id(req.GetId()), iam.GroupPrefix) { badFields["id"] = "Incorrectly formatted identifier." } if req.GetVersion() == 0 { @@ -584,7 +584,7 @@ func validateRemoveGroupMembersRequest(req *pbs.RemoveGroupMembersRequest) error badFields["member_ids"] = "Must be non-empty." } for _, id := range req.GetMemberIds() { - if !handlers.ValidId(iam.UserPrefix, id) { + if !handlers.ValidId(handlers.Id(id), iam.UserPrefix) { badFields["member_ids"] = fmt.Sprintf("Must only contain valid user ids but found %q.", id) break } diff --git a/internal/servers/controller/handlers/host_catalogs/host_catalog_service.go b/internal/servers/controller/handlers/host_catalogs/host_catalog_service.go index 0acd93c35..21a4bcdcc 100644 --- a/internal/servers/controller/handlers/host_catalogs/host_catalog_service.go +++ b/internal/servers/controller/handlers/host_catalogs/host_catalog_service.go @@ -435,13 +435,13 @@ func toProto(in *static.HostCatalog) *pb.HostCatalog { // * The type asserted by the ID and/or field is known // * If relevant, the type derived from the id prefix matches what is claimed by the type field func validateGetRequest(req *pbs.GetHostCatalogRequest) error { - return handlers.ValidateGetRequest(static.HostCatalogPrefix, req, handlers.NoopValidatorFn) + return handlers.ValidateGetRequest(handlers.NoopValidatorFn, req, static.HostCatalogPrefix) } func validateCreateRequest(req *pbs.CreateHostCatalogRequest) error { return handlers.ValidateCreateRequest(req.GetItem(), func() map[string]string { badFields := map[string]string{} - if !handlers.ValidId(scope.Project.Prefix(), req.GetItem().GetScopeId()) { + if !handlers.ValidId(handlers.Id(req.GetItem().GetScopeId()), scope.Project.Prefix()) { badFields["scope_id"] = "This field must be a valid project scope id." } switch host.SubtypeFromType(req.GetItem().GetType()) { @@ -454,7 +454,7 @@ func validateCreateRequest(req *pbs.CreateHostCatalogRequest) error { } func validateUpdateRequest(req *pbs.UpdateHostCatalogRequest) error { - return handlers.ValidateUpdateRequest(static.HostCatalogPrefix, req, req.GetItem(), func() map[string]string { + return handlers.ValidateUpdateRequest(req, req.GetItem(), func() map[string]string { badFields := map[string]string{} switch host.SubtypeFromId(req.GetId()) { case host.StaticSubtype: @@ -463,16 +463,16 @@ func validateUpdateRequest(req *pbs.UpdateHostCatalogRequest) error { } } return badFields - }) + }, static.HostCatalogPrefix) } func validateDeleteRequest(req *pbs.DeleteHostCatalogRequest) error { - return handlers.ValidateDeleteRequest(static.HostCatalogPrefix, req, handlers.NoopValidatorFn) + return handlers.ValidateDeleteRequest(handlers.NoopValidatorFn, req, static.HostCatalogPrefix) } func validateListRequest(req *pbs.ListHostCatalogsRequest) error { badFields := map[string]string{} - if !handlers.ValidId(scope.Project.Prefix(), req.GetScopeId()) && + if !handlers.ValidId(handlers.Id(req.GetScopeId()), scope.Project.Prefix()) && !req.GetRecursive() { badFields["scope_id"] = "This field must be a valid project scope ID or the list operation must be recursive." } diff --git a/internal/servers/controller/handlers/host_sets/host_set_service.go b/internal/servers/controller/handlers/host_sets/host_set_service.go index 1eece1bb2..bc9b829fb 100644 --- a/internal/servers/controller/handlers/host_sets/host_set_service.go +++ b/internal/servers/controller/handlers/host_sets/host_set_service.go @@ -500,13 +500,13 @@ func toProto(in *static.HostSet, hs []*static.Host) *pb.HostSet { // * The type asserted by the ID and/or field is known // * If relevant, the type derived from the id prefix matches what is claimed by the type field func validateGetRequest(req *pbs.GetHostSetRequest) error { - return handlers.ValidateGetRequest(static.HostSetPrefix, req, handlers.NoopValidatorFn) + return handlers.ValidateGetRequest(handlers.NoopValidatorFn, req, static.HostSetPrefix) } func validateCreateRequest(req *pbs.CreateHostSetRequest) error { return handlers.ValidateCreateRequest(req.GetItem(), func() map[string]string { badFields := map[string]string{} - if !handlers.ValidId(static.HostCatalogPrefix, req.GetItem().GetHostCatalogId()) { + if !handlers.ValidId(handlers.Id(req.GetItem().GetHostCatalogId()), static.HostCatalogPrefix) { badFields["host_catalog_id"] = "The field is incorrectly formatted." } switch host.SubtypeFromId(req.GetItem().GetHostCatalogId()) { @@ -520,7 +520,7 @@ func validateCreateRequest(req *pbs.CreateHostSetRequest) error { } func validateUpdateRequest(req *pbs.UpdateHostSetRequest) error { - return handlers.ValidateUpdateRequest(static.HostSetPrefix, req, req.GetItem(), func() map[string]string { + return handlers.ValidateUpdateRequest(req, req.GetItem(), func() map[string]string { badFields := map[string]string{} switch host.SubtypeFromId(req.GetId()) { case host.StaticSubtype: @@ -529,16 +529,16 @@ func validateUpdateRequest(req *pbs.UpdateHostSetRequest) error { } } return badFields - }) + }, static.HostSetPrefix) } func validateDeleteRequest(req *pbs.DeleteHostSetRequest) error { - return handlers.ValidateDeleteRequest(static.HostSetPrefix, req, handlers.NoopValidatorFn) + return handlers.ValidateDeleteRequest(handlers.NoopValidatorFn, req, static.HostSetPrefix) } func validateListRequest(req *pbs.ListHostSetsRequest) error { badFields := map[string]string{} - if !handlers.ValidId(static.HostCatalogPrefix, req.GetHostCatalogId()) { + if !handlers.ValidId(handlers.Id(req.GetHostCatalogId()), static.HostCatalogPrefix) { badFields["host_catalog_id"] = "The field is incorrectly formatted." } if _, err := handlers.NewFilter(req.GetFilter()); err != nil { @@ -552,7 +552,7 @@ func validateListRequest(req *pbs.ListHostSetsRequest) error { func validateAddRequest(req *pbs.AddHostSetHostsRequest) error { badFields := map[string]string{} - if !handlers.ValidId(static.HostSetPrefix, req.GetId()) { + if !handlers.ValidId(handlers.Id(req.GetId()), static.HostSetPrefix) { badFields["id"] = "Incorrectly formatted identifier." } if req.GetVersion() == 0 { @@ -562,7 +562,7 @@ func validateAddRequest(req *pbs.AddHostSetHostsRequest) error { badFields["host_ids"] = "Must be non-empty." } for _, id := range req.GetHostIds() { - if !handlers.ValidId(static.HostPrefix, id) { + if !handlers.ValidId(handlers.Id(id), static.HostPrefix) { badFields["host_ids"] = "Incorrectly formatted host identifier." break } @@ -575,14 +575,14 @@ func validateAddRequest(req *pbs.AddHostSetHostsRequest) error { func validateSetRequest(req *pbs.SetHostSetHostsRequest) error { badFields := map[string]string{} - if !handlers.ValidId(static.HostSetPrefix, req.GetId()) { + if !handlers.ValidId(handlers.Id(req.GetId()), static.HostSetPrefix) { badFields["id"] = "Incorrectly formatted identifier." } if req.GetVersion() == 0 { badFields["version"] = "Required field." } for _, id := range req.GetHostIds() { - if !handlers.ValidId(static.HostPrefix, id) { + if !handlers.ValidId(handlers.Id(id), static.HostPrefix) { badFields["host_ids"] = "Incorrectly formatted host identifier." break } @@ -595,7 +595,7 @@ func validateSetRequest(req *pbs.SetHostSetHostsRequest) error { func validateRemoveRequest(req *pbs.RemoveHostSetHostsRequest) error { badFields := map[string]string{} - if !handlers.ValidId(static.HostSetPrefix, req.GetId()) { + if !handlers.ValidId(handlers.Id(req.GetId()), static.HostSetPrefix) { badFields["id"] = "Incorrectly formatted identifier." } if req.GetVersion() == 0 { @@ -605,7 +605,7 @@ func validateRemoveRequest(req *pbs.RemoveHostSetHostsRequest) error { badFields["host_ids"] = "Must be non-empty." } for _, id := range req.GetHostIds() { - if !handlers.ValidId(static.HostPrefix, id) { + if !handlers.ValidId(handlers.Id(id), static.HostPrefix) { badFields["host_ids"] = "Incorrectly formatted host identifier." break } diff --git a/internal/servers/controller/handlers/hosts/host_service.go b/internal/servers/controller/handlers/hosts/host_service.go index 910490b65..651f7fc5d 100644 --- a/internal/servers/controller/handlers/hosts/host_service.go +++ b/internal/servers/controller/handlers/hosts/host_service.go @@ -386,20 +386,20 @@ func toProto(in *static.Host, members []*static.HostSet) (*pb.Host, error) { // * The type asserted by the ID and/or field is known // * If relevant, the type derived from the id prefix matches what is claimed by the type field func validateGetRequest(req *pbs.GetHostRequest) error { - return handlers.ValidateGetRequest(static.HostPrefix, req, func() map[string]string { + return handlers.ValidateGetRequest(func() map[string]string { badFields := map[string]string{} ct := host.SubtypeFromId(req.GetId()) if ct == host.UnknownSubtype { badFields["id"] = "Improperly formatted identifier used." } return badFields - }) + }, req, static.HostPrefix) } func validateCreateRequest(req *pbs.CreateHostRequest) error { return handlers.ValidateCreateRequest(req.GetItem(), func() map[string]string { badFields := map[string]string{} - if !handlers.ValidId(static.HostCatalogPrefix, req.GetItem().GetHostCatalogId()) { + if !handlers.ValidId(handlers.Id(req.GetItem().GetHostCatalogId()), static.HostCatalogPrefix) { badFields["host_catalog_id"] = "The field is incorrectly formatted." } switch host.SubtypeFromId(req.GetItem().GetHostCatalogId()) { @@ -431,7 +431,7 @@ func validateCreateRequest(req *pbs.CreateHostRequest) error { } func validateUpdateRequest(req *pbs.UpdateHostRequest) error { - return handlers.ValidateUpdateRequest(static.HostPrefix, req, req.GetItem(), func() map[string]string { + return handlers.ValidateUpdateRequest(req, req.GetItem(), func() map[string]string { badFields := map[string]string{} switch host.SubtypeFromId(req.GetId()) { case host.StaticSubtype: @@ -455,16 +455,16 @@ func validateUpdateRequest(req *pbs.UpdateHostRequest) error { badFields["id"] = "Improperly formatted identifier used." } return badFields - }) + }, static.HostPrefix) } func validateDeleteRequest(req *pbs.DeleteHostRequest) error { - return handlers.ValidateDeleteRequest(static.HostPrefix, req, handlers.NoopValidatorFn) + return handlers.ValidateDeleteRequest(handlers.NoopValidatorFn, req, static.HostPrefix) } func validateListRequest(req *pbs.ListHostsRequest) error { badFields := map[string]string{} - if !handlers.ValidId(static.HostCatalogPrefix, req.GetHostCatalogId()) { + if !handlers.ValidId(handlers.Id(req.GetHostCatalogId()), static.HostCatalogPrefix) { badFields["host_catalog_id"] = "The field is incorrectly formatted." } if _, err := handlers.NewFilter(req.GetFilter()); err != nil { diff --git a/internal/servers/controller/handlers/option_test.go b/internal/servers/controller/handlers/option_test.go new file mode 100644 index 000000000..d7f734dde --- /dev/null +++ b/internal/servers/controller/handlers/option_test.go @@ -0,0 +1,30 @@ +package handlers + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +// Test_GetOpts provides unit tests for GetOpts and all the options +func Test_GetOpts(t *testing.T) { + t.Parallel() + t.Run("WithDiscardUnknownFields", func(t *testing.T) { + assert := assert.New(t) + // test default of 0 + opts := getOpts() + testOpts := getDefaultOptions() + testOpts.withDiscardUnknownFields = false + assert.Equal(opts, testOpts) + + opts = getOpts(WithDiscardUnknownFields(true)) + testOpts = getDefaultOptions() + testOpts.withDiscardUnknownFields = true + assert.Equal(opts, testOpts) + + opts = getOpts(WithDiscardUnknownFields(false)) + testOpts = getDefaultOptions() + testOpts.withDiscardUnknownFields = false + assert.Equal(opts, testOpts) + }) +} diff --git a/internal/servers/controller/handlers/options.go b/internal/servers/controller/handlers/options.go new file mode 100644 index 000000000..38972f9df --- /dev/null +++ b/internal/servers/controller/handlers/options.go @@ -0,0 +1,31 @@ +package handlers + +// getOpts - iterate the inbound Options and return a struct +func getOpts(opt ...Option) options { + opts := getDefaultOptions() + for _, o := range opt { + o(&opts) + } + return opts +} + +// Option - how Options are passed as arguments +type Option func(*options) + +// options = how options are represented +type options struct { + withDiscardUnknownFields bool +} + +func getDefaultOptions() options { + return options{} +} + +// WithDiscardUnknownFields provides an option to cause StructToProto to ignore +// unknown fields. This is useful in some instances when we need to unmarshal a +// value from a pb.Struct after we've added some custom extra fields. +func WithDiscardUnknownFields(discard bool) Option { + return func(o *options) { + o.withDiscardUnknownFields = discard + } +} diff --git a/internal/servers/controller/handlers/outgoing_interceptor.go b/internal/servers/controller/handlers/outgoing_interceptor.go index 7586b651d..e6fd6fa53 100644 --- a/internal/servers/controller/handlers/outgoing_interceptor.go +++ b/internal/servers/controller/handlers/outgoing_interceptor.go @@ -2,38 +2,80 @@ package handlers import ( "context" + "fmt" "net/http" "strings" + authtokenpb "github.com/hashicorp/boundary/internal/gen/controller/api/resources/authtokens" pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services" + "google.golang.org/protobuf/proto" ) const ( - HttpOnlyCookieName = "wt-http-token-cookie" - JsVisibleCookieName = "wt-js-token-cookie" + HttpOnlyCookieName = "wt-http-token-cookie" + JsVisibleCookieName = "wt-js-token-cookie" + tokenTypeField = "token_type" + finalRedirectUrlField = "final_redirect_url" ) func OutgoingInterceptor(ctx context.Context, w http.ResponseWriter, m proto.Message) error { m = m.ProtoReflect().Interface() + if !m.ProtoReflect().IsValid() { + // This would be the case if it's a nil pointer + w.WriteHeader(http.StatusNoContent) + return nil + } switch m := m.(type) { case *pbs.AuthenticateResponse: - if strings.EqualFold(m.GetTokenType(), "cookie") { - tok := m.GetItem().GetToken() - m.GetItem().Token = "" - half := len(tok) / 2 - jsTok := http.Cookie{ - Name: JsVisibleCookieName, - Value: tok[:half], - } - httpTok := http.Cookie{ - Name: HttpOnlyCookieName, - Value: tok[half:], - HttpOnly: true, - } - http.SetCookie(w, &jsTok) - http.SetCookie(w, &httpTok) + if m.GetAttributes() == nil || m.GetAttributes().GetFields() == nil { + // We may not have a token depending on the subcommand; nothing to + // do if there are no attributes + return nil } + fields := m.GetAttributes().GetFields() + // It's a redirect + if urlField, ok := fields[finalRedirectUrlField]; ok { + u := urlField.GetStringValue() + if u == "" { + return fmt.Errorf("unable to convert final request url to string") + } + delete(fields, finalRedirectUrlField) + w.Header().Set("Location", u) + w.WriteHeader(http.StatusFound) + return nil + } + // It's a token response + if _, ok := fields[tokenTypeField]; ok { + aToken := &authtokenpb.AuthToken{} + // We may have "token_type" if it's a token, or it may not be a token at + // all, so ignore unknown fields + if err := StructToProto(m.GetAttributes(), aToken, WithDiscardUnknownFields(true)); err != nil { + return err + } + tokenType := m.GetAttributes().GetFields()[tokenTypeField].GetStringValue() + if strings.EqualFold(tokenType, "cookie") { + tok := aToken.GetToken() + if tok == "" { + // Response did not include a token, continue + return nil + } + delete(m.GetAttributes().GetFields(), "token") + half := len(tok) / 2 + jsTok := http.Cookie{ + Name: JsVisibleCookieName, + Value: tok[:half], + } + httpTok := http.Cookie{ + Name: HttpOnlyCookieName, + Value: tok[half:], + HttpOnly: true, + } + http.SetCookie(w, &jsTok) + http.SetCookie(w, &httpTok) + } + } + case *pbs.AuthenticateLoginResponse: if strings.EqualFold(m.GetTokenType(), "cookie") { tok := m.GetItem().GetToken() diff --git a/internal/servers/controller/handlers/outgoing_interceptor_test.go b/internal/servers/controller/handlers/outgoing_interceptor_test.go index 78125d21d..e21d93e4c 100644 --- a/internal/servers/controller/handlers/outgoing_interceptor_test.go +++ b/internal/servers/controller/handlers/outgoing_interceptor_test.go @@ -9,14 +9,16 @@ import ( pb "github.com/hashicorp/boundary/internal/gen/controller/api/resources/authtokens" pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/structpb" ) func TestOutgoingSplitCookie(t *testing.T) { rec := httptest.NewRecorder() - OutgoingInterceptor(context.Background(), rec, &pbs.AuthenticateResponse{ - TokenType: "cookie", - Item: &pb.AuthToken{Token: "t_abc_1234567890"}, - }) + attrs, err := ProtoToStruct(&pb.AuthToken{Token: "t_abc_1234567890"}) + require.NoError(t, err) + attrs.GetFields()["token_type"] = structpb.NewStringValue("cookie") + require.NoError(t, OutgoingInterceptor(context.Background(), rec, &pbs.AuthenticateResponse{Attributes: attrs})) assert.ElementsMatch(t, rec.Result().Cookies(), []*http.Cookie{ {Name: HttpOnlyCookieName, Value: "34567890", HttpOnly: true, Raw: "wt-http-token-cookie=34567890; HttpOnly"}, {Name: JsVisibleCookieName, Value: "t_abc_12", Raw: "wt-js-token-cookie=t_abc_12"}, diff --git a/internal/servers/controller/handlers/roles/role_service.go b/internal/servers/controller/handlers/roles/role_service.go index a6f8f09f2..0f22f383f 100644 --- a/internal/servers/controller/handlers/roles/role_service.go +++ b/internal/servers/controller/handlers/roles/role_service.go @@ -641,19 +641,19 @@ func toProto(in *iam.Role, principals []iam.PrincipalRole, grants []*iam.RoleGra // * All required parameters are set // * There are no conflicting parameters provided func validateGetRequest(req *pbs.GetRoleRequest) error { - return handlers.ValidateGetRequest(iam.RolePrefix, req, handlers.NoopValidatorFn) + return handlers.ValidateGetRequest(handlers.NoopValidatorFn, req, iam.RolePrefix) } func validateCreateRequest(req *pbs.CreateRoleRequest) error { return handlers.ValidateCreateRequest(req.GetItem(), func() map[string]string { badFields := map[string]string{} item := req.GetItem() - if !handlers.ValidId(scope.Org.Prefix(), item.GetScopeId()) && - !handlers.ValidId(scope.Project.Prefix(), item.GetScopeId()) && + if !handlers.ValidId(handlers.Id(item.GetScopeId()), scope.Org.Prefix()) && + !handlers.ValidId(handlers.Id(item.GetScopeId()), scope.Project.Prefix()) && scope.Global.String() != item.GetScopeId() { badFields["scope_id"] = "This field is missing or improperly formatted." } - if item.GetGrantScopeId() != nil && handlers.ValidId(scope.Project.Prefix(), item.GetScopeId()) { + if item.GetGrantScopeId() != nil && handlers.ValidId(handlers.Id(item.GetScopeId()), scope.Project.Prefix()) { if item.GetGrantScopeId().GetValue() != item.GetScopeId() { badFields["grant_scope_id"] = "When the role is in a project scope this value must be that project's scope ID." } @@ -669,7 +669,7 @@ func validateCreateRequest(req *pbs.CreateRoleRequest) error { } func validateUpdateRequest(req *pbs.UpdateRoleRequest) error { - return handlers.ValidateUpdateRequest(iam.RolePrefix, req, req.GetItem(), func() map[string]string { + return handlers.ValidateUpdateRequest(req, req.GetItem(), func() map[string]string { badFields := map[string]string{} if req.GetItem().GetPrincipalIds() != nil { badFields["principal_ids"] = "This is a read only field and cannot be specified in an update request." @@ -683,25 +683,25 @@ func validateUpdateRequest(req *pbs.UpdateRoleRequest) error { if req.GetItem().GetGrantStrings() != nil { badFields["grant_strings"] = "This is a read only field and cannot be specified in an update request." } - if req.GetItem().GetGrantScopeId() != nil && handlers.ValidId(scope.Project.Prefix(), req.GetItem().GetScopeId()) { + if req.GetItem().GetGrantScopeId() != nil && handlers.ValidId(handlers.Id(req.GetItem().GetScopeId()), scope.Project.Prefix()) { if req.GetItem().GetGrantScopeId().GetValue() != req.GetItem().GetScopeId() { badFields["grant_scope_id"] = "When the role is in a project scope this value must be that project's scope ID" } } return badFields - }) + }, iam.RolePrefix) } func validateDeleteRequest(req *pbs.DeleteRoleRequest) error { - return handlers.ValidateDeleteRequest(iam.RolePrefix, req, func() map[string]string { + return handlers.ValidateDeleteRequest(func() map[string]string { return nil - }) + }, req, iam.RolePrefix) } func validateListRequest(req *pbs.ListRolesRequest) error { badFields := map[string]string{} - if !handlers.ValidId(scope.Org.Prefix(), req.GetScopeId()) && - !handlers.ValidId(scope.Project.Prefix(), req.GetScopeId()) && + if !handlers.ValidId(handlers.Id(req.GetScopeId()), scope.Org.Prefix()) && + !handlers.ValidId(handlers.Id(req.GetScopeId()), scope.Project.Prefix()) && req.GetScopeId() != scope.Global.String() { badFields["scope_id"] = "Improperly formatted field." } @@ -716,7 +716,7 @@ func validateListRequest(req *pbs.ListRolesRequest) error { func validateAddRolePrincipalsRequest(req *pbs.AddRolePrincipalsRequest) error { badFields := map[string]string{} - if !handlers.ValidId(iam.RolePrefix, req.GetId()) { + if !handlers.ValidId(handlers.Id(req.GetId()), iam.RolePrefix) { badFields["id"] = "Incorrectly formatted identifier." } if req.GetVersion() == 0 { @@ -726,7 +726,7 @@ func validateAddRolePrincipalsRequest(req *pbs.AddRolePrincipalsRequest) error { badFields["principal_ids"] = "Must be non-empty." } for _, id := range req.GetPrincipalIds() { - if !handlers.ValidId(iam.GroupPrefix, id) && !handlers.ValidId(iam.UserPrefix, id) { + if !handlers.ValidId(handlers.Id(id), iam.GroupPrefix) && !handlers.ValidId(handlers.Id(id), iam.UserPrefix) { badFields["principal_ids"] = "Must only have valid group and/or user ids." break } @@ -743,14 +743,14 @@ func validateAddRolePrincipalsRequest(req *pbs.AddRolePrincipalsRequest) error { func validateSetRolePrincipalsRequest(req *pbs.SetRolePrincipalsRequest) error { badFields := map[string]string{} - if !handlers.ValidId(iam.RolePrefix, req.GetId()) { + if !handlers.ValidId(handlers.Id(req.GetId()), iam.RolePrefix) { badFields["id"] = "Incorrectly formatted identifier." } if req.GetVersion() == 0 { badFields["version"] = "Required field." } for _, id := range req.GetPrincipalIds() { - if !handlers.ValidId(iam.GroupPrefix, id) && !handlers.ValidId(iam.UserPrefix, id) { + if !handlers.ValidId(handlers.Id(id), iam.GroupPrefix) && !handlers.ValidId(handlers.Id(id), iam.UserPrefix) { badFields["principal_ids"] = "Must only have valid group and/or user ids." break } @@ -767,7 +767,7 @@ func validateSetRolePrincipalsRequest(req *pbs.SetRolePrincipalsRequest) error { func validateRemoveRolePrincipalsRequest(req *pbs.RemoveRolePrincipalsRequest) error { badFields := map[string]string{} - if !handlers.ValidId(iam.RolePrefix, req.GetId()) { + if !handlers.ValidId(handlers.Id(req.GetId()), iam.RolePrefix) { badFields["id"] = "Incorrectly formatted identifier." } if req.GetVersion() == 0 { @@ -777,7 +777,7 @@ func validateRemoveRolePrincipalsRequest(req *pbs.RemoveRolePrincipalsRequest) e badFields["principal_ids"] = "Must be non-empty." } for _, id := range req.GetPrincipalIds() { - if !handlers.ValidId(iam.GroupPrefix, id) && !handlers.ValidId(iam.UserPrefix, id) { + if !handlers.ValidId(handlers.Id(id), iam.GroupPrefix) && !handlers.ValidId(handlers.Id(id), iam.UserPrefix) { badFields["principal_ids"] = "Must only have valid group and/or user ids." break } @@ -790,7 +790,7 @@ func validateRemoveRolePrincipalsRequest(req *pbs.RemoveRolePrincipalsRequest) e func validateAddRoleGrantsRequest(req *pbs.AddRoleGrantsRequest) error { badFields := map[string]string{} - if !handlers.ValidId(iam.RolePrefix, req.GetId()) { + if !handlers.ValidId(handlers.Id(req.GetId()), iam.RolePrefix) { badFields["id"] = "Incorrectly formatted identifier." } if req.GetVersion() == 0 { @@ -817,7 +817,7 @@ func validateAddRoleGrantsRequest(req *pbs.AddRoleGrantsRequest) error { func validateSetRoleGrantsRequest(req *pbs.SetRoleGrantsRequest) error { badFields := map[string]string{} - if !handlers.ValidId(iam.RolePrefix, req.GetId()) { + if !handlers.ValidId(handlers.Id(req.GetId()), iam.RolePrefix) { badFields["id"] = "Incorrectly formatted identifier." } if req.GetVersion() == 0 { @@ -841,7 +841,7 @@ func validateSetRoleGrantsRequest(req *pbs.SetRoleGrantsRequest) error { func validateRemoveRoleGrantsRequest(req *pbs.RemoveRoleGrantsRequest) error { badFields := map[string]string{} - if !handlers.ValidId(iam.RolePrefix, req.GetId()) { + if !handlers.ValidId(handlers.Id(req.GetId()), iam.RolePrefix) { badFields["id"] = "Incorrectly formatted identifier." } if req.GetVersion() == 0 { diff --git a/internal/servers/controller/handlers/scopes/scope_service.go b/internal/servers/controller/handlers/scopes/scope_service.go index bd2db6258..3b0b2d96a 100644 --- a/internal/servers/controller/handlers/scopes/scope_service.go +++ b/internal/servers/controller/handlers/scopes/scope_service.go @@ -7,6 +7,8 @@ import ( "strings" "github.com/hashicorp/boundary/internal/auth" + "github.com/hashicorp/boundary/internal/auth/oidc" + "github.com/hashicorp/boundary/internal/auth/password" pb "github.com/hashicorp/boundary/internal/gen/controller/api/resources/scopes" pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services" "github.com/hashicorp/boundary/internal/iam" @@ -323,20 +325,39 @@ func (s Service) createInRepo(ctx context.Context, authResults auth.VerifyResult func (s Service) updateInRepo(ctx context.Context, parentScope *pb.ScopeInfo, scopeId string, mask []string, item *pb.Scope) (*pb.Scope, error) { var opts []iam.Option + var scopeDesc, scopeName, scopePrimaryAuthMethodId string if desc := item.GetDescription(); desc != nil { - opts = append(opts, iam.WithDescription(desc.GetValue())) + scopeDesc = desc.GetValue() + opts = append(opts, iam.WithDescription(scopeDesc)) } if name := item.GetName(); name != nil { - opts = append(opts, iam.WithName(name.GetValue())) + scopeName = name.GetValue() + opts = append(opts, iam.WithName(scopeName)) + } + if primaryAuthMethodId := item.GetPrimaryAuthMethodId(); primaryAuthMethodId != nil { + if !handlers.ValidId(handlers.Id(primaryAuthMethodId.GetValue()), password.AuthMethodPrefix, oidc.AuthMethodPrefix) { + return nil, handlers.InvalidArgumentErrorf("Error in provided request.", map[string]string{"primary_auth_method_id": "Improperly formatted identifier"}) + } + scopePrimaryAuthMethodId = primaryAuthMethodId.GetValue() + opts = append(opts, iam.WithPrimaryAuthMethodId(scopePrimaryAuthMethodId)) } version := item.GetVersion() var iamScope *iam.Scope var err error - switch parentScope.GetType() { - case scope.Global.String(): + switch { + case scopeId == scope.Global.String(): + // boundary does not allow you to create a new global scope, so + // we'll build the required scope by hand for the update. + s := iam.AllocScope() + s.PublicId = scopeId + iamScope = &s + iamScope.Description = scopeDesc + iamScope.Name = scopeName + iamScope.PrimaryAuthMethodId = scopePrimaryAuthMethodId + case parentScope.GetType() == scope.Global.String(): iamScope, err = iam.NewOrg(opts...) - case scope.Org.String(): + case parentScope.GetType() == scope.Org.String(): iamScope, err = iam.NewProject(parentScope.GetId(), opts...) } if err != nil { @@ -453,6 +474,10 @@ func ToProto(in *iam.Scope) *pb.Scope { if in.GetName() != "" { out.Name = &wrapperspb.StringValue{Value: in.GetName()} } + if in.GetPrimaryAuthMethodId() != "" { + out.PrimaryAuthMethodId = &wrapperspb.StringValue{Value: in.GetPrimaryAuthMethodId()} + } + return &out } @@ -467,11 +492,11 @@ func validateGetRequest(req *pbs.GetScopeRequest) error { switch { case id == "global": case strings.HasPrefix(id, scope.Org.Prefix()): - if !handlers.ValidId(scope.Org.Prefix(), id) { + if !handlers.ValidId(handlers.Id(id), scope.Org.Prefix()) { badFields["id"] = "Invalidly formatted scope id." } case strings.HasPrefix(id, scope.Project.Prefix()): - if !handlers.ValidId(scope.Project.Prefix(), id) { + if !handlers.ValidId(handlers.Id(id), scope.Project.Prefix()) { badFields["id"] = "Invalidly formatted scope id." } default: @@ -497,7 +522,7 @@ func validateCreateRequest(req *pbs.CreateScopeRequest) error { badFields["type"] = "Org scopes can only be created under the global scope." } case scope.Project.String(): - if !handlers.ValidId(scope.Org.Prefix(), item.GetScopeId()) { + if !handlers.ValidId(handlers.Id(item.GetScopeId()), scope.Org.Prefix()) { badFields["type"] = "Project scopes can only be created under an org scope." } } @@ -525,14 +550,14 @@ func validateUpdateRequest(req *pbs.UpdateScopeRequest) error { switch { case id == "global": case strings.HasPrefix(id, scope.Org.Prefix()): - if !handlers.ValidId(scope.Org.Prefix(), id) { + if !handlers.ValidId(handlers.Id(id), scope.Org.Prefix()) { badFields["id"] = "Invalidly formatted scope id." } if req.GetItem().GetType() != "" && !strings.EqualFold(scope.Org.String(), req.GetItem().GetType()) { badFields["type"] = "Cannot modify the resource type." } case strings.HasPrefix(id, scope.Project.Prefix()): - if !handlers.ValidId(scope.Project.Prefix(), id) { + if !handlers.ValidId(handlers.Id(id), scope.Project.Prefix()) { badFields["id"] = "Invalidly formatted scope id." } if req.GetItem().GetType() != "" && !strings.EqualFold(scope.Project.String(), req.GetItem().GetType()) { @@ -566,6 +591,9 @@ func validateUpdateRequest(req *pbs.UpdateScopeRequest) error { if item.GetUpdatedTime() != nil { badFields["updated_time"] = "This is a read only field and cannot be specified in an update request." } + if item.GetPrimaryAuthMethodId().GetValue() != "" && !handlers.ValidId(handlers.Id(item.GetPrimaryAuthMethodId().GetValue()), password.AuthMethodPrefix, oidc.AuthMethodPrefix) { + badFields["primary_auth_method_id"] = "Improperly formatted identifier." + } if len(badFields) > 0 { return handlers.InvalidArgumentErrorf("Error in provided request.", badFields) } @@ -580,11 +608,11 @@ func validateDeleteRequest(req *pbs.DeleteScopeRequest) error { case id == "global": badFields["id"] = "Invalid to delete the global scope." case strings.HasPrefix(id, scope.Org.Prefix()): - if !handlers.ValidId(scope.Org.Prefix(), id) { + if !handlers.ValidId(handlers.Id(id), scope.Org.Prefix()) { badFields["id"] = "Invalidly formatted scope id." } case strings.HasPrefix(id, scope.Project.Prefix()): - if !handlers.ValidId(scope.Project.Prefix(), id) { + if !handlers.ValidId(handlers.Id(id), scope.Project.Prefix()) { badFields["id"] = "Invalidly formatted scope id." } default: @@ -598,7 +626,7 @@ func validateDeleteRequest(req *pbs.DeleteScopeRequest) error { func validateListRequest(req *pbs.ListScopesRequest) error { badFields := map[string]string{} - if req.GetScopeId() != scope.Global.String() && !handlers.ValidId(scope.Org.Prefix(), req.GetScopeId()) { + if req.GetScopeId() != scope.Global.String() && !handlers.ValidId(handlers.Id(req.GetScopeId()), scope.Org.Prefix()) { badFields["scope_id"] = "Must be 'global' or a valid org scope id when listing." } if _, err := handlers.NewFilter(req.GetFilter()); err != nil { diff --git a/internal/servers/controller/handlers/scopes/scope_service_test.go b/internal/servers/controller/handlers/scopes/scope_service_test.go index 7dce0020d..ebc5db35c 100644 --- a/internal/servers/controller/handlers/scopes/scope_service_test.go +++ b/internal/servers/controller/handlers/scopes/scope_service_test.go @@ -56,6 +56,49 @@ func createDefaultScopesAndRepo(t *testing.T) (*iam.Scope, *iam.Scope, func() (* return oRes, pRes, repoFn } +var globalAuthorizedCollectionActions = map[string]*structpb.ListValue{ + "auth-methods": { + Values: []*structpb.Value{ + structpb.NewStringValue("create"), + structpb.NewStringValue("list"), + }, + }, + "auth-tokens": { + Values: []*structpb.Value{ + structpb.NewStringValue("list"), + }, + }, + "groups": { + Values: []*structpb.Value{ + structpb.NewStringValue("create"), + structpb.NewStringValue("list"), + }, + }, + "roles": { + Values: []*structpb.Value{ + structpb.NewStringValue("create"), + structpb.NewStringValue("list"), + }, + }, + "scopes": { + Values: []*structpb.Value{ + structpb.NewStringValue("create"), + structpb.NewStringValue("list"), + }, + }, + "sessions": { + Values: []*structpb.Value{ + structpb.NewStringValue("list"), + }, + }, + "users": { + Values: []*structpb.Value{ + structpb.NewStringValue("create"), + structpb.NewStringValue("list"), + }, + }, +} + var orgAuthorizedCollectionActions = map[string]*structpb.ListValue{ "auth-methods": { Values: []*structpb.Value{ @@ -776,8 +819,14 @@ func TestUpdate(t *testing.T) { tested, err := scopes.NewService(repoFn) require.NoError(t, err, "Error when getting new project service.") + iamRepo, err := repoFn() + require.NoError(t, err) + global, err := iamRepo.LookupScope(context.Background(), "global") + require.NoError(t, err) + var orgVersion uint32 = 2 var projVersion uint32 = 2 + var globalVersion uint32 = global.Version resetOrg := func() { orgVersion++ @@ -797,6 +846,16 @@ func TestUpdate(t *testing.T) { projVersion++ } + resetGlobal := func() { + repo, err := repoFn() + require.NoError(t, err, "Couldn't get a new repo") + globalScope := iam.AllocScope() + globalScope.PublicId = "global" + global, _, err = repo.UpdateScope(context.Background(), &globalScope, globalVersion, []string{"Name", "Description"}) + require.NoError(t, err, "Failed to reset the global scope") + globalVersion = global.Version + } + projCreated, err := ptypes.Timestamp(proj.GetCreateTime().GetTimestamp()) require.NoError(t, err, "Error converting proto to timestamp") projToMerge := &pbs.UpdateScopeRequest{ @@ -807,6 +866,10 @@ func TestUpdate(t *testing.T) { Id: org.GetPublicId(), } + globalToMerge := &pbs.UpdateScopeRequest{ + Id: "global", + } + cases := []struct { name string scopeId string @@ -868,6 +931,32 @@ func TestUpdate(t *testing.T) { }, }, }, + { + name: "Update global", + scopeId: scope.Global.String(), + req: &pbs.UpdateScopeRequest{ + UpdateMask: &field_mask.FieldMask{ + Paths: []string{"name", "description"}, + }, + Item: &pb.Scope{ + Name: &wrapperspb.StringValue{Value: "new"}, + Description: &wrapperspb.StringValue{Value: "desc"}, + Type: scope.Global.String(), + }, + }, + res: &pbs.UpdateScopeResponse{ + Item: &pb.Scope{ + Id: scope.Global.String(), + Scope: &pb.ScopeInfo{Id: scope.Global.String(), Type: scope.Global.String(), Name: scope.Global.String(), Description: "Global Scope"}, + Name: &wrapperspb.StringValue{Value: "new"}, + Description: &wrapperspb.StringValue{Value: "desc"}, + CreatedTime: global.GetCreateTime().GetTimestamp(), + Type: scope.Global.String(), + AuthorizedActions: []string{"read", "update", "delete"}, + AuthorizedCollectionActions: globalAuthorizedCollectionActions, + }, + }, + }, { name: "Multiple Paths in single string", scopeId: org.GetPublicId(), @@ -1115,8 +1204,15 @@ func TestUpdate(t *testing.T) { assert, require := assert.New(t), require.New(t) var req *pbs.UpdateScopeRequest - switch tc.scopeId { - case scope.Global.String(): + switch { + case tc.scopeId == scope.Global.String() && tc.req.Item.GetType() == scope.Global.String(): + tc.req.Item.Version = globalVersion + ver = globalVersion + req = proto.Clone(globalToMerge).(*pbs.UpdateScopeRequest) + if tc.err == nil { + defer resetGlobal() + } + case tc.scopeId == scope.Global.String(): req = proto.Clone(orgToMerge).(*pbs.UpdateScopeRequest) if tc.err == nil { defer resetOrg() diff --git a/internal/servers/controller/handlers/sessions/session_service.go b/internal/servers/controller/handlers/sessions/session_service.go index d89352a1a..ead4c019c 100644 --- a/internal/servers/controller/handlers/sessions/session_service.go +++ b/internal/servers/controller/handlers/sessions/session_service.go @@ -337,12 +337,12 @@ func toProto(in *session.Session) *pb.Session { // * All required parameters are set // * There are no conflicting parameters provided func validateGetRequest(req *pbs.GetSessionRequest) error { - return handlers.ValidateGetRequest(session.SessionPrefix, req, handlers.NoopValidatorFn) + return handlers.ValidateGetRequest(handlers.NoopValidatorFn, req, session.SessionPrefix) } func validateListRequest(req *pbs.ListSessionsRequest) error { badFields := map[string]string{} - if !handlers.ValidId(scope.Project.Prefix(), req.GetScopeId()) && + if !handlers.ValidId(handlers.Id(req.GetScopeId()), scope.Project.Prefix()) && !req.GetRecursive() { badFields["scope_id"] = "This field must be a valid project scope ID or the list operation must be recursive." } @@ -357,7 +357,7 @@ func validateListRequest(req *pbs.ListSessionsRequest) error { func validateCancelRequest(req *pbs.CancelSessionRequest) error { badFields := map[string]string{} - if !handlers.ValidId(session.SessionPrefix, req.GetId()) { + if !handlers.ValidId(handlers.Id(req.GetId()), session.SessionPrefix) { badFields["id"] = "Improperly formatted identifier." } if req.GetVersion() == 0 { diff --git a/internal/servers/controller/handlers/subtypes.go b/internal/servers/controller/handlers/subtypes.go index 40bed5d7b..c49867328 100644 --- a/internal/servers/controller/handlers/subtypes.go +++ b/internal/servers/controller/handlers/subtypes.go @@ -6,12 +6,22 @@ import ( "google.golang.org/protobuf/types/known/structpb" ) -func StructToProto(fields *structpb.Struct, p proto.Message) error { +func StructToProto(fields *structpb.Struct, p proto.Message, opt ...Option) error { + if fields == nil { + // If there is not struct, don't update the default proto message. + return nil + } js, err := fields.MarshalJSON() if err != nil { return err } - if err := protojson.Unmarshal(js, p); err != nil { + opts := getOpts(opt...) + if opts.withDiscardUnknownFields { + err = (protojson.UnmarshalOptions{DiscardUnknown: true}.Unmarshal(js, p)) + } else { + err = protojson.Unmarshal(js, p) + } + if err != nil { return err } return nil diff --git a/internal/servers/controller/handlers/targets/target_service.go b/internal/servers/controller/handlers/targets/target_service.go index e7b6f6dde..45b834011 100644 --- a/internal/servers/controller/handlers/targets/target_service.go +++ b/internal/servers/controller/handlers/targets/target_service.go @@ -866,13 +866,13 @@ func toProto(in target.Target, m []*target.TargetSet) (*pb.Target, error) { // * All required parameters are set // * There are no conflicting parameters provided func validateGetRequest(req *pbs.GetTargetRequest) error { - return handlers.ValidateGetRequest(target.TcpTargetPrefix, req, handlers.NoopValidatorFn) + return handlers.ValidateGetRequest(handlers.NoopValidatorFn, req, target.TcpTargetPrefix) } func validateCreateRequest(req *pbs.CreateTargetRequest) error { return handlers.ValidateCreateRequest(req.GetItem(), func() map[string]string { badFields := map[string]string{} - if !handlers.ValidId(scope.Project.Prefix(), req.GetItem().GetScopeId()) { + if !handlers.ValidId(handlers.Id(req.GetItem().GetScopeId()), scope.Project.Prefix()) { badFields["scope_id"] = "This field is required to have a properly formatted project scope id." } if req.GetItem().GetName() == nil || req.GetItem().GetName().GetValue() == "" { @@ -917,7 +917,7 @@ func validateCreateRequest(req *pbs.CreateTargetRequest) error { } func validateUpdateRequest(req *pbs.UpdateTargetRequest) error { - return handlers.ValidateUpdateRequest(target.TcpTargetPrefix, req, req.GetItem(), func() map[string]string { + return handlers.ValidateUpdateRequest(req, req.GetItem(), func() map[string]string { badFields := map[string]string{} if handlers.MaskContains(req.GetUpdateMask().GetPaths(), "name") && req.GetItem().GetName().GetValue() == "" { badFields["name"] = "This field cannot be set to empty." @@ -953,16 +953,16 @@ func validateUpdateRequest(req *pbs.UpdateTargetRequest) error { } } return badFields - }) + }, target.TcpTargetPrefix) } func validateDeleteRequest(req *pbs.DeleteTargetRequest) error { - return handlers.ValidateDeleteRequest(target.TcpTargetPrefix, req, handlers.NoopValidatorFn) + return handlers.ValidateDeleteRequest(handlers.NoopValidatorFn, req, target.TcpTargetPrefix) } func validateListRequest(req *pbs.ListTargetsRequest) error { badFields := map[string]string{} - if !handlers.ValidId(scope.Project.Prefix(), req.GetScopeId()) && + if !handlers.ValidId(handlers.Id(req.GetScopeId()), scope.Project.Prefix()) && !req.GetRecursive() { badFields["scope_id"] = "This field must be a valid project scope ID or the list operation must be recursive." } @@ -977,7 +977,7 @@ func validateListRequest(req *pbs.ListTargetsRequest) error { func validateAddRequest(req *pbs.AddTargetHostSetsRequest) error { badFields := map[string]string{} - if !handlers.ValidId(target.TcpTargetPrefix, req.GetId()) { + if !handlers.ValidId(handlers.Id(req.GetId()), target.TcpTargetPrefix) { badFields["id"] = "Incorrectly formatted identifier." } if req.GetVersion() == 0 { @@ -987,7 +987,7 @@ func validateAddRequest(req *pbs.AddTargetHostSetsRequest) error { badFields["host_set_ids"] = "Must be non-empty." } for _, id := range req.GetHostSetIds() { - if !handlers.ValidId(static.HostSetPrefix, id) { + if !handlers.ValidId(handlers.Id(id), static.HostSetPrefix) { badFields["host_set_ids"] = fmt.Sprintf("Incorrectly formatted host set identifier %q.", id) break } @@ -1000,14 +1000,14 @@ func validateAddRequest(req *pbs.AddTargetHostSetsRequest) error { func validateSetRequest(req *pbs.SetTargetHostSetsRequest) error { badFields := map[string]string{} - if !handlers.ValidId(target.TcpTargetPrefix, req.GetId()) { + if !handlers.ValidId(handlers.Id(req.GetId()), target.TcpTargetPrefix) { badFields["id"] = "Incorrectly formatted identifier." } if req.GetVersion() == 0 { badFields["version"] = "Required field." } for _, id := range req.GetHostSetIds() { - if !handlers.ValidId(static.HostSetPrefix, id) { + if !handlers.ValidId(handlers.Id(id), static.HostSetPrefix) { badFields["host_set_ids"] = fmt.Sprintf("Incorrectly formatted host set identifier %q.", id) break } @@ -1020,7 +1020,7 @@ func validateSetRequest(req *pbs.SetTargetHostSetsRequest) error { func validateRemoveRequest(req *pbs.RemoveTargetHostSetsRequest) error { badFields := map[string]string{} - if !handlers.ValidId(target.TcpTargetPrefix, req.GetId()) { + if !handlers.ValidId(handlers.Id(req.GetId()), target.TcpTargetPrefix) { badFields["id"] = "Incorrectly formatted identifier." } if req.GetVersion() == 0 { @@ -1030,7 +1030,7 @@ func validateRemoveRequest(req *pbs.RemoveTargetHostSetsRequest) error { badFields["host_set_ids"] = "Must be non-empty." } for _, id := range req.GetHostSetIds() { - if !handlers.ValidId(static.HostSetPrefix, id) { + if !handlers.ValidId(handlers.Id(id), static.HostSetPrefix) { badFields["host_set_ids"] = fmt.Sprintf("Incorrectly formatted host set identifier %q.", id) break } @@ -1047,7 +1047,7 @@ func validateAuthorizeSessionRequest(req *pbs.AuthorizeSessionRequest) error { scopeIdEmpty := req.GetScopeId() == "" scopeNameEmpty := req.GetScopeName() == "" if nameEmpty { - if !handlers.ValidId(target.TcpTargetPrefix, req.GetId()) { + if !handlers.ValidId(handlers.Id(req.GetId()), target.TcpTargetPrefix) { badFields["id"] = "Incorrectly formatted identifier." } if !scopeIdEmpty { diff --git a/internal/servers/controller/handlers/users/user_service.go b/internal/servers/controller/handlers/users/user_service.go index 15a68f5ae..167253868 100644 --- a/internal/servers/controller/handlers/users/user_service.go +++ b/internal/servers/controller/handlers/users/user_service.go @@ -481,13 +481,13 @@ func toProto(in *iam.User, accts []string) *pb.User { // * All required parameters are set // * There are no conflicting parameters provided func validateGetRequest(req *pbs.GetUserRequest) error { - return handlers.ValidateGetRequest(iam.UserPrefix, req, handlers.NoopValidatorFn) + return handlers.ValidateGetRequest(handlers.NoopValidatorFn, req, iam.UserPrefix) } func validateCreateRequest(req *pbs.CreateUserRequest) error { return handlers.ValidateCreateRequest(req.GetItem(), func() map[string]string { badFields := map[string]string{} - if !handlers.ValidId(scope.Org.Prefix(), req.GetItem().GetScopeId()) && + if !handlers.ValidId(handlers.Id(req.GetItem().GetScopeId()), scope.Org.Prefix()) && scope.Global.String() != req.GetItem().GetScopeId() { badFields["scope_id"] = "Must be 'global' or a valid org scope id." } @@ -496,16 +496,16 @@ func validateCreateRequest(req *pbs.CreateUserRequest) error { } func validateUpdateRequest(req *pbs.UpdateUserRequest) error { - return handlers.ValidateUpdateRequest(iam.UserPrefix, req, req.GetItem(), handlers.NoopValidatorFn) + return handlers.ValidateUpdateRequest(req, req.GetItem(), handlers.NoopValidatorFn, iam.UserPrefix) } func validateDeleteRequest(req *pbs.DeleteUserRequest) error { - return handlers.ValidateDeleteRequest(iam.UserPrefix, req, handlers.NoopValidatorFn) + return handlers.ValidateDeleteRequest(handlers.NoopValidatorFn, req, iam.UserPrefix) } func validateListRequest(req *pbs.ListUsersRequest) error { badFields := map[string]string{} - if !handlers.ValidId(scope.Org.Prefix(), req.GetScopeId()) && + if !handlers.ValidId(handlers.Id(req.GetScopeId()), scope.Org.Prefix()) && req.GetScopeId() != scope.Global.String() { badFields["scope_id"] = "Must be 'global' or a valid org scope id when listing." } @@ -520,7 +520,7 @@ func validateListRequest(req *pbs.ListUsersRequest) error { func validateAddUserAccountsRequest(req *pbs.AddUserAccountsRequest) error { badFields := map[string]string{} - if !handlers.ValidId(iam.UserPrefix, req.GetId()) { + if !handlers.ValidId(handlers.Id(req.GetId()), iam.UserPrefix) { badFields["id"] = "Incorrectly formatted identifier." } if req.GetVersion() == 0 { @@ -531,7 +531,7 @@ func validateAddUserAccountsRequest(req *pbs.AddUserAccountsRequest) error { } for _, a := range req.GetAccountIds() { // TODO: Increase the type of auth accounts that can be added to a user. - if !handlers.ValidId(password.AccountPrefix, a) { + if !handlers.ValidId(handlers.Id(a), password.AccountPrefix) { badFields["account_ids"] = "Values must be valid account ids." break } @@ -544,7 +544,7 @@ func validateAddUserAccountsRequest(req *pbs.AddUserAccountsRequest) error { func validateSetUserAccountsRequest(req *pbs.SetUserAccountsRequest) error { badFields := map[string]string{} - if !handlers.ValidId(iam.UserPrefix, req.GetId()) { + if !handlers.ValidId(handlers.Id(req.GetId()), iam.UserPrefix) { badFields["id"] = "Incorrectly formatted identifier." } if req.GetVersion() == 0 { @@ -552,7 +552,7 @@ func validateSetUserAccountsRequest(req *pbs.SetUserAccountsRequest) error { } for _, a := range req.GetAccountIds() { // TODO: Increase the type of auth accounts that can be added to a user. - if !handlers.ValidId(password.AccountPrefix, a) { + if !handlers.ValidId(handlers.Id(a), password.AccountPrefix) { badFields["account_ids"] = "Values must be valid account ids." break } @@ -565,7 +565,7 @@ func validateSetUserAccountsRequest(req *pbs.SetUserAccountsRequest) error { func validateRemoveUserAccountsRequest(req *pbs.RemoveUserAccountsRequest) error { badFields := map[string]string{} - if !handlers.ValidId(iam.UserPrefix, req.GetId()) { + if !handlers.ValidId(handlers.Id(req.GetId()), iam.UserPrefix) { badFields["id"] = "Incorrectly formatted identifier." } if req.GetVersion() == 0 { @@ -576,7 +576,7 @@ func validateRemoveUserAccountsRequest(req *pbs.RemoveUserAccountsRequest) error } for _, a := range req.GetAccountIds() { // TODO: Increase the type of auth accounts that can be added to a user. - if !handlers.ValidId(password.AccountPrefix, a) { + if !handlers.ValidId(handlers.Id(a), password.AccountPrefix) { badFields["account_ids"] = "Values must be valid account ids." break } diff --git a/internal/servers/controller/handlers/verifiers.go b/internal/servers/controller/handlers/verifiers.go index ba7633262..49a80e8eb 100644 --- a/internal/servers/controller/handlers/verifiers.go +++ b/internal/servers/controller/handlers/verifiers.go @@ -13,6 +13,9 @@ import ( type CustomValidatorFunc func() map[string]string +// A boundary resource identifier +type Id string + var NoopValidatorFn CustomValidatorFunc = func() map[string]string { return nil } type ApiResource interface { @@ -75,9 +78,9 @@ type UpdateRequest interface { GetUpdateMask() *field_mask.FieldMask } -func ValidateUpdateRequest(prefix string, r UpdateRequest, i ApiResource, fn CustomValidatorFunc) error { +func ValidateUpdateRequest(r UpdateRequest, i ApiResource, fn CustomValidatorFunc, prefix ...string) error { badFields := map[string]string{} - if !ValidId(prefix, r.GetId()) { + if !ValidId(Id(r.GetId()), prefix...) { badFields["id"] = "Improperly formatted path identifier." } if r.GetUpdateMask() == nil { @@ -138,9 +141,9 @@ type GetRequest interface { GetId() string } -func ValidateGetRequest(prefix string, r GetRequest, fn CustomValidatorFunc) error { +func ValidateGetRequest(fn CustomValidatorFunc, r GetRequest, prefix ...string) error { badFields := map[string]string{} - if !ValidId(prefix, r.GetId()) { + if !ValidId(Id(r.GetId()), prefix...) { badFields["id"] = "Invalid formatted identifier." } for k, v := range fn() { @@ -156,9 +159,9 @@ type DeleteRequest interface { GetId() string } -func ValidateDeleteRequest(prefix string, r DeleteRequest, fn CustomValidatorFunc) error { +func ValidateDeleteRequest(fn CustomValidatorFunc, r DeleteRequest, prefix ...string) error { badFields := map[string]string{} - if !ValidId(prefix, r.GetId()) { + if !ValidId(Id(r.GetId()), prefix...) { badFields["id"] = "Incorrectly formatted identifier." } for k, v := range fn() { @@ -172,13 +175,19 @@ func ValidateDeleteRequest(prefix string, r DeleteRequest, fn CustomValidatorFun var reInvalidID = regexp.MustCompile("[^A-Za-z0-9]") -func ValidId(prefix, id string) bool { - prefix = prefix + "_" - if !strings.HasPrefix(id, prefix) { - return false +func ValidId(i Id, prefixes ...string) bool { + id := string(i) + for _, prefix := range prefixes { + prefix = prefix + "_" + if !strings.HasPrefix(id, prefix) { + continue + } + id = strings.TrimPrefix(id, prefix) + if !reInvalidID.Match([]byte(id)) { + return true + } } - id = strings.TrimPrefix(id, prefix) - return !reInvalidID.Match([]byte(id)) + return false } func ValidNameDescription(in string) bool { diff --git a/internal/servers/controller/handlers/verifiers_test.go b/internal/servers/controller/handlers/verifiers_test.go index 4514bbf80..bc1893836 100644 --- a/internal/servers/controller/handlers/verifiers_test.go +++ b/internal/servers/controller/handlers/verifiers_test.go @@ -28,15 +28,17 @@ func errorIncludesFields(t *testing.T, err error, wantFields []string) { // message. User was picked arbitrarily. func TestValidId(t *testing.T) { - assert.True(t, ValidId("prefix", "prefix_somerandomid")) - assert.True(t, ValidId("prefix", "prefix_short")) - assert.True(t, ValidId("prefix", "prefix_thisisalongidentifierwhichstillworks")) + assert.True(t, ValidId(Id("prefix_somerandomid"), "prefix")) + assert.True(t, ValidId(Id("prefix_somerandomid"), "notprefix", "prefix")) + assert.True(t, ValidId(Id("prefix_short"), "prefix")) + assert.True(t, ValidId(Id("prefix_thisisalongidentifierwhichstillworks"), "prefix")) - assert.False(t, ValidId("prefix", "prefixsomerandomid")) - assert.False(t, ValidId("prefix", "prefix_this has spaces")) - assert.False(t, ValidId("prefix", "prefix_includes-dash")) - assert.False(t, ValidId("prefix", "prefix_other@strange!characters")) - assert.False(t, ValidId("short", "prefix_short")) + assert.False(t, ValidId(Id("prefixsomerandomid"), "prefix")) + assert.False(t, ValidId(Id("prefixsomerandomid"), "prefix", "alsobadprefix")) + assert.False(t, ValidId(Id("prefix_this has spaces"), "prefix")) + assert.False(t, ValidId(Id("prefix_includes-dash"), "prefix")) + assert.False(t, ValidId(Id("prefix_other@strange!characters"), "prefix")) + assert.False(t, ValidId(Id("prefix_short"), "short")) } func TestValidNameDescription(t *testing.T) { @@ -95,7 +97,7 @@ func TestValidateGetRequest(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { - err := ValidateGetRequest(tc.prefix, tc.req, tc.valFn) + err := ValidateGetRequest(tc.valFn, tc.req, tc.prefix) if len(tc.badFields) == 0 { if !assert.NoError(t, err) { errorIncludesFields(t, err, []string{}) @@ -158,7 +160,7 @@ func TestValidateDeleteRequest(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { - err := ValidateDeleteRequest(tc.prefix, tc.req, tc.valFn) + err := ValidateDeleteRequest(tc.valFn, tc.req, tc.prefix) if len(tc.badFields) == 0 { if !assert.NoError(t, err) { errorIncludesFields(t, err, []string{}) @@ -462,7 +464,7 @@ func TestValidateUpdateRequest(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { - err := ValidateUpdateRequest(tc.prefix, tc.req, tc.item, tc.valFn) + err := ValidateUpdateRequest(tc.req, tc.item, tc.valFn, tc.prefix) if len(tc.badFields) == 0 { if !assert.NoError(t, err) { errorIncludesFields(t, err, []string{}) diff --git a/internal/servers/controller/multi_test.go b/internal/servers/controller/multi_test.go index dd444f82d..bbe874a9e 100644 --- a/internal/servers/controller/multi_test.go +++ b/internal/servers/controller/multi_test.go @@ -1,10 +1,12 @@ package controller_test import ( + "encoding/json" "testing" "time" "github.com/hashicorp/boundary/api/authmethods" + "github.com/hashicorp/boundary/api/authtokens" "github.com/hashicorp/boundary/api/scopes" "github.com/hashicorp/boundary/internal/servers/controller" "github.com/hashicorp/boundary/internal/types/scope" @@ -30,16 +32,18 @@ func TestAuthenticationMulti(t *testing.T) { defer c2.Shutdown() auth := authmethods.NewClient(c1.Client()) - token1Result, err := auth.Authenticate(c1.Context(), c1.Server().DevAuthMethodId, "login", map[string]interface{}{"login_name": c1.Server().DevLoginName, "password": c1.Server().DevPassword}) + token1Result, err := auth.Authenticate(c1.Context(), c1.Server().DevPasswordAuthMethodId, "login", map[string]interface{}{"login_name": c1.Server().DevLoginName, "password": c1.Server().DevPassword}) require.Nil(err) - token1 := token1Result.Item + token1 := new(authtokens.AuthToken) + require.NoError(json.Unmarshal(token1Result.GetRawAttributes(), token1)) require.NotNil(token1) time.Sleep(5 * time.Second) auth = authmethods.NewClient(c2.Client()) - token2Result, err := auth.Authenticate(c2.Context(), c2.Server().DevAuthMethodId, "login", map[string]interface{}{"login_name": c2.Server().DevLoginName, "password": c2.Server().DevPassword}) + token2Result, err := auth.Authenticate(c2.Context(), c2.Server().DevPasswordAuthMethodId, "login", map[string]interface{}{"login_name": c2.Server().DevLoginName, "password": c2.Server().DevPassword}) require.Nil(err) - token2 := token2Result.Item + token2 := new(authtokens.AuthToken) + require.NoError(json.Unmarshal(token2Result.GetRawAttributes(), token2)) require.NotNil(token2) assert.NotEqual(token1.Token, token2.Token) diff --git a/internal/servers/controller/testing.go b/internal/servers/controller/testing.go index 6697d7d50..a2cf5978e 100644 --- a/internal/servers/controller/testing.go +++ b/internal/servers/controller/testing.go @@ -2,6 +2,7 @@ package controller import ( "context" + "encoding/json" "fmt" "net" "strconv" @@ -128,9 +129,9 @@ func (tc *TestController) Token() *authtokens.AuthToken { tc.t.Error("no default auth method ID configured") return nil } - token, err := authmethods.NewClient(tc.Client()).Authenticate( + result, err := authmethods.NewClient(tc.Client()).Authenticate( tc.Context(), - tc.b.DevAuthMethodId, + tc.b.DevPasswordAuthMethodId, "login", map[string]interface{}{ "login_name": tc.b.DevLoginName, @@ -141,7 +142,12 @@ func (tc *TestController) Token() *authtokens.AuthToken { tc.t.Error(fmt.Errorf("error logging in: %w", err)) return nil } - return token.Item + token := new(authtokens.AuthToken) + if err := json.Unmarshal(result.GetRawAttributes(), token); err != nil { + tc.t.Error(fmt.Errorf("error unmarshaling token: %w", err)) + return nil + } + return token } func (tc *TestController) UnprivilegedToken() *authtokens.AuthToken { @@ -149,9 +155,9 @@ func (tc *TestController) UnprivilegedToken() *authtokens.AuthToken { tc.t.Error("no default auth method ID configured") return nil } - token, err := authmethods.NewClient(tc.Client()).Authenticate( + result, err := authmethods.NewClient(tc.Client()).Authenticate( tc.Context(), - tc.b.DevAuthMethodId, + tc.b.DevPasswordAuthMethodId, "login", map[string]interface{}{ "login_name": tc.b.DevUnprivilegedLoginName, @@ -162,7 +168,12 @@ func (tc *TestController) UnprivilegedToken() *authtokens.AuthToken { tc.t.Error(fmt.Errorf("error logging in: %w", err)) return nil } - return token.Item + token := new(authtokens.AuthToken) + if err := json.Unmarshal(result.GetRawAttributes(), token); err != nil { + tc.t.Error(fmt.Errorf("error unmarshaling token: %w", err)) + return nil + } + return token } func (tc *TestController) addrs(purpose string) []string { @@ -384,9 +395,9 @@ func NewTestController(t *testing.T, opts *TestControllerOpts) *TestController { } if opts.DefaultAuthMethodId != "" { - tc.b.DevAuthMethodId = opts.DefaultAuthMethodId + tc.b.DevPasswordAuthMethodId = opts.DefaultAuthMethodId } else { - tc.b.DevAuthMethodId = DefaultTestAuthMethodId + tc.b.DevPasswordAuthMethodId = DefaultTestAuthMethodId } if opts.DefaultLoginName != "" { tc.b.DevLoginName = opts.DefaultLoginName @@ -428,7 +439,7 @@ func NewTestController(t *testing.T, opts *TestControllerOpts) *TestController { if opts.InitialResourcesSuffix != "" { suffix := opts.InitialResourcesSuffix - tc.b.DevAuthMethodId = "ampw_" + suffix + tc.b.DevPasswordAuthMethodId = "ampw_" + suffix tc.b.DevHostCatalogId = "hcst_" + suffix tc.b.DevHostId = "hst_" + suffix tc.b.DevHostSetId = "hsst_" + suffix @@ -486,7 +497,7 @@ func NewTestController(t *testing.T, opts *TestControllerOpts) *TestController { t.Fatal(err) } if !opts.DisableAuthMethodCreation { - if _, _, err := tc.b.CreateInitialAuthMethod(ctx); err != nil { + if _, _, err := tc.b.CreateInitialPasswordAuthMethod(ctx); err != nil { t.Fatal(err) } if !opts.DisableScopesCreation { @@ -547,7 +558,7 @@ func (tc *TestController) AddClusterControllerMember(t *testing.T, opts *TestCon } nextOpts := &TestControllerOpts{ DatabaseUrl: tc.c.conf.DatabaseUrl, - DefaultAuthMethodId: tc.c.conf.DevAuthMethodId, + DefaultAuthMethodId: tc.c.conf.DevPasswordAuthMethodId, RootKms: tc.c.conf.RootKms, WorkerAuthKms: tc.c.conf.WorkerAuthKms, RecoveryKms: tc.c.conf.RecoveryKms, diff --git a/internal/servers/controller/tickers.go b/internal/servers/controller/tickers.go index 883558e4f..82f9e46c9 100644 --- a/internal/servers/controller/tickers.go +++ b/internal/servers/controller/tickers.go @@ -116,3 +116,41 @@ func (c *Controller) startTerminateCompletedSessionsTicking(cancelCtx context.Co } }() } + +func (c *Controller) startCloseExpiredPendingTokens(cancelCtx context.Context) { + go func() { + r := rand.New(rand.NewSource(time.Now().UnixNano())) + // desynchronize calls from the controllers, to ease the load on the DB. + getRandomInterval := func() time.Duration { + // 0 to 0.5 adjustment to the base + f := r.Float64() / 2 + // Half a chance to be faster, not slower + if r.Float32() > 0.5 { + f = -1 * f + } + return terminationInterval + time.Duration(f*float64(time.Minute)) + } + timer := time.NewTimer(0) + for { + select { + case <-cancelCtx.Done(): + c.logger.Info("closing expired pending tokens ticking shutting down") + return + + case <-timer.C: + repo, err := c.AuthTokenRepoFn() + if err != nil { + c.logger.Error("error fetching repository for closing expired pending tokens", "error", err) + } else { + closeCount, err := repo.CloseExpiredPendingTokens(cancelCtx) + if err != nil { + c.logger.Error("error performing closing expired pending tokens", "error", err) + } else if closeCount > 0 { + c.logger.Info("closing expired pending tokens completed sessions successful", "pending_tokens_closed", closeCount) + } + } + timer.Reset(getRandomInterval()) + } + } + }() +} diff --git a/internal/session/testing.go b/internal/session/testing.go index b17dd98ca..05d96e9a5 100644 --- a/internal/session/testing.go +++ b/internal/session/testing.go @@ -138,8 +138,7 @@ func TestSessionParams(t *testing.T, conn *gorm.DB, wrapper wrapping.Wrapper, ia authMethod := password.TestAuthMethods(t, conn, org.PublicId, 1)[0] acct := password.TestAccounts(t, conn, authMethod.GetPublicId(), 1)[0] - user, err := iamRepo.LookupUserWithLogin(ctx, acct.GetPublicId(), iam.WithAutoVivify(true)) - require.NoError(err) + user := iam.TestUser(t, iamRepo, org.PublicId, iam.WithAccountIds(acct.PublicId)) authTokenRepo, err := authtoken.NewRepository(rw, rw, kms) require.NoError(err) diff --git a/internal/session/util.go b/internal/session/util.go index 02f3d8f1f..175eeb398 100644 --- a/internal/session/util.go +++ b/internal/session/util.go @@ -2,42 +2,27 @@ package session import ( "crypto/ed25519" - "crypto/sha256" - "fmt" - "io" "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/kms" wrapping "github.com/hashicorp/go-kms-wrapping" - "github.com/hashicorp/go-kms-wrapping/wrappers/aead" - "github.com/hashicorp/go-kms-wrapping/wrappers/multiwrapper" - "golang.org/x/crypto/hkdf" ) // DeriveED25519Key generates a key based on the scope's session DEK, the // requesting user, and the generated job ID. func DeriveED25519Key(wrapper wrapping.Wrapper, userId, jobId string) (ed25519.PublicKey, ed25519.PrivateKey, error) { const op = "session.DeriveED25519Key" - var aeadWrapper *aead.Wrapper - switch w := wrapper.(type) { - case *multiwrapper.MultiWrapper: - raw := w.WrapperForKeyID("__base__") - var ok bool - if aeadWrapper, ok = raw.(*aead.Wrapper); !ok { - return nil, nil, errors.New(errors.InvalidParameter, op, "unexpected wrapper type from multiwrapper base") - } - case *aead.Wrapper: - aeadWrapper = w - default: - return nil, nil, errors.New(errors.InvalidParameter, op, fmt.Sprintf("unknown wrapper type %T", wrapper)) + var uId, jId []byte + if userId != "" { + uId = []byte(userId) } - reader := hkdf.New(sha256.New, aeadWrapper.GetKeyBytes(), []byte(jobId), []byte(userId)) - limitedReader := &io.LimitedReader{ - R: reader, - N: 32, + if jobId != "" { + jId = []byte(jobId) } - pub, pvt, err := ed25519.GenerateKey(limitedReader) + + reader, err := kms.NewDerivedReader(wrapper, 32, uId, jId) if err != nil { - return nil, nil, errors.Wrap(err, op, errors.WithCode(errors.GenKey)) + return nil, nil, errors.Wrap(err, op) } - return pub, pvt, nil + return ed25519.GenerateKey(reader) } diff --git a/internal/session/util_test.go b/internal/session/util_test.go new file mode 100644 index 000000000..78203ec14 --- /dev/null +++ b/internal/session/util_test.go @@ -0,0 +1,76 @@ +package session + +import ( + "crypto/ed25519" + "testing" + + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/kms" + wrapping "github.com/hashicorp/go-kms-wrapping" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestDeriveED25519Key(t *testing.T) { + wrapper := db.TestWrapper(t) + type keys struct { + pub ed25519.PublicKey + priv ed25519.PrivateKey + } + + type args struct { + wrapper wrapping.Wrapper + userId string + jobId string + } + tests := []struct { + name string + args args + want keys + wantErr bool + wantErrCode errors.Code + }{ + { + name: "valid", + args: args{ + wrapper: wrapper, + userId: "userId", + jobId: "jobId", + }, + want: func() keys { + reader, err := kms.NewDerivedReader(wrapper, 32, []byte("userId"), []byte("jobId")) + require.NoError(t, err) + pub, priv, err := ed25519.GenerateKey(reader) + require.NoError(t, err) + return keys{pub: pub, priv: priv} + }(), + }, + { + name: "nil-wrapper", + args: args{ + wrapper: nil, + userId: "userId", + jobId: "jobId", + }, + wantErr: true, + wantErrCode: errors.InvalidParameter, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + gotPub, gotPriv, err := DeriveED25519Key(tt.args.wrapper, tt.args.userId, tt.args.jobId) + if tt.wantErr { + require.Error(err) + assert.Truef(errors.Match(errors.T(errors.InvalidParameter), err), "unexpected error: %s", err) + assert.Nil(gotPub) + assert.Nil(gotPriv) + return + } + require.NoError(err) + assert.Equalf(tt.want.pub, gotPub, "gotPub = %v, want %v", gotPub, tt.want.pub) + assert.Equalf(tt.want.priv, gotPriv, "gotPriv = %v, want %v", gotPriv, tt.want.priv) + }) + } +} diff --git a/internal/tests/api/accounts/account_test.go b/internal/tests/api/accounts/account_test.go index 89ccb7a86..a5cc28e7e 100644 --- a/internal/tests/api/accounts/account_test.go +++ b/internal/tests/api/accounts/account_test.go @@ -8,6 +8,7 @@ import ( "github.com/hashicorp/boundary/api" "github.com/hashicorp/boundary/api/accounts" "github.com/hashicorp/boundary/api/authmethods" + "github.com/hashicorp/boundary/internal/auth/oidc" "github.com/hashicorp/boundary/internal/auth/password" "github.com/hashicorp/boundary/internal/iam" "github.com/hashicorp/boundary/internal/servers/controller" @@ -15,7 +16,7 @@ import ( "github.com/stretchr/testify/require" ) -func TestList(t *testing.T) { +func TestListPassword(t *testing.T) { assert, require := assert.New(t), require.New(t) tc := controller.NewTestController(t, nil) defer tc.Shutdown() @@ -66,6 +67,63 @@ func TestList(t *testing.T) { assert.Equal(filterItem.Id, ulResult.Items[0].Id) } +func TestListOidc(t *testing.T) { + assert, require := assert.New(t), require.New(t) + tc := controller.NewTestController(t, nil) + defer tc.Shutdown() + + client := tc.Client() + require.NotNil(client) + token := tc.Token() + require.NotNil(token) + client.SetToken(token.Token) + org := iam.TestOrg(t, tc.IamRepo(), iam.WithUserId(token.UserId)) + amClient := authmethods.NewClient(client) + + amResult, err := amClient.Create(tc.Context(), "oidc", org.PublicId, + authmethods.WithName("foo"), + authmethods.WithOidcAuthMethodApiUrlPrefix("https://api.com"), + authmethods.WithOidcAuthMethodIssuer("https://example.com"), + authmethods.WithOidcAuthMethodClientSecret("secret"), + authmethods.WithOidcAuthMethodClientId("client-id")) + require.NoError(err) + require.NotNil(amResult) + am := amResult.Item + + accountClient := accounts.NewClient(client) + + lr, err := accountClient.List(tc.Context(), am.Id) + require.NoError(err) + expected := lr.Items + assert.Len(expected, 0) + + cr, err := accountClient.Create(tc.Context(), am.Id, + accounts.WithOidcAccountSubject("subject0")) + require.NoError(err) + expected = append(expected, cr.Item) + + ulResult, err := accountClient.List(tc.Context(), am.Id) + require.NoError(err) + assert.ElementsMatch(comparableSlice(expected[:1]), comparableSlice(ulResult.Items)) + + for i := 1; i < 10; i++ { + newAcctResult, err := accountClient.Create(tc.Context(), am.Id, + accounts.WithOidcAccountSubject(fmt.Sprintf("subject-%d", i))) + require.NoError(err) + expected = append(expected, newAcctResult.Item) + } + ulResult, err = accountClient.List(tc.Context(), am.Id) + require.NoError(err) + assert.ElementsMatch(comparableSlice(expected), comparableSlice(ulResult.Items)) + + filterItem := expected[3] + ulResult, err = accountClient.List(tc.Context(), am.Id, + accounts.WithFilter(fmt.Sprintf(`"/item/attributes/subject"==%q`, filterItem.Attributes["subject"]))) + require.NoError(err) + assert.Len(ulResult.Items, 1) + assert.Equal(filterItem.Id, ulResult.Items[0].Id) +} + func comparableSlice(in []*accounts.Account) []accounts.Account { var filtered []accounts.Account for _, i := range in { @@ -82,7 +140,7 @@ func comparableSlice(in []*accounts.Account) []accounts.Account { return filtered } -func TestCrud(t *testing.T) { +func TestCrudPassword(t *testing.T) { assert, require := assert.New(t), require.New(t) tc := controller.NewTestController(t, nil) defer tc.Shutdown() @@ -120,6 +178,55 @@ func TestCrud(t *testing.T) { require.NoError(err) } +func TestCrudOidc(t *testing.T) { + assert, require := assert.New(t), require.New(t) + tc := controller.NewTestController(t, nil) + defer tc.Shutdown() + + client := tc.Client() + token := tc.Token() + client.SetToken(token.Token) + amClient := authmethods.NewClient(client) + amResult, err := amClient.Create(tc.Context(), "oidc", "global", + authmethods.WithName("foo"), + authmethods.WithOidcAuthMethodApiUrlPrefix("https://api.com"), + authmethods.WithOidcAuthMethodIssuer("https://example.com"), + authmethods.WithOidcAuthMethodClientSecret("secret"), + authmethods.WithOidcAuthMethodClientId("client-id")) + require.NoError(err) + require.NotNil(amResult) + amId := amResult.Item.Id + + accountClient := accounts.NewClient(client) + + checkAccount := func(step string, u *accounts.Account, err error, wantedName string, wantedVersion uint32) { + assert.NoError(err, step) + require.NotNil(u, "returned no resource", step) + gotName := "" + if u.Name != "" { + gotName = u.Name + } + assert.Equal(wantedName, gotName, step) + assert.EqualValues(wantedVersion, u.Version) + } + + u, err := accountClient.Create(tc.Context(), amId, accounts.WithName("foo"), + accounts.WithOidcAccountSubject("subject")) + checkAccount("create", u.Item, err, "foo", 1) + + u, err = accountClient.Read(tc.Context(), u.Item.Id) + checkAccount("read", u.Item, err, "foo", 1) + + u, err = accountClient.Update(tc.Context(), u.Item.Id, u.Item.Version, accounts.WithName("bar")) + checkAccount("update", u.Item, err, "bar", 2) + + u, err = accountClient.Update(tc.Context(), u.Item.Id, u.Item.Version, accounts.DefaultName()) + checkAccount("update", u.Item, err, "", 3) + + _, err = accountClient.Delete(tc.Context(), u.Item.Id) + require.NoError(err) +} + func TestCustomMethods(t *testing.T) { assert, require := assert.New(t), require.New(t) tc := controller.NewTestController(t, nil) @@ -209,3 +316,64 @@ func TestErrors(t *testing.T) { require.NotNil(apiErr) assert.EqualValues(http.StatusBadRequest, apiErr.Response().StatusCode()) } + +func TestErrorsOidc(t *testing.T) { + assert, require := assert.New(t), require.New(t) + tc := controller.NewTestController(t, nil) + defer tc.Shutdown() + + client := tc.Client() + token := tc.Token() + client.SetToken(token.Token) + + amClient := authmethods.NewClient(client) + amResult, err := amClient.Create(tc.Context(), "oidc", "global", + authmethods.WithName("foo"), + authmethods.WithOidcAuthMethodApiUrlPrefix("https://api.com"), + authmethods.WithOidcAuthMethodIssuer("https://example.com"), + authmethods.WithOidcAuthMethodClientSecret("secret"), + authmethods.WithOidcAuthMethodClientId("client-id")) + require.NoError(err) + require.NotNil(amResult) + amId := amResult.Item.Id + + accountClient := accounts.NewClient(client) + + u, err := accountClient.Create(tc.Context(), amId, + accounts.WithOidcAccountSubject("subject1")) + require.NoError(err) + assert.NotNil(u) + + // Updating the wrong version should fail. + _, err = accountClient.Update(tc.Context(), u.Item.Id, 73, accounts.WithName("anything")) + require.Error(err) + apiErr := api.AsServerError(err) + require.NotNil(apiErr) + assert.EqualValues(http.StatusNotFound, apiErr.Response().StatusCode()) + + // Create another resource with the same name. + _, err = accountClient.Create(tc.Context(), amId, + accounts.WithOidcAccountSubject("subject1")) + require.Error(err) + apiErr = api.AsServerError(err) + require.NotNil(apiErr) + + _, err = accountClient.Read(tc.Context(), oidc.AccountPrefix+"_doesntexis") + require.Error(err) + apiErr = api.AsServerError(err) + require.NotNil(apiErr) + assert.EqualValues(http.StatusNotFound, apiErr.Response().StatusCode()) + + _, err = accountClient.Read(tc.Context(), "invalid id") + require.Error(err) + apiErr = api.AsServerError(err) + require.NotNil(apiErr) + assert.EqualValues(http.StatusBadRequest, apiErr.Response().StatusCode()) + + // Can't update issuer + _, err = accountClient.Update(tc.Context(), u.Item.Id, u.Item.Version, accounts.WithOidcAccountSubject("new")) + require.Error(err) + apiErr = api.AsServerError(err) + require.NotNil(apiErr) + assert.EqualValues(http.StatusBadRequest, apiErr.Response().StatusCode()) +} diff --git a/internal/tests/api/authmethods/authenticate_test.go b/internal/tests/api/authmethods/authenticate_test.go index 790052fd8..837f24c92 100644 --- a/internal/tests/api/authmethods/authenticate_test.go +++ b/internal/tests/api/authmethods/authenticate_test.go @@ -1,6 +1,7 @@ package authmethods_test import ( + "encoding/json" "fmt" "net/http" "testing" @@ -21,11 +22,11 @@ func TestAuthenticate(t *testing.T) { client := tc.Client() methods := authmethods.NewClient(client) - tok, err := methods.Authenticate(tc.Context(), tc.Server().DevAuthMethodId, "login", map[string]interface{}{"login_name": "user", "password": "passpass"}) + tok, err := methods.Authenticate(tc.Context(), tc.Server().DevPasswordAuthMethodId, "login", map[string]interface{}{"login_name": "user", "password": "passpass"}) require.NoError(err) assert.NotNil(tok) - _, err = methods.Authenticate(tc.Context(), tc.Server().DevAuthMethodId, "login", map[string]interface{}{"login_name": "user", "password": "wrong"}) + _, err = methods.Authenticate(tc.Context(), tc.Server().DevPasswordAuthMethodId, "login", map[string]interface{}{"login_name": "user", "password": "wrong"}) require.Error(err) apiErr := api.AsServerError(err) require.NotNil(apiErr) @@ -35,16 +36,17 @@ func TestAuthenticate(t *testing.T) { reqBody := map[string]interface{}{ "credentials": map[string]interface{}{"login_name": "user", "password": "passpass"}, } - req, err := client.NewRequest(tc.Context(), "POST", fmt.Sprintf("auth-methods/%s:authenticate", tc.Server().DevAuthMethodId), reqBody) + req, err := client.NewRequest(tc.Context(), "POST", fmt.Sprintf("auth-methods/%s:authenticate", tc.Server().DevPasswordAuthMethodId), reqBody) require.NoError(err) resp, err := client.Do(req) require.NoError(err) - target := new(authtokens.AuthTokenReadResult) - target.Item = new(authtokens.AuthToken) - apiErr, err = resp.Decode(target.Item) + result := new(authmethods.AuthenticateResult) + apiErr, err = resp.Decode(result) require.NoError(err) require.Nil(apiErr) - require.NotNil(target.GetItem()) - require.NotEmpty(target.GetItem().(*authtokens.AuthToken).Token) + + token := new(authtokens.AuthToken) + require.NoError(json.Unmarshal(result.GetRawAttributes(), token)) + require.NotEmpty(token.Token) } diff --git a/internal/tests/api/authmethods/authmethod_test.go b/internal/tests/api/authmethods/authmethod_test.go new file mode 100644 index 000000000..eb811317b --- /dev/null +++ b/internal/tests/api/authmethods/authmethod_test.go @@ -0,0 +1,168 @@ +package authmethods_test + +import ( + "net/http" + "testing" + + "github.com/hashicorp/boundary/api" + "github.com/hashicorp/boundary/api/authmethods" + "github.com/hashicorp/boundary/internal/auth/password" + "github.com/hashicorp/boundary/internal/servers/controller" + capoidc "github.com/hashicorp/cap/oidc" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const global = "global" + +func TestCrud(t *testing.T) { + assert, require := assert.New(t), require.New(t) + tc := controller.NewTestController(t, nil) + defer tc.Shutdown() + + client := tc.Client() + token := tc.Token() + client.SetToken(token.Token) + amClient := authmethods.NewClient(client) + + checkAuthMethod := func(step string, u *authmethods.AuthMethod, wantedName string, wantedVersion uint32) { + require.NotNil(u, "returned no resource", step) + gotName := "" + if u.Name != "" { + gotName = u.Name + } + assert.Equal(wantedName, gotName, step) + assert.EqualValues(wantedVersion, u.Version) + } + + u, err := amClient.Create(tc.Context(), "password", global, + authmethods.WithName("bar")) + require.NoError(err) + checkAuthMethod("create", u.Item, "bar", 1) + + u, err = amClient.Read(tc.Context(), u.Item.Id) + require.NoError(err) + checkAuthMethod("read", u.Item, "bar", 1) + + u, err = amClient.Update(tc.Context(), u.Item.Id, u.Item.Version, authmethods.WithName("buz")) + require.NoError(err) + checkAuthMethod("update", u.Item, "buz", 2) + + u, err = amClient.Update(tc.Context(), u.Item.Id, u.Item.Version, authmethods.DefaultName()) + require.NoError(err) + checkAuthMethod("update", u.Item, "", 3) + + _, err = amClient.Delete(tc.Context(), u.Item.Id) + require.NoError(err) + + // OIDC auth methods + u, err = amClient.Create(tc.Context(), "oidc", global, + authmethods.WithName("foo"), + authmethods.WithOidcAuthMethodApiUrlPrefix("https://api.com"), + authmethods.WithOidcAuthMethodIssuer("https://example.com"), + authmethods.WithOidcAuthMethodClientSecret("secret"), + authmethods.WithOidcAuthMethodClientId("client-id")) + require.NoError(err) + checkAuthMethod("create", u.Item, "foo", 1) + + u, err = amClient.Read(tc.Context(), u.Item.Id) + require.NoError(err) + checkAuthMethod("read", u.Item, "foo", 1) + + u, err = amClient.Update(tc.Context(), u.Item.Id, u.Item.Version, authmethods.WithName("bar")) + require.NoError(err) + checkAuthMethod("update", u.Item, "bar", 2) + + u, err = amClient.Update(tc.Context(), u.Item.Id, u.Item.Version, authmethods.DefaultName()) + require.NoError(err) + checkAuthMethod("update", u.Item, "", 3) + + _, err = amClient.Delete(tc.Context(), u.Item.Id) + require.NoError(err) +} + +func TestCustomMethods(t *testing.T) { + tc := controller.NewTestController(t, nil) + defer tc.Shutdown() + + client := tc.Client() + token := tc.Token() + client.SetToken(token.Token) + + amClient := authmethods.NewClient(client) + + tp := capoidc.StartTestProvider(t) + tpClientId := "alice-rp" + tpClientSecret := "her-dog's-name" + tp.SetClientCreds(tpClientId, tpClientSecret) + _, _, tpAlg, _ := tp.SigningKeys() + + u, err := amClient.Create(tc.Context(), "oidc", global, + authmethods.WithName("foo"), + authmethods.WithOidcAuthMethodIssuer(tp.Addr()), + authmethods.WithOidcAuthMethodApiUrlPrefix("https://example.com"), + authmethods.WithOidcAuthMethodClientSecret("secret"), + authmethods.WithOidcAuthMethodClientId("client-id"), + authmethods.WithOidcAuthMethodSigningAlgorithms([]string{string(tpAlg)}), + authmethods.WithOidcAuthMethodIdpCaCerts([]string{tp.CACert()})) + require.NoError(t, err) + + const newState = "active-private" + u, err = amClient.ChangeState(tc.Context(), u.Item.Id, u.Item.Version, newState) + assert.NoError(t, err) + assert.NotNil(t, u) + assert.Equal(t, newState, u.Item.Attributes["state"]) + + _, err = amClient.ChangeState(tc.Context(), u.Item.Id, u.Item.Version, "") + assert.Error(t, err) +} + +func TestErrors(t *testing.T) { + assert, require := assert.New(t), require.New(t) + tc := controller.NewTestController(t, nil) + defer tc.Shutdown() + + client := tc.Client() + token := tc.Token() + client.SetToken(token.Token) + amClient := authmethods.NewClient(client) + + u, err := amClient.Create(tc.Context(), "password", global, + authmethods.WithName("foo")) + require.NoError(err) + assert.NotNil(u) + + // Updating the wrong version should fail. + _, err = amClient.Update(tc.Context(), u.Item.Id, 73, authmethods.WithName("anything")) + require.Error(err) + apiErr := api.AsServerError(err) + require.NotNil(apiErr) + assert.EqualValues(http.StatusNotFound, apiErr.Response().StatusCode()) + + // Create another resource with the same name. + _, err = amClient.Create(tc.Context(), "password", global, + authmethods.WithName("foo")) + require.Error(err) + apiErr = api.AsServerError(err) + require.NotNil(apiErr) + + // TODO: Confirm that we can't create an oidc auth method with the same name. + + _, err = amClient.Read(tc.Context(), password.AuthMethodPrefix+"_doesntexis") + require.Error(err) + apiErr = api.AsServerError(err) + require.NotNil(apiErr) + assert.EqualValues(http.StatusNotFound, apiErr.Response().StatusCode()) + + _, err = amClient.Read(tc.Context(), "invalid id") + require.Error(err) + apiErr = api.AsServerError(err) + require.NotNil(apiErr) + assert.EqualValues(http.StatusBadRequest, apiErr.Response().StatusCode()) + + _, err = amClient.Update(tc.Context(), u.Item.Id, u.Item.Version) + require.Error(err) + apiErr = api.AsServerError(err) + require.NotNil(apiErr) + assert.EqualValues(http.StatusBadRequest, apiErr.Response().StatusCode()) +} diff --git a/internal/tests/api/authtokens/authtoken_test.go b/internal/tests/api/authtokens/authtoken_test.go index 251725a74..f4e190d22 100644 --- a/internal/tests/api/authtokens/authtoken_test.go +++ b/internal/tests/api/authtokens/authtoken_test.go @@ -1,6 +1,7 @@ package authtokens_test import ( + "encoding/json" "fmt" "net/http" "sort" @@ -11,6 +12,7 @@ import ( "github.com/hashicorp/boundary/api/authmethods" "github.com/hashicorp/boundary/api/authtokens" "github.com/hashicorp/boundary/api/roles" + "github.com/hashicorp/boundary/api/scopes" "github.com/hashicorp/boundary/internal/authtoken" "github.com/hashicorp/boundary/internal/iam" "github.com/hashicorp/boundary/internal/servers/controller" @@ -34,6 +36,12 @@ func TestList(t *testing.T) { require.NotNil(amResult) amId := amResult.Item.Id + scopeClient := scopes.NewClient(client) + scopeResult, err := scopeClient.Update(tc.Context(), org.PublicId, org.Version, scopes.WithAutomaticVersioning(true), scopes.WithPrimaryAuthMethodId(amId)) + require.NoError(err) + require.NotNil(scopeResult) + require.Equal(amId, scopeResult.Item.PrimaryAuthMethodId) + rolesClient := roles.NewClient(client) role, err := rolesClient.Create(tc.Context(), org.GetPublicId()) require.NoError(err) @@ -59,18 +67,22 @@ func TestList(t *testing.T) { var expected []*authtokens.AuthToken methods := authmethods.NewClient(client) - at, err := methods.Authenticate(tc.Context(), amId, "login", map[string]interface{}{"login_name": "user", "password": "passpass"}) + result, err := methods.Authenticate(tc.Context(), amId, "login", map[string]interface{}{"login_name": "user", "password": "passpass"}) require.NoError(err) - expected = append(expected, at.Item) + token = new(authtokens.AuthToken) + require.NoError(json.Unmarshal(result.GetRawAttributes(), token)) + expected = append(expected, token) atl, err = tokens.List(tc.Context(), org.GetPublicId()) require.NoError(err) assert.ElementsMatch(comparableSlice(expected), comparableSlice(atl.Items)) for i := 1; i < 10; i++ { - at, err = methods.Authenticate(tc.Context(), amId, "login", map[string]interface{}{"login_name": "user", "password": "passpass"}) + result, err = methods.Authenticate(tc.Context(), amId, "login", map[string]interface{}{"login_name": "user", "password": "passpass"}) require.NoError(err) - expected = append(expected, at.Item) + token = new(authtokens.AuthToken) + require.NoError(json.Unmarshal(result.GetRawAttributes(), token)) + expected = append(expected, token) } atl, err = tokens.List(tc.Context(), org.GetPublicId()) require.NoError(err) @@ -118,12 +130,14 @@ func TestCrud(t *testing.T) { tokens := authtokens.NewClient(client) methods := authmethods.NewClient(client) - want, err := methods.Authenticate(tc.Context(), tc.Server().DevAuthMethodId, "login", map[string]interface{}{"login_name": "user", "password": "passpass"}) + result, err := methods.Authenticate(tc.Context(), tc.Server().DevPasswordAuthMethodId, "login", map[string]interface{}{"login_name": "user", "password": "passpass"}) require.NoError(err) + wantToken := new(authtokens.AuthToken) + require.NoError(json.Unmarshal(result.GetRawAttributes(), wantToken)) - at, err := tokens.Read(tc.Context(), want.Item.Id) + at, err := tokens.Read(tc.Context(), wantToken.Id) require.NoError(err) - assert.EqualValues(comparableResource(want.Item), comparableResource(at.Item)) + assert.EqualValues(comparableResource(wantToken), comparableResource(at.Item)) _, err = tokens.Delete(tc.Context(), at.Item.Id) require.NoError(err) diff --git a/internal/types/action/action.go b/internal/types/action/action.go index 6ea386ce8..1869627d5 100644 --- a/internal/types/action/action.go +++ b/internal/types/action/action.go @@ -39,6 +39,7 @@ const ( RemoveAccounts Type = 30 ReadSelf Type = 31 CancelSelf Type = 32 + ChangeState Type = 33 ) var Map = map[string]Type{ @@ -74,6 +75,7 @@ var Map = map[string]Type{ RemoveAccounts.String(): RemoveAccounts, ReadSelf.String(): ReadSelf, CancelSelf.String(): CancelSelf, + ChangeState.String(): ChangeState, } func (a Type) String() string { @@ -111,6 +113,7 @@ func (a Type) String() string { "remove-accounts", "read:self", "cancel:self", + "change-state", }[a] } diff --git a/internal/types/action/action_test.go b/internal/types/action/action_test.go index 14ebe0088..52620acd8 100644 --- a/internal/types/action/action_test.go +++ b/internal/types/action/action_test.go @@ -83,6 +83,10 @@ func TestAction(t *testing.T) { action: CancelSelf, want: "cancel:self", }, + { + action: ChangeState, + want: "change-state", + }, } for _, tt := range tests { t.Run(tt.want, func(t *testing.T) { diff --git a/internal/ui/VERSION b/internal/ui/VERSION index ba5559876..fa98afd5f 100644 --- a/internal/ui/VERSION +++ b/internal/ui/VERSION @@ -1,4 +1,4 @@ -2f5ea0fcbadedc626f43acfa8f9c2503a03da4dc Merge pull request #492 from hashicorp/remove-unused-apple-entitlement +80cffeb48b1750d8e4583e72d67f4bf73fdb91d8 Merge pull request #517 from hashicorp/nat/release-test-trigger # This file determines the version of the UI to embed in the boundary binary. # Update this file by running 'make update-ui-version' from the root of this repo. # Set UI_COMMITISH when running the above target to update to a specific version. diff --git a/internal/website/permstable/permstable.go b/internal/website/permstable/permstable.go index 548dc83c0..a57834434 100644 --- a/internal/website/permstable/permstable.go +++ b/internal/website/permstable/permstable.go @@ -325,7 +325,7 @@ var account = &Resource{ rudActions("an account", true), &Action{ Name: "set-password", - Description: "Set a password on an account, without requring the current password", + Description: "Set a password on an account, without requiring the current password", Examples: []string{ "id=;actions=set-password", "id=;type=;actions=set-password", diff --git a/scripts/uigen.sh b/scripts/uigen.sh index 9ae776428..7eff8916a 100755 --- a/scripts/uigen.sh +++ b/scripts/uigen.sh @@ -12,8 +12,8 @@ fi ( cd "$UI_CLONE_DIR" - if ! docker-compose -f docker-compose-embedding.yml run build; then - yarn install && yarn build + if ! yarn install && yarn build; then + echo "Please ensure Node v14+ and Yarn v1.22.10+ are installed." fi ) diff --git a/website/content/docs/concepts/security/permissions.mdx b/website/content/docs/concepts/security/permissions.mdx index 99d435761..a62a98afc 100644 --- a/website/content/docs/concepts/security/permissions.mdx +++ b/website/content/docs/concepts/security/permissions.mdx @@ -305,7 +305,7 @@ wildcard or templated grant strings.
  • set-password: Set a password on an account, without - requring the current password + requiring the current password