From 5d2f34d8e249877cdd8e1ddda4976dcc4b732048 Mon Sep 17 00:00:00 2001 From: Jeff Levin Date: Mon, 7 Mar 2022 09:33:01 -0900 Subject: [PATCH] ValidatedQueries: start of validated queries API (#44731) * adds an api endpoint for use with public dashboards that validates orgId, dashboard, and panel when running a query. This feature is in ALPHA and should not be enabled yet. Testing is based on new mock sqlstore. Co-authored-by: Jesse Weaver Co-authored-by: Leandro Deveikis --- pkg/api/api.go | 3 + pkg/api/common_test.go | 28 +- pkg/api/metrics.go | 101 ++++ pkg/api/metrics_test.go | 501 ++++++++++++++++++ pkg/api/org_test.go | 24 +- pkg/api/org_users_test.go | 12 +- pkg/api/quota_test.go | 2 +- pkg/api/team_members_test.go | 4 +- pkg/components/simplejson/simplejson.go | 18 + pkg/components/simplejson/simplejson_test.go | 17 + pkg/models/dashboards.go | 22 + .../teamguardian/database/database.go | 4 +- 12 files changed, 707 insertions(+), 29 deletions(-) create mode 100644 pkg/api/metrics_test.go diff --git a/pkg/api/api.go b/pkg/api/api.go index 8c78e917a24..37d1d1fb421 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -389,6 +389,9 @@ func (hs *HTTPServer) registerRoutes() { // DataSource w/ expressions apiRoute.Post("/ds/query", authorize(reqSignedIn, ac.EvalPermission(ActionDatasourcesQuery)), routing.Wrap(hs.QueryMetricsV2)) + // Validated query + apiRoute.Post("/dashboards/org/:orgId/uid/:dashboardUid/panels/:panelId/query", authorize(reqSignedIn, ac.EvalPermission(ActionDatasourcesQuery)), routing.Wrap(hs.QueryMetricsFromDashboard)) + apiRoute.Group("/alerts", func(alertsRoute routing.RouteRegister) { alertsRoute.Post("/test", routing.Wrap(hs.AlertTest)) alertsRoute.Post("/:alertId/pause", reqEditorRole, routing.Wrap(hs.PauseAlert)) diff --git a/pkg/api/common_test.go b/pkg/api/common_test.go index 4c304f8cefd..22e9546a806 100644 --- a/pkg/api/common_test.go +++ b/pkg/api/common_test.go @@ -37,6 +37,7 @@ import ( "github.com/grafana/grafana/pkg/services/searchusers" "github.com/grafana/grafana/pkg/services/searchusers/filters" "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/grafana/grafana/pkg/services/sqlstore/mockstore" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/web" "github.com/stretchr/testify/require" @@ -274,7 +275,7 @@ type accessControlScenarioContext struct { acmock *accesscontrolmock.Mock // db is a test database initialized with InitTestDB - db *sqlstore.SQLStore + db sqlstore.Store // cfg is the setting provider cfg *setting.Cfg @@ -337,7 +338,25 @@ func setupHTTPServer(t *testing.T, useFakeAccessControl bool, enableAccessContro return setupHTTPServerWithCfg(t, useFakeAccessControl, enableAccessControl, cfg) } +func setupHTTPServerWithMockDb(t *testing.T, useFakeAccessControl bool, enableAccessControl bool) accessControlScenarioContext { + // Use a new conf + features := featuremgmt.WithFeatures("accesscontrol", enableAccessControl) + cfg := setting.NewCfg() + cfg.IsFeatureToggleEnabled = features.IsEnabled + + db := sqlstore.InitTestDB(t) + db.Cfg = cfg + + return setupHTTPServerWithCfgDb(t, useFakeAccessControl, enableAccessControl, cfg, db, mockstore.NewSQLStoreMock()) +} + func setupHTTPServerWithCfg(t *testing.T, useFakeAccessControl, enableAccessControl bool, cfg *setting.Cfg) accessControlScenarioContext { + db := sqlstore.InitTestDB(t) + db.Cfg = cfg + return setupHTTPServerWithCfgDb(t, useFakeAccessControl, enableAccessControl, cfg, db, db) +} + +func setupHTTPServerWithCfgDb(t *testing.T, useFakeAccessControl, enableAccessControl bool, cfg *setting.Cfg, db *sqlstore.SQLStore, store sqlstore.Store) accessControlScenarioContext { t.Helper() features := featuremgmt.WithFeatures("accesscontrol", enableAccessControl) @@ -345,13 +364,10 @@ func setupHTTPServerWithCfg(t *testing.T, useFakeAccessControl, enableAccessCont var acmock *accesscontrolmock.Mock - // Use a test DB - db := sqlstore.InitTestDB(t) - db.Cfg = cfg - dashboardsStore := dashboardsstore.ProvideDashboardStore(db) routeRegister := routing.NewRouteRegister() + // Create minimal HTTP Server hs := &HTTPServer{ Cfg: cfg, @@ -360,7 +376,7 @@ func setupHTTPServerWithCfg(t *testing.T, useFakeAccessControl, enableAccessCont Live: newTestLive(t), QuotaService: "a.QuotaService{Cfg: cfg}, RouteRegister: routeRegister, - SQLStore: db, + SQLStore: store, searchUsersService: searchusers.ProvideUsersService(db, filters.ProvideOSSSearchUserFilter()), dashboardService: dashboardservice.ProvideDashboardService(dashboardsStore, nil), } diff --git a/pkg/api/metrics.go b/pkg/api/metrics.go index b250c2a3817..db0cad5d71f 100644 --- a/pkg/api/metrics.go +++ b/pkg/api/metrics.go @@ -1,14 +1,18 @@ package api import ( + "context" "errors" "net/http" + "strconv" "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/query" + "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/tsdb/legacydata" "github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/web" @@ -40,6 +44,103 @@ func (hs *HTTPServer) QueryMetricsV2(c *models.ReqContext) response.Response { return toJsonStreamingResponse(resp) } +func parseDashboardQueryParams(params map[string]string) (models.GetDashboardQuery, int64, error) { + query := models.GetDashboardQuery{} + + if params[":orgId"] == "" || params[":dashboardUid"] == "" || params[":panelId"] == "" { + return query, 0, models.ErrDashboardOrPanelIdentifierNotSet + } + + orgId, err := strconv.ParseInt(params[":orgId"], 10, 64) + if err != nil { + return query, 0, models.ErrDashboardPanelIdentifierInvalid + } + + panelId, err := strconv.ParseInt(params[":panelId"], 10, 64) + if err != nil { + return query, 0, models.ErrDashboardPanelIdentifierInvalid + } + + query.Uid = params[":dashboardUid"] + query.OrgId = orgId + + return query, panelId, nil +} + +func checkDashboardAndPanel(ctx context.Context, ss sqlstore.Store, query models.GetDashboardQuery, panelId int64) error { + // Query the dashboard + if err := ss.GetDashboard(ctx, &query); err != nil { + return err + } + + if query.Result == nil { + return models.ErrDashboardCorrupt + } + + // dashboard saved but no panels + dashboard := query.Result + if dashboard.Data == nil { + return models.ErrDashboardCorrupt + } + + // FIXME: parse the dashboard JSON in a more performant/structured way. + panels := dashboard.Data.Get("panels") + + for i := 0; ; i++ { + panel, ok := panels.CheckGetIndex(i) + if !ok { + break + } + + if panel.Get("id").MustInt64(-1) == panelId { + return nil + } + } + + // no panel with that ID + return models.ErrDashboardPanelNotFound +} + +// QueryMetricsV2 returns query metrics. +// POST /api/ds/query DataSource query w/ expressions +func (hs *HTTPServer) QueryMetricsFromDashboard(c *models.ReqContext) response.Response { + // check feature flag + if !hs.Features.IsEnabled(featuremgmt.FlagValidatedQueries) { + return response.Respond(http.StatusNotFound, "404 page not found\n") + } + + // build query + reqDTO := dtos.MetricRequest{} + if err := web.Bind(c.Req, &reqDTO); err != nil { + return response.Error(http.StatusBadRequest, "bad request data", err) + } + params := web.Params(c.Req) + getDashboardQuery, panelId, err := parseDashboardQueryParams(params) + + // check dashboard: inside the statement is the happy path. we should maybe + // refactor this as it's not super obvious + if err == nil { + err = checkDashboardAndPanel(c.Req.Context(), hs.SQLStore, getDashboardQuery, panelId) + } + + // 404 if dashboard or panel not found + if err != nil { + c.Logger.Warn("Failed to find dashboard or panel for validated query", "err", err) + var dashboardErr models.DashboardErr + if ok := errors.As(err, &dashboardErr); ok { + return response.Error(dashboardErr.StatusCode, dashboardErr.Error(), err) + } + return response.Error(http.StatusNotFound, "Dashboard or panel not found", err) + } + + // return panel data + resp, err := hs.queryDataService.QueryData(c.Req.Context(), c.SignedInUser, c.SkipCache, reqDTO, true) + if err != nil { + return hs.handleQueryMetricsError(err) + } + return toJsonStreamingResponse(resp) +} + // QueryMetrics returns query metrics // POST /api/tsdb/query //nolint: staticcheck // legacydata.DataResponse deprecated diff --git a/pkg/api/metrics_test.go b/pkg/api/metrics_test.go new file mode 100644 index 00000000000..634ce0ee74e --- /dev/null +++ b/pkg/api/metrics_test.go @@ -0,0 +1,501 @@ +package api + +import ( + "context" + "fmt" + "net/http" + "strconv" + "strings" + "testing" + + "github.com/grafana/grafana/pkg/services/featuremgmt" + "github.com/grafana/grafana/pkg/services/sqlstore/mockstore" + + "golang.org/x/oauth2" + + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/services/query" + "github.com/grafana/grafana/pkg/services/secrets/fakes" + "github.com/stretchr/testify/assert" +) + +var ( + queryDatasourceInput = `{ + "from": "", + "to": "", + "queries": [ + { + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "queryType": "randomWalk", + "refId": "A" + } + ] + }` + + getDashboardByIdOutput = `{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 0, + "links": [], + "liveNow": false, + "panels": [ + { + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 2, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "title": "Panel Title", + "type": "timeseries" + } + ], + "schemaVersion": 35, + "style": "dark", + "tags": [], + "templating": { + "list": [] + }, + "time": { + "from": "now-6h", + "to": "now" + }, + "timepicker": {}, + "timezone": "", + "title": "New dashboard", + "version": 0, + "weekStart": "" + }` +) + +type fakePluginRequestValidator struct { + err error +} + +func (rv *fakePluginRequestValidator) Validate(dsURL string, req *http.Request) error { + return rv.err +} + +type fakeOAuthTokenService struct { + passThruEnabled bool + token *oauth2.Token +} + +func (ts *fakeOAuthTokenService) GetCurrentOAuthToken(context.Context, *models.SignedInUser) *oauth2.Token { + return ts.token +} + +func (ts *fakeOAuthTokenService) IsOAuthPassThruEnabled(*models.DataSource) bool { + return ts.passThruEnabled +} + +func (c *dashboardFakePluginClient) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { + c.req = req + resp := backend.Responses{} + return &backend.QueryDataResponse{Responses: resp}, nil +} + +type dashboardFakePluginClient struct { + plugins.Client + + req *backend.QueryDataRequest +} + +// `/dashboards/org/:orgId/uid/:dashboardUid/panels/:panelId/query` endpoints test +func TestAPIEndpoint_Metrics_QueryMetricsFromDashboard(t *testing.T) { + sc := setupHTTPServerWithMockDb(t, false, false) + + setInitCtxSignedInViewer(sc.initCtx) + sc.hs.queryDataService = query.ProvideService( + nil, + nil, + nil, + &fakePluginRequestValidator{}, + fakes.NewFakeSecretsService(), + &dashboardFakePluginClient{}, + &fakeOAuthTokenService{}, + ) + + sc.hs.Features = featuremgmt.WithFeatures(featuremgmt.FlagValidatedQueries, true) + + dashboardJson, err := simplejson.NewFromReader(strings.NewReader(getDashboardByIdOutput)) + if err != nil { + t.Fatalf("Failed to unmarshal dashboard json: %v", err) + } + + mockDb := sc.hs.SQLStore.(*mockstore.SQLStoreMock) + + t.Run("Can query a valid dashboard", func(t *testing.T) { + mockDb.ExpectedDashboard = &models.Dashboard{ + Uid: "1", + OrgId: testOrgID, + Data: dashboardJson, + } + mockDb.ExpectedError = nil + + response := callAPI( + sc.server, + http.MethodPost, + fmt.Sprintf("/api/dashboards/org/%d/uid/%s/panels/%s/query", testOrgID, "1", "2"), + strings.NewReader(queryDatasourceInput), + t, + ) + assert.Equal(t, http.StatusOK, response.Code) + }) + + t.Run("Cannot query without a valid orgid or dashboard or panel ID", func(t *testing.T) { + mockDb.ExpectedDashboard = nil + mockDb.ExpectedError = models.ErrDashboardOrPanelIdentifierNotSet + + response := callAPI( + sc.server, + http.MethodPost, + "/api/dashboards/org//uid//panels//query", + strings.NewReader(queryDatasourceInput), + t, + ) + assert.Equal(t, http.StatusBadRequest, response.Code) + assert.JSONEq( + t, + fmt.Sprintf( + "{\"error\":\"%[1]s\",\"message\":\"%[1]s\"}", + models.ErrDashboardOrPanelIdentifierNotSet, + ), + response.Body.String(), + ) + }) + + t.Run("Cannot query without a valid orgid", func(t *testing.T) { + response := callAPI( + sc.server, + http.MethodPost, + fmt.Sprintf("/api/dashboards/org//uid/%s/panels/%s/query", "1", "2"), + strings.NewReader(queryDatasourceInput), + t, + ) + assert.Equal(t, http.StatusBadRequest, response.Code) + assert.JSONEq( + t, + fmt.Sprintf( + "{\"error\":\"%[1]s\",\"message\":\"%[1]s\"}", + models.ErrDashboardOrPanelIdentifierNotSet, + ), + response.Body.String(), + ) + }) + + t.Run("Cannot query without a valid dashboard or panel ID", func(t *testing.T) { + response := callAPI( + sc.server, + http.MethodPost, + fmt.Sprintf("/api/dashboards/org//uid/%s/panels/%s/query", "1", "2"), + strings.NewReader(queryDatasourceInput), + t, + ) + assert.Equal(t, http.StatusBadRequest, response.Code) + assert.JSONEq( + t, + fmt.Sprintf( + "{\"error\":\"%[1]s\",\"message\":\"%[1]s\"}", + models.ErrDashboardOrPanelIdentifierNotSet, + ), + response.Body.String(), + ) + }) + + t.Run("Cannot query when ValidatedQueries is disabled", func(t *testing.T) { + sc.hs.Features = featuremgmt.WithFeatures(featuremgmt.FlagValidatedQueries, false) + + response := callAPI( + sc.server, + http.MethodPost, + "/api/dashboards/uid/1/panels/1/query", + strings.NewReader(queryDatasourceInput), + t, + ) + + assert.Equal(t, http.StatusNotFound, response.Code) + assert.Equal( + t, + "404 page not found\n", + response.Body.String(), + ) + }) +} + +func TestAPIEndpoint_Metrics_checkDashboardAndPanel(t *testing.T) { + dashboardJson, err := simplejson.NewFromReader(strings.NewReader(getDashboardByIdOutput)) + if err != nil { + t.Fatalf("Failed to unmarshal dashboard json: %v", err) + } + + tests := []struct { + name string + orgId int64 + dashboardUid string + panelId int64 + dashboardQueryResult *models.Dashboard + expectedError error + }{ + { + name: "Work when correct dashboardId and panelId given", + orgId: testOrgID, + dashboardUid: "1", + panelId: 2, + dashboardQueryResult: &models.Dashboard{ + Uid: "1", + OrgId: testOrgID, + Data: dashboardJson, + }, + expectedError: nil, + }, + { + name: "404 on invalid orgId", + orgId: 7, + dashboardUid: "1", + panelId: 2, + dashboardQueryResult: nil, + expectedError: models.ErrDashboardNotFound, + }, + { + name: "404 on invalid dashboardId", + orgId: testOrgID, + dashboardUid: "", + panelId: 2, + dashboardQueryResult: nil, + expectedError: models.ErrDashboardNotFound, + }, + { + name: "404 on invalid panelId", + orgId: testOrgID, + dashboardUid: "1", + panelId: 0, + dashboardQueryResult: nil, + expectedError: models.ErrDashboardNotFound, + }, + { + name: "Fails when the dashboard does not exist", + orgId: testOrgID, + dashboardUid: "1", + panelId: 2, + dashboardQueryResult: nil, + expectedError: models.ErrDashboardNotFound, + }, + { + name: "Fails when the panel does not exist", + orgId: testOrgID, + dashboardUid: "1", + panelId: 3, + dashboardQueryResult: &models.Dashboard{ + Id: 1, + OrgId: testOrgID, + Data: dashboardJson, + }, + expectedError: models.ErrDashboardPanelNotFound, + }, + { + name: "Fails when the dashboard contents are nil", + orgId: testOrgID, + dashboardUid: "1", + panelId: 3, + dashboardQueryResult: &models.Dashboard{ + Uid: "1", + OrgId: testOrgID, + Data: nil, + }, + expectedError: models.ErrDashboardCorrupt, + }, + } + + ss := mockstore.NewSQLStoreMock() + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + ss.ExpectedDashboard = test.dashboardQueryResult + ss.ExpectedError = test.expectedError + + query := models.GetDashboardQuery{ + OrgId: test.orgId, + Uid: test.dashboardUid, + } + + assert.Equal(t, test.expectedError, checkDashboardAndPanel(context.Background(), ss, query, test.panelId)) + }) + } +} + +func TestAPIEndpoint_Metrics_ParseDashboardQueryParams(t *testing.T) { + tests := []struct { + name string + params map[string]string + expectedDashboardQuery models.GetDashboardQuery + expectedPanelId int64 + expectedError error + }{ + { + name: "Work when correct orgId, dashboardId and panelId given", + params: map[string]string{ + ":orgId": strconv.FormatInt(testOrgID, 10), + ":dashboardUid": "1", + ":panelId": "2", + }, + expectedDashboardQuery: models.GetDashboardQuery{ + Uid: "1", + OrgId: 1, + }, + expectedPanelId: 2, + expectedError: nil, + }, + { + name: "Get error when dashboardUid not given", + params: map[string]string{ + ":orgId": strconv.FormatInt(testOrgID, 10), + ":dashboardUid": "", + ":panelId": "1", + }, + expectedDashboardQuery: models.GetDashboardQuery{}, + expectedPanelId: 0, + expectedError: models.ErrDashboardOrPanelIdentifierNotSet, + }, + { + name: "Get error when panelId not given", + params: map[string]string{ + ":orgId": strconv.FormatInt(testOrgID, 10), + ":dashboardUid": "1", + ":panelId": "", + }, + expectedDashboardQuery: models.GetDashboardQuery{}, + expectedPanelId: 0, + expectedError: models.ErrDashboardOrPanelIdentifierNotSet, + }, + { + name: "Get error when orgId not given", + params: map[string]string{ + ":orgId": "", + ":dashboardUid": "1", + ":panelId": "2", + }, + expectedDashboardQuery: models.GetDashboardQuery{}, + expectedPanelId: 0, + expectedError: models.ErrDashboardOrPanelIdentifierNotSet, + }, + { + name: "Get error when panelId not is invalid", + params: map[string]string{ + ":orgId": strconv.FormatInt(testOrgID, 10), + ":dashboardUid": "1", + ":panelId": "aaa", + }, + expectedDashboardQuery: models.GetDashboardQuery{}, + expectedPanelId: 0, + expectedError: models.ErrDashboardPanelIdentifierInvalid, + }, + { + name: "Get error when orgId not is invalid", + params: map[string]string{ + ":orgId": "aaa", + ":dashboardUid": "1", + ":panelId": "2", + }, + expectedDashboardQuery: models.GetDashboardQuery{}, + expectedPanelId: 0, + expectedError: models.ErrDashboardPanelIdentifierInvalid, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + // other validations? + dashboardQuery, panelId, err := parseDashboardQueryParams(test.params) + assert.Equal(t, test.expectedDashboardQuery, dashboardQuery) + assert.Equal(t, test.expectedPanelId, panelId) + assert.Equal(t, test.expectedError, err) + }) + } +} diff --git a/pkg/api/org_test.go b/pkg/api/org_test.go index afee711cae8..5504345d43d 100644 --- a/pkg/api/org_test.go +++ b/pkg/api/org_test.go @@ -185,7 +185,7 @@ func TestAPIEndpoint_PutCurrentOrgAddress_AccessControl(t *testing.T) { // `/api/orgs/` endpoints test // setupOrgsDBForAccessControlTests stores users and create specified number of orgs -func setupOrgsDBForAccessControlTests(t *testing.T, db sqlstore.SQLStore, user models.SignedInUser, orgsCount int) { +func setupOrgsDBForAccessControlTests(t *testing.T, db sqlstore.Store, user models.SignedInUser, orgsCount int) { t.Helper() _, err := db.CreateUser(context.Background(), models.CreateUserCommand{Email: user.Email, SkipOrgSetup: true, Login: user.Login}) @@ -231,7 +231,7 @@ func TestAPIEndpoint_CreateOrgs_AccessControl(t *testing.T) { sc := setupHTTPServer(t, true, true) setInitCtxSignedInViewer(sc.initCtx) - setupOrgsDBForAccessControlTests(t, *sc.db, *sc.initCtx.SignedInUser, 0) + setupOrgsDBForAccessControlTests(t, sc.db, *sc.initCtx.SignedInUser, 0) input := strings.NewReader(fmt.Sprintf(testCreateOrgCmd, 2)) t.Run("AccessControl allows creating Orgs with correct permissions", func(t *testing.T) { @@ -252,7 +252,7 @@ func TestAPIEndpoint_DeleteOrgs_LegacyAccessControl(t *testing.T) { sc := setupHTTPServer(t, true, false) setInitCtxSignedInViewer(sc.initCtx) - setupOrgsDBForAccessControlTests(t, *sc.db, *sc.initCtx.SignedInUser, 2) + setupOrgsDBForAccessControlTests(t, sc.db, *sc.initCtx.SignedInUser, 2) t.Run("Viewer cannot delete Orgs", func(t *testing.T) { response := callAPI(sc.server, http.MethodDelete, fmt.Sprintf(deleteOrgsURL, 2), nil, t) @@ -270,7 +270,7 @@ func TestAPIEndpoint_DeleteOrgs_AccessControl(t *testing.T) { sc := setupHTTPServer(t, true, true) setInitCtxSignedInViewer(sc.initCtx) - setupOrgsDBForAccessControlTests(t, *sc.db, *sc.initCtx.SignedInUser, 2) + setupOrgsDBForAccessControlTests(t, sc.db, *sc.initCtx.SignedInUser, 2) t.Run("AccessControl prevents deleting Orgs with incorrect permissions", func(t *testing.T) { setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: "orgs:invalid"}}, 2) @@ -331,7 +331,7 @@ func TestAPIEndpoint_GetOrg_LegacyAccessControl(t *testing.T) { setInitCtxSignedInViewer(sc.initCtx) // Create two orgs, to fetch another one than the logged in one - setupOrgsDBForAccessControlTests(t, *sc.db, *sc.initCtx.SignedInUser, 2) + setupOrgsDBForAccessControlTests(t, sc.db, *sc.initCtx.SignedInUser, 2) t.Run("Viewer cannot view another Org", func(t *testing.T) { response := callAPI(sc.server, http.MethodGet, fmt.Sprintf(getOrgsURL, 2), nil, t) @@ -350,7 +350,7 @@ func TestAPIEndpoint_GetOrg_AccessControl(t *testing.T) { setInitCtxSignedInViewer(sc.initCtx) // Create two orgs, to fetch another one than the logged in one - setupOrgsDBForAccessControlTests(t, *sc.db, *sc.initCtx.SignedInUser, 2) + setupOrgsDBForAccessControlTests(t, sc.db, *sc.initCtx.SignedInUser, 2) t.Run("AccessControl allows viewing another org with correct permissions", func(t *testing.T) { setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: ActionOrgsRead}}, 2) @@ -374,7 +374,7 @@ func TestAPIEndpoint_GetOrgByName_LegacyAccessControl(t *testing.T) { setInitCtxSignedInViewer(sc.initCtx) // Create two orgs, to fetch another one than the logged in one - setupOrgsDBForAccessControlTests(t, *sc.db, *sc.initCtx.SignedInUser, 2) + setupOrgsDBForAccessControlTests(t, sc.db, *sc.initCtx.SignedInUser, 2) t.Run("Viewer cannot view another Org", func(t *testing.T) { response := callAPI(sc.server, http.MethodGet, fmt.Sprintf(getOrgsByNameURL, "TestOrg2"), nil, t) @@ -393,7 +393,7 @@ func TestAPIEndpoint_GetOrgByName_AccessControl(t *testing.T) { setInitCtxSignedInViewer(sc.initCtx) // Create two orgs, to fetch another one than the logged in one - setupOrgsDBForAccessControlTests(t, *sc.db, *sc.initCtx.SignedInUser, 2) + setupOrgsDBForAccessControlTests(t, sc.db, *sc.initCtx.SignedInUser, 2) t.Run("AccessControl allows viewing another org with correct permissions", func(t *testing.T) { setAccessControlPermissions(sc.acmock, []*accesscontrol.Permission{{Action: ActionOrgsRead}}, accesscontrol.GlobalOrgID) @@ -412,7 +412,7 @@ func TestAPIEndpoint_PutOrg_LegacyAccessControl(t *testing.T) { setInitCtxSignedInViewer(sc.initCtx) // Create two orgs, to update another one than the logged in one - setupOrgsDBForAccessControlTests(t, *sc.db, *sc.initCtx.SignedInUser, 2) + setupOrgsDBForAccessControlTests(t, sc.db, *sc.initCtx.SignedInUser, 2) input := strings.NewReader(testUpdateOrgNameForm) @@ -433,7 +433,7 @@ func TestAPIEndpoint_PutOrg_AccessControl(t *testing.T) { setInitCtxSignedInViewer(sc.initCtx) // Create two orgs, to update another one than the logged in one - setupOrgsDBForAccessControlTests(t, *sc.db, *sc.initCtx.SignedInUser, 2) + setupOrgsDBForAccessControlTests(t, sc.db, *sc.initCtx.SignedInUser, 2) input := strings.NewReader(testUpdateOrgNameForm) t.Run("AccessControl allows updating another org with correct permissions", func(t *testing.T) { @@ -460,7 +460,7 @@ func TestAPIEndpoint_PutOrgAddress_LegacyAccessControl(t *testing.T) { setInitCtxSignedInViewer(sc.initCtx) // Create two orgs, to update another one than the logged in one - setupOrgsDBForAccessControlTests(t, *sc.db, *sc.initCtx.SignedInUser, 2) + setupOrgsDBForAccessControlTests(t, sc.db, *sc.initCtx.SignedInUser, 2) input := strings.NewReader(testUpdateOrgAddressForm) @@ -481,7 +481,7 @@ func TestAPIEndpoint_PutOrgAddress_AccessControl(t *testing.T) { setInitCtxSignedInViewer(sc.initCtx) // Create two orgs, to update another one than the logged in one - setupOrgsDBForAccessControlTests(t, *sc.db, *sc.initCtx.SignedInUser, 2) + setupOrgsDBForAccessControlTests(t, sc.db, *sc.initCtx.SignedInUser, 2) input := strings.NewReader(testUpdateOrgAddressForm) t.Run("AccessControl allows updating another org address with correct permissions", func(t *testing.T) { diff --git a/pkg/api/org_users_test.go b/pkg/api/org_users_test.go index a098d163932..38858f640bf 100644 --- a/pkg/api/org_users_test.go +++ b/pkg/api/org_users_test.go @@ -276,7 +276,7 @@ var ( // setupOrgUsersDBForAccessControlTests creates three users placed in two orgs // Org1: testServerAdminViewer, testEditorOrg1 // Org2: testServerAdminViewer, testAdminOrg2 -func setupOrgUsersDBForAccessControlTests(t *testing.T, db sqlstore.SQLStore) { +func setupOrgUsersDBForAccessControlTests(t *testing.T, db sqlstore.Store) { t.Helper() var err error @@ -337,7 +337,7 @@ func TestGetOrgUsersAPIEndpoint_AccessControlMetadata(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { sc := setupHTTPServer(t, false, tc.enableAccessControl) - setupOrgUsersDBForAccessControlTests(t, *sc.db) + setupOrgUsersDBForAccessControlTests(t, sc.db) setInitCtxSignedInUser(sc.initCtx, tc.user) // Perform test @@ -434,7 +434,7 @@ func TestGetOrgUsersAPIEndpoint_AccessControl(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { sc := setupHTTPServer(t, false, tc.enableAccessControl) - setupOrgUsersDBForAccessControlTests(t, *sc.db) + setupOrgUsersDBForAccessControlTests(t, sc.db) setInitCtxSignedInUser(sc.initCtx, tc.user) // Perform test @@ -544,7 +544,7 @@ func TestPostOrgUsersAPIEndpoint_AccessControl(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { sc := setupHTTPServer(t, false, tc.enableAccessControl) - setupOrgUsersDBForAccessControlTests(t, *sc.db) + setupOrgUsersDBForAccessControlTests(t, sc.db) setInitCtxSignedInUser(sc.initCtx, tc.user) // Perform request @@ -672,7 +672,7 @@ func TestPatchOrgUsersAPIEndpoint_AccessControl(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { sc := setupHTTPServer(t, false, tc.enableAccessControl) - setupOrgUsersDBForAccessControlTests(t, *sc.db) + setupOrgUsersDBForAccessControlTests(t, sc.db) setInitCtxSignedInUser(sc.initCtx, tc.user) // Perform request @@ -792,7 +792,7 @@ func TestDeleteOrgUsersAPIEndpoint_AccessControl(t *testing.T) { for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { sc := setupHTTPServer(t, false, tc.enableAccessControl) - setupOrgUsersDBForAccessControlTests(t, *sc.db) + setupOrgUsersDBForAccessControlTests(t, sc.db) setInitCtxSignedInUser(sc.initCtx, tc.user) response := callAPI(sc.server, http.MethodDelete, fmt.Sprintf(url, tc.targetOrg, tc.targetUserId), nil, t) diff --git a/pkg/api/quota_test.go b/pkg/api/quota_test.go index 3deaab02bf7..46b332d41c8 100644 --- a/pkg/api/quota_test.go +++ b/pkg/api/quota_test.go @@ -38,7 +38,7 @@ func setupDBAndSettingsForAccessControlQuotaTests(t *testing.T, sc accessControl setting.Quota = sc.hs.Cfg.Quota // Create two orgs with the context user - setupOrgsDBForAccessControlTests(t, *sc.db, *sc.initCtx.SignedInUser, 2) + setupOrgsDBForAccessControlTests(t, sc.db, *sc.initCtx.SignedInUser, 2) } func TestAPIEndpoint_GetCurrentOrgQuotas_LegacyAccessControl(t *testing.T) { diff --git a/pkg/api/team_members_test.go b/pkg/api/team_members_test.go index 0453400a521..a5089aa71fc 100644 --- a/pkg/api/team_members_test.go +++ b/pkg/api/team_members_test.go @@ -102,7 +102,7 @@ func TestTeamMembersAPIEndpoint_userLoggedIn(t *testing.T) { }) } -func createUser(db *sqlstore.SQLStore, orgId int64, t *testing.T) int64 { +func createUser(db sqlstore.Store, orgId int64, t *testing.T) int64 { user, err := db.CreateUser(context.Background(), models.CreateUserCommand{ Login: fmt.Sprintf("TestUser%d", rand.Int()), OrgId: orgId, @@ -113,7 +113,7 @@ func createUser(db *sqlstore.SQLStore, orgId int64, t *testing.T) int64 { return user.Id } -func setupTeamTestScenario(userCount int, db *sqlstore.SQLStore, t *testing.T) int64 { +func setupTeamTestScenario(userCount int, db sqlstore.Store, t *testing.T) int64 { user, err := db.CreateUser(context.Background(), models.CreateUserCommand{SkipOrgSetup: true, Login: testUserLogin}) require.NoError(t, err) testOrg, err := db.CreateOrgWithMember("TestOrg", user.Id) diff --git a/pkg/components/simplejson/simplejson.go b/pkg/components/simplejson/simplejson.go index e76653ebdbc..8d763350a21 100644 --- a/pkg/components/simplejson/simplejson.go +++ b/pkg/components/simplejson/simplejson.go @@ -181,6 +181,24 @@ func (j *Json) GetIndex(index int) *Json { return &Json{nil} } +// CheckGetIndex returns a pointer to a new `Json` object +// for `index` in its `array` representation, and a `bool` +// indicating success or failure +// +// useful for chained operations when success is important: +// if data, ok := js.Get("top_level").CheckGetIndex(0); ok { +// log.Println(data) +// } +func (j *Json) CheckGetIndex(index int) (*Json, bool) { + a, err := j.Array() + if err == nil { + if len(a) > index { + return &Json{a[index]}, true + } + } + return nil, false +} + // SetIndex modifies `Json` array by `index` and `value` // for `index` in its `array` representation func (j *Json) SetIndex(index int, val interface{}) { diff --git a/pkg/components/simplejson/simplejson_test.go b/pkg/components/simplejson/simplejson_test.go index 477f1a42854..0c01e8a41b9 100644 --- a/pkg/components/simplejson/simplejson_test.go +++ b/pkg/components/simplejson/simplejson_test.go @@ -45,6 +45,23 @@ func TestSimplejson(t *testing.T) { awsval, _ = aws.GetIndex(1).Get("subkeythree").Int() assert.Equal(t, 3, awsval) + arr := js.Get("test").Get("array") + assert.NotEqual(t, nil, arr) + val, ok := arr.CheckGetIndex(0) + assert.Equal(t, ok, true) + valInt, _ := val.Int() + assert.Equal(t, valInt, 1) + val, ok = arr.CheckGetIndex(1) + assert.Equal(t, ok, true) + valStr, _ := val.String() + assert.Equal(t, valStr, "2") + val, ok = arr.CheckGetIndex(2) + assert.Equal(t, ok, true) + valInt, _ = val.Int() + assert.Equal(t, valInt, 3) + _, ok = arr.CheckGetIndex(3) + assert.Equal(t, ok, false) + i, _ := js.Get("test").Get("int").Int() assert.Equal(t, 10, i) diff --git a/pkg/models/dashboards.go b/pkg/models/dashboards.go index ed1d5606a8e..d345e0833c0 100644 --- a/pkg/models/dashboards.go +++ b/pkg/models/dashboards.go @@ -21,6 +21,16 @@ var ( StatusCode: 404, Status: "not-found", } + ErrDashboardCorrupt = DashboardErr{ + Reason: "Dashboard data is missing or corrupt", + StatusCode: 500, + Status: "not-found", + } + ErrDashboardPanelNotFound = DashboardErr{ + Reason: "Dashboard panel not found", + StatusCode: 404, + Status: "not-found", + } ErrDashboardFolderNotFound = DashboardErr{ Reason: "Folder not found", StatusCode: 404, @@ -105,6 +115,18 @@ var ( Reason: "Unique identifier needed to be able to get a dashboard", StatusCode: 400, } + ErrDashboardIdentifierInvalid = DashboardErr{ + Reason: "Dashboard ID not a number", + StatusCode: 400, + } + ErrDashboardPanelIdentifierInvalid = DashboardErr{ + Reason: "Dashboard panel ID not a number", + StatusCode: 400, + } + ErrDashboardOrPanelIdentifierNotSet = DashboardErr{ + Reason: "Unique identifier needed to be able to get a dashboard panel", + StatusCode: 400, + } ErrProvisionedDashboardNotFound = DashboardErr{ Reason: "Dashboard is not provisioned", StatusCode: 404, diff --git a/pkg/services/teamguardian/database/database.go b/pkg/services/teamguardian/database/database.go index cdcbedf30ae..c2913ce4127 100644 --- a/pkg/services/teamguardian/database/database.go +++ b/pkg/services/teamguardian/database/database.go @@ -8,10 +8,10 @@ import ( ) type TeamGuardianStoreImpl struct { - sqlStore *sqlstore.SQLStore + sqlStore sqlstore.Store } -func ProvideTeamGuardianStore(sqlStore *sqlstore.SQLStore) *TeamGuardianStoreImpl { +func ProvideTeamGuardianStore(sqlStore sqlstore.Store) *TeamGuardianStoreImpl { return &TeamGuardianStoreImpl{sqlStore: sqlStore} }