diff --git a/README.md b/README.md index 665eb4e..916deec 100644 --- a/README.md +++ b/README.md @@ -109,6 +109,8 @@ spec: # Desired name for produced ClusterRole target: name: example-policy + annotations: {} + labels: {} # This is where the allowed policies are expressed # Ref: https://kubernetes.io/docs/reference/access-authn-authz/rbac/ @@ -223,7 +225,18 @@ spec: apiGroup: "" kind: ServiceAccount + # (Optional) + # ServiceAccounts can be selected by some metadata + # This field is mutually exclusive with 'nameSelector' + metaSelector: + + # Select by matching labels + matchLabels: + managed-by: custom-operator + + # (Optional) # ServiceAccount names can be matched by exact name, or a Golang regular expression. + # This field is mutually exclusive with 'metaSelector' # Attention: Only one can be performed. nameSelector: @@ -239,6 +252,7 @@ spec: # negative: false # expression: "^(.*)$" + # (Optional) # To look for a ServiceAccount, namespaces can be matched by exact name, # by their labels, or a Golang regular expression. # Attention: Only one can be performed. @@ -264,6 +278,15 @@ spec: # For those members selected in the previous section targets: + # (Required) + # Name of the RoleBinding objects to be created + name: example-policy + + # Add some metadata to the RoleBinding objects + annotations: {} + labels: {} + + # (Optional) # Target namespaces can be matched by exact name, # by their labels, or a Golang regular expression. # Attention: Only one can be performed. diff --git a/api/v1alpha1/dynamicrolebinding_types.go b/api/v1alpha1/dynamicrolebinding_types.go index c2996de..5e184b8 100644 --- a/api/v1alpha1/dynamicrolebinding_types.go +++ b/api/v1alpha1/dynamicrolebinding_types.go @@ -22,7 +22,12 @@ import ( type MatchRegexT struct { Negative bool `json:"negative,omitempty"` - Expression string `json:"expression"` + Expression string `json:"expression,omitempty"` +} + +// TODO +type MetaSelectorT struct { + MatchLabels map[string]string `json:"matchLabels,omitempty"` } // TODO @@ -43,7 +48,8 @@ type DynamicRoleBindingSourceSubject struct { ApiGroup string `json:"apiGroup"` Kind string `json:"kind"` - NameSelector NameSelectorT `json:"nameSelector"` + MetaSelector MetaSelectorT `json:"metaSelector,omitempty"` + NameSelector NameSelectorT `json:"nameSelector,omitempty"` NamespaceSelector NamespaceSelectorT `json:"namespaceSelector,omitempty"` } @@ -56,7 +62,11 @@ type DynamicRoleBindingSource struct { // TODO type DynamicRoleBindingTargets struct { - NamespaceSelector NamespaceSelectorT `json:"namespaceSelector"` + Name string `json:"name"` + Annotations map[string]string `json:"annotations,omitempty"` + Labels map[string]string `json:"labels,omitempty"` + + NamespaceSelector NamespaceSelectorT `json:"namespaceSelector,omitempty"` } // DynamicRoleBindingSpec defines the desired state of DynamicRoleBinding diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 99ea498..287997a 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -216,6 +216,7 @@ func (in *DynamicRoleBindingSource) DeepCopy() *DynamicRoleBindingSource { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DynamicRoleBindingSourceSubject) DeepCopyInto(out *DynamicRoleBindingSourceSubject) { *out = *in + in.MetaSelector.DeepCopyInto(&out.MetaSelector) in.NameSelector.DeepCopyInto(&out.NameSelector) in.NamespaceSelector.DeepCopyInto(&out.NamespaceSelector) } @@ -273,6 +274,20 @@ func (in *DynamicRoleBindingStatus) DeepCopy() *DynamicRoleBindingStatus { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DynamicRoleBindingTargets) DeepCopyInto(out *DynamicRoleBindingTargets) { *out = *in + if in.Annotations != nil { + in, out := &in.Annotations, &out.Annotations + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Labels != nil { + in, out := &in.Labels, &out.Labels + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } in.NamespaceSelector.DeepCopyInto(&out.NamespaceSelector) } @@ -301,6 +316,28 @@ func (in *MatchRegexT) DeepCopy() *MatchRegexT { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *MetaSelectorT) DeepCopyInto(out *MetaSelectorT) { + *out = *in + if in.MatchLabels != nil { + in, out := &in.MatchLabels, &out.MatchLabels + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MetaSelectorT. +func (in *MetaSelectorT) DeepCopy() *MetaSelectorT { + if in == nil { + return nil + } + out := new(MetaSelectorT) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *NameSelectorT) DeepCopyInto(out *NameSelectorT) { *out = *in diff --git a/config/crd/bases/kuberbac.prosimcorp.com_dynamicrolebindings.yaml b/config/crd/bases/kuberbac.prosimcorp.com_dynamicrolebindings.yaml index f325efb..bdc919f 100644 --- a/config/crd/bases/kuberbac.prosimcorp.com_dynamicrolebindings.yaml +++ b/config/crd/bases/kuberbac.prosimcorp.com_dynamicrolebindings.yaml @@ -52,6 +52,14 @@ spec: type: string kind: type: string + metaSelector: + description: TODO + properties: + matchLabels: + additionalProperties: + type: string + type: object + type: object nameSelector: description: TODO properties: @@ -65,8 +73,6 @@ spec: type: string negative: type: boolean - required: - - expression type: object type: object namespaceSelector: @@ -86,14 +92,11 @@ spec: type: string negative: type: boolean - required: - - expression type: object type: object required: - apiGroup - kind - - nameSelector type: object required: - clusterRole @@ -110,6 +113,16 @@ spec: targets: description: TODO properties: + annotations: + additionalProperties: + type: string + type: object + labels: + additionalProperties: + type: string + type: object + name: + type: string namespaceSelector: description: TODO properties: @@ -127,12 +140,10 @@ spec: type: string negative: type: boolean - required: - - expression type: object type: object required: - - namespaceSelector + - name type: object required: - source diff --git a/config/samples/kuberbac_v1alpha1_dynamicclusterrole.yaml b/config/samples/kuberbac_v1alpha1_dynamicclusterrole.yaml index e23f650..a01df92 100644 --- a/config/samples/kuberbac_v1alpha1_dynamicclusterrole.yaml +++ b/config/samples/kuberbac_v1alpha1_dynamicclusterrole.yaml @@ -10,6 +10,8 @@ spec: # Desired name for produced ClusterRole target: name: example-policy + annotations: {} + labels: {} # This is where the allowed policies are expressed # Ref: https://kubernetes.io/docs/reference/access-authn-authz/rbac/ diff --git a/config/samples/kuberbac_v1alpha1_dynamicrolebinding.yaml b/config/samples/kuberbac_v1alpha1_dynamicrolebinding.yaml index 48de668..d3c67a3 100644 --- a/config/samples/kuberbac_v1alpha1_dynamicrolebinding.yaml +++ b/config/samples/kuberbac_v1alpha1_dynamicrolebinding.yaml @@ -43,7 +43,18 @@ spec: apiGroup: "" kind: ServiceAccount + # (Optional) + # ServiceAccounts can be selected by some metadata + # This field is mutually exclusive with 'nameSelector' + metaSelector: + + # Select by matching labels + matchLabels: + managed-by: custom-operator + + # (Optional) # ServiceAccount names can be matched by exact name, or a Golang regular expression. + # This field is mutually exclusive with 'metaSelector' # Attention: Only one can be performed. nameSelector: @@ -59,6 +70,7 @@ spec: # negative: false # expression: "^(.*)$" + # (Optional) # To look for a ServiceAccount, namespaces can be matched by exact name, # by their labels, or a Golang regular expression. # Attention: Only one can be performed. @@ -84,6 +96,15 @@ spec: # For those members selected in the previous section targets: + # (Required) + # Name of the RoleBinding objects to be created + name: example-policy + + # Add some metadata to the RoleBinding objects + annotations: {} + labels: {} + + # (Optional) # Target namespaces can be matched by exact name, # by their labels, or a Golang regular expression. # Attention: Only one can be performed. diff --git a/docs/prototype/dynamicRoleBinding.yaml b/docs/prototype/dynamicRoleBinding.yaml index 48de668..c21af6e 100644 --- a/docs/prototype/dynamicRoleBinding.yaml +++ b/docs/prototype/dynamicRoleBinding.yaml @@ -43,7 +43,18 @@ spec: apiGroup: "" kind: ServiceAccount + # (Optional) + # ServiceAccounts can be selected by some metadata + # This field is mutually exclusive with 'nameSelector' + metaSelector: + + # Select by matching labels + matchLabels: + managed-by: custom-operator + + # (Optional) # ServiceAccount names can be matched by exact name, or a Golang regular expression. + # This field is mutually exclusive with 'metaSelector' # Attention: Only one can be performed. nameSelector: @@ -59,6 +70,7 @@ spec: # negative: false # expression: "^(.*)$" + # (Optional) # To look for a ServiceAccount, namespaces can be matched by exact name, # by their labels, or a Golang regular expression. # Attention: Only one can be performed. @@ -84,6 +96,15 @@ spec: # For those members selected in the previous section targets: + # (Required) + # Name of the RoleBinding objects to be created + name: example-policy + + # Add some metadata to the RoleBinding objects + annotations: {} + labels: {} + + # (Optional) # Target namespaces can be matched by exact name, # by their labels, or a Golang regular expression. # Attention: Only one can be performed. @@ -102,4 +123,5 @@ spec: # Select those ServiceAccounts in namespaces different from: kube-system, kube-public or default # matchRegex: # negative: true - # expression: "^(default|kube-system|kube-public)$" \ No newline at end of file + # expression: "^(default|kube-system|kube-public)$" + \ No newline at end of file diff --git a/internal/controller/dynamicrolebinding_sync.go b/internal/controller/dynamicrolebinding_sync.go index 77b425a..6558c84 100644 --- a/internal/controller/dynamicrolebinding_sync.go +++ b/internal/controller/dynamicrolebinding_sync.go @@ -68,6 +68,15 @@ func (r *DynamicRoleBindingReconciler) CheckNamespaceSelector(ctx context.Contex // FilterNamespaceListBySelector returns a list of namespaces that match a namespaceSelector field func (r *DynamicRoleBindingReconciler) FilterNamespaceListBySelector(ctx context.Context, namespaceList *corev1.NamespaceList, namespaceSelector *kuberbacv1alpha1.NamespaceSelectorT) (namespaces []string, err error) { + // Return all namespaces if namespaceSelector is empty + if reflect.ValueOf(*namespaceSelector).IsZero() { + for _, namespace := range namespaceList.Items { + namespaces = append(namespaces, namespace.Name) + } + + return namespaces, err + } + // Check just only field is filled err = r.CheckNamespaceSelector(ctx, namespaceSelector) if err != nil { @@ -123,7 +132,7 @@ func (r *DynamicRoleBindingReconciler) FilterNamespaceListBySelector(ctx context } // GetServiceAccountsBySelectors TODO -func (r *DynamicRoleBindingReconciler) GetServiceAccountsBySelectors(ctx context.Context, filteredNamespaceList []string, nameSelector *kuberbacv1alpha1.NameSelectorT, namespaceSelector *kuberbacv1alpha1.NamespaceSelectorT) (result *corev1.ServiceAccountList, err error) { +func (r *DynamicRoleBindingReconciler) GetServiceAccountsBySelectors(ctx context.Context, filteredNamespaceList []string, subject *kuberbacv1alpha1.DynamicRoleBindingSourceSubject) (result *corev1.ServiceAccountList, err error) { result = &corev1.ServiceAccountList{} @@ -133,10 +142,23 @@ func (r *DynamicRoleBindingReconciler) GetServiceAccountsBySelectors(ctx context return result, err } - // + // Check nameSelector and labelSelector are NOT filled together + if !reflect.ValueOf(subject.NameSelector).IsZero() && !reflect.ValueOf(subject.MetaSelector).IsZero() { + err = fmt.Errorf("nameSelector and labelSelector are mutually exclusive") + return result, err + } + + // Check just only nameSelector is used at once when filled + if !reflect.ValueOf(subject.NameSelector).IsZero() { + if err = r.CheckNameSelector(ctx, &subject.NameSelector); err != nil { + return result, err + } + } + + // Compile regex expression when filled matchRegex := ®exp.Regexp{} - if nameSelector.MatchRegex.Expression != "" { - matchRegex, err = regexp.Compile(nameSelector.MatchRegex.Expression) + if subject.NameSelector.MatchRegex.Expression != "" { + matchRegex, err = regexp.Compile(subject.NameSelector.MatchRegex.Expression) if err != nil { return result, err } @@ -146,13 +168,21 @@ func (r *DynamicRoleBindingReconciler) GetServiceAccountsBySelectors(ctx context for _, serviceAccount := range tmpServiceAccountList.Items { // Ignore namespaces not present in desired list - if !slices.Contains(filteredNamespaceList, serviceAccount.Namespace) { + if len(filteredNamespaceList) != 0 && !slices.Contains(filteredNamespaceList, serviceAccount.Namespace) { continue } - // Matching by fixed list is preferred - if len(nameSelector.MatchList) > 0 { - if slices.Contains(nameSelector.MatchList, serviceAccount.Name) { + // Matching by labels is preferred + if !reflect.ValueOf(subject.MetaSelector.MatchLabels).IsZero() { + if globals.IsSubset(subject.MetaSelector.MatchLabels, serviceAccount.Labels) { + result.Items = append(result.Items, serviceAccount) + } + continue + } + + // Matching by fixed list + if len(subject.NameSelector.MatchList) > 0 { + if slices.Contains(subject.NameSelector.MatchList, serviceAccount.Name) { result.Items = append(result.Items, serviceAccount) } continue @@ -161,12 +191,12 @@ func (r *DynamicRoleBindingReconciler) GetServiceAccountsBySelectors(ctx context // Match by regex nameMatched := matchRegex.MatchString(serviceAccount.Name) - if !nameMatched && nameSelector.MatchRegex.Negative { + if !nameMatched && subject.NameSelector.MatchRegex.Negative { result.Items = append(result.Items, serviceAccount) continue } - if nameMatched && !nameSelector.MatchRegex.Negative { + if nameMatched && !subject.NameSelector.MatchRegex.Negative { result.Items = append(result.Items, serviceAccount) } @@ -185,26 +215,12 @@ func (r *DynamicRoleBindingReconciler) SyncTarget(ctx context.Context, resource return err } - // Enforce source.nameSelector has only one type of selector filled - err = r.CheckNameSelector(ctx, &resource.Spec.Source.Subject.NameSelector) - if err != nil { - err = fmt.Errorf("source.subject.nameSelector is invalid: %s", err.Error()) - return err - } - // Check namespaceSelector does NOT exist for subjects other than ServiceAccount if slices.Contains([]string{"Group", "User"}, resource.Spec.Source.Subject.Kind) && - !reflect.ValueOf(resource.Spec.Source.Subject.NamespaceSelector).IsZero() { + (!reflect.ValueOf(resource.Spec.Source.Subject.NamespaceSelector).IsZero() || + !reflect.ValueOf(resource.Spec.Source.Subject.MetaSelector).IsZero()) { - err = fmt.Errorf("namespaceSelector is not allowed for subjects other than ServiceAccount") - return err - } - - // Enforce namespaceSelector for ServiceAccount subjects - if resource.Spec.Source.Subject.Kind == "ServiceAccount" && - reflect.ValueOf(resource.Spec.Source.Subject.NamespaceSelector).IsZero() { - - err = fmt.Errorf("namespaceSelector is required for ServiceAccount subjects") + err = fmt.Errorf("namespaceSelector and labelSelector are only allowed for ServiceAccount subjects") return err } @@ -216,10 +232,11 @@ func (r *DynamicRoleBindingReconciler) SyncTarget(ctx context.Context, resource } // - sourceFilteredNamespaces, err := r.FilterNamespaceListBySelector(ctx, namespaceList, &resource.Spec.Source.Subject.NamespaceSelector) + subjectFilteredNamespaces, err := r.FilterNamespaceListBySelector(ctx, namespaceList, &resource.Spec.Source.Subject.NamespaceSelector) if err != nil { return err } + targetFilteredNamespaces, err := r.FilterNamespaceListBySelector(ctx, namespaceList, &resource.Spec.Targets.NamespaceSelector) if err != nil { return err @@ -232,11 +249,18 @@ func (r *DynamicRoleBindingReconciler) SyncTarget(ctx context.Context, resource if slices.Contains([]string{"Group", "User"}, resource.Spec.Source.Subject.Kind) { // MatchRegex nameSelector is not allowed for these subjects + // TODO: Stop or not the process flow????? if !reflect.ValueOf(resource.Spec.Source.Subject.NameSelector.MatchRegex).IsZero() { err = fmt.Errorf("MatchRegex nameSelector is not allowed for subjects: Group, User") return err } + // MatchList nameSelector is required for these subjects + if reflect.ValueOf(resource.Spec.Source.Subject.NameSelector.MatchList).IsZero() { + err = fmt.Errorf("MatchList nameSelector is required for subjects: Group, User") + return err + } + // for _, listItem := range resource.Spec.Source.Subject.NameSelector.MatchList { expandedSubjects = append(expandedSubjects, rbacv1.Subject{ @@ -250,8 +274,7 @@ func (r *DynamicRoleBindingReconciler) SyncTarget(ctx context.Context, resource // Expand ServiceAccount subjects if resource.Spec.Source.Subject.Kind == "ServiceAccount" { - serviceAccounts, err := r.GetServiceAccountsBySelectors(ctx, sourceFilteredNamespaces, - &resource.Spec.Source.Subject.NameSelector, &resource.Spec.Source.Subject.NamespaceSelector) + serviceAccounts, err := r.GetServiceAccountsBySelectors(ctx, subjectFilteredNamespaces, &resource.Spec.Source.Subject) if err != nil { err = fmt.Errorf("error getting selected ServiceAccounts: %s", err.Error()) return err @@ -275,15 +298,16 @@ func (r *DynamicRoleBindingReconciler) SyncTarget(ctx context.Context, resource "kuberbac.prosimcorp.com/owner-namespace": resource.ObjectMeta.Namespace, } - if len(resource.ObjectMeta.Annotations) == 0 { - resource.ObjectMeta.Labels = map[string]string{} + if len(resource.Spec.Targets.Annotations) == 0 { + resource.Spec.Targets.Annotations = map[string]string{} } - maps.Copy(resource.ObjectMeta.Annotations, referenceAnnotations) + maps.Copy(resource.Spec.Targets.Annotations, referenceAnnotations) roleBindingResource := rbacv1.RoleBinding{ ObjectMeta: metav1.ObjectMeta{ - Name: resource.ObjectMeta.Name, - Annotations: resource.ObjectMeta.Annotations, + Name: resource.Spec.Targets.Name, + Labels: resource.Spec.Targets.Labels, + Annotations: resource.Spec.Targets.Annotations, }, RoleRef: rbacv1.RoleRef{ APIGroup: "rbac.authorization.k8s.io", @@ -293,22 +317,42 @@ func (r *DynamicRoleBindingReconciler) SyncTarget(ctx context.Context, resource Subjects: expandedSubjects, } + // Get Rolebindings + existentRoleBindingList := rbacv1.RoleBindingList{} + err = r.Client.List(ctx, &existentRoleBindingList) + if err != nil { + return err + } + // Create the RoleBinding resource on targeted namespaces for _, namespace := range targetFilteredNamespaces { roleBindingResource.SetNamespace(namespace) + + // Check potential already existing RoleBindings that match the same name and namespace + roleBindingFound := false + for _, roleBinding := range existentRoleBindingList.Items { + + if roleBinding.Namespace != namespace || roleBinding.Name != roleBindingResource.Name { + continue + } + + if !globals.IsSubset(roleBindingResource.Annotations, roleBinding.Annotations) { + roleBindingFound = true + break + } + } + + if roleBindingFound { + continue + } + + // Finally, update it!! err = r.Client.Update(ctx, roleBindingResource.DeepCopy()) if err != nil { log.Printf("error updating RoleBinding: %s", err.Error()) } } - // Get Rolebindings - existentRoleBindingList := rbacv1.RoleBindingList{} - err = r.Client.List(ctx, &existentRoleBindingList) - if err != nil { - return err - } - // For cleaning potential previous abandoned resources, get the list of namespaces // that are not reconciled in this loop to look for RoleBindings there targetNamespacesComplementaryList := slices.DeleteFunc(namespaceList.Items,