From 9652ebe03258c518d3cee7fc57c0868b2974c60c Mon Sep 17 00:00:00 2001 From: Andrew Gaffney <109917432+anGaffney@users.noreply.github.com> Date: Fri, 5 Sep 2025 09:37:18 -0400 Subject: [PATCH] feat(cmd): Support Vault LDAP Credential Library --- internal/cmd/commands.go | 12 + .../commands/credentiallibrariescmd/funcs.go | 6 + .../vault-ldap_credentiallibraries.gen.go | 278 ++++++++++++++++++ .../vault-ldap_funcs.go | 83 ++++++ internal/cmd/gencli/input.go | 16 + .../cli/boundary/_credential_libraries.bash | 11 + .../cli/boundary/credential_libraries.bats | 67 ++++- 7 files changed, 471 insertions(+), 2 deletions(-) create mode 100644 internal/cmd/commands/credentiallibrariescmd/vault-ldap_credentiallibraries.gen.go create mode 100644 internal/cmd/commands/credentiallibrariescmd/vault-ldap_funcs.go diff --git a/internal/cmd/commands.go b/internal/cmd/commands.go index f2f5f050c6..bbd7cc37c0 100644 --- a/internal/cmd/commands.go +++ b/internal/cmd/commands.go @@ -495,6 +495,12 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) { Func: "create", } }), + "credential-libraries create vault-ldap": wrapper.Wrap(func() wrapper.WrappableCommand { + return &credentiallibrariescmd.VaultLdapCommand{ + Command: base.NewCommand(ui, opts...), + Func: "create", + } + }), "credential-libraries update": wrapper.Wrap(func() wrapper.WrappableCommand { return &credentiallibrariescmd.Command{ Command: base.NewCommand(ui, opts...), @@ -519,6 +525,12 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) { Func: "update", } }), + "credential-libraries update vault-ldap": wrapper.Wrap(func() wrapper.WrappableCommand { + return &credentiallibrariescmd.VaultLdapCommand{ + Command: base.NewCommand(ui, opts...), + Func: "update", + } + }), "credential-stores": func() (cli.Command, error) { return &credentialstorescmd.Command{ diff --git a/internal/cmd/commands/credentiallibrariescmd/funcs.go b/internal/cmd/commands/credentiallibrariescmd/funcs.go index c68d50e2f7..287ecf4c1e 100644 --- a/internal/cmd/commands/credentiallibrariescmd/funcs.go +++ b/internal/cmd/commands/credentiallibrariescmd/funcs.go @@ -151,6 +151,8 @@ func printItemTable(item *credentiallibraries.CredentialLibrary, resp *api.Respo keySubstMap = genericKeySubstMap case "vault-ssh-certificate": keySubstMap = sshCertKeySubstMap + case "vault-ldap": + keySubstMap = ldapKeySubstMap } maxLength := base.MaxAttributesLength(nonAttributeMap, item.Attributes, keySubstMap) @@ -218,3 +220,7 @@ var sshCertKeySubstMap = map[string]string{ "critical_options": "Critical Options", "extensions": "Extensions", } + +var ldapKeySubstMap = map[string]string{ + "path": "Path", +} diff --git a/internal/cmd/commands/credentiallibrariescmd/vault-ldap_credentiallibraries.gen.go b/internal/cmd/commands/credentiallibrariescmd/vault-ldap_credentiallibraries.gen.go new file mode 100644 index 0000000000..e2391ae246 --- /dev/null +++ b/internal/cmd/commands/credentiallibrariescmd/vault-ldap_credentiallibraries.gen.go @@ -0,0 +1,278 @@ +// Code generated by "make cli"; DO NOT EDIT. +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package credentiallibrariescmd + +import ( + "errors" + "fmt" + + "github.com/hashicorp/boundary/api" + "github.com/hashicorp/boundary/api/credentiallibraries" + "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 initVaultLdapFlags() { + flagsOnce.Do(func() { + extraFlags := extraVaultLdapActionsFlagsMapFunc() + for k, v := range extraFlags { + flagsVaultLdapMap[k] = append(flagsVaultLdapMap[k], v...) + } + }) +} + +var ( + _ cli.Command = (*VaultLdapCommand)(nil) + _ cli.CommandAutocomplete = (*VaultLdapCommand)(nil) +) + +type VaultLdapCommand struct { + *base.Command + + Func string + + plural string + + extraVaultLdapCmdVars +} + +func (c *VaultLdapCommand) AutocompleteArgs() complete.Predictor { + initVaultLdapFlags() + return complete.PredictAnything +} + +func (c *VaultLdapCommand) AutocompleteFlags() complete.Flags { + initVaultLdapFlags() + return c.Flags().Completions() +} + +func (c *VaultLdapCommand) Synopsis() string { + if extra := extraVaultLdapSynopsisFunc(c); extra != "" { + return extra + } + + synopsisStr := "credential-library" + + synopsisStr = fmt.Sprintf("%s %s", "vault-ldap-type", synopsisStr) + + return common.SynopsisFunc(c.Func, synopsisStr) +} + +func (c *VaultLdapCommand) Help() string { + initVaultLdapFlags() + + var helpStr string + helpMap := common.HelpMap("credential library") + + switch c.Func { + + default: + + helpStr = c.extraVaultLdapHelpFunc(helpMap) + + } + + // Keep linter from complaining if we don't actually generate code using it + _ = helpMap + return helpStr +} + +var flagsVaultLdapMap = map[string][]string{ + + "create": {"credential-store-id", "name", "description"}, + + "update": {"id", "name", "description", "version"}, +} + +func (c *VaultLdapCommand) Flags() *base.FlagSets { + if len(flagsVaultLdapMap[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, "vault-ldap-type credential library", flagsVaultLdapMap, c.Func) + + extraVaultLdapFlagsFunc(c, set, f) + + return set +} + +func (c *VaultLdapCommand) Run(args []string) int { + initVaultLdapFlags() + + switch c.Func { + case "": + return cli.RunResultHelp + + } + + c.plural = "vault-ldap-type credential library" + switch c.Func { + case "list": + c.plural = "vault-ldap-type credential libraries" + } + + f := c.Flags() + + if err := f.Parse(args); err != nil { + c.PrintCliError(err) + return base.CommandUserError + } + + if strutil.StrListContains(flagsVaultLdapMap[c.Func], "id") && c.FlagId == "" { + c.PrintCliError(errors.New("ID is required but not passed in via -id")) + return base.CommandUserError + } + + var opts []credentiallibraries.Option + + if strutil.StrListContains(flagsVaultLdapMap[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 + } + credentiallibrariesClient := credentiallibraries.NewClient(client) + + switch c.FlagName { + case "": + case "null": + opts = append(opts, credentiallibraries.DefaultName()) + default: + opts = append(opts, credentiallibraries.WithName(c.FlagName)) + } + + switch c.FlagDescription { + case "": + case "null": + opts = append(opts, credentiallibraries.DefaultDescription()) + default: + opts = append(opts, credentiallibraries.WithDescription(c.FlagDescription)) + } + + if c.FlagFilter != "" { + opts = append(opts, credentiallibraries.WithFilter(c.FlagFilter)) + } + + var version uint32 + + switch c.Func { + + case "update": + switch c.FlagVersion { + case 0: + opts = append(opts, credentiallibraries.WithAutomaticVersioning(true)) + default: + version = uint32(c.FlagVersion) + } + + } + + if ok := extraVaultLdapFlagsHandlingFunc(c, f, &opts); !ok { + return base.CommandUserError + } + + var resp *api.Response + var item *credentiallibraries.CredentialLibrary + + var createResult *credentiallibraries.CredentialLibraryCreateResult + + var updateResult *credentiallibraries.CredentialLibraryUpdateResult + + switch c.Func { + + case "create": + createResult, err = credentiallibrariesClient.Create(c.Context, "vault-ldap", c.FlagCredentialStoreId, opts...) + if exitCode := c.checkFuncError(err); exitCode > 0 { + return exitCode + } + resp = createResult.GetResponse() + item = createResult.GetItem() + + case "update": + updateResult, err = credentiallibrariesClient.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 = executeExtraVaultLdapActions(c, resp, item, err, credentiallibrariesClient, version, opts) + if exitCode := c.checkFuncError(err); exitCode > 0 { + return exitCode + } + + output, err := printCustomVaultLdapActionOutput(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 *VaultLdapCommand) 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 ( + extraVaultLdapActionsFlagsMapFunc = func() map[string][]string { return nil } + extraVaultLdapSynopsisFunc = func(*VaultLdapCommand) string { return "" } + extraVaultLdapFlagsFunc = func(*VaultLdapCommand, *base.FlagSets, *base.FlagSet) {} + extraVaultLdapFlagsHandlingFunc = func(*VaultLdapCommand, *base.FlagSets, *[]credentiallibraries.Option) bool { return true } + executeExtraVaultLdapActions = func(_ *VaultLdapCommand, inResp *api.Response, inItem *credentiallibraries.CredentialLibrary, inErr error, _ *credentiallibraries.Client, _ uint32, _ []credentiallibraries.Option) (*api.Response, *credentiallibraries.CredentialLibrary, error) { + return inResp, inItem, inErr + } + printCustomVaultLdapActionOutput = func(*VaultLdapCommand) (bool, error) { return false, nil } +) diff --git a/internal/cmd/commands/credentiallibrariescmd/vault-ldap_funcs.go b/internal/cmd/commands/credentiallibrariescmd/vault-ldap_funcs.go new file mode 100644 index 0000000000..be370e26d1 --- /dev/null +++ b/internal/cmd/commands/credentiallibrariescmd/vault-ldap_funcs.go @@ -0,0 +1,83 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package credentiallibrariescmd + +import ( + "github.com/hashicorp/boundary/api/credentiallibraries" + "github.com/hashicorp/boundary/internal/cmd/base" +) + +func init() { + extraVaultLdapActionsFlagsMapFunc = extraVaultLdapActionsFlagsMapFuncImpl + extraVaultLdapFlagsFunc = extraVaultLdapFlagsFuncImpl + extraVaultLdapFlagsHandlingFunc = extraVaultLdapFlagHandlingFuncImpl +} + +type extraVaultLdapCmdVars struct { + flagPath string +} + +func extraVaultLdapActionsFlagsMapFuncImpl() map[string][]string { + flags := map[string][]string{ + "create": { + pathFlagName, + }, + "update": { + pathFlagName, + }, + } + return flags +} + +func extraVaultLdapFlagsFuncImpl(c *VaultLdapCommand, set *base.FlagSets, _ *base.FlagSet) { + f := set.NewFlagSet("Vault Ldap Credential Library Options") + + for _, name := range flagsVaultLdapMap[c.Func] { + switch name { + case pathFlagName: + f.StringVar(&base.StringVar{ + Name: pathFlagName, + Target: &c.flagPath, + Usage: "The path in vault to request credentials from.", + }) + } + } +} + +func extraVaultLdapFlagHandlingFuncImpl(c *VaultLdapCommand, _ *base.FlagSets, opts *[]credentiallibraries.Option) bool { + switch c.flagPath { + case "": + default: + *opts = append(*opts, credentiallibraries.WithVaultLdapCredentialLibraryPath(c.flagPath)) + } + return true +} + +func (c *VaultLdapCommand) extraVaultLdapHelpFunc(_ map[string]func() string) string { + var helpStr string + switch c.Func { + case "create": + helpStr = base.WrapForHelpText([]string{ + "Usage: boundary credential-libraries create vault-ldap -credential-store-id [options] [args]", + "", + " Create a vault-ldap-type credential library. Example:", + "", + ` $ boundary credential-libraries create vault-ldap -credential-store-id csvlt_1234567890 -vault-path "/ldap/static-cred/einstein"`, + "", + "", + }) + + case "update": + helpStr = base.WrapForHelpText([]string{ + "Usage: boundary credential-libraries update vault-ldap [options] [args]", + "", + " Update a vault-ldap-type credential library given its ID. Example:", + "", + ` $ boundary credential-libraries update vault-ldap -id clvllt_1234567890 -name devops -description "For DevOps usage"`, + "", + "", + }) + } + return helpStr + c.Flags().Help() +} diff --git a/internal/cmd/gencli/input.go b/internal/cmd/gencli/input.go index b6174b96ca..b5c1b68a0f 100644 --- a/internal/cmd/gencli/input.go +++ b/internal/cmd/gencli/input.go @@ -357,6 +357,22 @@ var inputStructs = map[string][]*cmdInfo{ VersionedActions: []string{"update"}, PrefixAttributeFieldErrorsWithSubactionPrefix: true, }, + { + ResourceType: resource.CredentialLibrary.String(), + Pkg: "credentiallibraries", + StdActions: []string{"create", "update"}, + SubActionPrefix: "vault-ldap", + HasExtraCommandVars: true, + SkipNormalHelp: true, + HasExtraHelpFunc: true, + HasId: true, + HasName: true, + HasDescription: true, + NeedsSubtypeInCreate: true, + Container: "CredentialStore", + VersionedActions: []string{"update"}, + PrefixAttributeFieldErrorsWithSubactionPrefix: true, + }, }, "credentials": { { diff --git a/internal/tests/cli/boundary/_credential_libraries.bash b/internal/tests/cli/boundary/_credential_libraries.bash index a4ba27e689..534e6ec1c1 100644 --- a/internal/tests/cli/boundary/_credential_libraries.bash +++ b/internal/tests/cli/boundary/_credential_libraries.bash @@ -18,6 +18,17 @@ function create_vault_generic_library() { create vault-generic $@ } +function create_vault_ldap_library() { + boundary credential-libraries \ + create vault-ldap $@ +} + +function update_vault_ldap_library() { + boundary credential-libraries \ + update vault-ldap $@ +} + + function create_vault_library() { boundary credential-libraries \ create vault $@ diff --git a/internal/tests/cli/boundary/credential_libraries.bats b/internal/tests/cli/boundary/credential_libraries.bats index 33d086aacc..47f9677f99 100644 --- a/internal/tests/cli/boundary/credential_libraries.bats +++ b/internal/tests/cli/boundary/credential_libraries.bats @@ -52,7 +52,7 @@ export NEW_VAULT_LIB="test_vault" [ "$status" -eq 0 ] } -@test "boundary/credential-libraries: can not create already created $NEW_VAULT_LIB vault-generic libary" { +@test "boundary/credential-libraries: can not create already created $NEW_VAULT_LIB vault-generic library" { skip_if_no_vault local csid=$(credential_store_id $NEW_STORE $DEFAULT_P_ID) @@ -106,7 +106,7 @@ export NEW_VAULT_LIB="test_vault" [ "$status" -eq 0 ] } -@test "boundary/credential-libraries: can not create already created $NEW_VAULT_LIB vault-ssh-certificate libary" { +@test "boundary/credential-libraries: can not create already created $NEW_VAULT_LIB vault-ssh-certificate library" { skip_if_no_vault local csid=$(credential_store_id $NEW_STORE $DEFAULT_P_ID) @@ -587,6 +587,69 @@ export NEW_VAULT_LIB="test_vault" [ "$status" -eq 0 ] } +@test "boundary/credential-libraries: can create $NEW_VAULT_LIB vault-ldap library in credential store $NEW_STORE" { + skip_if_no_vault + + local csid=$(credential_store_id $NEW_STORE $DEFAULT_P_ID) + run create_vault_ldap_library \ + -name $NEW_VAULT_LIB -credential-store-id $csid \ + -vault-path /ldap/static-cred/einstein \ + echo "$output" + [ "$status" -eq 0 ] +} + + +@test "boundary/credential-libraries: can not create already created $NEW_VAULT_LIB vault-ldap library" { + skip_if_no_vault + + local csid=$(credential_store_id $NEW_STORE $DEFAULT_P_ID) + run create_vault_ldap_library \ + -name $NEW_VAULT_LIB -credential-store-id $csid \ + -vault-path /ldap/static-cred/einstein \ + echo "$output" + [ "$status" -eq 1 ] +} + +@test "boundary/credential-libraries: can update $NEW_VAULT_LIB vault-ldap library description" { + skip_if_no_vault + + local csid=$(credential_store_id $NEW_STORE $DEFAULT_P_ID) + local clid=$(credential_library_id $NEW_VAULT_LIB $csid) + run update_vault_ldap_library -id $clid -description hellothere + echo "$output" + [ "$status" -eq 0 ] + + run read_credential_library $clid + echo "$output" + [ "$status" -eq 0 ] + got=$(echo "$output") + + run field_eq "$got" ".item.attributes.description" "null" + [ "$status" -eq 0 ] +} + +@test "boundary/credential-libraries: can read $NEW_VAULT_LIB vault-ldap library" { + skip_if_no_vault + + local csid=$(credential_store_id $NEW_STORE $DEFAULT_P_ID) + local clid=$(credential_library_id $NEW_VAULT_LIB $csid) + run read_credential_library $clid + echo "$output" + [ "$status" -eq 0 ] +} + +@test "boundary/credential-libraries: can delete $NEW_VAULT_LIB vault-ldaplibrary" { + skip_if_no_vault + + local csid=$(credential_store_id $NEW_STORE $DEFAULT_P_ID) + local clid=$(credential_library_id $NEW_VAULT_LIB $csid) + run delete_credential_library $clid + echo "$output" + [ "$status" -eq 0 ] + run has_status_code "$output" "204" + [ "$status" -eq 0 ] +} + # Note, deleting the cred store will revoke the vault token @test "boundary/credential-stores: cleanup can delete $NEW_STORE vault store" { skip_if_no_vault