* Zanzana: Role bindings hooks WIP * Empty hooks for role bindings * implement hooks for role bindings * add tests * apply review suggestions
303 lines
8.3 KiB
Go
303 lines
8.3 KiB
Go
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
|
|
}
|