Skip to content

Commit

Permalink
Perform token validation as part of 'k6 cloud login' (#3930)
Browse files Browse the repository at this point in the history
* Perform token validation after 'k6 cloud login'
  • Loading branch information
joanlopez authored Sep 9, 2024
1 parent 6355b4b commit 741610b
Show file tree
Hide file tree
Showing 2 changed files with 91 additions and 29 deletions.
68 changes: 43 additions & 25 deletions cloudapi/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -188,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
}
Expand All @@ -205,47 +211,38 @@ 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
}

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
}

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
}
Expand All @@ -258,3 +255,24 @@ 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) {
data := validateTokenRequest{Token: c.token}
req, err := c.NewRequest("POST", c.baseURL+"/validate-token", data)
if err != nil {
return nil, err
}

vtr := ValidateTokenResponse{}
err = c.Do(req, &vtr)
if err != nil {
return nil, err
}

return &vtr, nil
}
52 changes: 48 additions & 4 deletions cmd/cloud_login.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package cmd

import (
"encoding/json"
"errors"
"fmt"
"syscall"

Expand All @@ -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"
)

Expand Down Expand Up @@ -90,6 +92,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:
Expand All @@ -115,6 +120,13 @@ func (c *cmdCloudLogin) run(cmd *cobra.Command, _ []string) error {
newCloudConf.Token = null.StringFrom(vals["Token"])
}

if newCloudConf.Token.Valid {
err := validateToken(c.globalState, currentJSONConfigRaw, newCloudConf.Token.String)
if err != nil {
return err
}
}

if currentDiskConf.Collectors == nil {
currentDiskConf.Collectors = make(map[string]json.RawMessage)
}
Expand All @@ -127,13 +139,45 @@ 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,
))
}
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
}

0 comments on commit 741610b

Please sign in to comment.