From 693ae7237ce2c354bb2f94205e71f7a9182a467f Mon Sep 17 00:00:00 2001 From: sbailey <1661003+spbsoluble@users.noreply.github.com> Date: Tue, 20 Aug 2024 15:05:27 -0700 Subject: [PATCH] fix(login): `login` now passively combines environmental config and config file data. chore(deps): update `keyfactor-go-client` Signed-off-by: sbailey <1661003+spbsoluble@users.noreply.github.com> --- cmd/login_test.go | 274 +++++++++++++++++++++++++++------------------- cmd/root.go | 54 +++++---- cmd/storeTypes.go | 67 ++++++++++-- go.mod | 6 +- go.sum | 8 +- 5 files changed, 260 insertions(+), 149 deletions(-) diff --git a/cmd/login_test.go b/cmd/login_test.go index 6fda933c..9ac32904 100644 --- a/cmd/login_test.go +++ b/cmd/login_test.go @@ -17,14 +17,43 @@ package cmd import ( "encoding/json" "fmt" - "github.com/joho/godotenv" - "github.com/stretchr/testify/assert" "os" "os/user" "path/filepath" + "regexp" "testing" + + "github.com/joho/godotenv" + "github.com/stretchr/testify/assert" ) +func extractAndParseJSON(data string) ([]map[string]interface{}, error) { + // Improved regular expression to match JSON objects or arrays + jsonPattern := regexp.MustCompile(`\{[^{}]*\}|\[[^\[\]]*\]`) + + // Find all JSON-like strings within the non-structured string + jsonStrings := jsonPattern.FindAllString(data, -1) + + var results []map[string]interface{} + for _, jsonString := range jsonStrings { + var parsedData map[string]interface{} + err := json.Unmarshal([]byte(jsonString), &parsedData) + if err != nil { + // If the JSON is not a map, try unmarshalling into a slice of interfaces + var parsedSlice []interface{} + err := json.Unmarshal([]byte(jsonString), &parsedSlice) + if err != nil { + // Skip non-JSON matches + continue + } + parsedData = map[string]interface{}{"data": parsedSlice} + } + results = append(results, parsedData) + } + + return results, nil +} + func Test_LoginHelpCmd(t *testing.T) { // Test root help testCmd := RootCmd @@ -67,10 +96,12 @@ func Test_LoginCmdConfigParams(t *testing.T) { testCmd := RootCmd // test testCmd.SetArgs([]string{"stores", "list", "--exp", "--config", "$HOME/.keyfactor/extra_config.json"}) - output := captureOutput(func() { - err := testCmd.Execute() - assert.NoError(t, err) - }) + output := captureOutput( + func() { + err := testCmd.Execute() + assert.NoError(t, err) + }, + ) t.Logf("output: %s", output) var stores []string if err := json.Unmarshal([]byte(output), &stores); err != nil { @@ -82,34 +113,38 @@ func Test_LoginCmdConfigParams(t *testing.T) { } func testLogout(t *testing.T) { - t.Run(fmt.Sprintf("Logout"), func(t *testing.T) { - testCmd := RootCmd - // test - testCmd.SetArgs([]string{"logout"}) - output := captureOutput(func() { - err := testCmd.Execute() - assert.NoError(t, err) - }) - t.Logf("output: %s", output) + t.Run( + fmt.Sprintf("Logout"), func(t *testing.T) { + testCmd := RootCmd + // test + testCmd.SetArgs([]string{"logout"}) + output := captureOutput( + func() { + err := testCmd.Execute() + assert.NoError(t, err) + }, + ) + t.Logf("output: %s", output) - assert.Contains(t, output, "Logged out successfully!") + assert.Contains(t, output, "Logged out successfully!") - // Get the current user's information - currentUser, err := user.Current() - if err != nil { - fmt.Println("Error:", err) - return - } + // Get the current user's information + currentUser, err := user.Current() + if err != nil { + fmt.Println("Error:", err) + return + } - // Define the path to the file in the user's home directory - filePath := filepath.Join(currentUser.HomeDir, ".keyfactor/command_config.json") - _, err = os.Stat(filePath) + // Define the path to the file in the user's home directory + filePath := filepath.Join(currentUser.HomeDir, ".keyfactor/command_config.json") + _, err = os.Stat(filePath) - // Test that the config file does not exist - if _, fErr := os.Stat(filePath); !os.IsNotExist(fErr) { - t.Errorf("Config file %s still exists, please remove", filePath) - } - }) + // Test that the config file does not exist + if _, fErr := os.Stat(filePath); !os.IsNotExist(fErr) { + t.Errorf("Config file %s still exists, please remove", filePath) + } + }, + ) } @@ -120,26 +155,31 @@ func testConfigValid(t *testing.T) { //t.Logf("envUsername: %s", envUsername) //t.Logf("envPassword: %s", envPassword) t.Logf("Attempting to run `store-types list`") - t.Run(fmt.Sprintf("List store types"), func(t *testing.T) { - testCmd := RootCmd - t.Log("Setting args") - testCmd.SetArgs([]string{"store-types", "list"}) - t.Logf("args: %v", testCmd.Args) - t.Log("Capturing output") - output := captureOutput(func() { - tErr := testCmd.Execute() - assert.NoError(t, tErr) - }) - t.Logf("output: %s", output) - - var storeTypes []map[string]interface{} - if err := json.Unmarshal([]byte(output), &storeTypes); err != nil { - t.Fatalf("Error unmarshalling JSON: %v", err) - } + t.Run( + fmt.Sprintf("List store types"), func(t *testing.T) { + testCmd := RootCmd + t.Log("Setting args") + testCmd.SetArgs([]string{"store-types", "list"}) + t.Logf("args: %v", testCmd.Args) + t.Log("Capturing output") + output := captureOutput( + func() { + tErr := testCmd.Execute() + assert.NoError(t, tErr) + }, + ) + t.Logf("output: %s", output) + + jsonResp, jErr := extractAndParseJSON(output) + if jErr != nil { + t.Fatalf("Error parsing JSON: %v", jErr) + } - // Verify that the length of the response is greater than 0 - assert.True(t, len(storeTypes) >= 0, "Expected non-empty list of store types") - }) + // Verify that the length of the response is greater than 0 + assert.True(t, len(jsonResp) >= 0, "Expected non-empty list of store types") + // parse only what fits JSON regex from output + }, + ) } func testConfigExists(t *testing.T, filePath string, allowExist bool) { @@ -149,77 +189,89 @@ func testConfigExists(t *testing.T, filePath string, allowExist bool) { } else { testName = "Config file does not exist" } - t.Run(fmt.Sprintf(testName), func(t *testing.T) { - _, fErr := os.Stat(filePath) - if allowExist { - assert.True(t, allowExist && fErr == nil) - // Load the config file from JSON to map[string]interface{} - fileConfigJSON := make(map[string]interface{}) - file, _ := os.Open(filePath) - defer file.Close() - decoder := json.NewDecoder(file) - err := decoder.Decode(&fileConfigJSON) - if err != nil { - t.Errorf("Error decoding config file: %s", err) - } - // Verify that the config file has the correct keys - assert.Contains(t, fileConfigJSON, "servers") - kfcServers, ok := fileConfigJSON["servers"].(map[string]interface{}) - if !ok { - t.Errorf("Error decoding config file: %s", err) - assert.False(t, ok, "Error decoding config file") - return + t.Run( + fmt.Sprintf(testName), func(t *testing.T) { + _, fErr := os.Stat(filePath) + if allowExist { + if fErr != nil { + t.Errorf("Unknown error: %s", fErr) + t.FailNow() + } + assert.True(t, allowExist && fErr == nil) + // Load the config file from JSON to map[string]interface{} + fileConfigJSON := make(map[string]interface{}) + file, _ := os.Open(filePath) + defer file.Close() + decoder := json.NewDecoder(file) + err := decoder.Decode(&fileConfigJSON) + if err != nil { + t.Errorf("Error decoding config file: %s", err) + } + // Verify that the config file has the correct keys + assert.Contains(t, fileConfigJSON, "servers") + kfcServers, ok := fileConfigJSON["servers"].(map[string]interface{}) + if !ok { + t.Errorf("Error decoding config file: %s", err) + assert.False(t, ok, "Error decoding config file") + return + } + assert.Contains(t, kfcServers, "default") + defaultServer := kfcServers["default"].(map[string]interface{}) + assert.Contains(t, defaultServer, "host") + assert.Contains(t, defaultServer, "username") + assert.Contains(t, defaultServer, "password") + } else { + assert.True(t, !allowExist && os.IsNotExist(fErr)) } - assert.Contains(t, kfcServers, "default") - defaultServer := kfcServers["default"].(map[string]interface{}) - assert.Contains(t, defaultServer, "host") - assert.Contains(t, defaultServer, "kfcUsername") - assert.Contains(t, defaultServer, "kfcPassword") - } else { - assert.True(t, !allowExist && os.IsNotExist(fErr)) - } - }) + }, + ) } func testEnvCredsOnly(t *testing.T, filePath string, allowExist bool) { - t.Run(fmt.Sprintf("Auth w/ env ONLY"), func(t *testing.T) { - // Load .env file - err := godotenv.Load("../.env_1040") - if err != nil { - t.Errorf("Error loading .env file") - } - testLogout(t) - testConfigExists(t, filePath, false) - testConfigValid(t) - }) + t.Run( + fmt.Sprintf("Auth w/ env ONLY"), func(t *testing.T) { + // Load .env file + //err := godotenv.Load("../.env_1040") + //if err != nil { + // t.Errorf("Error loading .env file") + //} + testLogout(t) + testConfigExists(t, filePath, false) + testConfigValid(t) + }, + ) } func testEnvCredsToFile(t *testing.T, filePath string, allowExist bool) { - t.Run(fmt.Sprintf("Auth w/ env ONLY"), func(t *testing.T) { - // Load .env file - err := godotenv.Load("../.env_1040") - if err != nil { - t.Errorf("Error loading .env file") - } - testLogout(t) - testConfigExists(t, filePath, false) - testConfigValid(t) - }) + t.Run( + fmt.Sprintf("Auth w/ env ONLY"), func(t *testing.T) { + // Load .env file + err := godotenv.Load("../.env_1040") + if err != nil { + t.Errorf("Error loading .env file") + } + testLogout(t) + testConfigExists(t, filePath, false) + testConfigValid(t) + }, + ) } func testLoginNoPrompt(t *testing.T, filePath string) { // Test logging in w/o args and w/o prompt - t.Run(fmt.Sprintf("login no prompt"), func(t *testing.T) { - testCmd := RootCmd - testCmd.SetArgs([]string{"login", "--no-prompt"}) - noPromptErr := testCmd.Execute() - if noPromptErr != nil { - t.Errorf("RootCmd() = %v, shouldNotPass %v", noPromptErr, true) - } - testConfigExists(t, filePath, true) - os.Unsetenv("KEYFACTOR_USERNAME") - os.Unsetenv("KEYFACTOR_PASSWORD") - testConfigValid(t) - //testLogout(t) - }) + t.Run( + fmt.Sprintf("login no prompt"), func(t *testing.T) { + testCmd := RootCmd + testCmd.SetArgs([]string{"login", "--no-prompt"}) + noPromptErr := testCmd.Execute() + if noPromptErr != nil { + t.Errorf("RootCmd() = %v, shouldNotPass %v", noPromptErr, true) + } + testConfigExists(t, filePath, true) + os.Unsetenv("KEYFACTOR_USERNAME") + os.Unsetenv("KEYFACTOR_PASSWORD") + testConfigValid(t) + //testLogout(t) + }, + ) } diff --git a/cmd/root.go b/cmd/root.go index 3ddaf820..516ae936 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -81,7 +81,14 @@ func initClient( return authViaProvider() } - log.Debug().Msg("call: authEnvVars()") + log.Debug(). + Str("flagConfigFile", flagConfigFile). + Str("flagProfile", flagProfile). + Str("flagAuthProviderType", flagAuthProviderType). + Str("flagAuthProviderProfile", flagAuthProviderProfile). + Bool("noPrompt", noPrompt). + Bool("saveConfig", saveConfig). + Msg("call: authEnvVars()") commandConfig, _ = authEnvVars(flagConfigFile, flagProfile, saveConfig) // check if commandConfig is empty @@ -169,24 +176,33 @@ func initClient( } if !validConfigFileEntry(commandConfig, flagProfile) { - if !noPrompt { - // Auth user interactively - authConfigEntry := commandConfig.Servers[flagProfile] - commandConfig, _ = authInteractive( - authConfigEntry.Hostname, - authConfigEntry.Username, - authConfigEntry.Password, - authConfigEntry.Domain, - authConfigEntry.APIPath, - flagProfile, - false, - false, - flagConfigFile, - ) - } else { - //log.Fatalf("[ERROR] auth config profile: %s", flagProfile) - log.Error().Str("flagProfile", flagProfile).Msg("invalid auth config profile") - return nil, fmt.Errorf("invalid auth config profile: %s", flagProfile) + commandConfigFile, _ := authConfigFile(flagConfigFile, flagProfile, "", noPrompt, false) + // add non empty entries from commandConfigFile to commandConfig + for k, v := range commandConfigFile.Servers { + if v.Hostname != "" || v.Username != "" || v.Password != "" || v.Domain != "" || v.APIPath != "" { + commandConfig.Servers[k] = v + } + } + if !validConfigFileEntry(commandConfig, flagProfile) { + if !noPrompt { + // Auth user interactively + authConfigEntry := commandConfig.Servers[flagProfile] + commandConfig, _ = authInteractive( + authConfigEntry.Hostname, + authConfigEntry.Username, + authConfigEntry.Password, + authConfigEntry.Domain, + authConfigEntry.APIPath, + flagProfile, + false, + false, + flagConfigFile, + ) + } else { + //log.Fatalf("[ERROR] auth config profile: %s", flagProfile) + log.Error().Str("flagProfile", flagProfile).Msg("invalid auth config profile") + return nil, fmt.Errorf("invalid auth config profile: %s", flagProfile) + } } } diff --git a/cmd/storeTypes.go b/cmd/storeTypes.go index 3d8b56ba..7c6e7f75 100644 --- a/cmd/storeTypes.go +++ b/cmd/storeTypes.go @@ -17,15 +17,16 @@ package cmd import ( "encoding/json" "fmt" - "github.com/AlecAivazis/survey/v2" - "github.com/Keyfactor/keyfactor-go-client/v2/api" - "github.com/rs/zerolog/log" - "github.com/spf13/cobra" "io" "net/http" "os" "sort" "strings" + + "github.com/AlecAivazis/survey/v2" + "github.com/Keyfactor/keyfactor-go-client/v2/api" + "github.com/rs/zerolog/log" + "github.com/spf13/cobra" ) var storeTypesCmd = &cobra.Command{ @@ -50,7 +51,11 @@ var storesTypesListCmd = &cobra.Command{ // Authenticate authConfig := createAuthConfigFromParams(kfcHostName, kfcUsername, kfcPassword, kfcDomain, kfcAPIPath) - kfClient, _ := initClient(configFile, profile, providerType, providerProfile, noPrompt, authConfig, false) + kfClient, authErr := initClient(configFile, profile, providerType, providerProfile, noPrompt, authConfig, false) + if authErr != nil { + log.Error().Err(authErr).Msg("unable to authenticate") + return authErr + } // CLI Logic storeTypes, err := kfClient.ListCertificateStoreTypes() @@ -292,7 +297,10 @@ var storesTypeDeleteCmd = &cobra.Command{ } if dryRun { - outputResult(fmt.Sprintf("dry run delete called on certificate store type (%v) with ID: %d", st, id), outputFormat) + outputResult( + fmt.Sprintf("dry run delete called on certificate store type (%v) with ID: %d", st, id), + outputFormat, + ) } else { log.Debug().Interface("storeType", st). Int("id", id). @@ -410,7 +418,12 @@ func getStoreTypesInternet(gitRef string) (map[string]interface{}, error) { //resp, err := http.Get("https://raw.githubusercontent.com/keyfactor/kfutil/main/store_types.json") //resp, err := http.Get("https://raw.githubusercontent.com/keyfactor/kfctl/master/storetypes/storetypes.json") - resp, rErr := http.Get(fmt.Sprintf("https://raw.githubusercontent.com/Keyfactor/kfutil/%s/store_types.json", gitRef)) + resp, rErr := http.Get( + fmt.Sprintf( + "https://raw.githubusercontent.com/Keyfactor/kfutil/%s/store_types.json", + gitRef, + ), + ) if rErr != nil { return nil, rErr } @@ -489,7 +502,13 @@ func init() { // GET store type templates storeTypesCmd.AddCommand(fetchStoreTypesCmd) - fetchStoreTypesCmd.Flags().StringVarP(&gitRef, FlagGitRef, "b", "main", "The git branch or tag to reference when pulling store-types from the internet.") + fetchStoreTypesCmd.Flags().StringVarP( + &gitRef, + FlagGitRef, + "b", + "main", + "The git branch or tag to reference when pulling store-types from the internet.", + ) // LIST command storeTypesCmd.AddCommand(storesTypesListCmd) @@ -504,10 +523,28 @@ func init() { var storeTypeName string var storeTypeID int storeTypesCmd.AddCommand(storesTypeCreateCmd) - storesTypeCreateCmd.Flags().StringVarP(&storeTypeName, "name", "n", "", "Short name of the certificate store type to get. Valid choices are: "+validTypesString) + storesTypeCreateCmd.Flags().StringVarP( + &storeTypeName, + "name", + "n", + "", + "Short name of the certificate store type to get. Valid choices are: "+validTypesString, + ) storesTypeCreateCmd.Flags().BoolVarP(&listValidStoreTypes, "list", "l", false, "List valid store types.") - storesTypeCreateCmd.Flags().StringVarP(&filePath, "from-file", "f", "", "Path to a JSON file containing certificate store type data for a single store.") - storesTypeCreateCmd.Flags().StringVarP(&gitRef, FlagGitRef, "b", "main", "The git branch or tag to reference when pulling store-types from the internet.") + storesTypeCreateCmd.Flags().StringVarP( + &filePath, + "from-file", + "f", + "", + "Path to a JSON file containing certificate store type data for a single store.", + ) + storesTypeCreateCmd.Flags().StringVarP( + &gitRef, + FlagGitRef, + "b", + "main", + "The git branch or tag to reference when pulling store-types from the internet.", + ) storesTypeCreateCmd.Flags().BoolVarP(&createAll, "all", "a", false, "Create all store types.") // UPDATE command @@ -519,7 +556,13 @@ func init() { var dryRun bool storeTypesCmd.AddCommand(storesTypeDeleteCmd) storesTypeDeleteCmd.Flags().IntVarP(&storeTypeID, "id", "i", -1, "ID of the certificate store type to delete.") - storesTypeDeleteCmd.Flags().StringVarP(&storeTypeName, "name", "n", "", "Name of the certificate store type to delete.") + storesTypeDeleteCmd.Flags().StringVarP( + &storeTypeName, + "name", + "n", + "", + "Name of the certificate store type to delete.", + ) storesTypeDeleteCmd.Flags().BoolVarP(&dryRun, "dry-run", "t", false, "Specifies whether to perform a dry run.") storesTypeDeleteCmd.MarkFlagsMutuallyExclusive("id", "name") storesTypeDeleteCmd.Flags().BoolVarP(&deleteAll, "all", "a", false, "Delete all store types.") diff --git a/go.mod b/go.mod index 4e1f1f09..98281817 100644 --- a/go.mod +++ b/go.mod @@ -1,14 +1,14 @@ module kfutil -go 1.21 +go 1.22 require ( github.com/AlecAivazis/survey/v2 v2.3.7 github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.2 github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.4.0 github.com/Jeffail/gabs v1.4.0 - github.com/Keyfactor/keyfactor-go-client-sdk v1.0.2 - github.com/Keyfactor/keyfactor-go-client/v2 v2.2.10 + github.com/Keyfactor/keyfactor-go-client-sdk v1.0.1 + github.com/Keyfactor/keyfactor-go-client/v2 v2.2.10-rc.1 github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 github.com/creack/pty v1.1.21 github.com/google/go-cmp v0.6.0 diff --git a/go.sum b/go.sum index 4b26669a..b018be9d 100644 --- a/go.sum +++ b/go.sum @@ -12,10 +12,10 @@ github.com/Jeffail/gabs v1.4.0 h1://5fYRRTq1edjfIrQGvdkcd22pkYUrHZ5YC/H2GJVAo= github.com/Jeffail/gabs v1.4.0/go.mod h1:6xMvQMK4k33lb7GUUpaAPh6nKMmemQeg5d4gn7/bOXc= github.com/Keyfactor/keyfactor-go-client v1.4.3 h1:CmGvWcuIbDRFM0PfYOQH6UdtAgplvZBpU++KTU8iseg= github.com/Keyfactor/keyfactor-go-client v1.4.3/go.mod h1:3ZymLNCaSazglcuYeNfm9nrzn22wcwLjIWURrnUygBo= -github.com/Keyfactor/keyfactor-go-client-sdk v1.0.2 h1:caLlzFCz2L4Dth/9wh+VlypFATmOMmCSQkCPKOKMxw8= -github.com/Keyfactor/keyfactor-go-client-sdk v1.0.2/go.mod h1:Z5pSk8YFGXHbKeQ1wTzVN8A4P/fZmtAwqu3NgBHbDOs= -github.com/Keyfactor/keyfactor-go-client/v2 v2.2.10 h1:YrkOtGihmFah6YbuO8nr7+scNi6y17Hyo3Malx4hZW0= -github.com/Keyfactor/keyfactor-go-client/v2 v2.2.10/go.mod h1:fiv/ai955uffPu+ZVye5OfOR+fHoVS/sbfVwTWokNrc= +github.com/Keyfactor/keyfactor-go-client-sdk v1.0.1 h1:cs8hhvsY3MJ2o1K11HLTRCjRT8SbsKhhi73Y4By2CI0= +github.com/Keyfactor/keyfactor-go-client-sdk v1.0.1/go.mod h1:Z5pSk8YFGXHbKeQ1wTzVN8A4P/fZmtAwqu3NgBHbDOs= +github.com/Keyfactor/keyfactor-go-client/v2 v2.2.10-rc.1 h1:XCKKnL2kmebm3y7T5iAQ8e1Gbs10/iASAHwhv7M1c/o= +github.com/Keyfactor/keyfactor-go-client/v2 v2.2.10-rc.1/go.mod h1:fiv/ai955uffPu+ZVye5OfOR+fHoVS/sbfVwTWokNrc= github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s= github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=