diff --git a/go.mod b/go.mod index 4c09840b4..752f3b28b 100644 --- a/go.mod +++ b/go.mod @@ -76,8 +76,9 @@ require ( github.com/valyala/fastjson v1.6.3 // indirect github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c // indirect github.com/yusufpapurcu/wmi v1.2.2 // indirect + golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect - golang.org/x/sys v0.5.0 // indirect + golang.org/x/sys v0.12.0 // indirect golang.org/x/text v0.7.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect ) diff --git a/go.sum b/go.sum index 6f0d6618f..0fe3341e9 100644 --- a/go.sum +++ b/go.sum @@ -222,6 +222,8 @@ github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -264,6 +266,8 @@ golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY= golang.org/x/term v0.0.0-20210916214954-140adaaadfaf/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= diff --git a/internal/install/command.go b/internal/install/command.go index c400f59cb..453d9d5cf 100644 --- a/internal/install/command.go +++ b/internal/install/command.go @@ -16,6 +16,7 @@ import ( "github.com/newrelic/newrelic-cli/internal/install/types" "github.com/newrelic/newrelic-cli/internal/utils" nrErrors "github.com/newrelic/newrelic-client-go/v2/pkg/errors" + nrRegion "github.com/newrelic/newrelic-client-go/v2/pkg/region" ) var ( @@ -46,9 +47,10 @@ var Command = &cobra.Command{ sg := initSegment() sg.Track(types.EventTypes.InstallStarted) - detailErr := validateProfile(config.DefaultMaxTimeoutSeconds, sg) + detailErr := validateProfile(config.DefaultMaxTimeoutSeconds, sg) if detailErr != nil { + sg.Track(detailErr.EventName) log.Fatal(detailErr) } @@ -126,19 +128,31 @@ func validateProfile(maxTimeoutSeconds int, sg *segment.Segment) *types.DetailEr if accountID == 0 { errorOccured = true - detailErr = types.NewDetailError(types.EventTypes.AccountIDMissing, "account ID is required") + detailErr = types.NewDetailError(types.EventTypes.AccountIDMissing, "Account ID is required.") return detailErr } if APIKey == "" { errorOccured = true - detailErr = types.NewDetailError(types.EventTypes.APIKeyMissing, "API key is required") + detailErr = types.NewDetailError(types.EventTypes.APIKeyMissing, "User API key is required.") + return detailErr + } + + if !utils.IsValidUserAPIKeyFormat(APIKey) { + errorOccured = true + detailErr = types.NewDetailError(types.EventTypes.InvalidUserAPIKeyFormat, `Invalid user API key format detected. Please provide a valid user API key. User API keys usually have a prefix of "NRAK-" or "NRAA-".`) return detailErr } if region == "" { errorOccured = true - detailErr = types.NewDetailError(types.EventTypes.RegionMissing, "region is required") + detailErr = types.NewDetailError(types.EventTypes.RegionMissing, "Region is required.") + return detailErr + } + + if _, err := nrRegion.Parse(region); err != nil { + errorOccured = true + detailErr = types.NewDetailError(types.EventTypes.InvalidRegion, `Invalid region provided. Valid regions are "US" or "EU".`) return detailErr } diff --git a/internal/install/command_test.go b/internal/install/command_test.go index 97621130b..5af8875f7 100644 --- a/internal/install/command_test.go +++ b/internal/install/command_test.go @@ -1,3 +1,5 @@ +//go:build unit + package install import ( @@ -22,8 +24,7 @@ func TestInstallCommand(t *testing.T) { testcobra.CheckCobraMetadata(t, Command) testcobra.CheckCobraRequiredFlags(t, Command, []string{}) } -func TestCommandValiProfile(t *testing.T) { - +func TestCommandValidProfile(t *testing.T) { accountID := os.Getenv("NEW_RELIC_ACCOUNT_ID") apiKey := os.Getenv("NEW_RELIC_API_KEY") region := os.Getenv("NEW_RELIC_REGION") @@ -50,12 +51,21 @@ func TestCommandValiProfile(t *testing.T) { os.Setenv("NEW_RELIC_API_KEY", "") err = validateProfile(5, c) assert.Equal(t, types.EventTypes.APIKeyMissing, err.EventName) + + os.Setenv("NEW_RELIC_API_KEY", "67890") + err = validateProfile(5, c) + assert.Equal(t, types.EventTypes.InvalidUserAPIKeyFormat, err.EventName) + if apiKey == "" { - os.Setenv("NEW_RELIC_API_KEY", "67890") + os.Setenv("NEW_RELIC_API_KEY", "NRAK-67890") } else { os.Setenv("NEW_RELIC_API_KEY", apiKey) } + os.Setenv("NEW_RELIC_REGION", "au") + err = validateProfile(5, c) + assert.Equal(t, types.EventTypes.InvalidRegion, err.EventName) + os.Setenv("NEW_RELIC_REGION", "") err = validateProfile(5, c) assert.Equal(t, types.EventTypes.RegionMissing, err.EventName) diff --git a/internal/install/types/errors.go b/internal/install/types/errors.go index 209f9478c..3e6f3724c 100644 --- a/internal/install/types/errors.go +++ b/internal/install/types/errors.go @@ -37,6 +37,8 @@ var EventTypes = struct { NrIntegrationPollingErrror EventType OtherError EventType UnableToLocatePostedData EventType + InvalidUserAPIKeyFormat EventType + InvalidRegion EventType }{ InstallStarted: "InstallStarted", AccountIDMissing: "AccountIDMissing", @@ -54,6 +56,8 @@ var EventTypes = struct { UnableToDiscover: "UnableToDiscover", NrIntegrationPollingErrror: "NrIntegrationPollingErrror", OtherError: "OtherError", + InvalidUserAPIKeyFormat: "InvalidUserAPIKeyFormat", + InvalidRegion: "InvalidRegion", } func TryParseEventType(e string) (EventType, bool) { @@ -84,7 +88,12 @@ func TryParseEventType(e string) (EventType, bool) { return EventTypes.UnableToDiscover, true case "NrIntegrationPollingErrror": return EventTypes.NrIntegrationPollingErrror, true + case "InvalidUserAPIKeyFormat": + return EventTypes.InvalidUserAPIKeyFormat, true + case "InvalidRegion": + return EventTypes.InvalidRegion, true } + return "", false } diff --git a/internal/utils/utils.go b/internal/utils/utils.go index 2ec241767..8ae93fc82 100644 --- a/internal/utils/utils.go +++ b/internal/utils/utils.go @@ -8,14 +8,15 @@ import ( "os" "os/signal" "reflect" + "regexp" "strconv" "strings" "syscall" "time" "github.com/mitchellh/go-homedir" - log "github.com/sirupsen/logrus" + "golang.org/x/exp/slices" ) var ( @@ -185,3 +186,21 @@ func IsExitStatusCode(exitCode int, err error) bool { exitCodeString := fmt.Sprintf("exit status %d", exitCode) return strings.Contains(err.Error(), exitCodeString) } + +func IsValidUserAPIKeyFormat(key string) bool { + validUserAPIKeyPrefixes := []string{"NRAK", "NRAA"} + prefix, keyString, found := strings.Cut(key, "-") + + // If no hyphen was found, we don't have a user API key. + if !found { + return false + } + + if !slices.Contains(validUserAPIKeyPrefixes, prefix) { + return false + } + + var isAlphanumeric = regexp.MustCompile("^[a-zA-Z0-9_]*$").MatchString(keyString) + + return isAlphanumeric +} diff --git a/internal/utils/utils_test.go b/internal/utils/utils_test.go index f74905888..75aff0377 100644 --- a/internal/utils/utils_test.go +++ b/internal/utils/utils_test.go @@ -1,3 +1,5 @@ +//go:build unit + package utils import ( @@ -102,3 +104,22 @@ func TestLogIfFatal(t *testing.T) { require.Equal(t, c.expectFatal, fatal) } } + +func TestIsValidUserAPIKeyFormat_Valid(t *testing.T) { + result := IsValidUserAPIKeyFormat("NRAK-ABCDEBFGJIJKLMNOPQRSTUVWXYZ") + assert.True(t, result) +} + +func TestIsValidUserAPIKeyFormat_Invalid(t *testing.T) { + // Invalid prefix + result := IsValidUserAPIKeyFormat("NRBR-ABCDEBFGJIJKLMNOPQRSTUVWXYZ") + assert.False(t, result) + + // A license key is not a valid User API key (note, this is not a real license key) + result = IsValidUserAPIKeyFormat("4321abcsksd344ndlsdm20231mwd21230md12cbsdhk2") + assert.False(t, result) + + // Special characters are invaid after the prefix hyphen + result = IsValidUserAPIKeyFormat("NRAK-@$%^!") + assert.False(t, result) +}