diff --git a/pkg/server/wire.go b/pkg/server/wire.go index 7c4dfeadb54..3bc71a93c21 100644 --- a/pkg/server/wire.go +++ b/pkg/server/wire.go @@ -246,6 +246,7 @@ var wireBasicSet = wire.NewSet( searchV2.ProvideService, searchV2.ProvideSearchHTTPService, store.ProvideService, + store.ProvideSystemUsersService, export.ProvideService, live.ProvideService, pushhttp.ProvideService, diff --git a/pkg/services/quota/quotaimpl/quota_test.go b/pkg/services/quota/quotaimpl/quota_test.go index 6614bcbdaf9..54d5c127cd1 100644 --- a/pkg/services/quota/quotaimpl/quota_test.go +++ b/pkg/services/quota/quotaimpl/quota_test.go @@ -479,6 +479,6 @@ func setupEnv(t *testing.T, sqlStore *sqlstore.SQLStore, b bus.Bus, quotaService secretsService, nil, m, &foldertest.FakeService{}, &acmock.Mock{}, &dashboards.FakeDashboardService{}, nil, b, &acmock.Mock{}, annotationstest.NewFakeAnnotationsRepo(), ) require.NoError(t, err) - _, err = storesrv.ProvideService(sqlStore, featuremgmt.WithFeatures(), sqlStore.Cfg, quotaService) + _, err = storesrv.ProvideService(sqlStore, featuremgmt.WithFeatures(), sqlStore.Cfg, quotaService, storesrv.ProvideSystemUsersService()) require.NoError(t, err) } diff --git a/pkg/services/store/service.go b/pkg/services/store/service.go index cc3beab7d10..fc17c6b85b1 100644 --- a/pkg/services/store/service.go +++ b/pkg/services/store/service.go @@ -40,14 +40,6 @@ const RootContent = "content" const RootDevenv = "devenv" const RootSystem = "system" -const brandingStorage = "branding" -const SystemBrandingStorage = "system/" + brandingStorage - -var ( - SystemBrandingReader = &user.SignedInUser{OrgID: ac.GlobalOrgID} - SystemBrandingAdmin = &user.SignedInUser{OrgID: ac.GlobalOrgID} -) - const MAX_UPLOAD_SIZE = 1 * 1024 * 1024 // 3MB type DeleteFolderCmd struct { @@ -96,6 +88,7 @@ type standardStorageService struct { cfg *GlobalStorageConfig authService storageAuthService quotaService quota.Service + systemUsers SystemUsersFilterProvider } func ProvideService( @@ -103,6 +96,7 @@ func ProvideService( features featuremgmt.FeatureToggles, cfg *setting.Cfg, quotaService quota.Service, + systemUsersService SystemUsers, ) (StorageService, error) { settings, err := LoadStorageConfig(cfg, features) if err != nil { @@ -208,22 +202,17 @@ func ProvideService( } if storageName == RootSystem { - if user == SystemBrandingReader { + filter, err := systemUsersService.GetFilter(user) + if err != nil { + grafanaStorageLogger.Error("failed to create path filter for system user", "userID", user.UserID, "userLogin", user.Login, "err", err) return map[string]filestorage.PathFilter{ - ActionFilesRead: createSystemBrandingPathFilter(), + ActionFilesRead: denyAllPathFilter, ActionFilesWrite: denyAllPathFilter, ActionFilesDelete: denyAllPathFilter, } } - if user == SystemBrandingAdmin { - systemBrandingFilter := createSystemBrandingPathFilter() - return map[string]filestorage.PathFilter{ - ActionFilesRead: systemBrandingFilter, - ActionFilesWrite: systemBrandingFilter, - ActionFilesDelete: systemBrandingFilter, - } - } + return filter } if storageName == RootContent { @@ -262,7 +251,7 @@ func ProvideService( } }) - s := newStandardStorageService(sql, globalRoots, initializeOrgStorages, authService, cfg) + s := newStandardStorageService(sql, globalRoots, initializeOrgStorages, authService, cfg, systemUsersService) s.quotaService = quotaService s.cfg = settings @@ -298,20 +287,13 @@ func readQuotaConfig(cfg *setting.Cfg) (*quota.Map, error) { return limits, nil } -func createSystemBrandingPathFilter() filestorage.PathFilter { - return filestorage.NewPathFilter( - []string{filestorage.Delimiter + brandingStorage + filestorage.Delimiter}, // access to all folders and files inside `/branding/` - []string{filestorage.Delimiter + brandingStorage}, // access to the `/branding` folder itself, but not to any other sibling folder - nil, - nil) -} - func newStandardStorageService( sql db.DB, globalRoots []storageRuntime, initializeOrgStorages func(orgId int64) []storageRuntime, authService storageAuthService, cfg *setting.Cfg, + systemUsers SystemUsersFilterProvider, ) *standardStorageService { prefixes := make(map[string]bool) @@ -336,6 +318,7 @@ func newStandardStorageService( sql: sql, tree: res, authService: authService, + systemUsers: systemUsers, } } diff --git a/pkg/services/store/service_test.go b/pkg/services/store/service_test.go index c74b744af16..42683061453 100644 --- a/pkg/services/store/service_test.go +++ b/pkg/services/store/service_test.go @@ -74,7 +74,7 @@ func TestListFiles(t *testing.T) { store := newStandardStorageService(db.InitTestDB(t), roots, func(orgId int64) []storageRuntime { return make([]storageRuntime, 0) - }, allowAllAuthService, cfg) + }, allowAllAuthService, cfg, nil) frame, err := store.List(context.Background(), dummyUser, "public/testdata") require.NoError(t, err) @@ -94,7 +94,7 @@ func TestListFilesWithoutPermissions(t *testing.T) { store := newStandardStorageService(db.InitTestDB(t), roots, func(orgId int64) []storageRuntime { return make([]storageRuntime, 0) - }, denyAllAuthService, cfg) + }, denyAllAuthService, cfg, nil) frame, err := store.List(context.Background(), dummyUser, "public/testdata") require.NoError(t, err) rowLen, err := frame.RowLen() @@ -114,7 +114,7 @@ func setupUploadStore(t *testing.T, authService storageAuthService) (StorageServ } store := newStandardStorageService(db.InitTestDB(t), []storageRuntime{sqlStorage}, func(orgId int64) []storageRuntime { return make([]storageRuntime, 0) - }, authService, cfg) + }, authService, cfg, nil) store.cfg = &GlobalStorageConfig{ AllowUnsanitizedSvgUpload: true, } @@ -268,7 +268,7 @@ func TestSetupWithNonUniqueStoragePrefixes(t *testing.T) { newStandardStorageService(db.InitTestDB(t), []storageRuntime{sqlStorage, sqlStorage2}, func(orgId int64) []storageRuntime { return make([]storageRuntime, 0) - }, allowAllAuthService, cfg) + }, allowAllAuthService, cfg, nil) } func TestContentRootWithNestedStorage(t *testing.T) { @@ -293,7 +293,7 @@ func TestContentRootWithNestedStorage(t *testing.T) { store := newStandardStorageService(db.InitTestDB(t), []storageRuntime{contentStorage, nestedStorage}, func(orgId int64) []storageRuntime { return []storageRuntime{nestedOrgedStorage, contentStorage} - }, allowAllAuthService, cfg) + }, allowAllAuthService, cfg, nil) store.cfg = &GlobalStorageConfig{ AllowUnsanitizedSvgUpload: true, } @@ -531,7 +531,7 @@ func TestShadowingExistingFolderByNestedContentRoot(t *testing.T) { }) require.NoError(t, err) - store := newStandardStorageService(db, []storageRuntime{nestedStorage, contentStorage}, func(orgId int64) []storageRuntime { return make([]storageRuntime, 0) }, allowAllAuthService, cfg) + store := newStandardStorageService(db, []storageRuntime{nestedStorage, contentStorage}, func(orgId int64) []storageRuntime { return make([]storageRuntime, 0) }, allowAllAuthService, cfg, nil) store.cfg = &GlobalStorageConfig{ AllowUnsanitizedSvgUpload: true, } diff --git a/pkg/services/store/system_users.go b/pkg/services/store/system_users.go new file mode 100644 index 00000000000..048710a949d --- /dev/null +++ b/pkg/services/store/system_users.go @@ -0,0 +1,109 @@ +package store + +import ( + "fmt" + "sync" + + "github.com/grafana/grafana/pkg/infra/filestorage" + ac "github.com/grafana/grafana/pkg/services/accesscontrol" + "github.com/grafana/grafana/pkg/services/user" +) + +type SystemUserType string + +// SystemUsersFilterProvider interface internal to `pkg/store` service. +// Used by the Storage service to retrieve path filter for system users +type SystemUsersFilterProvider interface { + GetFilter(user *user.SignedInUser) (map[string]filestorage.PathFilter, error) +} + +// SystemUsersProvider interface used by `pkg/store` clients +// Used by Grafana services to retrieve users having access only to their own slice of storage +// For example, service 'Dashboard' could have exclusive access to paths matching `system/dashboard/*` +// by creating a system user with appropriate permissions. +type SystemUsersProvider interface { + GetUser(userType SystemUserType, orgID int64) (*user.SignedInUser, error) +} + +type SystemUsers interface { + SystemUsersFilterProvider + SystemUsersProvider + + // RegisterUser extension point - allows other Grafana services to register their own user type and assign them path-based permissions + RegisterUser(userType SystemUserType, filterFn func() map[string]filestorage.PathFilter) +} + +func ProvideSystemUsersService() SystemUsers { + return &hardcodedSystemUsers{ + mutex: sync.RWMutex{}, + users: make(map[SystemUserType]map[int64]*user.SignedInUser), + createFilterByUser: make(map[*user.SignedInUser]func() map[string]filestorage.PathFilter), + } +} + +type hardcodedSystemUsers struct { + mutex sync.RWMutex + + // map of user type -> map of user per orgID + users map[SystemUserType]map[int64]*user.SignedInUser + + // map of user -> create filter function. all users of the same type will point to the same function + createFilterByUser map[*user.SignedInUser]func() map[string]filestorage.PathFilter +} + +func (h *hardcodedSystemUsers) GetFilter(user *user.SignedInUser) (map[string]filestorage.PathFilter, error) { + h.mutex.Lock() + defer h.mutex.Unlock() + + createFn, ok := h.createFilterByUser[user] + if !ok { + return nil, fmt.Errorf("user %s with id %d has not been initialized", user.Login, user.UserID) + } + + return createFn(), nil +} + +func (h *hardcodedSystemUsers) GetUser(userType SystemUserType, orgID int64) (*user.SignedInUser, error) { + h.mutex.Lock() + defer h.mutex.Unlock() + + userPerOrgIdMap, ok := h.users[userType] + if !ok { + return nil, fmt.Errorf("user type %s is unknown", userType) + } + + orgSignedInUser, ok := userPerOrgIdMap[orgID] + if ok { + return orgSignedInUser, nil + } + + // user for the given org does not yet exist - initialize it + + globalUser, globalUserExists := userPerOrgIdMap[ac.GlobalOrgID] + if !globalUserExists { + return nil, fmt.Errorf("initialization error: user type %s should exist for global org id: %d", userType, ac.GlobalOrgID) + } + + globalUserFn, globalUserFnExists := h.createFilterByUser[globalUser] + if !globalUserFnExists { + return nil, fmt.Errorf("initialization error: user type %s should be associated with a create filter function", userType) + } + + newUser := &user.SignedInUser{ + Login: string(userType), + OrgID: orgID, + } + userPerOrgIdMap[orgID] = newUser + h.createFilterByUser[newUser] = globalUserFn + return newUser, nil +} + +func (h *hardcodedSystemUsers) RegisterUser(userType SystemUserType, filterFn func() map[string]filestorage.PathFilter) { + h.mutex.Lock() + defer h.mutex.Unlock() + + globalUser := &user.SignedInUser{OrgID: ac.GlobalOrgID, Login: string(userType)} + h.users[userType] = map[int64]*user.SignedInUser{ac.GlobalOrgID: globalUser} + + h.createFilterByUser[globalUser] = filterFn +} diff --git a/pkg/services/store/system_users_test.go b/pkg/services/store/system_users_test.go new file mode 100644 index 00000000000..422d975974f --- /dev/null +++ b/pkg/services/store/system_users_test.go @@ -0,0 +1,63 @@ +package store + +import ( + "testing" + + "github.com/grafana/grafana/pkg/infra/filestorage" + "github.com/grafana/grafana/pkg/services/user" + "github.com/stretchr/testify/require" +) + +const admin SystemUserType = "storageAdmin" + +func TestRetrievalOfNotInitializedOrg(t *testing.T) { + service := setupSystemUsers() + + orgID := int64(1) + user, err := service.GetUser(admin, orgID) + require.NoError(t, err) + + require.Equal(t, string(admin), user.Login) + require.Equal(t, orgID, user.OrgID) + + userFromSubsequentCall, err := service.GetUser(admin, orgID) + require.NoError(t, err) + require.Same(t, user, userFromSubsequentCall) +} + +func TestRetrievalOfFilterForInitializedUser(t *testing.T) { + service := setupSystemUsers() + + orgID := int64(1) + reportsAdminUser, err := service.GetUser(admin, orgID) + require.NoError(t, err) + + filter, err := service.GetFilter(reportsAdminUser) + require.NoError(t, err) + require.NotNil(t, filter) +} + +func TestRetrievalOfFilterForNotInitializedUser(t *testing.T) { + service := setupSystemUsers() + + orgID := int64(1) + + filter, err := service.GetFilter(&user.SignedInUser{ + OrgID: orgID, + Login: string(admin), + }) + require.Error(t, err) + require.Nil(t, filter) +} + +func setupSystemUsers() SystemUsers { + service := ProvideSystemUsersService() + + service.RegisterUser(admin, func() map[string]filestorage.PathFilter { + return map[string]filestorage.PathFilter{ + ActionFilesRead: denyAllPathFilter, + } + }) + + return service +}