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:
Tania 2022-11-24 15:15:32 +01:00 committed by GitHub
parent e84a01e870
commit 76372a240c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 190 additions and 34 deletions

View File

@ -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,

View File

@ -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)
} }

View File

@ -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,
} }
} }

View File

@ -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,
} }

View 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
}

View 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
}