package unifiedauth import ( "context" 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" utilerrors "k8s.io/apimachinery/pkg/util/errors" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/client-go/rest" "k8s.io/client-go/tools/record" "k8s.io/klog/v2" controllerruntime "sigs.k8s.io/controller-runtime" "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/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" rbacAPIVersion = "rbac.authorization.k8s.io/v1" clusterProxyResource = "clusters/proxy" clusterProxyAPIGroup = "cluster.karmada.io" karmadaImpersontorName = "karmada-impersonator" ) // Controller is to sync impersonation config to member clusters for unified authentication. type Controller struct { client.Client // used to operate Cluster resources. ControllerPlaneConfig *rest.Config EventRecorder record.EventRecorder ClusterClientSetFunc func(string, client.Client, *util.ClientOption) (*util.ClusterClient, error) } // 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(context.TODO(), 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 } err := c.syncImpersonationConfig(cluster) if err != nil { klog.Errorf("Failed to sync impersonation config for cluster %s. Error: %v.", cluster.Name, err) return controllerruntime.Result{Requeue: true}, err } return controllerruntime.Result{}, nil } // Start starts a goroutine to ensure impersonation secret for upgrade scenario. func (c *Controller) Start(ctx context.Context) error { go c.ensureImpersonationSecret() return nil } func (c *Controller) syncImpersonationConfig(cluster *clusterv1alpha1.Cluster) error { // step1: list all clusterroles clusterRoleList := &rbacv1.ClusterRoleList{} if err := c.Client.List(context.TODO(), clusterRoleList); err != nil { klog.Errorf("Failed to list clusterroles, error: %v", err) return err } // step2: found out clusterroles that matches current cluster allMatchedClusterRoles := sets.NewString() for _, clusterRole := range clusterRoleList.Items { for i := range clusterRole.Rules { if util.PolicyRuleAPIGroupMatches(&clusterRole.Rules[i], clusterProxyAPIGroup) && util.PolicyRuleResourceMatches(&clusterRole.Rules[i], clusterProxyResource) && util.PolicyRuleResourceNameMatches(&clusterRole.Rules[i], cluster.Name) { allMatchedClusterRoles.Insert(clusterRole.Name) } } } // step3: found out reference clusterRolebindings and collecting subjects. clusterRoleBindings := &rbacv1.ClusterRoleBindingList{} var allSubjects []rbacv1.Subject if len(allMatchedClusterRoles) != 0 { if err := c.Client.List(context.TODO(), clusterRoleBindings); err != nil { klog.Errorf("Failed to list clusterrolebindings, error: %v", err) return err } for _, clusterRoleBinding := range clusterRoleBindings.Items { if clusterRoleBinding.RoleRef.Kind == util.ClusterRoleKind && allMatchedClusterRoles.Has(clusterRoleBinding.RoleRef.Name) { allSubjects = append(allSubjects, clusterRoleBinding.Subjects...) } } } // step4: generate rules for impersonation rules := util.GenerateImpersonationRules(allSubjects) // step5: 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 } // step6: 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 (c *Controller) buildImpersonationClusterRole(cluster *clusterv1alpha1.Cluster, rules []rbacv1.PolicyRule) error { impersonationClusterRole := &rbacv1.ClusterRole{ TypeMeta: metav1.TypeMeta{ APIVersion: rbacAPIVersion, Kind: util.ClusterRoleKind, }, ObjectMeta: metav1.ObjectMeta{ Name: karmadaImpersontorName, }, Rules: rules, } clusterRoleObj, err := helper.ToUnstructured(impersonationClusterRole) if err != nil { klog.Errorf("Failed to transform clusterrole %s. Error: %v", impersonationClusterRole.GetName(), err) return nil } return c.buildWorks(cluster, clusterRoleObj) } func (c *Controller) buildImpersonationClusterRoleBinding(cluster *clusterv1alpha1.Cluster) error { impersonatorClusterRoleBinding := &rbacv1.ClusterRoleBinding{ TypeMeta: metav1.TypeMeta{ APIVersion: rbacAPIVersion, Kind: util.ClusterRoleBindingKind, }, ObjectMeta: metav1.ObjectMeta{ Name: karmadaImpersontorName, }, Subjects: []rbacv1.Subject{ { Kind: rbacv1.ServiceAccountKind, Namespace: names.NamespaceKarmadaCluster, Name: names.GenerateServiceAccountName("impersonator"), }, }, RoleRef: rbacv1.RoleRef{ APIGroup: rbacv1.GroupName, Kind: util.ClusterRoleKind, Name: karmadaImpersontorName, }, } clusterRoleBindingObj, err := helper.ToUnstructured(impersonatorClusterRoleBinding) if err != nil { klog.Errorf("Failed to transform clusterrolebinding %s. Error: %v", impersonatorClusterRoleBinding.GetName(), err) return nil } return c.buildWorks(cluster, clusterRoleBindingObj) } func (c *Controller) buildWorks(cluster *clusterv1alpha1.Cluster, obj *unstructured.Unstructured) error { workNamespace, err := names.GenerateExecutionSpaceName(cluster.Name) if err != nil { klog.Errorf("Failed to generate execution space name for member cluster %s, err is %v", cluster.Name, err) return err } 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) 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(e event.CreateEvent) bool { return true }, UpdateFunc: func(e event.UpdateEvent) bool { if _, ok := e.ObjectNew.(*clusterv1alpha1.Cluster); ok { return false } if _, ok := e.ObjectOld.(*clusterv1alpha1.Cluster); ok { return false } return true }, DeleteFunc: func(e event.DeleteEvent) bool { if _, ok := e.Object.(*clusterv1alpha1.Cluster); ok { return false } return true }, GenericFunc: func(event.GenericEvent) bool { return false }, } return utilerrors.NewAggregate([]error{ controllerruntime.NewControllerManagedBy(mgr).For(&clusterv1alpha1.Cluster{}).WithEventFilter(clusterPredicateFunc). Watches(&source.Kind{Type: &rbacv1.ClusterRole{}}, handler.EnqueueRequestsFromMapFunc(c.newClusterRoleMapFunc())). Watches(&source.Kind{Type: &rbacv1.ClusterRoleBinding{}}, handler.EnqueueRequestsFromMapFunc(c.newClusterRoleBindingMapFunc())). Complete(c), mgr.Add(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) } } // found out which clusters need to sync impersonation config from rules like: // resources: ["cluster/proxy"] // resourceNmaes: ["cluster1", "cluster2"] func (c *Controller) generateRequestsFromClusterRole(clusterRole *rbacv1.ClusterRole) []reconcile.Request { var requests []reconcile.Request for i := range clusterRole.Rules { if util.PolicyRuleAPIGroupMatches(&clusterRole.Rules[i], clusterProxyAPIGroup) && util.PolicyRuleResourceMatches(&clusterRole.Rules[i], clusterProxyResource) { if len(clusterRole.Rules[i].ResourceNames) == 0 { // if rule.ResourceNames == 0, means to match all clusters clusterList := &clusterv1alpha1.ClusterList{} if err := c.Client.List(context.TODO(), clusterList); err != nil { klog.Errorf("Failed to list clusters, error: %v", err) return nil } for _, cluster := range clusterList.Items { requests = append(requests, reconcile.Request{NamespacedName: types.NamespacedName{ Name: cluster.Name, }}) } } else { for _, ruleName := range clusterRole.Rules[i].ResourceNames { requests = append(requests, reconcile.Request{NamespacedName: types.NamespacedName{ Name: ruleName, }}) } } } } return requests }