From 7cfb05032422b650ec83ec4e7e43c51d6bdad598 Mon Sep 17 00:00:00 2001 From: Le Zhang Date: Thu, 10 Aug 2023 13:06:34 -0400 Subject: [PATCH] Issue open-horizon#3717 - NodeSecret: 'hzn secretsmanager secrete' commands supports node secret format Signed-off-by: Le Zhang --- cli/hzn.go | 12 ++-- cli/secrets_manager/secrets_manager.go | 83 +++++++++++++++++++++++--- 2 files changed, 83 insertions(+), 12 deletions(-) diff --git a/cli/hzn.go b/cli/hzn.go index 88dd90c84..639c26c9b 100644 --- a/cli/hzn.go +++ b/cli/hzn.go @@ -801,17 +801,21 @@ Environment Variables: smUserPw := smCmd.Flag("user-pw", msgPrinter.Sprintf("Horizon Exchange credentials to query secrets manager resources. The default is HZN_EXCHANGE_USER_AUTH environment variable. If you don't prepend it with the user's org, it will automatically be prepended with the value of the HZN_ORG_ID environment variable.")).Short('u').PlaceHolder("USER:PW").String() smSecretCmd := smCmd.Command("secret", msgPrinter.Sprintf("List and manage secrets in the secrets manager.")) smSecretListCmd := smSecretCmd.Command("list | ls", msgPrinter.Sprintf("Display the names of the secrets in the secrets manager.")).Alias("ls").Alias("list") + smSecretListNodeId := smSecretListCmd.Flag("nodeId", msgPrinter.Sprintf("The node id of the node secret to list. If omit, the secret to list will be org or user secret")).Short('n').String() smSecretListName := smSecretListCmd.Arg("secretName", msgPrinter.Sprintf("List just this one secret. Returns a boolean indicating the existence of the secret. This is the name of the secret used in the secrets manager. If the secret does not exist, returns with exit code 1.")).String() smSecretAddCmd := smSecretCmd.Command("add", msgPrinter.Sprintf("Add a secret to the secrets manager.")) smSecretAddName := smSecretAddCmd.Arg("secretName", msgPrinter.Sprintf("The name of the secret. It must be unique within your organization. This name is used in deployment policies and patterns to bind this secret to a secret name in a service definition.")).Required().String() + smSecretAddNodeId := smSecretAddCmd.Flag("nodeId", msgPrinter.Sprintf("The node id that this secret will be applied to. If omit, the secret will be added as org or user secret")).Short('n').String() smSecretAddFile := smSecretAddCmd.Flag("secretFile", msgPrinter.Sprintf("Filepath to a file containing the secret details. Mutually exclusive with --secretDetail. Specify -f- to read from stdin.")).Short('f').String() smSecretAddKey := smSecretAddCmd.Flag("secretKey", msgPrinter.Sprintf("A key for the secret.")).Required().String() smSecretAddDetail := smSecretAddCmd.Flag("secretDetail", msgPrinter.Sprintf("The secret details as a string. Secret details are the actual secret itself, not the name of the secret. For example, a password, a private key, etc. are examples of secret details. Mutually exclusive with --secretFile.")).Short('d').String() smSecretAddOverwrite := smSecretAddCmd.Flag("overwrite", msgPrinter.Sprintf("Overwrite the existing secret if it exists in the secrets manager. It will skip the 'do you want to overwrite' prompt.")).Short('O').Bool() smSecretRemoveCmd := smSecretCmd.Command("remove | rm", msgPrinter.Sprintf("Remove a secret in the secrets manager.")).Alias("rm").Alias("remove") + smSecretRemoveNodeId := smSecretRemoveCmd.Flag("nodeId", msgPrinter.Sprintf("The node id of the secret to be removed. If omit, the secret to be removed will be org or user secret")).Short('n').String() smSecretRemoveForce := smSecretRemoveCmd.Flag("force", msgPrinter.Sprintf("Skip the 'are you sure?' prompt.")).Short('f').Bool() smSecretRemoveName := smSecretRemoveCmd.Arg("secretName", msgPrinter.Sprintf("The name of the secret to be removed from the secrets manager.")).Required().String() smSecretReadCmd := smSecretCmd.Command("read", msgPrinter.Sprintf("Read the details of a secret stored in the secrets manager. This consists of the key and value pair provided on secret creation.")) + smSecretReadNodeId := smSecretReadCmd.Flag("nodeId", msgPrinter.Sprintf("The node id of the node secret to read. If omit, the secret to read will be org or user secret")).Short('n').String() smSecretReadName := smSecretReadCmd.Arg("secretName", msgPrinter.Sprintf("The name of the secret to read in the secrets manager.")).Required().String() versionCmd := app.Command("version", msgPrinter.Sprintf("Show the Horizon version.")) // using a cmd for this instead of --version flag, because kingpin takes over the latter and can't get version only when it is needed @@ -1504,12 +1508,12 @@ Environment Variables: fdo.VoucherDownload(*fdoOrg, *fdoUserPw, *fdoVoucherDownloadDevice, *fdoVoucherDownloadFile, *fdoVoucherDownloadOverwrite) case smSecretListCmd.FullCommand(): - secret_manager.SecretList(*smOrg, *smUserPw, *smSecretListName) + secret_manager.SecretList(*smOrg, *smUserPw, *smSecretListName, *smSecretListNodeId) case smSecretAddCmd.FullCommand(): - secret_manager.SecretAdd(*smOrg, *smUserPw, *smSecretAddName, *smSecretAddFile, *smSecretAddKey, *smSecretAddDetail, *smSecretAddOverwrite) + secret_manager.SecretAdd(*smOrg, *smUserPw, *smSecretAddName, *smSecretAddNodeId, *smSecretAddFile, *smSecretAddKey, *smSecretAddDetail, *smSecretAddOverwrite) case smSecretRemoveCmd.FullCommand(): - secret_manager.SecretRemove(*smOrg, *smUserPw, *smSecretRemoveName, *smSecretRemoveForce) + secret_manager.SecretRemove(*smOrg, *smUserPw, *smSecretRemoveName, *smSecretRemoveNodeId, *smSecretRemoveForce) case smSecretReadCmd.FullCommand(): - secret_manager.SecretRead(*smOrg, *smUserPw, *smSecretReadName) + secret_manager.SecretRead(*smOrg, *smUserPw, *smSecretReadName, *smSecretReadNodeId) } } diff --git a/cli/secrets_manager/secrets_manager.go b/cli/secrets_manager/secrets_manager.go index 61985051f..a9ace9b2d 100644 --- a/cli/secrets_manager/secrets_manager.go +++ b/cli/secrets_manager/secrets_manager.go @@ -63,10 +63,16 @@ func queryWithRetry(query func() int, retryCount, retryInterval int) (httpCode i return httpCode } -// If secretName is empty, lists all the org level secrets and non-empty directories for the specified org in the secrets manager -// If secretName is specified, prints a json object indicating whether the given secret exists or not in the secrets manager for the org -// If the name provided is a directory, lists all the secrets in the directory. -func SecretList(org, credToUse, secretName string) { +// If secretName is empty +// - nodeId is empty: lists all the org level secrets and non-empty directories for the specified org in the secrets manager +// - else: list all the node secrets for the specified org +// If secretName is +// - nodeId is empty: prints a json object indicating whether the given secret exists or not in the secrets manager for the org +// - else: prints node secrets as a json object indicating whether the given secret exists or not +// If the name provided is a directory, +// - nodeId is empty: lists all the secrets in the directory +// - else: list all the node secrets for the given directory +func SecretList(org, credToUse, secretName, secretNodeId string) { // get message printer msgPrinter := i18n.GetMessagePrinter() @@ -75,9 +81,22 @@ func SecretList(org, credToUse, secretName string) { secretName = secretName[:len(secretName)-1] } + if !strings.Contains(secretName, "/node") && secretNodeId != "" { + // add /node/{nodeID} to the path + secretName = getSecretPathForNodeLevelSecret(secretName, secretNodeId) + } + // if given secretName := "", nodeId is specified, then secretName will be convert to "node/{nodeId}" + // if given secretName is a directory: "user/userdevadmin", nodeId is specified, then secretName will be converted to "user/{userId}/node/{nodeId}" + // if given secretName is "test-password", nodeId is specified, then secretname will be converted to "node/{nodeId}/test-password" + // if given secretName is "user/userdevadmin/test-password", nodeId is specified, then secretName will be converted to "user/{userId}/node/{nodeId}/test-password": + // query the agbot secure api var resp []byte listQuery := func() int { + // call LIST: org/secrets/node/{nodeId} + // org/secrets/user/{userId}/node/{nodeId} + // org/secrets/node/{nodeId}/test-password + // org/secrets/user/{userId}/node/{nodeId}/test-password return cliutils.AgbotList("org"+cliutils.AddSlash(org)+"/secrets"+cliutils.AddSlash(secretName), cliutils.OrgAndCreds(org, credToUse), []int{200, 400, 401, 403, 404, 503, 504}, &resp) } @@ -89,12 +108,18 @@ func SecretList(org, credToUse, secretName string) { isSecretDirectory := secretName == "" // listing user secrets - user/ + // listing org node secrets - node/ + // listing user node secrets - user//node/ if !isSecretDirectory { nameParts := strings.Split(secretName, "/") partsLength := len(nameParts) - isSecretDirectory = nameParts[0] == "user" && partsLength == 2 + isUserSecretDirectory := nameParts[0] == "user" && (partsLength == 2 || partsLength == 4) + isOrgNodeSecretDirectory := nameParts[0] == "node" && partsLength == 2 + isSecretDirectory = isUserSecretDirectory || isOrgNodeSecretDirectory } + fmt.Printf("Lily - isSecretDirectory: %v\n", isSecretDirectory) + // parse and print the response if retCode == 400 || retCode == 401 || retCode == 403 || retCode == 503 || retCode == 504 { respString, _ := strconv.Unquote(string(resp)) @@ -134,7 +159,7 @@ func SecretList(org, credToUse, secretName string) { // Adds or updates a secret in the secrets manager. Secret names are unique, if a secret already exists with the same name, the user // will be prompted if they want to overwrite the existing secret, unless the secretOverwrite flag is set -func SecretAdd(org, credToUse, secretName, secretFile, secretKey, secretDetail string, secretOverwrite bool) { +func SecretAdd(org, credToUse, secretName, secretNodeId, secretFile, secretKey, secretDetail string, secretOverwrite bool) { // get message printer msgPrinter := i18n.GetMessagePrinter() @@ -143,6 +168,12 @@ func SecretAdd(org, credToUse, secretName, secretFile, secretKey, secretDetail s secretName = secretName[:len(secretName)-1] } + // org: test-password, user secret: user/userdevadmin/test-password + if !strings.Contains(secretName, "/node") && secretNodeId != "" { + // add /node/{nodeID} to the path + secretName = getSecretPathForNodeLevelSecret(secretName, secretNodeId) + } + // check the input if secretFile != "" && secretDetail != "" { cliutils.Fatal(cliutils.CLI_INPUT_ERROR, msgPrinter.Sprintf("-f is mutually exclusive with --secretDetail.")) @@ -151,6 +182,9 @@ func SecretAdd(org, credToUse, secretName, secretFile, secretKey, secretDetail s cliutils.Fatal(cliutils.CLI_INPUT_ERROR, msgPrinter.Sprintf("Must specify either -f or --secretDetail.")) } + // eg: + // org secret: hzn sm secret add --secretKey password -d password321 -O -o userdev -u userdevadmin:userdevadminpw test-password + // user secret: hzn sm secret add --secretKey password -d password123 -o userdev -u userdevadmin:userdevadminpw user/userdevadmin/test-password // check if the secret already exists by querying the api secretExists := false var resp []byte @@ -222,7 +256,7 @@ func SecretAdd(org, credToUse, secretName, secretFile, secretKey, secretDetail s } // Removes a secret in the secrets manager. If the secret does not exist, an error (fatal) is raised -func SecretRemove(org, credToUse, secretName string, forceRemoval bool) { +func SecretRemove(org, credToUse, secretName, secretNodeId string, forceRemoval bool) { // get message printer msgPrinter := i18n.GetMessagePrinter() @@ -231,6 +265,12 @@ func SecretRemove(org, credToUse, secretName string, forceRemoval bool) { secretName = secretName[:len(secretName)-1] } + // org: test-password, user secret: user/userdevadmin/test-password + if !strings.Contains(secretName, "/node") && secretNodeId != "" { + // add /node/{nodeID} to the path + secretName = getSecretPathForNodeLevelSecret(secretName, secretNodeId) + } + // confirm secret removal if !forceRemoval { cliutils.ConfirmRemove(msgPrinter.Sprintf("Are you sure you want to remove secret %s from the secrets manager?", secretName)) @@ -260,12 +300,17 @@ func SecretRemove(org, credToUse, secretName string, forceRemoval bool) { } // Pulls secret details from the secrets manager. If the secret does not exist, an error (fatal) is raised -func SecretRead(org, credToUse, secretName string) { +func SecretRead(org, credToUse, secretName, secretNodeId string) { // get rid of trailing / from secret name if strings.HasSuffix(secretName, "/") { secretName = secretName[:len(secretName)-1] } + if !strings.Contains(secretName, "/node") && secretNodeId != "" { + // add /node/{nodeID} to the path + secretName = getSecretPathForNodeLevelSecret(secretName, secretNodeId) + } + // query the agbot secure api var resp []byte listQuery := func() int { @@ -287,3 +332,25 @@ func SecretRead(org, credToUse, secretName string) { } } + +func getSecretPathForNodeLevelSecret(secretName string, secretNodeId string) string { + secretName = strings.TrimSpace(secretName) + if secretName == "" { + secretName = fmt.Sprintf("node/%v", secretNodeId) + } else { + parts := strings.Split(secretName, "/") + if len(parts) == 1 { + // org level, secretName: "test-password" + secretName = fmt.Sprintf("node/%v/%v", secretNodeId, secretName) + } else if len(parts) == 2 { + // list secret given a directory, eg: user/userdevadmin + secretName = fmt.Sprintf("%v/node/%v", secretName, secretNodeId) + } else if len(parts) == 3 { + // user level + // parts: [user, ${userid}, test-password] + secretName = fmt.Sprintf("%v/%v/node/%v/%v", parts[0], parts[1], secretNodeId, parts[2]) + } + } + + return secretName +}