From 2324996ff64c7f7ac9f3b24cc54572d70335a9c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joan=20L=C3=B3pez=20de=20la=20Franca=20Beltran?= Date: Wed, 4 Sep 2024 10:53:44 +0200 Subject: [PATCH 1/3] Perform token validation after 'k6 cloud login' --- cloudapi/api.go | 31 +++++++++++++++++++++++++++++++ cmd/cloud_login.go | 43 +++++++++++++++++++++++++++++++++++++++---- 2 files changed, 70 insertions(+), 4 deletions(-) diff --git a/cloudapi/api.go b/cloudapi/api.go index a7a61270fae..563f0093021 100644 --- a/cloudapi/api.go +++ b/cloudapi/api.go @@ -61,6 +61,13 @@ type LoginResponse struct { Token string `json:"token"` } +// ValidateTokenResponse is the response of a token validation. +type ValidateTokenResponse struct { + IsValid bool `json:"is_valid"` + Message string `json:"message"` + Token string `json:"token-info"` +} + func (c *Client) handleLogEntriesFromCloud(ctrr CreateTestRunResponse) { logger := c.logger.WithField("source", "grafana-k6-cloud") for _, logEntry := range ctrr.Logs { @@ -258,3 +265,27 @@ func (c *Client) Login(email string, password string) (*LoginResponse, error) { return &lr, nil } + +// ValidateToken calls the endpoint to validate the Client's token and returns the result. +func (c *Client) ValidateToken() (*ValidateTokenResponse, error) { + url := fmt.Sprintf("%s/validate-token", c.baseURL) + + data := struct { + Token string `json:"token"` + }{ + c.token, + } + + req, err := c.NewRequest("POST", url, data) + if err != nil { + return nil, err + } + + vtr := ValidateTokenResponse{} + err = c.Do(req, &vtr) + if err != nil { + return nil, err + } + + return &vtr, nil +} diff --git a/cmd/cloud_login.go b/cmd/cloud_login.go index 376a404e64a..41bc21f958a 100644 --- a/cmd/cloud_login.go +++ b/cmd/cloud_login.go @@ -2,6 +2,7 @@ package cmd import ( "encoding/json" + "errors" "fmt" "syscall" @@ -12,6 +13,7 @@ import ( "go.k6.io/k6/cloudapi" "go.k6.io/k6/cmd/state" + "go.k6.io/k6/lib/consts" "go.k6.io/k6/ui" ) @@ -63,6 +65,8 @@ the "k6 run -o cloud" command. } // run is the code that runs when the user executes `k6 cloud login` +// +//nolint:funlen func (c *cmdCloudLogin) run(cmd *cobra.Command, _ []string) error { currentDiskConf, err := readDiskConfig(c.globalState) if err != nil { @@ -78,6 +82,18 @@ func (c *cmdCloudLogin) run(cmd *cobra.Command, _ []string) error { } } + // We want to use this fully consolidated config for things like + // host addresses, so users can overwrite them with env vars. + consolidatedCurrentConfig, warn, err := cloudapi.GetConsolidatedConfig( + currentJSONConfigRaw, c.globalState.Env, "", nil, nil) + if err != nil { + return err + } + + if warn != "" { + c.globalState.Logger.Warn(warn) + } + // But we don't want to save them back to the JSON file, we only // want to save what already existed there and the login details. newCloudConf := currentJSONConfig @@ -90,6 +106,9 @@ func (c *cmdCloudLogin) run(cmd *cobra.Command, _ []string) error { newCloudConf.Token = null.StringFromPtr(nil) printToStdout(c.globalState, " token reset\n") case show.Bool: + valueColor := getColor(c.globalState.Flags.NoColor || !c.globalState.Stdout.IsTTY, color.FgCyan) + printToStdout(c.globalState, fmt.Sprintf(" token: %s\n", valueColor.Sprint(newCloudConf.Token.String))) + return nil case token.Valid: newCloudConf.Token = token default: @@ -115,6 +134,26 @@ func (c *cmdCloudLogin) run(cmd *cobra.Command, _ []string) error { newCloudConf.Token = null.StringFrom(vals["Token"]) } + if newCloudConf.Token.Valid { + client := cloudapi.NewClient( + c.globalState.Logger, + newCloudConf.Token.String, + consolidatedCurrentConfig.Host.String, + consts.Version, + consolidatedCurrentConfig.Timeout.TimeDuration(), + ) + + var res *cloudapi.ValidateTokenResponse + res, err = client.ValidateToken() + if err != nil { + return err + } + + if !res.IsValid { + return errors.New(`your API token is invalid, please generate a new one at https://app.k6.io/account/api-token`) + } + } + if currentDiskConf.Collectors == nil { currentDiskConf.Collectors = make(map[string]json.RawMessage) } @@ -127,10 +166,6 @@ func (c *cmdCloudLogin) run(cmd *cobra.Command, _ []string) error { } if newCloudConf.Token.Valid { - valueColor := getColor(c.globalState.Flags.NoColor || !c.globalState.Stdout.IsTTY, color.FgCyan) - if !c.globalState.Flags.Quiet { - printToStdout(c.globalState, fmt.Sprintf(" token: %s\n", valueColor.Sprint(newCloudConf.Token.String))) - } printToStdout(c.globalState, fmt.Sprintf( "Logged in successfully, token saved in %s\n", c.globalState.Flags.ConfigFilePath, )) From cdbca393fa99b7b176cba5ab24ee5ebc3af52fd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joan=20L=C3=B3pez=20de=20la=20Franca=20Beltran?= Date: Mon, 9 Sep 2024 10:06:32 +0200 Subject: [PATCH 2/3] Apply suggestions from code review --- cloudapi/api.go | 55 ++++++++++++++++++---------------------------- cmd/cloud_login.go | 6 +++-- 2 files changed, 25 insertions(+), 36 deletions(-) diff --git a/cloudapi/api.go b/cloudapi/api.go index 563f0093021..7d26ef14c8e 100644 --- a/cloudapi/api.go +++ b/cloudapi/api.go @@ -195,8 +195,7 @@ func (c *Client) TestFinished(referenceID string, thresholds ThresholdResult, ta // GetTestProgress for the provided referenceID. func (c *Client) GetTestProgress(referenceID string) (*TestProgressResponse, error) { - url := fmt.Sprintf("%s/test-progress/%s", c.baseURL, referenceID) - req, err := c.NewRequest(http.MethodGet, url, nil) + req, err := c.NewRequest(http.MethodGet, c.baseURL+"/test-progress/"+referenceID, nil) if err != nil { return nil, err } @@ -212,9 +211,7 @@ func (c *Client) GetTestProgress(referenceID string) (*TestProgressResponse, err // StopCloudTestRun tells the cloud to stop the test with the provided referenceID. func (c *Client) StopCloudTestRun(referenceID string) error { - url := fmt.Sprintf("%s/tests/%s/stop", c.baseURL, referenceID) - - req, err := c.NewRequest("POST", url, nil) + req, err := c.NewRequest("POST", c.baseURL+"/tests/"+referenceID+"/stop", nil) if err != nil { return err } @@ -222,17 +219,14 @@ func (c *Client) StopCloudTestRun(referenceID string) error { return c.Do(req, nil) } +type validateOptionsRequest struct { + Options lib.Options `json:"options"` +} + // ValidateOptions sends the provided options to the cloud for validation. func (c *Client) ValidateOptions(options lib.Options) error { - url := fmt.Sprintf("%s/validate-options", c.baseURL) - - data := struct { - Options lib.Options `json:"options"` - }{ - options, - } - - req, err := c.NewRequest("POST", url, data) + data := validateOptionsRequest{Options: options} + req, err := c.NewRequest("POST", c.baseURL+"/validate-options", data) if err != nil { return err } @@ -240,19 +234,15 @@ func (c *Client) ValidateOptions(options lib.Options) error { return c.Do(req, nil) } +type loginRequest struct { + Email string `json:"email"` + Password string `json:"password"` +} + // Login the user with the specified email and password. func (c *Client) Login(email string, password string) (*LoginResponse, error) { - url := fmt.Sprintf("%s/login", c.baseURL) - - data := struct { - Email string `json:"email"` - Password string `json:"password"` - }{ - email, - password, - } - - req, err := c.NewRequest("POST", url, data) + data := loginRequest{Email: email, Password: password} + req, err := c.NewRequest("POST", c.baseURL+"/login", data) if err != nil { return nil, err } @@ -266,17 +256,14 @@ func (c *Client) Login(email string, password string) (*LoginResponse, error) { return &lr, nil } +type validateTokenRequest struct { + Token string `json:"token"` +} + // ValidateToken calls the endpoint to validate the Client's token and returns the result. func (c *Client) ValidateToken() (*ValidateTokenResponse, error) { - url := fmt.Sprintf("%s/validate-token", c.baseURL) - - data := struct { - Token string `json:"token"` - }{ - c.token, - } - - req, err := c.NewRequest("POST", url, data) + data := validateTokenRequest{Token: c.token} + req, err := c.NewRequest("POST", c.baseURL+"/validate-token", data) if err != nil { return nil, err } diff --git a/cmd/cloud_login.go b/cmd/cloud_login.go index 41bc21f958a..7087381220b 100644 --- a/cmd/cloud_login.go +++ b/cmd/cloud_login.go @@ -146,11 +146,13 @@ func (c *cmdCloudLogin) run(cmd *cobra.Command, _ []string) error { var res *cloudapi.ValidateTokenResponse res, err = client.ValidateToken() if err != nil { - return err + return fmt.Errorf("can't validate the API token: %s", err.Error()) } if !res.IsValid { - return errors.New(`your API token is invalid, please generate a new one at https://app.k6.io/account/api-token`) + return errors.New("your API token is invalid - " + + "please, consult the Grafana Cloud k6 documentation for instructions on how to generate a new one:\n" + + "https://grafana.com/docs/grafana-cloud/testing/k6/author-run/tokens-and-cli-authentication") } } From e0de560de85835dbc704302e9b9334fa0fbc8b08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joan=20L=C3=B3pez=20de=20la=20Franca=20Beltran?= Date: Mon, 9 Sep 2024 14:25:09 +0200 Subject: [PATCH 3/3] Apply suggestions from code review --- cmd/cloud_login.go | 69 +++++++++++++++++++++++++--------------------- 1 file changed, 38 insertions(+), 31 deletions(-) diff --git a/cmd/cloud_login.go b/cmd/cloud_login.go index 7087381220b..0213fcf57b4 100644 --- a/cmd/cloud_login.go +++ b/cmd/cloud_login.go @@ -65,8 +65,6 @@ the "k6 run -o cloud" command. } // run is the code that runs when the user executes `k6 cloud login` -// -//nolint:funlen func (c *cmdCloudLogin) run(cmd *cobra.Command, _ []string) error { currentDiskConf, err := readDiskConfig(c.globalState) if err != nil { @@ -82,18 +80,6 @@ func (c *cmdCloudLogin) run(cmd *cobra.Command, _ []string) error { } } - // We want to use this fully consolidated config for things like - // host addresses, so users can overwrite them with env vars. - consolidatedCurrentConfig, warn, err := cloudapi.GetConsolidatedConfig( - currentJSONConfigRaw, c.globalState.Env, "", nil, nil) - if err != nil { - return err - } - - if warn != "" { - c.globalState.Logger.Warn(warn) - } - // But we don't want to save them back to the JSON file, we only // want to save what already existed there and the login details. newCloudConf := currentJSONConfig @@ -135,24 +121,9 @@ func (c *cmdCloudLogin) run(cmd *cobra.Command, _ []string) error { } if newCloudConf.Token.Valid { - client := cloudapi.NewClient( - c.globalState.Logger, - newCloudConf.Token.String, - consolidatedCurrentConfig.Host.String, - consts.Version, - consolidatedCurrentConfig.Timeout.TimeDuration(), - ) - - var res *cloudapi.ValidateTokenResponse - res, err = client.ValidateToken() + err := validateToken(c.globalState, currentJSONConfigRaw, newCloudConf.Token.String) if err != nil { - return fmt.Errorf("can't validate the API token: %s", err.Error()) - } - - if !res.IsValid { - return errors.New("your API token is invalid - " + - "please, consult the Grafana Cloud k6 documentation for instructions on how to generate a new one:\n" + - "https://grafana.com/docs/grafana-cloud/testing/k6/author-run/tokens-and-cli-authentication") + return err } } @@ -174,3 +145,39 @@ func (c *cmdCloudLogin) run(cmd *cobra.Command, _ []string) error { } return nil } + +func validateToken(gs *state.GlobalState, jsonRawConf json.RawMessage, token string) error { + // We want to use this fully consolidated config for things like + // host addresses, so users can overwrite them with env vars. + consolidatedCurrentConfig, warn, err := cloudapi.GetConsolidatedConfig( + jsonRawConf, gs.Env, "", nil, nil) + if err != nil { + return err + } + + if warn != "" { + gs.Logger.Warn(warn) + } + + client := cloudapi.NewClient( + gs.Logger, + token, + consolidatedCurrentConfig.Host.String, + consts.Version, + consolidatedCurrentConfig.Timeout.TimeDuration(), + ) + + var res *cloudapi.ValidateTokenResponse + res, err = client.ValidateToken() + if err != nil { + return fmt.Errorf("can't validate the API token: %s", err.Error()) + } + + if !res.IsValid { + return errors.New("your API token is invalid - " + + "please, consult the Grafana Cloud k6 documentation for instructions on how to generate a new one:\n" + + "https://grafana.com/docs/grafana-cloud/testing/k6/author-run/tokens-and-cli-authentication") + } + + return nil +}