mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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 <jesse.weaver@grafana.com> Co-authored-by: Leandro Deveikis <leandro.deveikis@gmail.com>
This commit is contained in:
@@ -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))
|
||||
|
@@ -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),
|
||||
}
|
||||
|
@@ -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
|
||||
|
501
pkg/api/metrics_test.go
Normal file
501
pkg/api/metrics_test.go
Normal file
@@ -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)
|
||||
})
|
||||
}
|
||||
}
|
@@ -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) {
|
||||
|
@@ -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)
|
||||
|
@@ -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) {
|
||||
|
@@ -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)
|
||||
|
@@ -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{}) {
|
||||
|
@@ -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)
|
||||
|
||||
|
@@ -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,
|
||||
|
@@ -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}
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user