From 80fc87339a6f93f7348a6abc2b7210540bbb1f3d Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Thu, 27 Nov 2025 15:11:34 +0100 Subject: [PATCH] Zanzana: Role binding hooks (#114470) * Zanzana: Role bindings hooks WIP * Empty hooks for role bindings * implement hooks for role bindings * add tests * apply review suggestions --- pkg/registry/apis/iam/register.go | 6 + pkg/registry/apis/iam/role_binding_hooks.go | 302 ++++++++++++ .../apis/iam/role_binding_hooks_test.go | 448 ++++++++++++++++++ 3 files changed, 756 insertions(+) create mode 100644 pkg/registry/apis/iam/role_binding_hooks.go create mode 100644 pkg/registry/apis/iam/role_binding_hooks_test.go diff --git a/pkg/registry/apis/iam/register.go b/pkg/registry/apis/iam/register.go index 99c9dda7d8d..2417c84aed8 100644 --- a/pkg/registry/apis/iam/register.go +++ b/pkg/registry/apis/iam/register.go @@ -346,6 +346,12 @@ func (b *IdentityAccessManagementAPIBuilder) UpdateAPIGroupInfo(apiGroupInfo *ge if err != nil { return err } + if enableZanzanaSync { + b.logger.Info("Enabling hooks for RoleBinding to sync to Zanzana") + roleBindingStore.AfterCreate = b.AfterRoleBindingCreate + roleBindingStore.AfterDelete = b.AfterRoleBindingDelete + roleBindingStore.BeginUpdate = b.BeginRoleBindingUpdate + } storage[iamv0.RoleBindingInfo.StoragePath()] = roleBindingStore } //nolint:staticcheck // not yet migrated to OpenFeature diff --git a/pkg/registry/apis/iam/role_binding_hooks.go b/pkg/registry/apis/iam/role_binding_hooks.go new file mode 100644 index 00000000000..c88c46976eb --- /dev/null +++ b/pkg/registry/apis/iam/role_binding_hooks.go @@ -0,0 +1,302 @@ +package iam + +import ( + "context" + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apiserver/pkg/registry/generic/registry" + + iamv0 "github.com/grafana/grafana/apps/iam/pkg/apis/iam/v0alpha1" + v1 "github.com/grafana/grafana/pkg/services/authz/proto/v1" +) + +const resourceType = "rolebinding" + +// AfterRoleBindingCreate is a post-create hook that writes the role binding to Zanzana (openFGA) +func (b *IdentityAccessManagementAPIBuilder) AfterRoleBindingCreate(obj runtime.Object, _ *metav1.CreateOptions) { + if b.zClient == nil { + return + } + + rb, ok := obj.(*iamv0.RoleBinding) + if !ok { + b.logger.Error("failed to convert object to RoleBinding type", "object", obj) + return + } + + operation := "create" + + // Grab a ticket to write to Zanzana + // This limits the amount of concurrent connections to Zanzana + wait := time.Now() + b.zTickets <- true + hooksWaitHistogram.WithLabelValues(resourceType, operation).Observe(time.Since(wait).Seconds()) + + go func(rb *iamv0.RoleBinding) { + start := time.Now() + status := "success" + + defer func() { + // Release the ticket after write is done + <-b.zTickets + // Record operation duration and count + hooksDurationHistogram.WithLabelValues(resourceType, operation, status).Observe(time.Since(start).Seconds()) + }() + + b.logger.Debug("writing role binding to zanzana", + "namespace", rb.Namespace, + "name", rb.Name, + "subject", rb.Spec.Subject.Name, + "roleRefs", rb.Spec.RoleRefs, + ) + + ctx, cancel := context.WithTimeout(context.Background(), defaultWriteTimeout) + defer cancel() + + operations := make([]*v1.MutateOperation, 0, len(rb.Spec.RoleRefs)) + for _, roleRef := range rb.Spec.RoleRefs { + operations = append(operations, &v1.MutateOperation{ + Operation: &v1.MutateOperation_CreateRoleBinding{ + CreateRoleBinding: &v1.CreateRoleBindingOperation{ + SubjectKind: string(rb.Spec.Subject.Kind), + SubjectName: rb.Spec.Subject.Name, + RoleKind: string(roleRef.Kind), + RoleName: roleRef.Name, + }, + }, + }) + } + + if len(operations) == 0 { + return + } + + err := b.zClient.Mutate(ctx, &v1.MutateRequest{ + Namespace: rb.Namespace, + Operations: operations, + }) + + if err != nil { + status = "failure" + b.logger.Error("failed to write role binding to zanzana", + "err", err, + "namespace", rb.Namespace, + "name", rb.Name, + "subject", rb.Spec.Subject.Name, + "roleRefs", rb.Spec.RoleRefs, + ) + } + }(rb.DeepCopy()) // Pass a copy of the object +} + +// AfterRoleBindingDelete is a post-delete hook that removes the role binding from Zanzana (openFGA) +func (b *IdentityAccessManagementAPIBuilder) AfterRoleBindingDelete(obj runtime.Object, _ *metav1.DeleteOptions) { + if b.zClient == nil { + return + } + + rb, ok := obj.(*iamv0.RoleBinding) + if !ok { + b.logger.Error("failed to convert object to RoleBinding type", "object", obj) + return + } + + operation := "delete" + + // Grab a ticket to write to Zanzana + // This limits the amount of concurrent connections to Zanzana + wait := time.Now() + b.zTickets <- true + hooksWaitHistogram.WithLabelValues(resourceType, operation).Observe(time.Since(wait).Seconds()) + + go func(rb *iamv0.RoleBinding) { + start := time.Now() + status := "success" + + defer func() { + // Release the ticket after write is done + <-b.zTickets + // Record operation duration and count + hooksDurationHistogram.WithLabelValues(resourceType, operation, status).Observe(time.Since(start).Seconds()) + }() + + b.logger.Debug("deleting role binding from zanzana", + "namespace", rb.Namespace, + "name", rb.Name, + "subject", rb.Spec.Subject.Name, + "roleRefs", rb.Spec.RoleRefs, + ) + + ctx, cancel := context.WithTimeout(context.Background(), defaultWriteTimeout) + defer cancel() + + operations := make([]*v1.MutateOperation, 0, len(rb.Spec.RoleRefs)) + for _, roleRef := range rb.Spec.RoleRefs { + operations = append(operations, &v1.MutateOperation{ + Operation: &v1.MutateOperation_DeleteRoleBinding{ + DeleteRoleBinding: &v1.DeleteRoleBindingOperation{ + SubjectKind: string(rb.Spec.Subject.Kind), + SubjectName: rb.Spec.Subject.Name, + RoleKind: string(roleRef.Kind), + RoleName: roleRef.Name, + }, + }, + }) + } + + if len(operations) == 0 { + return + } + + err := b.zClient.Mutate(ctx, &v1.MutateRequest{ + Namespace: rb.Namespace, + Operations: operations, + }) + + if err != nil { + status = "failure" + b.logger.Error("failed to delete role binding from zanzana", + "err", err, + "namespace", rb.Namespace, + "name", rb.Name, + "subject", rb.Spec.Subject.Name, + "roleRefs", rb.Spec.RoleRefs, + ) + } + }(rb.DeepCopy()) // Pass a copy of the object +} + +// BeginRoleBindingUpdate is a pre-update hook that prepares zanzana updates. +// It performs the zanzana write after K8s update succeeds. +func (b *IdentityAccessManagementAPIBuilder) BeginRoleBindingUpdate(ctx context.Context, obj, oldObj runtime.Object, options *metav1.UpdateOptions) (registry.FinishFunc, error) { + if b.zClient == nil { + return nil, nil + } + + // Extract role bindings from both old and new objects + oldRB, ok := oldObj.(*iamv0.RoleBinding) + if !ok { + return nil, nil + } + + newRB, ok := obj.(*iamv0.RoleBinding) + if !ok { + return nil, nil + } + + if oldRB.Spec.Subject.Name == newRB.Spec.Subject.Name && roleRefsEqual(oldRB.Spec.RoleRefs, newRB.Spec.RoleRefs) { + return nil, nil // No changes to the role binding + } + + if newRB.Spec.Subject.Name == "" { + b.logger.Error("invalid role binding", + "namespace", newRB.Namespace, + "name", newRB.Name, + "subject", newRB.Spec.Subject.Name, + "roleRefs", newRB.Spec.RoleRefs, + ) + return nil, nil + } + + // Return a finish function that performs the zanzana write only on success + return func(ctx context.Context, success bool) { + if !success { + return + } + + wait := time.Now() + b.zTickets <- true + hooksWaitHistogram.WithLabelValues(resourceType, "update").Observe(time.Since(wait).Seconds()) + + go func() { + start := time.Now() + status := "success" + + defer func() { + <-b.zTickets + // Record operation duration and count + hooksDurationHistogram.WithLabelValues(resourceType, "update", status).Observe(time.Since(start).Seconds()) + }() + + b.logger.Debug("updating role binding in zanzana", + "namespace", newRB.Namespace, + "name", newRB.Name, + "oldSubject", oldRB.Spec.Subject.Name, + "newSubject", newRB.Spec.Subject.Name, + "oldRoleRefs", oldRB.Spec.RoleRefs, + "newRoleRefs", newRB.Spec.RoleRefs, + ) + + ctx, cancel := context.WithTimeout(context.Background(), defaultWriteTimeout) + defer cancel() + + operations := make([]*v1.MutateOperation, 0, len(oldRB.Spec.RoleRefs)) + for _, roleRef := range oldRB.Spec.RoleRefs { + operations = append(operations, &v1.MutateOperation{ + Operation: &v1.MutateOperation_DeleteRoleBinding{ + DeleteRoleBinding: &v1.DeleteRoleBindingOperation{ + SubjectKind: string(oldRB.Spec.Subject.Kind), + SubjectName: oldRB.Spec.Subject.Name, + RoleKind: string(roleRef.Kind), + RoleName: roleRef.Name, + }, + }, + }) + } + for _, roleRef := range newRB.Spec.RoleRefs { + operations = append(operations, &v1.MutateOperation{ + Operation: &v1.MutateOperation_CreateRoleBinding{ + CreateRoleBinding: &v1.CreateRoleBindingOperation{ + SubjectKind: string(newRB.Spec.Subject.Kind), + SubjectName: newRB.Spec.Subject.Name, + RoleKind: string(roleRef.Kind), + RoleName: roleRef.Name, + }, + }, + }) + } + + // Only make the request if there are deletes or writes + if len(operations) == 0 { + b.logger.Debug("no role bindings to update in zanzana", "namespace", newRB.Namespace, "name", newRB.Name) + return + } + + err := b.zClient.Mutate(ctx, &v1.MutateRequest{ + Namespace: newRB.Namespace, + Operations: operations, + }) + if err != nil { + status = "failure" + b.logger.Error("failed to update role binding in zanzana", + "err", err, + "namespace", newRB.Namespace, + "name", newRB.Name, + ) + } + }() + }, nil +} + +func roleRefsEqual(oldRoleRefs, newRoleRefs []iamv0.RoleBindingspecRoleRef) bool { + if len(oldRoleRefs) != len(newRoleRefs) { + return false + } + + oldRoleRefsMap := make(map[string]string) + for _, roleRef := range oldRoleRefs { + oldRoleRefsMap[roleRef.Name] = string(roleRef.Kind) + } + for _, roleRef := range newRoleRefs { + refKind, ok := oldRoleRefsMap[roleRef.Name] + if !ok { + return false + } + if refKind != string(roleRef.Kind) { + return false + } + } + return true +} diff --git a/pkg/registry/apis/iam/role_binding_hooks_test.go b/pkg/registry/apis/iam/role_binding_hooks_test.go new file mode 100644 index 00000000000..dd9646282fe --- /dev/null +++ b/pkg/registry/apis/iam/role_binding_hooks_test.go @@ -0,0 +1,448 @@ +package iam + +import ( + "context" + "slices" + "sync" + "testing" + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/stretchr/testify/require" + + 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" +) + +func TestAfterRoleBindingCreate(t *testing.T) { + var wg sync.WaitGroup + b := &IdentityAccessManagementAPIBuilder{ + logger: log.NewNopLogger(), + zTickets: make(chan bool, 1), + } + + t.Run("should create zanzana entry for role binding", func(t *testing.T) { + wg.Add(1) + roleBinding := iamv0.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "binding-1", + Namespace: "org-1", + }, + Spec: iamv0.RoleBindingSpec{ + Subject: iamv0.RoleBindingspecSubject{ + Kind: "user", + Name: "user-1", + }, + RoleRefs: []iamv0.RoleBindingspecRoleRef{ + { + Kind: "role", + Name: "role-1", + }, + }, + }, + } + + testRoleBinding := func(ctx context.Context, req *v1.MutateRequest) error { + defer wg.Done() + require.NotNil(t, req) + require.NotNil(t, req.Operations) + require.Len(t, req.Operations, 1) + require.Equal(t, "org-1", req.Namespace) + + expectedOperation := &v1.MutateOperation{ + Operation: &v1.MutateOperation_CreateRoleBinding{ + CreateRoleBinding: &v1.CreateRoleBindingOperation{ + SubjectKind: "user", + SubjectName: "user-1", + RoleKind: "role", + RoleName: "role-1", + }, + }, + } + + actualCreate := req.Operations[0].Operation.(*v1.MutateOperation_CreateRoleBinding).CreateRoleBinding + expectedCreate := expectedOperation.Operation.(*v1.MutateOperation_CreateRoleBinding).CreateRoleBinding + + require.Equal(t, expectedCreate.SubjectKind, actualCreate.SubjectKind) + require.Equal(t, expectedCreate.SubjectName, actualCreate.SubjectName) + require.Equal(t, expectedCreate.RoleKind, actualCreate.RoleKind) + require.Equal(t, expectedCreate.RoleName, actualCreate.RoleName) + + return nil + } + + b.zClient = &FakeZanzanaClient{mutateCallback: testRoleBinding} + b.AfterRoleBindingCreate(&roleBinding, nil) + wg.Wait() + }) + + t.Run("should not write to zanzana when zClient is nil", func(t *testing.T) { + builder := &IdentityAccessManagementAPIBuilder{ + logger: log.NewNopLogger(), + zTickets: make(chan bool, 1), + zClient: nil, + } + + roleBinding := iamv0.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "binding-3", + Namespace: "org-3", + }, + Spec: iamv0.RoleBindingSpec{ + Subject: iamv0.RoleBindingspecSubject{ + Kind: "user", + Name: "user-3", + }, + RoleRefs: []iamv0.RoleBindingspecRoleRef{ + { + Kind: "role", + Name: "role-3", + }, + }, + }, + } + + // Should not panic or error when zClient is nil + builder.AfterRoleBindingCreate(&roleBinding, nil) + }) +} + +func TestBeginRoleBindingUpdate(t *testing.T) { + var wg sync.WaitGroup + b := &IdentityAccessManagementAPIBuilder{ + logger: log.NewNopLogger(), + zTickets: make(chan bool, 1), + } + + t.Run("should update zanzana entry when role binding changed", func(t *testing.T) { + wg.Add(1) + oldBinding := iamv0.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "binding-1", + Namespace: "org-1", + }, + Spec: iamv0.RoleBindingSpec{ + Subject: iamv0.RoleBindingspecSubject{ + Kind: "user", + Name: "user-1", + }, + RoleRefs: []iamv0.RoleBindingspecRoleRef{ + { + Kind: "role", + Name: "role-foo", + }, + { + Kind: "role", + Name: "role-2", + }, + }, + }, + } + + newBinding := iamv0.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "binding-1", + Namespace: "org-1", + }, + Spec: iamv0.RoleBindingSpec{ + Subject: iamv0.RoleBindingspecSubject{ + Kind: "user", + Name: "user-1", + }, + RoleRefs: []iamv0.RoleBindingspecRoleRef{ + { + Kind: "role", + Name: "role-bar", + }, + }, + }, + } + + testRoleBindingUpdate := func(ctx context.Context, req *v1.MutateRequest) error { + defer wg.Done() + require.NotNil(t, req) + require.Equal(t, "org-1", req.Namespace) + + require.NotNil(t, req.Operations) + require.Len(t, req.Operations, 3) + + // Should write new binding and delete old one + require.True(t, containsOperation(req.Operations, &v1.MutateOperation{ + Operation: &v1.MutateOperation_DeleteRoleBinding{ + DeleteRoleBinding: &v1.DeleteRoleBindingOperation{ + SubjectKind: "user", + SubjectName: "user-1", + RoleKind: "role", + RoleName: "role-foo", + }, + }, + })) + + require.True(t, containsOperation(req.Operations, &v1.MutateOperation{ + Operation: &v1.MutateOperation_CreateRoleBinding{ + CreateRoleBinding: &v1.CreateRoleBindingOperation{ + SubjectKind: "user", + SubjectName: "user-1", + RoleKind: "role", + RoleName: "role-bar", + }, + }, + })) + + return nil + } + + b.zClient = &FakeZanzanaClient{mutateCallback: testRoleBindingUpdate} + + finishFunc, err := b.BeginRoleBindingUpdate(context.Background(), &newBinding, &oldBinding, nil) + require.NoError(t, err) + require.NotNil(t, finishFunc) + + finishFunc(context.Background(), true) + wg.Wait() + }) + + t.Run("should return nil finish func when bindings are identical", func(t *testing.T) { + oldBinding := iamv0.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "binding-2", + Namespace: "org-2", + }, + Spec: iamv0.RoleBindingSpec{ + Subject: iamv0.RoleBindingspecSubject{ + Kind: "user", + Name: "user-1", + }, + RoleRefs: []iamv0.RoleBindingspecRoleRef{ + { + Kind: "role", + Name: "role-1", + }, + }, + }, + } + + newBinding := iamv0.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "binding-2", + Namespace: "org-2", + }, + Spec: iamv0.RoleBindingSpec{ + Subject: iamv0.RoleBindingspecSubject{ + Kind: "user", + Name: "user-1", + }, + RoleRefs: []iamv0.RoleBindingspecRoleRef{ + { + Kind: "role", + Name: "role-1", + }, + }, + }, + } + + writeCalled := false + testNoWriteOnNoChange := func(ctx context.Context, req *v1.MutateRequest) error { + writeCalled = true + require.Fail(t, "Write should not be called when bindings are identical") + return nil + } + + b.zClient = &FakeZanzanaClient{mutateCallback: testNoWriteOnNoChange} + + finishFunc, err := b.BeginRoleBindingUpdate(context.Background(), &newBinding, &oldBinding, nil) + require.NoError(t, err) + require.Nil(t, finishFunc) // Should return nil when bindings are identical + + // Verify write was never called + time.Sleep(100 * time.Millisecond) + require.False(t, writeCalled, "Write callback should not be called when bindings are identical") + }) + + t.Run("should return nil finish func when new binding has empty subject name", func(t *testing.T) { + oldBinding := iamv0.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "binding-8", + Namespace: "org-8", + }, + Spec: iamv0.RoleBindingSpec{ + Subject: iamv0.RoleBindingspecSubject{ + Kind: "user", + Name: "user-1", + }, + RoleRefs: []iamv0.RoleBindingspecRoleRef{ + { + Kind: "role", + Name: "role-1", + }, + }, + }, + } + + newBinding := iamv0.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "binding-8", + Namespace: "org-8", + }, + Spec: iamv0.RoleBindingSpec{ + Subject: iamv0.RoleBindingspecSubject{ + Kind: "", + Name: "", // Empty name - should cause early return + }, + RoleRefs: []iamv0.RoleBindingspecRoleRef{ + { + Kind: "role", + Name: "role-1", + }, + }, + }, + } + + writeCalled := false + testNoWriteOnInvalidBinding := func(ctx context.Context, req *v1.MutateRequest) error { + writeCalled = true + require.Fail(t, "Write should not be called when new binding has empty subject name") + return nil + } + + b.zClient = &FakeZanzanaClient{mutateCallback: testNoWriteOnInvalidBinding} + + finishFunc, err := b.BeginRoleBindingUpdate(context.Background(), &newBinding, &oldBinding, nil) + require.NoError(t, err) + require.Nil(t, finishFunc) // Should return nil when new binding has empty subject name + + // Verify write was never called + time.Sleep(100 * time.Millisecond) + require.False(t, writeCalled, "Write callback should not be called when new binding has empty subject name") + }) +} + +func TestAfterRoleBindingDelete(t *testing.T) { + var wg sync.WaitGroup + b := &IdentityAccessManagementAPIBuilder{ + logger: log.NewNopLogger(), + zTickets: make(chan bool, 1), + } + + t.Run("should delete zanzana entry for team binding with member permission", func(t *testing.T) { + wg.Add(1) + roleBinding := iamv0.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "binding-1", + Namespace: "org-1", + }, + Spec: iamv0.RoleBindingSpec{ + Subject: iamv0.RoleBindingspecSubject{ + Kind: "user", + Name: "user-1", + }, + RoleRefs: []iamv0.RoleBindingspecRoleRef{ + { + Kind: "role", + Name: "role-1", + }, + { + Kind: "role", + Name: "role-2", + }, + }, + }, + } + + testRoleBindingDelete := func(ctx context.Context, req *v1.MutateRequest) error { + defer wg.Done() + require.NotNil(t, req) + require.Equal(t, "org-1", req.Namespace) + + // Should have deletes but no writes + require.NotNil(t, req.Operations) + require.Len(t, req.Operations, 2) + require.True(t, containsOperation(req.Operations, &v1.MutateOperation{ + Operation: &v1.MutateOperation_DeleteRoleBinding{ + DeleteRoleBinding: &v1.DeleteRoleBindingOperation{ + SubjectKind: "user", + SubjectName: "user-1", + RoleKind: "role", + RoleName: "role-1", + }, + }, + })) + require.True(t, containsOperation(req.Operations, &v1.MutateOperation{ + Operation: &v1.MutateOperation_DeleteRoleBinding{ + DeleteRoleBinding: &v1.DeleteRoleBindingOperation{ + SubjectKind: "user", + SubjectName: "user-1", + RoleKind: "role", + RoleName: "role-2", + }, + }, + })) + + return nil + } + + b.zClient = &FakeZanzanaClient{mutateCallback: testRoleBindingDelete} + b.AfterRoleBindingDelete(&roleBinding, nil) + wg.Wait() + }) + + t.Run("should not delete from zanzana when zClient is nil", func(t *testing.T) { + builder := &IdentityAccessManagementAPIBuilder{ + logger: log.NewNopLogger(), + zTickets: make(chan bool, 1), + zClient: nil, + } + + roleBinding := iamv0.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "binding-3", + Namespace: "org-3", + }, + Spec: iamv0.RoleBindingSpec{ + Subject: iamv0.RoleBindingspecSubject{ + Kind: "user", + Name: "user-3", + }, + RoleRefs: []iamv0.RoleBindingspecRoleRef{ + { + Kind: "role", + Name: "role-3", + }, + }, + }, + } + + // Should not panic or error when zClient is nil + builder.AfterRoleBindingDelete(&roleBinding, nil) + }) +} + +func containsOperation(operations []*v1.MutateOperation, operation *v1.MutateOperation) bool { + return slices.ContainsFunc(operations, func(o *v1.MutateOperation) bool { + switch operation.Operation.(type) { + case *v1.MutateOperation_DeleteRoleBinding: + deleteOperation := operation.Operation.(*v1.MutateOperation_DeleteRoleBinding) + deleteO, ok := o.Operation.(*v1.MutateOperation_DeleteRoleBinding) + if !ok { + return false + } + return deleteO.DeleteRoleBinding.SubjectKind == deleteOperation.DeleteRoleBinding.SubjectKind && + deleteO.DeleteRoleBinding.SubjectName == deleteOperation.DeleteRoleBinding.SubjectName && + deleteO.DeleteRoleBinding.RoleKind == deleteOperation.DeleteRoleBinding.RoleKind && + deleteO.DeleteRoleBinding.RoleName == deleteOperation.DeleteRoleBinding.RoleName + case *v1.MutateOperation_CreateRoleBinding: + createOperation := operation.Operation.(*v1.MutateOperation_CreateRoleBinding) + createO, ok := o.Operation.(*v1.MutateOperation_CreateRoleBinding) + if !ok { + return false + } + return createO.CreateRoleBinding.SubjectKind == createOperation.CreateRoleBinding.SubjectKind && + createO.CreateRoleBinding.SubjectName == createOperation.CreateRoleBinding.SubjectName && + createO.CreateRoleBinding.RoleKind == createOperation.CreateRoleBinding.RoleKind && + createO.CreateRoleBinding.RoleName == createOperation.CreateRoleBinding.RoleName + } + return false + }) +}