From 2e3bc96c3e1987bef95abad0d4a5db654efbba5c Mon Sep 17 00:00:00 2001 From: Eamonn Mansour <47121388+eamansour@users.noreply.github.com> Date: Tue, 29 Oct 2024 12:50:47 +0000 Subject: [PATCH] feat: Add description flag and validation to secrets Signed-off-by: Eamonn Mansour <47121388+eamansour@users.noreply.github.com> --- .secrets.baseline | 66 ++++++- docs/generated/errors-list.md | 3 +- docs/generated/galasactl_secrets_set.md | 1 + pkg/cmd/secretsSet.go | 3 + pkg/errors/errorMessage.go | 4 +- pkg/secrets/secrets.go | 29 ++- pkg/secrets/secretsDelete.go | 2 +- pkg/secrets/secretsGet.go | 2 +- pkg/secrets/secretsGet_test.go | 62 ++++--- pkg/secrets/secretsSet.go | 41 +++-- pkg/secrets/secretsSet_test.go | 168 ++++++++++++++++++ pkg/secretsformatter/GalasaSecret.go | 65 +++++++ pkg/secretsformatter/secretsFormatter.go | 1 + pkg/secretsformatter/summaryFormatter.go | 5 +- pkg/secretsformatter/summaryFormatter_test.go | 50 ++++-- pkg/secretsformatter/yamlFormatter.go | 9 +- pkg/secretsformatter/yamlFormatter_test.go | 15 +- 17 files changed, 440 insertions(+), 86 deletions(-) create mode 100644 pkg/secretsformatter/GalasaSecret.go diff --git a/.secrets.baseline b/.secrets.baseline index d2cbf240..24c19b65 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -156,10 +156,18 @@ "verified_result": null }, { - "hashed_secret": "347cd9c53ff77d41a7b22aa56c7b4efaf54658e3", + "hashed_secret": "679d55ddc3c3d0f6ea2d11275a5d084669c98d56", "is_secret": false, "is_verified": false, - "line_number": 57, + "line_number": 62, + "type": "Secret Keyword", + "verified_result": null + }, + { + "hashed_secret": "3b938c1150a71e71e5f1ffeadbe6475f0f6a2e36", + "is_secret": false, + "is_verified": false, + "line_number": 122, "type": "Secret Keyword", "verified_result": null }, @@ -167,7 +175,53 @@ "hashed_secret": "2dfbe3ec00a96d6f711d9a70f78be17f6fd574ca", "is_secret": false, "is_verified": false, - "line_number": 274, + "line_number": 284, + "type": "Secret Keyword", + "verified_result": null + } + ], + "pkg/secrets/secretsSet.go": [ + { + "hashed_secret": "28aa91a8e751e5c49714ac040e98812f9110a1fd", + "is_secret": false, + "is_verified": false, + "line_number": 54, + "type": "Secret Keyword", + "verified_result": null + } + ], + "pkg/secrets/secretsSet_test.go": [ + { + "hashed_secret": "89e7fc0c50091804bfeb26cddefc0e701dd60fab", + "is_secret": false, + "is_verified": false, + "line_number": 316, + "type": "Secret Keyword", + "verified_result": null + }, + { + "hashed_secret": "edbd5e119f94badb9f99a67ac6ff4c7a5204ad61", + "is_secret": false, + "is_verified": false, + "line_number": 822, + "type": "Secret Keyword", + "verified_result": null + }, + { + "hashed_secret": "ea531d9e3ac1dc2beec9c298fb0026d59e4e2262", + "is_secret": false, + "is_verified": false, + "line_number": 825, + "type": "Secret Keyword", + "verified_result": null + } + ], + "pkg/secretsformatter/GalasaSecret.go": [ + { + "hashed_secret": "1949c4c92eb313637b3b6f654f5cce42df0dde88", + "is_secret": false, + "is_verified": false, + "line_number": 62, "type": "Secret Keyword", "verified_result": null } @@ -187,7 +241,7 @@ "hashed_secret": "11747ed2a3904f82931baf592443772259ea8dc1", "is_secret": false, "is_verified": false, - "line_number": 19, + "line_number": 20, "type": "Secret Keyword", "verified_result": null } @@ -204,10 +258,10 @@ ], "pkg/secretsformatter/yamlFormatter_test.go": [ { - "hashed_secret": "347cd9c53ff77d41a7b22aa56c7b4efaf54658e3", + "hashed_secret": "679d55ddc3c3d0f6ea2d11275a5d084669c98d56", "is_secret": false, "is_verified": false, - "line_number": 25, + "line_number": 29, "type": "Secret Keyword", "verified_result": null } diff --git a/docs/generated/errors-list.md b/docs/generated/errors-list.md index be97f98f..8c605362 100644 --- a/docs/generated/errors-list.md +++ b/docs/generated/errors-list.md @@ -167,7 +167,7 @@ The `galasactl` tool can generate the following errors: - GAL1169E: An attempt to delete a secret named '{}' failed. Unexpected http status code {} received from the server. Error details from the server are not in a valid json format. Cause: '{}' - GAL1170E: An attempt to delete a secret named '{}' failed. Unexpected http status code {} received from the server. Error details from the server are: '{}' - GAL1171E: An attempt to delete a secret named '{}' failed. Unexpected http status code {} received from the server. Error details from the server are not in the json format. -- GAL1172E: Invalid secret name provided. The name provided with the --name flag cannot be empty or contain spaces. +- GAL1172E: Invalid secret name provided. The name provided with the --name flag cannot be empty or contain spaces, and must only contain characters in the Latin-1 character set. - GAL1173E: An attempt to delete a secret named '{}' failed. Sending the delete request to the Galasa service failed. Cause is {} - GAL1174E: An attempt to get a secret named '{}' failed. Unexpected http status code {} received from the server. - GAL1175E: An attempt to get a secret named '{}' failed. Unexpected http status code {} received from the server. Error details from the server could not be read. Cause: {} @@ -189,6 +189,7 @@ The `galasactl` tool can generate the following errors: - GAL1191E: Failed to set a secret named '{}'. Unexpected http status code {} received from the server. Error details from the server are not in the json format. - GAL1192E: Failed to set a secret named '{}'. Sending the put request to the Galasa service failed. Cause is {} - GAL1193E: Invalid flag combination provided. --username cannot be provided with --base64-username, --password cannot be provided with --base64-password, and --token cannot be provided with --base64-token. Use the --help flag for more information, or refer to the documentation at https://galasa.dev/docs/reference/cli-commands. +- GAL1194E: Invalid secret description provided. The description provided with the --description flag cannot be an empty string, and must only contain characters in the Latin-1 character set. - GAL1225E: Failed to open file '{}' cause: {}. Check that this file exists, and that you have read permissions. - GAL1226E: Internal failure. Contents of gzip could be read, but not decoded. New gzip reader failed: file: {} error: {} - GAL1227E: Internal failure. Contents of gzip could not be decoded. {} error: {} diff --git a/docs/generated/galasactl_secrets_set.md b/docs/generated/galasactl_secrets_set.md index c7f1ef31..5fee0ec9 100644 --- a/docs/generated/galasactl_secrets_set.md +++ b/docs/generated/galasactl_secrets_set.md @@ -16,6 +16,7 @@ galasactl secrets set [flags] --base64-password string a base64-encoded password to set into a secret --base64-token string a base64-encoded token to set into a secret --base64-username string a base64-encoded username to set into a secret + --description string the description to associate with the secret being created or updated -h, --help Displays the options for the 'secrets set' command. --name string A mandatory flag that identifies the secret to be created or manipulated. --password string a password to set into a secret diff --git a/pkg/cmd/secretsSet.go b/pkg/cmd/secretsSet.go index 0a860fe6..b665142f 100644 --- a/pkg/cmd/secretsSet.go +++ b/pkg/cmd/secretsSet.go @@ -25,6 +25,7 @@ type SecretsSetCmdValues struct { username string password string token string + description string } type SecretsSetCommand struct { @@ -104,6 +105,7 @@ func (cmd *SecretsSetCommand) createCobraCmd( base64TokenFlag := "base64-token" secretsSetCobraCmd.Flags().StringVar(&cmd.values.secretType, "type", "", fmt.Sprintf("the desired secret type to convert an existing secret into. Supported types are: %v.", galasaapi.AllowedGalasaSecretTypeEnumValues)) + secretsSetCobraCmd.Flags().StringVar(&cmd.values.description, "description", "", "the description to associate with the secret being created or updated") secretsSetCobraCmd.Flags().StringVar(&cmd.values.username, usernameFlag, "", "a username to set into a secret") secretsSetCobraCmd.Flags().StringVar(&cmd.values.password, passwordFlag, "", "a password to set into a secret") secretsSetCobraCmd.Flags().StringVar(&cmd.values.token, tokenFlag, "", "a token to set into a secret") @@ -186,6 +188,7 @@ func (cmd *SecretsSetCommand) executeSecretsSet( cmd.values.base64Password, cmd.values.base64Token, cmd.values.secretType, + cmd.values.description, console, apiClient, byteReader, diff --git a/pkg/errors/errorMessage.go b/pkg/errors/errorMessage.go index b8bc5642..454a44db 100644 --- a/pkg/errors/errorMessage.go +++ b/pkg/errors/errorMessage.go @@ -268,7 +268,7 @@ var ( GALASA_ERROR_DELETE_SECRET_UNPARSEABLE_CONTENT = NewMessageType("GAL1169E: An attempt to delete a secret named '%s' failed. Unexpected http status code %v received from the server. Error details from the server are not in a valid json format. Cause: '%s'", 1169, STACK_TRACE_NOT_WANTED) GALASA_ERROR_DELETE_SECRET_SERVER_REPORTED_ERROR = NewMessageType("GAL1170E: An attempt to delete a secret named '%s' failed. Unexpected http status code %v received from the server. Error details from the server are: '%s'", 1170, STACK_TRACE_NOT_WANTED) GALASA_ERROR_DELETE_SECRET_EXPLANATION_NOT_JSON = NewMessageType("GAL1171E: An attempt to delete a secret named '%s' failed. Unexpected http status code %v received from the server. Error details from the server are not in the json format.", 1171, STACK_TRACE_NOT_WANTED) - GALASA_ERROR_INVALID_SECRET_NAME = NewMessageType("GAL1172E: Invalid secret name provided. The name provided with the --name flag cannot be empty or contain spaces.", 1172, STACK_TRACE_NOT_WANTED) + GALASA_ERROR_INVALID_SECRET_NAME = NewMessageType("GAL1172E: Invalid secret name provided. The name provided with the --name flag cannot be empty or contain spaces, and must only contain characters in the Latin-1 character set.", 1172, STACK_TRACE_NOT_WANTED) GALASA_ERROR_DELETE_SECRET_REQUEST_FAILED = NewMessageType("GAL1173E: An attempt to delete a secret named '%s' failed. Sending the delete request to the Galasa service failed. Cause is %v", 1173, STACK_TRACE_NOT_WANTED) GALASA_ERROR_GET_SECRET_NO_RESPONSE_CONTENT = NewMessageType("GAL1174E: An attempt to get a secret named '%s' failed. Unexpected http status code %v received from the server.", 1174, STACK_TRACE_NOT_WANTED) @@ -294,6 +294,8 @@ var ( GALASA_ERROR_SET_SECRET_REQUEST_FAILED = NewMessageType("GAL1192E: Failed to set a secret named '%s'. Sending the put request to the Galasa service failed. Cause is %v", 1192, STACK_TRACE_NOT_WANTED) GALASA_ERROR_SET_SECRET_INVALID_FLAG_COMBINATION = NewMessageType("GAL1193E: Invalid flag combination provided. --username cannot be provided with --base64-username, --password cannot be provided with --base64-password, and --token cannot be provided with --base64-token."+SEE_COMMAND_REFERENCE, 1193, STACK_TRACE_NOT_WANTED) + GALASA_ERROR_INVALID_SECRET_DESCRIPTION = NewMessageType("GAL1194E: Invalid secret description provided. The description provided with the --description flag cannot be an empty string, and must only contain characters in the Latin-1 character set.", 1194, STACK_TRACE_NOT_WANTED) + // Warnings... GALASA_WARNING_MAVEN_NO_GALASA_OBR_REPO = NewMessageType("GAL2000W: Warning: Maven configuration file settings.xml should contain a reference to a Galasa repository so that the galasa OBR can be resolved. The official release repository is '%s', and 'pre-release' repository is '%s'", 2000, STACK_TRACE_WANTED) diff --git a/pkg/secrets/secrets.go b/pkg/secrets/secrets.go index f6ef23d8..4e00963b 100644 --- a/pkg/secrets/secrets.go +++ b/pkg/secrets/secrets.go @@ -12,12 +12,35 @@ import ( galasaErrors "github.com/galasa-dev/cli/pkg/errors" ) -func validateSecretName(secretName string) error { +func validateSecretName(secretName string) (string, error) { var err error secretName = strings.TrimSpace(secretName) - if secretName == "" || strings.ContainsAny(secretName, " \n\t") { + if secretName == "" || strings.ContainsAny(secretName, " \n\t") || !isLatin1(secretName) { err = galasaErrors.NewGalasaError(galasaErrors.GALASA_ERROR_INVALID_SECRET_NAME) } - return err + return secretName, err +} + +func validateDescription(description string) (string, error) { + var err error + description = strings.TrimSpace(description) + + if description == "" || !isLatin1(description) { + err = galasaErrors.NewGalasaError(galasaErrors.GALASA_ERROR_INVALID_SECRET_DESCRIPTION) + } + return description, err +} + +// Checks if a given string contains only characters in the Latin-1 character set (codepoints 0-255), +// returning true if so, and false otherwise +func isLatin1(str string) bool { + isValidLatin1 := true + for _, character := range str { + if character > 255 { + isValidLatin1 = false + break + } + } + return isValidLatin1 } diff --git a/pkg/secrets/secretsDelete.go b/pkg/secrets/secretsDelete.go index 42af9e01..bf192ba4 100644 --- a/pkg/secrets/secretsDelete.go +++ b/pkg/secrets/secretsDelete.go @@ -25,7 +25,7 @@ func DeleteSecret( ) error { var err error - err = validateSecretName(secretName) + secretName, err = validateSecretName(secretName) if err == nil { log.Printf("Secret name validated OK") err = sendDeleteSecretRequest(secretName, apiClient, byteReader) diff --git a/pkg/secrets/secretsGet.go b/pkg/secrets/secretsGet.go index 4e7e7a58..00287ec8 100644 --- a/pkg/secrets/secretsGet.go +++ b/pkg/secrets/secretsGet.go @@ -69,7 +69,7 @@ func getSecretByName( ) (*galasaapi.GalasaSecret, error) { var err error var secret *galasaapi.GalasaSecret - err = validateSecretName(secretName) + secretName, err = validateSecretName(secretName) if err == nil { secret, err = getSecretFromRestApi(secretName, apiClient, byteReader) } diff --git a/pkg/secrets/secretsGet_test.go b/pkg/secrets/secretsGet_test.go index f1c3897d..de3310e6 100644 --- a/pkg/secrets/secretsGet_test.go +++ b/pkg/secrets/secretsGet_test.go @@ -6,16 +6,16 @@ package secrets import ( - "encoding/json" - "fmt" - "net/http" - "strconv" - "testing" - - "github.com/galasa-dev/cli/pkg/api" - "github.com/galasa-dev/cli/pkg/galasaapi" - "github.com/galasa-dev/cli/pkg/utils" - "github.com/stretchr/testify/assert" + "encoding/json" + "fmt" + "net/http" + "strconv" + "testing" + + "github.com/galasa-dev/cli/pkg/api" + "github.com/galasa-dev/cli/pkg/galasaapi" + "github.com/galasa-dev/cli/pkg/utils" + "github.com/stretchr/testify/assert" ) const ( @@ -25,7 +25,7 @@ const ( DUMMY_PASSWORD = "dummy-password" ) -func createMockGalasaSecret(secretName string) galasaapi.GalasaSecret { +func createMockGalasaSecret(secretName string, description string) galasaapi.GalasaSecret { secret := *galasaapi.NewGalasaSecret() secret.SetApiVersion(API_VERSION) @@ -36,6 +36,10 @@ func createMockGalasaSecret(secretName string) galasaapi.GalasaSecret { secretMetadata.SetEncoding(DUMMY_ENCODING) secretMetadata.SetType("UsernamePassword") + if description != "" { + secretMetadata.SetDescription(description) + } + secretData := *galasaapi.NewGalasaSecretData() secretData.SetUsername(DUMMY_USERNAME) secretData.SetPassword(DUMMY_PASSWORD) @@ -45,26 +49,27 @@ func createMockGalasaSecret(secretName string) galasaapi.GalasaSecret { return secret } -func generateExpectedSecretYaml(secretName string) string { +func generateExpectedSecretYaml(secretName string, description string) string { return fmt.Sprintf(`apiVersion: %s kind: GalasaSecret metadata: name: %s + description: %s encoding: %s type: UsernamePassword data: username: %s - password: %s - token: null`, API_VERSION, secretName, DUMMY_ENCODING, DUMMY_USERNAME, DUMMY_PASSWORD) + password: %s`, API_VERSION, secretName, description, DUMMY_ENCODING, DUMMY_USERNAME, DUMMY_PASSWORD) } func TestCanGetASecretByName(t *testing.T) { // Given... secretName := "SYSTEM1" + description := "my SYSTEM1 secret" outputFormat := "summary" // Create the mock secret to return - secret := createMockGalasaSecret(secretName) + secret := createMockGalasaSecret(secretName, description) secretBytes, _ := json.Marshal(secret) secretJson := string(secretBytes) @@ -97,11 +102,12 @@ func TestCanGetASecretByName(t *testing.T) { mockByteReader) // Then... - expectedOutput := fmt.Sprintf(`name type -%s UsernamePassword + expectedOutput := fmt.Sprintf( +`name type description +%s UsernamePassword %s Total:1 -`, secretName) +`, secretName, description) assert.Nil(t, err, "GetSecrets returned an unexpected error") assert.Equal(t, expectedOutput, console.ReadText()) } @@ -109,10 +115,11 @@ Total:1 func TestCanGetASecretByNameInYamlFormat(t *testing.T) { // Given... secretName := "SYSTEM1" + description := "my SYSTEM1 secret" outputFormat := "yaml" // Create the mock secret to return - secret := createMockGalasaSecret(secretName) + secret := createMockGalasaSecret(secretName, description) secretBytes, _ := json.Marshal(secret) secretJson := string(secretBytes) @@ -145,7 +152,7 @@ func TestCanGetASecretByNameInYamlFormat(t *testing.T) { mockByteReader) // Then... - expectedOutput := generateExpectedSecretYaml(secretName) + "\n" + expectedOutput := generateExpectedSecretYaml(secretName, description) + "\n" assert.Nil(t, err, "GetSecrets returned an unexpected error") assert.Equal(t, expectedOutput, console.ReadText()) } @@ -160,8 +167,10 @@ func TestCanGetAllSecretsOk(t *testing.T) { secrets := make([]galasaapi.GalasaSecret, 0) secret1Name := "BOB" secret2Name := "BLAH" - secret1 := createMockGalasaSecret(secret1Name) - secret2 := createMockGalasaSecret(secret2Name) + description1 := "my BOB secret" + description2 := "my BLAH secret" + secret1 := createMockGalasaSecret(secret1Name, description1) + secret2 := createMockGalasaSecret(secret2Name, description2) secrets = append(secrets, secret1, secret2) secretsBytes, _ := json.Marshal(secrets) @@ -196,12 +205,13 @@ func TestCanGetAllSecretsOk(t *testing.T) { mockByteReader) // Then... - expectedOutput := fmt.Sprintf(`name type -%s UsernamePassword -%s UsernamePassword + expectedOutput := fmt.Sprintf( +`name type description +%s UsernamePassword %s +%s UsernamePassword %s Total:2 -`, secret1Name, secret2Name) +`, secret1Name, description1, secret2Name, description2) assert.Nil(t, err, "GetSecrets returned an unexpected error") assert.Equal(t, expectedOutput, console.ReadText()) } diff --git a/pkg/secrets/secretsSet.go b/pkg/secrets/secretsSet.go index 8011cf7f..f5865cba 100644 --- a/pkg/secrets/secretsSet.go +++ b/pkg/secrets/secretsSet.go @@ -32,31 +32,37 @@ func SetSecret( base64Password string, base64Token string, secretType string, + description string, console spi.Console, apiClient *galasaapi.APIClient, byteReader spi.ByteReader, ) error { var err error - err = validateSecretName(secretName) + secretName, err = validateSecretName(secretName) if err == nil { log.Printf("Secret name validated OK") - - err = validateFlagCombination(username, password, token, base64Username, base64Password, base64Token) + if description != "" { + description, err = validateDescription(description) + } if err == nil { - requestUsername := createSecretRequestUsername(username, base64Username) - requestPassword := createSecretRequestPassword(password, base64Password) - requestToken := createSecretRequestToken(token, base64Token) - - var secretTypeValue galasaapi.NullableGalasaSecretType - if secretType != "" { - secretTypeValue, err = validateSecretType(secretType) - } - + err = validateFlagCombination(username, password, token, base64Username, base64Password, base64Token) + if err == nil { - secretRequest := createSecretRequest(secretName, requestUsername, requestPassword, requestToken, secretTypeValue) - err = sendSetSecretRequest(secretRequest, apiClient, byteReader) + requestUsername := createSecretRequestUsername(username, base64Username) + requestPassword := createSecretRequestPassword(password, base64Password) + requestToken := createSecretRequestToken(token, base64Token) + + var secretTypeValue galasaapi.NullableGalasaSecretType + if secretType != "" { + secretTypeValue, err = validateSecretType(secretType) + } + + if err == nil { + secretRequest := createSecretRequest(secretName, requestUsername, requestPassword, requestToken, secretTypeValue, description) + err = sendSetSecretRequest(secretRequest, apiClient, byteReader) + } } } } @@ -109,10 +115,15 @@ func createSecretRequest( password galasaapi.SecretRequestPassword, token galasaapi.SecretRequestToken, secretType galasaapi.NullableGalasaSecretType, + description string, ) *galasaapi.SecretRequest { secretRequest := galasaapi.NewSecretRequest() secretRequest.SetName(secretName) + if description != "" { + secretRequest.SetDescription(description) + } + if secretType.IsSet() { secretRequest.SetType(*secretType.Get()) } @@ -201,7 +212,7 @@ func validateFlagCombination( base64Token string, ) error { var err error - + // Make sure that a field and its base64 equivalent haven't both been provided if (username != "" && base64Username != "") || (password != "" && base64Password != "") || diff --git a/pkg/secrets/secretsSet_test.go b/pkg/secrets/secretsSet_test.go index 02d30764..1359c544 100644 --- a/pkg/secrets/secretsSet_test.go +++ b/pkg/secrets/secretsSet_test.go @@ -38,6 +38,7 @@ func TestCanCreateAUsernameSecret(t *testing.T) { base64Password := "" base64Token := "" secretType := "" + description := "" // Create the expected HTTP interactions with the API server createSecretInteraction := utils.NewHttpInteraction("/secrets/" + secretName, http.MethodPut) @@ -87,6 +88,7 @@ func TestCanCreateAUsernameSecret(t *testing.T) { base64Password, base64Token, secretType, + description, console, apiClient, mockByteReader) @@ -106,6 +108,7 @@ func TestCanCreateAUsernamePasswordSecret(t *testing.T) { base64Password := "" base64Token := "" secretType := "" + description := "my secret description" // Create the expected HTTP interactions with the API server createSecretInteraction := utils.NewHttpInteraction("/secrets/" + secretName, http.MethodPut) @@ -115,6 +118,7 @@ func TestCanCreateAUsernamePasswordSecret(t *testing.T) { secretRequest := readSecretRequestBody(req) assert.Equal(t, secretRequest.GetName(), secretName) assert.Empty(t, secretRequest.GetType()) + assert.Equal(t, secretRequest.GetDescription(), description) requestUsername := secretRequest.GetUsername() assert.Equal(t, requestUsername.GetValue(), username) @@ -155,6 +159,7 @@ func TestCanCreateAUsernamePasswordSecret(t *testing.T) { base64Password, base64Token, secretType, + description, console, apiClient, mockByteReader) @@ -174,6 +179,7 @@ func TestCanCreateAUsernameTokenSecret(t *testing.T) { base64Password := "" base64Token := "" secretType := "" + description := "" // Create the expected HTTP interactions with the API server createSecretInteraction := utils.NewHttpInteraction("/secrets/" + secretName, http.MethodPut) @@ -223,6 +229,7 @@ func TestCanCreateAUsernameTokenSecret(t *testing.T) { base64Password, base64Token, secretType, + description, console, apiClient, mockByteReader) @@ -242,6 +249,7 @@ func TestCanCreateATokenSecret(t *testing.T) { base64Password := "" base64Token := "" secretType := "" + description := "" // Create the expected HTTP interactions with the API server createSecretInteraction := utils.NewHttpInteraction("/secrets/" + secretName, http.MethodPut) @@ -291,6 +299,7 @@ func TestCanCreateATokenSecret(t *testing.T) { base64Password, base64Token, secretType, + description, console, apiClient, mockByteReader) @@ -310,6 +319,7 @@ func TestCanUpdateASecret(t *testing.T) { base64Password := "" base64Token := "" secretType := "" + description := "" // Create the expected HTTP interactions with the API server updateSecretInteraction := utils.NewHttpInteraction("/secrets/" + secretName, http.MethodPut) @@ -359,6 +369,7 @@ func TestCanUpdateASecret(t *testing.T) { base64Password, base64Token, secretType, + description, console, apiClient, mockByteReader) @@ -378,6 +389,7 @@ func TestCanUpdateAUsernamePasswordSecretInBase64Format(t *testing.T) { base64Password := "my-base64-password" base64Token := "" secretType := "" + description := "" // Create the expected HTTP interactions with the API server updateSecretInteraction := utils.NewHttpInteraction("/secrets/" + secretName, http.MethodPut) @@ -427,6 +439,7 @@ func TestCanUpdateAUsernamePasswordSecretInBase64Format(t *testing.T) { base64Password, base64Token, secretType, + description, console, apiClient, mockByteReader) @@ -446,6 +459,7 @@ func TestCanUpdateATokenSecretInBase64Format(t *testing.T) { base64Password := "" base64Token := "my-base64-token" secretType := "" + description := "" // Create the expected HTTP interactions with the API server updateSecretInteraction := utils.NewHttpInteraction("/secrets/" + secretName, http.MethodPut) @@ -495,6 +509,7 @@ func TestCanUpdateATokenSecretInBase64Format(t *testing.T) { base64Password, base64Token, secretType, + description, console, apiClient, mockByteReader) @@ -514,6 +529,7 @@ func TestCanUpdateASecretsTypeOk(t *testing.T) { base64Password := "" base64Token := "my-base64-token" secretType := "token" + description := "my new token" // Create the expected HTTP interactions with the API server updateSecretInteraction := utils.NewHttpInteraction("/secrets/" + secretName, http.MethodPut) @@ -563,6 +579,7 @@ func TestCanUpdateASecretsTypeOk(t *testing.T) { base64Password, base64Token, secretType, + description, console, apiClient, mockByteReader) @@ -582,6 +599,7 @@ func TestUpdateSecretWithNoNameThrowsError(t *testing.T) { base64Password := "" base64Token := "my-base64-token" secretType := "" + description := "" // Validation should fail, so no HTTP interactions should take place interactions := []utils.HttpInteraction{} @@ -604,6 +622,7 @@ func TestUpdateSecretWithNoNameThrowsError(t *testing.T) { base64Password, base64Token, secretType, + description, console, apiClient, mockByteReader) @@ -615,6 +634,141 @@ func TestUpdateSecretWithNoNameThrowsError(t *testing.T) { assert.Contains(t, errorMsg, "Invalid secret name provided") } +func TestUpdateSecretWithNonLatin1NameThrowsError(t *testing.T) { + // Given... + secretName := string(rune(300)) + "NONLATIN1" + username := "" + password := "" + token := "" + base64Username := "" + base64Password := "" + base64Token := "my-base64-token" + secretType := "" + description := "" + + // Validation should fail, so no HTTP interactions should take place + interactions := []utils.HttpInteraction{} + + server := utils.NewMockHttpServer(t, interactions) + defer server.Server.Close() + + console := utils.NewMockConsole() + apiServerUrl := server.Server.URL + apiClient := api.InitialiseAPI(apiServerUrl) + mockByteReader := utils.NewMockByteReader() + + // When... + err := SetSecret( + secretName, + username, + password, + token, + base64Username, + base64Password, + base64Token, + secretType, + description, + console, + apiClient, + mockByteReader) + + // Then... + assert.NotNil(t, err, "SetSecret did not return an error as expected") + errorMsg := err.Error() + assert.Contains(t, errorMsg, "GAL1172E") + assert.Contains(t, errorMsg, "Invalid secret name provided") +} + +func TestUpdateSecretWithNonLatin1DescriptionThrowsError(t *testing.T) { + // Given... + secretName := "MYSECRET" + username := "" + password := "" + token := "" + base64Username := "" + base64Password := "" + base64Token := "my-base64-token" + secretType := "" + description := string(rune(256)) + " is not latin-1" + + // Validation should fail, so no HTTP interactions should take place + interactions := []utils.HttpInteraction{} + + server := utils.NewMockHttpServer(t, interactions) + defer server.Server.Close() + + console := utils.NewMockConsole() + apiServerUrl := server.Server.URL + apiClient := api.InitialiseAPI(apiServerUrl) + mockByteReader := utils.NewMockByteReader() + + // When... + err := SetSecret( + secretName, + username, + password, + token, + base64Username, + base64Password, + base64Token, + secretType, + description, + console, + apiClient, + mockByteReader) + + // Then... + assert.NotNil(t, err, "SetSecret did not return an error as expected") + errorMsg := err.Error() + assert.Contains(t, errorMsg, "GAL1194E") + assert.Contains(t, errorMsg, "Invalid secret description provided") +} + +func TestUpdateSecretWithBlankDescriptionThrowsError(t *testing.T) { + // Given... + secretName := "MYSECRET" + username := "" + password := "" + token := "" + base64Username := "" + base64Password := "" + base64Token := "my-base64-token" + secretType := "" + description := " " + + // Validation should fail, so no HTTP interactions should take place + interactions := []utils.HttpInteraction{} + + server := utils.NewMockHttpServer(t, interactions) + defer server.Server.Close() + + console := utils.NewMockConsole() + apiServerUrl := server.Server.URL + apiClient := api.InitialiseAPI(apiServerUrl) + mockByteReader := utils.NewMockByteReader() + + // When... + err := SetSecret( + secretName, + username, + password, + token, + base64Username, + base64Password, + base64Token, + secretType, + description, + console, + apiClient, + mockByteReader) + + // Then... + assert.NotNil(t, err, "SetSecret did not return an error as expected") + errorMsg := err.Error() + assert.Contains(t, errorMsg, "GAL1194E") + assert.Contains(t, errorMsg, "Invalid secret description provided") +} + func TestUpdateSecretWithUnknownTypeThrowsError(t *testing.T) { // Given... secretName := "MYSECRET" @@ -625,6 +779,7 @@ func TestUpdateSecretWithUnknownTypeThrowsError(t *testing.T) { base64Password := "" base64Token := "my-base64-token" secretType := "UNKNOWN" + description := "this should fail!" // Validation should fail, so no HTTP interactions should take place interactions := []utils.HttpInteraction{} @@ -647,6 +802,7 @@ func TestUpdateSecretWithUnknownTypeThrowsError(t *testing.T) { base64Password, base64Token, secretType, + description, console, apiClient, mockByteReader) @@ -669,6 +825,7 @@ func TestUpdateSecretWithInvalidFlagCombinationThrowsError(t *testing.T) { base64Password := "my-base64-password" base64Token := "my-base64-token" secretType := "" + description := "" // Validation should fail, so no HTTP interactions should take place interactions := []utils.HttpInteraction{} @@ -691,6 +848,7 @@ func TestUpdateSecretWithInvalidFlagCombinationThrowsError(t *testing.T) { base64Password, base64Token, secretType, + description, console, apiClient, mockByteReader) @@ -712,6 +870,7 @@ func TestSetSecretFailsWithNoExplanationErrorPayloadGivesCorrectMessage(t *testi base64Password := "" base64Token := "my-base64-token" secretType := "" + description := "" // Create the expected HTTP interactions with the API server updateSecretInteraction := utils.NewHttpInteraction("/secrets/" + secretName, http.MethodPut) @@ -741,6 +900,7 @@ func TestSetSecretFailsWithNoExplanationErrorPayloadGivesCorrectMessage(t *testi base64Password, base64Token, secretType, + description, console, apiClient, mockByteReader) @@ -763,6 +923,7 @@ func TestSetSecretFailsWithNonJsonContentTypeExplanationErrorPayloadGivesCorrect base64Password := "" base64Token := "my-base64-token" secretType := "" + description := "" // Create the expected HTTP interactions with the API server updateSecretInteraction := utils.NewHttpInteraction("/secrets/" + secretName, http.MethodPut) @@ -794,6 +955,7 @@ func TestSetSecretFailsWithNonJsonContentTypeExplanationErrorPayloadGivesCorrect base64Password, base64Token, secretType, + description, console, apiClient, mockByteReader) @@ -817,6 +979,7 @@ func TestSetSecretFailsWithBadlyFormedJsonContentExplanationErrorPayloadGivesCor base64Password := "" base64Token := "my-base64-token" secretType := "" + description := "" // Create the expected HTTP interactions with the API server updateSecretInteraction := utils.NewHttpInteraction("/secrets/" + secretName, http.MethodPut) @@ -848,6 +1011,7 @@ func TestSetSecretFailsWithBadlyFormedJsonContentExplanationErrorPayloadGivesCor base64Password, base64Token, secretType, + description, console, apiClient, mockByteReader) @@ -872,6 +1036,7 @@ func TestSetSecretFailsWithValidErrorResponsePayloadGivesCorrectMessage(t *testi base64Password := "" base64Token := "my-base64-token" secretType := "" + description := "" apiErrorCode := 5000 apiErrorMessage := "this is an error from the API server" @@ -911,6 +1076,7 @@ func TestSetSecretFailsWithValidErrorResponsePayloadGivesCorrectMessage(t *testi base64Password, base64Token, secretType, + description, console, apiClient, mockByteReader) @@ -934,6 +1100,7 @@ func TestSecretsSetFailsWithFailureToReadResponseBodyGivesCorrectMessage(t *test base64Password := "" base64Token := "my-base64-token" secretType := "" + description := "" // Create the expected HTTP interactions with the API server updateSecretInteraction := utils.NewHttpInteraction("/secrets/" + secretName, http.MethodPut) @@ -965,6 +1132,7 @@ func TestSecretsSetFailsWithFailureToReadResponseBodyGivesCorrectMessage(t *test base64Password, base64Token, secretType, + description, console, apiClient, mockByteReader) diff --git a/pkg/secretsformatter/GalasaSecret.go b/pkg/secretsformatter/GalasaSecret.go new file mode 100644 index 00000000..37c48674 --- /dev/null +++ b/pkg/secretsformatter/GalasaSecret.go @@ -0,0 +1,65 @@ +/* + * Copyright contributors to the Galasa project + * + * SPDX-License-Identifier: EPL-2.0 + */ +package secretsformatter + +import ( + "time" + + "github.com/galasa-dev/cli/pkg/galasaapi" +) + +// The auto-generated OpenAPI structs don't include `yaml` annotations, which causes +// issues when it comes to marshalling data into GalasaSecret structs in order to display +// secrets in YAML format. This is a manually-maintained struct that includes `yaml` annotations. +type GalasaSecret struct { + ApiVersion *string `json:"apiVersion,omitempty" yaml:"apiVersion,omitempty"` + Kind *string `json:"kind,omitempty" yaml:"kind,omitempty"` + Metadata *GalasaSecretMetadata `json:"metadata,omitempty" yaml:"metadata,omitempty"` + Data *GalasaSecretData `json:"data,omitempty" yaml:"data,omitempty"` +} + +type GalasaSecretMetadata struct { + Name *string `json:"name,omitempty" yaml:"name,omitempty"` + Description *string `json:"description,omitempty" yaml:"description,omitempty"` + LastUpdatedTime *time.Time `json:"lastUpdatedTime,omitempty" yaml:"lastUpdatedTime,omitempty"` + LastUpdatedBy *string `json:"lastUpdatedBy,omitempty" yaml:"lastUpdatedBy,omitempty"` + Encoding *string `json:"encoding,omitempty" yaml:"encoding,omitempty"` + Type *galasaapi.GalasaSecretType `json:"type,omitempty" yaml:"type,omitempty"` +} + +type GalasaSecretData struct { + Username *string `json:"username,omitempty" yaml:"username,omitempty"` + Password *string `json:"password,omitempty" yaml:"password,omitempty"` + Token *string `json:"token,omitempty" yaml:"token,omitempty"` +} + +func NewGalasaSecret(secret galasaapi.GalasaSecret) *GalasaSecret { + return &GalasaSecret{ + ApiVersion: secret.ApiVersion, + Kind: secret.Kind, + Metadata: NewGalasaSecretMetadata(secret.Metadata), + Data: NewGalasaSecretData(secret.Data), + } +} + +func NewGalasaSecretMetadata(metadata *galasaapi.GalasaSecretMetadata) *GalasaSecretMetadata { + return &GalasaSecretMetadata{ + Name: metadata.Name, + Description: metadata.Description, + LastUpdatedTime: metadata.LastUpdatedTime, + LastUpdatedBy: metadata.LastUpdatedBy, + Encoding: metadata.Encoding, + Type: metadata.Type, + } +} + +func NewGalasaSecretData(data *galasaapi.GalasaSecretData) *GalasaSecretData { + return &GalasaSecretData{ + Username: data.Username, + Password: data.Password, + Token: data.Token, + } +} \ No newline at end of file diff --git a/pkg/secretsformatter/secretsFormatter.go b/pkg/secretsformatter/secretsFormatter.go index ab2984f3..acdf2a7c 100644 --- a/pkg/secretsformatter/secretsFormatter.go +++ b/pkg/secretsformatter/secretsFormatter.go @@ -26,6 +26,7 @@ import ( const ( HEADER_SECRET_NAME = "name" HEADER_SECRET_TYPE = "type" + HEADER_SECRET_DESCRIPTION = "description" ) type SecretsFormatter interface { diff --git a/pkg/secretsformatter/summaryFormatter.go b/pkg/secretsformatter/summaryFormatter.go index be74c2b1..1c5ca849 100644 --- a/pkg/secretsformatter/summaryFormatter.go +++ b/pkg/secretsformatter/summaryFormatter.go @@ -38,15 +38,16 @@ func (*SecretSummaryFormatter) FormatSecrets(secrets []galasaapi.GalasaSecret) ( if totalSecrets > 0 { var table [][]string - var headers = []string{ HEADER_SECRET_NAME, HEADER_SECRET_TYPE } + var headers = []string{ HEADER_SECRET_NAME, HEADER_SECRET_TYPE, HEADER_SECRET_DESCRIPTION } table = append(table, headers) for _, secret := range secrets { var line []string name := secret.Metadata.GetName() secretType := secret.Metadata.GetType() + secretDescription := secret.Metadata.GetDescription() - line = append(line, name, string(secretType)) + line = append(line, name, string(secretType), secretDescription) table = append(table, line) } diff --git a/pkg/secretsformatter/summaryFormatter_test.go b/pkg/secretsformatter/summaryFormatter_test.go index 2c77e332..7324a640 100644 --- a/pkg/secretsformatter/summaryFormatter_test.go +++ b/pkg/secretsformatter/summaryFormatter_test.go @@ -6,10 +6,11 @@ package secretsformatter import ( - "testing" + "fmt" + "testing" - "github.com/galasa-dev/cli/pkg/galasaapi" - "github.com/stretchr/testify/assert" + "github.com/galasa-dev/cli/pkg/galasaapi" + "github.com/stretchr/testify/assert" ) const ( @@ -19,7 +20,7 @@ const ( DUMMY_PASSWORD = "dummy-password" ) -func createMockGalasaSecret(secretName string) galasaapi.GalasaSecret { +func createMockGalasaSecretWithDescription(secretName string, description string) galasaapi.GalasaSecret { secret := *galasaapi.NewGalasaSecret() secret.SetApiVersion(API_VERSION) @@ -30,6 +31,10 @@ func createMockGalasaSecret(secretName string) galasaapi.GalasaSecret { secretMetadata.SetEncoding(DUMMY_ENCODING) secretMetadata.SetType("UsernamePassword") + if description != "" { + secretMetadata.SetDescription(description) + } + secretData := *galasaapi.NewGalasaSecretData() secretData.SetUsername(DUMMY_USERNAME) secretData.SetPassword(DUMMY_PASSWORD) @@ -56,7 +61,9 @@ func TestSecretSummaryFormatterNoDataReturnsTotalCountAllZeros(t *testing.T) { func TestSecretSummaryFormatterSingleDataReturnsCorrectly(t *testing.T) { // Given... formatter := NewSecretSummaryFormatter() - secret1 := createMockGalasaSecret("MYSECRET") + description := "secret for system1" + secretName := "MYSECRET" + secret1 := createMockGalasaSecretWithDescription(secretName, description) secrets := []galasaapi.GalasaSecret{ secret1 } // When... @@ -64,12 +71,12 @@ func TestSecretSummaryFormatterSingleDataReturnsCorrectly(t *testing.T) { // Then... assert.Nil(t, err) - expectedFormattedOutput := - `name type -MYSECRET UsernamePassword + expectedFormattedOutput := fmt.Sprintf( +`name type description +%s UsernamePassword %s Total:1 -` +`, secretName, description) assert.Equal(t, expectedFormattedOutput, actualFormattedOutput) } @@ -77,9 +84,17 @@ func TestSecretSummaryFormatterMultipleDataSeperatesWithNewLine(t *testing.T) { // Given.. formatter := NewSecretSummaryFormatter() secrets := make([]galasaapi.GalasaSecret, 0) - secret1 := createMockGalasaSecret("SECRET1") - secret2 := createMockGalasaSecret("SECRET_2") - secret3 := createMockGalasaSecret("SECRET-3") + + secret1Name := "SECRET1" + secret1Description := "my first secret" + secret2Name := "SECRET2" + secret2Description := "my second secret" + secret3Name := "SECRET3" + secret3Description := "my third secret" + + secret1 := createMockGalasaSecretWithDescription(secret1Name, secret1Description) + secret2 := createMockGalasaSecretWithDescription(secret2Name, secret2Description) + secret3 := createMockGalasaSecretWithDescription(secret3Name, secret3Description) secrets = append(secrets, secret1, secret2, secret3) // When... @@ -87,12 +102,13 @@ func TestSecretSummaryFormatterMultipleDataSeperatesWithNewLine(t *testing.T) { // Then... assert.Nil(t, err) - expectedFormattedOutput := `name type -SECRET1 UsernamePassword -SECRET_2 UsernamePassword -SECRET-3 UsernamePassword + expectedFormattedOutput := fmt.Sprintf( +`name type description +%s UsernamePassword %s +%s UsernamePassword %s +%s UsernamePassword %s Total:3 -` +`, secret1Name, secret1Description, secret2Name, secret2Description, secret3Name, secret3Description) assert.Equal(t, expectedFormattedOutput, actualFormattedOutput) } diff --git a/pkg/secretsformatter/yamlFormatter.go b/pkg/secretsformatter/yamlFormatter.go index 13e0a46b..2f5865f7 100644 --- a/pkg/secretsformatter/yamlFormatter.go +++ b/pkg/secretsformatter/yamlFormatter.go @@ -32,6 +32,7 @@ func (*SecretYamlFormatter) FormatSecrets(secrets []galasaapi.GalasaSecret) (str buff := strings.Builder{} for index, secret := range secrets { + galasaSecret := NewGalasaSecret(secret) secretString := "" if index > 0 { @@ -39,15 +40,9 @@ func (*SecretYamlFormatter) FormatSecrets(secrets []galasaapi.GalasaSecret) (str } var yamlRepresentationBytes []byte - yamlRepresentationBytes, err = yaml.Marshal(secret) + yamlRepresentationBytes, err = yaml.Marshal(galasaSecret) if err == nil { yamlStr := string(yamlRepresentationBytes) - - // The generated bean serialises in json as 'apiVersion' which is correct. In yaml it serialises as 'apiversion' (incorrect) - // So this is a hack to correct that failure. - // Note: This will corrupt any value string which also has 'apiversion' inside it ! - // TODO: The fix is to change the bean and add a 'yaml' annotation so it gets rendered correctly. Golang has yaml annotations, but does the generator support them ? - yamlStr = strings.ReplaceAll(yamlStr, "apiversion", "apiVersion") secretString += yamlStr } diff --git a/pkg/secretsformatter/yamlFormatter_test.go b/pkg/secretsformatter/yamlFormatter_test.go index 72f5dbcb..714e80a3 100644 --- a/pkg/secretsformatter/yamlFormatter_test.go +++ b/pkg/secretsformatter/yamlFormatter_test.go @@ -6,13 +6,17 @@ package secretsformatter import ( - "fmt" - "testing" + "fmt" + "testing" - "github.com/galasa-dev/cli/pkg/galasaapi" - "github.com/stretchr/testify/assert" + "github.com/galasa-dev/cli/pkg/galasaapi" + "github.com/stretchr/testify/assert" ) +func createMockGalasaSecret(secretName string) galasaapi.GalasaSecret { + return createMockGalasaSecretWithDescription(secretName, "") +} + func generateExpectedSecretYaml(secretName string) string { return fmt.Sprintf(`apiVersion: %s kind: GalasaSecret @@ -22,8 +26,7 @@ metadata: type: UsernamePassword data: username: %s - password: %s - token: null`, API_VERSION, secretName, DUMMY_ENCODING, DUMMY_USERNAME, DUMMY_PASSWORD) + password: %s`, API_VERSION, secretName, DUMMY_ENCODING, DUMMY_USERNAME, DUMMY_PASSWORD) } func TestSecretsYamlFormatterNoDataReturnsBlankString(t *testing.T) {