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:
+5
-2
@@ -199,8 +199,11 @@ func (hs *HTTPServer) CreateFolder(c *contextmodel.ReqContext) response.Response
|
||||
return apierrors.ToFolderErrorResponse(err)
|
||||
}
|
||||
|
||||
if err := hs.setDefaultFolderPermissions(c.Req.Context(), cmd.OrgID, cmd.SignedInUser, folder); err != nil {
|
||||
hs.log.Error("Could not set the default folder permissions", "folder", folder.Title, "user", cmd.SignedInUser, "error", err)
|
||||
// Only set default permissions if the Folder API Server is disabled.
|
||||
if !hs.Features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) {
|
||||
if err := hs.setDefaultFolderPermissions(c.Req.Context(), cmd.OrgID, cmd.SignedInUser, folder); err != nil {
|
||||
hs.log.Error("Could not set the default folder permissions", "folder", folder.Title, "user", cmd.SignedInUser, "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Clear permission cache for the user who's created the folder, so that new permissions are fetched for their next call
|
||||
|
||||
@@ -677,3 +677,94 @@ func TestGetFolderLegacyAndUnifiedStorage(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestSetDefaultPermissionsWhenCreatingFolder(t *testing.T) {
|
||||
folderService := &foldertest.FakeService{}
|
||||
setUpRBACGuardian(t)
|
||||
folderWithoutParentInput := "{ \"uid\": \"uid\", \"title\": \"Folder\"}"
|
||||
|
||||
type testCase struct {
|
||||
description string
|
||||
expectedCallsToSetPermissions int
|
||||
expectedCode int
|
||||
expectedFolder *folder.Folder
|
||||
permissions []accesscontrol.Permission
|
||||
featuresArr []any
|
||||
input string
|
||||
}
|
||||
|
||||
tcs := []testCase{
|
||||
{
|
||||
description: "folder creation succeeds, via legacy storage",
|
||||
expectedCallsToSetPermissions: 1,
|
||||
input: folderWithoutParentInput,
|
||||
expectedCode: http.StatusOK,
|
||||
expectedFolder: &folder.Folder{UID: "uid", Title: "Folder"},
|
||||
permissions: []accesscontrol.Permission{{Action: dashboards.ActionFoldersCreate}},
|
||||
},
|
||||
{
|
||||
description: "folder creation succeeds, via API Server",
|
||||
expectedCallsToSetPermissions: 0,
|
||||
input: folderWithoutParentInput,
|
||||
expectedCode: http.StatusOK,
|
||||
expectedFolder: &folder.Folder{UID: "uid", Title: "Folder"},
|
||||
permissions: []accesscontrol.Permission{{Action: dashboards.ActionFoldersCreate}},
|
||||
featuresArr: []any{featuremgmt.FlagKubernetesClientDashboardsFolders},
|
||||
},
|
||||
}
|
||||
|
||||
// we need to save these values because they are defined at `setting` package level
|
||||
// and modified when we invoke setting.NewCfgFromINIFile
|
||||
prevCookieSameSiteDisabled := setting.CookieSameSiteDisabled
|
||||
prevCookieSameSiteMode := setting.CookieSameSiteMode
|
||||
|
||||
cfg := setting.NewCfg()
|
||||
cfg.Raw.Section("rbac").Key("resources_with_managed_permissions_on_creation").SetValue("folder")
|
||||
tmpCfg, err := setting.NewCfgFromINIFile(cfg.Raw)
|
||||
require.NoError(t, err)
|
||||
cfg.RBAC = tmpCfg.RBAC
|
||||
|
||||
// restore previous values so other tests don't break
|
||||
// ex: TestHTTPServer_RotateUserAuthToken
|
||||
setting.CookieSameSiteDisabled = prevCookieSameSiteDisabled
|
||||
setting.CookieSameSiteMode = prevCookieSameSiteMode
|
||||
|
||||
for _, tc := range tcs {
|
||||
t.Run(tc.description, func(t *testing.T) {
|
||||
folderService.ExpectedFolder = tc.expectedFolder
|
||||
folderPermService := acmock.NewMockedPermissionsService()
|
||||
folderPermService.On("SetPermissions", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return([]accesscontrol.ResourcePermission{}, nil)
|
||||
|
||||
srv := SetupAPITestServer(t, func(hs *HTTPServer) {
|
||||
hs.Cfg = cfg
|
||||
|
||||
featuresArr := append(tc.featuresArr, featuremgmt.FlagNestedFolders)
|
||||
hs.Features = featuremgmt.WithFeatures(
|
||||
featuresArr...,
|
||||
)
|
||||
hs.folderService = folderService
|
||||
hs.folderPermissionsService = folderPermService
|
||||
hs.accesscontrolService = actest.FakeService{}
|
||||
})
|
||||
|
||||
input := strings.NewReader(tc.input)
|
||||
req := srv.NewPostRequest("/api/folders", input)
|
||||
req = webtest.RequestWithSignedInUser(req, userWithPermissions(1, tc.permissions))
|
||||
resp, err := srv.SendJSON(req)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tc.expectedCode, resp.StatusCode)
|
||||
|
||||
folder := dtos.Folder{}
|
||||
err = json.NewDecoder(resp.Body).Decode(&folder)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, resp.Body.Close())
|
||||
|
||||
folderPermService.AssertNumberOfCalls(t, "SetPermissions", tc.expectedCallsToSetPermissions)
|
||||
|
||||
if tc.expectedCode == http.StatusOK {
|
||||
assert.Equal(t, "uid", folder.UID)
|
||||
assert.Equal(t, "Folder", folder.Title)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package ossaccesscontrol
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/routing"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
@@ -107,17 +106,6 @@ func ProvideFolderPermissions(
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
// if the folder is not found, this may be on the create path,
|
||||
// where the write path to legacy will then go through the read
|
||||
// path and try to read from both legacy & unified before it exists on both
|
||||
if features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) && errors.Is(err, dashboards.ErrFolderNotFound) {
|
||||
_, err = folderService.GetLegacy(ctx, &folder.GetFolderQuery{
|
||||
UID: &resourceID,
|
||||
OrgID: orgID,
|
||||
SignedInUser: ident,
|
||||
})
|
||||
return err
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -719,7 +719,10 @@ func (dr *DashboardServiceImpl) SaveFolderForProvisionedDashboards(ctx context.C
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dr.setDefaultFolderPermissions(ctx, dto, f, true)
|
||||
// Only set default permissions if the Folder API Server is disabled.
|
||||
if !dr.features.IsEnabledGlobally(featuremgmt.FlagKubernetesClientDashboardsFolders) {
|
||||
dr.setDefaultFolderPermissions(ctx, dto, f, true)
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,8 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
"gopkg.in/ini.v1"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
|
||||
@@ -18,6 +20,8 @@ import (
|
||||
dashboardv0alpha1 "github.com/grafana/grafana/pkg/apis/dashboard/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/client"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
@@ -1112,6 +1116,72 @@ func TestGetDashboardsByPluginID(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestSetDefaultPermissionsWhenSavingFolderForProvisionedDashboards(t *testing.T) {
|
||||
fakeStore := dashboards.FakeDashboardStore{}
|
||||
defer fakeStore.AssertExpectations(t)
|
||||
|
||||
type testCase struct {
|
||||
description string
|
||||
expectedCallsToSetPermissions int
|
||||
featuresArr []any
|
||||
}
|
||||
|
||||
tcs := []testCase{
|
||||
{
|
||||
description: "folder creation succeeds, via legacy storage",
|
||||
expectedCallsToSetPermissions: 1,
|
||||
},
|
||||
{
|
||||
description: "folder creation succeeds, via API Server",
|
||||
expectedCallsToSetPermissions: 0,
|
||||
featuresArr: []any{featuremgmt.FlagKubernetesClientDashboardsFolders},
|
||||
},
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
service := &DashboardServiceImpl{
|
||||
cfg: cfg,
|
||||
dashboardStore: &fakeStore,
|
||||
folderPermissions: folderPermService,
|
||||
folderService: &foldertest.FakeService{
|
||||
ExpectedFolder: &folder.Folder{
|
||||
ID: 0,
|
||||
UID: "general",
|
||||
},
|
||||
},
|
||||
log: log.NewNopLogger(),
|
||||
}
|
||||
|
||||
origNewDashboardGuardian := guardian.New
|
||||
defer func() { guardian.New = origNewDashboardGuardian }()
|
||||
guardian.MockDashboardGuardian(&guardian.FakeDashboardGuardian{CanSaveValue: true})
|
||||
|
||||
cmd := &folder.CreateFolderCommand{
|
||||
Title: "foo",
|
||||
OrgID: 1,
|
||||
}
|
||||
|
||||
service.features = featuremgmt.WithFeatures(tc.featuresArr...)
|
||||
folder, err := service.SaveFolderForProvisionedDashboards(context.Background(), cmd)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, folder)
|
||||
|
||||
folderPermService.AssertNumberOfCalls(t, "SetPermissions", tc.expectedCallsToSetPermissions)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSaveProvisionedDashboard(t *testing.T) {
|
||||
fakeStore := dashboards.FakeDashboardStore{}
|
||||
defer fakeStore.AssertExpectations(t)
|
||||
|
||||
Reference in New Issue
Block a user