diff --git a/api/credentials/option.gen.go b/api/credentials/option.gen.go index ac95b9e249..fd2739fbc6 100644 --- a/api/credentials/option.gen.go +++ b/api/credentials/option.gen.go @@ -122,6 +122,30 @@ func WithUsernamePasswordCredentialPassword(inPassword string) Option { } } +func WithSshPrivateKeyCredentialPrivateKey(inPrivateKey string) Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["private_key"] = inPrivateKey + o.postMap["attributes"] = val + } +} + +func WithSshPrivateKeyCredentialUsername(inUsername string) Option { + return func(o *options) { + raw, ok := o.postMap["attributes"] + if !ok { + raw = interface{}(map[string]interface{}{}) + } + val := raw.(map[string]interface{}) + val["username"] = inUsername + o.postMap["attributes"] = val + } +} + func WithUsernamePasswordCredentialUsername(inUsername string) Option { return func(o *options) { raw, ok := o.postMap["attributes"] diff --git a/api/credentials/ssh_private_key_attributes.gen.go b/api/credentials/ssh_private_key_attributes.gen.go new file mode 100644 index 0000000000..3ec70fa1e3 --- /dev/null +++ b/api/credentials/ssh_private_key_attributes.gen.go @@ -0,0 +1,8 @@ +// Code generated by "make api"; DO NOT EDIT. +package credentials + +type SshPrivateKeyAttributes struct { + Username string `json:"username,omitempty"` + PrivateKey string `json:"private_key,omitempty"` + PrivateKeyHmac string `json:"private_key_hmac,omitempty"` +} diff --git a/internal/api/genapi/input.go b/internal/api/genapi/input.go index dd7fa32c36..c94df66b91 100644 --- a/internal/api/genapi/input.go +++ b/internal/api/genapi/input.go @@ -469,6 +469,21 @@ var inputStructs = []*structInfo{ }, }, }, + { + inProto: &credentials.SshPrivateKeyAttributes{}, + outFile: "credentials/ssh_private_key_attributes.gen.go", + subtypeName: "SshPrivateKeyCredential", + fieldOverrides: []fieldInfo{ + { + Name: "Username", + SkipDefault: true, + }, + { + Name: "PrivateKey", + SkipDefault: true, + }, + }, + }, { inProto: &credentials.Credential{}, outFile: "credentials/credential.gen.go", diff --git a/internal/cmd/commands.go b/internal/cmd/commands.go index 3b00886162..1730e9d32c 100644 --- a/internal/cmd/commands.go +++ b/internal/cmd/commands.go @@ -472,6 +472,12 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) { Func: "create", }, nil }, + "credentials create ssh-private-key": func() (cli.Command, error) { + return &credentialscmd.SshPrivateKeyCommand{ + Command: base.NewCommand(ui), + Func: "create", + }, nil + }, "credentials update": func() (cli.Command, error) { return &credentialscmd.Command{ Command: base.NewCommand(ui), @@ -484,6 +490,12 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) { Func: "update", }, nil }, + "credentials update ssh-private-key": func() (cli.Command, error) { + return &credentialscmd.SshPrivateKeyCommand{ + Command: base.NewCommand(ui), + Func: "update", + }, nil + }, "groups": func() (cli.Command, error) { return &groupscmd.Command{ diff --git a/internal/cmd/commands/credentialscmd/funcs.go b/internal/cmd/commands/credentialscmd/funcs.go index dc061be8cf..efec87e6d7 100644 --- a/internal/cmd/commands/credentialscmd/funcs.go +++ b/internal/cmd/commands/credentialscmd/funcs.go @@ -9,6 +9,12 @@ import ( "github.com/hashicorp/boundary/internal/cmd/base" ) +const ( + usernameFlagName = "username" + passwordFlagName = "password" + privateKeyFlagName = "private-key" +) + func (c *Command) extraHelpFunc(helpMap map[string]func() string) string { var helpStr string switch c.Func { @@ -42,7 +48,7 @@ func (c *Command) extraHelpFunc(helpMap map[string]func() string) string { "", " This command allows update operations on Boundary credential resources. Example:", "", - " Update a username password credential:", + " Update a username/password credential:", "", ` $ boundary credentials update username-password -id cred_1234567890 -name devops -description "For DevOps usage"`, "", @@ -172,6 +178,7 @@ func printItemTable(result api.GenericResult) string { } var keySubstMap = map[string]string{ - "username": "Username", - "password_hmac": "Password HMAC", + "username": "Username", + "password_hmac": "Password HMAC", + "private_key_hmac": "Private Key HMAC", } diff --git a/internal/cmd/commands/credentialscmd/ssh-private-key_credentials.gen.go b/internal/cmd/commands/credentialscmd/ssh-private-key_credentials.gen.go new file mode 100644 index 0000000000..053e12277d --- /dev/null +++ b/internal/cmd/commands/credentialscmd/ssh-private-key_credentials.gen.go @@ -0,0 +1,258 @@ +// 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 initSshPrivateKeyFlags() { + flagsOnce.Do(func() { + extraFlags := extraSshPrivateKeyActionsFlagsMapFunc() + for k, v := range extraFlags { + flagsSshPrivateKeyMap[k] = append(flagsSshPrivateKeyMap[k], v...) + } + }) +} + +var ( + _ cli.Command = (*SshPrivateKeyCommand)(nil) + _ cli.CommandAutocomplete = (*SshPrivateKeyCommand)(nil) +) + +type SshPrivateKeyCommand struct { + *base.Command + + Func string + + plural string + + extraSshPrivateKeyCmdVars +} + +func (c *SshPrivateKeyCommand) AutocompleteArgs() complete.Predictor { + initSshPrivateKeyFlags() + return complete.PredictAnything +} + +func (c *SshPrivateKeyCommand) AutocompleteFlags() complete.Flags { + initSshPrivateKeyFlags() + return c.Flags().Completions() +} + +func (c *SshPrivateKeyCommand) Synopsis() string { + if extra := extraSshPrivateKeySynopsisFunc(c); extra != "" { + return extra + } + + synopsisStr := "credential" + + synopsisStr = fmt.Sprintf("%s %s", "ssh-private-key-type", synopsisStr) + + return common.SynopsisFunc(c.Func, synopsisStr) +} + +func (c *SshPrivateKeyCommand) Help() string { + initSshPrivateKeyFlags() + + var helpStr string + helpMap := common.HelpMap("credential") + + switch c.Func { + + default: + + helpStr = c.extraSshPrivateKeyHelpFunc(helpMap) + + } + + // Keep linter from complaining if we don't actually generate code using it + _ = helpMap + return helpStr +} + +var flagsSshPrivateKeyMap = map[string][]string{ + + "create": {"credential-store-id", "name", "description"}, + + "update": {"id", "name", "description", "version"}, +} + +func (c *SshPrivateKeyCommand) Flags() *base.FlagSets { + if len(flagsSshPrivateKeyMap[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, "ssh-private-key-type credential", flagsSshPrivateKeyMap, c.Func) + + extraSshPrivateKeyFlagsFunc(c, set, f) + + return set +} + +func (c *SshPrivateKeyCommand) Run(args []string) int { + initSshPrivateKeyFlags() + + switch c.Func { + case "": + return cli.RunResultHelp + + } + + c.plural = "ssh-private-key-type credential" + switch c.Func { + case "list": + c.plural = "ssh-private-key-type credentials" + } + + f := c.Flags() + + if err := f.Parse(args); err != nil { + c.PrintCliError(err) + return base.CommandUserError + } + + if strutil.StrListContains(flagsSshPrivateKeyMap[c.Func], "id") && c.FlagId == "" { + c.PrintCliError(errors.New("ID is required but not passed in via -id")) + return base.CommandUserError + } + + var opts []credentials.Option + + if strutil.StrListContains(flagsSshPrivateKeyMap[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 ok := extraSshPrivateKeyFlagsHandlingFunc(c, f, &opts); !ok { + return base.CommandUserError + } + + var result api.GenericResult + + switch c.Func { + + case "create": + result, err = credentialsClient.Create(c.Context, "ssh_private_key", c.FlagCredentialStoreId, opts...) + + case "update": + result, err = credentialsClient.Update(c.Context, c.FlagId, version, opts...) + + } + + result, err = executeExtraSshPrivateKeyActions(c, result, err, credentialsClient, version, opts) + + if err != nil { + if apiErr := api.AsServerError(err); apiErr != nil { + var opts []base.Option + + opts = append(opts, base.WithAttributeFieldPrefix("ssh_private_key")) + + c.PrintApiError(apiErr, fmt.Sprintf("Error from controller when performing %s on %s", c.Func, c.plural), opts...) + return base.CommandApiError + } + c.PrintCliError(fmt.Errorf("Error trying to %s %s: %s", c.Func, c.plural, err.Error())) + return base.CommandCliError + } + + output, err := printCustomSshPrivateKeyActionOutput(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(result)) + + case "json": + if ok := c.PrintJsonItem(result); !ok { + return base.CommandCliError + } + } + + return base.CommandSuccess +} + +var ( + extraSshPrivateKeyActionsFlagsMapFunc = func() map[string][]string { return nil } + extraSshPrivateKeySynopsisFunc = func(*SshPrivateKeyCommand) string { return "" } + extraSshPrivateKeyFlagsFunc = func(*SshPrivateKeyCommand, *base.FlagSets, *base.FlagSet) {} + extraSshPrivateKeyFlagsHandlingFunc = func(*SshPrivateKeyCommand, *base.FlagSets, *[]credentials.Option) bool { return true } + executeExtraSshPrivateKeyActions = func(_ *SshPrivateKeyCommand, inResult api.GenericResult, inErr error, _ *credentials.Client, _ uint32, _ []credentials.Option) (api.GenericResult, error) { + return inResult, inErr + } + printCustomSshPrivateKeyActionOutput = func(*SshPrivateKeyCommand) (bool, error) { return false, nil } +) diff --git a/internal/cmd/commands/credentialscmd/ssh-private-key_funcs.go b/internal/cmd/commands/credentialscmd/ssh-private-key_funcs.go new file mode 100644 index 0000000000..feb6fdb33b --- /dev/null +++ b/internal/cmd/commands/credentialscmd/ssh-private-key_funcs.go @@ -0,0 +1,98 @@ +package credentialscmd + +import ( + "github.com/hashicorp/boundary/api/credentials" + "github.com/hashicorp/boundary/internal/cmd/base" + "github.com/hashicorp/go-secure-stdlib/parseutil" +) + +func init() { + extraSshPrivateKeyFlagsFunc = extraSshPrivateKeyFlagsFuncImpl + extraSshPrivateKeyActionsFlagsMapFunc = extraSshPrivateKeyActionsFlagsMapFuncImpl + extraSshPrivateKeyFlagsHandlingFunc = extraSshPrivateKeyFlagHandlingFuncImpl +} + +type extraSshPrivateKeyCmdVars struct { + flagUsername string + flagPrivateKey string +} + +func extraSshPrivateKeyActionsFlagsMapFuncImpl() map[string][]string { + flags := map[string][]string{ + "create": { + usernameFlagName, + privateKeyFlagName, + }, + } + flags["update"] = flags["create"] + return flags +} + +func extraSshPrivateKeyFlagsFuncImpl(c *SshPrivateKeyCommand, set *base.FlagSets, _ *base.FlagSet) { + f := set.NewFlagSet("SSH Private Key Credential Options") + + for _, name := range flagsSshPrivateKeyMap[c.Func] { + switch name { + case usernameFlagName: + f.StringVar(&base.StringVar{ + Name: usernameFlagName, + Target: &c.flagUsername, + Usage: "The username associated with the credential.", + }) + case privateKeyFlagName: + f.StringVar(&base.StringVar{ + Name: privateKeyFlagName, + Target: &c.flagPrivateKey, + Usage: "The SSH private key associated with the credential. This can be the value itself, refer to a file on disk (file://) from which the value will be read, or an env var (env://) from which the value will be read.", + }) + } + } +} + +func extraSshPrivateKeyFlagHandlingFuncImpl(c *SshPrivateKeyCommand, _ *base.FlagSets, opts *[]credentials.Option) bool { + switch c.flagUsername { + case "": + default: + *opts = append(*opts, credentials.WithSshPrivateKeyCredentialUsername(c.flagUsername)) + } + switch c.flagPrivateKey { + case "": + default: + privateKey, err := parseutil.ParsePath(c.flagPrivateKey) + if err != nil && err.Error() != parseutil.ErrNotAUrl.Error() { + c.UI.Error("Error parsing private key flag: " + err.Error()) + return false + } + *opts = append(*opts, credentials.WithSshPrivateKeyCredentialPrivateKey(privateKey)) + } + + return true +} + +func (c *SshPrivateKeyCommand) extraSshPrivateKeyHelpFunc(_ map[string]func() string) string { + var helpStr string + switch c.Func { + case "create": + helpStr = base.WrapForHelpText([]string{ + "Usage: boundary credentials create ssh-private-key -credential-store-id [options] [args]", + "", + " Create an SSH private key credential. Example:", + "", + ` $ boundary credentials create ssh-private-key -credential-store-id csvlt_1234567890 -username user -private-key file:///home/user/.ssh/id_ed25519`, + "", + "", + }) + + case "update": + helpStr = base.WrapForHelpText([]string{ + "Usage: boundary credentials update ssh-private-key [options] [args]", + "", + " Update an SSH private key credential given its ID. Example:", + "", + ` $ boundary credentials update ssh-private-key -id clvlt_1234567890 -name devops -description "For DevOps usage"`, + "", + "", + }) + } + return helpStr + c.Flags().Help() +} diff --git a/internal/cmd/commands/credentialscmd/username_password_credentials.gen.go b/internal/cmd/commands/credentialscmd/username-password_credentials.gen.go similarity index 96% rename from internal/cmd/commands/credentialscmd/username_password_credentials.gen.go rename to internal/cmd/commands/credentialscmd/username-password_credentials.gen.go index a9c7833d71..dcd646fbe4 100644 --- a/internal/cmd/commands/credentialscmd/username_password_credentials.gen.go +++ b/internal/cmd/commands/credentialscmd/username-password_credentials.gen.go @@ -55,7 +55,7 @@ func (c *UsernamePasswordCommand) Synopsis() string { synopsisStr := "credential" - synopsisStr = fmt.Sprintf("%s %s", "username_password-type", synopsisStr) + synopsisStr = fmt.Sprintf("%s %s", "username-password-type", synopsisStr) return common.SynopsisFunc(c.Func, synopsisStr) } @@ -93,7 +93,7 @@ func (c *UsernamePasswordCommand) Flags() *base.FlagSets { set := c.FlagSet(base.FlagSetHTTP | base.FlagSetClient | base.FlagSetOutputFormat) f := set.NewFlagSet("Command Options") - common.PopulateCommonFlags(c.Command, f, "username_password-type credential", flagsUsernamePasswordMap, c.Func) + common.PopulateCommonFlags(c.Command, f, "username-password-type credential", flagsUsernamePasswordMap, c.Func) extraUsernamePasswordFlagsFunc(c, set, f) @@ -109,10 +109,10 @@ func (c *UsernamePasswordCommand) Run(args []string) int { } - c.plural = "username_password-type credential" + c.plural = "username-password-type credential" switch c.Func { case "list": - c.plural = "username_password-type credentials" + c.plural = "username-password-type credentials" } f := c.Flags() diff --git a/internal/cmd/commands/credentialscmd/username_password_funcs.go b/internal/cmd/commands/credentialscmd/username-password_funcs.go similarity index 81% rename from internal/cmd/commands/credentialscmd/username_password_funcs.go rename to internal/cmd/commands/credentialscmd/username-password_funcs.go index fd6b0cf894..fe83731f74 100644 --- a/internal/cmd/commands/credentialscmd/username_password_funcs.go +++ b/internal/cmd/commands/credentialscmd/username-password_funcs.go @@ -3,6 +3,7 @@ package credentialscmd import ( "github.com/hashicorp/boundary/api/credentials" "github.com/hashicorp/boundary/internal/cmd/base" + "github.com/hashicorp/go-secure-stdlib/parseutil" ) func init() { @@ -11,11 +12,6 @@ func init() { extraUsernamePasswordFlagsHandlingFunc = extraUsernamePasswordFlagHandlingFuncImpl } -const ( - usernameFlagName = "username" - passwordFlagName = "password" -) - type extraUsernamePasswordCmdVars struct { flagUsername string flagPassword string @@ -33,7 +29,7 @@ func extraUsernamePasswordActionsFlagsMapFuncImpl() map[string][]string { } func extraUsernamePasswordFlagsFuncImpl(c *UsernamePasswordCommand, set *base.FlagSets, _ *base.FlagSet) { - f := set.NewFlagSet("Username Password Credential Options") + f := set.NewFlagSet("Username/Password Credential Options") for _, name := range flagsUsernamePasswordMap[c.Func] { switch name { @@ -47,7 +43,7 @@ func extraUsernamePasswordFlagsFuncImpl(c *UsernamePasswordCommand, set *base.Fl f.StringVar(&base.StringVar{ Name: passwordFlagName, Target: &c.flagPassword, - Usage: "The password associated with the credential.", + Usage: "The password associated with the credential. This can be the value itself, refer to a file on disk (file://) from which the value will be read, or an env var (env://) from which the value will be read.", }) } } @@ -62,7 +58,12 @@ func extraUsernamePasswordFlagHandlingFuncImpl(c *UsernamePasswordCommand, _ *ba switch c.flagPassword { case "": default: - *opts = append(*opts, credentials.WithUsernamePasswordCredentialPassword(c.flagPassword)) + password, err := parseutil.ParsePath(c.flagPassword) + if err != nil && err.Error() != parseutil.ErrNotAUrl.Error() { + c.UI.Error("Error parsing password flag: " + err.Error()) + return false + } + *opts = append(*opts, credentials.WithUsernamePasswordCredentialPassword(password)) } return true diff --git a/internal/cmd/gencli/input.go b/internal/cmd/gencli/input.go index 27e3c5882e..d986c8f0d0 100644 --- a/internal/cmd/gencli/input.go +++ b/internal/cmd/gencli/input.go @@ -263,6 +263,22 @@ var inputStructs = map[string][]*cmdInfo{ NeedsSubtypeInCreate: true, PrefixAttributeFieldErrorsWithSubactionPrefix: true, }, + { + ResourceType: resource.Credential.String(), + Pkg: "credentials", + StdActions: []string{"create", "update"}, + SubActionPrefix: "ssh_private_key", + HasExtraCommandVars: true, + SkipNormalHelp: true, + HasExtraHelpFunc: true, + HasId: true, + HasName: true, + HasDescription: true, + Container: "CredentialStore", + VersionedActions: []string{"update"}, + NeedsSubtypeInCreate: true, + PrefixAttributeFieldErrorsWithSubactionPrefix: true, + }, }, "groups": { { diff --git a/internal/cmd/gencli/templates.go b/internal/cmd/gencli/templates.go index 4cb5fd21ef..e353905d92 100644 --- a/internal/cmd/gencli/templates.go +++ b/internal/cmd/gencli/templates.go @@ -24,7 +24,7 @@ func fillTemplates() { fName := pkg if data.SubActionPrefix != "" { - fName = fmt.Sprintf("%s_%s", data.SubActionPrefix, fName) + fName = fmt.Sprintf("%s_%s", strcase.ToKebab(data.SubActionPrefix), fName) } outFile, err := filepath.Abs(fmt.Sprintf("%s/%scmd/%s.gen.go", os.Getenv("CLI_GEN_BASEPATH"), pkg, fName)) if err != nil { @@ -137,7 +137,7 @@ func (c *{{ camelCase .SubActionPrefix }}Command) Synopsis() string { synopsisStr := "{{ lowerSpaceCase .ResourceType }}" {{ if .SubActionPrefix }} - synopsisStr = fmt.Sprintf("%s %s", "{{ .SubActionPrefix }}-type", synopsisStr) + synopsisStr = fmt.Sprintf("%s %s", "{{ kebabCase .SubActionPrefix }}-type", synopsisStr) {{ end }} return common.SynopsisFunc(c.Func, synopsisStr) } @@ -199,7 +199,7 @@ func (c *{{ camelCase .SubActionPrefix }}Command) Flags() *base.FlagSets { set := c.FlagSet(base.FlagSetHTTP | base.FlagSetClient | base.FlagSetOutputFormat) f := set.NewFlagSet("Command Options") - common.PopulateCommonFlags(c.Command, f, "{{ if .SubActionPrefix }}{{ .SubActionPrefix }}-type {{ end }}{{ lowerSpaceCase .ResourceType }}", flags{{ camelCase .SubActionPrefix }}Map, c.Func) + common.PopulateCommonFlags(c.Command, f, "{{ if .SubActionPrefix }}{{ kebabCase .SubActionPrefix }}-type {{ end }}{{ lowerSpaceCase .ResourceType }}", flags{{ camelCase .SubActionPrefix }}Map, c.Func) {{ if .HasGenericAttributes }} f = set.NewFlagSet("Attribute Options") @@ -239,10 +239,10 @@ func (c *{{ camelCase .SubActionPrefix }}Command) Run(args []string) int { {{ end }} } - c.plural = "{{ if .SubActionPrefix }}{{ .SubActionPrefix }}-type {{ end }}{{ lowerSpaceCase .ResourceType }}" + c.plural = "{{ if .SubActionPrefix }}{{ kebabCase .SubActionPrefix }}-type {{ end }}{{ lowerSpaceCase .ResourceType }}" switch c.Func { case "list": - c.plural = "{{ if .SubActionPrefix }}{{ .SubActionPrefix }}-type {{ end }}{{ lowerSpaceCase .ResourceType }}s" + c.plural = "{{ if .SubActionPrefix }}{{ kebabCase .SubActionPrefix }}-type {{ end }}{{ lowerSpaceCase .ResourceType }}s" } f := c.Flags() diff --git a/internal/credential/static/ssh_private_key_credential.go b/internal/credential/static/ssh_private_key_credential.go index cabfff4827..108699c186 100644 --- a/internal/credential/static/ssh_private_key_credential.go +++ b/internal/credential/static/ssh_private_key_credential.go @@ -42,7 +42,7 @@ func NewSshPrivateKeyCredential( case err.Error() == (&ssh.PassphraseMissingError{}).Error(): // This is okay, if it's brokered and the client can use it, no worries default: - return nil, errors.Wrap(ctx, err, op) + return nil, errors.Wrap(ctx, err, op, errors.WithCode(errors.InvalidParameter)) } } diff --git a/internal/daemon/controller/handlers/credentials/credential_service.go b/internal/daemon/controller/handlers/credentials/credential_service.go index 8354e6b5bb..ff98140d2f 100644 --- a/internal/daemon/controller/handlers/credentials/credential_service.go +++ b/internal/daemon/controller/handlers/credentials/credential_service.go @@ -20,6 +20,7 @@ import ( "github.com/hashicorp/boundary/internal/types/resource" "github.com/hashicorp/boundary/internal/types/subtypes" pb "github.com/hashicorp/boundary/sdk/pbs/controller/api/resources/credentials" + "golang.org/x/crypto/ssh" "google.golang.org/grpc/codes" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/wrapperspb" @@ -324,7 +325,7 @@ func (s Service) createInRepo(ctx context.Context, scopeId string, item *pb.Cred const op = "credentials.(Service).createInRepo" switch item.GetType() { case credential.UsernamePasswordSubtype.String(): - cred, err := toUsernamePasswordStorageCredential(item.GetCredentialStoreId(), item) + cred, err := toUsernamePasswordStorageCredential(ctx, item.GetCredentialStoreId(), item) if err != nil { return nil, errors.Wrap(ctx, err, op) } @@ -380,7 +381,7 @@ func (s Service) updateInRepo( 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 := toUsernamePasswordStorageCredential(storeId, in) + cred, err := toUsernamePasswordStorageCredential(ctx, storeId, in) if err != nil { return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to convert to username/password storage credential")) } @@ -561,7 +562,7 @@ func toProto(in credential.Static, opt ...handlers.Option) (*pb.Credential, erro return &out, nil } -func toUsernamePasswordStorageCredential(storeId string, in *pb.Credential) (out *static.UsernamePasswordCredential, err error) { +func toUsernamePasswordStorageCredential(ctx context.Context, storeId string, in *pb.Credential) (out *static.UsernamePasswordCredential, err error) { const op = "credentials.toUsernamePasswordStorageCredential" var opts []static.Option if in.GetName() != nil { @@ -578,7 +579,7 @@ func toUsernamePasswordStorageCredential(storeId string, in *pb.Credential) (out credential.Password(attrs.GetPassword().GetValue()), opts...) if err != nil { - return nil, errors.WrapDeprecated(err, op, errors.WithMsg("unable to build credential")) + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to build credential")) } return cs, err @@ -602,7 +603,7 @@ func toSshPrivateKeyStorageCredential(ctx context.Context, storeId string, in *p credential.PrivateKey(attrs.GetPrivateKey().GetValue()), opts...) if err != nil { - return nil, errors.WrapDeprecated(err, op, errors.WithMsg("unable to build credential")) + return nil, errors.Wrap(ctx, err, op, errors.WithMsg("unable to build credential")) } return cs, err @@ -638,13 +639,25 @@ func validateCreateRequest(req *pbs.CreateCredentialRequest) error { if req.Item.GetUsernamePasswordAttributes().GetPassword().GetValue() == "" { badFields[passwordField] = "Field required for creating a username-password credential." } + case credential.SshPrivateKeySubtype.String(): if req.Item.GetSshPrivateKeyAttributes().GetUsername().GetValue() == "" { badFields[usernameField] = "Field required for creating an SSH private key credential." } - if req.Item.GetSshPrivateKeyAttributes().GetPrivateKey().GetValue() == "" { + privateKey := req.Item.GetSshPrivateKeyAttributes().GetPrivateKey().GetValue() + if privateKey == "" { badFields[privateKeyField] = "Field required for creating an SSH private key credential." + } else { + _, err := ssh.ParsePrivateKey([]byte(privateKey)) + switch { + case err == nil: + case err.Error() == (&ssh.PassphraseMissingError{}).Error(): + // This is okay, if it's brokered and the client can use it, no worries + default: + badFields[privateKeyField] = "Unable to parse given private key value." + } } + default: badFields[globals.TypeField] = fmt.Sprintf("Unsupported credential type %q", req.Item.GetType()) } @@ -666,14 +679,26 @@ func validateUpdateRequest(req *pbs.UpdateCredentialRequest) error { if handlers.MaskContains(req.GetUpdateMask().GetPaths(), passwordField) && attrs.GetPassword().GetValue() == "" { badFields[passwordField] = "This is a required field and cannot be set to empty." } + case credential.SshPrivateKeySubtype.String(): attrs := req.GetItem().GetSshPrivateKeyAttributes() if handlers.MaskContains(req.GetUpdateMask().GetPaths(), usernameField) && attrs.GetUsername().GetValue() == "" { badFields[usernameField] = "This is a required field and cannot be set to empty." } - if handlers.MaskContains(req.GetUpdateMask().GetPaths(), privateKeyField) && attrs.GetPrivateKey().GetValue() == "" { + privateKey := attrs.GetPrivateKey().GetValue() + if handlers.MaskContains(req.GetUpdateMask().GetPaths(), privateKeyField) && privateKey == "" { badFields[privateKeyField] = "This is a required field and cannot be set to empty." + } else { + _, err := ssh.ParsePrivateKey([]byte(privateKey)) + switch { + case err == nil: + case err.Error() == (&ssh.PassphraseMissingError{}).Error(): + // This is okay, if it's brokered and the client can use it, no worries + default: + badFields[privateKeyField] = "Unable to parse given private key value." + } } + default: badFields[globals.TypeField] = "Cannot modify resource type." }