mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Storage: Add system users service (#57767)
* Storage: Add access for reporting * reporting upload user per org * add some basic comments * Move reporting storage to enterprise * add comments Co-authored-by: Artur Wierzbicki <artur.wierzbicki@grafana.com>
This commit is contained in:
parent
e84a01e870
commit
76372a240c
@ -246,6 +246,7 @@ var wireBasicSet = wire.NewSet(
|
|||||||
searchV2.ProvideService,
|
searchV2.ProvideService,
|
||||||
searchV2.ProvideSearchHTTPService,
|
searchV2.ProvideSearchHTTPService,
|
||||||
store.ProvideService,
|
store.ProvideService,
|
||||||
|
store.ProvideSystemUsersService,
|
||||||
export.ProvideService,
|
export.ProvideService,
|
||||||
live.ProvideService,
|
live.ProvideService,
|
||||||
pushhttp.ProvideService,
|
pushhttp.ProvideService,
|
||||||
|
@ -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(),
|
secretsService, nil, m, &foldertest.FakeService{}, &acmock.Mock{}, &dashboards.FakeDashboardService{}, nil, b, &acmock.Mock{}, annotationstest.NewFakeAnnotationsRepo(),
|
||||||
)
|
)
|
||||||
require.NoError(t, err)
|
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)
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
@ -40,14 +40,6 @@ const RootContent = "content"
|
|||||||
const RootDevenv = "devenv"
|
const RootDevenv = "devenv"
|
||||||
const RootSystem = "system"
|
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
|
const MAX_UPLOAD_SIZE = 1 * 1024 * 1024 // 3MB
|
||||||
|
|
||||||
type DeleteFolderCmd struct {
|
type DeleteFolderCmd struct {
|
||||||
@ -96,6 +88,7 @@ type standardStorageService struct {
|
|||||||
cfg *GlobalStorageConfig
|
cfg *GlobalStorageConfig
|
||||||
authService storageAuthService
|
authService storageAuthService
|
||||||
quotaService quota.Service
|
quotaService quota.Service
|
||||||
|
systemUsers SystemUsersFilterProvider
|
||||||
}
|
}
|
||||||
|
|
||||||
func ProvideService(
|
func ProvideService(
|
||||||
@ -103,6 +96,7 @@ func ProvideService(
|
|||||||
features featuremgmt.FeatureToggles,
|
features featuremgmt.FeatureToggles,
|
||||||
cfg *setting.Cfg,
|
cfg *setting.Cfg,
|
||||||
quotaService quota.Service,
|
quotaService quota.Service,
|
||||||
|
systemUsersService SystemUsers,
|
||||||
) (StorageService, error) {
|
) (StorageService, error) {
|
||||||
settings, err := LoadStorageConfig(cfg, features)
|
settings, err := LoadStorageConfig(cfg, features)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -208,22 +202,17 @@ func ProvideService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if storageName == RootSystem {
|
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{
|
return map[string]filestorage.PathFilter{
|
||||||
ActionFilesRead: createSystemBrandingPathFilter(),
|
ActionFilesRead: denyAllPathFilter,
|
||||||
ActionFilesWrite: denyAllPathFilter,
|
ActionFilesWrite: denyAllPathFilter,
|
||||||
ActionFilesDelete: denyAllPathFilter,
|
ActionFilesDelete: denyAllPathFilter,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if user == SystemBrandingAdmin {
|
return filter
|
||||||
systemBrandingFilter := createSystemBrandingPathFilter()
|
|
||||||
return map[string]filestorage.PathFilter{
|
|
||||||
ActionFilesRead: systemBrandingFilter,
|
|
||||||
ActionFilesWrite: systemBrandingFilter,
|
|
||||||
ActionFilesDelete: systemBrandingFilter,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if storageName == RootContent {
|
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.quotaService = quotaService
|
||||||
s.cfg = settings
|
s.cfg = settings
|
||||||
|
|
||||||
@ -298,20 +287,13 @@ func readQuotaConfig(cfg *setting.Cfg) (*quota.Map, error) {
|
|||||||
return limits, nil
|
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(
|
func newStandardStorageService(
|
||||||
sql db.DB,
|
sql db.DB,
|
||||||
globalRoots []storageRuntime,
|
globalRoots []storageRuntime,
|
||||||
initializeOrgStorages func(orgId int64) []storageRuntime,
|
initializeOrgStorages func(orgId int64) []storageRuntime,
|
||||||
authService storageAuthService,
|
authService storageAuthService,
|
||||||
cfg *setting.Cfg,
|
cfg *setting.Cfg,
|
||||||
|
systemUsers SystemUsersFilterProvider,
|
||||||
) *standardStorageService {
|
) *standardStorageService {
|
||||||
prefixes := make(map[string]bool)
|
prefixes := make(map[string]bool)
|
||||||
|
|
||||||
@ -336,6 +318,7 @@ func newStandardStorageService(
|
|||||||
sql: sql,
|
sql: sql,
|
||||||
tree: res,
|
tree: res,
|
||||||
authService: authService,
|
authService: authService,
|
||||||
|
systemUsers: systemUsers,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -74,7 +74,7 @@ func TestListFiles(t *testing.T) {
|
|||||||
|
|
||||||
store := newStandardStorageService(db.InitTestDB(t), roots, func(orgId int64) []storageRuntime {
|
store := newStandardStorageService(db.InitTestDB(t), roots, func(orgId int64) []storageRuntime {
|
||||||
return make([]storageRuntime, 0)
|
return make([]storageRuntime, 0)
|
||||||
}, allowAllAuthService, cfg)
|
}, allowAllAuthService, cfg, nil)
|
||||||
frame, err := store.List(context.Background(), dummyUser, "public/testdata")
|
frame, err := store.List(context.Background(), dummyUser, "public/testdata")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@ -94,7 +94,7 @@ func TestListFilesWithoutPermissions(t *testing.T) {
|
|||||||
|
|
||||||
store := newStandardStorageService(db.InitTestDB(t), roots, func(orgId int64) []storageRuntime {
|
store := newStandardStorageService(db.InitTestDB(t), roots, func(orgId int64) []storageRuntime {
|
||||||
return make([]storageRuntime, 0)
|
return make([]storageRuntime, 0)
|
||||||
}, denyAllAuthService, cfg)
|
}, denyAllAuthService, cfg, nil)
|
||||||
frame, err := store.List(context.Background(), dummyUser, "public/testdata")
|
frame, err := store.List(context.Background(), dummyUser, "public/testdata")
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
rowLen, err := frame.RowLen()
|
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 {
|
store := newStandardStorageService(db.InitTestDB(t), []storageRuntime{sqlStorage}, func(orgId int64) []storageRuntime {
|
||||||
return make([]storageRuntime, 0)
|
return make([]storageRuntime, 0)
|
||||||
}, authService, cfg)
|
}, authService, cfg, nil)
|
||||||
store.cfg = &GlobalStorageConfig{
|
store.cfg = &GlobalStorageConfig{
|
||||||
AllowUnsanitizedSvgUpload: true,
|
AllowUnsanitizedSvgUpload: true,
|
||||||
}
|
}
|
||||||
@ -268,7 +268,7 @@ func TestSetupWithNonUniqueStoragePrefixes(t *testing.T) {
|
|||||||
|
|
||||||
newStandardStorageService(db.InitTestDB(t), []storageRuntime{sqlStorage, sqlStorage2}, func(orgId int64) []storageRuntime {
|
newStandardStorageService(db.InitTestDB(t), []storageRuntime{sqlStorage, sqlStorage2}, func(orgId int64) []storageRuntime {
|
||||||
return make([]storageRuntime, 0)
|
return make([]storageRuntime, 0)
|
||||||
}, allowAllAuthService, cfg)
|
}, allowAllAuthService, cfg, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestContentRootWithNestedStorage(t *testing.T) {
|
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 {
|
store := newStandardStorageService(db.InitTestDB(t), []storageRuntime{contentStorage, nestedStorage}, func(orgId int64) []storageRuntime {
|
||||||
return []storageRuntime{nestedOrgedStorage, contentStorage}
|
return []storageRuntime{nestedOrgedStorage, contentStorage}
|
||||||
}, allowAllAuthService, cfg)
|
}, allowAllAuthService, cfg, nil)
|
||||||
store.cfg = &GlobalStorageConfig{
|
store.cfg = &GlobalStorageConfig{
|
||||||
AllowUnsanitizedSvgUpload: true,
|
AllowUnsanitizedSvgUpload: true,
|
||||||
}
|
}
|
||||||
@ -531,7 +531,7 @@ func TestShadowingExistingFolderByNestedContentRoot(t *testing.T) {
|
|||||||
})
|
})
|
||||||
require.NoError(t, err)
|
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{
|
store.cfg = &GlobalStorageConfig{
|
||||||
AllowUnsanitizedSvgUpload: true,
|
AllowUnsanitizedSvgUpload: true,
|
||||||
}
|
}
|
||||||
|
109
pkg/services/store/system_users.go
Normal file
109
pkg/services/store/system_users.go
Normal file
@ -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
|
||||||
|
}
|
63
pkg/services/store/system_users_test.go
Normal file
63
pkg/services/store/system_users_test.go
Normal file
@ -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
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user