Plugins: Modify interface for plugin validations to allow taking PDC into account (#96089)

* Request interceptor: Do not block PDC

* Apply change after feedback received

* Add test

* Check if secure socks proxy configured for the instance

* Apply suggestions from code review

* Add dedicated service for datasource request URL validation (#99179)

---------

Co-authored-by: Will Browne <wbrowne@users.noreply.github.com>
This commit is contained in:
Sofia Papagiannaki
2025-01-24 17:01:46 +02:00
committed by GitHub
co-authored by Will Browne
parent 33a53d170b
commit d192a44469
18 changed files with 161 additions and 85 deletions
+22 -22
View File
@@ -24,35 +24,35 @@ import (
"github.com/grafana/grafana/pkg/web"
)
func ProvideService(dataSourceCache datasources.CacheService, plugReqValidator validations.PluginRequestValidator,
func ProvideService(dataSourceCache datasources.CacheService, datasourceReqValidator validations.DataSourceRequestValidator,
pluginStore pluginstore.Store, cfg *setting.Cfg, httpClientProvider httpclient.Provider,
oauthTokenService *oauthtoken.Service, dsService datasources.DataSourceService,
tracer tracing.Tracer, secretsService secrets.Service, features featuremgmt.FeatureToggles) *DataSourceProxyService {
return &DataSourceProxyService{
DataSourceCache: dataSourceCache,
PluginRequestValidator: plugReqValidator,
pluginStore: pluginStore,
Cfg: cfg,
HTTPClientProvider: httpClientProvider,
OAuthTokenService: oauthTokenService,
DataSourcesService: dsService,
tracer: tracer,
secretsService: secretsService,
features: features,
DataSourceCache: dataSourceCache,
DataSourceRequestValidator: datasourceReqValidator,
pluginStore: pluginStore,
Cfg: cfg,
HTTPClientProvider: httpClientProvider,
OAuthTokenService: oauthTokenService,
DataSourcesService: dsService,
tracer: tracer,
secretsService: secretsService,
features: features,
}
}
type DataSourceProxyService struct {
DataSourceCache datasources.CacheService
PluginRequestValidator validations.PluginRequestValidator
pluginStore pluginstore.Store
Cfg *setting.Cfg
HTTPClientProvider httpclient.Provider
OAuthTokenService *oauthtoken.Service
DataSourcesService datasources.DataSourceService
tracer tracing.Tracer
secretsService secrets.Service
features featuremgmt.FeatureToggles
DataSourceCache datasources.CacheService
DataSourceRequestValidator validations.DataSourceRequestValidator
pluginStore pluginstore.Store
Cfg *setting.Cfg
HTTPClientProvider httpclient.Provider
OAuthTokenService *oauthtoken.Service
DataSourcesService datasources.DataSourceService
tracer tracing.Tracer
secretsService secrets.Service
features featuremgmt.FeatureToggles
}
func (p *DataSourceProxyService) ProxyDataSourceRequest(c *contextmodel.ReqContext) {
@@ -108,7 +108,7 @@ func toAPIError(c *contextmodel.ReqContext, err error) {
}
func (p *DataSourceProxyService) proxyDatasourceRequest(c *contextmodel.ReqContext, ds *datasources.DataSource) {
err := p.PluginRequestValidator.Validate(ds.URL, c.Req)
err := p.DataSourceRequestValidator.Validate(ds, c.Req)
if err != nil {
c.JsonApiErr(http.StatusForbidden, "Access denied", err)
return
@@ -94,8 +94,8 @@ func TestDatasourceProxy_proxyDatasourceRequest(t *testing.T) {
}}
p := DataSourceProxyService{
PluginRequestValidator: &fakePluginRequestValidator{},
pluginStore: pluginStore,
DataSourceRequestValidator: &fakeDataSourceRequestValidator{},
pluginStore: pluginStore,
}
responseRecorder := httptest.NewRecorder()
@@ -129,8 +129,8 @@ func TestDatasourceProxy_proxyDatasourceRequest(t *testing.T) {
}
}
type fakePluginRequestValidator struct{}
type fakeDataSourceRequestValidator struct{}
func (rv *fakePluginRequestValidator) Validate(_ string, _ *http.Request) error {
func (rv *fakeDataSourceRequestValidator) Validate(_ *datasources.DataSource, _ *http.Request) error {
return nil
}
+10
View File
@@ -70,6 +70,16 @@ type DataSource struct {
Created time.Time `json:"created,omitempty"`
Updated time.Time `json:"updated,omitempty"`
isSecureSocksDSProxyEnabled *bool `xorm:"-"`
}
func (ds *DataSource) IsSecureSocksDSProxyEnabled() bool {
if ds.isSecureSocksDSProxyEnabled == nil {
enabled := ds.JsonData != nil && ds.JsonData.Get("enableSecureSocksProxy").MustBool(false)
ds.isSecureSocksDSProxyEnabled = &enabled
}
return *ds.isSecureSocksDSProxyEnabled
}
type TeamHTTPHeadersJSONData struct {
+55
View File
@@ -102,3 +102,58 @@ func TestTeamHTTPHeaders(t *testing.T) {
})
}
}
func TestIsSecureSocksDSProxyEnabled(t *testing.T) {
testCases := []struct {
desc string
ds *DataSource
want bool
}{
{
desc: "Empty json",
ds: &DataSource{
JsonData: simplejson.New(),
},
want: false,
},
{
desc: "Json with enableSecureSocksProxy",
ds: &DataSource{
JsonData: simplejson.NewFromAny(map[string]interface{}{
"enableSecureSocksProxy": true,
}),
},
want: true,
},
{
desc: "Json with string enableSecureSocksProxy",
ds: &DataSource{
JsonData: simplejson.NewFromAny(map[string]interface{}{
"enableSecureSocksProxy": "true",
}),
},
want: false,
},
{
desc: "Json with enableSecureSocksProxy false",
ds: &DataSource{
JsonData: simplejson.NewFromAny(map[string]interface{}{
"enableSecureSocksProxy": false,
}),
},
want: false,
},
{
desc: "Json with no json data",
ds: &DataSource{},
want: false,
},
}
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
actual := tc.ds.IsSecureSocksDSProxyEnabled()
assert.Equal(t, tc.want, actual)
})
}
}
@@ -745,7 +745,7 @@ func (s *Service) httpClientOptions(ctx context.Context, ds *datasources.DataSou
}
}
if ds.JsonData != nil && ds.JsonData.Get("enableSecureSocksProxy").MustBool(false) {
if ds.IsSecureSocksDSProxyEnabled() {
proxyOpts := &sdkproxy.Options{
Enabled: true,
Auth: &sdkproxy.AuthOptions{
@@ -152,18 +152,18 @@ func buildQueryDataService(t *testing.T, cs datasources.CacheService, fpc *fakeP
setting.NewCfg(),
cs,
nil,
&fakePluginRequestValidator{},
&fakeDataSourceRequestValidator{},
fpc,
pCtxProvider,
)
}
// copied from pkg/api/metrics_test.go
type fakePluginRequestValidator struct {
type fakeDataSourceRequestValidator struct {
err error
}
func (rv *fakePluginRequestValidator) Validate(dsURL string, req *http.Request) error {
func (rv *fakeDataSourceRequestValidator) Validate(ds *datasources.DataSource, req *http.Request) error {
return rv.err
}
+18 -18
View File
@@ -42,19 +42,19 @@ func ProvideService(
cfg *setting.Cfg,
dataSourceCache datasources.CacheService,
expressionService *expr.Service,
pluginRequestValidator validations.PluginRequestValidator,
dataSourceRequestValidator validations.DataSourceRequestValidator,
pluginClient plugins.Client,
pCtxProvider *plugincontext.Provider,
) *ServiceImpl {
g := &ServiceImpl{
cfg: cfg,
dataSourceCache: dataSourceCache,
expressionService: expressionService,
pluginRequestValidator: pluginRequestValidator,
pluginClient: pluginClient,
pCtxProvider: pCtxProvider,
log: log.New("query_data"),
concurrentQueryLimit: cfg.SectionWithEnvOverrides("query").Key("concurrent_query_limit").MustInt(runtime.NumCPU()),
cfg: cfg,
dataSourceCache: dataSourceCache,
expressionService: expressionService,
dataSourceRequestValidator: dataSourceRequestValidator,
pluginClient: pluginClient,
pCtxProvider: pCtxProvider,
log: log.New("query_data"),
concurrentQueryLimit: cfg.SectionWithEnvOverrides("query").Key("concurrent_query_limit").MustInt(runtime.NumCPU()),
}
g.log.Info("Query Service initialization")
return g
@@ -70,14 +70,14 @@ type Service interface {
var _ Service = (*ServiceImpl)(nil)
type ServiceImpl struct {
cfg *setting.Cfg
dataSourceCache datasources.CacheService
expressionService *expr.Service
pluginRequestValidator validations.PluginRequestValidator
pluginClient plugins.Client
pCtxProvider *plugincontext.Provider
log log.Logger
concurrentQueryLimit int
cfg *setting.Cfg
dataSourceCache datasources.CacheService
expressionService *expr.Service
dataSourceRequestValidator validations.DataSourceRequestValidator
pluginClient plugins.Client
pCtxProvider *plugincontext.Provider
log log.Logger
concurrentQueryLimit int
}
// Run ServiceImpl.
@@ -244,7 +244,7 @@ func (s *ServiceImpl) handleExpressions(ctx context.Context, user identity.Reque
func (s *ServiceImpl) handleQuerySingleDatasource(ctx context.Context, user identity.Requester, parsedReq *parsedRequest) (*backend.QueryDataResponse, error) {
queries := parsedReq.getFlattenedQueries()
ds := queries[0].datasource
if err := s.pluginRequestValidator.Validate(ds.URL, nil); err != nil {
if err := s.dataSourceRequestValidator.Validate(ds, nil); err != nil {
return nil, datasources.ErrDataSourceAccessDenied
}
+4 -4
View File
@@ -462,7 +462,7 @@ func setup(t *testing.T) *testContext {
t.Helper()
pc := &fakePluginClient{}
dc := &fakeDataSourceCache{cache: dss}
rv := &fakePluginRequestValidator{}
rv := &fakeDataSourceRequestValidator{}
sqlStore, cfg := db.InitTestDBWithCfg(t)
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
@@ -497,7 +497,7 @@ func setup(t *testing.T) *testContext {
type testContext struct {
pluginContext *fakePluginClient
secretStore secretskvs.SecretsKVStore
pluginRequestValidator *fakePluginRequestValidator
pluginRequestValidator *fakeDataSourceRequestValidator
queryService *ServiceImpl // implementation belonging to this package
signedInUser *user.SignedInUser
}
@@ -518,11 +518,11 @@ func metricRequestWithQueries(t *testing.T, rawQueries ...string) dtos.MetricReq
}
}
type fakePluginRequestValidator struct {
type fakeDataSourceRequestValidator struct {
err error
}
func (rv *fakePluginRequestValidator) Validate(dsURL string, req *http.Request) error {
func (rv *fakeDataSourceRequestValidator) Validate(ds *datasources.DataSource, req *http.Request) error {
return rv.err
}
+16 -4
View File
@@ -2,14 +2,26 @@ package validations
import (
"net/http"
"github.com/grafana/grafana/pkg/services/datasources"
)
type OSSPluginRequestValidator struct{}
type OSSDataSourceRequestValidator struct{}
func (*OSSPluginRequestValidator) Validate(string, *http.Request) error {
func (*OSSDataSourceRequestValidator) Validate(*datasources.DataSource, *http.Request) error {
return nil
}
func ProvideValidator() *OSSPluginRequestValidator {
return &OSSPluginRequestValidator{}
func ProvideValidator() *OSSDataSourceRequestValidator {
return &OSSDataSourceRequestValidator{}
}
type OSSDataSourceRequestURLValidator struct{}
func (*OSSDataSourceRequestURLValidator) Validate(string) error {
return nil
}
func ProvideURLValidator() *OSSDataSourceRequestURLValidator {
return &OSSDataSourceRequestURLValidator{}
}
+9 -2
View File
@@ -2,11 +2,18 @@ package validations
import (
"net/http"
"github.com/grafana/grafana/pkg/services/datasources"
)
type PluginRequestValidator interface {
type DataSourceRequestValidator interface {
// Validate performs a request validation based
// on the data source URL and some of the request
// attributes (headers, cookies, etc).
Validate(dsURL string, req *http.Request) error
Validate(ds *datasources.DataSource, req *http.Request) error
}
type DataSourceRequestURLValidator interface {
// Validate performs a request validation based on the data source URL
Validate(dsURL string) error
}