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

590 lines
18 KiB

package api
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/http/httptest"
"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/models"
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/datasources/permissions"
"github.com/grafana/grafana/pkg/services/sqlstore/mockstore"
"github.com/grafana/grafana/pkg/setting"
)
const (
testOrgID int64 = 1
testUserID int64 = 1
testUserLogin string = "testUser"
)
func TestDataSourcesProxy_userLoggedIn(t *testing.T) {
mockSQLStore := mockstore.NewSQLStoreMock()
mockDatasourcePermissionService := permissions.NewMockDatasourcePermissionService()
loggedInUserScenario(t, "When calling GET on", "/api/datasources/", "/api/datasources/", func(sc *scenarioContext) {
// Stubs the database query
ds := []*models.DataSource{
{Name: "mmm"},
{Name: "ZZZ"},
{Name: "BBB"},
{Name: "aaa"},
}
mockDatasourcePermissionService.DsResult = ds
// handler func being tested
hs := &HTTPServer{
Cfg: setting.NewCfg(),
pluginStore: &fakePluginStore{},
DataSourcesService: &dataSourcesServiceMock{
expectedDatasources: ds,
},
DatasourcePermissionsService: mockDatasourcePermissionService,
}
sc.handlerFunc = hs.GetDataSources
sc.fakeReq("GET", "/api/datasources").exec()
respJSON := []map[string]interface{}{}
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: &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{},
}
sc.m.Post(sc.url, routing.Wrap(func(c *models.ReqContext) response.Response {
c.Req.Body = mockRequestBody(models.AddDataSourceCommand{
Name: "Test",
Url: "invalid:url",
Access: "direct",
Type: "test",
})
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: &models.DataSource{},
},
}
sc := setupScenarioContext(t, "/api/datasources")
sc.m.Post(sc.url, routing.Wrap(func(c *models.ReqContext) response.Response {
c.Req.Body = mockRequestBody(models.AddDataSourceCommand{
Name: name,
Url: url,
Access: "direct",
Type: "test",
})
return hs.AddDataSource(c)
}))
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
assert.Equal(t, 200, sc.resp.Code)
}
// Updating data sources with invalid URLs should lead to an error.
func TestUpdateDataSource_InvalidURL(t *testing.T) {
hs := &HTTPServer{
DataSourcesService: &dataSourcesServiceMock{},
}
sc := setupScenarioContext(t, "/api/datasources/1234")
sc.m.Put(sc.url, routing.Wrap(func(c *models.ReqContext) response.Response {
c.Req.Body = mockRequestBody(models.AddDataSourceCommand{
Name: "Test",
Url: "invalid:url",
Access: "direct",
Type: "test",
})
return hs.AddDataSource(c)
}))
sc.fakeReqWithParams("PUT", sc.url, map[string]string{}).exec()
assert.Equal(t, 400, 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: &models.DataSource{},
},
}
sc := setupScenarioContext(t, "/api/datasources/1234")
sc.m.Put(sc.url, routing.Wrap(func(c *models.ReqContext) response.Response {
c.Req.Body = mockRequestBody(models.AddDataSourceCommand{
Name: name,
Url: url,
Access: "direct",
Type: "test",
})
return hs.AddDataSource(c)
}))
sc.fakeReqWithParams("PUT", sc.url, map[string]string{}).exec()
assert.Equal(t, 200, sc.resp.Code)
}
func TestAPI_Datasources_AccessControl(t *testing.T) {
testDatasource := models.DataSource{
Id: 3,
Uid: "testUID",
OrgId: testOrgID,
Name: "test",
Url: "http://localhost:5432",
Type: "postgresql",
Access: "Proxy",
}
testDatasourceReadOnly := models.DataSource{
Id: 4,
Uid: "testUID",
OrgId: testOrgID,
Name: "test",
Url: "http://localhost:5432",
Type: "postgresql",
Access: "Proxy",
ReadOnly: true,
}
addDatasourceBody := func() io.Reader {
s, _ := json.Marshal(models.AddDataSourceCommand{
Name: "test",
Url: "http://localhost:5432",
Type: "postgresql",
Access: "Proxy",
})
return bytes.NewReader(s)
}
dsServiceMock := &dataSourcesServiceMock{
expectedDatasource: &testDatasource,
}
dsPermissionService := permissions.NewMockDatasourcePermissionService()
dsPermissionService.DsResult = []*models.DataSource{
&testDatasource,
}
updateDatasourceBody := func() io.Reader {
s, _ := json.Marshal(models.UpdateDataSourceCommand{
Name: "test",
Url: "http://localhost:5432",
Type: "postgresql",
Access: "Proxy",
})
return bytes.NewReader(s)
}
type acTestCaseWithHandler struct {
body func() io.Reader
accessControlTestCase
expectedDS *models.DataSource
expectedSQLError error
}
tests := []acTestCaseWithHandler{
{
body: updateDatasourceBody,
accessControlTestCase: accessControlTestCase{
expectedCode: http.StatusNotFound,
desc: "DatasourcesPut should return 404 if datasource not found",
url: fmt.Sprintf("/api/datasources/%v", "12345678"),
method: http.MethodPut,
permissions: []*ac.Permission{
{
Action: datasources.ActionWrite,
Scope: datasources.ScopeAll,
},
},
},
expectedSQLError: models.ErrDataSourceNotFound,
},
{
accessControlTestCase: accessControlTestCase{
expectedCode: http.StatusOK,
desc: "DatasourcesGet should return 200 for user with correct permissions",
url: "/api/datasources/",
method: http.MethodGet,
permissions: []*ac.Permission{{Action: datasources.ActionRead, Scope: datasources.ScopeAll}},
},
},
{
accessControlTestCase: accessControlTestCase{
expectedCode: http.StatusForbidden,
desc: "DatasourcesGet should return 403 for user without required permissions",
url: "/api/datasources/",
method: http.MethodGet,
permissions: []*ac.Permission{{Action: "wrong"}},
},
},
{
body: addDatasourceBody,
accessControlTestCase: accessControlTestCase{
expectedCode: http.StatusOK,
desc: "DatasourcesPost should return 200 for user with correct permissions",
url: "/api/datasources/",
method: http.MethodPost,
permissions: []*ac.Permission{{Action: datasources.ActionCreate}},
},
expectedDS: &testDatasource,
},
{
accessControlTestCase: accessControlTestCase{
expectedCode: http.StatusForbidden,
desc: "DatasourcesPost should return 403 for user without required permissions",
url: "/api/datasources/",
method: http.MethodPost,
permissions: []*ac.Permission{{Action: "wrong"}},
},
},
{
body: updateDatasourceBody,
accessControlTestCase: accessControlTestCase{
expectedCode: http.StatusOK,
desc: "DatasourcesPut should return 200 for user with correct permissions",
url: fmt.Sprintf("/api/datasources/%v", testDatasource.Id),
method: http.MethodPut,
permissions: []*ac.Permission{
{
Action: datasources.ActionWrite,
Scope: fmt.Sprintf("datasources:id:%v", testDatasource.Id),
},
},
},
expectedDS: &testDatasource,
},
{
accessControlTestCase: accessControlTestCase{
expectedCode: http.StatusForbidden,
desc: "DatasourcesPut should return 403 for user without required permissions",
url: fmt.Sprintf("/api/datasources/%v", testDatasource.Id),
method: http.MethodPut,
permissions: []*ac.Permission{{Action: "wrong"}},
},
},
{
body: updateDatasourceBody,
accessControlTestCase: accessControlTestCase{
expectedCode: http.StatusForbidden,
desc: "DatasourcesPut should return 403 for read only datasource",
url: fmt.Sprintf("/api/datasources/%v", testDatasourceReadOnly.Id),
method: http.MethodPut,
permissions: []*ac.Permission{
{
Action: datasources.ActionWrite,
Scope: fmt.Sprintf("datasources:id:%v", testDatasourceReadOnly.Id),
},
},
},
expectedDS: &testDatasourceReadOnly,
},
{
accessControlTestCase: accessControlTestCase{
expectedCode: http.StatusOK,
desc: "DatasourcesDeleteByID should return 200 for user with correct permissions",
url: fmt.Sprintf("/api/datasources/%v", testDatasource.Id),
method: http.MethodDelete,
permissions: []*ac.Permission{
{
Action: datasources.ActionDelete,
Scope: fmt.Sprintf("datasources:id:%v", testDatasource.Id),
},
},
},
expectedDS: &testDatasource,
},
{
accessControlTestCase: accessControlTestCase{
expectedCode: http.StatusForbidden,
desc: "DatasourcesDeleteByID should return 403 for user without required permissions",
url: fmt.Sprintf("/api/datasources/%v", testDatasource.Id),
method: http.MethodDelete,
permissions: []*ac.Permission{{Action: "wrong"}},
},
},
{
accessControlTestCase: accessControlTestCase{
expectedCode: http.StatusOK,
desc: "DatasourcesDeleteByUID should return 200 for user with correct permissions",
url: fmt.Sprintf("/api/datasources/uid/%v", testDatasource.Uid),
method: http.MethodDelete,
permissions: []*ac.Permission{
{
Action: datasources.ActionDelete,
Scope: fmt.Sprintf("datasources:uid:%v", testDatasource.Uid),
},
},
},
expectedDS: &testDatasource,
},
{
accessControlTestCase: accessControlTestCase{
expectedCode: http.StatusForbidden,
desc: "DatasourcesDeleteByUID should return 403 for user without required permissions",
url: fmt.Sprintf("/api/datasources/uid/%v", testDatasource.Uid),
method: http.MethodDelete,
permissions: []*ac.Permission{{Action: "wrong"}},
},
},
{
accessControlTestCase: accessControlTestCase{
expectedCode: http.StatusOK,
desc: "DatasourcesDeleteByName should return 200 for user with correct permissions",
url: fmt.Sprintf("/api/datasources/name/%v", testDatasource.Name),
method: http.MethodDelete,
permissions: []*ac.Permission{
{
Action: datasources.ActionDelete,
Scope: fmt.Sprintf("datasources:name:%v", testDatasource.Name),
},
},
},
expectedDS: &testDatasource,
},
{
accessControlTestCase: accessControlTestCase{
expectedCode: http.StatusForbidden,
desc: "DatasourcesDeleteByName should return 403 for user without required permissions",
url: fmt.Sprintf("/api/datasources/name/%v", testDatasource.Name),
method: http.MethodDelete,
permissions: []*ac.Permission{{Action: "wrong"}},
},
},
{
accessControlTestCase: accessControlTestCase{
expectedCode: http.StatusOK,
desc: "DatasourcesGetByID should return 200 for user with correct permissions",
url: fmt.Sprintf("/api/datasources/%v", testDatasource.Id),
method: http.MethodGet,
permissions: []*ac.Permission{
{
Action: datasources.ActionRead,
Scope: fmt.Sprintf("datasources:id:%v", testDatasource.Id),
},
},
},
expectedDS: &testDatasource,
},
{
accessControlTestCase: accessControlTestCase{
expectedCode: http.StatusForbidden,
desc: "DatasourcesGetByID should return 403 for user without required permissions",
url: fmt.Sprintf("/api/datasources/%v", testDatasource.Id),
method: http.MethodGet,
permissions: []*ac.Permission{{Action: "wrong"}},
},
},
{
accessControlTestCase: accessControlTestCase{
expectedCode: http.StatusOK,
desc: "DatasourcesGetByUID should return 200 for user with correct permissions",
url: fmt.Sprintf("/api/datasources/uid/%v", testDatasource.Uid),
method: http.MethodGet,
permissions: []*ac.Permission{
{
Action: datasources.ActionRead,
Scope: fmt.Sprintf("datasources:uid:%v", testDatasource.Uid),
},
},
},
expectedDS: &testDatasource,
},
{
accessControlTestCase: accessControlTestCase{
expectedCode: http.StatusForbidden,
desc: "DatasourcesGetByUID should return 403 for user without required permissions",
url: fmt.Sprintf("/api/datasources/uid/%v", testDatasource.Uid),
method: http.MethodGet,
permissions: []*ac.Permission{{Action: "wrong"}},
},
},
{
accessControlTestCase: accessControlTestCase{
expectedCode: http.StatusOK,
desc: "DatasourcesGetByName should return 200 for user with correct permissions",
url: fmt.Sprintf("/api/datasources/name/%v", testDatasource.Name),
method: http.MethodGet,
permissions: []*ac.Permission{
{
Action: datasources.ActionRead,
Scope: fmt.Sprintf("datasources:name:%v", testDatasource.Name),
},
},
},
expectedDS: &testDatasource,
},
{
accessControlTestCase: accessControlTestCase{
expectedCode: http.StatusForbidden,
desc: "DatasourcesGetByName should return 403 for user without required permissions",
url: fmt.Sprintf("/api/datasources/name/%v", testDatasource.Name),
method: http.MethodGet,
permissions: []*ac.Permission{{Action: "wrong"}},
},
expectedDS: &testDatasource,
},
{
accessControlTestCase: accessControlTestCase{
expectedCode: http.StatusOK,
desc: "DatasourcesGetIdByName should return 200 for user with correct permissions",
url: fmt.Sprintf("/api/datasources/id/%v", testDatasource.Name),
method: http.MethodGet,
permissions: []*ac.Permission{
{
Action: datasources.ActionIDRead,
Scope: fmt.Sprintf("datasources:name:%v", testDatasource.Name),
},
},
},
expectedDS: &testDatasource,
},
{
accessControlTestCase: accessControlTestCase{
expectedCode: http.StatusForbidden,
desc: "DatasourcesGetIdByName should return 403 for user without required permissions",
url: fmt.Sprintf("/api/datasources/id/%v", testDatasource.Name),
method: http.MethodGet,
permissions: []*ac.Permission{{Action: "wrong"}},
},
expectedDS: &testDatasource,
},
}
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
cfg := setting.NewCfg()
sc, hs := setupAccessControlScenarioContext(t, cfg, test.url, test.permissions)
// mock sqlStore and datasource permission service
dsServiceMock.expectedError = test.expectedSQLError
dsServiceMock.expectedDatasource = test.expectedDS
dsPermissionService.DsResult = []*models.DataSource{test.expectedDS}
if test.expectedDS == nil {
dsPermissionService.DsResult = nil
}
hs.DataSourcesService = dsServiceMock
hs.DatasourcePermissionsService = dsPermissionService
// Create a middleware to pretend user is logged in
pretendSignInMiddleware := func(c *models.ReqContext) {
sc.context = c
sc.context.UserId = testUserID
sc.context.OrgId = testOrgID
sc.context.Login = testUserLogin
sc.context.OrgRole = models.ROLE_VIEWER
sc.context.IsSignedIn = true
}
sc.m.Use(pretendSignInMiddleware)
sc.resp = httptest.NewRecorder()
hs.SettingsProvider = &setting.OSSImpl{Cfg: cfg}
var err error
if test.body != nil {
sc.req, err = http.NewRequest(test.method, test.url, test.body())
sc.req.Header.Add("Content-Type", "application/json")
} else {
sc.req, err = http.NewRequest(test.method, test.url, nil)
}
assert.NoError(t, err)
sc.exec()
assert.Equal(t, test.expectedCode, sc.resp.Code)
})
}
}
type dataSourcesServiceMock struct {
datasources.DataSourceService
expectedDatasources []*models.DataSource
expectedDatasource *models.DataSource
expectedError error
}
func (m *dataSourcesServiceMock) GetDataSource(ctx context.Context, query *models.GetDataSourceQuery) error {
query.Result = m.expectedDatasource
return m.expectedError
}
func (m *dataSourcesServiceMock) GetDataSources(ctx context.Context, query *models.GetDataSourcesQuery) error {
query.Result = m.expectedDatasources
return m.expectedError
}
func (m *dataSourcesServiceMock) GetDataSourcesByType(ctx context.Context, query *models.GetDataSourcesByTypeQuery) error {
return m.expectedError
}
func (m *dataSourcesServiceMock) GetDefaultDataSource(ctx context.Context, query *models.GetDefaultDataSourceQuery) error {
return m.expectedError
}
func (m *dataSourcesServiceMock) DeleteDataSource(ctx context.Context, cmd *models.DeleteDataSourceCommand) error {
return m.expectedError
}
func (m *dataSourcesServiceMock) AddDataSource(ctx context.Context, cmd *models.AddDataSourceCommand) error {
cmd.Result = m.expectedDatasource
return m.expectedError
}
func (m *dataSourcesServiceMock) UpdateDataSource(ctx context.Context, cmd *models.UpdateDataSourceCommand) error {
cmd.Result = m.expectedDatasource
return m.expectedError
}
func (m *dataSourcesServiceMock) DecryptedValues(ctx context.Context, ds *models.DataSource) (map[string]string, error) {
decryptedValues := make(map[string]string)
return decryptedValues, m.expectedError
}