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:
Cory Forseth 2025-09-05 10:46:30 -05:00 committed by GitHub
parent d692303e76
commit 02227855e8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 210 additions and 2 deletions

View File

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

View File

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

View File

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

View File

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

View File

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