The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/pkg/api/datasources_test.go

565 lines
18 KiB

package api
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/infra/db/dbtest"
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
"github.com/grafana/grafana/pkg/services/accesscontrol/actest"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/datasources/guardian"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/web"
"github.com/grafana/grafana/pkg/web/webtest"
)
const (
testOrgID int64 = 1
testUserID int64 = 1
testUserLogin string = "testUser"
)
func TestDataSourcesProxy_userLoggedIn(t *testing.T) {
mockSQLStore := dbtest.NewFakeDB()
loggedInUserScenario(t, "When calling GET on", "/api/datasources/", "/api/datasources/", func(sc *scenarioContext) {
// Stubs the database query
ds := []*datasources.DataSource{
{Name: "mmm"},
{Name: "ZZZ"},
{Name: "BBB"},
{Name: "aaa"},
}
// handler func being tested
hs := &HTTPServer{
Cfg: setting.NewCfg(),
pluginStore: &pluginstore.FakePluginStore{},
DataSourcesService: &dataSourcesServiceMock{
expectedDatasources: ds,
},
dsGuardian: guardian.ProvideGuardian(),
}
sc.handlerFunc = hs.GetDataSources
sc.fakeReq("GET", "/api/datasources").exec()
respJSON := []map[string]any{}
err := json.NewDecoder(sc.resp.Body).Decode(&respJSON)
require.NoError(t, err)
assert.Equal(t, "aaa", respJSON[0]["name"])
assert.Equal(t, "BBB", respJSON[1]["name"])
assert.Equal(t, "mmm", respJSON[2]["name"])
assert.Equal(t, "ZZZ", respJSON[3]["name"])
}, mockSQLStore)
loggedInUserScenario(t, "Should be able to save a data source when calling DELETE on non-existing",
"/api/datasources/name/12345", "/api/datasources/name/:name", func(sc *scenarioContext) {
// handler func being tested
hs := &HTTPServer{
Cfg: setting.NewCfg(),
pluginStore: &pluginstore.FakePluginStore{},
}
sc.handlerFunc = hs.DeleteDataSourceByName
sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec()
assert.Equal(t, 404, sc.resp.Code)
}, mockSQLStore)
}
// Adding data sources with invalid URLs should lead to an error.
func TestAddDataSource_InvalidURL(t *testing.T) {
sc := setupScenarioContext(t, "/api/datasources")
hs := &HTTPServer{
DataSourcesService: &dataSourcesServiceMock{},
Cfg: setting.NewCfg(),
}
sc.m.Post(sc.url, routing.Wrap(func(c *contextmodel.ReqContext) response.Response {
c.Req.Body = mockRequestBody(datasources.AddDataSourceCommand{
Name: "Test",
URL: "invalid:url",
Access: "direct",
Type: "test",
})
c.SignedInUser = authedUserWithPermissions(1, 1, []ac.Permission{})
return hs.AddDataSource(c)
}))
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
assert.Equal(t, 400, sc.resp.Code)
}
// Adding data sources with URLs not specifying protocol should work.
func TestAddDataSource_URLWithoutProtocol(t *testing.T) {
const name = "Test"
const url = "localhost:5432"
hs := &HTTPServer{
DataSourcesService: &dataSourcesServiceMock{
expectedDatasource: &datasources.DataSource{},
},
Cfg: setting.NewCfg(),
AccessControl: acimpl.ProvideAccessControl(setting.NewCfg()),
accesscontrolService: actest.FakeService{},
}
sc := setupScenarioContext(t, "/api/datasources")
sc.m.Post(sc.url, routing.Wrap(func(c *contextmodel.ReqContext) response.Response {
c.Req.Body = mockRequestBody(datasources.AddDataSourceCommand{
Name: name,
URL: url,
Access: "direct",
Type: "test",
})
c.SignedInUser = authedUserWithPermissions(1, 1, []ac.Permission{})
return hs.AddDataSource(c)
}))
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
assert.Equal(t, 200, sc.resp.Code)
}
// Using a custom header whose name matches the name specified for auth proxy header should fail
func TestAddDataSource_InvalidJSONData(t *testing.T) {
hs := &HTTPServer{
DataSourcesService: &dataSourcesServiceMock{},
Cfg: setting.NewCfg(),
}
sc := setupScenarioContext(t, "/api/datasources")
hs.Cfg = setting.NewCfg()
hs.Cfg.AuthProxy.Enabled = true
hs.Cfg.AuthProxy.HeaderName = "X-AUTH-PROXY-HEADER"
jsonData := simplejson.New()
jsonData.Set("httpHeaderName1", hs.Cfg.AuthProxy.HeaderName)
sc.m.Post(sc.url, routing.Wrap(func(c *contextmodel.ReqContext) response.Response {
c.Req.Body = mockRequestBody(datasources.AddDataSourceCommand{
Name: "Test",
URL: "localhost:5432",
Access: "direct",
Type: "test",
JsonData: jsonData,
})
c.SignedInUser = authedUserWithPermissions(1, 1, []ac.Permission{})
return hs.AddDataSource(c)
}))
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
assert.Equal(t, 400, sc.resp.Code)
}
// Updating data sources with invalid URLs should lead to an error.
func TestUpdateDataSource_InvalidURL(t *testing.T) {
hs := &HTTPServer{
DataSourcesService: &dataSourcesServiceMock{},
Cfg: setting.NewCfg(),
}
sc := setupScenarioContext(t, "/api/datasources/1234")
sc.m.Put(sc.url, routing.Wrap(func(c *contextmodel.ReqContext) response.Response {
c.Req.Body = mockRequestBody(datasources.AddDataSourceCommand{
Name: "Test",
URL: "invalid:url",
Access: "direct",
Type: "test",
})
c.SignedInUser = authedUserWithPermissions(1, 1, []ac.Permission{})
return hs.AddDataSource(c)
}))
sc.fakeReqWithParams("PUT", sc.url, map[string]string{}).exec()
assert.Equal(t, 400, sc.resp.Code)
}
// Using a custom header whose name matches the name specified for auth proxy header should fail
func TestUpdateDataSource_InvalidJSONData(t *testing.T) {
hs := &HTTPServer{
DataSourcesService: &dataSourcesServiceMock{},
Cfg: setting.NewCfg(),
}
sc := setupScenarioContext(t, "/api/datasources/1234")
hs.Cfg.AuthProxy.Enabled = true
hs.Cfg.AuthProxy.HeaderName = "X-AUTH-PROXY-HEADER"
jsonData := simplejson.New()
jsonData.Set("httpHeaderName1", hs.Cfg.AuthProxy.HeaderName)
sc.m.Put(sc.url, routing.Wrap(func(c *contextmodel.ReqContext) response.Response {
c.Req.Body = mockRequestBody(datasources.AddDataSourceCommand{
Name: "Test",
URL: "localhost:5432",
Access: "direct",
Type: "test",
JsonData: jsonData,
})
c.SignedInUser = authedUserWithPermissions(1, 1, []ac.Permission{})
return hs.AddDataSource(c)
}))
sc.fakeReqWithParams("PUT", sc.url, map[string]string{}).exec()
assert.Equal(t, 400, sc.resp.Code)
}
// Using a team HTTP header whose name matches the name specified for auth proxy header should fail
func TestUpdateDataSourceTeamHTTPHeaders_InvalidJSONData(t *testing.T) {
tenantID := "1234"
testcases := []struct {
desc string
data datasources.TeamHTTPHeaders
want int
}{
{
desc: "We should only allow for headers being X-Prom-Label-Policy",
data: datasources.TeamHTTPHeaders{
Headers: datasources.TeamHeaders{
tenantID: []datasources.TeamHTTPHeader{
{
Header: "Authorization",
Value: "foo!=bar",
},
},
}},
want: 400,
},
{
desc: "Allowed header but no team id",
data: datasources.TeamHTTPHeaders{
Headers: datasources.TeamHeaders{"": []datasources.TeamHTTPHeader{
{
Header: "X-Prom-Label-Policy",
Value: "foo=bar",
},
},
}},
want: 400,
},
{
desc: "Allowed team id and header name with invalid header values ",
data: datasources.TeamHTTPHeaders{
Headers: datasources.TeamHeaders{tenantID: []datasources.TeamHTTPHeader{
{
Header: "X-Prom-Label-Policy",
Value: "Bad value",
},
},
}},
want: 400,
},
// Complete valid case, with team id, header name and header value
{
desc: "Allowed header and header values ",
data: datasources.TeamHTTPHeaders{
Headers: datasources.TeamHeaders{tenantID: []datasources.TeamHTTPHeader{
{
Header: "X-Prom-Label-Policy",
Value: `1234:{ name!="value",foo!~"bar" }`,
},
},
}},
want: 200,
},
}
for _, tc := range testcases {
t.Run(tc.desc, func(t *testing.T) {
hs := &HTTPServer{
DataSourcesService: &dataSourcesServiceMock{
expectedDatasource: &datasources.DataSource{},
},
Cfg: setting.NewCfg(),
Features: featuremgmt.WithFeatures(featuremgmt.FlagTeamHttpHeaders),
accesscontrolService: actest.FakeService{},
AccessControl: actest.FakeAccessControl{
ExpectedEvaluate: true,
ExpectedErr: nil,
},
}
sc := setupScenarioContext(t, fmt.Sprintf("/api/datasources/%s", tenantID))
hs.Cfg.AuthProxy.Enabled = true
jsonData := simplejson.New()
jsonData.Set("teamHttpHeaders", tc.data)
sc.m.Put(sc.url, routing.Wrap(func(c *contextmodel.ReqContext) response.Response {
c.Req.Body = mockRequestBody(datasources.AddDataSourceCommand{
Name: "Test",
URL: "localhost:5432",
Access: "direct",
Type: "test",
JsonData: jsonData,
})
c.SignedInUser = authedUserWithPermissions(1, 1, []ac.Permission{
{Action: datasources.ActionPermissionsWrite, Scope: datasources.ScopeAll},
})
return hs.AddDataSource(c)
}))
sc.fakeReqWithParams("PUT", sc.url, map[string]string{}).exec()
assert.Equal(t, tc.want, sc.resp.Code)
})
}
}
// Updating data sources with URLs not specifying protocol should work.
func TestUpdateDataSource_URLWithoutProtocol(t *testing.T) {
const name = "Test"
const url = "localhost:5432"
hs := &HTTPServer{
DataSourcesService: &dataSourcesServiceMock{
expectedDatasource: &datasources.DataSource{},
},
Cfg: setting.NewCfg(),
AccessControl: acimpl.ProvideAccessControl(setting.NewCfg()),
accesscontrolService: actest.FakeService{},
}
sc := setupScenarioContext(t, "/api/datasources/1234")
sc.m.Put(sc.url, routing.Wrap(func(c *contextmodel.ReqContext) response.Response {
c.Req.Body = mockRequestBody(datasources.AddDataSourceCommand{
Name: name,
URL: url,
Access: "direct",
Type: "test",
})
c.SignedInUser = authedUserWithPermissions(1, 1, []ac.Permission{})
return hs.AddDataSource(c)
}))
sc.fakeReqWithParams("PUT", sc.url, map[string]string{}).exec()
assert.Equal(t, 200, sc.resp.Code)
}
// Updating data source name where data source with same name exists.
func TestUpdateDataSourceByID_DataSourceNameExists(t *testing.T) {
hs := &HTTPServer{
DataSourcesService: &dataSourcesServiceMock{
expectedDatasource: &datasources.DataSource{},
mockUpdateDataSource: func(ctx context.Context, cmd *datasources.UpdateDataSourceCommand) (*datasources.DataSource, error) {
return nil, datasources.ErrDataSourceNameExists
},
},
Cfg: setting.NewCfg(),
AccessControl: acimpl.ProvideAccessControl(setting.NewCfg()),
accesscontrolService: actest.FakeService{},
Live: newTestLive(t, nil),
}
sc := setupScenarioContext(t, "/api/datasources/1")
sc.m.Put(sc.url, routing.Wrap(func(c *contextmodel.ReqContext) response.Response {
c.Req = web.SetURLParams(c.Req, map[string]string{":id": "1"})
c.Req.Body = mockRequestBody(datasources.UpdateDataSourceCommand{
Access: "direct",
Type: "test",
Name: "test",
})
return hs.UpdateDataSourceByID(c)
}))
sc.fakeReqWithParams("PUT", sc.url, map[string]string{}).exec()
require.Equal(t, http.StatusConflict, sc.resp.Code)
}
func TestAPI_datasources_AccessControl(t *testing.T) {
type testCase struct {
desc string
urls []string
method string
body string
permission []ac.Permission
expectedCode int
}
tests := []testCase{
{
desc: "should be able to update datasource with correct permission",
urls: []string{"api/datasources/1", "/api/datasources/uid/1"},
method: http.MethodPut,
body: `{"name": "test", "url": "http://localhost:5432", "type": "postgresql", "access": "Proxy"}`,
permission: []ac.Permission{
{Action: datasources.ActionWrite, Scope: datasources.ScopeProvider.GetResourceScope("1")},
{Action: datasources.ActionWrite, Scope: datasources.ScopeProvider.GetResourceScopeUID("1")},
},
expectedCode: http.StatusOK,
},
{
desc: "should not be able to update datasource without correct permission",
urls: []string{"api/datasources/1", "/api/datasources/uid/1"},
method: http.MethodPut,
permission: []ac.Permission{},
expectedCode: http.StatusForbidden,
},
{
desc: "should be able to fetch datasource with correct permission",
urls: []string{"api/datasources/1", "/api/datasources/uid/1", "/api/datasources/name/test"},
method: http.MethodGet,
permission: []ac.Permission{
{Action: datasources.ActionRead, Scope: datasources.ScopeProvider.GetResourceScope("1")},
{Action: datasources.ActionRead, Scope: datasources.ScopeProvider.GetResourceScopeUID("1")},
{Action: datasources.ActionRead, Scope: datasources.ScopeProvider.GetResourceScopeName("test")},
},
expectedCode: http.StatusOK,
},
{
desc: "should not be able to fetch datasource without correct permission",
urls: []string{"api/datasources/1", "/api/datasources/uid/1"},
method: http.MethodGet,
permission: []ac.Permission{},
expectedCode: http.StatusForbidden,
},
{
desc: "should be able to create datasource with correct permission",
urls: []string{"/api/datasources"},
method: http.MethodPost,
body: `{"name": "test", "url": "http://localhost:5432", "type": "postgresql", "access": "Proxy"}`,
permission: []ac.Permission{{Action: datasources.ActionCreate}},
expectedCode: http.StatusOK,
},
{
desc: "should not be able to create datasource without correct permission",
urls: []string{"/api/datasources"},
method: http.MethodPost,
permission: []ac.Permission{},
expectedCode: http.StatusForbidden,
},
{
desc: "should be able to delete datasource with correct permission",
urls: []string{"/api/datasources/1", "/api/datasources/uid/1"},
method: http.MethodDelete,
permission: []ac.Permission{
{Action: datasources.ActionDelete, Scope: datasources.ScopeProvider.GetResourceScope("1")},
{Action: datasources.ActionDelete, Scope: datasources.ScopeProvider.GetResourceScopeUID("1")},
},
expectedCode: http.StatusOK,
},
{
desc: "should not be able to delete datasource without correct permission",
urls: []string{"/api/datasources/1", "/api/datasources/uid/1"},
method: http.MethodDelete,
permission: []ac.Permission{},
expectedCode: http.StatusForbidden,
},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
server := SetupAPITestServer(t, func(hs *HTTPServer) {
hs.Cfg = setting.NewCfg()
hs.DataSourcesService = &dataSourcesServiceMock{expectedDatasource: &datasources.DataSource{}}
hs.accesscontrolService = actest.FakeService{}
hs.Live = newTestLive(t, hs.SQLStore)
})
for _, url := range tt.urls {
var body io.Reader
if tt.body != "" {
body = strings.NewReader(tt.body)
}
res, err := server.SendJSON(webtest.RequestWithSignedInUser(server.NewRequest(tt.method, url, body), authedUserWithPermissions(1, 1, tt.permission)))
require.NoError(t, err)
assert.Equal(t, tt.expectedCode, res.StatusCode)
require.NoError(t, res.Body.Close())
}
})
}
}
func TestValidateLBACHeader(t *testing.T) {
testcases := []struct {
desc string
teamHeaderValue string
want bool
}{
{
desc: "Should allow valid header",
teamHeaderValue: `1234:{ name!="value",foo!~"bar" }`,
want: true,
},
{
desc: "Should allow valid selector",
teamHeaderValue: `1234:{ name!="value",foo!~"bar/baz.foo" }`,
want: true,
},
{
desc: "Should return false for incorrect header value",
teamHeaderValue: `1234:!="value",foo!~"bar" }`,
want: false,
},
}
for _, tc := range testcases {
t.Run(tc.desc, func(t *testing.T) {
assert.Equal(t, tc.want, validateLBACHeader(tc.teamHeaderValue))
})
}
}
type dataSourcesServiceMock struct {
datasources.DataSourceService
expectedDatasources []*datasources.DataSource
expectedDatasource *datasources.DataSource
expectedError error
mockUpdateDataSource func(ctx context.Context, cmd *datasources.UpdateDataSourceCommand) (*datasources.DataSource, error)
}
func (m *dataSourcesServiceMock) GetDataSource(ctx context.Context, query *datasources.GetDataSourceQuery) (*datasources.DataSource, error) {
return m.expectedDatasource, m.expectedError
}
func (m *dataSourcesServiceMock) GetDataSources(ctx context.Context, query *datasources.GetDataSourcesQuery) ([]*datasources.DataSource, error) {
return m.expectedDatasources, m.expectedError
}
func (m *dataSourcesServiceMock) GetDataSourcesByType(ctx context.Context, query *datasources.GetDataSourcesByTypeQuery) ([]*datasources.DataSource, error) {
return m.expectedDatasources, m.expectedError
}
func (m *dataSourcesServiceMock) DeleteDataSource(ctx context.Context, cmd *datasources.DeleteDataSourceCommand) error {
return m.expectedError
}
func (m *dataSourcesServiceMock) AddDataSource(ctx context.Context, cmd *datasources.AddDataSourceCommand) (*datasources.DataSource, error) {
return m.expectedDatasource, m.expectedError
}
func (m *dataSourcesServiceMock) UpdateDataSource(ctx context.Context, cmd *datasources.UpdateDataSourceCommand) (*datasources.DataSource, error) {
if m.mockUpdateDataSource != nil {
return m.mockUpdateDataSource(ctx, cmd)
}
return m.expectedDatasource, m.expectedError
}
func (m *dataSourcesServiceMock) DecryptedValues(ctx context.Context, ds *datasources.DataSource) (map[string]string, error) {
decryptedValues := make(map[string]string)
return decryptedValues, m.expectedError
}