diff --git a/api/credentials/json_attributes.gen.go b/api/credentials/json_attributes.gen.go new file mode 100644 index 000000000..9101419ff --- /dev/null +++ b/api/credentials/json_attributes.gen.go @@ -0,0 +1,38 @@ +// Code generated by "make api"; DO NOT EDIT. +package credentials + +import ( + "fmt" + + "github.com/mitchellh/mapstructure" +) + +type JsonAttributes struct { + Object map[string]interface{} `json:"object,omitempty"` + ObjectHmac string `json:"object_hmac,omitempty"` +} + +func AttributesMapToJsonAttributes(in map[string]interface{}) (*JsonAttributes, error) { + if in == nil { + return nil, fmt.Errorf("nil input map") + } + var out JsonAttributes + dec, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{ + Result: &out, + TagName: "json", + }) + if err != nil { + return nil, fmt.Errorf("error creating mapstructure decoder: %w", err) + } + if err := dec.Decode(in); err != nil { + return nil, fmt.Errorf("error decoding: %w", err) + } + return &out, nil +} + +func (pt *Credential) GetJsonAttributes() (*JsonAttributes, error) { + if pt.Type != "json" { + return nil, fmt.Errorf("asked to fetch %s-type attributes but credential is of type %s", "json", pt.Type) + } + return AttributesMapToJsonAttributes(pt.Attributes) +} diff --git a/api/credentials/option.gen.go b/api/credentials/option.gen.go index 2d6539eeb..d8dfdf1c8 100644 --- a/api/credentials/option.gen.go +++ b/api/credentials/option.gen.go @@ -110,6 +110,18 @@ func DefaultName() Option { } } +func WithJsonCredentialObject(inObject map[string]interface{}) Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["object"] = inObject + o.postMap["attributes"] = val + } +} + func WithUsernamePasswordCredentialPassword(inPassword string) Option { return func(o *options) { raw, ok := o.postMap["attributes"] diff --git a/internal/api/genapi/input.go b/internal/api/genapi/input.go index 23cd87d21..61e941023 100644 --- a/internal/api/genapi/input.go +++ b/internal/api/genapi/input.go @@ -549,6 +549,21 @@ var inputStructs = []*structInfo{ mapstructureConversionTemplate, }, }, + { + inProto: &credentials.JsonAttributes{}, + outFile: "credentials/json_attributes.gen.go", + subtypeName: "JsonCredential", + fieldOverrides: []fieldInfo{ + { + Name: "Object", + SkipDefault: true, + }, + }, + parentTypeName: "Credential", + templates: []*template.Template{ + mapstructureConversionTemplate, + }, + }, { inProto: &credentials.Credential{}, outFile: "credentials/credential.gen.go", diff --git a/internal/cmd/base/base.go b/internal/cmd/base/base.go index 04a6b0eca..8534b1256 100644 --- a/internal/cmd/base/base.go +++ b/internal/cmd/base/base.go @@ -123,6 +123,10 @@ type Command struct { FlagSecrets string FlagScrts []CombinedSliceFlagValue + // Object values + FlagObject string + FlagKv []CombinedSliceFlagValue + client *api.Client // This will be intialized, if needed, in Config() when instantiating a diff --git a/internal/cmd/commands.go b/internal/cmd/commands.go index 7c09b2529..3075ee6ff 100644 --- a/internal/cmd/commands.go +++ b/internal/cmd/commands.go @@ -478,6 +478,12 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) { Func: "create", }, nil }, + "credentials create json": func() (cli.Command, error) { + return &credentialscmd.JsonCommand{ + Command: base.NewCommand(ui), + Func: "create", + }, nil + }, "credentials update": func() (cli.Command, error) { return &credentialscmd.Command{ Command: base.NewCommand(ui), @@ -496,6 +502,12 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) { Func: "update", }, nil }, + "credentials update json": func() (cli.Command, error) { + return &credentialscmd.JsonCommand{ + Command: base.NewCommand(ui), + Func: "update", + }, nil + }, "groups": func() (cli.Command, error) { return &groupscmd.Command{ diff --git a/internal/cmd/commands/connect/credentials_test.go b/internal/cmd/commands/connect/credentials_test.go index 8b63433a5..91f749b86 100644 --- a/internal/cmd/commands/connect/credentials_test.go +++ b/internal/cmd/commands/connect/credentials_test.go @@ -78,6 +78,24 @@ var ( }, } + staticKv = &targets.SessionCredential{ + CredentialSource: &targets.CredentialSource{ + Type: "static", + CredentialType: "json", + CredentialStoreId: "csst_id", + Description: "test", + Name: "test unspecified json cred", + Id: "credjson_id", + }, + Secret: &targets.SessionSecret{ + Decoded: map[string]interface{}{ + "secret": map[string]interface{}{ + "username": "password", + }, + }, + }, + } + unspecifiedCred = &targets.SessionCredential{ CredentialSource: &targets.CredentialSource{ Type: "static", @@ -236,11 +254,24 @@ func Test_parseCredentials(t *testing.T) { }, wantErr: false, }, + { + name: "unspecified-static-json", + creds: []*targets.SessionCredential{ + staticKv, + }, + wantCreds: credentials{ + unspecified: []*targets.SessionCredential{ + staticKv, + }, + }, + wantErr: false, + }, { name: "mixed", creds: []*targets.SessionCredential{ staticSshPrivateKey, unspecifiedCred1, vaultSshPrivateKey, typedUsernamePassword, unspecifiedCred, vaultUsernamePassword, typedSshPrivateKey, staticUsernamePassword, + staticKv, }, wantCreds: credentials{ sshPrivateKey: []sshPrivateKey{ @@ -278,7 +309,7 @@ func Test_parseCredentials(t *testing.T) { }, }, unspecified: []*targets.SessionCredential{ - unspecifiedCred, unspecifiedCred1, + unspecifiedCred, unspecifiedCred1, staticKv, }, }, wantErr: false, diff --git a/internal/cmd/commands/credentialscmd/funcs.go b/internal/cmd/commands/credentialscmd/funcs.go index e7794a455..65856a328 100644 --- a/internal/cmd/commands/credentialscmd/funcs.go +++ b/internal/cmd/commands/credentialscmd/funcs.go @@ -14,6 +14,7 @@ const ( passwordFlagName = "password" privateKeyFlagName = "private-key" privateKeyPassphraseFlagName = "private-key-passphrase" + secretFlagName = "secret" ) func (c *Command) extraHelpFunc(helpMap map[string]func() string) string { diff --git a/internal/cmd/commands/credentialscmd/json_credentials.gen.go b/internal/cmd/commands/credentialscmd/json_credentials.gen.go new file mode 100644 index 000000000..2f01e8f4f --- /dev/null +++ b/internal/cmd/commands/credentialscmd/json_credentials.gen.go @@ -0,0 +1,294 @@ +// Code generated by "make cli"; DO NOT EDIT. +package credentialscmd + +import ( + "errors" + "fmt" + + "github.com/hashicorp/boundary/api" + "github.com/hashicorp/boundary/api/credentials" + "github.com/hashicorp/boundary/internal/cmd/base" + "github.com/hashicorp/boundary/internal/cmd/common" + "github.com/hashicorp/go-secure-stdlib/strutil" + "github.com/mitchellh/cli" + "github.com/posener/complete" +) + +func initJsonFlags() { + flagsOnce.Do(func() { + extraFlags := extraJsonActionsFlagsMapFunc() + for k, v := range extraFlags { + flagsJsonMap[k] = append(flagsJsonMap[k], v...) + } + }) +} + +var ( + _ cli.Command = (*JsonCommand)(nil) + _ cli.CommandAutocomplete = (*JsonCommand)(nil) +) + +type JsonCommand struct { + *base.Command + + Func string + + plural string +} + +func (c *JsonCommand) AutocompleteArgs() complete.Predictor { + initJsonFlags() + return complete.PredictAnything +} + +func (c *JsonCommand) AutocompleteFlags() complete.Flags { + initJsonFlags() + return c.Flags().Completions() +} + +func (c *JsonCommand) Synopsis() string { + if extra := extraJsonSynopsisFunc(c); extra != "" { + return extra + } + + synopsisStr := "credential" + + synopsisStr = fmt.Sprintf("%s %s", "json-type", synopsisStr) + + return common.SynopsisFunc(c.Func, synopsisStr) +} + +func (c *JsonCommand) Help() string { + initJsonFlags() + + var helpStr string + helpMap := common.HelpMap("credential") + + switch c.Func { + + default: + + helpStr = c.extraJsonHelpFunc(helpMap) + + } + + // Keep linter from complaining if we don't actually generate code using it + _ = helpMap + return helpStr +} + +var flagsJsonMap = map[string][]string{ + + "create": {"credential-store-id", "name", "description", "object", "kv", "string-kv", "bool-kv", "num-kv"}, + + "update": {"id", "name", "description", "version", "object", "kv", "string-kv", "bool-kv", "num-kv"}, +} + +func (c *JsonCommand) Flags() *base.FlagSets { + if len(flagsJsonMap[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, "json-type credential", flagsJsonMap, c.Func) + + f = set.NewFlagSet("Object Options") + common.PopulateObjectFlags(c.Command, f, flagsJsonMap, c.Func) + + extraJsonFlagsFunc(c, set, f) + + return set +} + +func (c *JsonCommand) Run(args []string) int { + initJsonFlags() + + switch c.Func { + case "": + return cli.RunResultHelp + + } + + c.plural = "json-type credential" + switch c.Func { + case "list": + c.plural = "json-type credentials" + } + + f := c.Flags() + + if err := f.Parse(args); err != nil { + c.PrintCliError(err) + return base.CommandUserError + } + + if strutil.StrListContains(flagsJsonMap[c.Func], "id") && c.FlagId == "" { + c.PrintCliError(errors.New("ID is required but not passed in via -id")) + return base.CommandUserError + } + + if c.FlagObject != "" && len(c.FlagKv) > 0 { + c.PrintCliError(errors.New("-object flag cannot be used along with the following flags: kv, bool-kv, num-kv, string-kv")) + return base.CommandUserError + } + + var opts []credentials.Option + + if strutil.StrListContains(flagsJsonMap[c.Func], "credential-store-id") { + switch c.Func { + + case "create": + if c.FlagCredentialStoreId == "" { + c.PrintCliError(errors.New("CredentialStore ID must be passed in via -credential-store-id or BOUNDARY_CREDENTIAL_STORE_ID")) + return base.CommandUserError + } + + } + } + + client, err := c.Client() + if c.WrapperCleanupFunc != nil { + defer func() { + if err := c.WrapperCleanupFunc(); err != nil { + c.PrintCliError(fmt.Errorf("Error cleaning kms wrapper: %w", err)) + } + }() + } + if err != nil { + c.PrintCliError(fmt.Errorf("Error creating API client: %w", err)) + return base.CommandCliError + } + credentialsClient := credentials.NewClient(client) + + switch c.FlagName { + case "": + case "null": + opts = append(opts, credentials.DefaultName()) + default: + opts = append(opts, credentials.WithName(c.FlagName)) + } + + switch c.FlagDescription { + case "": + case "null": + opts = append(opts, credentials.DefaultDescription()) + default: + opts = append(opts, credentials.WithDescription(c.FlagDescription)) + } + + if c.FlagFilter != "" { + opts = append(opts, credentials.WithFilter(c.FlagFilter)) + } + + var version uint32 + + switch c.Func { + + case "update": + switch c.FlagVersion { + case 0: + opts = append(opts, credentials.WithAutomaticVersioning(true)) + default: + version = uint32(c.FlagVersion) + } + + } + + if err := common.HandleAttributeFlags( + c.Command, + "kv", + c.FlagObject, + c.FlagKv, + func() {}, + func(in map[string]interface{}) { + opts = append(opts, credentials.WithJsonCredentialObject(in)) + }); err != nil { + c.PrintCliError(fmt.Errorf("Error evaluating kv flags to: %s", err.Error())) + return base.CommandCliError + } + + if ok := extraJsonFlagsHandlingFunc(c, f, &opts); !ok { + return base.CommandUserError + } + + var resp *api.Response + var item *credentials.Credential + + var createResult *credentials.CredentialCreateResult + + var updateResult *credentials.CredentialUpdateResult + + switch c.Func { + + case "create": + createResult, err = credentialsClient.Create(c.Context, "json", c.FlagCredentialStoreId, opts...) + if exitCode := c.checkFuncError(err); exitCode > 0 { + return exitCode + } + resp = createResult.GetResponse() + item = createResult.GetItem() + + case "update": + updateResult, err = credentialsClient.Update(c.Context, c.FlagId, version, opts...) + if exitCode := c.checkFuncError(err); exitCode > 0 { + return exitCode + } + resp = updateResult.GetResponse() + item = updateResult.GetItem() + + } + + resp, item, err = executeExtraJsonActions(c, resp, item, err, credentialsClient, version, opts) + if exitCode := c.checkFuncError(err); exitCode > 0 { + return exitCode + } + + output, err := printCustomJsonActionOutput(c) + if err != nil { + c.PrintCliError(err) + return base.CommandUserError + } + if output { + return base.CommandSuccess + } + + switch c.Func { + + } + + switch base.Format(c.UI) { + case "table": + c.UI.Output(printItemTable(item, resp)) + + case "json": + if ok := c.PrintJsonItem(resp); !ok { + return base.CommandCliError + } + } + + return base.CommandSuccess +} + +func (c *JsonCommand) checkFuncError(err error) int { + if err == nil { + return 0 + } + 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 +} + +var ( + extraJsonActionsFlagsMapFunc = func() map[string][]string { return nil } + extraJsonSynopsisFunc = func(*JsonCommand) string { return "" } + extraJsonFlagsFunc = func(*JsonCommand, *base.FlagSets, *base.FlagSet) {} + extraJsonFlagsHandlingFunc = func(*JsonCommand, *base.FlagSets, *[]credentials.Option) bool { return true } + executeExtraJsonActions = func(_ *JsonCommand, inResp *api.Response, inItem *credentials.Credential, inErr error, _ *credentials.Client, _ uint32, _ []credentials.Option) (*api.Response, *credentials.Credential, error) { + return inResp, inItem, inErr + } + printCustomJsonActionOutput = func(*JsonCommand) (bool, error) { return false, nil } +) diff --git a/internal/cmd/commands/credentialscmd/json_credentials_funcs.go b/internal/cmd/commands/credentialscmd/json_credentials_funcs.go new file mode 100644 index 000000000..ee46b411e --- /dev/null +++ b/internal/cmd/commands/credentialscmd/json_credentials_funcs.go @@ -0,0 +1,33 @@ +package credentialscmd + +import ( + "github.com/hashicorp/boundary/internal/cmd/base" +) + +func (c *JsonCommand) extraJsonHelpFunc(_ map[string]func() string) string { + var helpStr string + switch c.Func { + case "create": + helpStr = base.WrapForHelpText([]string{ + "Usage: boundary credentials create json -credential-store-id [options] [args]", + "", + " Create a json credential. Example:", + "", + ` $ boundary credentials create json -credential-store-id csst_1234567890 -object file:///home/user/secret`, + "", + "", + }) + + case "update": + helpStr = base.WrapForHelpText([]string{ + "Usage: boundary credentials update json [options] [args]", + "", + " Update a json credential given its ID. Example:", + "", + ` $ boundary credentials update json -id csst_1234567890 -name devops -description "For DevOps usage"`, + "", + "", + }) + } + return helpStr + c.Flags().Help() +} diff --git a/internal/cmd/commands/hostcatalogscmd/plugin_hostcatalogs.gen.go b/internal/cmd/commands/hostcatalogscmd/plugin_hostcatalogs.gen.go index c99c05c44..76b7aa340 100644 --- a/internal/cmd/commands/hostcatalogscmd/plugin_hostcatalogs.gen.go +++ b/internal/cmd/commands/hostcatalogscmd/plugin_hostcatalogs.gen.go @@ -131,6 +131,16 @@ func (c *PluginCommand) Run(args []string) int { return base.CommandUserError } + if c.FlagAttributes != "" && len(c.FlagAttrs) > 0 { + c.PrintCliError(errors.New("-attributes flag cannot be used along with the following flags: attr, bool-attr, num-attr, string-attr")) + return base.CommandUserError + } + + if c.FlagSecrets != "" && len(c.FlagScrts) > 0 { + c.PrintCliError(errors.New("-secrets flag cannot be used along with the following flags: secret, bool-secret, num-secret, string-secret")) + return base.CommandUserError + } + var opts []hostcatalogs.Option if strutil.StrListContains(flagsPluginMap[c.Func], "scope-id") { diff --git a/internal/cmd/commands/hostsetscmd/plugin_hostsets.gen.go b/internal/cmd/commands/hostsetscmd/plugin_hostsets.gen.go index 0eeac1ba4..39792dcd7 100644 --- a/internal/cmd/commands/hostsetscmd/plugin_hostsets.gen.go +++ b/internal/cmd/commands/hostsetscmd/plugin_hostsets.gen.go @@ -130,6 +130,11 @@ func (c *PluginCommand) Run(args []string) int { return base.CommandUserError } + if c.FlagAttributes != "" && len(c.FlagAttrs) > 0 { + c.PrintCliError(errors.New("-attributes flag cannot be used along with the following flags: attr, bool-attr, num-attr, string-attr")) + return base.CommandUserError + } + var opts []hostsets.Option if strutil.StrListContains(flagsPluginMap[c.Func], "host-catalog-id") { diff --git a/internal/cmd/common/flags.go b/internal/cmd/common/flags.go index 1159eaafe..2f0420e1a 100644 --- a/internal/cmd/common/flags.go +++ b/internal/cmd/common/flags.go @@ -124,7 +124,7 @@ func PopulateAttributeFlags(c *base.Command, f *base.FlagSet, flagNames map[stri f.StringVar(&base.StringVar{ Name: "attributes", Target: &c.FlagAttributes, - Usage: `A JSON map value to use as the entirety of the request's attributes map. Usually this will be sourced from a file via "file://" syntax.`, + Usage: `A JSON map value to use as the entirety of the request's attributes map. Usually this will be sourced from a file via "file://" syntax. Is exclusive with the other attr flags.`, }) case "attr": f.CombinationSliceVar(&base.CombinationSliceVar{ @@ -174,7 +174,7 @@ func PopulateSecretFlags(c *base.Command, f *base.FlagSet, flagNames map[string] f.StringVar(&base.StringVar{ Name: "secrets", Target: &c.FlagSecrets, - Usage: `A JSON map value to use as the entirety of the request's secrets map. Usually this will be sourced from a file via "file://" syntax.`, + Usage: `A JSON map value to use as the entirety of the request's secrets map. Usually this will be sourced from a file via "file://" syntax. Is exclusive with the other secret flags.`, }) case "secret": f.CombinationSliceVar(&base.CombinationSliceVar{ @@ -216,6 +216,56 @@ func PopulateSecretFlags(c *base.Command, f *base.FlagSet, flagNames map[string] } } +func PopulateObjectFlags(c *base.Command, f *base.FlagSet, flagNames map[string][]string, command string) { + keyDelimiter := "." + for _, name := range flagNames[command] { + switch name { + case "object": + f.StringVar(&base.StringVar{ + Name: "object", + Target: &c.FlagObject, + Usage: `A JSON map value to use as the entirety of the request's object map. Usually this will be sourced from a file via "file://" syntax. Is exclusive with the other kv flags.`, + }) + case "kv": + f.CombinationSliceVar(&base.CombinationSliceVar{ + Name: "kv", + Target: &c.FlagKv, + KvSplit: true, + KeyDelimiter: &keyDelimiter, + ProtoCompatKey: true, + Usage: `A key=value pair to add to the request's object map. The type is automatically inferred. Use -string-kv, -bool-kv, or -num-kv if the type needs to be overridden. Can be specified multiple times. Supports sourcing values from files via "file://" and env vars via "env://"`, + }) + case "string-kv": + f.CombinationSliceVar(&base.CombinationSliceVar{ + Name: "string-kv", + Target: &c.FlagKv, + KvSplit: true, + KeyDelimiter: &keyDelimiter, + ProtoCompatKey: true, + Usage: `A key=value string value to add to the request's object map. Can be specified multiple times. Supports sourcing values from files via "file://" and env vars via "env://"`, + }) + case "bool-kv": + f.CombinationSliceVar(&base.CombinationSliceVar{ + Name: "bool-kv", + Target: &c.FlagKv, + KvSplit: true, + KeyDelimiter: &keyDelimiter, + ProtoCompatKey: true, + Usage: `A key=value bool value to add to the request's object map. Can be specified multiple times. Supports sourcing values from files via "file://" and env vars via "env://"`, + }) + case "num-kv": + f.CombinationSliceVar(&base.CombinationSliceVar{ + Name: "num-kv", + Target: &c.FlagKv, + KvSplit: true, + KeyDelimiter: &keyDelimiter, + ProtoCompatKey: true, + Usage: `A key=value numeric value to add to the request's object map. Can be specified multiple times. Supports sourcing values from files via "file://" and env vars via "env://"`, + }) + } + } +} + // From https://stackoverflow.com/a/13340826, modified to remove exponents var jsonNumberRegex = regexp.MustCompile(`^-?(?:0|[1-9]\d*)(?:\.\d+)?$`) diff --git a/internal/cmd/gencli/input.go b/internal/cmd/gencli/input.go index 975eded4c..00adaddef 100644 --- a/internal/cmd/gencli/input.go +++ b/internal/cmd/gencli/input.go @@ -73,6 +73,10 @@ type cmdInfo struct { // -secret, -string-secret, etc. HasGenericSecrets bool + // HasJsonObject controls whether to generate flags for -object, + // -kv, -string-kv, -bool-kv, -num-kv. + HasJsonObject bool + // IsPluginType controls whether standard plugin flags are generated IsPluginType bool @@ -279,6 +283,22 @@ var inputStructs = map[string][]*cmdInfo{ NeedsSubtypeInCreate: true, PrefixAttributeFieldErrorsWithSubactionPrefix: true, }, + { + ResourceType: resource.Credential.String(), + Pkg: "credentials", + StdActions: []string{"create", "update"}, + SubActionPrefix: "json", + SkipNormalHelp: true, + HasExtraHelpFunc: true, + HasId: true, + HasName: true, + HasDescription: true, + Container: "CredentialStore", + VersionedActions: []string{"update"}, + NeedsSubtypeInCreate: true, + PrefixAttributeFieldErrorsWithSubactionPrefix: true, + HasJsonObject: true, + }, }, "groups": { { diff --git a/internal/cmd/gencli/templates.go b/internal/cmd/gencli/templates.go index b2b7cd6fc..f843e002b 100644 --- a/internal/cmd/gencli/templates.go +++ b/internal/cmd/gencli/templates.go @@ -171,15 +171,16 @@ func (c *{{ camelCase .SubActionPrefix }}Command) Help() string { var flags{{ camelCase .SubActionPrefix }}Map = map[string][]string{ {{ with $attrFlags := ", \"attributes\", \"attr\", \"string-attr\", \"bool-attr\", \"num-attr\"" }} {{ with $secretFlags := ", \"secrets\", \"secret\", \"string-secret\", \"bool-secret\", \"num-secret\"" }} + {{ with $objectFlags := ", \"object\", \"kv\", \"string-kv\", \"bool-kv\", \"num-kv\"" }} {{ range $i, $action := $input.StdActions }} {{ if eq $action "create" }} - "create": { "{{ kebabCase $input.Container }}-id", "name", "description" {{ if $input.IsPluginType }} , "plugin-id", "plugin-name" {{ end }} {{ if $input.HasGenericAttributes }} {{ $attrFlags }} {{ end }} {{ if $input.HasGenericSecrets }} {{ $secretFlags }} {{ end }} }, + "create": { "{{ kebabCase $input.Container }}-id", "name", "description" {{ if $input.IsPluginType }} , "plugin-id", "plugin-name" {{ end }} {{ if $input.HasGenericAttributes }} {{ $attrFlags }} {{ end }} {{ if $input.HasGenericSecrets }} {{ $secretFlags }} {{ end }} {{ if $input.HasJsonObject }} {{ $objectFlags }} {{ end }} }, {{ end }} {{ if eq $action "read" }} "read": {"id"}, {{ end }} {{ if eq $action "update" }} - "update": {"id", "name", "description" {{ if hasAction $input.VersionedActions "update" }}, "version" {{ end }} {{ if $input.HasGenericAttributes }} {{ $attrFlags }} {{ end }} {{ if $input.HasGenericSecrets }} {{ $secretFlags }} {{ end }} }, + "update": {"id", "name", "description" {{ if hasAction $input.VersionedActions "update" }}, "version" {{ end }} {{ if $input.HasGenericAttributes }} {{ $attrFlags }} {{ end }} {{ if $input.HasGenericSecrets }} {{ $secretFlags }} {{ end }} {{ if $input.HasJsonObject }} {{ $objectFlags }} {{ end }} }, {{ end }} {{ if eq $action "delete" }} "delete": {"id"}, @@ -190,6 +191,7 @@ var flags{{ camelCase .SubActionPrefix }}Map = map[string][]string{ {{ end }} {{ end }} {{ end }} + {{ end }} } func (c *{{ camelCase .SubActionPrefix }}Command) Flags() *base.FlagSets { @@ -211,6 +213,11 @@ func (c *{{ camelCase .SubActionPrefix }}Command) Flags() *base.FlagSets { common.PopulateSecretFlags(c.Command, f, flags{{ camelCase .SubActionPrefix }}Map, c.Func) {{ end }} + {{ if .HasJsonObject }} + f = set.NewFlagSet("Object Options") + common.PopulateObjectFlags(c.Command, f, flags{{ camelCase .SubActionPrefix }}Map, c.Func) + {{ end }} + extra{{ camelCase .SubActionPrefix }}FlagsFunc(c, set, f) return set @@ -259,6 +266,27 @@ func (c *{{ camelCase .SubActionPrefix }}Command) Run(args []string) int { } {{ end }} + {{ if .HasGenericAttributes }} + if c.FlagAttributes != "" && len(c.FlagAttrs) > 0 { + c.PrintCliError(errors.New("-attributes flag cannot be used along with the following flags: attr, bool-attr, num-attr, string-attr")) + return base.CommandUserError + } + {{ end }} + + {{ if .HasGenericSecrets }} + if c.FlagSecrets != "" && len(c.FlagScrts) > 0 { + c.PrintCliError(errors.New("-secrets flag cannot be used along with the following flags: secret, bool-secret, num-secret, string-secret")) + return base.CommandUserError + } + {{ end }} + + {{ if .HasJsonObject }} + if c.FlagObject != "" && len(c.FlagKv) > 0 { + c.PrintCliError(errors.New("-object flag cannot be used along with the following flags: kv, bool-kv, num-kv, string-kv")) + return base.CommandUserError + } + {{ end }} + var opts []{{ .Pkg }}.Option if strutil.StrListContains(flags{{ camelCase .SubActionPrefix }}Map[c.Func], "{{ kebabCase .Container }}-id") { @@ -395,6 +423,21 @@ func (c *{{ camelCase .SubActionPrefix }}Command) Run(args []string) int { } {{ end }} + {{ if .HasJsonObject }} + if err := common.HandleAttributeFlags( + c.Command, + "kv", + c.FlagObject, + c.FlagKv, + func() {}, + func(in map[string]interface{}) { + opts = append(opts, {{ .Pkg }}.WithJsonCredentialObject(in)) + }); err != nil { + c.PrintCliError(fmt.Errorf("Error evaluating kv flags to: %s", err.Error())) + return base.CommandCliError + } + {{ end }} + if ok := extra{{ camelCase .SubActionPrefix }}FlagsHandlingFunc(c, f, &opts, ); !ok { return base.CommandUserError } diff --git a/internal/credential/credential.go b/internal/credential/credential.go index 27e6cc843..c29710b62 100644 --- a/internal/credential/credential.go +++ b/internal/credential/credential.go @@ -6,6 +6,7 @@ import ( "context" "github.com/hashicorp/boundary/internal/boundary" + "google.golang.org/protobuf/types/known/structpb" ) // Domain defines the domain for the credential package. @@ -29,6 +30,7 @@ const ( UnspecifiedType Type = "unspecified" UsernamePasswordType Type = "username_password" SshPrivateKeyType Type = "ssh_private_key" + JsonType Type = "json" ) // A Library is a resource that provides credentials that are of the same @@ -119,6 +121,11 @@ type Password string // PrivateKey represents a secret private key. type PrivateKey []byte +// JsonObject represents a JSON object that is serialized. +type JsonObject struct { + structpb.Struct +} + // UsernamePassword is a credential containing a username and a password. type UsernamePassword interface { Credential diff --git a/internal/credential/public_ids.go b/internal/credential/public_ids.go index f3d37f1a6..eb7b853ae 100644 --- a/internal/credential/public_ids.go +++ b/internal/credential/public_ids.go @@ -15,6 +15,9 @@ func init() { if err := subtypes.Register(Domain, SshPrivateKeySubtype, SshPrivateKeyCredentialPrefix); err != nil { panic(err) } + if err := subtypes.Register(Domain, JsonSubtype, JsonCredentialPrefix); err != nil { + panic(err) + } } const ( @@ -24,6 +27,9 @@ const ( SshPrivateKeyCredentialPrefix = "credspk" SshPrivateKeySubtype = subtypes.Subtype("ssh_private_key") + + JsonCredentialPrefix = "credjson" + JsonSubtype = subtypes.Subtype("json") ) func NewUsernamePasswordCredentialId(ctx context.Context) (string, error) { @@ -41,3 +47,11 @@ func NewSshPrivateKeyCredentialId(ctx context.Context) (string, error) { } return id, nil } + +func NewJsonCredentialId(ctx context.Context) (string, error) { + id, err := db.NewPublicId(JsonCredentialPrefix) + if err != nil { + return "", errors.Wrap(ctx, err, "credential.NewJsonCredentialId") + } + return id, nil +} diff --git a/internal/credential/redact.go b/internal/credential/redact.go index 476a05a4a..2a41b130c 100644 --- a/internal/credential/redact.go +++ b/internal/credential/redact.go @@ -5,6 +5,7 @@ import "encoding/json" const ( redactedPassword = "[REDACTED: password]" redactedPrivateKey = "[REDACTED: private key]" + redactedJson = "[REDACTED: json]" ) // String returns a string with the password redacted. @@ -37,3 +38,18 @@ func (s PrivateKey) GoString() string { func (s PrivateKey) MarshalJSON() ([]byte, error) { return json.Marshal([]byte(redactedPrivateKey)) } + +// String returns a string with the json secret redacted. +func (s *JsonObject) String() string { + return redactedJson +} + +// GoString returns a string with the json secret redacted. +func (s *JsonObject) GoString() string { + return redactedJson +} + +// MarshalJSON returns a JSON-encoded byte slice with the json redacted. +func (s *JsonObject) MarshalJSON() ([]byte, error) { + return json.Marshal([]byte(redactedJson)) +} diff --git a/internal/credential/redact_test.go b/internal/credential/redact_test.go index 0afb0759b..be2421877 100644 --- a/internal/credential/redact_test.go +++ b/internal/credential/redact_test.go @@ -7,6 +7,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/structpb" ) func TestPassword_String(t *testing.T) { @@ -130,3 +131,57 @@ func TestPrivateKey_MarshalJSON(t *testing.T) { assert.Equal(testB, sec.B) }) } + +func TestJsonObject_String(t *testing.T) { + t.Parallel() + t.Run("redacted", func(t *testing.T) { + assert := assert.New(t) + const want = redactedJson + json := &JsonObject{} + assert.Equalf(want, json.String(), "JsonObject.String() = %v, want %v", json.String(), want) + + // Verify stringer is called + s := fmt.Sprintf("%s", json) + assert.Equalf(want, s, "JsonObject.String() = %v, want %v", s, want) + }) +} + +func TestJsonObject_GoString(t *testing.T) { + t.Parallel() + t.Run("redacted", func(t *testing.T) { + assert := assert.New(t) + const want = redactedJson + json := &JsonObject{} + assert.Equalf(want, json.GoString(), "JsonObject.GoString() = %v, want %v", json.GoString(), want) + + // Verify gostringer is called + s := fmt.Sprintf("%#v", json) + assert.Equalf(want, s, "JsonObject.GoString() = %v, want %v", s, want) + }) +} + +func TestJsonObject_MarshalJSON(t *testing.T) { + t.Parallel() + t.Run("redacted", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + want, err := json.Marshal([]byte(redactedJson)) + require.NoError(err) + json := &JsonObject{} + got, err := json.MarshalJSON() + require.NoError(err) + assert.Equalf(want, got, "JsonObject.MarshalJSON() = %s, want %s", got, want) + }) + t.Run("within-struct", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + secret := JsonObject{ + structpb.Struct{ + Fields: map[string]*structpb.Value{ + "secret": structpb.NewStringValue("password"), + }, + }, + } + bSecret, err := secret.MarshalJSON() + require.NoError(err) + assert.Equal("\"W1JFREFDVEVEOiBqc29uXQ==\"", string(bSecret)) // The marshaled value is in base64 + }) +} diff --git a/internal/credential/static/fields.go b/internal/credential/static/fields.go index 1f832cfae..0f0fd2a42 100644 --- a/internal/credential/static/fields.go +++ b/internal/credential/static/fields.go @@ -8,4 +8,5 @@ const ( passwordField = "Password" privateKeyField = "PrivateKey" PrivateKeyPassphraseField = "PrivateKeyPassphrase" + objectField = "Object" ) diff --git a/internal/credential/static/json_credential.go b/internal/credential/static/json_credential.go new file mode 100644 index 000000000..2f2bb11f6 --- /dev/null +++ b/internal/credential/static/json_credential.go @@ -0,0 +1,138 @@ +package static + +import ( + "context" + "encoding/json" + + "github.com/hashicorp/boundary/internal/credential" + "github.com/hashicorp/boundary/internal/credential/static/store" + "github.com/hashicorp/boundary/internal/errors" + "github.com/hashicorp/boundary/internal/libs/crypto" + "github.com/hashicorp/boundary/internal/oplog" + wrapping "github.com/hashicorp/go-kms-wrapping/v2" + "github.com/hashicorp/go-kms-wrapping/v2/extras/structwrapping" + "google.golang.org/protobuf/proto" +) + +var _ credential.Static = (*JsonCredential)(nil) + +// A JsonCredential contains the credential with a json secret. +// It is owned by a credential store. +type JsonCredential struct { + *store.JsonCredential + tableName string `gorm:"-"` +} + +// NewJsonCredential creates a new in memory static Credential containing a +// json secret that is assigned to storeId. Name and description are the only +// valid options. All other options are ignored. +func NewJsonCredential( + ctx context.Context, + storeId string, + object credential.JsonObject, + opt ...Option, +) (*JsonCredential, error) { + const op = "static.NewJsonCredential" + var objectB []byte + var err error + + // Since the secret is an unordered map of dynamically typed values, the hmac value will not be consistent. + // In order to calculate a consistent hmac value, the input must be deterministic, + // which is done by marshalling the secret. + if len(object.AsMap()) > 0 { + objectB, err = json.Marshal(object.AsMap()) + if err != nil { + return nil, errors.New(ctx, errors.InvalidParameter, op, "invalid secret") + } + } + + opts := getOpts(opt...) + jsonCred := &JsonCredential{ + JsonCredential: &store.JsonCredential{ + StoreId: storeId, + Name: opts.withName, + Description: opts.withDescription, + Object: objectB, + }, + } + + return jsonCred, nil +} + +func allocJsonCredential() *JsonCredential { + return &JsonCredential{ + JsonCredential: &store.JsonCredential{}, + } +} + +func (c *JsonCredential) clone() *JsonCredential { + cp := proto.Clone(c.JsonCredential) + return &JsonCredential{ + JsonCredential: cp.(*store.JsonCredential), + } +} + +// TableName returns the table name. +func (c *JsonCredential) TableName() string { + if c.tableName != "" { + return c.tableName + } + return "credential_static_json_credential" +} + +// SetTableName sets the table name. +func (c *JsonCredential) SetTableName(n string) { + c.tableName = n +} + +func (c *JsonCredential) oplog(op oplog.OpType) oplog.Metadata { + metadata := oplog.Metadata{ + "resource-public-id": []string{c.PublicId}, + "resource-type": []string{"credential-static-json"}, + "op-type": []string{op.String()}, + } + if c.StoreId != "" { + metadata["store-id"] = []string{c.StoreId} + } + return metadata +} + +func (c *JsonCredential) encrypt(ctx context.Context, cipher wrapping.Wrapper) error { + const op = "static.(JsonCredential).encrypt" + if len(c.Object) == 0 { + return errors.New(ctx, errors.InvalidParameter, op, "no object defined") + } + if err := structwrapping.WrapStruct(ctx, cipher, c.JsonCredential, nil); err != nil { + return errors.Wrap(ctx, err, op, errors.WithCode(errors.Encrypt)) + } + keyId, err := cipher.KeyId(ctx) + if err != nil { + return errors.Wrap(ctx, err, op, errors.WithCode(errors.Encrypt), errors.WithMsg("error reading cipher key id")) + } + c.KeyId = keyId + if err := c.hmacObject(ctx, cipher); err != nil { + return errors.Wrap(ctx, err, op) + } + return nil +} + +func (c *JsonCredential) decrypt(ctx context.Context, cipher wrapping.Wrapper) error { + const op = "static.(JsonCredential).decrypt" + if err := structwrapping.UnwrapStruct(ctx, cipher, c.JsonCredential, nil); err != nil { + return errors.Wrap(ctx, err, op, errors.WithCode(errors.Decrypt)) + } + return nil +} + +func (c *JsonCredential) hmacObject(ctx context.Context, cipher wrapping.Wrapper) error { + const op = "static.(JsonCredential).hmacObject" + if cipher == nil { + return errors.New(ctx, errors.InvalidParameter, op, "missing cipher") + } + hm, err := crypto.HmacSha256(ctx, c.Object, cipher, []byte(c.StoreId), nil) + if err != nil { + return errors.Wrap(ctx, err, op) + } + c.ObjectHmac = []byte(hm) + return nil +} diff --git a/internal/credential/static/json_credential_test.go b/internal/credential/static/json_credential_test.go new file mode 100644 index 000000000..cc72ba9ae --- /dev/null +++ b/internal/credential/static/json_credential_test.go @@ -0,0 +1,162 @@ +package static + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/boundary/internal/credential" + "github.com/hashicorp/boundary/internal/credential/static/store" + "github.com/hashicorp/boundary/internal/db" + "github.com/hashicorp/boundary/internal/iam" + "github.com/hashicorp/boundary/internal/kms" + "github.com/hashicorp/boundary/internal/libs/crypto" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/testing/protocmp" +) + +func TestJsonCredential_New(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + kkms := kms.TestKms(t, conn, wrapper) + rw := db.New(conn) + + obj, objBytes, err := TestJsonObject() + assert.NoError(t, err) + + _, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + cs := TestCredentialStore(t, conn, wrapper, prj.PublicId) + + type args struct { + object credential.JsonObject + storeId string + options []Option + } + + tests := []struct { + name string + args args + want *JsonCredential + wantCreateErr bool + wantEncryptErr bool + wantAllocError bool + }{ + { + name: "missing-store-id", + args: args{ + object: obj, + }, + want: allocJsonCredential(), + wantCreateErr: true, + }, + { + name: "valid-no-options", + args: args{ + object: obj, + storeId: cs.PublicId, + }, + want: &JsonCredential{ + JsonCredential: &store.JsonCredential{ + Object: objBytes, + StoreId: cs.PublicId, + }, + }, + }, + { + name: "valid-with-name", + args: args{ + object: obj, + storeId: cs.PublicId, + options: []Option{WithName("json-credential")}, + }, + want: &JsonCredential{ + JsonCredential: &store.JsonCredential{ + Object: objBytes, + StoreId: cs.PublicId, + Name: "json-credential", + }, + }, + }, + { + name: "valid-with-description", + args: args{ + object: obj, + storeId: cs.PublicId, + options: []Option{WithDescription("key-value secrets")}, + }, + want: &JsonCredential{ + JsonCredential: &store.JsonCredential{ + Object: objBytes, + StoreId: cs.PublicId, + Description: "key-value secrets", + }, + }, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + ctx := context.Background() + + got, err := NewJsonCredential(ctx, tt.args.storeId, tt.args.object, tt.args.options...) + if tt.wantAllocError { + assert.Error(err) + return + } + require.NoError(err) + require.NotNil(got) + assert.Emptyf(got.PublicId, "PublicId set") + + id, err := credential.NewJsonCredentialId(ctx) + require.NoError(err) + + tt.want.PublicId = id + got.PublicId = id + + databaseWrapper, err := kkms.GetWrapper(context.Background(), prj.PublicId, kms.KeyPurposeDatabase) + require.NoError(err) + + err = got.encrypt(ctx, databaseWrapper) + if tt.wantEncryptErr { + require.Error(err) + return + } + assert.NoError(err) + + err = rw.Create(context.Background(), got) + if tt.wantCreateErr { + require.Error(err) + return + } + assert.NoError(err) + + got2 := allocJsonCredential() + got2.PublicId = id + assert.Equal(id, got2.GetPublicId()) + require.NoError(rw.LookupById(ctx, got2)) + + err = got2.decrypt(ctx, databaseWrapper) + require.NoError(err) + + // Timestamps and version are automatically set + tt.want.CreateTime = got2.CreateTime + tt.want.UpdateTime = got2.UpdateTime + tt.want.Version = got2.Version + + // KeyId is allocated via kms no need to validate in this test + tt.want.KeyId = got2.KeyId + got2.ObjectEncrypted = nil + + // encrypt also calculates the hmac, validate it is correct + hm, err := crypto.HmacSha256(ctx, got.Object, databaseWrapper, []byte(got.StoreId), nil) + require.NoError(err) + tt.want.ObjectHmac = []byte(hm) + + assert.Empty(cmp.Diff(tt.want, got2.clone(), protocmp.Transform())) + }) + } +} diff --git a/internal/credential/static/repository_credential.go b/internal/credential/static/repository_credential.go index 74e0bebca..2c423e5b8 100644 --- a/internal/credential/static/repository_credential.go +++ b/internal/credential/static/repository_credential.go @@ -192,6 +192,89 @@ func (r *Repository) CreateSshPrivateKeyCredential( return newCred, nil } +// CreateJsonCredential inserts c into the repository and returns a new +// JsonCredential containing the credential's PublicId. c is not +// changed. c must not contain a PublicId. The PublicId is generated and +// assigned by this method. c must contain a valid StoreId. +// +// The object is encrypted and a HmacSha256 of the object is +// calculated. Only the ObjectHmac is returned, the plain-text and encrypted +// object is not returned. +// +// Both c.Name and c.Description are optional. If c.Name is set, it must be +// unique within c.ProjectId. Both c.CreateTime and c.UpdateTime are ignored. +func (r *Repository) CreateJsonCredential( + ctx context.Context, + projectId string, + c *JsonCredential, + _ ...Option, +) (*JsonCredential, error) { + const op = "static.(Repository).CreateJsonCredential" + if c == nil { + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing credential") + } + if c.JsonCredential == nil { + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing embedded credential") + } + if projectId == "" { + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing project id") + } + if c.Object == nil { + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing object") + } + if c.StoreId == "" { + return nil, errors.New(ctx, errors.InvalidParameter, op, "missing store id") + } + if c.PublicId != "" { + return nil, errors.New(ctx, errors.InvalidParameter, op, "public id not empty") + } + + c = c.clone() + id, err := credential.NewJsonCredentialId(ctx) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + c.PublicId = id + oplogWrapper, err := r.kms.GetWrapper(ctx, projectId, kms.KeyPurposeOplog) + if err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get oplog wrapper")) + } + + // encrypt + databaseWrapper, err := r.kms.GetWrapper(ctx, projectId, kms.KeyPurposeDatabase) + if err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get database wrapper")) + } + if err := c.encrypt(ctx, databaseWrapper); err != nil { + return nil, errors.Wrap(ctx, err, op) + } + + var newCred *JsonCredential + _, err = r.writer.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, + func(_ db.Reader, w db.Writer) error { + newCred = c.clone() + if err := w.Create(ctx, newCred, + db.WithOplog(oplogWrapper, newCred.oplog(oplog.OpType_OP_TYPE_CREATE))); err != nil { + return errors.Wrap(ctx, err, op) + } + + return nil + }, + ) + if err != nil { + if errors.IsUniqueError(err) { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("in store: %s: name %s already exists", c.StoreId, c.Name))) + } + return nil, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("in store: %s", c.StoreId))) + } + + // Clear object fields, only ObjectHmac should be returned + newCred.ObjectEncrypted = nil + newCred.Object = nil + + return newCred, nil +} + // LookupCredential returns the Credential for the publicId. Returns // nil, nil if no Credential is found for the publicId. // TODO: This should hit a view and return the interface type... @@ -234,6 +317,20 @@ func (r *Repository) LookupCredential(ctx context.Context, publicId string, _ .. spkCred.PrivateKeyPassphraseEncrypted = nil spkCred.PrivateKeyPassphrase = nil cred = spkCred + + case credential.JsonSubtype: + jsonCred := allocJsonCredential() + jsonCred.PublicId = publicId + if err := r.reader.LookupByPublicId(ctx, jsonCred); err != nil { + if errors.IsNotFoundError(err) { + return nil, nil + } + return nil, errors.Wrap(ctx, err, op, errors.WithMsg(fmt.Sprintf("failed for : %s", publicId))) + } + // Clear object fields, only ObjectHmac should be returned + jsonCred.ObjectEncrypted = nil + jsonCred.Object = nil + cred = jsonCred } return cred, nil @@ -485,8 +582,137 @@ func (r *Repository) UpdateSshPrivateKeyCredential(ctx context.Context, return returnedCredential, rowsUpdated, nil } -// ListCredentials returns a slice of UsernamePasswordCredentials for the -// storeId. WithLimit is the only option supported. +// UpdateJsonCredential updates the repository entry for c.PublicId +// with the values in c for the fields listed in fieldMaskPaths. It returns a +// new JsonCredential containing the updated values and a count of the +// number of records updated. c is not changed. +// +// c must contain a valid PublicId. Only Name, Description and +// Json can be changed. If c.Name is set to a non-empty string, it must be +// unique within c.ProjectId. +// +// An attribute of c will be set to NULL in the database if the attribute in c +// is the zero value and it is included in fieldMaskPaths. +func (r *Repository) UpdateJsonCredential(ctx context.Context, + projectId string, + c *JsonCredential, + version uint32, + fieldMaskPaths []string, + _ ...Option, +) (*JsonCredential, int, error) { + const op = "static.(Repository).UpdateJsonCredential" + if c == nil { + return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing credential") + } + if c.JsonCredential == nil { + return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing embedded credential") + } + if c.PublicId == "" { + return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidPublicId, op, "missing public id") + } + if version == 0 { + return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing version") + } + if projectId == "" { + return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing project id") + } + if c.StoreId == "" { + return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "missing store id") + } + c = c.clone() + + // each field in the json secret will be passed into the fieldMaskPaths as an individual path + // that shares the same prefix: attributes.object. + // for example attributes.object.username & attributes.object.password + // fieldMaskPaths elements will be deduped if sharing the same prefix attributes.object. + // and the values will be substituted with a single value Object. + var hasSecret bool + reducedFieldMaskPaths := []string{} + for _, f := range fieldMaskPaths { + if strings.HasPrefix(f, "attributes.object.") { + hasSecret = true + continue + } + reducedFieldMaskPaths = append(reducedFieldMaskPaths, f) + } + if hasSecret { + reducedFieldMaskPaths = append(reducedFieldMaskPaths, objectField) + } + + for _, f := range reducedFieldMaskPaths { + switch { + case strings.EqualFold(nameField, f): + case strings.EqualFold(descriptionField, f): + case strings.EqualFold(objectField, f): + default: + return nil, db.NoRowsAffected, errors.New(ctx, errors.InvalidFieldMask, op, f) + } + } + dbMask, nullFields := dbw.BuildUpdatePaths( + map[string]interface{}{ + nameField: c.Name, + descriptionField: c.Description, + objectField: c.Object, + }, + reducedFieldMaskPaths, + nil, + ) + if len(dbMask) == 0 && len(nullFields) == 0 { + return nil, db.NoRowsAffected, errors.New(ctx, errors.EmptyFieldMask, op, "missing field mask") + } + + if hasSecret { + // Json secret has been updated, re-encrypt and recalculate hmac + databaseWrapper, err := r.kms.GetWrapper(ctx, projectId, kms.KeyPurposeDatabase) + if err != nil { + return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get database wrapper")) + } + if err := c.encrypt(ctx, databaseWrapper); err != nil { + return nil, db.NoRowsAffected, errors.Wrap(ctx, err, op) + } + + // Set ObjectHmac and ObjectEncrypted masks for update. + dbMask = append(dbMask, "ObjectHmac", "ObjectEncrypted") + } + + oplogWrapper, err := r.kms.GetWrapper(ctx, projectId, kms.KeyPurposeOplog) + if err != nil { + return nil, db.NoRowsAffected, + errors.Wrap(ctx, err, op, errors.WithMsg("unable to get oplog wrapper")) + } + + var rowsUpdated int + var returnedCredential *JsonCredential + _, err = r.writer.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, + func(_ db.Reader, w db.Writer) error { + returnedCredential = c.clone() + var err error + rowsUpdated, err = w.Update(ctx, returnedCredential, + dbMask, nullFields, + db.WithOplog(oplogWrapper, returnedCredential.oplog(oplog.OpType_OP_TYPE_UPDATE)), + db.WithVersion(&version)) + if err != nil { + return errors.Wrap(ctx, err, op) + } + if rowsUpdated > 1 { + return errors.New(ctx, errors.MultipleRecords, op, "more than 1 resource would have been updated") + } + return nil + }, + ) + if err != nil { + return nil, db.NoRowsAffected, err + } + + // Clear object fields, only ObjectHmac should be returned + returnedCredential.ObjectEncrypted = nil + returnedCredential.Object = nil + + return returnedCredential, rowsUpdated, nil +} + +// ListCredentials returns a slice of UsernamePasswordCredentials, SshPrivateKeyCredentials, and JsonCredentials +// for the storeId. WithLimit is the only option supported. // TODO: This should hit a view and return the interface type... func (r *Repository) ListCredentials(ctx context.Context, storeId string, opt ...Option) ([]credential.Static, error) { const op = "static.(Repository).ListCredentials" @@ -512,7 +738,13 @@ func (r *Repository) ListCredentials(ctx context.Context, storeId string, opt .. return nil, errors.Wrap(ctx, err, op) } - ret := make([]credential.Static, 0, len(upCreds)+len(spkCreds)) + var jsonCreds []*JsonCredential + err = r.reader.SearchWhere(ctx, &jsonCreds, "store_id = ?", []interface{}{storeId}, db.WithLimit(limit)) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + + ret := make([]credential.Static, 0, len(upCreds)+len(spkCreds)+len(jsonCreds)) for _, c := range upCreds { // Clear password fields, only PasswordHmac should be returned @@ -532,6 +764,13 @@ func (r *Repository) ListCredentials(ctx context.Context, storeId string, opt .. ret = append(ret, c) } + for _, c := range jsonCreds { + // Clear the object fields, only ObjectHmac should be returned + c.ObjectEncrypted = nil + c.Object = nil + ret = append(ret, c) + } + return ret, nil } @@ -560,6 +799,11 @@ func (r *Repository) DeleteCredential(ctx context.Context, projectId, id string, c.PublicId = id input = c md = c.oplog(oplog.OpType_OP_TYPE_DELETE) + case credential.JsonSubtype: + c := allocJsonCredential() + c.PublicId = id + input = c + md = c.oplog(oplog.OpType_OP_TYPE_DELETE) default: return db.NoRowsAffected, errors.New(ctx, errors.InvalidParameter, op, "unknown type") } diff --git a/internal/credential/static/repository_credential_test.go b/internal/credential/static/repository_credential_test.go index 56d1cdb3a..b17075575 100644 --- a/internal/credential/static/repository_credential_test.go +++ b/internal/credential/static/repository_credential_test.go @@ -2,6 +2,7 @@ package static import ( "context" + "encoding/json" "testing" "time" @@ -17,6 +18,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/crypto/ssh/testdata" + "google.golang.org/protobuf/types/known/structpb" ) func TestRepository_CreateUsernamePasswordCredential(t *testing.T) { @@ -398,6 +400,165 @@ func TestRepository_CreateSshPrivateKeyCredential(t *testing.T) { }) } +func TestRepository_CreateJsonCredential(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + _, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + + obj, objBytes, err := TestJsonObject() + assert.NoError(t, err) + + cs := TestCredentialStore(t, conn, wrapper, prj.PublicId) + + tests := []struct { + name string + projectId string + cred *JsonCredential + wantErr bool + wantErrCode errors.Code + }{ + { + name: "missing-store", + wantErr: true, + wantErrCode: errors.InvalidParameter, + }, + { + name: "missing-embedded-cred", + cred: &JsonCredential{}, + wantErr: true, + wantErrCode: errors.InvalidParameter, + }, + { + name: "missing-project-id", + cred: &JsonCredential{ + JsonCredential: &store.JsonCredential{ + Object: objBytes, + StoreId: cs.PublicId, + }, + }, + wantErr: true, + wantErrCode: errors.InvalidParameter, + }, + { + name: "missing-secret", + projectId: prj.PublicId, + cred: &JsonCredential{ + JsonCredential: &store.JsonCredential{ + StoreId: cs.PublicId, + }, + }, + wantErr: true, + wantErrCode: errors.InvalidParameter, + }, + { + name: "missing-store-id", + projectId: prj.PublicId, + cred: &JsonCredential{ + JsonCredential: &store.JsonCredential{ + Object: objBytes, + }, + }, + wantErr: true, + wantErrCode: errors.InvalidParameter, + }, + { + name: "valid", + projectId: prj.PublicId, + cred: &JsonCredential{ + JsonCredential: &store.JsonCredential{ + Object: objBytes, + StoreId: cs.PublicId, + }, + }, + }, + } + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + ctx := context.Background() + kkms := kms.TestKms(t, conn, wrapper) + repo, err := NewRepository(ctx, rw, rw, kkms) + require.NoError(err) + require.NotNil(repo) + + got, err := repo.CreateJsonCredential(ctx, tt.projectId, tt.cred) + if tt.wantErr { + assert.Truef(errors.Match(errors.T(tt.wantErr), err), "want err: %q got: %q", tt.wantErr, err) + assert.Nil(got) + return + } + require.NoError(err) + assertPublicId(t, credential.JsonCredentialPrefix, got.PublicId) + assert.Nil(got.Object) + assert.Nil(got.ObjectEncrypted) + + // Validate secret + lookupCred := allocJsonCredential() + lookupCred.PublicId = got.PublicId + require.NoError(rw.LookupById(ctx, lookupCred)) + + databaseWrapper, err := kkms.GetWrapper(context.Background(), tt.projectId, kms.KeyPurposeDatabase) + require.NoError(err) + require.NoError(lookupCred.decrypt(ctx, databaseWrapper)) + assert.Equal(tt.cred.Object, lookupCred.Object) + + assert.Empty(got.Object) + assert.Empty(got.ObjectEncrypted) + assert.NotEmpty(got.ObjectHmac) + + // Validate hmac + hm, err := crypto.HmacSha256(ctx, tt.cred.Object, databaseWrapper, []byte(tt.cred.StoreId), nil) + require.NoError(err) + assert.Equal([]byte(hm), got.ObjectHmac) + + // Validate oplog + assert.NoError(db.TestVerifyOplog(t, rw, got.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_CREATE), db.WithCreateNotBefore(10*time.Second))) + }) + } + + t.Run("duplicate-names", func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + ctx := context.Background() + kms := kms.TestKms(t, conn, wrapper) + repo, err := NewRepository(ctx, rw, rw, kms) + require.NoError(err) + require.NotNil(repo) + org, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + prj2 := iam.TestProject(t, iam.TestRepo(t, conn, wrapper), org.PublicId) + require.NoError(err) + + prjCs := TestCredentialStore(t, conn, wrapper, prj.GetPublicId()) + prj2Cs := TestCredentialStore(t, conn, wrapper, prj2.GetPublicId()) + + in, err := NewJsonCredential(ctx, prjCs.GetPublicId(), obj, WithName("my-name"), WithDescription("original")) + assert.NoError(err) + + got, err := repo.CreateJsonCredential(ctx, prj.PublicId, in) + require.NoError(err) + assert.Equal(in.Name, got.Name) + assert.Equal(in.Description, got.Description) + + in2, err := NewJsonCredential(ctx, prjCs.GetPublicId(), obj, WithName("my-name"), WithDescription("different")) + require.NoError(err) + got2, err := repo.CreateJsonCredential(ctx, prj.GetPublicId(), in2) + assert.Truef(errors.Match(errors.T(errors.NotUnique), err), "want err code: %v got err: %v", errors.NotUnique, err) + assert.Nil(got2) + + // Creating credential in different scope should not conflict + in3, err := NewJsonCredential(ctx, prj2Cs.GetPublicId(), obj, WithName("my-name"), WithDescription("different")) + require.NoError(err) + got3, err := repo.CreateJsonCredential(ctx, org.GetPublicId(), in3) + require.NoError(err) + assert.Equal(in3.Name, got3.Name) + assert.Equal(in3.Description, got3.Description) + + assert.NotEqual(got.PublicId, got3.PublicId) + }) +} + func TestRepository_LookupCredential(t *testing.T) { t.Parallel() conn, _ := db.TestSetup(t, "postgres") @@ -411,6 +572,11 @@ func TestRepository_LookupCredential(t *testing.T) { spkCredWithPass := TestSshPrivateKeyCredential(t, conn, wrapper, "username", string(testdata.PEMEncryptedKeys[0].PEMBytes), store.PublicId, prj.PublicId, WithPrivateKeyPassphrase([]byte(testdata.PEMEncryptedKeys[0].EncryptionKey))) + obj, _, err := TestJsonObject() + assert.NoError(t, err) + + jsonCred := TestJsonCredential(t, conn, wrapper, store.PublicId, prj.PublicId, obj) + tests := []struct { name string id string @@ -432,6 +598,11 @@ func TestRepository_LookupCredential(t *testing.T) { id: spkCredWithPass.GetPublicId(), want: spkCredWithPass, }, + { + name: "json-valid", + id: jsonCred.GetPublicId(), + want: jsonCred, + }, { name: "empty-public-id", id: "", @@ -476,7 +647,6 @@ func TestRepository_LookupCredential(t *testing.T) { assert.Empty(v.PrivateKey) assert.Empty(v.PrivateKeyEncrypted) assert.NotEmpty(v.PrivateKeyHmac) - want, ok := tt.want.(*SshPrivateKeyCredential) require.True(ok) assert.Empty(v.PrivateKeyPassphrase) @@ -484,6 +654,10 @@ func TestRepository_LookupCredential(t *testing.T) { if len(want.PrivateKeyPassphrase) > 0 { assert.NotEmpty(v.PrivateKeyPassphraseHmac) } + case *JsonCredential: + assert.Empty(v.Object) + assert.Empty(v.ObjectEncrypted) + assert.NotEmpty(v.ObjectHmac) default: require.Fail("unknown type") } @@ -499,11 +673,16 @@ func TestRepository_ListCredentials(t *testing.T) { kms := kms.TestKms(t, conn, wrapper) defaultLimit := 5 - total := 20 + total := 30 _, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) store := TestCredentialStore(t, conn, wrapper, prj.GetPublicId()) - TestUsernamePasswordCredentials(t, conn, wrapper, "user", "pass", store.GetPublicId(), prj.GetPublicId(), total/2) - TestSshPrivateKeyCredentials(t, conn, wrapper, "user", TestSshPrivateKeyPem, store.GetPublicId(), prj.GetPublicId(), total/2) + TestUsernamePasswordCredentials(t, conn, wrapper, "user", "pass", store.GetPublicId(), prj.GetPublicId(), total/3) + TestSshPrivateKeyCredentials(t, conn, wrapper, "user", TestSshPrivateKeyPem, store.GetPublicId(), prj.GetPublicId(), total/3) + + obj, _, err := TestJsonObject() + assert.NoError(t, err) + + TestJsonCredentials(t, conn, wrapper, store.GetPublicId(), prj.GetPublicId(), obj, total/3) type args struct { storeId string @@ -527,7 +706,7 @@ func TestRepository_ListCredentials(t *testing.T) { args: args{ storeId: store.PublicId, }, - wantCnt: defaultLimit * 2, + wantCnt: defaultLimit * 3, }, { name: "custom-limit", @@ -535,7 +714,7 @@ func TestRepository_ListCredentials(t *testing.T) { storeId: store.PublicId, opt: []Option{WithLimit(3)}, }, - wantCnt: 3 * 2, + wantCnt: 9, }, { name: "bad-store", @@ -568,6 +747,10 @@ func TestRepository_ListCredentials(t *testing.T) { assert.Empty(v.PrivateKey) assert.Empty(v.PrivateKeyEncrypted) assert.NotEmpty(v.PrivateKeyHmac) + case *JsonCredential: + assert.Empty(v.Object) + assert.Empty(v.ObjectEncrypted) + assert.NotEmpty(v.ObjectHmac) default: require.Fail("unknown type") } @@ -589,6 +772,11 @@ func TestRepository_DeleteCredential(t *testing.T) { upCred := TestUsernamePasswordCredential(t, conn, wrapper, "user", "pass", store.GetPublicId(), prj.GetPublicId()) spkCred := TestSshPrivateKeyCredential(t, conn, wrapper, "user", TestSshPrivateKeyPem, store.GetPublicId(), prj.GetPublicId()) + obj, _, err := TestJsonObject() + assert.NoError(t, err) + + jsonCred := TestJsonCredential(t, conn, wrapper, store.PublicId, prj.PublicId, obj) + tests := []struct { name string in string @@ -616,6 +804,11 @@ func TestRepository_DeleteCredential(t *testing.T) { in: spkCred.GetPublicId(), want: 1, }, + { + name: "with existing json id", + in: jsonCred.GetPublicId(), + want: 1, + }, } for _, tt := range tests { @@ -1700,14 +1893,14 @@ Hdtbe1Kk0rHxN0yIKqXNAAAACWplZmZAYXJjaAECAwQ= } func TestSshPrivateKeyConstraints(t *testing.T) { - t.Parallel() + ctx := context.Background() conn, _ := db.TestSetup(t, "postgres") rw := db.New(conn) wrapper := db.TestWrapper(t) - assert, require := assert.New(t), require.New(t) - ctx := context.Background() kkms := kms.TestKms(t, conn, wrapper) repo, err := NewRepository(ctx, rw, rw, kkms) + + assert, require := assert.New(t), require.New(t) assert.NoError(err) require.NotNil(repo) @@ -1771,3 +1964,440 @@ func TestSshPrivateKeyConstraints(t *testing.T) { }) } } + +func TestRepository_UpdateJsonCredential(t *testing.T) { + t.Parallel() + conn, _ := db.TestSetup(t, "postgres") + rw := db.New(conn) + wrapper := db.TestWrapper(t) + + _, objBytes, err := TestJsonObject() + assert.NoError(t, err) + + secondJsonSecret := &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "username": structpb.NewStringValue("new-user"), + "password": structpb.NewStringValue("new-password"), + "hash": structpb.NewStringValue("0987654321"), + }, + } + secondBSecret, err := json.Marshal(secondJsonSecret) + assert.NoError(t, err) + + changeName := func(n string) func(credential *JsonCredential) *JsonCredential { + return func(c *JsonCredential) *JsonCredential { + c.Name = n + return c + } + } + + changeDescription := func(d string) func(*JsonCredential) *JsonCredential { + return func(c *JsonCredential) *JsonCredential { + c.Description = d + return c + } + } + + makeNil := func() func(*JsonCredential) *JsonCredential { + return func(_ *JsonCredential) *JsonCredential { + return nil + } + } + + makeEmbeddedNil := func() func(*JsonCredential) *JsonCredential { + return func(_ *JsonCredential) *JsonCredential { + return &JsonCredential{} + } + } + + setPublicId := func(n string) func(*JsonCredential) *JsonCredential { + return func(c *JsonCredential) *JsonCredential { + c.PublicId = n + return c + } + } + + deleteStoreId := func() func(*JsonCredential) *JsonCredential { + return func(c *JsonCredential) *JsonCredential { + c.StoreId = "" + return c + } + } + + deleteVersion := func() func(*JsonCredential) *JsonCredential { + return func(c *JsonCredential) *JsonCredential { + c.Version = 0 + return c + } + } + + changeObject := func(s []byte) func(*JsonCredential) *JsonCredential { + return func(c *JsonCredential) *JsonCredential { + c.Object = s + return c + } + } + + combine := func(fns ...func(cs *JsonCredential) *JsonCredential) func(*JsonCredential) *JsonCredential { + return func(c *JsonCredential) *JsonCredential { + for _, fn := range fns { + c = fn(c) + } + return c + } + } + + tests := []struct { + name string + orig *JsonCredential + chgFn func(*JsonCredential) *JsonCredential + masks []string + want *JsonCredential + wantCount int + wantErr errors.Code + }{ + { + name: "nil-credential", + orig: &JsonCredential{ + JsonCredential: &store.JsonCredential{ + Object: objBytes, + }, + }, + chgFn: makeNil(), + masks: []string{"Name", "Description"}, + wantErr: errors.InvalidParameter, + }, + { + name: "nil-embedded-credential", + orig: &JsonCredential{ + JsonCredential: &store.JsonCredential{ + Object: objBytes, + }, + }, + chgFn: makeEmbeddedNil(), + masks: []string{"Name", "Description"}, + wantErr: errors.InvalidParameter, + }, + { + name: "no-public-id", + orig: &JsonCredential{ + JsonCredential: &store.JsonCredential{ + Object: objBytes, + }, + }, + chgFn: setPublicId(""), + masks: []string{"Name", "Description"}, + wantErr: errors.InvalidPublicId, + }, + { + name: "no-store-id", + orig: &JsonCredential{ + JsonCredential: &store.JsonCredential{ + Object: objBytes, + }, + }, + chgFn: deleteStoreId(), + masks: []string{"Name", "Description"}, + wantErr: errors.InvalidParameter, + }, + { + name: "no-version", + orig: &JsonCredential{ + JsonCredential: &store.JsonCredential{ + Object: objBytes, + }, + }, + chgFn: deleteVersion(), + masks: []string{"Name", "Description"}, + wantErr: errors.InvalidParameter, + }, + { + name: "updating-non-existent-credential", + orig: &JsonCredential{ + JsonCredential: &store.JsonCredential{ + Name: "test-name-repo", + Object: objBytes, + }, + }, + chgFn: combine(setPublicId("abcd_OOOOOOOOOO"), changeName("test-update-name-repo")), + masks: []string{"Name"}, + wantErr: errors.RecordNotFound, + }, + { + name: "empty-field-mask", + orig: &JsonCredential{ + JsonCredential: &store.JsonCredential{ + Name: "test-name-repo", + Object: objBytes, + }, + }, + chgFn: changeName("test-update-name-repo"), + wantErr: errors.EmptyFieldMask, + }, + { + name: "read-only-fields-in-field-mask", + orig: &JsonCredential{ + JsonCredential: &store.JsonCredential{ + Name: "test-name-repo", + Object: objBytes, + }, + }, + chgFn: changeName("test-update-name-repo"), + masks: []string{"PublicId", "CreateTime", "UpdateTime", "ScopeId"}, + wantErr: errors.InvalidFieldMask, + }, + { + name: "unknown-field-in-field-mask", + orig: &JsonCredential{ + JsonCredential: &store.JsonCredential{ + Name: "test-name-repo", + Object: objBytes, + }, + }, + chgFn: changeName("test-update-name-repo"), + masks: []string{"FIELD_DNE"}, + wantErr: errors.InvalidFieldMask, + }, + { + name: "change-name", + orig: &JsonCredential{ + JsonCredential: &store.JsonCredential{ + Name: "test-name-repo", + Object: objBytes, + }, + }, + chgFn: changeName("test-update-name-repo"), + masks: []string{"Name"}, + want: &JsonCredential{ + JsonCredential: &store.JsonCredential{ + Name: "test-update-name-repo", + Object: objBytes, + }, + }, + wantCount: 1, + }, + { + name: "change-description", + orig: &JsonCredential{ + JsonCredential: &store.JsonCredential{ + Description: "test-description-repo", + Object: objBytes, + }, + }, + chgFn: changeDescription("test-update-description-repo"), + masks: []string{"Description"}, + want: &JsonCredential{ + JsonCredential: &store.JsonCredential{ + Description: "test-update-description-repo", + Object: objBytes, + }, + }, + wantCount: 1, + }, + { + name: "change-name-and-description", + orig: &JsonCredential{ + JsonCredential: &store.JsonCredential{ + Name: "test-name-repo", + Description: "test-description-repo", + Object: objBytes, + }, + }, + chgFn: combine(changeDescription("test-update-description-repo"), changeName("test-update-name-repo")), + masks: []string{"Name", "Description"}, + want: &JsonCredential{ + JsonCredential: &store.JsonCredential{ + Name: "test-update-name-repo", + Description: "test-update-description-repo", + Object: objBytes, + }, + }, + wantCount: 1, + }, + { + name: "change-json-secret", + orig: &JsonCredential{ + JsonCredential: &store.JsonCredential{ + Object: objBytes, + }, + }, + chgFn: changeObject(secondBSecret), + masks: []string{"attributes.object.username", "attributes.object.password", "attributes.object.hash"}, + want: &JsonCredential{ + JsonCredential: &store.JsonCredential{ + Object: secondBSecret, + }, + }, + wantCount: 1, + }, + { + name: "do-not-delete-json-secret", + orig: &JsonCredential{ + JsonCredential: &store.JsonCredential{ + Object: objBytes, + }, + }, + masks: []string{}, + chgFn: changeObject(nil), + want: &JsonCredential{ + JsonCredential: &store.JsonCredential{ + Object: objBytes, + }, + }, + wantErr: errors.EmptyFieldMask, + wantCount: 0, + }, + { + name: "delete-name", + orig: &JsonCredential{ + JsonCredential: &store.JsonCredential{ + Name: "test-name-repo", + Description: "test-description-repo", + Object: objBytes, + }, + }, + masks: []string{"Name"}, + chgFn: combine(changeDescription("test-update-description-repo"), changeName("")), + want: &JsonCredential{ + JsonCredential: &store.JsonCredential{ + Description: "test-description-repo", + Object: objBytes, + }, + }, + wantCount: 1, + }, + { + name: "delete-description", + orig: &JsonCredential{ + JsonCredential: &store.JsonCredential{ + Name: "test-name-repo", + Description: "test-description-repo", + Object: objBytes, + }, + }, + masks: []string{"Description"}, + chgFn: combine(changeDescription(""), changeName("test-update-name-repo")), + want: &JsonCredential{ + JsonCredential: &store.JsonCredential{ + Name: "test-name-repo", + Object: objBytes, + }, + }, + wantCount: 1, + }, + { + name: "do-not-delete-name", + orig: &JsonCredential{ + JsonCredential: &store.JsonCredential{ + Name: "test-name-repo", + Description: "test-description-repo", + Object: objBytes, + }, + }, + masks: []string{"Description"}, + chgFn: combine(changeDescription("test-update-description-repo"), changeName("")), + want: &JsonCredential{ + JsonCredential: &store.JsonCredential{ + Name: "test-name-repo", + Description: "test-update-description-repo", + Object: objBytes, + }, + }, + wantCount: 1, + }, + { + name: "do-not-delete-description", + orig: &JsonCredential{ + JsonCredential: &store.JsonCredential{ + Name: "test-name-repo", + Description: "test-description-repo", + Object: objBytes, + }, + }, + masks: []string{"Name"}, + chgFn: combine(changeDescription(""), changeName("test-update-name-repo")), + want: &JsonCredential{ + JsonCredential: &store.JsonCredential{ + Name: "test-update-name-repo", + Description: "test-description-repo", + Object: objBytes, + }, + }, + wantCount: 1, + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + assert, require := assert.New(t), require.New(t) + ctx := context.Background() + kkms := kms.TestKms(t, conn, wrapper) + repo, err := NewRepository(ctx, rw, rw, kkms) + assert.NoError(err) + require.NotNil(repo) + + _, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + store := TestCredentialStore(t, conn, wrapper, prj.GetPublicId()) + tt.orig.StoreId = store.PublicId + + orig, err := repo.CreateJsonCredential(ctx, prj.GetPublicId(), tt.orig) + assert.NoError(err) + require.NotNil(orig) + + if tt.chgFn != nil { + orig = tt.chgFn(orig) + } + var version uint32 + if orig != nil { + version = orig.GetVersion() + } + got, gotCount, err := repo.UpdateJsonCredential(ctx, prj.GetPublicId(), orig, version, tt.masks) + if tt.wantErr != 0 { + assert.Truef(errors.Match(errors.T(tt.wantErr), err), "want err: %q got: %q", tt.wantErr, err) + assert.Equal(tt.wantCount, gotCount, "row count") + assert.Nil(got) + return + } + assert.NoError(err) + assert.Empty(tt.orig.PublicId) + require.NotNil(got) + assertPublicId(t, credential.JsonCredentialPrefix, got.PublicId) + assert.Equal(tt.wantCount, gotCount, "row count") + assert.NotSame(tt.orig, got) + assert.Equal(tt.orig.StoreId, got.StoreId) + underlyingDB, err := conn.SqlDB(ctx) + require.NoError(err) + dbassert := dbassert.New(t, underlyingDB) + if tt.want.Name == "" { + got := got.clone() + dbassert.IsNull(got, "name") + } else { + assert.Equal(tt.want.Name, got.Name) + } + + if tt.want.Description == "" { + got := got.clone() + dbassert.IsNull(got, "description") + } else { + assert.Equal(tt.want.Description, got.Description) + } + + // Validate only SecretHmac is returned + assert.Empty(got.Object) + assert.Empty(got.ObjectEncrypted) + assert.NotEmpty(got.ObjectHmac) + + // Validate hmac + databaseWrapper, err := kkms.GetWrapper(context.Background(), prj.GetPublicId(), kms.KeyPurposeDatabase) + require.NoError(err) + hm, err := crypto.HmacSha256(ctx, tt.want.Object, databaseWrapper, []byte(store.GetPublicId()), nil) + require.NoError(err) + assert.Equal([]byte(hm), got.ObjectHmac) + + if tt.wantCount > 0 { + assert.NoError(db.TestVerifyOplog(t, rw, got.PublicId, db.WithOperation(oplog.OpType_OP_TYPE_UPDATE), db.WithCreateNotBefore(10*time.Second))) + } + }) + } +} diff --git a/internal/credential/static/repository_credentials.go b/internal/credential/static/repository_credentials.go index 04f151c14..5a9746aee 100644 --- a/internal/credential/static/repository_credentials.go +++ b/internal/credential/static/repository_credentials.go @@ -27,10 +27,15 @@ func (r *Repository) Retrieve(ctx context.Context, projectId string, ids []strin if err != nil { return nil, errors.Wrap(ctx, err, op) } + var jsonCreds []*JsonCredential + err = r.reader.SearchWhere(ctx, &jsonCreds, "public_id in (?)", []interface{}{ids}) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } - if len(upCreds)+len(spkCreds) != len(ids) { + if len(upCreds)+len(spkCreds)+len(jsonCreds) != len(ids) { return nil, errors.New(ctx, errors.NotSpecificIntegrity, op, - fmt.Sprintf("mismatch between creds and number of ids requested, expected %d got %d", len(ids), len(upCreds)+len(spkCreds))) + fmt.Sprintf("mismatch between creds and number of ids requested, expected %d got %d", len(ids), len(upCreds)+len(spkCreds)+len(jsonCreds))) } out := make([]credential.Static, 0, len(ids)) @@ -60,5 +65,18 @@ func (r *Repository) Retrieve(ctx context.Context, projectId string, ids []strin out = append(out, c) } + for _, c := range jsonCreds { + // decrypt credential + databaseWrapper, err := r.kms.GetWrapper(ctx, projectId, kms.KeyPurposeDatabase) + if err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to get database wrapper")) + } + if err := c.decrypt(ctx, databaseWrapper); err != nil { + return nil, errors.Wrap(ctx, err, op) + } + + out = append(out, c) + } + return out, nil } diff --git a/internal/credential/static/repository_credentials_test.go b/internal/credential/static/repository_credentials_test.go index e2911ea22..f8c77b980 100644 --- a/internal/credential/static/repository_credentials_test.go +++ b/internal/credential/static/repository_credentials_test.go @@ -15,6 +15,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/crypto/ssh/testdata" + "google.golang.org/protobuf/types/known/structpb" ) func TestRepository_Retrieve(t *testing.T) { @@ -39,9 +40,25 @@ func TestRepository_Retrieve(t *testing.T) { string(testdata.PEMEncryptedKeys[0].PEMBytes), staticStore.GetPublicId(), prj.GetPublicId(), WithPrivateKeyPassphrase([]byte(testdata.PEMEncryptedKeys[0].EncryptionKey))) + obj, _, err := TestJsonObject() + assert.NoError(err) + + secondObj := credential.JsonObject{ + structpb.Struct{ + Fields: map[string]*structpb.Value{ + "username": structpb.NewStringValue("new-user"), + "password": structpb.NewStringValue("new-password"), + "hash": structpb.NewStringValue("0987654321"), + }, + }, + } + + jsonCred1 := TestJsonCredential(t, conn, wrapper, staticStore.GetPublicId(), prj.GetPublicId(), obj) + jsonCred2 := TestJsonCredential(t, conn, wrapper, staticStore.GetPublicId(), prj.GetPublicId(), secondObj) + type args struct { - credIds []string - projectIds string + credIds []string + projectId string } tests := []struct { name string @@ -59,16 +76,16 @@ func TestRepository_Retrieve(t *testing.T) { { name: "invalid-project-id", args: args{ - projectIds: org.GetPublicId(), - credIds: []string{upCred1.GetPublicId()}, + projectId: org.GetPublicId(), + credIds: []string{upCred1.GetPublicId()}, }, wantErr: true, }, { name: "valid-one-up-cred", args: args{ - projectIds: prj.GetPublicId(), - credIds: []string{upCred1.GetPublicId()}, + projectId: prj.GetPublicId(), + credIds: []string{upCred1.GetPublicId()}, }, wantCreds: []credential.Static{ upCred1, @@ -77,8 +94,8 @@ func TestRepository_Retrieve(t *testing.T) { { name: "valid-multiple-up-creds", args: args{ - projectIds: prj.GetPublicId(), - credIds: []string{upCred1.GetPublicId(), upCred2.GetPublicId()}, + projectId: prj.GetPublicId(), + credIds: []string{upCred1.GetPublicId(), upCred2.GetPublicId()}, }, wantCreds: []credential.Static{ upCred1, upCred2, @@ -87,8 +104,8 @@ func TestRepository_Retrieve(t *testing.T) { { name: "valid-ssh-pk-cred", args: args{ - projectIds: prj.GetPublicId(), - credIds: []string{spkCred1.GetPublicId()}, + projectId: prj.GetPublicId(), + credIds: []string{spkCred1.GetPublicId()}, }, wantCreds: []credential.Static{ spkCred1, @@ -97,28 +114,48 @@ func TestRepository_Retrieve(t *testing.T) { { name: "valid-multiple-ssh-pk-creds", args: args{ - projectIds: prj.GetPublicId(), - credIds: []string{spkCred1.GetPublicId(), spkCred2.GetPublicId(), spkCredWithPass.GetPublicId()}, + projectId: prj.GetPublicId(), + credIds: []string{spkCred1.GetPublicId(), spkCred2.GetPublicId(), spkCredWithPass.GetPublicId()}, }, wantCreds: []credential.Static{ spkCred1, spkCred2, spkCredWithPass, }, }, { - name: "valid-mixed-creds", + name: "valid-json-cred", args: args{ - projectIds: prj.GetPublicId(), - credIds: []string{upCred1.GetPublicId(), spkCred1.GetPublicId(), spkCredWithPass.GetPublicId(), spkCred2.GetPublicId(), upCred2.GetPublicId()}, + projectId: prj.GetPublicId(), + credIds: []string{jsonCred1.GetPublicId()}, }, wantCreds: []credential.Static{ - upCred1, spkCred1, spkCred2, upCred2, spkCredWithPass, + jsonCred1, + }, + }, + { + name: "valid-multiple-json-creds", + args: args{ + projectId: prj.GetPublicId(), + credIds: []string{jsonCred1.GetPublicId(), jsonCred2.GetPublicId()}, + }, + wantCreds: []credential.Static{ + jsonCred1, jsonCred2, + }, + }, + { + name: "valid-mixed-creds", + args: args{ + projectId: prj.GetPublicId(), + credIds: []string{upCred1.GetPublicId(), spkCred1.GetPublicId(), spkCredWithPass.GetPublicId(), spkCred2.GetPublicId(), upCred2.GetPublicId(), jsonCred1.GetPublicId(), jsonCred2.GetPublicId()}, + }, + wantCreds: []credential.Static{ + upCred1, spkCred1, spkCredWithPass, spkCred2, upCred2, jsonCred1, jsonCred2, }, }, } for _, tt := range tests { tt := tt t.Run(tt.name, func(t *testing.T) { - gotCreds, err := repo.Retrieve(context.Background(), tt.args.projectIds, tt.args.credIds) + gotCreds, err := repo.Retrieve(context.Background(), tt.args.projectId, tt.args.credIds) if tt.wantErr { require.Error(err) assert.Nil(gotCreds) @@ -131,7 +168,8 @@ func TestRepository_Retrieve(t *testing.T) { gotCreds, cmpopts.IgnoreUnexported( UsernamePasswordCredential{}, store.UsernamePasswordCredential{}, - SshPrivateKeyCredential{}, store.SshPrivateKeyCredential{}), + SshPrivateKeyCredential{}, store.SshPrivateKeyCredential{}, + JsonCredential{}, store.JsonCredential{}), cmpopts.IgnoreTypes(×tamp.Timestamp{}), cmpopts.IgnoreFields(SshPrivateKeyCredential{}, "PassphraseUnneeded"), cmpopts.SortSlices(func(x, y credential.Static) bool { diff --git a/internal/credential/static/store/static.pb.go b/internal/credential/static/store/static.pb.go index ba71ae593..d9da969e4 100644 --- a/internal/credential/static/store/static.pb.go +++ b/internal/credential/static/store/static.pb.go @@ -499,6 +499,160 @@ func (x *SshPrivateKeyCredential) GetPrivateKeyPassphraseHmac() []byte { return nil } +type JsonCredential struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // public_id is a surrogate key suitable for use in a public API. + // @inject_tag: `gorm:"primary_key"` + PublicId string `protobuf:"bytes,1,opt,name=public_id,json=publicId,proto3" json:"public_id,omitempty" gorm:"primary_key"` + // create_time is set by the database. + // @inject_tag: `gorm:"default:current_timestamp"` + CreateTime *timestamp.Timestamp `protobuf:"bytes,2,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty" gorm:"default:current_timestamp"` + // update_time is set by the database. + // @inject_tag: `gorm:"default:current_timestamp"` + UpdateTime *timestamp.Timestamp `protobuf:"bytes,3,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,4,opt,name=name,proto3" json:"name,omitempty" gorm:"default:null"` + // description is optional. + // @inject_tag: `gorm:"default:null"` + Description string `protobuf:"bytes,5,opt,name=description,proto3" json:"description,omitempty" gorm:"default:null"` + // store_id of the owning static credential store. + // It must be set. + // @inject_tag: `gorm:"not_null"` + StoreId string `protobuf:"bytes,6,opt,name=store_id,json=storeId,proto3" json:"store_id,omitempty" gorm:"not_null"` + // version allows optimistic locking of the resource. + // @inject_tag: `gorm:"default:null"` + Version uint32 `protobuf:"varint,7,opt,name=version,proto3" json:"version,omitempty" gorm:"default:null"` + // object is the plain-text of the json associated with the + // credential. We are not storing this plain-text object in the database. + // @inject_tag: `gorm:"-" wrapping:"pt,object"` + Object []byte `protobuf:"bytes,8,opt,name=object,proto3" json:"object,omitempty" gorm:"-" wrapping:"pt,object"` + // object_encrypted is the ciphertext of the object. It is stored in + // the database. + // @inject_tag: `gorm:"column:object_encrypted;not_null" wrapping:"ct,object"` + ObjectEncrypted []byte `protobuf:"bytes,9,opt,name=object_encrypted,json=objectEncrypted,proto3" json:"object_encrypted,omitempty" gorm:"column:object_encrypted;not_null" wrapping:"ct,object"` + // object_hmac is a sha256-hmac of the unencrypted object. It is recalculated + // everytime the object is updated. + // @inject_tag: `gorm:"not_null"` + ObjectHmac []byte `protobuf:"bytes,10,opt,name=object_hmac,json=objectHmac,proto3" json:"object_hmac,omitempty" gorm:"not_null"` + // The key_id of the kms database key used for encrypting this entry. + // It must be set. + // @inject_tag: `gorm:"not_null"` + KeyId string `protobuf:"bytes,11,opt,name=key_id,json=keyId,proto3" json:"key_id,omitempty" gorm:"not_null"` +} + +func (x *JsonCredential) Reset() { + *x = JsonCredential{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_storage_credential_static_store_v1_static_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *JsonCredential) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*JsonCredential) ProtoMessage() {} + +func (x *JsonCredential) ProtoReflect() protoreflect.Message { + mi := &file_controller_storage_credential_static_store_v1_static_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 JsonCredential.ProtoReflect.Descriptor instead. +func (*JsonCredential) Descriptor() ([]byte, []int) { + return file_controller_storage_credential_static_store_v1_static_proto_rawDescGZIP(), []int{3} +} + +func (x *JsonCredential) GetPublicId() string { + if x != nil { + return x.PublicId + } + return "" +} + +func (x *JsonCredential) GetCreateTime() *timestamp.Timestamp { + if x != nil { + return x.CreateTime + } + return nil +} + +func (x *JsonCredential) GetUpdateTime() *timestamp.Timestamp { + if x != nil { + return x.UpdateTime + } + return nil +} + +func (x *JsonCredential) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *JsonCredential) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *JsonCredential) GetStoreId() string { + if x != nil { + return x.StoreId + } + return "" +} + +func (x *JsonCredential) GetVersion() uint32 { + if x != nil { + return x.Version + } + return 0 +} + +func (x *JsonCredential) GetObject() []byte { + if x != nil { + return x.Object + } + return nil +} + +func (x *JsonCredential) GetObjectEncrypted() []byte { + if x != nil { + return x.ObjectEncrypted + } + return nil +} + +func (x *JsonCredential) GetObjectHmac() []byte { + if x != nil { + return x.ObjectHmac + } + return nil +} + +func (x *JsonCredential) GetKeyId() string { + if x != nil { + return x.KeyId + } + return "" +} + var File_controller_storage_credential_static_store_v1_static_proto protoreflect.FileDescriptor var file_controller_storage_credential_static_store_v1_static_proto_rawDesc = []byte{ @@ -639,12 +793,47 @@ var file_controller_storage_credential_static_store_v1_static_proto_rawDesc = [] 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x70, 0x61, 0x73, 0x73, 0x70, 0x68, 0x72, 0x61, 0x73, 0x65, 0x5f, 0x68, 0x6d, 0x61, 0x63, 0x52, 0x18, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, - 0x50, 0x61, 0x73, 0x73, 0x70, 0x68, 0x72, 0x61, 0x73, 0x65, 0x48, 0x6d, 0x61, 0x63, 0x42, 0x46, - 0x5a, 0x44, 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, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, - 0x69, 0x61, 0x6c, 0x2f, 0x73, 0x74, 0x61, 0x74, 0x69, 0x63, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, - 0x3b, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x50, 0x61, 0x73, 0x73, 0x70, 0x68, 0x72, 0x61, 0x73, 0x65, 0x48, 0x6d, 0x61, 0x63, 0x22, 0xaa, + 0x04, 0x0a, 0x0e, 0x4a, 0x73, 0x6f, 0x6e, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, + 0x6c, 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, 0x19, 0x0a, 0x08, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x06, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x07, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x49, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x76, + 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x76, 0x65, + 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x37, 0x0a, 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x18, + 0x08, 0x20, 0x01, 0x28, 0x0c, 0x42, 0x1f, 0xc2, 0xdd, 0x29, 0x1b, 0x0a, 0x06, 0x4f, 0x62, 0x6a, + 0x65, 0x63, 0x74, 0x12, 0x11, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, + 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x52, 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x12, 0x29, + 0x0a, 0x10, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, + 0x65, 0x64, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0f, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, + 0x45, 0x6e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x65, 0x64, 0x12, 0x49, 0x0a, 0x0b, 0x6f, 0x62, 0x6a, + 0x65, 0x63, 0x74, 0x5f, 0x68, 0x6d, 0x61, 0x63, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0c, 0x42, 0x28, + 0xc2, 0xdd, 0x29, 0x24, 0x0a, 0x0a, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x48, 0x6d, 0x61, 0x63, + 0x12, 0x16, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x6f, 0x62, 0x6a, + 0x65, 0x63, 0x74, 0x5f, 0x68, 0x6d, 0x61, 0x63, 0x52, 0x0a, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, + 0x48, 0x6d, 0x61, 0x63, 0x12, 0x15, 0x0a, 0x06, 0x6b, 0x65, 0x79, 0x5f, 0x69, 0x64, 0x18, 0x0b, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x6b, 0x65, 0x79, 0x49, 0x64, 0x42, 0x46, 0x5a, 0x44, 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, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, + 0x2f, 0x73, 0x74, 0x61, 0x74, 0x69, 0x63, 0x2f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x3b, 0x73, 0x74, + 0x6f, 0x72, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -659,25 +848,28 @@ func file_controller_storage_credential_static_store_v1_static_proto_rawDescGZIP return file_controller_storage_credential_static_store_v1_static_proto_rawDescData } -var file_controller_storage_credential_static_store_v1_static_proto_msgTypes = make([]protoimpl.MessageInfo, 3) +var file_controller_storage_credential_static_store_v1_static_proto_msgTypes = make([]protoimpl.MessageInfo, 4) var file_controller_storage_credential_static_store_v1_static_proto_goTypes = []interface{}{ (*CredentialStore)(nil), // 0: controller.storage.credential.static.store.v1.CredentialStore (*UsernamePasswordCredential)(nil), // 1: controller.storage.credential.static.store.v1.UsernamePasswordCredential (*SshPrivateKeyCredential)(nil), // 2: controller.storage.credential.static.store.v1.SshPrivateKeyCredential - (*timestamp.Timestamp)(nil), // 3: controller.storage.timestamp.v1.Timestamp + (*JsonCredential)(nil), // 3: controller.storage.credential.static.store.v1.JsonCredential + (*timestamp.Timestamp)(nil), // 4: controller.storage.timestamp.v1.Timestamp } var file_controller_storage_credential_static_store_v1_static_proto_depIdxs = []int32{ - 3, // 0: controller.storage.credential.static.store.v1.CredentialStore.create_time:type_name -> controller.storage.timestamp.v1.Timestamp - 3, // 1: controller.storage.credential.static.store.v1.CredentialStore.update_time:type_name -> controller.storage.timestamp.v1.Timestamp - 3, // 2: controller.storage.credential.static.store.v1.UsernamePasswordCredential.create_time:type_name -> controller.storage.timestamp.v1.Timestamp - 3, // 3: controller.storage.credential.static.store.v1.UsernamePasswordCredential.update_time:type_name -> controller.storage.timestamp.v1.Timestamp - 3, // 4: controller.storage.credential.static.store.v1.SshPrivateKeyCredential.create_time:type_name -> controller.storage.timestamp.v1.Timestamp - 3, // 5: controller.storage.credential.static.store.v1.SshPrivateKeyCredential.update_time:type_name -> controller.storage.timestamp.v1.Timestamp - 6, // [6:6] is the sub-list for method output_type - 6, // [6:6] is the sub-list for method input_type - 6, // [6:6] is the sub-list for extension type_name - 6, // [6:6] is the sub-list for extension extendee - 0, // [0:6] is the sub-list for field type_name + 4, // 0: controller.storage.credential.static.store.v1.CredentialStore.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 4, // 1: controller.storage.credential.static.store.v1.CredentialStore.update_time:type_name -> controller.storage.timestamp.v1.Timestamp + 4, // 2: controller.storage.credential.static.store.v1.UsernamePasswordCredential.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 4, // 3: controller.storage.credential.static.store.v1.UsernamePasswordCredential.update_time:type_name -> controller.storage.timestamp.v1.Timestamp + 4, // 4: controller.storage.credential.static.store.v1.SshPrivateKeyCredential.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 4, // 5: controller.storage.credential.static.store.v1.SshPrivateKeyCredential.update_time:type_name -> controller.storage.timestamp.v1.Timestamp + 4, // 6: controller.storage.credential.static.store.v1.JsonCredential.create_time:type_name -> controller.storage.timestamp.v1.Timestamp + 4, // 7: controller.storage.credential.static.store.v1.JsonCredential.update_time:type_name -> controller.storage.timestamp.v1.Timestamp + 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_storage_credential_static_store_v1_static_proto_init() } @@ -722,6 +914,18 @@ func file_controller_storage_credential_static_store_v1_static_proto_init() { return nil } } + file_controller_storage_credential_static_store_v1_static_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*JsonCredential); 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{ @@ -729,7 +933,7 @@ func file_controller_storage_credential_static_store_v1_static_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_controller_storage_credential_static_store_v1_static_proto_rawDesc, NumEnums: 0, - NumMessages: 3, + NumMessages: 4, NumExtensions: 0, NumServices: 0, }, diff --git a/internal/credential/static/testing.go b/internal/credential/static/testing.go index 9a08fc010..3dac0ab9c 100644 --- a/internal/credential/static/testing.go +++ b/internal/credential/static/testing.go @@ -2,6 +2,7 @@ package static import ( "context" + "encoding/json" "testing" "github.com/hashicorp/boundary/internal/credential" @@ -10,6 +11,7 @@ import ( wrapping "github.com/hashicorp/go-kms-wrapping/v2" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/structpb" ) const ( @@ -64,6 +66,21 @@ do/lpv8N1+5Eb3lOB3DrqcEqRwXzSQcO2QcpikNSHyPquGR689I3xUm6kWmpKs49aacTUx ` ) +// TestJsonObject returns a json object and it's marshalled format to be used for testing +func TestJsonObject() (credential.JsonObject, []byte, error) { + object := credential.JsonObject{ + structpb.Struct{ + Fields: map[string]*structpb.Value{ + "username": structpb.NewStringValue("user"), + "password": structpb.NewStringValue("password"), + "hash": structpb.NewStringValue("1234567890"), + }, + }, + } + b, err := json.Marshal(object.AsMap()) + return object, b, err +} + // TestCredentialStore creates a static credential store in the provided DB with // the provided project id and any values passed in through the Options vars. // If any errors are encountered during the creation of the store, the test will fail. @@ -262,3 +279,76 @@ func TestSshPrivateKeyCredentials( } return creds } + +// TestJsonCredential creates a json credential in the +// provided DB with the provided scope and any values passed in. If any +// errors are encountered during the creation of the store, the test will fail. +func TestJsonCredential( + t testing.TB, + conn *db.DB, + wrapper wrapping.Wrapper, + storeId, scopeId string, + object credential.JsonObject, + opt ...Option, +) *JsonCredential { + t.Helper() + ctx := context.Background() + kmsCache := kms.TestKms(t, conn, wrapper) + w := db.New(conn) + + opts := getOpts(opt...) + + databaseWrapper, err := kmsCache.GetWrapper(ctx, scopeId, kms.KeyPurposeDatabase) + assert.NoError(t, err) + require.NotNil(t, databaseWrapper) + + cred, err := NewJsonCredential(ctx, storeId, object, opt...) + require.NoError(t, err) + require.NotNil(t, cred) + + id := opts.withPublicId + if id == "" { + id, err = credential.NewJsonCredentialId(ctx) + require.NoError(t, err) + } + cred.PublicId = id + + err = cred.encrypt(ctx, databaseWrapper) + require.NoError(t, err) + + _, err2 := w.DoTx(ctx, db.StdRetryCnt, db.ExpBackoff{}, + func(_ db.Reader, iw db.Writer) error { + require.NoError(t, iw.Create(ctx, cred)) + return nil + }, + ) + require.NoError(t, err2) + + return cred +} + +// TestJsonCredentials creates count number of json +// credentials in the provided DB with the provided scope id. If any errors are +// encountered during the creation of the credentials, the test will fail. +func TestJsonCredentials( + t testing.TB, + conn *db.DB, + wrapper wrapping.Wrapper, + storeId, scopeId string, + object credential.JsonObject, + count int, +) []*JsonCredential { + t.Helper() + ctx := context.Background() + kmsCache := kms.TestKms(t, conn, wrapper) + + databaseWrapper, err := kmsCache.GetWrapper(ctx, scopeId, kms.KeyPurposeDatabase) + assert.NoError(t, err) + require.NotNil(t, databaseWrapper) + + creds := make([]*JsonCredential, 0, count) + for i := 0; i < count; i++ { + creds = append(creds, TestJsonCredential(t, conn, wrapper, storeId, scopeId, object)) + } + return creds +} diff --git a/internal/credential/static/testing_test.go b/internal/credential/static/testing_test.go index e76956da6..55cda86a3 100644 --- a/internal/credential/static/testing_test.go +++ b/internal/credential/static/testing_test.go @@ -85,3 +85,52 @@ func Test_TestUsernamePasswordCredentials(t *testing.T) { creds := TestUsernamePasswordCredentials(t, conn, wrapper, "user", "pass", store.GetPublicId(), prj.GetPublicId(), count) assert.Len(creds, count) } + +func Test_TestJsonCredential(t *testing.T) { + t.Parallel() + assert, require := assert.New(t), require.New(t) + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + kkms := kms.TestKms(t, conn, wrapper) + _, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + require.NotNil(prj) + assert.NotEmpty(prj.GetPublicId()) + + store := TestCredentialStore(t, conn, wrapper, prj.GetPublicId()) + + obj, objBytes, err := TestJsonObject() + assert.NoError(err) + + cred := TestJsonCredential(t, conn, wrapper, store.GetPublicId(), prj.GetPublicId(), obj, WithName("my-name"), WithDescription("my-description")) + require.NotNil(cred) + assert.NotEmpty(cred.GetPublicId()) + assert.Equal(cred.Name, "my-name") + assert.Equal(cred.Description, "my-description") + assert.Equal(cred.Object, objBytes) + + // Validate hmac + databaseWrapper, err := kkms.GetWrapper(context.Background(), prj.PublicId, kms.KeyPurposeDatabase) + require.NoError(err) + hm, err := crypto.HmacSha256(context.Background(), cred.Object, databaseWrapper, []byte(cred.StoreId), nil) + require.NoError(err) + assert.Equal([]byte(hm), cred.ObjectHmac) +} + +func Test_TestJsonCredentials(t *testing.T) { + t.Parallel() + assert, require := assert.New(t), require.New(t) + conn, _ := db.TestSetup(t, "postgres") + wrapper := db.TestWrapper(t) + _, prj := iam.TestScopes(t, iam.TestRepo(t, conn, wrapper)) + require.NotNil(prj) + assert.NotEmpty(prj.GetPublicId()) + + store := TestCredentialStore(t, conn, wrapper, prj.GetPublicId()) + + obj, _, err := TestJsonObject() + assert.NoError(err) + + count := 3 + creds := TestJsonCredentials(t, conn, wrapper, store.GetPublicId(), prj.GetPublicId(), obj, count) + assert.Len(creds, count) +} diff --git a/internal/daemon/controller/handlers/credentials/credential_service.go b/internal/daemon/controller/handlers/credentials/credential_service.go index 0211158f5..a9620ad3d 100644 --- a/internal/daemon/controller/handlers/credentials/credential_service.go +++ b/internal/daemon/controller/handlers/credentials/credential_service.go @@ -4,6 +4,7 @@ import ( "context" "crypto/x509" "encoding/base64" + "encoding/json" "fmt" "github.com/hashicorp/boundary/globals" @@ -24,6 +25,7 @@ import ( "golang.org/x/crypto/ssh" "google.golang.org/grpc/codes" "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/wrapperspb" ) @@ -32,12 +34,14 @@ const ( passwordField = "attributes.password" privateKeyField = "attributes.private_key" privateKeyPassphraseField = "attributes.private_key_passphrase" + objectField = "attributes.object" domain = "credential" ) var ( - upMaskManager handlers.MaskManager - spkMaskManager handlers.MaskManager + upMaskManager handlers.MaskManager + spkMaskManager handlers.MaskManager + jsonMaskManager handlers.MaskManager // IdActions contains the set of actions that can be performed on // individual resources @@ -66,6 +70,10 @@ func init() { handlers.MaskSource{&pb.Credential{}, &pb.SshPrivateKeyAttributes{}}); err != nil { panic(err) } + if jsonMaskManager, err = handlers.NewMaskManager(handlers.MaskDestination{&store.JsonCredential{}}, + handlers.MaskSource{&pb.Credential{}, &pb.JsonAttributes{}}); err != nil { + panic(err) + } } // Service handles request as described by the pbs.CredentialServiceServer interface. @@ -360,6 +368,23 @@ func (s Service) createInRepo(ctx context.Context, scopeId string, item *pb.Cred return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to create credential but no error returned from repository.") } return out, nil + case credential.JsonSubtype.String(): + cred, err := toJsonStorageCredential(ctx, item.GetCredentialStoreId(), item) + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + repo, err := s.repoFn() + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + out, err := repo.CreateJsonCredential(ctx, scopeId, cred) + if err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to create credential")) + } + if out == nil { + return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "Unable to create credential but no error returned from repository.") + } + return out, nil default: return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, fmt.Sprintf("Unsupported credential type %q", item.GetType())) } @@ -435,6 +460,30 @@ func (s Service) updateInRepo( } return out, nil + case credential.JsonSubtype: + dbMasks = append(dbMasks, jsonMaskManager.Translate(masks, "attributes", "object")...) + if len(dbMasks) == 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."}) + } + + cred, err := toJsonStorageCredential(ctx, storeId, in) + if err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to convert to json storage credential")) + } + cred.PublicId = id + repo, err := s.repoFn() + if err != nil { + return nil, errors.Wrap(ctx, err, op) + } + out, rowsUpdated, err := repo.UpdateJsonCredential(ctx, scopeId, cred, item.GetVersion(), dbMasks) + if err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to update credential")) + } + if rowsUpdated == 0 { + return nil, handlers.NotFoundErrorf("Credential %q doesn't exist or incorrect version provided.", id) + } + return out, nil + default: return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, fmt.Sprintf("Unsupported credential type %q", item.GetType())) @@ -527,6 +576,8 @@ func toProto(in credential.Static, opt ...handlers.Option) (*pb.Credential, erro out.Type = credential.UsernamePasswordSubtype.String() case *static.SshPrivateKeyCredential: out.Type = credential.SshPrivateKeySubtype.String() + case *static.JsonCredential: + out.Type = credential.JsonSubtype.String() } } if outputFields.Has(globals.DescriptionField) && in.GetDescription() != "" { @@ -571,6 +622,14 @@ func toProto(in credential.Static, opt ...handlers.Option) (*pb.Credential, erro }, } } + case *static.JsonCredential: + if outputFields.Has(globals.AttributesField) { + out.Attrs = &pb.Credential_JsonAttributes{ + JsonAttributes: &pb.JsonAttributes{ + ObjectHmac: base64.RawURLEncoding.EncodeToString(cred.GetObjectHmac()), + }, + } + } } return &out, nil } @@ -625,6 +684,38 @@ func toSshPrivateKeyStorageCredential(ctx context.Context, storeId string, in *p return cs, err } +func toJsonStorageCredential(ctx context.Context, storeId string, in *pb.Credential) (out *static.JsonCredential, err error) { + const op = "credentials.toJsonStorageCredential" + var opts []static.Option + if in.GetName() != nil { + opts = append(opts, static.WithName(in.GetName().GetValue())) + } + if in.GetDescription() != nil { + opts = append(opts, static.WithDescription(in.GetDescription().GetValue())) + } + + attrs := in.GetJsonAttributes() + object := attrs.GetObject() + if object == nil { + object, err = structpb.NewStruct(map[string]interface{}{}) + if err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to build credential")) + } + } + cs, err := static.NewJsonCredential( + ctx, + storeId, + credential.JsonObject{ + *object, + }, + opts...) + if err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to build credential")) + } + + return cs, err +} + // 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 @@ -637,6 +728,7 @@ func validateGetRequest(req *pbs.GetCredentialRequest) error { credential.UsernamePasswordCredentialPrefix, credential.PreviousUsernamePasswordCredentialPrefix, credential.SshPrivateKeyCredentialPrefix, + credential.JsonCredentialPrefix, ) } @@ -685,6 +777,18 @@ func validateCreateRequest(req *pbs.CreateCredentialRequest) error { } } + case credential.JsonSubtype.String(): + object := req.GetItem().GetJsonAttributes().GetObject() + if object == nil && len(object.AsMap()) <= 0 { + badFields[objectField] = "Field required for creating a json credential." + } + objectBytes, err := json.Marshal(object) + if err != nil { + badFields[objectField] = "Unable to parse given json value" + } else if len(objectBytes) <= 0 { + badFields[objectField] = "Field required for creating a json credential." + } + default: badFields[globals.TypeField] = fmt.Sprintf("Unsupported credential type %q", req.Item.GetType()) } @@ -738,6 +842,21 @@ func validateUpdateRequest(req *pbs.UpdateCredentialRequest) error { } } + case credential.JsonSubtype: + object := req.GetItem().GetJsonAttributes().GetObject() + if object != nil { + objectBytes, err := json.Marshal(object.AsMap()) + if err != nil { + badFields[objectField] = "Unable to parse given json value" + } + if handlers.MaskContains(req.GetUpdateMask().GetPaths(), objectField) && len(objectBytes) <= 0 { + badFields[objectField] = "This is a required field and cannot be set to empty." + } + } + if handlers.MaskContains(req.GetUpdateMask().GetPaths(), objectField) && object == nil { + badFields[objectField] = "This is a required field and cannot be set to empty." + } + default: badFields[globals.IdField] = "Unknown credential type." } @@ -747,6 +866,7 @@ func validateUpdateRequest(req *pbs.UpdateCredentialRequest) error { credential.UsernamePasswordCredentialPrefix, credential.PreviousUsernamePasswordCredentialPrefix, credential.SshPrivateKeyCredentialPrefix, + credential.JsonCredentialPrefix, ) } @@ -757,6 +877,7 @@ func validateDeleteRequest(req *pbs.DeleteCredentialRequest) error { credential.UsernamePasswordCredentialPrefix, credential.PreviousUsernamePasswordCredentialPrefix, credential.SshPrivateKeyCredentialPrefix, + credential.JsonCredentialPrefix, ) } diff --git a/internal/daemon/controller/handlers/credentials/credential_service_test.go b/internal/daemon/controller/handlers/credentials/credential_service_test.go index b7e402513..85c7fc6d9 100644 --- a/internal/daemon/controller/handlers/credentials/credential_service_test.go +++ b/internal/daemon/controller/handlers/credentials/credential_service_test.go @@ -3,6 +3,7 @@ package credentials import ( "context" "encoding/base64" + "encoding/json" "fmt" "strings" "testing" @@ -30,6 +31,7 @@ import ( "google.golang.org/protobuf/proto" "google.golang.org/protobuf/testing/protocmp" "google.golang.org/protobuf/types/known/fieldmaskpb" + "google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/timestamppb" "google.golang.org/protobuf/types/known/wrapperspb" ) @@ -100,6 +102,28 @@ func TestList(t *testing.T) { }, }, }) + + obj, objBytes, err := static.TestJsonObject() + assert.NoError(t, err) + + credJson := static.TestJsonCredential(t, conn, wrapper, store.GetPublicId(), prj.GetPublicId(), obj) + hm, err = crypto.HmacSha256(context.Background(), objBytes, databaseWrapper, []byte(store.GetPublicId()), nil) + require.NoError(t, err) + wantCreds = append(wantCreds, &pb.Credential{ + Id: credJson.GetPublicId(), + CredentialStoreId: store.GetPublicId(), + Scope: &scopepb.ScopeInfo{Id: prj.GetPublicId(), Type: scope.Project.String(), ParentScopeId: prj.GetParentId()}, + CreatedTime: credJson.GetCreateTime().GetTimestamp(), + UpdatedTime: credJson.GetUpdateTime().GetTimestamp(), + Version: credJson.GetVersion(), + Type: credential.JsonSubtype.String(), + AuthorizedActions: testAuthorizedActions, + Attrs: &pb.Credential_JsonAttributes{ + JsonAttributes: &pb.JsonAttributes{ + ObjectHmac: base64.RawURLEncoding.EncodeToString([]byte(hm)), + }, + }, + }) } cases := []struct { @@ -129,8 +153,8 @@ func TestList(t *testing.T) { }, { name: "Filter on Attribute", - req: &pbs.ListCredentialsRequest{CredentialStoreId: store.GetPublicId(), Filter: fmt.Sprintf(`"/item/attributes/username"==%q`, wantCreds[2].GetUsernamePasswordAttributes().GetUsername().Value)}, - res: &pbs.ListCredentialsResponse{Items: wantCreds[2:4]}, + req: &pbs.ListCredentialsRequest{CredentialStoreId: store.GetPublicId(), Filter: fmt.Sprintf(`"/item/attributes/username"==%q`, wantCreds[3].GetUsernamePasswordAttributes().GetUsername().Value)}, + res: &pbs.ListCredentialsResponse{Items: wantCreds[3:5]}, anonRes: &pbs.ListCredentialsResponse{}, // anonymous user does not have access to attributes }, { @@ -213,6 +237,12 @@ func TestGet(t *testing.T) { spkWithPassHm, err := crypto.HmacSha256(context.Background(), []byte(testdata.PEMEncryptedKeys[0].PEMBytes), databaseWrapper, []byte(store.GetPublicId()), nil) require.NoError(t, err) passHm, err := crypto.HmacSha256(context.Background(), []byte(testdata.PEMEncryptedKeys[0].EncryptionKey), databaseWrapper, []byte(store.GetPublicId()), nil) + + obj, objBytes, err := static.TestJsonObject() + assert.NoError(t, err) + + jsonCred := static.TestJsonCredential(t, conn, wrapper, store.GetPublicId(), prj.GetPublicId(), obj) + objectHmac, err := crypto.HmacSha256(context.Background(), objBytes, databaseWrapper, []byte(store.GetPublicId()), nil) require.NoError(t, err) cases := []struct { @@ -310,6 +340,27 @@ func TestGet(t *testing.T) { }, }, }, + { + name: "success-json", + id: jsonCred.GetPublicId(), + res: &pbs.GetCredentialResponse{ + Item: &pb.Credential{ + Id: jsonCred.GetPublicId(), + CredentialStoreId: jsonCred.GetStoreId(), + Scope: &scopepb.ScopeInfo{Id: store.GetProjectId(), Type: scope.Project.String(), ParentScopeId: prj.GetParentId()}, + Type: credential.JsonSubtype.String(), + AuthorizedActions: testAuthorizedActions, + CreatedTime: jsonCred.CreateTime.GetTimestamp(), + UpdatedTime: jsonCred.UpdateTime.GetTimestamp(), + Version: 1, + Attrs: &pb.Credential_JsonAttributes{ + JsonAttributes: &pb.JsonAttributes{ + ObjectHmac: base64.RawURLEncoding.EncodeToString([]byte(objectHmac)), + }, + }, + }, + }, + }, { name: "not found error", id: fmt.Sprintf("%s_1234567890", credential.UsernamePasswordCredentialPrefix), @@ -370,6 +421,11 @@ func TestDelete(t *testing.T) { upCred := static.TestUsernamePasswordCredential(t, conn, wrapper, "user", "pass", store.GetPublicId(), prj.GetPublicId()) spkCred := static.TestSshPrivateKeyCredential(t, conn, wrapper, "user", static.TestSshPrivateKeyPem, store.GetPublicId(), prj.GetPublicId()) + obj, _, err := static.TestJsonObject() + assert.NoError(t, err) + + jsonCred := static.TestJsonCredential(t, conn, wrapper, store.GetPublicId(), prj.GetPublicId(), obj) + cases := []struct { name string id string @@ -384,6 +440,10 @@ func TestDelete(t *testing.T) { name: "success-spk", id: spkCred.GetPublicId(), }, + { + name: "success-json", + id: jsonCred.GetPublicId(), + }, { name: "not found error", id: fmt.Sprintf("%s_1234567890", credential.UsernamePasswordCredentialPrefix), @@ -429,6 +489,9 @@ func TestCreate(t *testing.T) { _, prj := iam.TestScopes(t, iamRepo) store := static.TestCredentialStore(t, conn, wrapper, prj.GetPublicId()) + obj, objBytes, err := static.TestJsonObject() + assert.NoError(t, err) + cases := []struct { name string req *pbs.CreateCredentialRequest @@ -555,6 +618,18 @@ func TestCreate(t *testing.T) { res: nil, err: handlers.ApiErrorWithCode(codes.InvalidArgument), }, + { + name: "Must provide json secret", + req: &pbs.CreateCredentialRequest{Item: &pb.Credential{ + CredentialStoreId: store.GetPublicId(), + Type: credential.JsonSubtype.String(), + Attrs: &pb.Credential_JsonAttributes{ + JsonAttributes: &pb.JsonAttributes{}, + }, + }}, + res: nil, + err: handlers.ApiErrorWithCode(codes.InvalidArgument), + }, { name: "valid-up", req: &pbs.CreateCredentialRequest{Item: &pb.Credential{ @@ -637,6 +712,32 @@ func TestCreate(t *testing.T) { }, }, }, + { + name: "valid-json", + req: &pbs.CreateCredentialRequest{Item: &pb.Credential{ + CredentialStoreId: store.GetPublicId(), + Type: credential.JsonSubtype.String(), + Attrs: &pb.Credential_JsonAttributes{ + JsonAttributes: &pb.JsonAttributes{ + Object: &obj.Struct, + }, + }, + }}, + idPrefix: credential.JsonCredentialPrefix + "_", + res: &pbs.CreateCredentialResponse{ + Uri: fmt.Sprintf("credentials/%s_", credential.JsonCredentialPrefix), + Item: &pb.Credential{ + Id: store.GetPublicId(), + CredentialStoreId: store.GetPublicId(), + CreatedTime: store.GetCreateTime().GetTimestamp(), + UpdatedTime: store.GetUpdateTime().GetTimestamp(), + Scope: &scopepb.ScopeInfo{Id: prj.GetPublicId(), Type: prj.GetType(), ParentScopeId: prj.GetParentId()}, + Version: 1, + Type: credential.JsonSubtype.String(), + AuthorizedActions: testAuthorizedActions, + }, + }, + }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { @@ -702,6 +803,14 @@ func TestCreate(t *testing.T) { assert.Empty(got.GetItem().GetSshPrivateKeyAttributes().GetPrivateKeyPassphrase()) } + case credential.JsonSubtype.String(): + hm, err := crypto.HmacSha256(context.Background(), objBytes, databaseWrapper, []byte(store.GetPublicId()), nil) + require.NoError(err) + + // Validate attributes equal + assert.Equal(base64.RawURLEncoding.EncodeToString([]byte(hm)), got.GetItem().GetJsonAttributes().GetObjectHmac()) + assert.Empty(got.GetItem().GetJsonAttributes().GetObject()) + default: require.Fail("unknown type") } @@ -775,6 +884,28 @@ func TestUpdate(t *testing.T) { return cred, clean } + freshCredJson := func() (*static.JsonCredential, func()) { + t.Helper() + obj, _, err := static.TestJsonObject() + assert.NoError(t, err) + cred := static.TestJsonCredential(t, conn, wrapper, store.GetPublicId(), prj.GetPublicId(), obj) + clean := func() { + _, err := s.DeleteCredential(ctx, &pbs.DeleteCredentialRequest{Id: cred.GetPublicId()}) + require.NoError(t, err) + } + return cred, clean + } + + secondSecret := &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "username": structpb.NewStringValue("new-user"), + "password": structpb.NewStringValue("new-password"), + "hash": structpb.NewStringValue("0123456789"), + }, + } + secondSecretBytes, err := json.Marshal(secondSecret) + require.NoError(t, err) + databaseWrapper, err := kkms.GetWrapper(context.Background(), prj.GetPublicId(), kms.KeyPurposeDatabase) require.NoError(t, err) @@ -994,6 +1125,27 @@ func TestUpdate(t *testing.T) { return out }, }, + { + name: "update-json", + req: &pbs.UpdateCredentialRequest{ + UpdateMask: fieldmask("attributes.object.username", "attributes.object.password", "attributes.object.hash"), + Item: &pb.Credential{ + Attrs: &pb.Credential_JsonAttributes{ + JsonAttributes: &pb.JsonAttributes{ + Object: secondSecret, + }, + }, + }, + }, + res: func(in *pb.Credential) *pb.Credential { + out := proto.Clone(in).(*pb.Credential) + + hm, err := crypto.HmacSha256(context.Background(), secondSecretBytes, databaseWrapper, []byte(store.GetPublicId()), nil) + require.NoError(t, err) + out.GetJsonAttributes().ObjectHmac = base64.RawURLEncoding.EncodeToString([]byte(hm)) + return out + }, + }, { name: "update-spk-with-bad-passphrase", req: &pbs.UpdateCredentialRequest{ @@ -1043,6 +1195,7 @@ func TestUpdate(t *testing.T) { require.NoError(t, err) out.GetSshPrivateKeyAttributes().PrivateKeyHmac = base64.RawURLEncoding.EncodeToString([]byte(hm)) out.GetSshPrivateKeyAttributes().PrivateKeyPassphraseHmac = "" + return out }, }, @@ -1053,10 +1206,12 @@ func TestUpdate(t *testing.T) { assert, require := assert.New(t), require.New(t) var cred credential.Static var cleanup func() - switch { - case strings.Contains(tc.name, "spk"): + + if strings.Contains(tc.name, "spk") { cred, cleanup = freshCredSpk("user") - default: + } else if strings.Contains(tc.name, "json") { + cred, cleanup = freshCredJson() + } else { cred, cleanup = freshCredUp("user", "pass") } defer cleanup() @@ -1095,8 +1250,12 @@ func TestUpdate(t *testing.T) { } // cant update read only fields - cred, cleanupKv := freshCredUp("user", "pass") - defer cleanupKv() + credJson, cleanupJson := freshCredJson() + defer cleanupJson() + + // cant update read only fields + credUp, cleanUp := freshCredUp("user", "pass") + defer cleanUp() newStore := static.TestCredentialStore(t, conn, wrapper, prj.GetPublicId()) @@ -1129,11 +1288,11 @@ func TestUpdate(t *testing.T) { for _, tc := range roCases { t.Run(fmt.Sprintf("ReadOnlyField/%s", tc.path), func(t *testing.T) { req := &pbs.UpdateCredentialRequest{ - Id: cred.GetPublicId(), + Id: credUp.GetPublicId(), Item: tc.item, UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{tc.path}}, } - req.Item.Version = cred.Version + req.Item.Version = credUp.Version got, gErr := s.UpdateCredential(ctx, req) assert.Error(t, gErr) @@ -1145,6 +1304,24 @@ func TestUpdate(t *testing.T) { } matcher(t, gErr) assert.Nil(t, got) + + req = &pbs.UpdateCredentialRequest{ + Id: credJson.GetPublicId(), + Item: tc.item, + UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{tc.path}}, + } + req.Item.Version = credJson.Version + + got, gErr = s.UpdateCredential(ctx, req) + assert.Error(t, gErr) + matcher = tc.matcher + if matcher == nil { + matcher = func(t *testing.T, e error) { + assert.Truef(t, errors.Is(gErr, handlers.ApiErrorWithCode(codes.InvalidArgument)), "got error %v, wanted invalid argument", gErr) + } + } + matcher(t, gErr) + assert.Nil(t, got) }) } } diff --git a/internal/daemon/controller/handlers/targets/credentials.go b/internal/daemon/controller/handlers/targets/credentials.go index c42499d1a..eb1034ec4 100644 --- a/internal/daemon/controller/handlers/targets/credentials.go +++ b/internal/daemon/controller/handlers/targets/credentials.go @@ -168,7 +168,7 @@ func staticToWorkerCredential(ctx context.Context, cred credential.Static) (sess data, err := proto.Marshal(workerCred) if err != nil { - return nil, errors.Wrap(ctx, err, op, errors.WithMsg("marshalling dynamic secret to proto")) + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("marshalling static secret to proto")) } return data, nil } @@ -218,6 +218,21 @@ func staticToSessionCredential(ctx context.Context, cred credential.Static) (*pb return nil, errors.Wrap(ctx, err, op, errors.WithMsg("creating proto struct for ssh private key credential")) } + case *credstatic.JsonCredential: + var err error + credType = string(credential.JsonType) + object := map[string]interface{}{} + err = json.Unmarshal(c.GetObject(), &object) + if err != nil { + return nil, errors.New(ctx, errors.InvalidParameter, op, "unmarshalling json") + } + + credData, err = structpb.NewStruct(object) + if err != nil { + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("creating proto struct for json credential")) + } + secret = object + default: return nil, errors.New(ctx, errors.InvalidParameter, op, fmt.Sprintf("unsupported credential %T", c)) } diff --git a/internal/daemon/controller/handlers/targets/target_service.go b/internal/daemon/controller/handlers/targets/target_service.go index 0829c5e99..09733c7c2 100644 --- a/internal/daemon/controller/handlers/targets/target_service.go +++ b/internal/daemon/controller/handlers/targets/target_service.go @@ -1624,7 +1624,8 @@ func validateAddCredentialSourcesRequest(req *pbs.AddTargetCredentialSourcesRequ vault.CredentialLibraryPrefix, credential.UsernamePasswordCredentialPrefix, credential.PreviousUsernamePasswordCredentialPrefix, - credential.SshPrivateKeyCredentialPrefix) { + credential.SshPrivateKeyCredentialPrefix, + credential.JsonCredentialPrefix) { badFields[globals.ApplicationCredentialSourceIdsField] = fmt.Sprintf("Incorrectly formatted credential source identifier %q.", cl) break } @@ -1634,7 +1635,8 @@ func validateAddCredentialSourcesRequest(req *pbs.AddTargetCredentialSourcesRequ vault.CredentialLibraryPrefix, credential.UsernamePasswordCredentialPrefix, credential.PreviousUsernamePasswordCredentialPrefix, - credential.SshPrivateKeyCredentialPrefix) { + credential.SshPrivateKeyCredentialPrefix, + credential.JsonCredentialPrefix) { badFields[globals.BrokeredCredentialSourceIdsField] = fmt.Sprintf("Incorrectly formatted credential source identifier %q.", cl) break } @@ -1669,7 +1671,8 @@ func validateSetCredentialSourcesRequest(req *pbs.SetTargetCredentialSourcesRequ vault.CredentialLibraryPrefix, credential.UsernamePasswordCredentialPrefix, credential.PreviousUsernamePasswordCredentialPrefix, - credential.SshPrivateKeyCredentialPrefix) { + credential.SshPrivateKeyCredentialPrefix, + credential.JsonCredentialPrefix) { badFields[globals.ApplicationCredentialSourceIdsField] = fmt.Sprintf("Incorrectly formatted credential source identifier %q.", cl) break } @@ -1679,7 +1682,8 @@ func validateSetCredentialSourcesRequest(req *pbs.SetTargetCredentialSourcesRequ vault.CredentialLibraryPrefix, credential.UsernamePasswordCredentialPrefix, credential.PreviousUsernamePasswordCredentialPrefix, - credential.SshPrivateKeyCredentialPrefix) { + credential.SshPrivateKeyCredentialPrefix, + credential.JsonCredentialPrefix) { badFields[globals.BrokeredCredentialSourceIdsField] = fmt.Sprintf("Incorrectly formatted credential source identifier %q.", cl) break } @@ -1718,7 +1722,8 @@ func validateRemoveCredentialSourcesRequest(req *pbs.RemoveTargetCredentialSourc vault.CredentialLibraryPrefix, credential.UsernamePasswordCredentialPrefix, credential.PreviousUsernamePasswordCredentialPrefix, - credential.SshPrivateKeyCredentialPrefix) { + credential.SshPrivateKeyCredentialPrefix, + credential.JsonCredentialPrefix) { badFields[globals.ApplicationCredentialSourceIdsField] = fmt.Sprintf("Incorrectly formatted credential source identifier %q.", cl) break } @@ -1728,7 +1733,8 @@ func validateRemoveCredentialSourcesRequest(req *pbs.RemoveTargetCredentialSourc vault.CredentialLibraryPrefix, credential.UsernamePasswordCredentialPrefix, credential.PreviousUsernamePasswordCredentialPrefix, - credential.SshPrivateKeyCredentialPrefix) { + credential.SshPrivateKeyCredentialPrefix, + credential.JsonCredentialPrefix) { badFields[globals.BrokeredCredentialSourceIdsField] = fmt.Sprintf("Incorrectly formatted credential source identifier %q.", cl) break } @@ -1738,7 +1744,8 @@ func validateRemoveCredentialSourcesRequest(req *pbs.RemoveTargetCredentialSourc vault.CredentialLibraryPrefix, credential.UsernamePasswordCredentialPrefix, credential.PreviousUsernamePasswordCredentialPrefix, - credential.SshPrivateKeyCredentialPrefix) { + credential.SshPrivateKeyCredentialPrefix, + credential.JsonCredentialPrefix) { badFields[globals.InjectedApplicationCredentialSourceIdsField] = fmt.Sprintf("Incorrectly formatted credential source identifier %q.", cl) break } diff --git a/internal/db/schema/migrations/oss/postgres/54/01_static_json_creds.up.sql b/internal/db/schema/migrations/oss/postgres/54/01_static_json_creds.up.sql new file mode 100644 index 000000000..5c0ef66e8 --- /dev/null +++ b/internal/db/schema/migrations/oss/postgres/54/01_static_json_creds.up.sql @@ -0,0 +1,64 @@ +begin; + + create table credential_static_json_credential ( + public_id wt_public_id primary key, + store_id wt_public_id not null + constraint credential_static_store_fkey + references credential_static_store (public_id) + on delete cascade + on update cascade, + project_id wt_public_id not null, + name wt_name, + description wt_description, + create_time wt_timestamp, + update_time wt_timestamp, + version wt_version, + + object_encrypted bytea not null + constraint object_encrypted_must_not_be_empty + check(length(object_encrypted) > 0), + object_hmac bytea not null + constraint object_hmac_must_not_be_empty + check(length(object_hmac) > 0), + key_id kms_private_id not null + constraint kms_data_key_version_fkey + references kms_data_key_version (private_id) + on delete restrict + on update cascade, + constraint credential_static_fkey + foreign key (project_id, store_id, public_id) + references credential_static (project_id, store_id, public_id) + on delete cascade + on update cascade, + constraint credential_static_json_credential_store_id_name_uq + unique(store_id, name), + constraint credential_static_json_credential_store_id_public_id_uq + unique(store_id, public_id) + ); + comment on table credential_static_json_credential is + 'credential_static_json_credential is a table where each row is a resource that represents a static json credential. ' + 'It is a credential_static subtype and an aggregate root.'; + + create trigger update_version_column after update on credential_static_json_credential + for each row execute procedure update_version_column(); + + create trigger update_time_column before update on credential_static_json_credential + for each row execute procedure update_time_column(); + + create trigger default_create_time_column before insert on credential_static_json_credential + for each row execute procedure default_create_time(); + + create trigger immutable_columns before update on credential_static_json_credential + for each row execute procedure immutable_columns('public_id', 'store_id', 'project_id', 'create_time'); + + create trigger insert_credential_static_subtype before insert on credential_static_json_credential + for each row execute procedure insert_credential_static_subtype(); + + create trigger delete_credential_static_subtype after delete on credential_static_json_credential + for each row execute procedure delete_credential_static_subtype(); + + insert into oplog_ticket (name, version) + values + ('credential_static_json_credential', 1); + +commit; diff --git a/internal/db/sqltest/initdb.d/03_widgets_persona.sql b/internal/db/sqltest/initdb.d/03_widgets_persona.sql index 91685c50c..22a0ae253 100644 --- a/internal/db/sqltest/initdb.d/03_widgets_persona.sql +++ b/internal/db/sqltest/initdb.d/03_widgets_persona.sql @@ -416,6 +416,11 @@ begin; values ('cs______wup1', 'cred____wup1', 'widget static username password credential', 'None', 'b_user', 'encrypted_password', 'hmac-value', 'kdkv___widget'), ('cs______wup2', 'cred____wup2', 'widget static username password credential', 'None', 's_user', 'encrypted_password', 'hmac-value', 'kdkv___widget'); + + insert into credential_static_json_credential + (store_id, public_id, name, description, object_encrypted, object_hmac, key_id) + values + ('cs______wup1', 'cred____wjson1', 'widget static json credential', 'None', 'json_value'::bytea, 'hmac-value', 'kdkv___widget'); end; $$ language plpgsql; diff --git a/internal/db/sqltest/tests/target/target_static_credential.sql b/internal/db/sqltest/tests/target/target_static_credential.sql index 8dd88eaa0..508530f14 100644 --- a/internal/db/sqltest/tests/target/target_static_credential.sql +++ b/internal/db/sqltest/tests/target/target_static_credential.sql @@ -19,7 +19,8 @@ begin; insert into target_static_credential (project_id, target_id, credential_static_id, credential_purpose) values - ('p____bwidget', 'test______wb', 'cred____wup1', 'brokered'); + ('p____bwidget', 'test______wb', 'cred____wup1', 'brokered'), + ('p____bwidget', 'test______wb', 'cred____wjson1', 'brokered'); select lives_ok('insert_valid_target_static_credential', 'insert valid target_static_credential failed'); -- create a credential_static_store and target in a different project diff --git a/internal/proto/controller/api/resources/credentials/v1/credential.proto b/internal/proto/controller/api/resources/credentials/v1/credential.proto index 4410779a5..c1563f872 100644 --- a/internal/proto/controller/api/resources/credentials/v1/credential.proto +++ b/internal/proto/controller/api/resources/credentials/v1/credential.proto @@ -69,6 +69,11 @@ message Credential { (custom_options.v1.generate_sdk_option) = true, (custom_options.v1.subtype) = "ssh_private_key" ]; + JsonAttributes json_attributes = 103 [ + (google.api.field_visibility).restriction = "INTERNAL", + (custom_options.v1.generate_sdk_option) = true, + (custom_options.v1.subtype) = "json" + ]; } // Output only. The available actions on this resource for this user. @@ -155,3 +160,25 @@ message SshPrivateKeyAttributes { } ]; // @gotags: `class:"public"` } + +// The attributes of a JSON Credential. +message JsonAttributes { + // Input only. The secret associated with the credential. + google.protobuf.Struct object = 10 [ + json_name = "object", + (custom_options.v1.generate_sdk_option) = true, + (custom_options.v1.mask_mapping) = { + this: "attributes.object" + that: "Object" + } + ]; // @gotags: `class:"secret"` + + // Output only. The hmac value of the object. + string object_hmac = 20 [ + json_name = "object_hmac", + (custom_options.v1.mask_mapping) = { + this: "attributes.object_hmac" + that: "ObjectHmac" + } + ]; // @gotags: `class:"public"` +} diff --git a/internal/proto/controller/storage/credential/static/store/v1/static.proto b/internal/proto/controller/storage/credential/static/store/v1/static.proto index 06897c710..9a83c3dac 100644 --- a/internal/proto/controller/storage/credential/static/store/v1/static.proto +++ b/internal/proto/controller/storage/credential/static/store/v1/static.proto @@ -209,3 +209,66 @@ message SshPrivateKeyCredential { that: "attributes.private_key_passphrase_hmac" }]; } + +message JsonCredential { + // public_id is a surrogate key suitable for use in a public API. + // @inject_tag: `gorm:"primary_key"` + string public_id = 1; + + // create_time is set by the database. + // @inject_tag: `gorm:"default:current_timestamp"` + timestamp.v1.Timestamp create_time = 2; + + // update_time is set by the database. + // @inject_tag: `gorm:"default:current_timestamp"` + timestamp.v1.Timestamp update_time = 3; + + // 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" + }]; + + // description is optional. + // @inject_tag: `gorm:"default:null"` + string description = 5 [(custom_options.v1.mask_mapping) = { + this: "Description" + that: "description" + }]; + + // store_id of the owning static credential store. + // It must be set. + // @inject_tag: `gorm:"not_null"` + string store_id = 6; + + // version allows optimistic locking of the resource. + // @inject_tag: `gorm:"default:null"` + uint32 version = 7; + + // object is the plain-text of the json associated with the + // credential. We are not storing this plain-text object in the database. + // @inject_tag: `gorm:"-" wrapping:"pt,object"` + bytes object = 8 [(custom_options.v1.mask_mapping) = { + this: "Object" + that: "attributes.object" + }]; + + // object_encrypted is the ciphertext of the object. It is stored in + // the database. + // @inject_tag: `gorm:"column:object_encrypted;not_null" wrapping:"ct,object"` + bytes object_encrypted = 9; + + // object_hmac is a sha256-hmac of the unencrypted object. It is recalculated + // everytime the object is updated. + // @inject_tag: `gorm:"not_null"` + bytes object_hmac = 10 [(custom_options.v1.mask_mapping) = { + this: "ObjectHmac" + that: "attributes.object_hmac" + }]; + + // The key_id of the kms database key used for encrypting this entry. + // It must be set. + // @inject_tag: `gorm:"not_null"` + string key_id = 11; +} diff --git a/internal/tests/api/credentials/credentials_test.go b/internal/tests/api/credentials/credentials_test.go index 13d7ac065..b51b91b70 100644 --- a/internal/tests/api/credentials/credentials_test.go +++ b/internal/tests/api/credentials/credentials_test.go @@ -234,6 +234,102 @@ func TestCrudSpk(t *testing.T) { assert.EqualValues(http.StatusNotFound, apiErr.Response().StatusCode()) } +func TestCrudJson(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) + _, proj := iam.TestScopes(t, tc.IamRepo(), iam.WithUserId(token.UserId)) + + cs, err := credentialstores.NewClient(client).Create(tc.Context(), "static", proj.GetPublicId()) + require.NoError(err) + require.NotNil(cs) + + checkResource := func(step string, c *credentials.Credential, wantedName string, wantVersion uint32) { + assert.NotNil(c, "returned no resource", step) + assert.Equal(wantedName, c.Name, step) + assert.Equal(wantVersion, c.Version) + } + credClient := credentials.NewClient(client) + + obj := map[string]interface{}{ + "username": "admin", + "password": "pass", + } + cred, err := credClient.Create(tc.Context(), credential.JsonSubtype.String(), cs.Item.Id, credentials.WithName("foo"), credentials.WithJsonCredentialObject(obj)) + require.NoError(err) + require.NotNil(cred) + checkResource("create", cred.Item, "foo", 1) + + jsonAttributes, err := cred.GetItem().GetJsonAttributes() + require.NoError(err) + require.Nil(jsonAttributes.Object) + require.NotEmpty(jsonAttributes.ObjectHmac) + + sshAttributes, err := cred.GetItem().GetSshPrivateKeyAttributes() + require.Error(err) + require.Nil(sshAttributes) + + upAttributes, err := cred.GetItem().GetUsernamePasswordAttributes() + require.Error(err) + require.Nil(upAttributes) + + // Validate object hmac was set and object is not set + originalObjectHmac, ok := cred.GetItem().Attributes["object_hmac"].(string) + require.True(ok) + require.NotNil(originalObjectHmac) + object, ok := cred.GetItem().Attributes["object"].(string) + require.False(ok) + require.Empty(object) + + cred, err = credClient.Read(tc.Context(), cred.Item.Id) + require.NoError(err) + require.NotNil(cs) + checkResource("read", cred.Item, "foo", 1) + + // Validate object hmac was set and object is not set + objectHmac, ok := cred.GetItem().Attributes["object_hmac"].(string) + require.True(ok) + require.NotNil(objectHmac) + object, ok = cred.GetItem().Attributes["object"].(string) + require.False(ok) + require.Empty(object) + + cred, err = credClient.Update(tc.Context(), cred.Item.Id, cred.Item.Version, credentials.WithName("bar")) + require.NoError(err) + require.NotNil(cs) + checkResource("update", cred.Item, "bar", 2) + + cred, err = credClient.Update(tc.Context(), cred.Item.Id, cred.Item.Version, credentials.WithJsonCredentialObject(map[string]interface{}{ + "username": "not_admin", + "password": "not_password", + })) + require.NoError(err) + require.NotNil(cs) + checkResource("update", cred.Item, "bar", 3) + + // Validate secret hmac was set & is not the same as the original value & secret is not set + objectHmac, ok = cred.GetItem().Attributes["object_hmac"].(string) + require.True(ok) + require.NotNil(objectHmac) + require.NotEqual(originalObjectHmac, objectHmac) + object, ok = cred.GetItem().Attributes["secrets"].(string) + require.False(ok) + require.Empty(object) + + _, err = credClient.Delete(tc.Context(), cred.Item.Id) + assert.NoError(err) + + _, err = credClient.Delete(tc.Context(), cred.Item.Id) + require.Error(err) + apiErr := api.AsServerError(err) + assert.NotNil(apiErr) + assert.EqualValues(http.StatusNotFound, apiErr.Response().StatusCode()) +} + func TestErrors(t *testing.T) { assert, require := assert.New(t), require.New(t) tc := controller.NewTestController(t, nil) diff --git a/internal/tests/cli/boundary/_credentials.bash b/internal/tests/cli/boundary/_credentials.bash index fc740ed95..4a37b0aad 100644 --- a/internal/tests/cli/boundary/_credentials.bash +++ b/internal/tests/cli/boundary/_credentials.bash @@ -15,6 +15,18 @@ function create_username_password_credential() { -password env://BP } +function create_json_credential() { + local name=$1 + local sid=$2 + local args=$3 + + boundary credentials create json \ + -name $name \ + -description 'test json credential' \ + -credential-store-id $sid \ + $args +} + function read_credential() { boundary credentials read -id $1 -format json } diff --git a/internal/tests/cli/boundary/credentials.bats b/internal/tests/cli/boundary/credentials.bats index 1b4bc35a5..7b6b14b65 100644 --- a/internal/tests/cli/boundary/credentials.bats +++ b/internal/tests/cli/boundary/credentials.bats @@ -6,7 +6,8 @@ load _credential_stores load _credentials export NEW_STORE='credentials-test-store' -export NEW_CREDENTIAL='test' +export NEW_CREDENTIAL='test-up' +export NEW_JSON_CREDENTIAL='test-json' @test "boundary/login: can login as default user" { run login $DEFAULT_LOGIN @@ -27,6 +28,13 @@ export NEW_CREDENTIAL='test' [ "$status" -eq 0 ] } +@test "boundary/credentials: can create $NEW_JSON_CREDENTIAL credential in $NEW_STORE store" { + local csid=$(credential_store_id $NEW_STORE $DEFAULT_P_ID) + run create_json_credential $NEW_JSON_CREDENTIAL $csid '-string-kv username=admin -string-kv password=pass' + echo "$output" + [ "$status" -eq 0 ] +} + @test "boundary/credentials: can not create already created $NEW_CREDENTIAL credential" { local csid=$(credential_store_id $NEW_STORE $DEFAULT_P_ID) run create_username_password_credential $NEW_CREDENTIAL $csid 'username' 'password' @@ -34,6 +42,13 @@ export NEW_CREDENTIAL='test' [ "$status" -eq 1 ] } +@test "boundary/credentials: can not create already created $NEW_JSON_CREDENTIAL credential" { + local csid=$(credential_store_id $NEW_STORE $DEFAULT_P_ID) + run create_json_credential $NEW_JSON_CREDENTIAL $csid '-string-kv username=admin -string-kv password=pass' + echo "$output" + [ "$status" -eq 1 ] +} + @test "boundary/credentials: can read $NEW_CREDENTIAL credential" { local csid=$(credential_store_id $NEW_STORE $DEFAULT_P_ID) local cid=$(credential_id $NEW_CREDENTIAL $csid) @@ -42,6 +57,14 @@ export NEW_CREDENTIAL='test' [ "$status" -eq 0 ] } +@test "boundary/credentials: can read $NEW_JSON_CREDENTIAL credential" { + local csid=$(credential_store_id $NEW_STORE $DEFAULT_P_ID) + local cid=$(credential_id $NEW_JSON_CREDENTIAL $csid) + run read_credential $cid + echo "$output" + [ "$status" -eq 0 ] +} + @test "boundary/credentials: can delete $NEW_CREDENTIAL credential" { local csid=$(credential_store_id $NEW_STORE $DEFAULT_P_ID) local cid=$(credential_id $NEW_CREDENTIAL $csid) @@ -51,6 +74,43 @@ export NEW_CREDENTIAL='test' [ "$status" -eq 0 ] } +@test "boundary/credentials: can delete $NEW_JSON_CREDENTIAL credential" { + local csid=$(credential_store_id $NEW_STORE $DEFAULT_P_ID) + local cid=$(credential_id $NEW_JSON_CREDENTIAL $csid) + run delete_credential $cid + echo "$output" + run has_status_code "$output" "204" + [ "$status" -eq 0 ] +} + +@test "boundary/credentials: can not use object flag with kv flags for json credential" { + echo "{\"key\":\"value\"}" > cred_json_object + local object_file_path="file://$(pwd)/cred_json_object" + local csid=$(credential_store_id $NEW_STORE $DEFAULT_P_ID) + + local name='object & kv flag test' + run create_json_credential $name $csid "-object $object_file_path -kv password=pass" + echo "$output" + [ "$status" -eq 1 ] + + local name='object & bool-kv flag test' + run create_json_credential $name $csid "-object $object_file_path -bool-kv password=true" + echo "$output" + [ "$status" -eq 1 ] + + local name='object & num-kv flag test' + run create_json_credential $name $csid "-object $object_file_path -num-kv password=1234" + echo "$output" + [ "$status" -eq 1 ] + + local name='object & string-kv flag test' + run create_json_credential $name $csid "-object $object_file_path -string-kv password=pass" + echo "$output" + [ "$status" -eq 1 ] + + rm cred_json_object +} + @test "boundary/credential-stores: can not read deleted $NEW_CREDENTIAL credential" { local csid=$(credential_store_id $NEW_STORE $DEFAULT_P_ID) local cid=$(credential_id $NEW_CREDENTIAL $csid) @@ -72,4 +132,12 @@ export NEW_CREDENTIAL='test' run read_credential_store $csid echo "$output" [ "$status" -eq 1 ] +} + +@test "boundary/credential-stores: can not read deleted $NEW_JSON_CREDENTIAL credential" { + local csid=$(credential_store_id $NEW_STORE $DEFAULT_P_ID) + local cid=$(credential_id $NEW_JSON_CREDENTIAL $csid) + run read_credential $cid + echo "$output" + [ "$status" -eq 1 ] } \ No newline at end of file diff --git a/internal/tests/cli/boundary/target_credential_sources.bats b/internal/tests/cli/boundary/target_credential_sources.bats index 25ee31459..b559404b8 100644 --- a/internal/tests/cli/boundary/target_credential_sources.bats +++ b/internal/tests/cli/boundary/target_credential_sources.bats @@ -12,6 +12,7 @@ export NEW_STORE='test-for-add-credential-sources' export NEW_CREDENTIAL='first-credential' export NEW_CREDENTIAL1='second-credential' export NEW_CREDENTIAL2='third-credential' +export NEW_JSON_CREDENTIAL='json-credential' @test "boundary/login: can login as admin user" { run login $DEFAULT_LOGIN @@ -31,6 +32,13 @@ export NEW_CREDENTIAL2='third-credential' [ "$status" -eq 0 ] } +@test "boundary/credentials: can create $NEW_JSON_CREDENTIAL credential in $NEW_STORE store" { + local csid=$(credential_store_id $NEW_STORE $DEFAULT_P_ID) + run create_json_credential $NEW_JSON_CREDENTIAL $csid '-string-kv username=admin -string-kv password=pass' + echo "$output" + [ "$status" -eq 0 ] +} + @test "boundary/target: can add $NEW_CREDENTIAL credential source" { local csid=$(credential_store_id $NEW_STORE $DEFAULT_P_ID) local cid=$(credential_id $NEW_CREDENTIAL $csid) @@ -47,6 +55,26 @@ export NEW_CREDENTIAL2='third-credential' [ "$status" -eq 0 ] } +@test "boundary/target: can add $NEW_JSON_CREDENTIAL credential source" { + local csid=$(credential_store_id $NEW_STORE $DEFAULT_P_ID) + local cid=$(credential_id $NEW_JSON_CREDENTIAL $csid) + run add_target_brokered_credential_sources $DEFAULT_TARGET $cid + echo "$output" + [ "$status" -eq 0 ] +} + +@test "boundary/target: validate $NEW_CREDENTIAL & $NEW_JSON_CREDENTIAL credential source present" { + local csid=$(credential_store_id $NEW_STORE $DEFAULT_P_ID) + local credup_id=$(credential_id $NEW_CREDENTIAL $csid) + local credjson_id=$(credential_id $NEW_JSON_CREDENTIAL $csid) + run validate_credential_sources $DEFAULT_TARGET $credup_id + echo "$output" + [ "$status" -eq 0 ] + run validate_credential_sources $DEFAULT_TARGET $credjson_id + echo "$output" + [ "$status" -eq 0 ] +} + @test "boundary/target: cannot add duplicate credential source" { local csid=$(credential_store_id $NEW_STORE $DEFAULT_P_ID) local cid=$(credential_id $NEW_CREDENTIAL $csid) @@ -71,6 +99,22 @@ export NEW_CREDENTIAL2='third-credential' [ "$status" -eq 0 ] } +@test "boundary/target: can delete $NEW_JSON_CREDENTIAL credential source" { + local csid=$(credential_store_id $NEW_STORE $DEFAULT_P_ID) + local cid=$(credential_id $NEW_JSON_CREDENTIAL $csid) + run remove_target_brokered_credential_sources $DEFAULT_TARGET $cid + echo "$output" + [ "$status" -eq 0 ] +} + +@test "boundary/target: validate $NEW_JSON_CREDENTIAL credential source removed" { + local csid=$(credential_store_id $NEW_STORE $DEFAULT_P_ID) + local cid=$(credential_id $NEW_JSON_CREDENTIAL $csid) + run validate_credential_sources_not_present $DEFAULT_TARGET $cid + echo "$output" + [ "$status" -eq 0 ] +} + @test "boundary/credentials: can create additional credentials in $NEW_STORE store" { local csid=$(credential_store_id $NEW_STORE $DEFAULT_P_ID) run create_username_password_credential $NEW_CREDENTIAL1 $csid 'username' 'password' diff --git a/sdk/pbs/controller/api/resources/credentials/credential.pb.go b/sdk/pbs/controller/api/resources/credentials/credential.pb.go index 519349880..73e641199 100644 --- a/sdk/pbs/controller/api/resources/credentials/credential.pb.go +++ b/sdk/pbs/controller/api/resources/credentials/credential.pb.go @@ -56,6 +56,7 @@ type Credential struct { // *Credential_Attributes // *Credential_UsernamePasswordAttributes // *Credential_SshPrivateKeyAttributes + // *Credential_JsonAttributes Attrs isCredential_Attrs `protobuf_oneof:"attrs"` // 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" class:"public"` // @gotags: `class:"public"` @@ -184,6 +185,13 @@ func (x *Credential) GetSshPrivateKeyAttributes() *SshPrivateKeyAttributes { return nil } +func (x *Credential) GetJsonAttributes() *JsonAttributes { + if x, ok := x.GetAttrs().(*Credential_JsonAttributes); ok { + return x.JsonAttributes + } + return nil +} + func (x *Credential) GetAuthorizedActions() []string { if x != nil { return x.AuthorizedActions @@ -208,12 +216,18 @@ type Credential_SshPrivateKeyAttributes struct { SshPrivateKeyAttributes *SshPrivateKeyAttributes `protobuf:"bytes,102,opt,name=ssh_private_key_attributes,json=sshPrivateKeyAttributes,proto3,oneof"` } +type Credential_JsonAttributes struct { + JsonAttributes *JsonAttributes `protobuf:"bytes,103,opt,name=json_attributes,json=jsonAttributes,proto3,oneof"` +} + func (*Credential_Attributes) isCredential_Attrs() {} func (*Credential_UsernamePasswordAttributes) isCredential_Attrs() {} func (*Credential_SshPrivateKeyAttributes) isCredential_Attrs() {} +func (*Credential_JsonAttributes) isCredential_Attrs() {} + // The attributes of a UsernamePassword Credential. type UsernamePasswordAttributes struct { state protoimpl.MessageState @@ -366,6 +380,64 @@ func (x *SshPrivateKeyAttributes) GetPrivateKeyPassphraseHmac() string { return "" } +// The attributes of a JSON Credential. +type JsonAttributes struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Input only. The secret associated with the credential. + Object *structpb.Struct `protobuf:"bytes,10,opt,name=object,proto3" json:"object,omitempty" class:"secret"` // @gotags: `class:"secret"` + // Output only. The hmac value of the object. + ObjectHmac string `protobuf:"bytes,20,opt,name=object_hmac,proto3" json:"object_hmac,omitempty" class:"public"` // @gotags: `class:"public"` +} + +func (x *JsonAttributes) Reset() { + *x = JsonAttributes{} + if protoimpl.UnsafeEnabled { + mi := &file_controller_api_resources_credentials_v1_credential_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *JsonAttributes) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*JsonAttributes) ProtoMessage() {} + +func (x *JsonAttributes) ProtoReflect() protoreflect.Message { + mi := &file_controller_api_resources_credentials_v1_credential_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 JsonAttributes.ProtoReflect.Descriptor instead. +func (*JsonAttributes) Descriptor() ([]byte, []int) { + return file_controller_api_resources_credentials_v1_credential_proto_rawDescGZIP(), []int{3} +} + +func (x *JsonAttributes) GetObject() *structpb.Struct { + if x != nil { + return x.Object + } + return nil +} + +func (x *JsonAttributes) GetObjectHmac() string { + if x != nil { + return x.ObjectHmac + } + return "" +} + var File_controller_api_resources_credentials_v1_credential_proto protoreflect.FileDescriptor var file_controller_api_resources_credentials_v1_credential_proto_rawDesc = []byte{ @@ -388,7 +460,7 @@ var file_controller_api_resources_credentials_v1_credential_proto_rawDesc = []by 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 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, 0xd3, 0x07, 0x0a, 0x0a, + 0x70, 0x70, 0x65, 0x72, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xd6, 0x08, 0x0a, 0x0a, 0x43, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x30, 0x0a, 0x13, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x5f, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x5f, 0x69, @@ -446,75 +518,95 @@ var file_controller_api_resources_credentials_v1_credential_proto_rawDesc = []by 0x5f, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0xfa, 0xd2, 0xe4, 0x93, 0x02, 0x0a, 0x12, 0x08, 0x49, 0x4e, 0x54, 0x45, 0x52, 0x4e, 0x41, 0x4c, 0x48, 0x00, 0x52, 0x17, 0x73, 0x73, 0x68, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x41, 0x74, 0x74, - 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 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, 0x42, 0x07, 0x0a, 0x05, 0x61, 0x74, 0x74, 0x72, - 0x73, 0x22, 0xb6, 0x02, 0x0a, 0x1a, 0x55, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x50, 0x61, - 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, - 0x12, 0x61, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x0a, 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, 0x27, 0xa0, 0xda, 0x29, 0x01, 0xc2, 0xdd, 0x29, 0x1f, 0x0a, 0x13, 0x61, 0x74, 0x74, 0x72, - 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x12, - 0x08, 0x55, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, - 0x61, 0x6d, 0x65, 0x12, 0x61, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x18, - 0x14, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, + 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0x80, 0x01, 0x0a, 0x0f, 0x6a, 0x73, 0x6f, 0x6e, + 0x5f, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x18, 0x67, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x37, 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, 0x63, 0x72, 0x65, + 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73, 0x2e, 0x76, 0x31, 0x2e, 0x4a, 0x73, 0x6f, 0x6e, + 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x42, 0x1c, 0xa0, 0xda, 0x29, 0x01, + 0x9a, 0xe3, 0x29, 0x04, 0x6a, 0x73, 0x6f, 0x6e, 0xfa, 0xd2, 0xe4, 0x93, 0x02, 0x0a, 0x12, 0x08, + 0x49, 0x4e, 0x54, 0x45, 0x52, 0x4e, 0x41, 0x4c, 0x48, 0x00, 0x52, 0x0e, 0x6a, 0x73, 0x6f, 0x6e, + 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 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, 0x42, 0x07, 0x0a, 0x05, 0x61, + 0x74, 0x74, 0x72, 0x73, 0x22, 0xb6, 0x02, 0x0a, 0x1a, 0x55, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, + 0x65, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, + 0x74, 0x65, 0x73, 0x12, 0x61, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, + 0x0a, 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, 0x27, 0xa0, 0xda, 0x29, 0x01, 0xc2, 0xdd, 0x29, 0x1f, 0x0a, 0x13, 0x61, - 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, - 0x72, 0x64, 0x12, 0x08, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, 0x08, 0x70, 0x61, - 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x52, 0x0a, 0x0d, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, - 0x72, 0x64, 0x5f, 0x68, 0x6d, 0x61, 0x63, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x09, 0x42, 0x2c, 0xc2, - 0xdd, 0x29, 0x28, 0x0a, 0x18, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, - 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x5f, 0x68, 0x6d, 0x61, 0x63, 0x12, 0x0c, 0x50, - 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x48, 0x6d, 0x61, 0x63, 0x52, 0x0d, 0x70, 0x61, 0x73, - 0x73, 0x77, 0x6f, 0x72, 0x64, 0x5f, 0x68, 0x6d, 0x61, 0x63, 0x22, 0xee, 0x04, 0x0a, 0x17, 0x53, - 0x73, 0x68, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x41, 0x74, 0x74, 0x72, - 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0x61, 0x0a, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, - 0x6d, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, + 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, + 0x6d, 0x65, 0x12, 0x08, 0x55, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x52, 0x08, 0x75, 0x73, + 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x61, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, + 0x72, 0x64, 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, 0x27, 0xa0, 0xda, 0x29, 0x01, 0xc2, 0xdd, 0x29, 0x1f, - 0x0a, 0x13, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x75, 0x73, 0x65, - 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x08, 0x55, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x52, - 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x6c, 0x0a, 0x0b, 0x70, 0x72, 0x69, - 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 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, 0x2c, 0xa0, 0xda, - 0x29, 0x01, 0xc2, 0xdd, 0x29, 0x24, 0x0a, 0x16, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, - 0x65, 0x73, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x12, 0x0a, - 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x0b, 0x70, 0x72, 0x69, 0x76, - 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x12, 0x5d, 0x0a, 0x10, 0x70, 0x72, 0x69, 0x76, 0x61, - 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x68, 0x6d, 0x61, 0x63, 0x18, 0x1e, 0x20, 0x01, 0x28, - 0x09, 0x42, 0x31, 0xc2, 0xdd, 0x29, 0x2d, 0x0a, 0x1b, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, - 0x74, 0x65, 0x73, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f, - 0x68, 0x6d, 0x61, 0x63, 0x12, 0x0e, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, - 0x48, 0x6d, 0x61, 0x63, 0x52, 0x10, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, - 0x79, 0x5f, 0x68, 0x6d, 0x61, 0x63, 0x12, 0x97, 0x01, 0x0a, 0x16, 0x70, 0x72, 0x69, 0x76, 0x61, - 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x70, 0x61, 0x73, 0x73, 0x70, 0x68, 0x72, 0x61, 0x73, - 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, 0x41, 0xa0, 0xda, 0x29, 0x01, 0xc2, 0xdd, 0x29, 0x39, 0x0a, - 0x21, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x70, 0x72, 0x69, 0x76, + 0x0a, 0x13, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x70, 0x61, 0x73, + 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x08, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x52, + 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x12, 0x52, 0x0a, 0x0d, 0x70, 0x61, 0x73, + 0x73, 0x77, 0x6f, 0x72, 0x64, 0x5f, 0x68, 0x6d, 0x61, 0x63, 0x18, 0x1e, 0x20, 0x01, 0x28, 0x09, + 0x42, 0x2c, 0xc2, 0xdd, 0x29, 0x28, 0x0a, 0x18, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, + 0x65, 0x73, 0x2e, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x5f, 0x68, 0x6d, 0x61, 0x63, + 0x12, 0x0c, 0x50, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x48, 0x6d, 0x61, 0x63, 0x52, 0x0d, + 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64, 0x5f, 0x68, 0x6d, 0x61, 0x63, 0x22, 0xee, 0x04, + 0x0a, 0x17, 0x53, 0x73, 0x68, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x41, + 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x12, 0x61, 0x0a, 0x08, 0x75, 0x73, 0x65, + 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x0a, 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, 0x27, 0xa0, 0xda, 0x29, 0x01, 0xc2, + 0xdd, 0x29, 0x1f, 0x0a, 0x13, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, + 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x08, 0x55, 0x73, 0x65, 0x72, 0x6e, 0x61, + 0x6d, 0x65, 0x52, 0x08, 0x75, 0x73, 0x65, 0x72, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x6c, 0x0a, 0x0b, + 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 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, + 0x2c, 0xa0, 0xda, 0x29, 0x01, 0xc2, 0xdd, 0x29, 0x24, 0x0a, 0x16, 0x61, 0x74, 0x74, 0x72, 0x69, + 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, + 0x79, 0x12, 0x0a, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x52, 0x0b, 0x70, + 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x12, 0x5d, 0x0a, 0x10, 0x70, 0x72, + 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x68, 0x6d, 0x61, 0x63, 0x18, 0x1e, + 0x20, 0x01, 0x28, 0x09, 0x42, 0x31, 0xc2, 0xdd, 0x29, 0x2d, 0x0a, 0x1b, 0x61, 0x74, 0x74, 0x72, + 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, + 0x65, 0x79, 0x5f, 0x68, 0x6d, 0x61, 0x63, 0x12, 0x0e, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, + 0x4b, 0x65, 0x79, 0x48, 0x6d, 0x61, 0x63, 0x52, 0x10, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, + 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x68, 0x6d, 0x61, 0x63, 0x12, 0x97, 0x01, 0x0a, 0x16, 0x70, 0x72, + 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x70, 0x61, 0x73, 0x73, 0x70, 0x68, + 0x72, 0x61, 0x73, 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, 0x41, 0xa0, 0xda, 0x29, 0x01, 0xc2, 0xdd, + 0x29, 0x39, 0x0a, 0x21, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x70, + 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x70, 0x61, 0x73, 0x73, 0x70, + 0x68, 0x72, 0x61, 0x73, 0x65, 0x12, 0x14, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, + 0x79, 0x50, 0x61, 0x73, 0x73, 0x70, 0x68, 0x72, 0x61, 0x73, 0x65, 0x52, 0x16, 0x70, 0x72, 0x69, + 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x70, 0x61, 0x73, 0x73, 0x70, 0x68, 0x72, + 0x61, 0x73, 0x65, 0x12, 0x88, 0x01, 0x0a, 0x1b, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, + 0x6b, 0x65, 0x79, 0x5f, 0x70, 0x61, 0x73, 0x73, 0x70, 0x68, 0x72, 0x61, 0x73, 0x65, 0x5f, 0x68, + 0x6d, 0x61, 0x63, 0x18, 0x32, 0x20, 0x01, 0x28, 0x09, 0x42, 0x46, 0xc2, 0xdd, 0x29, 0x42, 0x0a, + 0x26, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x70, 0x61, 0x73, 0x73, 0x70, 0x68, 0x72, 0x61, - 0x73, 0x65, 0x12, 0x14, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, 0x50, 0x61, - 0x73, 0x73, 0x70, 0x68, 0x72, 0x61, 0x73, 0x65, 0x52, 0x16, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, - 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x70, 0x61, 0x73, 0x73, 0x70, 0x68, 0x72, 0x61, 0x73, 0x65, - 0x12, 0x88, 0x01, 0x0a, 0x1b, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, - 0x5f, 0x70, 0x61, 0x73, 0x73, 0x70, 0x68, 0x72, 0x61, 0x73, 0x65, 0x5f, 0x68, 0x6d, 0x61, 0x63, - 0x18, 0x32, 0x20, 0x01, 0x28, 0x09, 0x42, 0x46, 0xc2, 0xdd, 0x29, 0x42, 0x0a, 0x26, 0x61, 0x74, - 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, - 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x70, 0x61, 0x73, 0x73, 0x70, 0x68, 0x72, 0x61, 0x73, 0x65, 0x5f, - 0x68, 0x6d, 0x61, 0x63, 0x12, 0x18, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x4b, 0x65, 0x79, - 0x50, 0x61, 0x73, 0x73, 0x70, 0x68, 0x72, 0x61, 0x73, 0x65, 0x48, 0x6d, 0x61, 0x63, 0x52, 0x1b, - 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x70, 0x61, 0x73, 0x73, - 0x70, 0x68, 0x72, 0x61, 0x73, 0x65, 0x5f, 0x68, 0x6d, 0x61, 0x63, 0x42, 0x58, 0x5a, 0x56, 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, 0x73, 0x64, 0x6b, - 0x2f, 0x70, 0x62, 0x73, 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, 0x63, 0x72, - 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73, 0x3b, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, - 0x74, 0x69, 0x61, 0x6c, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x73, 0x65, 0x5f, 0x68, 0x6d, 0x61, 0x63, 0x12, 0x18, 0x50, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, + 0x4b, 0x65, 0x79, 0x50, 0x61, 0x73, 0x73, 0x70, 0x68, 0x72, 0x61, 0x73, 0x65, 0x48, 0x6d, 0x61, + 0x63, 0x52, 0x1b, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x5f, 0x6b, 0x65, 0x79, 0x5f, 0x70, + 0x61, 0x73, 0x73, 0x70, 0x68, 0x72, 0x61, 0x73, 0x65, 0x5f, 0x68, 0x6d, 0x61, 0x63, 0x22, 0xb2, + 0x01, 0x0a, 0x0e, 0x4a, 0x73, 0x6f, 0x6e, 0x41, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, + 0x73, 0x12, 0x54, 0x0a, 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x18, 0x0a, 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, 0x23, 0xa0, 0xda, 0x29, 0x01, + 0xc2, 0xdd, 0x29, 0x1b, 0x0a, 0x11, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, + 0x2e, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x12, 0x06, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x52, + 0x06, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x12, 0x4a, 0x0a, 0x0b, 0x6f, 0x62, 0x6a, 0x65, 0x63, + 0x74, 0x5f, 0x68, 0x6d, 0x61, 0x63, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x42, 0x28, 0xc2, 0xdd, + 0x29, 0x24, 0x0a, 0x16, 0x61, 0x74, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x65, 0x73, 0x2e, 0x6f, + 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x68, 0x6d, 0x61, 0x63, 0x12, 0x0a, 0x4f, 0x62, 0x6a, 0x65, + 0x63, 0x74, 0x48, 0x6d, 0x61, 0x63, 0x52, 0x0b, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x68, + 0x6d, 0x61, 0x63, 0x42, 0x58, 0x5a, 0x56, 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, 0x73, 0x64, 0x6b, 0x2f, 0x70, 0x62, 0x73, 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, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, + 0x73, 0x3b, 0x63, 0x72, 0x65, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x61, 0x6c, 0x73, 0x62, 0x06, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -529,35 +621,38 @@ func file_controller_api_resources_credentials_v1_credential_proto_rawDescGZIP() return file_controller_api_resources_credentials_v1_credential_proto_rawDescData } -var file_controller_api_resources_credentials_v1_credential_proto_msgTypes = make([]protoimpl.MessageInfo, 3) +var file_controller_api_resources_credentials_v1_credential_proto_msgTypes = make([]protoimpl.MessageInfo, 4) var file_controller_api_resources_credentials_v1_credential_proto_goTypes = []interface{}{ (*Credential)(nil), // 0: controller.api.resources.credentials.v1.Credential (*UsernamePasswordAttributes)(nil), // 1: controller.api.resources.credentials.v1.UsernamePasswordAttributes (*SshPrivateKeyAttributes)(nil), // 2: controller.api.resources.credentials.v1.SshPrivateKeyAttributes - (*scopes.ScopeInfo)(nil), // 3: controller.api.resources.scopes.v1.ScopeInfo - (*wrapperspb.StringValue)(nil), // 4: google.protobuf.StringValue - (*timestamppb.Timestamp)(nil), // 5: google.protobuf.Timestamp - (*structpb.Struct)(nil), // 6: google.protobuf.Struct + (*JsonAttributes)(nil), // 3: controller.api.resources.credentials.v1.JsonAttributes + (*scopes.ScopeInfo)(nil), // 4: controller.api.resources.scopes.v1.ScopeInfo + (*wrapperspb.StringValue)(nil), // 5: google.protobuf.StringValue + (*timestamppb.Timestamp)(nil), // 6: google.protobuf.Timestamp + (*structpb.Struct)(nil), // 7: google.protobuf.Struct } var file_controller_api_resources_credentials_v1_credential_proto_depIdxs = []int32{ - 3, // 0: controller.api.resources.credentials.v1.Credential.scope:type_name -> controller.api.resources.scopes.v1.ScopeInfo - 4, // 1: controller.api.resources.credentials.v1.Credential.name:type_name -> google.protobuf.StringValue - 4, // 2: controller.api.resources.credentials.v1.Credential.description:type_name -> google.protobuf.StringValue - 5, // 3: controller.api.resources.credentials.v1.Credential.created_time:type_name -> google.protobuf.Timestamp - 5, // 4: controller.api.resources.credentials.v1.Credential.updated_time:type_name -> google.protobuf.Timestamp - 6, // 5: controller.api.resources.credentials.v1.Credential.attributes:type_name -> google.protobuf.Struct + 4, // 0: controller.api.resources.credentials.v1.Credential.scope:type_name -> controller.api.resources.scopes.v1.ScopeInfo + 5, // 1: controller.api.resources.credentials.v1.Credential.name:type_name -> google.protobuf.StringValue + 5, // 2: controller.api.resources.credentials.v1.Credential.description:type_name -> google.protobuf.StringValue + 6, // 3: controller.api.resources.credentials.v1.Credential.created_time:type_name -> google.protobuf.Timestamp + 6, // 4: controller.api.resources.credentials.v1.Credential.updated_time:type_name -> google.protobuf.Timestamp + 7, // 5: controller.api.resources.credentials.v1.Credential.attributes:type_name -> google.protobuf.Struct 1, // 6: controller.api.resources.credentials.v1.Credential.username_password_attributes:type_name -> controller.api.resources.credentials.v1.UsernamePasswordAttributes 2, // 7: controller.api.resources.credentials.v1.Credential.ssh_private_key_attributes:type_name -> controller.api.resources.credentials.v1.SshPrivateKeyAttributes - 4, // 8: controller.api.resources.credentials.v1.UsernamePasswordAttributes.username:type_name -> google.protobuf.StringValue - 4, // 9: controller.api.resources.credentials.v1.UsernamePasswordAttributes.password:type_name -> google.protobuf.StringValue - 4, // 10: controller.api.resources.credentials.v1.SshPrivateKeyAttributes.username:type_name -> google.protobuf.StringValue - 4, // 11: controller.api.resources.credentials.v1.SshPrivateKeyAttributes.private_key:type_name -> google.protobuf.StringValue - 4, // 12: controller.api.resources.credentials.v1.SshPrivateKeyAttributes.private_key_passphrase:type_name -> google.protobuf.StringValue - 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 + 3, // 8: controller.api.resources.credentials.v1.Credential.json_attributes:type_name -> controller.api.resources.credentials.v1.JsonAttributes + 5, // 9: controller.api.resources.credentials.v1.UsernamePasswordAttributes.username:type_name -> google.protobuf.StringValue + 5, // 10: controller.api.resources.credentials.v1.UsernamePasswordAttributes.password:type_name -> google.protobuf.StringValue + 5, // 11: controller.api.resources.credentials.v1.SshPrivateKeyAttributes.username:type_name -> google.protobuf.StringValue + 5, // 12: controller.api.resources.credentials.v1.SshPrivateKeyAttributes.private_key:type_name -> google.protobuf.StringValue + 5, // 13: controller.api.resources.credentials.v1.SshPrivateKeyAttributes.private_key_passphrase:type_name -> google.protobuf.StringValue + 7, // 14: controller.api.resources.credentials.v1.JsonAttributes.object:type_name -> google.protobuf.Struct + 15, // [15:15] is the sub-list for method output_type + 15, // [15:15] 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_resources_credentials_v1_credential_proto_init() } @@ -602,11 +697,24 @@ func file_controller_api_resources_credentials_v1_credential_proto_init() { return nil } } + file_controller_api_resources_credentials_v1_credential_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*JsonAttributes); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } } file_controller_api_resources_credentials_v1_credential_proto_msgTypes[0].OneofWrappers = []interface{}{ (*Credential_Attributes)(nil), (*Credential_UsernamePasswordAttributes)(nil), (*Credential_SshPrivateKeyAttributes)(nil), + (*Credential_JsonAttributes)(nil), } type x struct{} out := protoimpl.TypeBuilder{ @@ -614,7 +722,7 @@ func file_controller_api_resources_credentials_v1_credential_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_controller_api_resources_credentials_v1_credential_proto_rawDesc, NumEnums: 0, - NumMessages: 3, + NumMessages: 4, NumExtensions: 0, NumServices: 0, },