Skip to content

Commit

Permalink
fix error models
Browse files Browse the repository at this point in the history
  • Loading branch information
yesoreyeram committed Dec 4, 2024
1 parent 4f9df7a commit 5dc82f8
Show file tree
Hide file tree
Showing 6 changed files with 113 additions and 37 deletions.
62 changes: 60 additions & 2 deletions pkg/models/errors.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,67 @@
package models

import "errors"
import (
"errors"
"fmt"
)

var (
// Config errors
ErrMissingAllowedHosts error = errors.New("datasource is missing allowed hosts/URLs. Configure it in the datasource settings page for enhanced security")
// Query errors
ErrEmptyPaginationListFieldName error = &QueryValidationError{field: "pagination_param_list_field_name"}
// Misc errors
ErrUnsuccessfulHTTPResponseStatus error = errors.New("unsuccessful HTTP response")
ErrParsingResponseBodyAsJson error = errors.New("unable to parse response body as JSON")
ErrMissingAllowedHosts error = errors.New("datasource is missing allowed hosts/URLs. Configure it in the datasource settings page for enhanced security")
)

type ConfigValidationError struct {
Field string
}

func (e *ConfigValidationError) Error() string {
return fmt.Sprintf("invalid or empty field: %s ", e.Field)
}

type QueryValidationError struct {
field string
}

func (e *QueryValidationError) Error() string {
return fmt.Sprintf("invalid or empty field: %s ", e.field)
}

type macroError struct {
message string
secondaryMessage string
macroName string
field string
}

func (e *macroError) Error() string {
if e.message == "invalid macro" {
return fmt.Sprintf("invalid %s macro", e.macroName)
}
if e.macroName == "" {
return e.message
}
if e.message == "" {
return fmt.Sprintf("insufficient arguments to %s macro", e.macroName)
}
return fmt.Sprintf("error applying macros to %s field%s. %s", e.field, e.secondaryMessage, e.message)
}

type queryParsingError struct {
message string
err error
}

func (e *queryParsingError) Error() string {
if e.err != nil && e.message == "" {
return e.Error()
}
if e.err != nil {
return fmt.Errorf("%s.%w", e.message, e.err).Error()
}
return e.message
}
25 changes: 12 additions & 13 deletions pkg/models/macros.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package models

