Access Control: Clean up permissions for deprovisioned data sources (#88483)

* make sure that DS permissions get correctly cleaned up when a DS is deleted through provisioning

* don't attempt to delete a DS if it's not found

* fixes for tests

* fix ds tests

* rename DS service used by DS provisioner to BaseDataSourceService to avoid confusions with the full DS service
This commit is contained in:
Ieva 2024-06-03 16:19:53 +03:00 committed by GitHub
parent f204dd5bf1
commit c16f502ec5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 90 additions and 69 deletions

View File

@ -46,7 +46,7 @@ func (m *MockPermissionsService) SetPermissions(ctx context.Context, orgID int64
func (m *MockPermissionsService) DeleteResourcePermissions(ctx context.Context, orgID int64, resourceID string) error { func (m *MockPermissionsService) DeleteResourcePermissions(ctx context.Context, orgID int64, resourceID string) error {
mockedArgs := m.Called(ctx, orgID, resourceID) mockedArgs := m.Called(ctx, orgID, resourceID)
return mockedArgs.Error(1) return mockedArgs.Error(0)
} }
func (m *MockPermissionsService) MapActions(permission accesscontrol.ResourcePermission) string { func (m *MockPermissionsService) MapActions(permission accesscontrol.ResourcePermission) string {

View File

@ -542,6 +542,71 @@ func TestService_UpdateDataSource(t *testing.T) {
}) })
} }
func TestService_DeleteDataSource(t *testing.T) {
t.Run("should not return an error if data source doesn't exist", func(t *testing.T) {
sqlStore := db.InitTestDB(t)
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
quotaService := quotatest.New(false, nil)
permissionSvc := acmock.NewMockedPermissionsService()
permissionSvc.On("DeleteResourcePermissions", mock.Anything, mock.Anything, mock.Anything).Return(nil).Maybe()
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, &setting.Cfg{}, featuremgmt.WithFeatures(), acmock.New(), permissionSvc, quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{}, nil)
require.NoError(t, err)
cmd := &datasources.DeleteDataSourceCommand{
UID: uuid.New().String(),
ID: 1,
OrgID: 1,
}
err = dsService.DeleteDataSource(context.Background(), cmd)
require.NoError(t, err)
})
t.Run("should successfully delete a data source that exists", func(t *testing.T) {
sqlStore := db.InitTestDB(t)
secretsService := secretsmng.SetupTestService(t, fakes.NewFakeSecretsStore())
secretsStore := secretskvs.NewSQLSecretsKVStore(sqlStore, secretsService, log.New("test.logger"))
quotaService := quotatest.New(false, nil)
permissionSvc := acmock.NewMockedPermissionsService()
permissionSvc.On("SetPermissions", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return([]accesscontrol.ResourcePermission{}, nil).Once()
permissionSvc.On("DeleteResourcePermissions", mock.Anything, mock.Anything, mock.Anything).Return(nil).Once()
dsService, err := ProvideService(sqlStore, secretsService, secretsStore, &setting.Cfg{}, featuremgmt.WithFeatures(), acmock.New(), permissionSvc, quotaService, &pluginstore.FakePluginStore{}, &pluginfakes.FakePluginClient{}, nil)
require.NoError(t, err)
// First add the datasource
ds, err := dsService.AddDataSource(context.Background(), &datasources.AddDataSourceCommand{
OrgID: 1,
Name: "test",
Type: "test",
UserID: 0,
})
require.NoError(t, err)
cmd := &datasources.DeleteDataSourceCommand{
ID: ds.ID,
UID: ds.UID,
OrgID: 1,
}
err = dsService.DeleteDataSource(context.Background(), cmd)
require.NoError(t, err)
// Data source doesn't exist anymore
ds, err = dsService.GetDataSource(context.Background(), &datasources.GetDataSourceQuery{
OrgID: 1,
UID: ds.UID,
})
require.Nil(t, ds)
require.ErrorIs(t, err, datasources.ErrDataSourceNotFound)
permissionSvc.AssertExpectations(t)
})
}
func TestService_NameScopeResolver(t *testing.T) { func TestService_NameScopeResolver(t *testing.T) {
retriever := &dataSourceMockRetriever{[]*datasources.DataSource{ retriever := &dataSourceMockRetriever{[]*datasources.DataSource{
{Name: "test-datasource", UID: "1"}, {Name: "test-datasource", UID: "1"},

View File

@ -14,7 +14,6 @@ import (
"github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/infra/metrics" "github.com/grafana/grafana/pkg/infra/metrics"
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/quota" "github.com/grafana/grafana/pkg/services/quota"
@ -162,12 +161,6 @@ func (ss *SqlStore) DeleteDataSource(ctx context.Context, cmd *datasources.Delet
} }
cmd.DeletedDatasourcesCount, _ = result.RowsAffected() cmd.DeletedDatasourcesCount, _ = result.RowsAffected()
// Remove associated AccessControl permissions
if _, errDeletingPerms := sess.Exec("DELETE FROM permission WHERE scope=?",
ac.Scope(datasources.ScopeProvider.GetResourceScope(ds.UID))); errDeletingPerms != nil {
return errDeletingPerms
}
} }
if cmd.UpdateSecretFn != nil { if cmd.UpdateSecretFn != nil {

View File

@ -12,7 +12,6 @@ import (
"github.com/grafana/grafana/pkg/events" "github.com/grafana/grafana/pkg/events"
"github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/db"
"github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/log"
ac "github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/featuremgmt"
) )
@ -350,46 +349,6 @@ func TestIntegrationDataAccess(t *testing.T) {
require.Equal(t, 0, len(dataSources)) require.Equal(t, 0, len(dataSources))
}) })
t.Run("DeleteDataSourceAccessControlPermissions", func(t *testing.T) {
store := db.InitTestDB(t)
ds := initDatasource(store)
ss := SqlStore{db: store}
// Init associated permission
errAddPermissions := store.WithTransactionalDbSession(context.TODO(), func(sess *db.Session) error {
_, err := sess.Table("permission").Insert(ac.Permission{
RoleID: 1,
Action: "datasources:read",
Scope: datasources.ScopeProvider.GetResourceScope(ds.UID),
Updated: time.Now(),
Created: time.Now(),
})
return err
})
require.NoError(t, errAddPermissions)
query := datasources.GetDataSourcesQuery{OrgID: 10}
errDeletingDS := ss.DeleteDataSource(context.Background(),
&datasources.DeleteDataSourceCommand{Name: ds.Name, OrgID: ds.OrgID},
)
require.NoError(t, errDeletingDS)
// Check associated permission
permCount := int64(0)
errGetPermissions := store.WithTransactionalDbSession(context.TODO(), func(sess *db.Session) error {
var err error
permCount, err = sess.Table("permission").Count()
return err
})
require.NoError(t, errGetPermissions)
require.Zero(t, permCount, "permissions associated to the data source should have been removed")
dataSources, err := ss.GetDataSources(context.Background(), &query)
require.NoError(t, err)
require.Equal(t, 0, len(dataSources))
})
t.Run("GetDataSources", func(t *testing.T) { t.Run("GetDataSources", func(t *testing.T) {
t.Run("Number of data sources returned limited to 6 per organization", func(t *testing.T) { t.Run("Number of data sources returned limited to 6 per organization", func(t *testing.T) {
db := db.InitTestDB(t) db := db.InitTestDB(t)

View File

@ -125,7 +125,7 @@ func TestDatasourceAsConfig(t *testing.T) {
}) })
t.Run("Remove one datasource should have removed old datasource", func(t *testing.T) { t.Run("Remove one datasource should have removed old datasource", func(t *testing.T) {
store := &spyStore{} store := &spyStore{items: []*datasources.DataSource{{Name: "old-data-source", OrgID: 1, UID: "old-data-source"}}}
orgFake := &orgtest.FakeOrgService{} orgFake := &orgtest.FakeOrgService{}
correlationsStore := &mockCorrelationsStore{} correlationsStore := &mockCorrelationsStore{}
dc := newDatasourceProvisioner(logger, store, correlationsStore, orgFake) dc := newDatasourceProvisioner(logger, store, correlationsStore, orgFake)
@ -142,7 +142,7 @@ func TestDatasourceAsConfig(t *testing.T) {
}) })
t.Run("Two configured datasource and purge others", func(t *testing.T) { t.Run("Two configured datasource and purge others", func(t *testing.T) {
store := &spyStore{items: []*datasources.DataSource{{Name: "old-graphite", OrgID: 1, ID: 1}, {Name: "old-graphite2", OrgID: 1, ID: 2}}} store := &spyStore{items: []*datasources.DataSource{{Name: "old-graphite", OrgID: 1, ID: 1}, {Name: "old-graphite3", OrgID: 1, ID: 2}}}
orgFake := &orgtest.FakeOrgService{} orgFake := &orgtest.FakeOrgService{}
correlationsStore := &mockCorrelationsStore{} correlationsStore := &mockCorrelationsStore{}
dc := newDatasourceProvisioner(logger, store, correlationsStore, orgFake) dc := newDatasourceProvisioner(logger, store, correlationsStore, orgFake)

View File

@ -12,7 +12,7 @@ import (
jsoniter "github.com/json-iterator/go" jsoniter "github.com/json-iterator/go"
) )
type Store interface { type BaseDataSourceService interface {
GetDataSource(ctx context.Context, query *datasources.GetDataSourceQuery) (*datasources.DataSource, error) GetDataSource(ctx context.Context, query *datasources.GetDataSourceQuery) (*datasources.DataSource, error)
GetPrunableProvisionedDataSources(ctx context.Context) ([]*datasources.DataSource, error) GetPrunableProvisionedDataSources(ctx context.Context) ([]*datasources.DataSource, error)
AddDataSource(ctx context.Context, cmd *datasources.AddDataSourceCommand) (*datasources.DataSource, error) AddDataSource(ctx context.Context, cmd *datasources.AddDataSourceCommand) (*datasources.DataSource, error)
@ -35,8 +35,8 @@ var (
// Provision scans a directory for provisioning config files // Provision scans a directory for provisioning config files
// and provisions the datasource in those files. // and provisions the datasource in those files.
func Provision(ctx context.Context, configDirectory string, store Store, correlationsStore CorrelationsStore, orgService org.Service) error { func Provision(ctx context.Context, configDirectory string, dsService BaseDataSourceService, correlationsStore CorrelationsStore, orgService org.Service) error {
dc := newDatasourceProvisioner(log.New("provisioning.datasources"), store, correlationsStore, orgService) dc := newDatasourceProvisioner(log.New("provisioning.datasources"), dsService, correlationsStore, orgService)
return dc.applyChanges(ctx, configDirectory) return dc.applyChanges(ctx, configDirectory)
} }
@ -45,15 +45,15 @@ func Provision(ctx context.Context, configDirectory string, store Store, correla
type DatasourceProvisioner struct { type DatasourceProvisioner struct {
log log.Logger log log.Logger
cfgProvider *configReader cfgProvider *configReader
store Store dsService BaseDataSourceService
correlationsStore CorrelationsStore correlationsStore CorrelationsStore
} }
func newDatasourceProvisioner(log log.Logger, store Store, correlationsStore CorrelationsStore, orgService org.Service) DatasourceProvisioner { func newDatasourceProvisioner(log log.Logger, dsService BaseDataSourceService, correlationsStore CorrelationsStore, orgService org.Service) DatasourceProvisioner {
return DatasourceProvisioner{ return DatasourceProvisioner{
log: log, log: log,
cfgProvider: &configReader{log: log, orgService: orgService}, cfgProvider: &configReader{log: log, orgService: orgService},
store: store, dsService: dsService,
correlationsStore: correlationsStore, correlationsStore: correlationsStore,
} }
} }
@ -65,7 +65,7 @@ func (dc *DatasourceProvisioner) provisionDataSources(ctx context.Context, cfg *
for _, ds := range cfg.Datasources { for _, ds := range cfg.Datasources {
cmd := &datasources.GetDataSourceQuery{OrgID: ds.OrgID, Name: ds.Name} cmd := &datasources.GetDataSourceQuery{OrgID: ds.OrgID, Name: ds.Name}
dataSource, err := dc.store.GetDataSource(ctx, cmd) dataSource, err := dc.dsService.GetDataSource(ctx, cmd)
if err != nil && !errors.Is(err, datasources.ErrDataSourceNotFound) { if err != nil && !errors.Is(err, datasources.ErrDataSourceNotFound) {
return err return err
} }
@ -73,14 +73,14 @@ func (dc *DatasourceProvisioner) provisionDataSources(ctx context.Context, cfg *
if errors.Is(err, datasources.ErrDataSourceNotFound) { if errors.Is(err, datasources.ErrDataSourceNotFound) {
insertCmd := createInsertCommand(ds) insertCmd := createInsertCommand(ds)
dc.log.Info("inserting datasource from configuration", "name", insertCmd.Name, "uid", insertCmd.UID) dc.log.Info("inserting datasource from configuration", "name", insertCmd.Name, "uid", insertCmd.UID)
_, err = dc.store.AddDataSource(ctx, insertCmd) _, err = dc.dsService.AddDataSource(ctx, insertCmd)
if err != nil { if err != nil {
return err return err
} }
} else { } else {
updateCmd := createUpdateCommand(ds, dataSource.ID) updateCmd := createUpdateCommand(ds, dataSource.ID)
dc.log.Debug("updating datasource from configuration", "name", updateCmd.Name, "uid", updateCmd.UID) dc.log.Debug("updating datasource from configuration", "name", updateCmd.Name, "uid", updateCmd.UID)
if _, err := dc.store.UpdateDataSource(ctx, updateCmd); err != nil { if _, err := dc.dsService.UpdateDataSource(ctx, updateCmd); err != nil {
if errors.Is(err, datasources.ErrDataSourceUpdatingOldVersion) { if errors.Is(err, datasources.ErrDataSourceUpdatingOldVersion) {
dc.log.Debug("ignoring old version of datasource", "name", updateCmd.Name, "uid", updateCmd.UID) dc.log.Debug("ignoring old version of datasource", "name", updateCmd.Name, "uid", updateCmd.UID)
} else { } else {
@ -96,7 +96,7 @@ func (dc *DatasourceProvisioner) provisionDataSources(ctx context.Context, cfg *
func (dc *DatasourceProvisioner) provisionCorrelations(ctx context.Context, cfg *configs) error { func (dc *DatasourceProvisioner) provisionCorrelations(ctx context.Context, cfg *configs) error {
for _, ds := range cfg.Datasources { for _, ds := range cfg.Datasources {
cmd := &datasources.GetDataSourceQuery{OrgID: ds.OrgID, Name: ds.Name} cmd := &datasources.GetDataSourceQuery{OrgID: ds.OrgID, Name: ds.Name}
dataSource, err := dc.store.GetDataSource(ctx, cmd) dataSource, err := dc.dsService.GetDataSource(ctx, cmd)
if errors.Is(err, datasources.ErrDataSourceNotFound) { if errors.Is(err, datasources.ErrDataSourceNotFound) {
return err return err
@ -154,7 +154,7 @@ func (dc *DatasourceProvisioner) applyChanges(ctx context.Context, configPath st
} }
} }
prunableProvisionedDataSources, err := dc.store.GetPrunableProvisionedDataSources(ctx) prunableProvisionedDataSources, err := dc.dsService.GetPrunableProvisionedDataSources(ctx)
if err != nil { if err != nil {
return err return err
} }
@ -238,17 +238,21 @@ func makeCreateCorrelationCommand(correlation map[string]any, SourceUID string,
func (dc *DatasourceProvisioner) deleteDatasources(ctx context.Context, dsToDelete []*deleteDatasourceConfig, willExistAfterProvisioning map[DataSourceMapKey]bool) error { func (dc *DatasourceProvisioner) deleteDatasources(ctx context.Context, dsToDelete []*deleteDatasourceConfig, willExistAfterProvisioning map[DataSourceMapKey]bool) error {
for _, ds := range dsToDelete { for _, ds := range dsToDelete {
getDsQuery := &datasources.GetDataSourceQuery{Name: ds.Name, OrgID: ds.OrgID} getDsQuery := &datasources.GetDataSourceQuery{Name: ds.Name, OrgID: ds.OrgID}
_, err := dc.store.GetDataSource(ctx, getDsQuery) existingDs, err := dc.dsService.GetDataSource(ctx, getDsQuery)
if err != nil && !errors.Is(err, datasources.ErrDataSourceNotFound) { if err != nil {
return err if errors.Is(err, datasources.ErrDataSourceNotFound) {
continue
} else {
return err
}
} }
// Skip publishing the event as the data source is not really deleted, it will be re-created during provisioning // Skip publishing the event as the data source is not really deleted, it will be re-created during provisioning
// This is to avoid cleaning up any resources related to the data source (e.g. correlations) // This is to avoid cleaning up any resources related to the data source (e.g. correlations)
skipPublish := willExistAfterProvisioning[DataSourceMapKey{Name: ds.Name, OrgId: ds.OrgID}] skipPublish := willExistAfterProvisioning[DataSourceMapKey{Name: ds.Name, OrgId: ds.OrgID}]
cmd := &datasources.DeleteDataSourceCommand{OrgID: ds.OrgID, Name: ds.Name, SkipPublish: skipPublish} cmd := &datasources.DeleteDataSourceCommand{OrgID: ds.OrgID, Name: ds.Name, UID: existingDs.UID, SkipPublish: skipPublish}
if err := dc.store.DeleteDataSource(ctx, cmd); err != nil { if err := dc.dsService.DeleteDataSource(ctx, cmd); err != nil {
return err return err
} }

View File

@ -118,7 +118,7 @@ func NewProvisioningServiceImpl() *ProvisioningServiceImpl {
// Used for testing purposes // Used for testing purposes
func newProvisioningServiceImpl( func newProvisioningServiceImpl(
newDashboardProvisioner dashboards.DashboardProvisionerFactory, newDashboardProvisioner dashboards.DashboardProvisionerFactory,
provisionDatasources func(context.Context, string, datasources.Store, datasources.CorrelationsStore, org.Service) error, provisionDatasources func(context.Context, string, datasources.BaseDataSourceService, datasources.CorrelationsStore, org.Service) error,
provisionPlugins func(context.Context, string, pluginstore.Store, pluginsettings.Service, org.Service) error, provisionPlugins func(context.Context, string, pluginstore.Store, pluginsettings.Service, org.Service) error,
) *ProvisioningServiceImpl { ) *ProvisioningServiceImpl {
return &ProvisioningServiceImpl{ return &ProvisioningServiceImpl{
@ -142,7 +142,7 @@ type ProvisioningServiceImpl struct {
pollingCtxCancel context.CancelFunc pollingCtxCancel context.CancelFunc
newDashboardProvisioner dashboards.DashboardProvisionerFactory newDashboardProvisioner dashboards.DashboardProvisionerFactory
dashboardProvisioner dashboards.DashboardProvisioner dashboardProvisioner dashboards.DashboardProvisioner
provisionDatasources func(context.Context, string, datasources.Store, datasources.CorrelationsStore, org.Service) error provisionDatasources func(context.Context, string, datasources.BaseDataSourceService, datasources.CorrelationsStore, org.Service) error
provisionPlugins func(context.Context, string, pluginstore.Store, pluginsettings.Service, org.Service) error provisionPlugins func(context.Context, string, pluginstore.Store, pluginsettings.Service, org.Service) error
provisionAlerting func(context.Context, prov_alerting.ProvisionerConfig) error provisionAlerting func(context.Context, prov_alerting.ProvisionerConfig) error
mutex sync.Mutex mutex sync.Mutex