mirror of https://github.com/grafana/grafana.git
				
				
				
			
		
			
				
	
	
		
			396 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			396 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Go
		
	
	
	
package api
 | 
						|
 | 
						|
import (
 | 
						|
	"context"
 | 
						|
	"encoding/json"
 | 
						|
	"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"
 | 
						|
	"github.com/grafana/grafana/pkg/plugins"
 | 
						|
	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/permissions"
 | 
						|
	"github.com/grafana/grafana/pkg/setting"
 | 
						|
	"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()
 | 
						|
	mockDatasourcePermissionService := permissions.NewMockDatasourcePermissionService()
 | 
						|
	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"},
 | 
						|
		}
 | 
						|
		mockDatasourcePermissionService.DsResult = ds
 | 
						|
 | 
						|
		// handler func being tested
 | 
						|
		hs := &HTTPServer{
 | 
						|
			Cfg:         setting.NewCfg(),
 | 
						|
			pluginStore: &plugins.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: &plugins.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",
 | 
						|
		})
 | 
						|
		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",
 | 
						|
		})
 | 
						|
		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.AuthProxyEnabled = true
 | 
						|
	hs.Cfg.AuthProxyHeaderName = "X-AUTH-PROXY-HEADER"
 | 
						|
	jsonData := simplejson.New()
 | 
						|
	jsonData.Set("httpHeaderName1", hs.Cfg.AuthProxyHeaderName)
 | 
						|
 | 
						|
	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,
 | 
						|
		})
 | 
						|
		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",
 | 
						|
		})
 | 
						|
		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.AuthProxyEnabled = true
 | 
						|
	hs.Cfg.AuthProxyHeaderName = "X-AUTH-PROXY-HEADER"
 | 
						|
	jsonData := simplejson.New()
 | 
						|
	jsonData.Set("httpHeaderName1", hs.Cfg.AuthProxyHeaderName)
 | 
						|
 | 
						|
	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,
 | 
						|
		})
 | 
						|
		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: &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",
 | 
						|
		})
 | 
						|
		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) {
 | 
						|
	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), userWithPermissions(1, tt.permission)))
 | 
						|
				require.NoError(t, err)
 | 
						|
				assert.Equal(t, tt.expectedCode, res.StatusCode)
 | 
						|
				require.NoError(t, res.Body.Close())
 | 
						|
			}
 | 
						|
		})
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
type dataSourcesServiceMock struct {
 | 
						|
	datasources.DataSourceService
 | 
						|
 | 
						|
	expectedDatasources []*datasources.DataSource
 | 
						|
	expectedDatasource  *datasources.DataSource
 | 
						|
	expectedError       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) GetDefaultDataSource(ctx context.Context, query *datasources.GetDefaultDataSourceQuery) (*datasources.DataSource, error) {
 | 
						|
	return nil, 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) {
 | 
						|
	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
 | 
						|
}
 |