package unifiedauth import ( "context" corev1 "k8s.io/api/core/v1" rbacv1 "k8s.io/api/rbac/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/client-go/tools/record" "k8s.io/klog/v2" controllerruntime "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/builder" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/event" "sigs.k8s.io/controller-runtime/pkg/handler" "sigs.k8s.io/controller-runtime/pkg/predicate" "sigs.k8s.io/controller-runtime/pkg/reconcile" "sigs.k8s.io/controller-runtime/pkg/source" clusterv1alpha1 "github.com/karmada-io/karmada/pkg/apis/cluster/v1alpha1" workv1alpha1 "github.com/karmada-io/karmada/pkg/apis/work/v1alpha1" "github.com/karmada-io/karmada/pkg/events" "github.com/karmada-io/karmada/pkg/util" "github.com/karmada-io/karmada/pkg/util/helper" "github.com/karmada-io/karmada/pkg/util/names" ) const ( // ControllerName is the controller name that will be used when reporting events. ControllerName = "unified-auth-controller" karmadaImpersonatorName = "karmada-impersonator" clusterAPIGroup = "cluster.karmada.io" clusterProxyResource = "clusters/proxy" searchAPIGroup = "search.karmada.io" proxyingProxyResource = "proxying/proxy" ) // Controller is to sync impersonation config to member clusters for unified authentication. type Controller struct { client.Client // used to operate Cluster resources. EventRecorder record.EventRecorder } // Reconcile performs a full reconciliation for the object referred to by the Request. func (c *Controller) Reconcile(ctx context.Context, req controllerruntime.Request) (controllerruntime.Result, error) { klog.V(4).Infof("Reconciling cluster %s", req.NamespacedName.String()) cluster := &clusterv1alpha1.Cluster{} if err := c.Client.Get(ctx, req.NamespacedName, cluster); err != nil { // The resource may no longer exist, in which case we stop processing. if apierrors.IsNotFound(err) { return controllerruntime.Result{}, nil } return controllerruntime.Result{Requeue: true}, err } if !cluster.DeletionTimestamp.IsZero() { // Do nothing, just return as we have added owner reference to Work. // Work will be removed automatically by garbage collector. return controllerruntime.Result{}, nil } if cluster.Spec.ImpersonatorSecretRef == nil { klog.Infof("Unified auth is disabled on cluster %s as it does not have an impersonator secret", cluster.Name) return controllerruntime.Result{}, nil } err := c.syncImpersonationConfig(cluster) if err != nil { klog.Errorf("Failed to sync impersonation config for cluster %s. Error: %v.", cluster.Name, err) c.EventRecorder.Eventf(cluster, corev1.EventTypeWarning, events.EventReasonSyncImpersonationConfigFailed, err.Error()) return controllerruntime.Result{Requeue: true}, err } c.EventRecorder.Eventf(cluster, corev1.EventTypeNormal, events.EventReasonSyncImpersonationConfigSucceed, "Sync impersonation config succeed.") return controllerruntime.Result{}, nil } func (c *Controller) syncImpersonationConfig(cluster *clusterv1alpha1.Cluster) error { // step1: find out target RBAC subjects. targetSubjects, err := findRBACSubjectsWithCluster(c.Client, cluster.Name) if err != nil { return err } // step2: generate rules for impersonation rules := util.GenerateImpersonationRules(targetSubjects) // step3: sync ClusterRole to cluster for impersonation if err := c.buildImpersonationClusterRole(cluster, rules); err != nil { klog.Errorf("Failed to sync impersonate ClusterRole to cluster(%s): %v", cluster.Name, err) return err } // step4: sync ClusterRoleBinding to cluster for impersonation if err := c.buildImpersonationClusterRoleBinding(cluster); err != nil { klog.Errorf("Failed to sync impersonate ClusterRoleBinding to cluster(%s): %v", cluster.Name, err) return err } return nil } func findRBACSubjectsWithCluster(c client.Client, cluster string) ([]rbacv1.Subject, error) { clusterRoleList := &rbacv1.ClusterRoleList{} if err := c.List(context.TODO(), clusterRoleList); err != nil { klog.Errorf("Failed to list ClusterRoles, error: %v", err) return nil, err } matchedClusterRoles := sets.NewString() for _, clusterRole := range clusterRoleList.Items { for index := range clusterRole.Rules { if util.PolicyRuleAPIGroupMatches(&clusterRole.Rules[index], clusterAPIGroup) && util.PolicyRuleResourceMatches(&clusterRole.Rules[index], clusterProxyResource) && util.PolicyRuleResourceNameMatches(&clusterRole.Rules[index], cluster) { matchedClusterRoles.Insert(clusterRole.Name) break } if util.PolicyRuleAPIGroupMatches(&clusterRole.Rules[index], searchAPIGroup) && util.PolicyRuleResourceMatches(&clusterRole.Rules[index], proxyingProxyResource) { matchedClusterRoles.Insert(clusterRole.Name) break } } } // short path: no matched ClusterRoles if matchedClusterRoles.Len() == 0 { return nil, nil } clusterRoleBindings := &rbacv1.ClusterRoleBindingList{} if err := c.List(context.TODO(), clusterRoleBindings); err != nil { klog.Errorf("Failed to list ClusterRoleBindings, error: %v", err) return nil, err } var targetSubjects []rbacv1.Subject for _, clusterRoleBinding := range clusterRoleBindings.Items { if clusterRoleBinding.RoleRef.Kind == util.ClusterRoleKind && matchedClusterRoles.Has(clusterRoleBinding.RoleRef.Name) { targetSubjects = append(targetSubjects, clusterRoleBinding.Subjects...) } } return targetSubjects, nil } func (c *Controller) buildImpersonationClusterRole(cluster *clusterv1alpha1.Cluster, rules []rbacv1.PolicyRule) error { impersonationClusterRole := &rbacv1.ClusterRole{ TypeMeta: metav1.TypeMeta{ APIVersion: rbacv1.SchemeGroupVersion.String(), Kind: util.ClusterRoleKind, }, ObjectMeta: metav1.ObjectMeta{ Name: karmadaImpersonatorName, }, Rules: rules, } clusterRoleObj, err := helper.ToUnstructured(impersonationClusterRole) if err != nil { klog.Errorf("Failed to transform ClusterRole %s. Error: %v", impersonationClusterRole.GetName(), err) return err } return c.buildWorks(cluster, clusterRoleObj) } func (c *Controller) buildImpersonationClusterRoleBinding(cluster *clusterv1alpha1.Cluster) error { impersonatorClusterRoleBinding := &rbacv1.ClusterRoleBinding{ TypeMeta: metav1.TypeMeta{ APIVersion: rbacv1.SchemeGroupVersion.String(), Kind: util.ClusterRoleBindingKind, }, ObjectMeta: metav1.ObjectMeta{ Name: karmadaImpersonatorName, }, Subjects: []rbacv1.Subject{ { Kind: rbacv1.ServiceAccountKind, Namespace: cluster.Spec.ImpersonatorSecretRef.Namespace, Name: names.GenerateServiceAccountName("impersonator"), }, }, RoleRef: rbacv1.RoleRef{ APIGroup: rbacv1.GroupName, Kind: util.ClusterRoleKind, Name: karmadaImpersonatorName, }, } clusterRoleBindingObj, err := helper.ToUnstructured(impersonatorClusterRoleBinding) if err != nil { klog.Errorf("Failed to transform ClusterRoleBinding %s. Error: %v", impersonatorClusterRoleBinding.GetName(), err) return err } return c.buildWorks(cluster, clusterRoleBindingObj) } func (c *Controller) buildWorks(cluster *clusterv1alpha1.Cluster, obj *unstructured.Unstructured) error { workNamespace := names.GenerateExecutionSpaceName(cluster.Name) clusterRoleBindingWorkName := names.GenerateWorkName(obj.GetKind(), obj.GetName(), obj.GetNamespace()) objectMeta := metav1.ObjectMeta{ Name: clusterRoleBindingWorkName, Namespace: workNamespace, Finalizers: []string{util.ExecutionControllerFinalizer}, OwnerReferences: []metav1.OwnerReference{ *metav1.NewControllerRef(cluster, cluster.GroupVersionKind()), }, } util.MergeLabel(obj, workv1alpha1.WorkNamespaceLabel, workNamespace) util.MergeLabel(obj, workv1alpha1.WorkNameLabel, clusterRoleBindingWorkName) util.MergeLabel(obj, util.ManagedByKarmadaLabel, util.ManagedByKarmadaLabelValue) if err := helper.CreateOrUpdateWork(c.Client, objectMeta, obj); err != nil { return err } return nil } // SetupWithManager creates a controller and register to controller manager. func (c *Controller) SetupWithManager(mgr controllerruntime.Manager) error { // clusterPredicateFunc only cares about create events clusterPredicateFunc := predicate.Funcs{ CreateFunc: func(event.CreateEvent) bool { return true }, UpdateFunc: func(event.UpdateEvent) bool { return false }, DeleteFunc: func(event.DeleteEvent) bool { return false }, GenericFunc: func(event.GenericEvent) bool { return false }, } return controllerruntime.NewControllerManagedBy(mgr). For(&clusterv1alpha1.Cluster{}, builder.WithPredicates(clusterPredicateFunc)). Watches(&source.Kind{Type: &rbacv1.ClusterRole{}}, handler.EnqueueRequestsFromMapFunc(c.newClusterRoleMapFunc())). Watches(&source.Kind{Type: &rbacv1.ClusterRoleBinding{}}, handler.EnqueueRequestsFromMapFunc(c.newClusterRoleBindingMapFunc())). Complete(c) } func (c *Controller) newClusterRoleMapFunc() handler.MapFunc { return func(a client.Object) []reconcile.Request { clusterRole := a.(*rbacv1.ClusterRole) return c.generateRequestsFromClusterRole(clusterRole) } } func (c *Controller) newClusterRoleBindingMapFunc() handler.MapFunc { return func(a client.Object) []reconcile.Request { clusterRoleBinding := a.(*rbacv1.ClusterRoleBinding) if clusterRoleBinding.RoleRef.Kind != util.ClusterRoleKind { return nil } clusterRole := &rbacv1.ClusterRole{} if err := c.Client.Get(context.TODO(), types.NamespacedName{Name: clusterRoleBinding.RoleRef.Name}, clusterRole); err != nil { klog.Errorf("Failed to get reference ClusterRole, error: %v", err) return nil } return c.generateRequestsFromClusterRole(clusterRole) } } // generateRequestsFromClusterRole generates the requests for which clusters need be synced with impersonation config. func (c *Controller) generateRequestsFromClusterRole(clusterRole *rbacv1.ClusterRole) []reconcile.Request { clusterList := &clusterv1alpha1.ClusterList{} if err := c.Client.List(context.TODO(), clusterList); err != nil { klog.Errorf("Failed to list existing clusters, error: %v", err) return nil } syncClusterNames := sets.NewString() for index, rule := range clusterRole.Rules { // for rule like: // apiGroup: "cluster.karmada.io" // resources: ["cluster/proxy"] // resourceNames: ["cluster1", "cluster2"] if util.PolicyRuleAPIGroupMatches(&clusterRole.Rules[index], clusterAPIGroup) && util.PolicyRuleResourceMatches(&clusterRole.Rules[index], clusterProxyResource) { if len(rule.ResourceNames) == 0 { // if the length of rule.ResourceNames is 0, means to match all clusters for _, cluster := range clusterList.Items { syncClusterNames.Insert(cluster.Name) } break } for _, resourceName := range rule.ResourceNames { syncClusterNames.Insert(resourceName) } } // for rule like: // apiGroup: "search.karmada.io" // resources: ["proxying/proxy"] if util.PolicyRuleAPIGroupMatches(&clusterRole.Rules[index], searchAPIGroup) && util.PolicyRuleResourceMatches(&clusterRole.Rules[index], proxyingProxyResource) { // push all existing clusters into requests for _, cluster := range clusterList.Items { syncClusterNames.Insert(cluster.Name) } break } } var requests []reconcile.Request for _, clusterName := range syncClusterNames.List() { requests = append(requests, reconcile.Request{NamespacedName: types.NamespacedName{Name: clusterName}}) } return requests }