Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 21 additions & 7 deletions backend/kubernetes/resources/konnector.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,19 +60,33 @@ func NewKonnectorManifests(konnectorImage string, hostAliases []corev1.HostAlias
Namespace: KonnectorNamespace,
},
},
// Broad access is required because the konnector dynamically manages CRDs
// and syncs arbitrary resource types discovered from the provider. Scoping
// down would require knowing the bound resource types in advance, which
// defeats the auto-discovery model.
// The konnector dynamically manages CRDs and syncs arbitrary resource types.
// Wildcard permissions are no longer used here; instead, the kubectl bind
// CLI dynamically creates RBAC for the bound custom resources.
ClusterRole: &rbacv1.ClusterRole{
ObjectMeta: metav1.ObjectMeta{
Name: KonnectorClusterRoleName,
},
Rules: []rbacv1.PolicyRule{
{
APIGroups: []string{"*"},
Resources: []string{"*"},
Verbs: []string{"*"},
APIGroups: []string{""},
Resources: []string{"namespaces", "secrets", "events", "serviceaccounts", "configmaps"},
Verbs: []string{"get", "list", "watch", "create", "update", "patch", "delete"},
},
{
APIGroups: []string{"kube-bind.io"},
Resources: []string{"apiservicebindings", "apiservicebindings/status", "clusterbindings", "clusterbindings/status", "servicebindings", "servicebindings/status", "serviceexports", "serviceexports/status"},
Verbs: []string{"get", "list", "watch", "create", "update", "patch", "delete"},
},
{
APIGroups: []string{"apiextensions.k8s.io"},
Resources: []string{"customresourcedefinitions"},
Verbs: []string{"get", "list", "watch", "create", "update", "patch", "delete"},
},
{
APIGroups: []string{"coordination.k8s.io"},
Resources: []string{"leases"},
Verbs: []string{"get", "list", "watch", "create", "update", "patch", "delete"},
},
},
},
Expand Down
21 changes: 18 additions & 3 deletions cli/pkg/kubectl/bind-apiservice/plugin/deploy_konnector.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,9 +108,24 @@ func getKonnectorClusterRole() *rbacv1.ClusterRole {
},
Rules: []rbacv1.PolicyRule{
{
APIGroups: []string{"*"},
Resources: []string{"*"},
Verbs: []string{"*"},
APIGroups: []string{""},
Resources: []string{"namespaces", "secrets", "events", "serviceaccounts", "configmaps"},
Verbs: []string{"get", "list", "watch", "create", "update", "patch", "delete"},
},
{
APIGroups: []string{"kube-bind.io"},
Resources: []string{"apiservicebindings", "apiservicebindings/status", "clusterbindings", "clusterbindings/status", "servicebindings", "servicebindings/status", "serviceexports", "serviceexports/status"},
Verbs: []string{"get", "list", "watch", "create", "update", "patch", "delete"},
},
{
APIGroups: []string{"apiextensions.k8s.io"},
Resources: []string{"customresourcedefinitions"},
Verbs: []string{"get", "list", "watch", "create", "update", "patch", "delete"},
},
{
APIGroups: []string{"coordination.k8s.io"},
Resources: []string{"leases"},
Verbs: []string{"get", "list", "watch", "create", "update", "patch", "delete"},
},
},
}
Expand Down
110 changes: 110 additions & 0 deletions cli/pkg/kubectl/bind-apiservice/plugin/rbac.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package plugin

import (
"context"
"fmt"

kubebindv1alpha2 "github.com/kube-bind/kube-bind/sdk/apis/kubebind/v1alpha2"

Check failure on line 7 in cli/pkg/kubectl/bind-apiservice/plugin/rbac.go

View workflow job for this annotation

GitHub Actions / lint

File is not properly formatted (gci)
rbacv1 "k8s.io/api/rbac/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
)

