Datasource: Propagate datasource secret decryption errors to the frontend (#52068) (#52557)

* update decrypt secrets function signature and add secrets error handling

* remove a couple instances of unnecessary logging since errors are properly handled now

* add unit test

* fix linting issues

(cherry picked from commit 9aa6ce2a50)

Co-authored-by: Michael Mandrus <41969079+mmandrus@users.noreply.github.com>
This commit is contained in:
Guilherme Caulada
2022-07-20 18:03:44 -03:00
committed by GitHub
co-authored by Michael Mandrus
parent e95c97c119
commit d1f1041aba
9 changed files with 94 additions and 37 deletions
+3 -7
View File
@@ -622,13 +622,9 @@ func (hs *HTTPServer) checkDatasourceHealth(c *models.ReqContext, ds *models.Dat
return response.JSON(http.StatusOK, payload)
}
func (hs *HTTPServer) decryptSecureJsonDataFn(ctx context.Context) func(ds *models.DataSource) map[string]string {
return func(ds *models.DataSource) map[string]string {
decryptedJsonData, err := hs.DataSourcesService.DecryptedValues(ctx, ds)
if err != nil {
hs.log.Error("Failed to decrypt secure json data", "error", err)
}
return decryptedJsonData
func (hs *HTTPServer) decryptSecureJsonDataFn(ctx context.Context) func(ds *models.DataSource) (map[string]string, error) {
return func(ds *models.DataSource) (map[string]string, error) {
return hs.DataSourcesService.DecryptedValues(ctx, ds)
}
}
+8 -1
View File
@@ -2,6 +2,7 @@ package api
import (
"errors"
"fmt"
"net/http"
"github.com/grafana/grafana-plugin-sdk-go/backend"
@@ -23,7 +24,13 @@ func (hs *HTTPServer) handleQueryMetricsError(err error) *response.NormalRespons
if errors.Is(err, models.ErrDataSourceNotFound) {
return response.Error(http.StatusNotFound, "Data source not found", err)
}
var badQuery *query.ErrBadQuery
var secretsPlugin models.ErrDatasourceSecretsPluginUserFriendly
if errors.As(err, &secretsPlugin) {
return response.Error(http.StatusInternalServerError, fmt.Sprint("Secrets Plugin error: ", err.Error()), err)
}
var badQuery query.ErrBadQuery
if errors.As(err, &badQuery) {
return response.Error(http.StatusBadRequest, util.Capitalize(badQuery.Message), err)
}
+48
View File
@@ -1,7 +1,9 @@
package api
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"strings"
@@ -37,6 +39,11 @@ type fakePluginRequestValidator struct {
err error
}
type secretsErrorResponseBody struct {
Error string `json:"error"`
Message string `json:"message"`
}
func (rv *fakePluginRequestValidator) Validate(dsURL string, req *http.Request) error {
return rv.err
}
@@ -101,3 +108,44 @@ func TestAPIEndpoint_Metrics_QueryMetricsV2(t *testing.T) {
require.Equal(t, http.StatusMultiStatus, resp.StatusCode)
})
}
func TestAPIEndpoint_Metrics_PluginDecryptionFailure(t *testing.T) {
qds := query.ProvideService(
nil,
nil,
nil,
&fakePluginRequestValidator{},
&fakeDatasources.FakeDataSourceService{SimulatePluginFailure: true},
&fakePluginClient{
QueryDataHandlerFunc: func(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
resp := backend.Responses{
"A": backend.DataResponse{
Error: fmt.Errorf("query failed"),
},
}
return &backend.QueryDataResponse{Responses: resp}, nil
},
},
&fakeOAuthTokenService{},
)
httpServer := SetupAPITestServer(t, func(hs *HTTPServer) {
hs.queryDataService = qds
})
t.Run("Status code is 500 and a secrets plugin error is returned if there is a problem getting secrets from the remote plugin", func(t *testing.T) {
req := httpServer.NewPostRequest("/api/ds/query", strings.NewReader(queryDatasourceInput))
webtest.RequestWithSignedInUser(req, &models.SignedInUser{UserId: 1, OrgId: 1, OrgRole: models.ROLE_VIEWER})
resp, err := httpServer.SendJSON(req)
require.NoError(t, err)
require.Equal(t, http.StatusInternalServerError, resp.StatusCode)
buf := new(bytes.Buffer)
_, err = buf.ReadFrom(resp.Body)
require.NoError(t, err)
require.NoError(t, resp.Body.Close())
var resObj secretsErrorResponseBody
err = json.Unmarshal(buf.Bytes(), &resObj)
require.NoError(t, err)
require.Equal(t, "unknown error", resObj.Error)
require.Contains(t, resObj.Message, "Secrets Plugin error:")
})
}
+3 -7
View File
@@ -126,12 +126,8 @@ func hiddenRefIDs(queries []Query) (map[string]struct{}, error) {
return hidden, nil
}
func (s *Service) decryptSecureJsonDataFn(ctx context.Context) func(ds *models.DataSource) map[string]string {
return func(ds *models.DataSource) map[string]string {
decryptedJsonData, err := s.dataSourceService.DecryptedValues(ctx, ds)
if err != nil {
logger.Error("Failed to decrypt secure json data", "error", err)
}
return decryptedJsonData
func (s *Service) decryptSecureJsonDataFn(ctx context.Context) func(ds *models.DataSource) (map[string]string, error) {
return func(ds *models.DataSource) (map[string]string, error) {
return s.dataSourceService.DecryptedValues(ctx, ds)
}
}
+9
View File
@@ -81,6 +81,15 @@ func (ds DataSource) AllowedCookies() []string {
return []string{}
}
// Specific error type for grpc secrets management so that we can show more detailed plugin errors to users
type ErrDatasourceSecretsPluginUserFriendly struct {
Err string
}
func (e ErrDatasourceSecretsPluginUserFriendly) Error() string {
return e.Err
}
// ----------------------
// COMMANDS
+10 -5
View File
@@ -3,22 +3,27 @@ package adapters
import (
"encoding/json"
"fmt"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana/pkg/models"
)
// ModelToInstanceSettings converts a models.DataSource to a backend.DataSourceInstanceSettings.
func ModelToInstanceSettings(ds *models.DataSource, decryptFn func(ds *models.DataSource) map[string]string,
// ModelToInstanceSettings converts a datasources.DataSource to a backend.DataSourceInstanceSettings.
func ModelToInstanceSettings(ds *models.DataSource, decryptFn func(ds *models.DataSource) (map[string]string, error),
) (*backend.DataSourceInstanceSettings, error) {
var jsonDataBytes json.RawMessage
if ds.JsonData != nil {
var err error
jsonDataBytes, err = ds.JsonData.MarshalJSON()
if err != nil {
return nil, err
return nil, fmt.Errorf("failed to convert data source to instance settings: %w", err)
}
}
decrypted, err := decryptFn(ds)
if err != nil {
return nil, err
}
return &backend.DataSourceInstanceSettings{
ID: ds.Id,
@@ -30,9 +35,9 @@ func ModelToInstanceSettings(ds *models.DataSource, decryptFn func(ds *models.Da
BasicAuthEnabled: ds.BasicAuth,
BasicAuthUser: ds.BasicAuthUser,
JSONData: jsonDataBytes,
DecryptedSecureJSONData: decryptFn(ds),
DecryptedSecureJSONData: decrypted,
Updated: ds.Updated,
}, nil
}, err
}
// BackendUserFromSignedInUser converts Grafana's SignedInUser model
+3 -7
View File
@@ -127,12 +127,8 @@ func (p *Provider) getCachedPluginSettings(ctx context.Context, pluginID string,
return ps, nil
}
func (p *Provider) decryptSecureJsonDataFn(ctx context.Context) func(ds *models.DataSource) map[string]string {
return func(ds *models.DataSource) map[string]string {
decryptedJsonData, err := p.dataSourceService.DecryptedValues(ctx, ds)
if err != nil {
p.logger.Error("Failed to decrypt secure json data", "error", err)
}
return decryptedJsonData
func (p *Provider) decryptSecureJsonDataFn(ctx context.Context) func(ds *models.DataSource) (map[string]string, error) {
return func(ds *models.DataSource) (map[string]string, error) {
return p.dataSourceService.DecryptedValues(ctx, ds)
}
}
@@ -11,8 +11,9 @@ import (
)
type FakeDataSourceService struct {
lastId int64
DataSources []*models.DataSource
lastId int64
DataSources []*models.DataSource
SimulatePluginFailure bool
}
var _ datasources.DataSourceService = &FakeDataSourceService{}
@@ -107,6 +108,9 @@ func (s *FakeDataSourceService) GetHTTPTransport(ctx context.Context, ds *models
}
func (s *FakeDataSourceService) DecryptedValues(ctx context.Context, ds *models.DataSource) (map[string]string, error) {
if s.SimulatePluginFailure {
return nil, models.ErrDatasourceSecretsPluginUserFriendly{Err: "unknown error"}
}
values := make(map[string]string)
return values, nil
}
+4 -8
View File
@@ -122,7 +122,7 @@ func (s *Service) handleQueryData(ctx context.Context, user *models.SignedInUser
instanceSettings, err := adapters.ModelToInstanceSettings(ds, s.decryptSecureJsonDataFn(ctx))
if err != nil {
return nil, fmt.Errorf("failed to convert data source to instance settings: %w", err)
return nil, err
}
req := &backend.QueryDataRequest{
@@ -304,12 +304,8 @@ func (s *Service) getDataSourceFromQuery(ctx context.Context, user *models.Signe
return nil, NewErrBadQuery("missing data source ID/UID")
}
func (s *Service) decryptSecureJsonDataFn(ctx context.Context) func(ds *models.DataSource) map[string]string {
return func(ds *models.DataSource) map[string]string {
decryptedJsonData, err := s.dataSourceService.DecryptedValues(ctx, ds)
if err != nil {
s.log.Error("Failed to decrypt secure json data", "error", err)
}
return decryptedJsonData
func (s *Service) decryptSecureJsonDataFn(ctx context.Context) func(ds *models.DataSource) (map[string]string, error) {
return func(ds *models.DataSource) (map[string]string, error) {
return s.dataSourceService.DecryptedValues(ctx, ds)
}
}