mirror of https://github.com/grafana/grafana
Alerting: update rule test endpoints to respect data source permissions (#47169)
* make eval.Evaluator an interface * inject Evaluator to TestingApiSrv * move conditionEval to RouteTestGrafanaRuleConfig because it is the only place where it is used * update rule test api to check data source permissionspull/47232/head
parent
51114527dc
commit
e94d0c1b96
@ -0,0 +1,31 @@ |
||||
package datasources |
||||
|
||||
import ( |
||||
"context" |
||||
|
||||
"github.com/grafana/grafana/pkg/models" |
||||
) |
||||
|
||||
type FakeCacheService struct { |
||||
DataSources []*models.DataSource |
||||
} |
||||
|
||||
var _ CacheService = &FakeCacheService{} |
||||
|
||||
func (c *FakeCacheService) GetDatasource(ctx context.Context, datasourceID int64, user *models.SignedInUser, skipCache bool) (*models.DataSource, error) { |
||||
for _, datasource := range c.DataSources { |
||||
if datasource.Id == datasourceID { |
||||
return datasource, nil |
||||
} |
||||
} |
||||
return nil, models.ErrDataSourceNotFound |
||||
} |
||||
|
||||
func (c *FakeCacheService) GetDatasourceByUID(ctx context.Context, datasourceUID string, user *models.SignedInUser, skipCache bool) (*models.DataSource, error) { |
||||
for _, datasource := range c.DataSources { |
||||
if datasource.Uid == datasourceUID { |
||||
return datasource, nil |
||||
} |
||||
} |
||||
return nil, models.ErrDataSourceNotFound |
||||
} |
@ -0,0 +1,278 @@ |
||||
package api |
||||
|
||||
import ( |
||||
"net/http" |
||||
"testing" |
||||
"time" |
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend" |
||||
"github.com/stretchr/testify/mock" |
||||
"github.com/stretchr/testify/require" |
||||
|
||||
models2 "github.com/grafana/grafana/pkg/models" |
||||
"github.com/grafana/grafana/pkg/services/accesscontrol" |
||||
acMock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" |
||||
"github.com/grafana/grafana/pkg/services/datasources" |
||||
"github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" |
||||
"github.com/grafana/grafana/pkg/services/ngalert/eval" |
||||
"github.com/grafana/grafana/pkg/services/ngalert/models" |
||||
"github.com/grafana/grafana/pkg/web" |
||||
) |
||||
|
||||
func TestRouteTestGrafanaRuleConfig(t *testing.T) { |
||||
t.Run("when fine-grained access is enabled", func(t *testing.T) { |
||||
rc := &models2.ReqContext{ |
||||
Context: &web.Context{ |
||||
Req: &http.Request{}, |
||||
}, |
||||
SignedInUser: &models2.SignedInUser{ |
||||
OrgId: 1, |
||||
}, |
||||
} |
||||
|
||||
t.Run("should return 401 if user cannot query a data source", func(t *testing.T) { |
||||
data1 := models.GenerateAlertQuery() |
||||
data2 := models.GenerateAlertQuery() |
||||
|
||||
ac := acMock.New().WithPermissions([]*accesscontrol.Permission{ |
||||
{Action: datasources.ActionQuery, Scope: datasources.ScopeProvider.GetResourceScopeUID(data1.DatasourceUID)}, |
||||
}) |
||||
|
||||
srv := createTestingApiSrv(nil, ac, nil) |
||||
|
||||
response := srv.RouteTestGrafanaRuleConfig(rc, definitions.TestRulePayload{ |
||||
Expr: "", |
||||
GrafanaManagedCondition: &models.EvalAlertConditionCommand{ |
||||
Condition: data1.RefID, |
||||
Data: []models.AlertQuery{data1, data2}, |
||||
Now: time.Time{}, |
||||
}, |
||||
}) |
||||
|
||||
require.Equal(t, http.StatusUnauthorized, response.Status()) |
||||
}) |
||||
|
||||
t.Run("should return 200 if user can query all data sources", func(t *testing.T) { |
||||
data1 := models.GenerateAlertQuery() |
||||
data2 := models.GenerateAlertQuery() |
||||
|
||||
ac := acMock.New().WithPermissions([]*accesscontrol.Permission{ |
||||
{Action: datasources.ActionQuery, Scope: datasources.ScopeProvider.GetResourceScopeUID(data1.DatasourceUID)}, |
||||
{Action: datasources.ActionQuery, Scope: datasources.ScopeProvider.GetResourceScopeUID(data2.DatasourceUID)}, |
||||
}) |
||||
|
||||
ds := &datasources.FakeCacheService{DataSources: []*models2.DataSource{ |
||||
{Uid: data1.DatasourceUID}, |
||||
{Uid: data2.DatasourceUID}, |
||||
}} |
||||
|
||||
evaluator := &eval.FakeEvaluator{} |
||||
var result []eval.Result |
||||
evaluator.EXPECT().ConditionEval(mock.Anything, mock.Anything, mock.Anything).Return(result, nil) |
||||
|
||||
srv := createTestingApiSrv(ds, ac, evaluator) |
||||
|
||||
response := srv.RouteTestGrafanaRuleConfig(rc, definitions.TestRulePayload{ |
||||
Expr: "", |
||||
GrafanaManagedCondition: &models.EvalAlertConditionCommand{ |
||||
Condition: data1.RefID, |
||||
Data: []models.AlertQuery{data1, data2}, |
||||
Now: time.Time{}, |
||||
}, |
||||
}) |
||||
|
||||
require.Equal(t, http.StatusOK, response.Status()) |
||||
|
||||
evaluator.AssertCalled(t, "ConditionEval", mock.Anything, mock.Anything, mock.Anything) |
||||
}) |
||||
}) |
||||
|
||||
t.Run("when fine-grained access is disabled", func(t *testing.T) { |
||||
rc := &models2.ReqContext{ |
||||
Context: &web.Context{ |
||||
Req: &http.Request{}, |
||||
}, |
||||
IsSignedIn: false, |
||||
SignedInUser: &models2.SignedInUser{ |
||||
OrgId: 1, |
||||
}, |
||||
} |
||||
ac := acMock.New().WithDisabled() |
||||
|
||||
t.Run("should require user to be signed in", func(t *testing.T) { |
||||
data1 := models.GenerateAlertQuery() |
||||
|
||||
ds := &datasources.FakeCacheService{DataSources: []*models2.DataSource{ |
||||
{Uid: data1.DatasourceUID}, |
||||
}} |
||||
|
||||
evaluator := &eval.FakeEvaluator{} |
||||
var result []eval.Result |
||||
evaluator.EXPECT().ConditionEval(mock.Anything, mock.Anything, mock.Anything).Return(result, nil) |
||||
|
||||
srv := createTestingApiSrv(ds, ac, evaluator) |
||||
|
||||
response := srv.RouteTestGrafanaRuleConfig(rc, definitions.TestRulePayload{ |
||||
Expr: "", |
||||
GrafanaManagedCondition: &models.EvalAlertConditionCommand{ |
||||
Condition: data1.RefID, |
||||
Data: []models.AlertQuery{data1}, |
||||
Now: time.Time{}, |
||||
}, |
||||
}) |
||||
|
||||
require.Equal(t, http.StatusUnauthorized, response.Status()) |
||||
evaluator.AssertNotCalled(t, "ConditionEval", mock.Anything, mock.Anything, mock.Anything) |
||||
|
||||
rc.IsSignedIn = true |
||||
|
||||
response = srv.RouteTestGrafanaRuleConfig(rc, definitions.TestRulePayload{ |
||||
Expr: "", |
||||
GrafanaManagedCondition: &models.EvalAlertConditionCommand{ |
||||
Condition: data1.RefID, |
||||
Data: []models.AlertQuery{data1}, |
||||
Now: time.Time{}, |
||||
}, |
||||
}) |
||||
|
||||
require.Equal(t, http.StatusOK, response.Status()) |
||||
|
||||
evaluator.AssertCalled(t, "ConditionEval", mock.Anything, mock.Anything, mock.Anything) |
||||
}) |
||||
}) |
||||
} |
||||
|
||||
func TestRouteEvalQueries(t *testing.T) { |
||||
t.Run("when fine-grained access is enabled", func(t *testing.T) { |
||||
rc := &models2.ReqContext{ |
||||
Context: &web.Context{ |
||||
Req: &http.Request{}, |
||||
}, |
||||
SignedInUser: &models2.SignedInUser{ |
||||
OrgId: 1, |
||||
}, |
||||
} |
||||
|
||||
t.Run("should return 401 if user cannot query a data source", func(t *testing.T) { |
||||
data1 := models.GenerateAlertQuery() |
||||
data2 := models.GenerateAlertQuery() |
||||
|
||||
ac := acMock.New().WithPermissions([]*accesscontrol.Permission{ |
||||
{Action: datasources.ActionQuery, Scope: datasources.ScopeProvider.GetResourceScopeUID(data1.DatasourceUID)}, |
||||
}) |
||||
|
||||
srv := &TestingApiSrv{ |
||||
accessControl: ac, |
||||
} |
||||
|
||||
response := srv.RouteEvalQueries(rc, definitions.EvalQueriesPayload{ |
||||
Data: []models.AlertQuery{data1, data2}, |
||||
Now: time.Time{}, |
||||
}) |
||||
|
||||
require.Equal(t, http.StatusUnauthorized, response.Status()) |
||||
}) |
||||
|
||||
t.Run("should return 200 if user can query all data sources", func(t *testing.T) { |
||||
data1 := models.GenerateAlertQuery() |
||||
data2 := models.GenerateAlertQuery() |
||||
|
||||
ac := acMock.New().WithPermissions([]*accesscontrol.Permission{ |
||||
{Action: datasources.ActionQuery, Scope: datasources.ScopeProvider.GetResourceScopeUID(data1.DatasourceUID)}, |
||||
{Action: datasources.ActionQuery, Scope: datasources.ScopeProvider.GetResourceScopeUID(data2.DatasourceUID)}, |
||||
}) |
||||
|
||||
ds := &datasources.FakeCacheService{DataSources: []*models2.DataSource{ |
||||
{Uid: data1.DatasourceUID}, |
||||
{Uid: data2.DatasourceUID}, |
||||
}} |
||||
|
||||
evaluator := &eval.FakeEvaluator{} |
||||
result := &backend.QueryDataResponse{ |
||||
Responses: map[string]backend.DataResponse{ |
||||
"test": { |
||||
Frames: nil, |
||||
Error: nil, |
||||
}, |
||||
}, |
||||
} |
||||
evaluator.EXPECT().QueriesAndExpressionsEval(mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(result, nil) |
||||
|
||||
srv := createTestingApiSrv(ds, ac, evaluator) |
||||
|
||||
response := srv.RouteEvalQueries(rc, definitions.EvalQueriesPayload{ |
||||
Data: []models.AlertQuery{data1, data2}, |
||||
Now: time.Time{}, |
||||
}) |
||||
|
||||
require.Equal(t, http.StatusOK, response.Status()) |
||||
|
||||
evaluator.AssertCalled(t, "QueriesAndExpressionsEval", mock.Anything, mock.Anything, mock.Anything, mock.Anything) |
||||
}) |
||||
}) |
||||
|
||||
t.Run("when fine-grained access is disabled", func(t *testing.T) { |
||||
rc := &models2.ReqContext{ |
||||
Context: &web.Context{ |
||||
Req: &http.Request{}, |
||||
}, |
||||
IsSignedIn: false, |
||||
SignedInUser: &models2.SignedInUser{ |
||||
OrgId: 1, |
||||
}, |
||||
} |
||||
ac := acMock.New().WithDisabled() |
||||
|
||||
t.Run("should require user to be signed in", func(t *testing.T) { |
||||
data1 := models.GenerateAlertQuery() |
||||
|
||||
ds := &datasources.FakeCacheService{DataSources: []*models2.DataSource{ |
||||
{Uid: data1.DatasourceUID}, |
||||
}} |
||||
|
||||
evaluator := &eval.FakeEvaluator{} |
||||
result := &backend.QueryDataResponse{ |
||||
Responses: map[string]backend.DataResponse{ |
||||
"test": { |
||||
Frames: nil, |
||||
Error: nil, |
||||
}, |
||||
}, |
||||
} |
||||
evaluator.EXPECT().QueriesAndExpressionsEval(mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(result, nil) |
||||
|
||||
srv := createTestingApiSrv(ds, ac, evaluator) |
||||
|
||||
response := srv.RouteEvalQueries(rc, definitions.EvalQueriesPayload{ |
||||
Data: []models.AlertQuery{data1}, |
||||
Now: time.Time{}, |
||||
}) |
||||
|
||||
require.Equal(t, http.StatusUnauthorized, response.Status()) |
||||
evaluator.AssertNotCalled(t, "QueriesAndExpressionsEval", mock.Anything, mock.Anything, mock.Anything, mock.Anything) |
||||
|
||||
rc.IsSignedIn = true |
||||
|
||||
response = srv.RouteEvalQueries(rc, definitions.EvalQueriesPayload{ |
||||
Data: []models.AlertQuery{data1}, |
||||
Now: time.Time{}, |
||||
}) |
||||
|
||||
require.Equal(t, http.StatusOK, response.Status()) |
||||
|
||||
evaluator.AssertCalled(t, "QueriesAndExpressionsEval", mock.Anything, mock.Anything, mock.Anything, mock.Anything) |
||||
}) |
||||
}) |
||||
} |
||||
|
||||
func createTestingApiSrv(ds *datasources.FakeCacheService, ac *acMock.Mock, evaluator *eval.FakeEvaluator) *TestingApiSrv { |
||||
if ac == nil { |
||||
ac = acMock.New().WithDisabled() |
||||
} |
||||
|
||||
return &TestingApiSrv{ |
||||
DatasourceCache: ds, |
||||
accessControl: ac, |
||||
evaluator: evaluator, |
||||
} |
||||
} |
@ -0,0 +1,124 @@ |
||||
// Code generated by mockery v2.10.0. DO NOT EDIT.
|
||||
|
||||
package eval |
||||
|
||||
import ( |
||||
backend "github.com/grafana/grafana-plugin-sdk-go/backend" |
||||
expr "github.com/grafana/grafana/pkg/expr" |
||||
|
||||
mock "github.com/stretchr/testify/mock" |
||||
|
||||
models "github.com/grafana/grafana/pkg/services/ngalert/models" |
||||
|
||||
time "time" |
||||
) |
||||
|
||||
// FakeEvaluator is an autogenerated mock type for the Evaluator type
|
||||
type FakeEvaluator struct { |
||||
mock.Mock |
||||
} |
||||
|
||||
type FakeEvaluator_Expecter struct { |
||||
mock *mock.Mock |
||||
} |
||||
|
||||
func (_m *FakeEvaluator) EXPECT() *FakeEvaluator_Expecter { |
||||
return &FakeEvaluator_Expecter{mock: &_m.Mock} |
||||
} |
||||
|
||||
// ConditionEval provides a mock function with given fields: condition, now, expressionService
|
||||
func (_m *FakeEvaluator) ConditionEval(condition *models.Condition, now time.Time, expressionService *expr.Service) (Results, error) { |
||||
ret := _m.Called(condition, now, expressionService) |
||||
|
||||
var r0 Results |
||||
if rf, ok := ret.Get(0).(func(*models.Condition, time.Time, *expr.Service) Results); ok { |
||||
r0 = rf(condition, now, expressionService) |
||||
} else { |
||||
if ret.Get(0) != nil { |
||||
r0 = ret.Get(0).(Results) |
||||
} |
||||
} |
||||
|
||||
var r1 error |
||||
if rf, ok := ret.Get(1).(func(*models.Condition, time.Time, *expr.Service) error); ok { |
||||
r1 = rf(condition, now, expressionService) |
||||
} else { |
||||
r1 = ret.Error(1) |
||||
} |
||||
|
||||
return r0, r1 |
||||
} |
||||
|
||||
// FakeEvaluator_ConditionEval_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ConditionEval'
|
||||
type FakeEvaluator_ConditionEval_Call struct { |
||||
*mock.Call |
||||
} |
||||
|
||||
// ConditionEval is a helper method to define mock.On call
|
||||
// - condition *models.Condition
|
||||
// - now time.Time
|
||||
// - expressionService *expr.Service
|
||||
func (_e *FakeEvaluator_Expecter) ConditionEval(condition interface{}, now interface{}, expressionService interface{}) *FakeEvaluator_ConditionEval_Call { |
||||
return &FakeEvaluator_ConditionEval_Call{Call: _e.mock.On("ConditionEval", condition, now, expressionService)} |
||||
} |
||||
|
||||
func (_c *FakeEvaluator_ConditionEval_Call) Run(run func(condition *models.Condition, now time.Time, expressionService *expr.Service)) *FakeEvaluator_ConditionEval_Call { |
||||
_c.Call.Run(func(args mock.Arguments) { |
||||
run(args[0].(*models.Condition), args[1].(time.Time), args[2].(*expr.Service)) |
||||
}) |
||||
return _c |
||||
} |
||||
|
||||
func (_c *FakeEvaluator_ConditionEval_Call) Return(_a0 Results, _a1 error) *FakeEvaluator_ConditionEval_Call { |
||||
_c.Call.Return(_a0, _a1) |
||||
return _c |
||||
} |
||||
|
||||
// QueriesAndExpressionsEval provides a mock function with given fields: orgID, data, now, expressionService
|
||||
func (_m *FakeEvaluator) QueriesAndExpressionsEval(orgID int64, data []models.AlertQuery, now time.Time, expressionService *expr.Service) (*backend.QueryDataResponse, error) { |
||||
ret := _m.Called(orgID, data, now, expressionService) |
||||
|
||||
var r0 *backend.QueryDataResponse |
||||
if rf, ok := ret.Get(0).(func(int64, []models.AlertQuery, time.Time, *expr.Service) *backend.QueryDataResponse); ok { |
||||
r0 = rf(orgID, data, now, expressionService) |
||||
} else { |
||||
if ret.Get(0) != nil { |
||||
r0 = ret.Get(0).(*backend.QueryDataResponse) |
||||
} |
||||
} |
||||
|
||||
var r1 error |
||||
if rf, ok := ret.Get(1).(func(int64, []models.AlertQuery, time.Time, *expr.Service) error); ok { |
||||
r1 = rf(orgID, data, now, expressionService) |
||||
} else { |
||||
r1 = ret.Error(1) |
||||
} |
||||
|
||||
return r0, r1 |
||||
} |
||||
|
||||
// FakeEvaluator_QueriesAndExpressionsEval_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'QueriesAndExpressionsEval'
|
||||
type FakeEvaluator_QueriesAndExpressionsEval_Call struct { |
||||
*mock.Call |
||||
} |
||||
|
||||
// QueriesAndExpressionsEval is a helper method to define mock.On call
|
||||
// - orgID int64
|
||||
// - data []models.AlertQuery
|
||||
// - now time.Time
|
||||
// - expressionService *expr.Service
|
||||
func (_e *FakeEvaluator_Expecter) QueriesAndExpressionsEval(orgID interface{}, data interface{}, now interface{}, expressionService interface{}) *FakeEvaluator_QueriesAndExpressionsEval_Call { |
||||
return &FakeEvaluator_QueriesAndExpressionsEval_Call{Call: _e.mock.On("QueriesAndExpressionsEval", orgID, data, now, expressionService)} |
||||
} |
||||
|
||||
func (_c *FakeEvaluator_QueriesAndExpressionsEval_Call) Run(run func(orgID int64, data []models.AlertQuery, now time.Time, expressionService *expr.Service)) *FakeEvaluator_QueriesAndExpressionsEval_Call { |
||||
_c.Call.Run(func(args mock.Arguments) { |
||||
run(args[0].(int64), args[1].([]models.AlertQuery), args[2].(time.Time), args[3].(*expr.Service)) |
||||
}) |
||||
return _c |
||||
} |
||||
|
||||
func (_c *FakeEvaluator_QueriesAndExpressionsEval_Call) Return(_a0 *backend.QueryDataResponse, _a1 error) *FakeEvaluator_QueriesAndExpressionsEval_Call { |
||||
_c.Call.Return(_a0, _a1) |
||||
return _c |
||||
} |
Loading…
Reference in new issue