package targetscmd import ( "bytes" "encoding/base64" "encoding/json" "fmt" "strings" "time" "github.com/hashicorp/boundary/api" "github.com/hashicorp/boundary/api/scopes" "github.com/hashicorp/boundary/api/targets" "github.com/hashicorp/boundary/globals" "github.com/hashicorp/boundary/internal/cmd/base" "github.com/hashicorp/boundary/internal/credential" "github.com/hashicorp/shared-secure-libs/strutil" "github.com/mitchellh/go-wordwrap" "github.com/posener/complete" ) func init() { extraActionsFlagsMapFunc = extraActionsFlagsMapFuncImpl extraSynopsisFunc = extraSynopsisFuncImpl extraFlagsFunc = extraFlagsFuncImpl extraFlagsHandlingFunc = extraFlagsHandlingFuncImpl executeExtraActions = executeExtraActionsImpl printCustomActionOutput = printCustomActionOutputImpl } type extraCmdVars struct { flagHostSets []string flagApplicationCredentialLibraries []string flagHostId string sar *targets.SessionAuthorizationResult } func extraActionsFlagsMapFuncImpl() map[string][]string { return map[string][]string{ "authorize-session": {"id", "host-id"}, "add-host-sets": {"id", "host-set", "version"}, "remove-host-sets": {"id", "host-set", "version"}, "set-host-sets": {"id", "host-set", "version"}, "add-credential-libraries": {"id", "application-credential-library", "version"}, "remove-credential-libraries": {"id", "application-credential-library", "version"}, "set-credential-libraries": {"id", "application-credential-library", "version"}, } } func extraSynopsisFuncImpl(c *Command) string { switch c.Func { case "add-host-sets", "set-host-sets", "remove-host-sets": var in string switch { case strings.HasPrefix(c.Func, "add"): in = "Add host sets to" case strings.HasPrefix(c.Func, "set"): in = "Set the full contents of the host sets on" case strings.HasPrefix(c.Func, "remove"): in = "Remove host sets from" } return wordwrap.WrapString(fmt.Sprintf("%s a target", in), base.TermWidth) case "add-credential-libraries", "set-credential-libraries", "remove-credential-libraries": var in string switch { case strings.HasPrefix(c.Func, "add"): in = "Add credential library to" case strings.HasPrefix(c.Func, "set"): in = "Set the full contents of the credential libraries on" case strings.HasPrefix(c.Func, "remove"): in = "Remove credential libraries from" } return wordwrap.WrapString(fmt.Sprintf("%s a target", in), base.TermWidth) case "authorize-session": return "Request session authorization against the target" default: return "" } } func (c *Command) extraHelpFunc(helpMap map[string]func() string) string { var helpStr string switch c.Func { case "": return base.WrapForHelpText([]string{ "Usage: boundary targets [sub command] [options] [args]", "", " This command allows operations on Boundary target resources. Example:", "", " Read a target:", "", ` $ boundary targets read -id ttcp_1234567890`, "", " Please see the targets subcommand help for detailed usage information.", }) case "create": helpStr = base.WrapForHelpText([]string{ "Usage: boundary targets create [type] [sub command] [options] [args]", "", " This command allows create operations on Boundary target resources. Example:", "", " Create a tcp-type target:", "", ` $ boundary targets create tcp -name prodops -description "For ProdOps usage"`, "", " Please see the typed subcommand help for detailed usage information.", }) case "update": helpStr = base.WrapForHelpText([]string{ "Usage: boundary targets update [type] [sub command] [options] [args]", "", " This command allows update operations on Boundary target resources. Example:", "", " Update a tcp-type target:", "", ` $ boundary targets update tcp -id ttcp_1234567890 -name devops -description "For DevOps usage"`, "", " Please see the typed subcommand help for detailed usage information.", }) case "add-host-sets": helpStr = base.WrapForHelpText([]string{ "Usage: boundary target add-host-sets [options] [args]", "", " This command allows adding host-set resources to target resources. Example:", "", " Add host-set resources to a tcp-type target:", "", ` $ boundary targets add-host-sets -id ttcp_1234567890 -host-set hsst_1234567890 -host-set hsst_0987654321`, "", "", }) case "remove-host-sets": helpStr = base.WrapForHelpText([]string{ "Usage: boundary target remove-host-sets [options] [args]", "", " This command allows removing host-set resources from target resources. Example:", "", " Remove host-set resources from a tcp-type target:", "", ` $ boundary targets add-host-sets -id ttcp_1234567890 -host hsst_1234567890 -host-set hsst_0987654321`, "", "", }) case "set-host-sets": helpStr = base.WrapForHelpText([]string{ "Usage: boundary target set-host-sets [options] [args]", "", " This command allows setting the complete set of host-set resources on a target resource. Example:", "", " Set host-set resources on a tcp-type target:", "", ` $ boundary targets set-host-sets -id ttcp_1234567890 -host-set hsst_1234567890`, "", "", }) case "add-credential-libraries": helpStr = base.WrapForHelpText([]string{ "Usage: boundary target add-credential-libraries [options] [args]", "", " This command allows adding credential-library resources to target resources. Example:", "", " Add credential-library resources to a tcp-type target:", "", ` $ boundary targets add-credential-libraries -id ttcp_1234567890 -application-credential-library clvlt_1234567890 -application-credential-library clvlt_0987654321`, "", "", }) case "remove-credential-libraries": helpStr = base.WrapForHelpText([]string{ "Usage: boundary target remove-credential-libraries [options] [args]", "", " This command allows removing credential-library resources from target resources. Example:", "", " Remove credential-library resources from a tcp-type target:", "", ` $ boundary targets remove-credential-libraries -id ttcp_1234567890 -application-credential-library clvlt_1234567890 -application-credential-library clvlt_0987654321`, "", "", }) case "set-credential-libraries": helpStr = base.WrapForHelpText([]string{ "Usage: boundary target set-credential-libraries [options] [args]", "", " This command allows setting the complete set of credential-library resources on a target resource. Example:", "", " Set credential-library resources on a tcp-type target:", "", ` $ boundary targets set-credential-libraries -id ttcp_1234567890 -application-credential-library clvlt_1234567890`, "", "", }) case "authorize-session": helpStr = base.WrapForHelpText([]string{ "Usage: boundary target authorize-session [options] [args]", "", " This command allows fetching session authorization credentials against a target. Example:", "", " Request an authorized session using the target ID:", "", ` $ boundary targets authorize-session -id ttcp_1234567890`, "", " Request an authorized session using the scope ID and target name:", "", ` $ boundary targets authorize-session -scope-id o_1234567890 -name prod-ssh`, "", "", }) } return helpStr + c.Flags().Help() } func extraFlagsFuncImpl(c *Command, _ *base.FlagSets, f *base.FlagSet) { for _, name := range flagsMap[c.Func] { switch name { case "host-set": f.StringSliceVar(&base.StringSliceVar{ Name: "host-set", Target: &c.flagHostSets, Usage: "The host-set resources to add, remove, or set. May be specified multiple times.", }) case "host-id": f.StringVar(&base.StringVar{ Name: "host-id", Target: &c.flagHostId, Usage: "The ID of a specific host to connect to out of the hosts from the target's host sets. If not specified, one is chosen at random.", }) case "application-credential-library": f.StringSliceVar(&base.StringSliceVar{ Name: "application-credential-library", Target: &c.flagApplicationCredentialLibraries, Usage: "The credential-library resource for application purpose to add, set, or remove. May be specified multiple times.", }) } } if c.Func == "authorize-session" { flagsMap[c.Func] = append(flagsMap[c.Func], "name", "scope-id", "scope-name") // We put these here to change usage and change defaults (don't want // them populated by default). Otherwise the common flags function will // populate these values, and they can't be changed after-the-fact. f.StringVar(&base.StringVar{ Name: "name", Target: &c.FlagName, Usage: "Target name, if authorizing the session via scope parameters and target name.", }) f.StringVar(&base.StringVar{ Name: "scope-id", Target: &c.FlagScopeId, EnvVar: "BOUNDARY_SCOPE_ID", Completion: complete.PredictAnything, Usage: "Target scope ID, if authorizing the session via scope parameters and target name. Mutually exclusive with -scope-name.", }) f.StringVar(&base.StringVar{ Name: "scope-name", Target: &c.FlagScopeName, EnvVar: "BOUNDARY_SCOPE_NAME", Completion: complete.PredictAnything, Usage: "Target scope name, if authorizing the session via scope parameters and target name. Mutually exclusive with -scope-id.", }) } } func extraFlagsHandlingFuncImpl(c *Command, _ *base.FlagSets, opts *[]targets.Option) bool { // This is custom logic because of the authorized-session handling. If we // support all resources to be looked up by name + scope info we can // eventually graduate this out to the main template. if strutil.StrListContains(flagsMap[c.Func], "id") { switch c.Func { case "authorize-session": if c.FlagId == "" && (c.FlagName == "" || (c.FlagScopeId == "" && c.FlagScopeName == "")) { c.UI.Error("ID was not passed in, but no combination of name and scope ID/name was passed in either") return false } if c.FlagId != "" && (c.FlagName != "" || c.FlagScopeId != "" || c.FlagScopeName != "") { c.UI.Error("Cannot specify a target ID and also other lookup parameters") return false } default: if c.FlagId == "" { c.UI.Error("ID is required but not passed in via -id") return false } } } if strutil.StrListContains(flagsMap[c.Func], "scope-id") && c.FlagScopeId != "" { *opts = append(*opts, targets.WithScopeId(c.FlagScopeId)) } switch c.Func { case "add-host-sets", "remove-host-sets": if len(c.flagHostSets) == 0 { c.UI.Error("No host-sets supplied via -host-set") return false } case "set-host-sets": switch len(c.flagHostSets) { case 0: c.UI.Error("No host-sets supplied via -host-set") return false case 1: if c.flagHostSets[0] == "null" { c.flagHostSets = nil } } case "add-credential-libraries", "remove-credential-libraries": // TODO: As we add other purposes, add them to this check if len(c.flagApplicationCredentialLibraries) == 0 { c.UI.Error("No credential-libraries supplied via -application-credential-library") return false } *opts = append(*opts, targets.WithApplicationCredentialLibraryIds(c.flagApplicationCredentialLibraries)) case "set-credential-libraries": // TODO: As we add other purposes, add them to this check switch len(c.flagApplicationCredentialLibraries) { case 0: c.UI.Error("No credential-libraries supplied via -application-credential-library") return false case 1: if c.flagApplicationCredentialLibraries[0] == "null" { *opts = append(*opts, targets.DefaultApplicationCredentialLibraryIds()) break } fallthrough default: *opts = append(*opts, targets.WithApplicationCredentialLibraryIds(c.flagApplicationCredentialLibraries)) } case "authorize-session": if len(c.flagHostId) != 0 { *opts = append(*opts, targets.WithHostId(c.flagHostId)) } } return true } func executeExtraActionsImpl(c *Command, origResult api.GenericResult, origError error, targetClient *targets.Client, version uint32, opts []targets.Option) (api.GenericResult, error) { switch c.Func { case "add-host-sets": return targetClient.AddHostSets(c.Context, c.FlagId, version, c.flagHostSets, opts...) case "remove-host-sets": return targetClient.RemoveHostSets(c.Context, c.FlagId, version, c.flagHostSets, opts...) case "set-host-sets": return targetClient.SetHostSets(c.Context, c.FlagId, version, c.flagHostSets, opts...) case "add-credential-libraries": return targetClient.AddCredentialLibraries(c.Context, c.FlagId, version, opts...) case "remove-credential-libraries": return targetClient.RemoveCredentialLibraries(c.Context, c.FlagId, version, opts...) case "set-credential-libraries": return targetClient.SetCredentialLibraries(c.Context, c.FlagId, version, opts...) case "authorize-session": var err error c.plural = "a session against target" c.sar, err = targetClient.AuthorizeSession(c.Context, c.FlagId, opts...) return nil, err } return origResult, origError } func (c *Command) printListTable(items []*targets.Target) string { if len(items) == 0 { return "No targets found" } var output []string output = []string{ "", "Target information:", } for i, item := range items { if i > 0 { output = append(output, "") } if item.Id != "" { output = append(output, fmt.Sprintf(" ID: %s", item.Id), ) } else { output = append(output, fmt.Sprintf(" ID: %s", "(not available)"), ) } if c.FlagRecursive && item.ScopeId != "" { output = append(output, fmt.Sprintf(" Scope ID: %s", item.ScopeId), ) } if item.Version > 0 { output = append(output, fmt.Sprintf(" Version: %d", item.Version), ) } if item.Type != "" { output = append(output, fmt.Sprintf(" Type: %s", item.Type), ) } if item.Name != "" { output = append(output, fmt.Sprintf(" Name: %s", item.Name), ) } if item.Description != "" { output = append(output, fmt.Sprintf(" Description: %s", item.Description), ) } if len(item.AuthorizedActions) > 0 { output = append(output, " Authorized Actions:", base.WrapSlice(6, item.AuthorizedActions), ) } } return base.WrapForHelpText(output) } func printItemTable(result api.GenericResult) string { item := result.GetItem().(*targets.Target) nonAttributeMap := map[string]interface{}{} if item.Id != "" { nonAttributeMap["ID"] = item.Id } if item.Version != 0 { nonAttributeMap["Version"] = item.Version } if item.Type != "" { nonAttributeMap["Type"] = item.Type } if !item.CreatedTime.IsZero() { nonAttributeMap["Created Time"] = item.CreatedTime.Local().Format(time.RFC1123) } if !item.UpdatedTime.IsZero() { nonAttributeMap["Updated Time"] = item.UpdatedTime.Local().Format(time.RFC1123) } if item.Name != "" { nonAttributeMap["Name"] = item.Name } if item.Description != "" { nonAttributeMap["Description"] = item.Description } if item.WorkerFilter != "" { nonAttributeMap["Worker Filter"] = item.WorkerFilter } if result.GetResponse() != nil && result.GetResponse().Map != nil { if result.GetResponse().Map[globals.SessionConnectionLimitField] != nil { nonAttributeMap["Session Connection Limit"] = item.SessionConnectionLimit } if result.GetResponse().Map[globals.SessionMaxSecondsField] != nil { nonAttributeMap["Session Max Seconds"] = item.SessionMaxSeconds } } maxLength := base.MaxAttributesLength(nonAttributeMap, item.Attributes, keySubstMap) var hostSetMaps []map[string]interface{} if len(item.HostSets) > 0 { for _, set := range item.HostSets { m := map[string]interface{}{ "ID": set.Id, "Host Catalog ID": set.HostCatalogId, } hostSetMaps = append(hostSetMaps, m) } if l := len("Host Catalog ID"); l > maxLength { maxLength = l } } var libraryMaps map[credential.Purpose][]map[string]interface{} if len(item.ApplicationCredentialLibraries) > 0 { if libraryMaps == nil { libraryMaps = make(map[credential.Purpose][]map[string]interface{}) } var applicationLibraryMaps []map[string]interface{} for _, lib := range item.ApplicationCredentialLibraries { m := map[string]interface{}{ "ID": lib.Id, "Credential Store ID": lib.CredentialStoreId, } applicationLibraryMaps = append(applicationLibraryMaps, m) } libraryMaps[credential.ApplicationPurpose] = applicationLibraryMaps if l := len("Credential Store ID"); l > maxLength { maxLength = l } } ret := []string{ "", "Target information:", base.WrapMap(2, maxLength+2, nonAttributeMap), } if item.Scope != nil { ret = append(ret, "", " Scope:", base.ScopeInfoForOutput(item.Scope, maxLength), ) } if len(item.AuthorizedActions) > 0 { ret = append(ret, "", " Authorized Actions:", base.WrapSlice(4, item.AuthorizedActions), ) } ret = append(ret, "", ) if len(item.HostSets) > 0 { ret = append(ret, " Host Sets:", ) for _, m := range hostSetMaps { ret = append(ret, base.WrapMap(4, maxLength, m), "", ) } } if len(libraryMaps) > 0 { if appMap := libraryMaps[credential.ApplicationPurpose]; len(appMap) > 0 { ret = append(ret, " Application Credential Libraries:", ) for _, m := range appMap { ret = append(ret, base.WrapMap(4, maxLength, m), "", ) } } } if len(item.Attributes) > 0 { ret = append(ret, " Attributes:", base.WrapMap(4, maxLength, item.Attributes), ) } return base.WrapForHelpText(ret) } func printCustomActionOutputImpl(c *Command) (bool, error) { switch c.Func { case "authorize-session": item := c.sar.GetItem().(*targets.SessionAuthorization) switch base.Format(c.UI) { case "table": var ret []string nonAttributeMap := map[string]interface{}{ "Session ID": item.SessionId, "Target ID": item.TargetId, "Scope ID": item.Scope.Id, "User ID": item.UserId, "Host ID": item.HostId, "Endpoint": item.Endpoint, "Created Time": item.CreatedTime.Local().Format(time.RFC1123), "Type": item.Type, "Authorization Token": item.AuthorizationToken, } maxLength := 0 for k := range nonAttributeMap { if len(k) > maxLength { maxLength = len(k) } } ret = append(ret, "", "Target information:") ret = append(ret, // We do +2 because there is another +2 offset for credentials below base.WrapMap(2, maxLength+2, nonAttributeMap), ) ret = append(ret, "", ) if len(item.Credentials) > 0 { ret = append(ret, " Credentials:", ) for _, cred := range item.Credentials { if cred.Secret == nil || len(cred.Secret.Raw) == 0 { continue } ret = append(ret, fmt.Sprintf(" Credential Store ID: %s", cred.CredentialLibrary.CredentialStoreId), fmt.Sprintf(" Credential Library ID: %s", cred.CredentialLibrary.Id), fmt.Sprintf(" Credential Library Type: %s", cred.CredentialLibrary.Type)) if len(cred.CredentialLibrary.Name) > 0 { ret = append(ret, fmt.Sprintf(" Credential Library Name: %s", cred.CredentialLibrary.Name)) } if len(cred.CredentialLibrary.Description) > 0 { ret = append(ret, fmt.Sprintf(" Credential Library Description: %s", cred.CredentialLibrary.Description)) } var secretStr []string switch cred.CredentialLibrary.Type { case "vault": // If it's Vault, the result will be JSON, except in // specific circumstances that aren't used for // credential fetching. So we can take the bytes // as-is (after base64-decoding), but we'll format // it nicely. in, err := base64.StdEncoding.DecodeString(strings.Trim(string(cred.Secret.Raw), `"`)) if err != nil { return false, fmt.Errorf("Error decoding secret as base64: %w", err) } dst := new(bytes.Buffer) if err := json.Indent(dst, in, " ", " "); err != nil { return false, fmt.Errorf("Error pretty-printing JSON: %w", err) } secretStr = strings.Split(dst.String(), "\n") if len(secretStr) > 0 { // Indent doesn't apply to the first line 🙄 secretStr[0] = fmt.Sprintf(" %s", secretStr[0]) } default: // If it's not Vault, and not another known type, // print out the base64-encoded value and leave it // to the user to sort out. secretStr = []string{fmt.Sprintf(" %s", secretStr)} } ret = append(ret, " Secret:") ret = append(ret, secretStr...) ret = append(ret, "") } } c.UI.Output(base.WrapForHelpText(ret)) return true, nil case "json": if ok := c.PrintJsonItem(c.sar); !ok { return false, fmt.Errorf("Error formatting as JSON") } return true, nil } } return false, nil } var keySubstMap = map[string]string{ "default_port": "Default Port", } func exampleOutput() string { item := &targets.Target{ Id: "ttcp_1234567890", ScopeId: "global", Scope: &scopes.ScopeInfo{ Id: "global", }, Name: "foo", Description: "The bar of foos", CreatedTime: time.Now().Add(-5 * time.Minute), UpdatedTime: time.Now(), Version: 3, Type: "tcp", HostSetIds: []string{"hsst_1234567890", "hsst_0987654321"}, HostSets: []*targets.HostSet{ { Id: "hsst_1234567890", HostCatalogId: "hcst_1234567890", }, { Id: "hsst_0987654321", HostCatalogId: "hcst_1234567890", }, }, ApplicationCredentialLibraryIds: []string{"clvlt_1234567890", "clvlt_0987654321"}, ApplicationCredentialLibraries: []*targets.CredentialLibrary{ { Id: "clvlt_1234567890", CredentialStoreId: "csvlt_1234567890", }, { Id: "clvlt_098765421", CredentialStoreId: "clvlt_0987654321", }, }, Attributes: map[string]interface{}{ "default_port": 22, }, } target := new(targets.TargetReadResult) target.Item = item return printItemTable(target) }