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
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user