mirror of https://github.com/grafana/grafana.git
Authz: propagate folder changes to Zanzana (#110599)
* wire sync hooks for folder create/update * cleanup * add hook tests * fix nil context * better context
This commit is contained in:
parent
d692303e76
commit
02227855e8
|
@ -25,6 +25,11 @@ func (c *ZanzanaPermissionStore) SetFolderParent(ctx context.Context, namespace,
|
|||
return err
|
||||
}
|
||||
|
||||
if parentUID == "" {
|
||||
// Setting the parent to empty means the folder is at root which Zanzana doesn't care about.
|
||||
return nil
|
||||
}
|
||||
|
||||
user, err := toFolderTuple(parentUID)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
package folders
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apiserver/pkg/registry/generic/registry"
|
||||
|
||||
"github.com/grafana/grafana-app-sdk/logging"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
)
|
||||
|
||||
// "Almost nobody should use this hook" but we do because we need ctx and AfterCreate doesn't have it.
|
||||
func (b *FolderAPIBuilder) beginCreate(_ context.Context, obj runtime.Object, _ *metav1.CreateOptions) (registry.FinishFunc, error) {
|
||||
meta, err := utils.MetaAccessor(obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if meta.GetFolder() == "" {
|
||||
// Zanzana only cares about parent-child folder relationships; nothing to do if folder is at root.
|
||||
return func(ctx context.Context, success bool) {}, nil
|
||||
}
|
||||
|
||||
return func(ctx context.Context, success bool) {
|
||||
if success {
|
||||
b.writeFolderToZanzana(ctx, meta)
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
// "Almost nobody should use this hook" but we do because we need ctx and AfterUpdate doesn't have it.
|
||||
func (b *FolderAPIBuilder) beginUpdate(_ context.Context, obj runtime.Object, old runtime.Object, _ *metav1.UpdateOptions) (registry.FinishFunc, error) {
|
||||
updatedMeta, err := utils.MetaAccessor(obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
oldMeta, err := utils.MetaAccessor(old)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if updatedMeta.GetFolder() == oldMeta.GetFolder() {
|
||||
// No change to parent folder, nothing to do.
|
||||
return func(ctx context.Context, success bool) {}, nil
|
||||
}
|
||||
|
||||
return func(ctx context.Context, success bool) {
|
||||
if success {
|
||||
b.writeFolderToZanzana(ctx, updatedMeta)
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (b *FolderAPIBuilder) writeFolderToZanzana(ctx context.Context, folder utils.GrafanaMetaAccessor) {
|
||||
err := b.permissionStore.SetFolderParent(ctx, folder.GetNamespace(), folder.GetName(), folder.GetFolder())
|
||||
if err != nil {
|
||||
logging.FromContext(ctx).Warn("failed to propagate folder to zanzana", "err", err)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,132 @@
|
|||
package folders
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/apps/iam/pkg/reconcilers"
|
||||
"github.com/grafana/grafana/pkg/services/folder"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
)
|
||||
|
||||
func TestFolderSyncHooks_Create(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
expectedCallsToZanzana int
|
||||
folder runtime.Object
|
||||
updateSuccessful bool
|
||||
}{
|
||||
{
|
||||
name: "folder at root does nothing",
|
||||
expectedCallsToZanzana: 0,
|
||||
folder: getFolderObj("foo", ""),
|
||||
updateSuccessful: true,
|
||||
},
|
||||
{
|
||||
name: "unsuccessful create does nothing",
|
||||
expectedCallsToZanzana: 0,
|
||||
folder: getFolderObj("foo", "bar"),
|
||||
updateSuccessful: false,
|
||||
},
|
||||
{
|
||||
name: "successful create writes to zanzana",
|
||||
expectedCallsToZanzana: 1,
|
||||
folder: getFolderObj("foo", "bar"),
|
||||
updateSuccessful: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
storeMock := newMockStore()
|
||||
b := &FolderAPIBuilder{
|
||||
permissionStore: storeMock,
|
||||
}
|
||||
|
||||
f, err := b.beginCreate(context.Background(), tt.folder, nil)
|
||||
if err != nil {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
f(nil, tt.updateSuccessful)
|
||||
|
||||
storeMock.AssertNumberOfCalls(t, "SetFolderParent", tt.expectedCallsToZanzana)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFolderSyncHooks_Update(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
expectedCallsToZanzana int
|
||||
newFolder runtime.Object
|
||||
oldFolder runtime.Object
|
||||
updateSuccessful bool
|
||||
}{
|
||||
{
|
||||
name: "no folder change does nothing",
|
||||
expectedCallsToZanzana: 0,
|
||||
oldFolder: getFolderObj("foo", "bar"),
|
||||
newFolder: getFolderObj("foo", "bar"),
|
||||
updateSuccessful: true,
|
||||
},
|
||||
{
|
||||
name: "unsuccessful update does nothing",
|
||||
expectedCallsToZanzana: 0,
|
||||
oldFolder: getFolderObj("foo", "bar"),
|
||||
newFolder: getFolderObj("foo", "hop"),
|
||||
updateSuccessful: false,
|
||||
},
|
||||
{
|
||||
name: "successful update writes to zanzana",
|
||||
expectedCallsToZanzana: 1,
|
||||
oldFolder: getFolderObj("foo", "bar"),
|
||||
newFolder: getFolderObj("foo", "hop"),
|
||||
updateSuccessful: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
storeMock := newMockStore()
|
||||
b := &FolderAPIBuilder{
|
||||
permissionStore: storeMock,
|
||||
}
|
||||
|
||||
f, err := b.beginUpdate(context.Background(), tt.newFolder, tt.oldFolder, nil)
|
||||
if err != nil {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
f(nil, tt.updateSuccessful)
|
||||
|
||||
storeMock.AssertNumberOfCalls(t, "SetFolderParent", tt.expectedCallsToZanzana)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func getFolderObj(uid, parentUid string) runtime.Object {
|
||||
f, _ := LegacyCreateCommandToUnstructured(&folder.CreateFolderCommand{
|
||||
UID: uid,
|
||||
ParentUID: parentUid,
|
||||
})
|
||||
return f
|
||||
}
|
||||
|
||||
func newMockStore() *mockZanzanaPermissionStore {
|
||||
store := mockZanzanaPermissionStore{}
|
||||
store.On("SetFolderParent", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil)
|
||||
return &store
|
||||
}
|
||||
|
||||
type mockZanzanaPermissionStore struct {
|
||||
mock.Mock
|
||||
reconcilers.PermissionStore
|
||||
}
|
||||
|
||||
func (m *mockZanzanaPermissionStore) SetFolderParent(_ context.Context, _, _, _ string) error {
|
||||
m.Called()
|
||||
return nil
|
||||
}
|
|
@ -6,6 +6,8 @@ import (
|
|||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana/apps/iam/pkg/reconcilers"
|
||||
"github.com/grafana/grafana/pkg/services/authz/zanzana"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
|
@ -51,6 +53,7 @@ type FolderAPIBuilder struct {
|
|||
acService accesscontrol.Service
|
||||
ac accesscontrol.AccessControl
|
||||
storage grafanarest.Storage
|
||||
permissionStore reconcilers.PermissionStore
|
||||
|
||||
authorizer authorizer.Authorizer
|
||||
parents parentsGetter
|
||||
|
@ -69,6 +72,7 @@ func RegisterAPIService(cfg *setting.Cfg,
|
|||
acService accesscontrol.Service,
|
||||
registerer prometheus.Registerer,
|
||||
unified resource.ResourceClient,
|
||||
zanzanaClient zanzana.Client,
|
||||
) *FolderAPIBuilder {
|
||||
builder := &FolderAPIBuilder{
|
||||
gv: resourceInfo.GroupVersion(),
|
||||
|
@ -81,6 +85,7 @@ func RegisterAPIService(cfg *setting.Cfg,
|
|||
permissionsOnCreate: cfg.RBAC.PermissionsOnCreation("folder"),
|
||||
authorizer: newLegacyAuthorizer(accessControl),
|
||||
searcher: unified,
|
||||
permissionStore: reconcilers.NewZanzanaPermissionStore(zanzanaClient),
|
||||
}
|
||||
apiregistration.RegisterAPI(builder)
|
||||
return builder
|
||||
|
@ -172,6 +177,11 @@ func (b *FolderAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.API
|
|||
return err
|
||||
}
|
||||
|
||||
if b.features.IsEnabledGlobally(featuremgmt.FlagZanzana) {
|
||||
store.BeginCreate = b.beginCreate
|
||||
store.BeginUpdate = b.beginUpdate
|
||||
}
|
||||
|
||||
dw, err := dualWriteBuilder(resourceInfo.GroupResource(), legacyStore, store)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
|
@ -797,7 +797,7 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
folderAPIBuilder := folders.RegisterAPIService(cfg, featureToggles, apiserverService, folderimplService, folderPermissionsService, accessControl, acimplService, registerer, resourceClient)
|
||||
folderAPIBuilder := folders.RegisterAPIService(cfg, featureToggles, apiserverService, folderimplService, folderPermissionsService, accessControl, acimplService, registerer, resourceClient, zanzanaClient)
|
||||
storageBackendImpl := noopstorage.ProvideStorageBackend()
|
||||
identityAccessManagementAPIBuilder, err := iam.RegisterAPIService(featureToggles, apiserverService, ssosettingsimplService, sqlStore, accessControl, accessClient, registerer, storageBackendImpl, storageBackendImpl)
|
||||
if err != nil {
|
||||
|
@ -1382,7 +1382,7 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
folderAPIBuilder := folders.RegisterAPIService(cfg, featureToggles, apiserverService, folderimplService, folderPermissionsService, accessControl, acimplService, registerer, resourceClient)
|
||||
folderAPIBuilder := folders.RegisterAPIService(cfg, featureToggles, apiserverService, folderimplService, folderPermissionsService, accessControl, acimplService, registerer, resourceClient, zanzanaClient)
|
||||
storageBackendImpl := noopstorage.ProvideStorageBackend()
|
||||
identityAccessManagementAPIBuilder, err := iam.RegisterAPIService(featureToggles, apiserverService, ssosettingsimplService, sqlStore, accessControl, accessClient, registerer, storageBackendImpl, storageBackendImpl)
|
||||
if err != nil {
|
||||
|
|
Loading…
Reference in New Issue