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.ProvideSearchHTTPService,
|
||||
store.ProvideService,
|
||||
store.ProvideSystemUsersService,
|
||||
export.ProvideService,
|
||||
live.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(),
|
||||
)
|
||||
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)
|
||||
}
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
}
|
||||
|
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