grafana-iam: Populate Zanzana on resource permission creation (#111654)

* `grafana-iam`: Populate Zanzana on resource permission creation

* use zanzana const

* Adding a toggle

* Add a new feature toggle to manage zanzana sync

* wire

* wire

* WIP

* Fix hook issue

* comments and tests

* Account for PR feedback

* Add a timeout to writes

* Check tuples len

* comment

* validate basic role

* shorter error

* object reads better than entry
This commit is contained in:
Gabriel MABILLE
2025-10-13 21:37:13 +02:00
committed by GitHub
parent 6c726cf0ea
commit 0e34164329
11 changed files with 397 additions and 38 deletions
@@ -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;
+157
View File
@@ -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),
)
}
}
+118
View File
@@ -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)
})
}
+13 -7
View File
@@ -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
+43 -29
View File
@@ -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,
+2 -2
View File
@@ -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
}
+15
View File
@@ -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]
+8
View File
@@ -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",
+1
View File
@@ -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
1 Name Stage Owner requiresDevMode RequiresRestart FrontendOnly
231 kubernetesAuthzApis experimental @grafana/identity-access-team false false false
232 kubernetesAuthZHandlerRedirect experimental @grafana/identity-access-team false false false
233 kubernetesAuthzResourcePermissionApis experimental @grafana/identity-access-team false false false
234 kubernetesAuthzZanzanaSync experimental @grafana/identity-access-team false false false
235 kubernetesAuthnMutation experimental @grafana/identity-access-team false false false
236 restoreDashboards experimental @grafana/grafana-frontend-platform false false false
237 skipTokenRotationIfRecent GA @grafana/identity-access-team false false false
+4
View File
@@ -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"
+32
View File
@@ -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",