func ensureKonnectorDynamicRBAC(ctx context.Context, config *rest.Config, binding *kubebindv1alpha2.APIServiceBinding, request *kubebindv1alpha2.APIServiceExportRequest) error {
kubeClient, err := kubernetes.NewForConfig(config)
if err != nil {
return err
}

roleName := fmt.Sprintf("kube-bind-konnector-%s", binding.Name)
ownerRef := metav1.OwnerReference{
APIVersion: kubebindv1alpha2.SchemeGroupVersion.String(),
Kind: "APIServiceBinding",
Name: binding.Name,
UID: binding.UID,
}

var rules []rbacv1.PolicyRule
for _, res := range request.Spec.Resources {
rules = append(rules, rbacv1.PolicyRule{
APIGroups: []string{res.Group},
Resources: []string{res.Resource, res.Resource + "/status"},
Verbs: []string{"get", "list", "watch", "create", "update", "patch", "delete"},
})
}
for _, claim := range request.Spec.PermissionClaims {
rules = append(rules, rbacv1.PolicyRule{
APIGroups: []string{claim.Group},
Resources: []string{claim.Resource, claim.Resource + "/status"},
Verbs: []string{"get", "list", "watch", "create", "update", "patch", "delete"},
})
}

clusterRole := &rbacv1.ClusterRole{
ObjectMeta: metav1.ObjectMeta{
Name: roleName,
OwnerReferences: []metav1.OwnerReference{ownerRef},
},
Rules: rules,
}

_, err = kubeClient.RbacV1().ClusterRoles().Create(ctx, clusterRole, metav1.CreateOptions{})
if err != nil {
if apierrors.IsAlreadyExists(err) {
existing, err := kubeClient.RbacV1().ClusterRoles().Get(ctx, roleName, metav1.GetOptions{})
if err != nil {
return err
}
existing.Rules = rules
existing.OwnerReferences = []metav1.OwnerReference{ownerRef}
_, err = kubeClient.RbacV1().ClusterRoles().Update(ctx, existing, metav1.UpdateOptions{})
if err != nil {
return err
}
} else {
return err
}
}

clusterRoleBinding := &rbacv1.ClusterRoleBinding{
ObjectMeta: metav1.ObjectMeta{
Name: roleName,
OwnerReferences: []metav1.OwnerReference{ownerRef},
},
RoleRef: rbacv1.RoleRef{
APIGroup: "rbac.authorization.k8s.io",
Kind: "ClusterRole",
Name: roleName,
},
Subjects: []rbacv1.Subject{
{
Kind: "ServiceAccount",
Name: KonnectorServiceAccount,
Namespace: KonnectorNamespace,
},
},
}

_, err = kubeClient.RbacV1().ClusterRoleBindings().Create(ctx, clusterRoleBinding, metav1.CreateOptions{})
if err != nil {
if apierrors.IsAlreadyExists(err) {
existing, err := kubeClient.RbacV1().ClusterRoleBindings().Get(ctx, roleName, metav1.GetOptions{})
if err != nil {
return err
}
existing.RoleRef = clusterRoleBinding.RoleRef
existing.Subjects = clusterRoleBinding.Subjects
existing.OwnerReferences = []metav1.OwnerReference{ownerRef}
_, err = kubeClient.RbacV1().ClusterRoleBindings().Update(ctx, existing, metav1.UpdateOptions{})
if err != nil {
return err
}
} else {
return err
}
}

return nil
}
10 changes: 10 additions & 0 deletions cli/pkg/kubectl/bind-apiservice/plugin/servicebindings.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,11 @@ func (b *BindAPIServiceOptions) createAPIServiceBindings(ctx context.Context, co
}
}

// Ensure dynamic RBAC is created for the konnector to access the bound resources
if err := ensureKonnectorDynamicRBAC(ctx, config, existing, request); err != nil {
return nil, fmt.Errorf("failed to create dynamic RBAC for konnector: %w", err)
}

return []*kubebindv1alpha2.APIServiceBinding{existing}, nil
}

Expand Down Expand Up @@ -106,6 +111,11 @@ func (b *BindAPIServiceOptions) createAPIServiceBindings(ctx context.Context, co
return nil, err
}

// Ensure dynamic RBAC is created for the konnector to access the bound resources
if err := ensureKonnectorDynamicRBAC(ctx, config, created, request); err != nil {
return nil, fmt.Errorf("failed to create dynamic RBAC for konnector: %w", err)
}

fmt.Fprintf(b.Options.IOStreams.ErrOut, "✅ Created APIServiceBinding %s for %d resources\n", bindingName, len(request.Spec.Resources))
return []*kubebindv1alpha2.APIServiceBinding{created}, nil
}
Loading