Plugins: Remove various custom headers logic (#54146)

Removes various custom headers logic sprinkled around in the backend. 
It should automatically be applied to outgoing HTTP requests via the 
CustomHeadersMiddleware.
This also removes decryption of SecureJSONData to populate custom 
headers in ngalert which seemed to have caused a ton of CPU usage.
pull/54283/head
Marcus Efraimsson 3 years ago committed by GitHub
parent 2f4c8e1b3d
commit 87afd9cadc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      pkg/services/ngalert/api/api.go
  2. 52
      pkg/services/ngalert/eval/eval.go
  3. 3
      pkg/services/ngalert/ngalert.go
  4. 5
      pkg/services/ngalert/schedule/schedule_unit_test.go
  5. 30
      pkg/services/query/query.go
  6. 17
      pkg/services/query/query_test.go
  7. 221
      pkg/tests/api/prometheus/prometheus_test.go
  8. 32
      pkg/tsdb/legacydata/service/service.go
  9. 33
      pkg/tsdb/legacydata/service/service_test.go

@ -22,7 +22,6 @@ import (
"github.com/grafana/grafana/pkg/services/ngalert/state"
"github.com/grafana/grafana/pkg/services/ngalert/store"
"github.com/grafana/grafana/pkg/services/quota"
"github.com/grafana/grafana/pkg/services/secrets"
"github.com/grafana/grafana/pkg/setting"
)
@ -76,7 +75,6 @@ type API struct {
DataProxy *datasourceproxy.DataSourceProxyService
MultiOrgAlertmanager *notifier.MultiOrgAlertmanager
StateManager *state.Manager
SecretsService secrets.Service
AccessControl accesscontrol.AccessControl
Policies *provisioning.NotificationPolicyService
ContactPointService *provisioning.ContactPointService
@ -128,7 +126,7 @@ func (api *API) RegisterAPIEndpoints(m *metrics.API) {
DatasourceCache: api.DatasourceCache,
log: logger,
accessControl: api.AccessControl,
evaluator: eval.NewEvaluator(api.Cfg, log.New("ngalert.eval"), api.DatasourceCache, api.SecretsService, api.ExpressionService),
evaluator: eval.NewEvaluator(api.Cfg, log.New("ngalert.eval"), api.DatasourceCache, api.ExpressionService),
}), m)
api.RegisterConfigurationApiEndpoints(NewConfiguration(
&ConfigSrv{

@ -11,14 +11,12 @@ import (
"strings"
"time"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/expr"
"github.com/grafana/grafana/pkg/expr/classic"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/secrets"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
@ -38,7 +36,6 @@ type evaluatorImpl struct {
cfg *setting.Cfg
log log.Logger
dataSourceCache datasources.CacheService
secretsService secrets.Service
expressionService *expr.Service
}
@ -46,13 +43,11 @@ func NewEvaluator(
cfg *setting.Cfg,
log log.Logger,
datasourceCache datasources.CacheService,
secretsService secrets.Service,
expressionService *expr.Service) Evaluator {
return &evaluatorImpl{
cfg: cfg,
log: log,
dataSourceCache: datasourceCache,
secretsService: secretsService,
expressionService: expressionService,
}
}
@ -164,7 +159,7 @@ type AlertExecCtx struct {
}
// getExprRequest validates the condition, gets the datasource information and creates an expr.Request from it.
func getExprRequest(ctx AlertExecCtx, data []models.AlertQuery, now time.Time, dsCacheService datasources.CacheService, secretsService secrets.Service) (*expr.Request, error) {
func getExprRequest(ctx AlertExecCtx, data []models.AlertQuery, now time.Time, dsCacheService datasources.CacheService) (*expr.Request, error) {
req := &expr.Request{
OrgId: ctx.OrgID,
Headers: map[string]string{
@ -207,19 +202,6 @@ func getExprRequest(ctx AlertExecCtx, data []models.AlertQuery, now time.Time, d
datasources[q.DatasourceUID] = ds
}
// If the datasource has been configured with custom HTTP headers
// then we need to add these to the request
decryptedData, err := secretsService.DecryptJsonData(ctx.Ctx, ds.SecureJsonData)
if err != nil {
return nil, err
}
customHeaders := getCustomHeaders(ds.JsonData, decryptedData)
for k, v := range customHeaders {
if _, ok := req.Headers[k]; !ok {
req.Headers[k] = v
}
}
req.Queries = append(req.Queries, expr.Query{
TimeRange: expr.TimeRange{
From: q.RelativeTimeRange.ToTimeRange(now).From,
@ -236,32 +218,6 @@ func getExprRequest(ctx AlertExecCtx, data []models.AlertQuery, now time.Time, d
return req, nil
}
func getCustomHeaders(jsonData *simplejson.Json, decryptedValues map[string]string) map[string]string {
headers := make(map[string]string)
if jsonData == nil {
return headers
}
index := 1
for {
headerNameSuffix := fmt.Sprintf("httpHeaderName%d", index)
headerValueSuffix := fmt.Sprintf("httpHeaderValue%d", index)
key := jsonData.Get(headerNameSuffix).MustString()
if key == "" {
// No (more) header values are available
break
}
if val, ok := decryptedValues[headerValueSuffix]; ok {
headers[key] = val
}
index++
}
return headers
}
type NumberValueCapture struct {
Var string // RefID
Labels data.Labels
@ -347,7 +303,7 @@ func queryDataResponseToExecutionResults(c models.Condition, execResp *backend.Q
return result
}
func executeQueriesAndExpressions(ctx AlertExecCtx, data []models.AlertQuery, now time.Time, exprService *expr.Service, dsCacheService datasources.CacheService, secretsService secrets.Service) (resp *backend.QueryDataResponse, err error) {
func executeQueriesAndExpressions(ctx AlertExecCtx, data []models.AlertQuery, now time.Time, exprService *expr.Service, dsCacheService datasources.CacheService) (resp *backend.QueryDataResponse, err error) {
defer func() {
if e := recover(); e != nil {
ctx.Log.Error("alert rule panic", "error", e, "stack", string(debug.Stack()))
@ -360,7 +316,7 @@ func executeQueriesAndExpressions(ctx AlertExecCtx, data []models.AlertQuery, no
}
}()
queryDataReq, err := getExprRequest(ctx, data, now, dsCacheService, secretsService)
queryDataReq, err := getExprRequest(ctx, data, now, dsCacheService)
if err != nil {
return nil, err
}
@ -611,7 +567,7 @@ func (e *evaluatorImpl) QueriesAndExpressionsEval(ctx context.Context, orgID int
alertExecCtx := AlertExecCtx{OrgID: orgID, Ctx: alertCtx, ExpressionsEnabled: e.cfg.ExpressionsEnabled, Log: e.log}
execResult, err := executeQueriesAndExpressions(alertExecCtx, data, now, e.expressionService, e.dataSourceCache, e.secretsService)
execResult, err := executeQueriesAndExpressions(alertExecCtx, data, now, e.expressionService, e.dataSourceCache)
if err != nil {
return nil, fmt.Errorf("failed to execute conditions: %w", err)
}

@ -158,7 +158,7 @@ func (ng *AlertNG) init() error {
Cfg: ng.Cfg.UnifiedAlerting,
C: clk,
Logger: ng.Log,
Evaluator: eval.NewEvaluator(ng.Cfg, ng.Log, ng.DataSourceCache, ng.SecretsService, ng.ExpressionService),
Evaluator: eval.NewEvaluator(ng.Cfg, ng.Log, ng.DataSourceCache, ng.ExpressionService),
InstanceStore: store,
RuleStore: store,
Metrics: ng.Metrics.GetSchedulerMetrics(),
@ -194,7 +194,6 @@ func (ng *AlertNG) init() error {
Schedule: ng.schedule,
DataProxy: ng.DataProxy,
QuotaService: ng.QuotaService,
SecretsService: ng.SecretsService,
TransactionManager: store,
InstanceStore: store,
RuleStore: store,

@ -30,8 +30,6 @@ import (
"github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/ngalert/state"
"github.com/grafana/grafana/pkg/services/ngalert/store"
"github.com/grafana/grafana/pkg/services/secrets/fakes"
secretsManager "github.com/grafana/grafana/pkg/services/secrets/manager"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
)
@ -501,8 +499,7 @@ func setupScheduler(t *testing.T, rs *store.FakeRuleStore, is *store.FakeInstanc
var evaluator eval.Evaluator = evalMock
if evalMock == nil {
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
evaluator = eval.NewEvaluator(&setting.Cfg{ExpressionsEnabled: true}, logger, nil, secretsService, expr.ProvideService(&setting.Cfg{ExpressionsEnabled: true}, nil, nil))
evaluator = eval.NewEvaluator(&setting.Cfg{ExpressionsEnabled: true}, logger, nil, expr.ProvideService(&setting.Cfg{ExpressionsEnabled: true}, nil, nil))
}
if registry == nil {

@ -4,7 +4,6 @@ import (
"context"
"fmt"
"net/http"
"strings"
"time"
"github.com/grafana/grafana/pkg/api/dtos"
@ -27,11 +26,6 @@ import (
"github.com/grafana/grafana-plugin-sdk-go/backend/httpclient"
)
const (
headerName = "httpHeaderName"
headerValue = "httpHeaderValue"
)
func ProvideService(
cfg *setting.Cfg,
dataSourceCache datasources.CacheService,
@ -185,10 +179,6 @@ func (s *Service) handleQueryData(ctx context.Context, user *user.SignedInUser,
}
}
for k, v := range customHeaders(ds.JsonData, instanceSettings.DecryptedSecureJSONData) {
req.Headers[k] = v
}
if parsedReq.httpRequest != nil {
proxyutil.ClearCookieHeader(parsedReq.httpRequest, ds.AllowedCookies())
if cookieStr := parsedReq.httpRequest.Header.Get("Cookie"); cookieStr != "" {
@ -216,26 +206,6 @@ type parsedRequest struct {
httpRequest *http.Request
}
func customHeaders(jsonData *simplejson.Json, decryptedJsonData map[string]string) map[string]string {
if jsonData == nil {
return nil
}
data := jsonData.MustMap()
headers := map[string]string{}
for k := range data {
if strings.HasPrefix(k, headerName) {
if header, ok := data[k].(string); ok {
valueKey := strings.ReplaceAll(k, headerName, headerValue)
headers[header] = decryptedJsonData[valueKey]
}
}
}
return headers
}
func (s *Service) parseMetricRequest(ctx context.Context, user *user.SignedInUser, skipCache bool, reqDTO dtos.MetricRequest) (*parsedRequest, error) {
if len(reqDTO.Queries) == 0 {
return nil, NewErrBadQuery("no queries found")

@ -2,7 +2,6 @@ package query_test
import (
"context"
"encoding/json"
"net/http"
"testing"
@ -27,22 +26,6 @@ import (
)
func TestQueryData(t *testing.T) {
t.Run("it attaches custom headers to the request", func(t *testing.T) {
tc := setup(t)
tc.dataSourceCache.ds.JsonData = simplejson.NewFromAny(map[string]interface{}{"httpHeaderName1": "foo", "httpHeaderName2": "bar"})
secureJsonData, err := json.Marshal(map[string]string{"httpHeaderValue1": "test-header", "httpHeaderValue2": "test-header2"})
require.NoError(t, err)
err = tc.secretStore.Set(context.Background(), tc.dataSourceCache.ds.OrgId, tc.dataSourceCache.ds.Name, "datasource", string(secureJsonData))
require.NoError(t, err)
_, err = tc.queryService.QueryData(context.Background(), nil, true, metricRequest(), false)
require.Nil(t, err)
require.Equal(t, map[string]string{"foo": "test-header", "bar": "test-header2"}, tc.pluginContext.req.Headers)
})
t.Run("it auth custom headers to the request", func(t *testing.T) {
token := &oauth2.Token{
TokenType: "bearer",

@ -0,0 +1,221 @@
package prometheus
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"testing"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/tests/testinfra"
"github.com/stretchr/testify/require"
)
func TestIntegrationPrometheusBuffered(t *testing.T) {
dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
DisableAnonymous: true,
})
grafanaListeningAddr, testEnv := testinfra.StartGrafanaEnv(t, dir, path)
ctx := context.Background()
createUser(t, testEnv.SQLStore, user.CreateUserCommand{
DefaultOrgRole: string(org.RoleAdmin),
Password: "admin",
Login: "admin",
})
var outgoingRequest *http.Request
outgoingServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
outgoingRequest = r
w.WriteHeader(http.StatusUnauthorized)
}))
t.Cleanup(outgoingServer.Close)
jsonData := simplejson.NewFromAny(map[string]interface{}{
"httpMethod": "post",
"httpHeaderName1": "X-CUSTOM-HEADER",
"customQueryParameters": "q1=1&q2=2",
})
secureJSONData := map[string]string{
"basicAuthPassword": "basicAuthPassword",
"httpHeaderValue1": "custom-header-value",
}
uid := "prometheus"
err := testEnv.Server.HTTPServer.DataSourcesService.AddDataSource(ctx, &datasources.AddDataSourceCommand{
OrgId: 1,
Access: datasources.DS_ACCESS_PROXY,
Name: "Prometheus",
Type: datasources.DS_PROMETHEUS,
Uid: uid,
Url: outgoingServer.URL,
BasicAuth: true,
BasicAuthUser: "basicAuthUser",
JsonData: jsonData,
SecureJsonData: secureJSONData,
})
require.NoError(t, err)
t.Run("When calling /api/ds/query should set expected headers on outgoing HTTP request", func(t *testing.T) {
query := simplejson.NewFromAny(map[string]interface{}{
"datasource": map[string]interface{}{
"uid": uid,
},
"expr": "up",
"instantQuery": true,
})
buf1 := &bytes.Buffer{}
err = json.NewEncoder(buf1).Encode(dtos.MetricRequest{
From: "now-1h",
To: "now",
Queries: []*simplejson.Json{query},
})
require.NoError(t, err)
u := fmt.Sprintf("http://admin:admin@%s/api/ds/query", grafanaListeningAddr)
// nolint:gosec
resp, err := http.Post(u, "application/json", buf1)
require.NoError(t, err)
require.Equal(t, http.StatusBadRequest, resp.StatusCode)
t.Cleanup(func() {
err := resp.Body.Close()
require.NoError(t, err)
})
_, err = io.ReadAll(resp.Body)
require.NoError(t, err)
require.NotNil(t, outgoingRequest)
require.Equal(t, "/api/v1/query_range?q1=1&q2=2", outgoingRequest.URL.String())
require.Equal(t, "custom-header-value", outgoingRequest.Header.Get("X-CUSTOM-HEADER"))
username, pwd, ok := outgoingRequest.BasicAuth()
require.True(t, ok)
require.Equal(t, "basicAuthUser", username)
require.Equal(t, "basicAuthPassword", pwd)
})
}
func TestIntegrationPrometheusClient(t *testing.T) {
dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
EnableFeatureToggles: []string{"prometheusStreamingJSONParser"},
})
grafanaListeningAddr, testEnv := testinfra.StartGrafanaEnv(t, dir, path)
ctx := context.Background()
createUser(t, testEnv.SQLStore, user.CreateUserCommand{
DefaultOrgRole: string(org.RoleAdmin),
Password: "admin",
Login: "admin",
})
var outgoingRequest *http.Request
outgoingServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
outgoingRequest = r
w.WriteHeader(http.StatusUnauthorized)
}))
t.Cleanup(outgoingServer.Close)
jsonData := simplejson.NewFromAny(map[string]interface{}{
"httpMethod": "post",
"httpHeaderName1": "X-CUSTOM-HEADER",
"customQueryParameters": "q1=1&q2=2",
})
secureJSONData := map[string]string{
"basicAuthPassword": "basicAuthPassword",
"httpHeaderValue1": "custom-header-value",
}
uid := "prometheus"
err := testEnv.Server.HTTPServer.DataSourcesService.AddDataSource(ctx, &datasources.AddDataSourceCommand{
OrgId: 1,
Access: datasources.DS_ACCESS_PROXY,
Name: "Prometheus",
Type: datasources.DS_PROMETHEUS,
Uid: uid,
Url: outgoingServer.URL,
BasicAuth: true,
BasicAuthUser: "basicAuthUser",
JsonData: jsonData,
SecureJsonData: secureJSONData,
})
require.NoError(t, err)
t.Run("When calling /api/ds/query should set expected headers on outgoing HTTP request", func(t *testing.T) {
query := simplejson.NewFromAny(map[string]interface{}{
"datasource": map[string]interface{}{
"uid": uid,
},
"expr": "up",
"instantQuery": true,
})
buf1 := &bytes.Buffer{}
err = json.NewEncoder(buf1).Encode(dtos.MetricRequest{
From: "now-1h",
To: "now",
Queries: []*simplejson.Json{query},
})
require.NoError(t, err)
u := fmt.Sprintf("http://admin:admin@%s/api/ds/query", grafanaListeningAddr)
// nolint:gosec
resp, err := http.Post(u, "application/json", buf1)
require.NoError(t, err)
require.Equal(t, http.StatusInternalServerError, resp.StatusCode)
t.Cleanup(func() {
err := resp.Body.Close()
require.NoError(t, err)
})
_, err = io.ReadAll(resp.Body)
require.NoError(t, err)
require.NotNil(t, outgoingRequest)
require.Equal(t, "/api/v1/query_range", outgoingRequest.URL.Path)
require.Contains(t, outgoingRequest.URL.String(), "&q1=1&q2=2")
require.Equal(t, "custom-header-value", outgoingRequest.Header.Get("X-CUSTOM-HEADER"))
username, pwd, ok := outgoingRequest.BasicAuth()
require.True(t, ok)
require.Equal(t, "basicAuthUser", username)
require.Equal(t, "basicAuthPassword", pwd)
})
t.Run("When calling /api/datasources/uid/{uid}/resources/api/v1/labels should set expected headers on outgoing HTTP request", func(t *testing.T) {
u := fmt.Sprintf("http://%s/api/datasources/uid/%s/resources/api/v1/labels", grafanaListeningAddr, uid)
// nolint:gosec
resp, err := http.Post(u, "application/json", nil)
require.NoError(t, err)
require.Equal(t, http.StatusUnauthorized, resp.StatusCode)
t.Cleanup(func() {
err := resp.Body.Close()
require.NoError(t, err)
})
_, err = io.ReadAll(resp.Body)
require.NoError(t, err)
require.NotNil(t, outgoingRequest)
require.Equal(t, "/api/v1/labels?q1=1&q2=2", outgoingRequest.URL.String())
require.Equal(t, "custom-header-value", outgoingRequest.Header.Get("X-CUSTOM-HEADER"))
username, pwd, ok := outgoingRequest.BasicAuth()
require.True(t, ok)
require.Equal(t, "basicAuthUser", username)
require.Equal(t, "basicAuthPassword", pwd)
})
}
func createUser(t *testing.T, store *sqlstore.SQLStore, cmd user.CreateUserCommand) int64 {
t.Helper()
store.Cfg.AutoAssignOrg = true
store.Cfg.AutoAssignOrgId = 1
u, err := store.CreateUser(context.Background(), cmd)
require.NoError(t, err)
return u.ID
}

@ -3,12 +3,10 @@ package service
import (
"context"
"fmt"
"strings"
"time"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/adapters"
"github.com/grafana/grafana/pkg/services/datasources"
@ -16,11 +14,6 @@ import (
"github.com/grafana/grafana/pkg/tsdb/legacydata"
)
const (
headerName = "httpHeaderName"
headerValue = "httpHeaderValue"
)
var oAuthIsOAuthPassThruEnabledFunc = func(oAuthTokenService oauthtoken.OAuthTokenService, ds *datasources.DataSource) bool {
return oAuthTokenService.IsOAuthPassThruEnabled(ds)
}
@ -126,11 +119,6 @@ func generateRequest(ctx context.Context, ds *datasources.DataSource, decryptedJ
Headers: query.Headers,
}
// Apply Configured Custom Headers to query request.
for k, v := range customHeaders(ds.JsonData, instanceSettings.DecryptedSecureJSONData) {
req.Headers[k] = v
}
for _, q := range query.Queries {
modelJSON, err := q.Model.MarshalJSON()
if err != nil {
@ -151,24 +139,4 @@ func generateRequest(ctx context.Context, ds *datasources.DataSource, decryptedJ
return req, nil
}
func customHeaders(jsonData *simplejson.Json, decryptedJsonData map[string]string) map[string]string {
if jsonData == nil {
return nil
}
data := jsonData.MustMap()
headers := map[string]string{}
for k := range data {
if strings.HasPrefix(k, headerName) {
if header, ok := data[k].(string); ok {
valueKey := strings.ReplaceAll(k, headerName, headerValue)
headers[header] = decryptedJsonData[valueKey]
}
}
}
return headers
}
var _ legacydata.RequestHandler = &Service{}

@ -64,39 +64,6 @@ func TestHandleRequest(t *testing.T) {
})
}
func Test_generateRequest(t *testing.T) {
t.Run("Should attach custom headers to request if present", func(t *testing.T) {
jsonData := simplejson.New()
jsonData.Set(headerName+"testOne", "x-test-one")
jsonData.Set("testOne", "x-test-wrong")
jsonData.Set(headerName+"testTwo", "x-test-two")
decryptedJsonData := map[string]string{
headerValue + "testOne": "secret-value-one",
headerValue + "testTwo": "secret-value-two",
"something": "else",
}
ds := &datasources.DataSource{Id: 12, Type: "unregisteredType", JsonData: jsonData}
query := legacydata.DataQuery{
TimeRange: &legacydata.DataTimeRange{},
Queries: []legacydata.DataSubQuery{
{RefID: "A", DataSource: &datasources.DataSource{Id: 1, Type: "test"}, Model: simplejson.New()},
{RefID: "B", DataSource: &datasources.DataSource{Id: 1, Type: "test"}, Model: simplejson.New()},
},
}
req, err := generateRequest(context.Background(), ds, decryptedJsonData, query)
require.NoError(t, err)
require.NotNil(t, req)
require.EqualValues(t,
map[string]string{
"x-test-one": "secret-value-one",
"x-test-two": "secret-value-two",
}, req.Headers)
})
}
type fakePluginsClient struct {
plugins.Client
backend.QueryDataHandlerFunc

Loading…
Cancel
Save