import (
"context"
"errors"
"fmt"
"regexp"
"strings"
Expand Down Expand Up @@ -38,7 +37,7 @@ func InterPolateMacros(queryString string, timeRange backend.TimeRange, pluginCo
macros := map[string]macroFunc{
"combineValues": func(query string, args []string) (string, error) {
if len(args) <= 3 {
return query, errorsource.DownstreamError(errors.New("insufficient arguments to combineValues macro"), false)
return query, errorsource.DownstreamError(&macroError{macroName: "combineValues"}, false)
}
if len(args) == 4 && args[3] == "*" {
return "", nil
Expand All @@ -57,7 +56,7 @@ func InterPolateMacros(queryString string, timeRange backend.TimeRange, pluginCo
},
"customInterval": func(query string, args []string) (string, error) {
if len(args) == 0 {
return query, errorsource.DownstreamError(errors.New("insufficient arguments to customInterval macro"), false)
return query, errorsource.DownstreamError(&macroError{macroName: "customInterval"}, false)
}
for argI := range args {
if argI == len(args)-1 {
Expand All @@ -66,7 +65,7 @@ func InterPolateMacros(queryString string, timeRange backend.TimeRange, pluginCo
if argI%2 != 0 {
duration, err := gtime.ParseDuration(args[argI-1])
if err != nil {
return query, errorsource.DownstreamError(errors.New("invalid customInterval macro"), false)
return query, errorsource.DownstreamError(&macroError{message: "invalid macro", macroName: "customInterval"}, false)
}
if timeRangeInMilliSeconds <= duration.Milliseconds() {
return args[argI], nil
Expand Down Expand Up @@ -107,59 +106,59 @@ func InterPolateMacros(queryString string, timeRange backend.TimeRange, pluginCo
func ApplyMacros(ctx context.Context, query Query, timeRange backend.TimeRange, pluginContext backend.PluginContext) (Query, error) {
url, err := InterPolateMacros(query.URL, timeRange, pluginContext)
if err != nil {
return query, fmt.Errorf("error applying macros to url field. %s", err.Error())
return query, &macroError{message: err.Error(), field: "url"}
}
query.URL = url

uql, err := InterPolateMacros(query.UQL, timeRange, pluginContext)
if err != nil {
return query, fmt.Errorf("error applying macros to uql field. %s", err.Error())
return query, &macroError{message: err.Error(), field: "uql"}
}
query.UQL = uql

groq, err := InterPolateMacros(query.GROQ, timeRange, pluginContext)
if err != nil {
return query, fmt.Errorf("error applying macros to uql field. %s", err.Error())
return query, &macroError{message: err.Error(), field: "groq"}
}
query.GROQ = groq

data, err := InterPolateMacros(query.Data, timeRange, pluginContext)
if err != nil {
return query, fmt.Errorf("error applying macros to data field. %s", err.Error())
return query, &macroError{message: err.Error(), field: "data"}
}
query.Data = data

body, err := InterPolateMacros(query.URLOptions.Body, timeRange, pluginContext)
if err != nil {
return query, fmt.Errorf("error applying macros to body data field. %s", err.Error())
return query, &macroError{message: err.Error(), field: "body data"}
}
query.URLOptions.Body = body

graphqlQuery, err := InterPolateMacros(query.URLOptions.BodyGraphQLQuery, timeRange, pluginContext)
if err != nil {
return query, fmt.Errorf("error applying macros to body graphql query field. %s", err.Error())
return query, &macroError{message: err.Error(), field: "body graphql"}
}
query.URLOptions.BodyGraphQLQuery = graphqlQuery

for idx, p := range query.URLOptions.Params {
up, err := InterPolateMacros(p.Value, timeRange, pluginContext)
if err != nil {
return query, fmt.Errorf("error applying macros to url parameter field %s. %s", p.Key, err.Error())
return query, &macroError{message: err.Error(), field: "url parameter", secondaryMessage: " " + p.Key}
}
query.URLOptions.Params[idx].Value = up
}

for idx, cc := range query.ComputedColumns {
up, err := InterPolateMacros(cc.Selector, timeRange, pluginContext)
if err != nil {
return query, fmt.Errorf("error applying macros to computed column %s (alias: %s). %s", cc.Selector, cc.Text, err.Error())
return query, &macroError{message: err.Error(), field: "computed column", secondaryMessage: fmt.Sprintf(" %s (alias: %s)", cc.Selector, cc.Text)}
}
query.ComputedColumns[idx].Selector = up
}

exp, err := InterPolateMacros(query.FilterExpression, timeRange, pluginContext)
if err != nil {
return query, fmt.Errorf("error applying macros to filter expression. %s", err.Error())
return query, &macroError{message: err.Error(), field: "filter expression"}
}
query.FilterExpression = exp

Expand Down
6 changes: 3 additions & 3 deletions pkg/models/macros_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ func TestInterPolateCombineValueMacros(t *testing.T) {
}, tt.pluginContext)
if tt.wantError != nil {
require.NotNil(t, err)
require.Equal(t, tt.wantError, err)
assert.Equal(t, tt.wantError.Error(), err.Error())
return
}
require.Nil(t, err)
Expand Down Expand Up @@ -84,7 +84,7 @@ func TestInterPolateFromToMacros(t *testing.T) {
got, err := models.InterPolateMacros(tt.query, *tr, tt.pluginContext)
if tt.wantError != nil {
require.NotNil(t, err)
require.Equal(t, tt.wantError, err)
assert.Equal(t, tt.wantError.Error(), err.Error())
return
}
require.Nil(t, err)
Expand Down Expand Up @@ -126,7 +126,7 @@ func TestInterPolateCustomIntervalMacros(t *testing.T) {
got, err := models.InterPolateMacros(tt.query, backend.TimeRange{From: time.UnixMilli(from), To: time.UnixMilli(to)}, tt.pluginContext)
if tt.wantError != nil {
require.NotNil(t, err)
require.Equal(t, tt.wantError, err)
assert.Equal(t, tt.wantError.Error(), err.Error())
return
}
require.Nil(t, err)
Expand Down
5 changes: 2 additions & 3 deletions pkg/models/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package models
import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"

Expand Down Expand Up @@ -303,12 +302,12 @@ func LoadQuery(ctx context.Context, backendQuery backend.DataQuery, pluginContex
var query Query
err := json.Unmarshal(backendQuery.JSON, &query)
if err != nil {
return query, errorsource.DownstreamError(fmt.Errorf("error while parsing the query json. %w", err), false)
return query, errorsource.DownstreamError(&queryParsingError{message: "error while parsing the query json", err: err}, false)
}
query = ApplyDefaultsToQuery(ctx, query, settings)
if query.PageMode == PaginationModeList && strings.TrimSpace(query.PageParamListFieldName) == "" {
// Downstream error as user input is not correct
return query, errorsource.DownstreamError(errors.New("pagination_param_list_field_name cannot be empty"), false)
return query, errorsource.DownstreamError(ErrEmptyPaginationListFieldName, false)
}
return ApplyMacros(ctx, query, backendQuery.TimeRange, pluginContext)
}
17 changes: 8 additions & 9 deletions pkg/models/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package models
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/textproto"
"strings"
Expand Down Expand Up @@ -123,29 +122,29 @@ type InfinitySettings struct {

func (s *InfinitySettings) Validate() error {
if (s.BasicAuthEnabled || s.AuthenticationMethod == AuthenticationMethodBasic || s.AuthenticationMethod == AuthenticationMethodDigestAuth) && s.Password == "" {
return errors.New("invalid or empty password detected")
return &ConfigValidationError{Field: "password"}
}
if s.AuthenticationMethod == AuthenticationMethodApiKey && (s.ApiKeyValue == "" || s.ApiKeyKey == "") {
return errors.New("invalid API key specified")
return &ConfigValidationError{Field: "API key"}
}
if s.AuthenticationMethod == AuthenticationMethodBearerToken && s.BearerToken == "" {
return errors.New("invalid or empty bearer token detected")
return &ConfigValidationError{Field: "Bearer token"}
}
if s.AuthenticationMethod == AuthenticationMethodAzureBlob {
if strings.TrimSpace(s.AzureBlobAccountName) == "" {
return errors.New("invalid/empty azure blob account name")
return &ConfigValidationError{Field: "Azure blob account name"}
}
if strings.TrimSpace(s.AzureBlobAccountKey) == "" {
return errors.New("invalid/empty azure blob key")
return &ConfigValidationError{Field: "Azure blob key"}
}
return nil
}
if s.AuthenticationMethod == AuthenticationMethodAWS && s.AWSSettings.AuthType == AWSAuthTypeKeys {
if strings.TrimSpace(s.AWSAccessKey) == "" {
return errors.New("invalid/empty AWS access key")
return &ConfigValidationError{Field: "AWS access key"}
}
if strings.TrimSpace(s.AWSSecretKey) == "" {
return errors.New("invalid/empty AWS secret key")
return &ConfigValidationError{Field: "AWS secret key"}
}
return nil
}
Expand All @@ -161,7 +160,7 @@ func (s *InfinitySettings) DoesAllowedHostsRequired() bool {
return false
}
// If there is specific authentication mechanism (except none and azure blob), then allowed hosts required
if s.AuthenticationMethod != AuthenticationMethodNone && s.AuthenticationMethod != AuthenticationMethodAzureBlob {
if s.AuthenticationMethod != "" && s.AuthenticationMethod != AuthenticationMethodNone && s.AuthenticationMethod != AuthenticationMethodAzureBlob {
return true
}
// If there are any TLS specific settings enabled, then allowed hosts required
Expand Down
35 changes: 28 additions & 7 deletions pkg/models/settings_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package models_test

import (
"context"
"errors"
"testing"

"github.com/grafana/grafana-infinity-datasource/pkg/models"
Expand Down Expand Up @@ -335,73 +334,95 @@ func TestInfinitySettings_Validate(t *testing.T) {
wantErr error
}{
{
name: "should not error when custom settings",
settings: models.InfinitySettings{},
},
{
name: "Should not error when no authentication method specified",
settings: models.InfinitySettings{AuthenticationMethod: models.AuthenticationMethodNone},
},
{
name: "Should error when missing allowed hosts with custom headers",
settings: models.InfinitySettings{AuthenticationMethod: models.AuthenticationMethodNone, CustomHeaders: map[string]string{"A": "B"}},
wantErr: models.ErrMissingAllowedHosts,
},
{
name: "Should error when missing allowed hosts with custom headers and empty Accept",
settings: models.InfinitySettings{AuthenticationMethod: models.AuthenticationMethodNone, CustomHeaders: map[string]string{"A": "B", "Accept": ""}},
wantErr: models.ErrMissingAllowedHosts,
},
{
name: "Should error when missing allowed hosts with custom headers and empty Content-Type",
settings: models.InfinitySettings{AuthenticationMethod: models.AuthenticationMethodNone, CustomHeaders: map[string]string{"A": "B", "Content-Type": ""}},
wantErr: models.ErrMissingAllowedHosts,
},
{
name: "Should error when missing allowed hosts with custom headers and empty Accept and Content-Type",
settings: models.InfinitySettings{AuthenticationMethod: models.AuthenticationMethodNone, CustomHeaders: map[string]string{"A": "B", "Accept": "", "Content-Type": ""}},
wantErr: models.ErrMissingAllowedHosts,
},
{
name: "Should error when missing allowed hosts with multiple custom headers",
settings: models.InfinitySettings{AuthenticationMethod: models.AuthenticationMethodNone, CustomHeaders: map[string]string{"A": "B", "C": "D"}},
wantErr: models.ErrMissingAllowedHosts,
},
{
name: "Should error basic authentication without password",
settings: models.InfinitySettings{AuthenticationMethod: models.AuthenticationMethodBasic},
wantErr: errors.New("invalid or empty password detected"),
wantErr: &models.ConfigValidationError{Field: "password"},
},
{
name: "Should error basic authentication with password but missing allowed hosts",
settings: models.InfinitySettings{AuthenticationMethod: models.AuthenticationMethodBasic, Password: "123"},
wantErr: models.ErrMissingAllowedHosts,
},
{
name: "Should error when API key authentication without key",
settings: models.InfinitySettings{AuthenticationMethod: models.AuthenticationMethodApiKey},
wantErr: errors.New("invalid API key specified"),
wantErr: &models.ConfigValidationError{Field: "API key"},
},
{
name: "Should error when API key authentication with key but without value",
settings: models.InfinitySettings{AuthenticationMethod: models.AuthenticationMethodApiKey, ApiKeyKey: "foo"},
wantErr: errors.New("invalid API key specified"),
wantErr: &models.ConfigValidationError{Field: "API key"},
},
{
name: "Should error when API key authentication with value but without key",
settings: models.InfinitySettings{AuthenticationMethod: models.AuthenticationMethodApiKey, ApiKeyValue: "bar"},
wantErr: errors.New("invalid API key specified"),
wantErr: &models.ConfigValidationError{Field: "API key"},
},
{
name: "Should error when API key authentication with key and value but missing allowed hosts",
settings: models.InfinitySettings{AuthenticationMethod: models.AuthenticationMethodApiKey, ApiKeyKey: "foo", ApiKeyValue: "bar"},
wantErr: models.ErrMissingAllowedHosts,
},
{
name: "Should error when bearer token authentication without token",
settings: models.InfinitySettings{AuthenticationMethod: models.AuthenticationMethodBearerToken},
wantErr: errors.New("invalid or empty bearer token detected"),
wantErr: &models.ConfigValidationError{Field: "Bearer token"},
},
{
name: "Should error when there are no allowed hosts and bearer token authentication with token but missing allowed hosts",
settings: models.InfinitySettings{AuthenticationMethod: models.AuthenticationMethodBearerToken, BearerToken: "foo"},
wantErr: models.ErrMissingAllowedHosts,
},
{
name: "Should error when there are no allowed hosts and secure query fields",
settings: models.InfinitySettings{SecureQueryFields: map[string]string{"foo": "bar"}},
wantErr: models.ErrMissingAllowedHosts,
},
{
name: "Should not error when there are no allowed hosts and valid URL",
settings: models.InfinitySettings{URL: "https://foo"},
},
{
name: "Should error when there are no allowed hosts and TLS client authentication",
settings: models.InfinitySettings{TLSClientAuth: true},
wantErr: models.ErrMissingAllowedHosts,
},
{
settings: models.InfinitySettings{TLSClientAuth: true},
name: "Should error when there are no allowed hosts and TLS cert authentication",
settings: models.InfinitySettings{TLSAuthWithCACert: true},
wantErr: models.ErrMissingAllowedHosts,
},
}
Expand Down

0 comments on commit 5dc82f8

Please sign in to comment.