diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index b879716fb13..551e8be9fb3 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -1030,6 +1030,10 @@ export interface FeatureToggles { */ kubernetesAuthzResourcePermissionApis?: boolean; /** + * Enable sync of Zanzana authorization store on AuthZ CRD mutations + */ + kubernetesAuthzZanzanaSync?: boolean; + /** * Enables create, delete, and update mutations for resources owned by IAM identity */ kubernetesAuthnMutation?: boolean; diff --git a/pkg/registry/apis/iam/hooks.go b/pkg/registry/apis/iam/hooks.go new file mode 100644 index 00000000000..e8bdbc33515 --- /dev/null +++ b/pkg/registry/apis/iam/hooks.go @@ -0,0 +1,157 @@ +package iam + +import ( + "context" + "errors" + "fmt" + "strings" + "time" + + "google.golang.org/protobuf/types/known/structpb" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + + iamv0 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1" + v1 "github.com/grafana/grafana/pkg/services/authz/proto/v1" + "github.com/grafana/grafana/pkg/services/authz/zanzana" +) + +var ( + errEmptyName = errors.New("name cannot be empty") + errInvalidBasicRole = errors.New("invalid basic role") + errUnknownKind = errors.New("unknown permission kind") + + defaultWriteTimeout = 15 * time.Second +) + +func toZanzanaSubject(kind iamv0.ResourcePermissionSpecPermissionKind, name string) (string, error) { + if name == "" { + return "", errEmptyName + } + switch kind { + case iamv0.ResourcePermissionSpecPermissionKindUser: + return zanzana.NewTupleEntry(zanzana.TypeUser, name, ""), nil + case iamv0.ResourcePermissionSpecPermissionKindServiceAccount: + return zanzana.NewTupleEntry(zanzana.TypeServiceAccount, name, ""), nil + case iamv0.ResourcePermissionSpecPermissionKindTeam: + return zanzana.NewTupleEntry(zanzana.TypeTeam, name, ""), nil + case iamv0.ResourcePermissionSpecPermissionKindBasicRole: + basicRole := zanzana.TranslateBasicRole(name) + if basicRole == "" { + return "", fmt.Errorf("%w: %s", errInvalidBasicRole, name) + } + + // e.g role:basic_viewer#assignee + return zanzana.NewTupleEntry(zanzana.TypeRole, basicRole, zanzana.RelationAssignee), nil + } + + // should not happen since we are after create + // validation webhook should have caught invalid kinds + return "", errUnknownKind +} + +func toZanzanaType(apiGroup string) string { + if apiGroup == "folder.grafana.app" { + return zanzana.TypeFolder + } + return zanzana.TypeResource +} + +func NewResourceTuple(object string, resource iamv0.ResourcePermissionspecResource, perm iamv0.ResourcePermissionspecPermission) (*v1.TupleKey, error) { + // Typ is "folder" or "resource" + typ := toZanzanaType(resource.ApiGroup) + + // subject + subject, err := toZanzanaSubject(perm.Kind, perm.Name) + if err != nil { + return nil, err + } + + key := &v1.TupleKey{ + // e.g. "user:{uid}", "serviceaccount:{uid}", "team:{uid}", "basicrole:{viewer|editor|admin}" + User: subject, + // "view", "edit", "admin" + Relation: strings.ToLower(perm.Verb), + // e.g. "folder:{name}" or "resource:{apiGroup}/{resource}/{name}" + Object: object, + } + + // For resources we add a condition to filter by apiGroup/resource + // e.g "group_filter": {"group_resource": "dashboards.grafana.app/dashboards"} + if typ == zanzana.TypeResource { + key.Condition = &v1.RelationshipCondition{ + Name: "group_filter", + Context: &structpb.Struct{ + Fields: map[string]*structpb.Value{ + "group_resource": structpb.NewStringValue( + resource.ApiGroup + "/" + resource.Resource, + ), + }, + }, + } + } + + return key, nil +} + +// AfterResourcePermissionCreate is a post-create hook that writes the resource permission to Zanzana (openFGA) +func (b *IdentityAccessManagementAPIBuilder) AfterResourcePermissionCreate(obj runtime.Object, _ *metav1.CreateOptions) { + if b.zClient == nil { + return + } + + rp, ok := obj.(*iamv0.ResourcePermission) + if !ok { + return + } + + resource := rp.Spec.Resource + permissions := rp.Spec.Permissions + + object := zanzana.NewObjectEntry(toZanzanaType(resource.ApiGroup), resource.ApiGroup, resource.Resource, "", resource.Name) + + tuples := make([]*v1.TupleKey, 0, len(permissions)) + for _, p := range permissions { + tuple, err := NewResourceTuple(object, resource, p) + if err != nil { + b.logger.Error("failed to create resource permission tuple", + "namespace", rp.Namespace, + "object", object, + "err", err, + ) + + continue + } + tuples = append(tuples, tuple) + } + + // Avoid writing if there are no valid tuples + if len(tuples) == 0 { + b.logger.Warn("no valid tuples to write", "namespace", rp.Namespace, "resource", object) + return + } + + b.logger.Debug("writing resource permission to zanzana", + "namespace", rp.Namespace, + "object", object, + "tuplesCnt", len(tuples), + ) + + ctx, cancel := context.WithTimeout(context.Background(), defaultWriteTimeout) + defer cancel() + + err := b.zClient.Write(ctx, &v1.WriteRequest{ + Namespace: rp.Namespace, + Writes: &v1.WriteRequestWrites{ + TupleKeys: tuples, + }, + }) + if err != nil { + b.logger.Error("failed to write resource permission to zanzana", + "err", err, + "namespace", rp.Namespace, + "object", object, + "tuplesCnt", len(tuples), + ) + } +} diff --git a/pkg/registry/apis/iam/hooks_test.go b/pkg/registry/apis/iam/hooks_test.go new file mode 100644 index 00000000000..8c11f2faa8b --- /dev/null +++ b/pkg/registry/apis/iam/hooks_test.go @@ -0,0 +1,118 @@ +package iam + +import ( + "context" + "testing" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + iamv0 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1" + "github.com/grafana/grafana/pkg/infra/log" + v1 "github.com/grafana/grafana/pkg/services/authz/proto/v1" + "github.com/grafana/grafana/pkg/services/authz/zanzana" + "github.com/stretchr/testify/require" +) + +type FakeZanzanaClient struct { + zanzana.Client + writeCallback func(context.Context, *v1.WriteRequest) error +} + +// Write implements zanzana.Client. +func (f *FakeZanzanaClient) Write(ctx context.Context, req *v1.WriteRequest) error { + return f.writeCallback(ctx, req) +} + +func TestAfterResourcePermissionCreate(t *testing.T) { + b := &IdentityAccessManagementAPIBuilder{ + logger: log.NewNopLogger(), + } + t.Run("should create zanzana entries for folder resource permissions", func(t *testing.T) { + folderPerm := iamv0.ResourcePermission{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "org-2", + }, + Spec: iamv0.ResourcePermissionSpec{ + Resource: iamv0.ResourcePermissionspecResource{ + ApiGroup: "folder.grafana.app", Resource: "folders", Name: "fold1", + }, + Permissions: []iamv0.ResourcePermissionspecPermission{ + {Kind: iamv0.ResourcePermissionSpecPermissionKindUser, Name: "u1", Verb: "View"}, + {Kind: iamv0.ResourcePermissionSpecPermissionKindBasicRole, Name: "Editor", Verb: "Edit"}, + }, + }, + } + + testFolderEntries := func(ctx context.Context, req *v1.WriteRequest) error { + require.NotNil(t, req) + require.NotNil(t, req.Writes) + require.Len(t, req.Writes.TupleKeys, 2) + require.Equal(t, req.Namespace, "org-2") + require.Equal( + t, + req.Writes.TupleKeys[0], + &v1.TupleKey{User: "user:u1", Relation: "view", Object: "folder:fold1"}, + ) + require.Equal( + t, + req.Writes.TupleKeys[1], + &v1.TupleKey{User: "role:basic_editor#assignee", Relation: "edit", Object: "folder:fold1"}, + ) + return nil + } + + b.zClient = &FakeZanzanaClient{writeCallback: testFolderEntries} + b.AfterResourcePermissionCreate(&folderPerm, nil) + }) + + t.Run("should create zanzana entries for dashboard resource permissions", func(t *testing.T) { + dashPerm := iamv0.ResourcePermission{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "default", + }, + Spec: iamv0.ResourcePermissionSpec{ + Resource: iamv0.ResourcePermissionspecResource{ + ApiGroup: "dashboard.grafana.app", Resource: "dashboards", Name: "dash1", + }, + Permissions: []iamv0.ResourcePermissionspecPermission{ + {Kind: iamv0.ResourcePermissionSpecPermissionKindServiceAccount, Name: "sa1", Verb: "View"}, + {Kind: iamv0.ResourcePermissionSpecPermissionKindTeam, Name: "team1", Verb: "Edit"}, + }, + }, + } + + testDashEntries := func(ctx context.Context, req *v1.WriteRequest) error { + object := "resource:dashboard.grafana.app/dashboards/dash1" + + require.NotNil(t, req) + require.NotNil(t, req.Writes) + require.Len(t, req.Writes.TupleKeys, 2) + require.Equal(t, req.Namespace, "default") + + tuple1 := req.Writes.TupleKeys[0] + require.NotNil(t, tuple1.Condition) + require.Equal(t, "group_filter", tuple1.Condition.Name) + tuple1.Condition = nil + require.Equal( + t, + tuple1, + &v1.TupleKey{User: "service-account:sa1", Relation: "view", Object: object}, + ) + + tuple2 := req.Writes.TupleKeys[1] + require.NotNil(t, tuple2.Condition) + require.Equal(t, "group_filter", tuple2.Condition.Name) + tuple2.Condition = nil + require.Equal( + t, + tuple2, + &v1.TupleKey{User: "team:team1", Relation: "edit", Object: object}, + ) + + return nil + } + + b.zClient = &FakeZanzanaClient{writeCallback: testDashEntries} + b.AfterResourcePermissionCreate(&dashPerm, nil) + }) +} diff --git a/pkg/registry/apis/iam/models.go b/pkg/registry/apis/iam/models.go index 700a4d04d1c..49e1662263a 100644 --- a/pkg/registry/apis/iam/models.go +++ b/pkg/registry/apis/iam/models.go @@ -1,13 +1,17 @@ package iam import ( - "github.com/grafana/authlib/types" "github.com/prometheus/client_golang/prometheus" "k8s.io/apiserver/pkg/authorization/authorizer" + "github.com/grafana/authlib/types" + + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/registry/apis/iam/legacy" "github.com/grafana/grafana/pkg/registry/apis/iam/user" "github.com/grafana/grafana/pkg/services/apiserver/builder" + "github.com/grafana/grafana/pkg/services/authz/zanzana" + "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/ssosettings" "github.com/grafana/grafana/pkg/storage/unified/resource" ) @@ -44,8 +48,14 @@ type IdentityAccessManagementAPIBuilder struct { legacyAccessClient types.AccessClient // accessClient is used for the core role apis accessClient types.AccessClient + // zClient is used to populate Zanzana with: + // - roles + // - permissions + // - assignments + zClient zanzana.Client - reg prometheus.Registerer + reg prometheus.Registerer + logger log.Logger // non-k8s api route display *user.LegacyDisplayREST @@ -54,11 +64,7 @@ type IdentityAccessManagementAPIBuilder struct { sso ssosettings.Service // Toggle for enabling authz management apis - enableAuthZApis bool - enableResourcePermissionApis bool - - // Toggle for enabling authn mutation - enableAuthnMutation bool + features featuremgmt.FeatureToggles // Toggle for enabling dual writer enableDualWriter bool diff --git a/pkg/registry/apis/iam/register.go b/pkg/registry/apis/iam/register.go index 495db9cd2c4..8c9ad1caae7 100644 --- a/pkg/registry/apis/iam/register.go +++ b/pkg/registry/apis/iam/register.go @@ -13,6 +13,7 @@ import ( "k8s.io/apiserver/pkg/admission" "k8s.io/apiserver/pkg/authorization/authorizer" "k8s.io/apiserver/pkg/registry/generic" + "k8s.io/apiserver/pkg/registry/generic/registry" "k8s.io/apiserver/pkg/registry/rest" genericapiserver "k8s.io/apiserver/pkg/server" common "k8s.io/kube-openapi/pkg/common" @@ -26,8 +27,8 @@ import ( "github.com/grafana/grafana/pkg/apimachinery/utils" legacyiamv0 "github.com/grafana/grafana/pkg/apis/iam/v0alpha1" grafanaregistry "github.com/grafana/grafana/pkg/apiserver/registry/generic" - grafanarest "github.com/grafana/grafana/pkg/apiserver/rest" "github.com/grafana/grafana/pkg/infra/db" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/registry/apis/iam/legacy" "github.com/grafana/grafana/pkg/registry/apis/iam/resourcepermission" "github.com/grafana/grafana/pkg/registry/apis/iam/serviceaccount" @@ -37,6 +38,7 @@ import ( "github.com/grafana/grafana/pkg/services/accesscontrol" gfauthorizer "github.com/grafana/grafana/pkg/services/apiserver/auth/authorizer" "github.com/grafana/grafana/pkg/services/apiserver/builder" + "github.com/grafana/grafana/pkg/services/authz/zanzana" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/ssosettings" "github.com/grafana/grafana/pkg/storage/legacysql" @@ -51,6 +53,7 @@ func RegisterAPIService( sql db.DB, ac accesscontrol.AccessControl, accessClient types.AccessClient, + zClient zanzana.Client, reg prometheus.Registerer, coreRolesStorage CoreRoleStorageBackend, rolesStorage RoleStorageBackend, @@ -62,40 +65,45 @@ func RegisterAPIService( authorizer := newIAMAuthorizer(accessClient, legacyAccessClient) builder := &IdentityAccessManagementAPIBuilder{ - store: store, - coreRolesStorage: coreRolesStorage, - rolesStorage: rolesStorage, - resourcePermissionsStorage: resourcepermission.ProvideStorageBackend(dbProvider), - roleBindingsStorage: roleBindingsStorage, - sso: ssoService, - authorizer: authorizer, - legacyAccessClient: legacyAccessClient, - accessClient: accessClient, - display: user.NewLegacyDisplayREST(store), - reg: reg, - enableAuthZApis: features.IsEnabledGlobally(featuremgmt.FlagKubernetesAuthzApis), - enableResourcePermissionApis: features.IsEnabledGlobally(featuremgmt.FlagKubernetesAuthzResourcePermissionApis), - enableAuthnMutation: features.IsEnabledGlobally(featuremgmt.FlagKubernetesAuthnMutation), - enableDualWriter: true, + store: store, + coreRolesStorage: coreRolesStorage, + rolesStorage: rolesStorage, + resourcePermissionsStorage: resourcepermission.ProvideStorageBackend(dbProvider), + roleBindingsStorage: roleBindingsStorage, + sso: ssoService, + authorizer: authorizer, + legacyAccessClient: legacyAccessClient, + accessClient: accessClient, + zClient: zClient, + display: user.NewLegacyDisplayREST(store), + reg: reg, + logger: log.New("iam.apis"), + features: features, + // enableAuthZApis: features.IsEnabledGlobally(featuremgmt.FlagKubernetesAuthzApis), + // enableResourcePermissionApis: features.IsEnabledGlobally(featuremgmt.FlagKubernetesAuthzResourcePermissionApis), + // enableAuthnMutation: features.IsEnabledGlobally(featuremgmt.FlagKubernetesAuthnMutation), + enableDualWriter: true, } apiregistration.RegisterAPI(builder) return builder, nil } +// TODO zClient, reg func NewAPIService( accessClient types.AccessClient, dbProvider legacysql.LegacyDatabaseProvider, - enabledApis map[string]bool, + features featuremgmt.FeatureToggles, ) *IdentityAccessManagementAPIBuilder { store := legacy.NewLegacySQLStores(dbProvider) resourcePermissionsStorage := resourcepermission.ProvideStorageBackend(dbProvider) resourceAuthorizer := gfauthorizer.NewResourceAuthorizer(accessClient) return &IdentityAccessManagementAPIBuilder{ - store: store, - display: user.NewLegacyDisplayREST(store), - resourcePermissionsStorage: resourcePermissionsStorage, - enableResourcePermissionApis: enabledApis["resourcepermissions"], + store: store, + display: user.NewLegacyDisplayREST(store), + resourcePermissionsStorage: resourcePermissionsStorage, + logger: log.New("iam.apis"), + features: features, authorizer: authorizer.AuthorizerFunc( func(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) { // For now only authorize resourcepermissions resource @@ -120,12 +128,12 @@ func (b *IdentityAccessManagementAPIBuilder) GetGroupVersion() schema.GroupVersi } func (b *IdentityAccessManagementAPIBuilder) InstallSchema(scheme *runtime.Scheme) error { - if b.enableAuthZApis { + if b.features.IsEnabledGlobally(featuremgmt.FlagKubernetesAuthzApis) { if err := iamv0.AddAuthZKnownTypes(scheme); err != nil { return err } } - if b.enableResourcePermissionApis { + if b.features.IsEnabledGlobally(featuremgmt.FlagKubernetesAuthzResourcePermissionApis) { if err := iamv0.AddResourcePermissionKnownTypes(scheme, iamv0.SchemeGroupVersion); err != nil { return err } @@ -152,6 +160,8 @@ func (b *IdentityAccessManagementAPIBuilder) AllowedV0Alpha1Resources() []string func (b *IdentityAccessManagementAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *genericapiserver.APIGroupInfo, opts builder.APIGroupOptions) error { storage := map[string]rest.Storage{} + enableAuthnMutation := b.features.IsEnabledGlobally(featuremgmt.FlagKubernetesAuthnMutation) + enableZanzanaSync := b.features.IsEnabledGlobally(featuremgmt.FlagKubernetesAuthzZanzanaSync) // teams + users must have shorter names because they are often used as part of another name opts.StorageOptsRegister(iamv0.TeamResourceInfo.GroupResource(), apistore.StorageOptions{ @@ -162,7 +172,7 @@ func (b *IdentityAccessManagementAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *ge }) teamResource := iamv0.TeamResourceInfo - teamLegacyStore := team.NewLegacyStore(b.store, b.legacyAccessClient, b.enableAuthnMutation) + teamLegacyStore := team.NewLegacyStore(b.store, b.legacyAccessClient, enableAuthnMutation) storage[teamResource.StoragePath()] = teamLegacyStore storage[teamResource.StoragePath("members")] = team.NewLegacyTeamMemberREST(b.store) @@ -185,7 +195,7 @@ func (b *IdentityAccessManagementAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *ge // User store registration userResource := iamv0.UserResourceInfo - legacyStore := user.NewLegacyStore(b.store, b.accessClient, b.enableAuthnMutation) + legacyStore := user.NewLegacyStore(b.store, b.accessClient, enableAuthnMutation) storage[userResource.StoragePath()] = legacyStore if b.enableDualWriter { @@ -206,7 +216,7 @@ func (b *IdentityAccessManagementAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *ge // Service Accounts store registration serviceAccountResource := iamv0.ServiceAccountResourceInfo - saLegacyStore := serviceaccount.NewLegacyStore(b.store, b.accessClient, b.enableAuthnMutation) + saLegacyStore := serviceaccount.NewLegacyStore(b.store, b.accessClient, enableAuthnMutation) storage[serviceAccountResource.StoragePath()] = saLegacyStore if b.enableDualWriter { @@ -230,7 +240,7 @@ func (b *IdentityAccessManagementAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *ge storage[ssoResource.StoragePath()] = sso.NewLegacyStore(b.sso) } - if b.enableAuthZApis { + if b.features.IsEnabledGlobally(featuremgmt.FlagKubernetesAuthzApis) { // v0alpha1 coreRoleStore, err := NewLocalStore(iamv0.CoreRoleInfo, apiGroupInfo.Scheme, opts.OptsGetter, b.reg, b.accessClient, b.coreRolesStorage) if err != nil { @@ -251,11 +261,15 @@ func (b *IdentityAccessManagementAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *ge storage[iamv0.RoleBindingInfo.StoragePath()] = roleBindingStore } - if b.enableResourcePermissionApis { + if b.features.IsEnabledGlobally(featuremgmt.FlagKubernetesAuthzResourcePermissionApis) { resourcePermissionStore, err := NewLocalStore(iamv0.ResourcePermissionInfo, apiGroupInfo.Scheme, opts.OptsGetter, b.reg, b.accessClient, b.resourcePermissionsStorage) if err != nil { return err } + if enableZanzanaSync { + b.logger.Info("Enabling AfterCreate hook for ResourcePermission to sync to Zanzana") + resourcePermissionStore.AfterCreate = b.AfterResourcePermissionCreate + } storage[iamv0.ResourcePermissionInfo.StoragePath()] = resourcePermissionStore } @@ -404,7 +418,7 @@ func (b *IdentityAccessManagementAPIBuilder) Mutate(ctx context.Context, a admis } func NewLocalStore(resourceInfo utils.ResourceInfo, scheme *runtime.Scheme, defaultOptsGetter generic.RESTOptionsGetter, - reg prometheus.Registerer, ac types.AccessClient, storageBackend resource.StorageBackend) (grafanarest.Storage, error) { + reg prometheus.Registerer, ac types.AccessClient, storageBackend resource.StorageBackend) (*registry.Store, error) { server, err := resource.NewResourceServer(resource.ResourceServerOptions{ Backend: storageBackend, Reg: reg, diff --git a/pkg/server/wire_gen.go b/pkg/server/wire_gen.go index 17d6b55028b..cd020ea22f3 100644 --- a/pkg/server/wire_gen.go +++ b/pkg/server/wire_gen.go @@ -813,7 +813,7 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api } folderAPIBuilder := folders.RegisterAPIService(cfg, featureToggles, apiserverService, folderimplService, folderPermissionsService, accessControl, acimplService, accessClient, registerer, resourceClient, zanzanaClient) storageBackendImpl := noopstorage.ProvideStorageBackend() - identityAccessManagementAPIBuilder, err := iam.RegisterAPIService(featureToggles, apiserverService, ssosettingsimplService, sqlStore, accessControl, accessClient, registerer, storageBackendImpl, storageBackendImpl, storageBackendImpl) + identityAccessManagementAPIBuilder, err := iam.RegisterAPIService(featureToggles, apiserverService, ssosettingsimplService, sqlStore, accessControl, accessClient, zanzanaClient, registerer, storageBackendImpl, storageBackendImpl, storageBackendImpl) if err != nil { return nil, err } @@ -1417,7 +1417,7 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac } folderAPIBuilder := folders.RegisterAPIService(cfg, featureToggles, apiserverService, folderimplService, folderPermissionsService, accessControl, acimplService, accessClient, registerer, resourceClient, zanzanaClient) storageBackendImpl := noopstorage.ProvideStorageBackend() - identityAccessManagementAPIBuilder, err := iam.RegisterAPIService(featureToggles, apiserverService, ssosettingsimplService, sqlStore, accessControl, accessClient, registerer, storageBackendImpl, storageBackendImpl, storageBackendImpl) + identityAccessManagementAPIBuilder, err := iam.RegisterAPIService(featureToggles, apiserverService, ssosettingsimplService, sqlStore, accessControl, accessClient, zanzanaClient, registerer, storageBackendImpl, storageBackendImpl, storageBackendImpl) if err != nil { return nil, err } diff --git a/pkg/services/authz/zanzana/zanzana.go b/pkg/services/authz/zanzana/zanzana.go index d7d1b1dfd45..a63f581b9ca 100644 --- a/pkg/services/authz/zanzana/zanzana.go +++ b/pkg/services/authz/zanzana/zanzana.go @@ -81,6 +81,21 @@ func NewTupleEntry(objectType, name, relation string) string { return obj } +func NewObjectEntry(objectType, group, resource, subresource, name string) string { + if objectType == TypeFolder { + return TypeFolder + ":" + name + } + + obj := fmt.Sprintf("%s:%s/%s", objectType, group, resource) + if subresource != "" { + obj = fmt.Sprintf("%s/%s", obj, subresource) + } + if name != "" { + obj = fmt.Sprintf("%s/%s", obj, name) + } + return obj +} + func TranslateToResourceTuple(subject string, action, kind, name string) (*openfgav1.TupleKey, bool) { translation, ok := resourceTranslations[kind] diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 2ca2f9c6e63..3a9823098b0 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -1781,6 +1781,14 @@ var ( HideFromAdminPage: true, HideFromDocs: true, }, + { + Name: "kubernetesAuthzZanzanaSync", + Description: "Enable sync of Zanzana authorization store on AuthZ CRD mutations", + Stage: FeatureStageExperimental, + Owner: identityAccessTeam, + HideFromAdminPage: true, + HideFromDocs: true, + }, { Name: "kubernetesAuthnMutation", Description: "Enables create, delete, and update mutations for resources owned by IAM identity", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 0155877dfcb..691b5b23a48 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -231,6 +231,7 @@ alertingBulkActionsInUI,GA,@grafana/alerting-squad,false,false,true kubernetesAuthzApis,experimental,@grafana/identity-access-team,false,false,false kubernetesAuthZHandlerRedirect,experimental,@grafana/identity-access-team,false,false,false kubernetesAuthzResourcePermissionApis,experimental,@grafana/identity-access-team,false,false,false +kubernetesAuthzZanzanaSync,experimental,@grafana/identity-access-team,false,false,false kubernetesAuthnMutation,experimental,@grafana/identity-access-team,false,false,false restoreDashboards,experimental,@grafana/grafana-frontend-platform,false,false,false skipTokenRotationIfRecent,GA,@grafana/identity-access-team,false,false,false diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index b4d70df2eb3..beae6f8bd83 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -934,6 +934,10 @@ const ( // Registers AuthZ resource permission /apis endpoints FlagKubernetesAuthzResourcePermissionApis = "kubernetesAuthzResourcePermissionApis" + // FlagKubernetesAuthzZanzanaSync + // Enable sync of Zanzana authorization store on AuthZ CRD mutations + FlagKubernetesAuthzZanzanaSync = "kubernetesAuthzZanzanaSync" + // FlagKubernetesAuthnMutation // Enables create, delete, and update mutations for resources owned by IAM identity FlagKubernetesAuthnMutation = "kubernetesAuthnMutation" diff --git a/pkg/services/featuremgmt/toggles_gen.json b/pkg/services/featuremgmt/toggles_gen.json index e76de582bc9..19526aeed92 100644 --- a/pkg/services/featuremgmt/toggles_gen.json +++ b/pkg/services/featuremgmt/toggles_gen.json @@ -2171,6 +2171,23 @@ "hideFromDocs": true } }, + { + "metadata": { + "name": "kubernetesAuthzZanzanaSync", + "resourceVersion": "1758887751768", + "creationTimestamp": "2025-09-26T09:35:02Z", + "annotations": { + "grafana.app/updatedTimestamp": "2025-09-26 11:55:51.768754 +0000 UTC" + } + }, + "spec": { + "description": "Enable sync of Zanzana authorization store on AuthZ CRD mutations", + "stage": "experimental", + "codeowner": "@grafana/identity-access-team", + "hideFromAdminPage": true, + "hideFromDocs": true + } + }, { "metadata": { "name": "kubernetesCorrelations", @@ -2257,6 +2274,21 @@ "requiresRestart": true } }, + { + "metadata": { + "name": "kubernetesZanzanaPopulate", + "resourceVersion": "1758879077577", + "creationTimestamp": "2025-09-26T09:31:17Z", + "deletionTimestamp": "2025-09-26T09:35:02Z" + }, + "spec": { + "description": "Populate Zanzana on AuthZ CRDs creation or update", + "stage": "experimental", + "codeowner": "@grafana/identity-access-team", + "hideFromAdminPage": true, + "hideFromDocs": true + } + }, { "metadata": { "name": "kubernetesStars",