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
|
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)
|
user, err := toFolderTuple(parentUID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/apps/iam/pkg/reconcilers"
|
||||||
|
"github.com/grafana/grafana/pkg/services/authz/zanzana"
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
@ -51,6 +53,7 @@ type FolderAPIBuilder struct {
|
||||||
acService accesscontrol.Service
|
acService accesscontrol.Service
|
||||||
ac accesscontrol.AccessControl
|
ac accesscontrol.AccessControl
|
||||||
storage grafanarest.Storage
|
storage grafanarest.Storage
|
||||||
|
permissionStore reconcilers.PermissionStore
|
||||||
|
|
||||||
authorizer authorizer.Authorizer
|
authorizer authorizer.Authorizer
|
||||||
parents parentsGetter
|
parents parentsGetter
|
||||||
|
@ -69,6 +72,7 @@ func RegisterAPIService(cfg *setting.Cfg,
|
||||||
acService accesscontrol.Service,
|
acService accesscontrol.Service,
|
||||||
registerer prometheus.Registerer,
|
registerer prometheus.Registerer,
|
||||||
unified resource.ResourceClient,
|
unified resource.ResourceClient,
|
||||||
|
zanzanaClient zanzana.Client,
|
||||||
) *FolderAPIBuilder {
|
) *FolderAPIBuilder {
|
||||||
builder := &FolderAPIBuilder{
|
builder := &FolderAPIBuilder{
|
||||||
gv: resourceInfo.GroupVersion(),
|
gv: resourceInfo.GroupVersion(),
|
||||||
|
@ -81,6 +85,7 @@ func RegisterAPIService(cfg *setting.Cfg,
|
||||||
permissionsOnCreate: cfg.RBAC.PermissionsOnCreation("folder"),
|
permissionsOnCreate: cfg.RBAC.PermissionsOnCreation("folder"),
|
||||||
authorizer: newLegacyAuthorizer(accessControl),
|
authorizer: newLegacyAuthorizer(accessControl),
|
||||||
searcher: unified,
|
searcher: unified,
|
||||||
|
permissionStore: reconcilers.NewZanzanaPermissionStore(zanzanaClient),
|
||||||
}
|
}
|
||||||
apiregistration.RegisterAPI(builder)
|
apiregistration.RegisterAPI(builder)
|
||||||
return builder
|
return builder
|
||||||
|
@ -172,6 +177,11 @@ func (b *FolderAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.API
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if b.features.IsEnabledGlobally(featuremgmt.FlagZanzana) {
|
||||||
|
store.BeginCreate = b.beginCreate
|
||||||
|
store.BeginUpdate = b.beginUpdate
|
||||||
|
}
|
||||||
|
|
||||||
dw, err := dualWriteBuilder(resourceInfo.GroupResource(), legacyStore, store)
|
dw, err := dualWriteBuilder(resourceInfo.GroupResource(), legacyStore, store)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
|
@ -797,7 +797,7 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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()
|
storageBackendImpl := noopstorage.ProvideStorageBackend()
|
||||||
identityAccessManagementAPIBuilder, err := iam.RegisterAPIService(featureToggles, apiserverService, ssosettingsimplService, sqlStore, accessControl, accessClient, registerer, storageBackendImpl, storageBackendImpl)
|
identityAccessManagementAPIBuilder, err := iam.RegisterAPIService(featureToggles, apiserverService, ssosettingsimplService, sqlStore, accessControl, accessClient, registerer, storageBackendImpl, storageBackendImpl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -1382,7 +1382,7 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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()
|
storageBackendImpl := noopstorage.ProvideStorageBackend()
|
||||||
identityAccessManagementAPIBuilder, err := iam.RegisterAPIService(featureToggles, apiserverService, ssosettingsimplService, sqlStore, accessControl, accessClient, registerer, storageBackendImpl, storageBackendImpl)
|
identityAccessManagementAPIBuilder, err := iam.RegisterAPIService(featureToggles, apiserverService, ssosettingsimplService, sqlStore, accessControl, accessClient, registerer, storageBackendImpl, storageBackendImpl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
Loading…
Reference in New Issue