Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

updated allow config section #1077

Draft
wants to merge 2 commits into
base: dev-3.0.0
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/proud-mice-grow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'grafana-infinity-datasource': minor
---

Updated allowed hosts configuration to skip when used with base URL
4 changes: 2 additions & 2 deletions pkg/infinity/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ func (client *Client) req(ctx context.Context, url string, body io.Reader, setti
return nil, http.StatusInternalServerError, duration, errorsource.DownstreamError(fmt.Errorf("invalid response received for the URL %s", url), false)
}
if res.StatusCode >= http.StatusBadRequest {
err = fmt.Errorf("%w. %s", ErrUnsuccessfulHTTPResponseStatus, res.Status)
err = fmt.Errorf("%w. %s", models.ErrUnsuccessfulHTTPResponseStatus, res.Status)
// Infinity can query anything and users are responsible for ensuring that endpoint/auth is correct
// therefore any incoming error is considered downstream
return nil, res.StatusCode, duration, errorsource.DownstreamError(err, false)
Expand All @@ -249,7 +249,7 @@ func (client *Client) req(ctx context.Context, url string, body io.Reader, setti
var out any
err := json.Unmarshal(bodyBytes, &out)
if err != nil {
err = fmt.Errorf("%w. %w", ErrParsingResponseBodyAsJson, err)
err = fmt.Errorf("%w. %w", models.ErrParsingResponseBodyAsJson, err)
err = errorsource.DownstreamError(err, false)
logger.Debug("error un-marshaling JSON response", "url", url, "error", err.Error())
}
Expand Down
8 changes: 0 additions & 8 deletions pkg/infinity/errors.go

This file was deleted.

67 changes: 67 additions & 0 deletions pkg/models/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package models

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")
)

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)
}
42 changes: 27 additions & 15 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,42 +122,52 @@ 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
}
if s.AuthenticationMethod != AuthenticationMethodNone && len(s.AllowedHosts) < 1 {
return errors.New("configure allowed hosts in the authentication section")
}
if s.HaveSecureHeaders() && len(s.AllowedHosts) < 1 {
return errors.New("configure allowed hosts in the authentication section")
if s.DoesAllowedHostsRequired() && len(s.AllowedHosts) < 1 {
return ErrMissingAllowedHosts
}
return nil
}

func (s *InfinitySettings) HaveSecureHeaders() bool {
func (s *InfinitySettings) DoesAllowedHostsRequired() bool {
// If base url is configured, there is no need for allowed hosts
if strings.TrimSpace(s.URL) != "" {
return false
}
// If there is specific authentication mechanism (except none and azure blob), then allowed hosts required
if s.AuthenticationMethod != "" && s.AuthenticationMethod != AuthenticationMethodNone && s.AuthenticationMethod != AuthenticationMethodAzureBlob {
return true
}
// If there are any TLS specific settings enabled, then allowed hosts required
if s.TLSAuthWithCACert || s.TLSClientAuth {
return true
}
// If there are custom headers (not generic headers such as Accept, Content Type etc), then allowed hosts required
if len(s.CustomHeaders) > 0 {
for k := range s.CustomHeaders {
if textproto.CanonicalMIMEHeaderKey(k) == "Accept" {
Expand All @@ -169,7 +178,10 @@ func (s *InfinitySettings) HaveSecureHeaders() bool {
}
return true
}
return false
}
// If there are custom query parameters, then allowed hosts required
if len(s.SecureQueryFields) > 0 {
return true
}
return false
}
Expand Down
Loading
Loading