Unistore: Create default permissions through Folder APIServer (#101420)

* Unistore: Declare a new storage to set default folder permissions

Signed-off-by: Maicon Costa <maiconscosta@gmail.com>

* Remove the setting of default permissions from folder legacy storage

Signed-off-by: Maicon Costa <maiconscosta@gmail.com>

* Disable setting of folder permissions when Api Server is enabled

Signed-off-by: Maicon Costa <maiconscosta@gmail.com>

* Reverts grafana/grafana#100019

Signed-off-by: Maicon Costa <maiconscosta@gmail.com>

* Add unit test

Signed-off-by: Maicon Costa <maiconscosta@gmail.com>

* check error on unit test

Signed-off-by: Maicon Costa <maiconscosta@gmail.com>

* Add unit test

Signed-off-by: Maicon Costa <maiconscosta@gmail.com>

* Remove unused fields

Signed-off-by: Maicon Costa <maiconscosta@gmail.com>

* Add unit tests for folder_storage

Signed-off-by: Maicon Costa <maiconscosta@gmail.com>

* Remove duplicated import

Signed-off-by: Maicon Costa <maiconscosta@gmail.com>

* Fix unit test

Signed-off-by: Maicon Costa <maiconscosta@gmail.com>

---------

Signed-off-by: Maicon Costa <maiconscosta@gmail.com>
This commit is contained in:
maicon
2025-03-11 11:33:08 +00:00
committed by GitHub
parent 79d079b638
commit 927f7befd6
9 changed files with 485 additions and 65 deletions
+161
View File
@@ -0,0 +1,161 @@
package folders
import (
"context"
"fmt"
"k8s.io/apimachinery/pkg/apis/meta/internalversion"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/registry/rest"
claims "github.com/grafana/authlib/types"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/apimachinery/utils"
"github.com/grafana/grafana/pkg/apis/folder/v0alpha1"
grafanarest "github.com/grafana/grafana/pkg/apiserver/rest"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
"github.com/grafana/grafana/pkg/services/dashboards/dashboardaccess"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/setting"
)
var (
_ rest.Scoper = (*folderStorage)(nil)
_ rest.SingularNameProvider = (*folderStorage)(nil)
_ rest.Getter = (*folderStorage)(nil)
_ rest.Lister = (*folderStorage)(nil)
_ rest.Storage = (*folderStorage)(nil)
_ rest.Creater = (*folderStorage)(nil)
_ rest.Updater = (*folderStorage)(nil)
_ rest.GracefulDeleter = (*folderStorage)(nil)
)
type folderStorage struct {
tableConverter rest.TableConvertor
cfg *setting.Cfg
features featuremgmt.FeatureToggles
folderPermissionsSvc accesscontrol.FolderPermissionsService
store grafanarest.Storage
}
func (s *folderStorage) New() runtime.Object {
return resourceInfo.NewFunc()
}
func (s *folderStorage) Destroy() {}
func (s *folderStorage) NamespaceScoped() bool {
return true // namespace == org
}
func (s *folderStorage) GetSingularName() string {
return resourceInfo.GetSingularName()
}
func (s *folderStorage) NewList() runtime.Object {
return resourceInfo.NewListFunc()
}
func (s *folderStorage) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) {
return s.tableConverter.ConvertToTable(ctx, object, tableOptions)
}
func (s *folderStorage) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) {
return s.store.List(ctx, options)
}
func (s *folderStorage) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) {
return s.store.Get(ctx, name, options)
}
func (s *folderStorage) Create(ctx context.Context,
obj runtime.Object,
createValidation rest.ValidateObjectFunc,
options *metav1.CreateOptions,
) (runtime.Object, error) {
obj, err := s.store.Create(ctx, obj, createValidation, options)
if err != nil {
return nil, err
}
info, err := request.NamespaceInfoFrom(ctx, true)
if err != nil {
return nil, err
}
user, err := identity.GetRequester(ctx)
if err != nil {
return nil, err
}
p, ok := obj.(*v0alpha1.Folder)
if !ok {
return nil, fmt.Errorf("expected folder?")
}
accessor, err := utils.MetaAccessor(p)
if err != nil {
return nil, err
}
parentUid := accessor.GetFolder()
err = s.setDefaultFolderPermissions(ctx, info.OrgID, user, p.ObjectMeta.Name, parentUid)
if err != nil {
return nil, err
}
return obj, nil
}
func (s *folderStorage) Update(ctx context.Context,
name string,
objInfo rest.UpdatedObjectInfo,
createValidation rest.ValidateObjectFunc,
updateValidation rest.ValidateObjectUpdateFunc,
forceAllowCreate bool,
options *metav1.UpdateOptions,
) (runtime.Object, bool, error) {
return s.store.Update(ctx, name, objInfo, createValidation, updateValidation, forceAllowCreate, options)
}
// GracefulDeleter
func (s *folderStorage) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) {
return s.store.Delete(ctx, name, deleteValidation, options)
}
// GracefulDeleter
func (s *folderStorage) DeleteCollection(ctx context.Context, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions, listOptions *internalversion.ListOptions) (runtime.Object, error) {
return nil, fmt.Errorf("DeleteCollection for folders not implemented")
}
func (s *folderStorage) setDefaultFolderPermissions(ctx context.Context, orgID int64, user identity.Requester, uid string, parentUID string) error {
if !s.cfg.RBAC.PermissionsOnCreation("folder") {
return nil
}
var permissions []accesscontrol.SetResourcePermissionCommand
if user.IsIdentityType(claims.TypeUser) {
userID, err := user.GetInternalID()
if err != nil {
return err
}
permissions = append(permissions, accesscontrol.SetResourcePermissionCommand{
UserID: userID, Permission: dashboardaccess.PERMISSION_ADMIN.String(),
})
}
isNested := parentUID != ""
if !isNested || !s.features.IsEnabled(ctx, featuremgmt.FlagNestedFolders) {
permissions = append(permissions, []accesscontrol.SetResourcePermissionCommand{
{BuiltinRole: string(org.RoleEditor), Permission: dashboardaccess.PERMISSION_EDIT.String()},
{BuiltinRole: string(org.RoleViewer), Permission: dashboardaccess.PERMISSION_VIEW.String()},
}...)
}
_, err := s.folderPermissionsSvc.SetPermissions(ctx, orgID, uid, permissions...)
return err
}
@@ -0,0 +1,132 @@
package folders
import (
"context"
"testing"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/apis/folder/v0alpha1"
"github.com/grafana/grafana/pkg/services/accesscontrol"
acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/setting"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"gopkg.in/ini.v1"
"k8s.io/apimachinery/pkg/apis/meta/internalversion"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/apiserver/pkg/registry/rest"
)
func TestSetDefaultPermissionsWhenCreatingFolder(t *testing.T) {
type testCase struct {
description string
expectedCallsToSetPermissions int
}
tcs := []testCase{
{
description: "folder creation succeeds, via legacy storage",
expectedCallsToSetPermissions: 1,
},
}
for _, tc := range tcs {
t.Run(tc.description, func(t *testing.T) {
folderPermService := acmock.NewMockedPermissionsService()
folderPermService.On("SetPermissions", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return([]accesscontrol.ResourcePermission{}, nil)
cfg := setting.NewCfg()
f := ini.Empty()
f.Section("rbac").Key("resources_with_managed_permissions_on_creation").SetValue("folder")
tempCfg, err := setting.NewCfgFromINIFile(f)
require.NoError(t, err)
cfg.RBAC = tempCfg.RBAC
fs := folderStorage{
folderPermissionsSvc: folderPermService,
store: &fakeStorage{},
cfg: cfg,
}
obj := &v0alpha1.Folder{}
ctx := request.WithNamespace(context.Background(), "org-2")
ctx = identity.WithRequester(ctx, &user.SignedInUser{
UserID: 1,
})
out, err := fs.Create(ctx, obj, func(ctx context.Context,
obj runtime.Object) error {
return nil
},
&metav1.CreateOptions{})
require.NoError(t, err)
require.NotNil(t, out)
folderPermService.AssertNumberOfCalls(t, "SetPermissions", tc.expectedCallsToSetPermissions)
})
}
}
var (
_ rest.Scoper = (*fakeStorage)(nil)
_ rest.SingularNameProvider = (*fakeStorage)(nil)
_ rest.Getter = (*fakeStorage)(nil)
_ rest.Lister = (*fakeStorage)(nil)
_ rest.Storage = (*fakeStorage)(nil)
_ rest.Creater = (*fakeStorage)(nil)
_ rest.Updater = (*fakeStorage)(nil)
_ rest.GracefulDeleter = (*fakeStorage)(nil)
)
type fakeStorage struct{}
func (s *fakeStorage) New() runtime.Object {
return nil
}
func (s *fakeStorage) Destroy() {}
func (s *fakeStorage) NamespaceScoped() bool {
return true
}
func (s *fakeStorage) GetSingularName() string {
return ""
}
func (s *fakeStorage) NewList() runtime.Object {
return nil
}
func (s *fakeStorage) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) {
return nil, nil
}
func (s *fakeStorage) List(ctx context.Context, options *internalversion.ListOptions) (runtime.Object, error) {
return nil, nil
}
func (s *fakeStorage) Get(ctx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) {
return nil, nil
}
func (s *fakeStorage) Create(ctx context.Context, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) {
return obj, nil
}
func (s *fakeStorage) Update(ctx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc,
updateValidation rest.ValidateObjectUpdateFunc, forceAllowCreate bool, options *metav1.UpdateOptions) (runtime.Object, bool, error) {
return nil, false, nil
}
func (s *fakeStorage) Delete(ctx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) {
return nil, false, nil
}
func (s *fakeStorage) DeleteCollection(ctx context.Context, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions, listOptions *internalversion.ListOptions) (runtime.Object, error) {
return nil, nil
}
+5 -43
View File
@@ -10,18 +10,14 @@ import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/registry/rest"
claims "github.com/grafana/authlib/types"
"github.com/grafana/grafana/pkg/api/apierrors"
"github.com/grafana/grafana/pkg/apimachinery/identity"
"github.com/grafana/grafana/pkg/apimachinery/utils"
"github.com/grafana/grafana/pkg/apis/folder/v0alpha1"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/dashboards/dashboardaccess"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/folder"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/util"
)
@@ -38,12 +34,11 @@ var (
)
type legacyStorage struct {
service folder.Service
namespacer request.NamespaceMapper
tableConverter rest.TableConvertor
cfg *setting.Cfg
features featuremgmt.FeatureToggles
folderPermissionsSvc accesscontrol.FolderPermissionsService
service folder.Service
namespacer request.NamespaceMapper
tableConverter rest.TableConvertor
cfg *setting.Cfg
features featuremgmt.FeatureToggles
}
func (s *legacyStorage) New() runtime.Object {
@@ -200,11 +195,6 @@ func (s *legacyStorage) Create(ctx context.Context,
return nil, &statusErr
}
err = s.setDefaultFolderPermissions(ctx, info.OrgID, user, out)
if err != nil {
return nil, err
}
// #TODO can we directly convert instead of doing a Get? the result of the Create
// has more data than the one of Get so there is more we can include in the k8s resource
// this way
@@ -216,34 +206,6 @@ func (s *legacyStorage) Create(ctx context.Context,
return r, nil
}
func (s *legacyStorage) setDefaultFolderPermissions(ctx context.Context, orgID int64, user identity.Requester, folder *folder.Folder) error {
if !s.cfg.RBAC.PermissionsOnCreation("folder") {
return nil
}
var permissions []accesscontrol.SetResourcePermissionCommand
if user.IsIdentityType(claims.TypeUser) {
userID, err := user.GetInternalID()
if err != nil {
return err
}
permissions = append(permissions, accesscontrol.SetResourcePermissionCommand{
UserID: userID, Permission: dashboardaccess.PERMISSION_ADMIN.String(),
})
}
isNested := folder.ParentUID != ""
if !isNested || !s.features.IsEnabled(ctx, featuremgmt.FlagNestedFolders) {
permissions = append(permissions, []accesscontrol.SetResourcePermissionCommand{
{BuiltinRole: string(org.RoleEditor), Permission: dashboardaccess.PERMISSION_EDIT.String()},
{BuiltinRole: string(org.RoleViewer), Permission: dashboardaccess.PERMISSION_VIEW.String()},
}...)
}
_, err := s.folderPermissionsSvc.SetPermissions(ctx, orgID, folder.UID, permissions...)
return err
}
func (s *legacyStorage) Update(ctx context.Context,
name string,
objInfo rest.UpdatedObjectInfo,
+17 -7
View File
@@ -148,28 +148,38 @@ func (b *FolderAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.API
}
legacyStore := &legacyStorage{
service: b.folderSvc,
namespacer: b.namespacer,
service: b.folderSvc,
namespacer: b.namespacer,
tableConverter: resourceInfo.TableConverter(),
features: b.features,
cfg: b.cfg,
}
opts.StorageOptions(resourceInfo.GroupResource(), apistore.StorageOptions{
RequireDeprecatedInternalID: true})
folderStore := &folderStorage{
tableConverter: resourceInfo.TableConverter(),
folderPermissionsSvc: b.folderPermissionsSvc,
features: b.features,
cfg: b.cfg,
}
opts.StorageOptions(resourceInfo.GroupResource(), apistore.StorageOptions{
RequireDeprecatedInternalID: true})
storage[resourceInfo.StoragePath()] = legacyStore
if optsGetter != nil && dualWriteBuilder != nil {
store, err := grafanaregistry.NewRegistryStore(scheme, resourceInfo, optsGetter)
if err != nil {
return err
}
storage[resourceInfo.StoragePath()], err = dualWriteBuilder(resourceInfo.GroupResource(), legacyStore, store)
dw, err := dualWriteBuilder(resourceInfo.GroupResource(), legacyStore, store)
if err != nil {
return err
}
folderStore.store = dw
}
storage[resourceInfo.StoragePath()] = folderStore
storage[resourceInfo.StoragePath("parents")] = &subParentsREST{
getter: storage[resourceInfo.StoragePath()].(rest.Getter), // Get the parents
}