From 0a35437fd5378c8570798794c6df20aca7da1f11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alby=20Hern=C3=A1ndez?= Date: Thu, 8 Aug 2024 01:40:36 +0100 Subject: [PATCH 1/3] feat: Add separateScopes flag on DynamicClusterRole --- api/v1alpha1/dynamicclusterrole_types.go | 5 +- ...ac.prosimcorp.com_dynamicclusterroles.yaml | 2 + .../kuberbac_v1alpha1_dynamicclusterrole.yaml | 4 + docs/prototype/dynamicClusterRole.yaml | 4 + .../controller/dynamicclusterrole_sync.go | 75 +++++++++++++++++-- 5 files changed, 83 insertions(+), 7 deletions(-) diff --git a/api/v1alpha1/dynamicclusterrole_types.go b/api/v1alpha1/dynamicclusterrole_types.go index 4d1929a..f119819 100644 --- a/api/v1alpha1/dynamicclusterrole_types.go +++ b/api/v1alpha1/dynamicclusterrole_types.go @@ -23,9 +23,12 @@ import ( // TargetT defines the spec of the target section of a DynamicClusterRole type TargetT struct { - Name string `json:"name"` + Name string `json:"name"` + Annotations map[string]string `json:"annotations,omitempty"` Labels map[string]string `json:"labels,omitempty"` + + SeparateScopes bool `json:"separateScopes,omitempty"` } // DynamicClusterRoleSpec defines the desired state of DynamicClusterRole diff --git a/config/crd/bases/kuberbac.prosimcorp.com_dynamicclusterroles.yaml b/config/crd/bases/kuberbac.prosimcorp.com_dynamicclusterroles.yaml index 50899eb..960f43f 100644 --- a/config/crd/bases/kuberbac.prosimcorp.com_dynamicclusterroles.yaml +++ b/config/crd/bases/kuberbac.prosimcorp.com_dynamicclusterroles.yaml @@ -159,6 +159,8 @@ spec: type: object name: type: string + separateScopes: + type: boolean required: - name type: object diff --git a/config/samples/kuberbac_v1alpha1_dynamicclusterrole.yaml b/config/samples/kuberbac_v1alpha1_dynamicclusterrole.yaml index a01df92..9d68026 100644 --- a/config/samples/kuberbac_v1alpha1_dynamicclusterrole.yaml +++ b/config/samples/kuberbac_v1alpha1_dynamicclusterrole.yaml @@ -13,6 +13,10 @@ spec: annotations: {} labels: {} + # This flag create two separated ClusterRoles: + # one for cluster-wide resources and another for namespace-scoped resources + separateScopes: false + # This is where the allowed policies are expressed # Ref: https://kubernetes.io/docs/reference/access-authn-authz/rbac/ allow: diff --git a/docs/prototype/dynamicClusterRole.yaml b/docs/prototype/dynamicClusterRole.yaml index c2bb7e9..cb93901 100644 --- a/docs/prototype/dynamicClusterRole.yaml +++ b/docs/prototype/dynamicClusterRole.yaml @@ -13,6 +13,10 @@ spec: annotations: {} labels: {} + # This flag create two separated ClusterRoles: + # one for cluster-wide resources and another for namespace-scoped resources + separateScopes: false + # This is where the allowed policies are expressed # Ref: https://kubernetes.io/docs/reference/access-authn-authz/rbac/ allow: diff --git a/internal/controller/dynamicclusterrole_sync.go b/internal/controller/dynamicclusterrole_sync.go index 05b7e71..f4ec39d 100644 --- a/internal/controller/dynamicclusterrole_sync.go +++ b/internal/controller/dynamicclusterrole_sync.go @@ -27,6 +27,10 @@ type GVKR struct { GVK schema.GroupVersionKind Resource string Subresource string + + // + Namespaced bool + UsableVerbs []string // Intended for future use polishing resulting verbs } // PolicyRulesProcessorT represents the things done @@ -102,6 +106,8 @@ func (p *PolicyRulesProcessorT) SetResourcesByGroup() (err error) { Version: version, Kind: apiResource.Kind, }, + Namespaced: apiResource.Namespaced, + UsableVerbs: apiResource.Verbs, }) } } @@ -520,6 +526,39 @@ func (p *PolicyRulesProcessorT) EvaluatePolicyRules(allowMap, denyMap map[string return result, err } +// SplitPolicyRules separates PolicyRules into two lists: clusterScopedRules and namespaceScopedRules +func (p *PolicyRulesProcessorT) SplitPolicyRules(policyRules []rbacv1.PolicyRule) (clusterScopedRules, namespaceScopedRules []rbacv1.PolicyRule) { + + for _, policyRule := range policyRules { + + // Look for current PolicyRule in the resourcesByGroup map + for _, resource := range p.ResourcesByGroup[policyRule.APIGroups[0]] { + + // + resourceName := resource.Resource + if resource.Subresource != "" { + resourceName += "/" + resource.Subresource + } + + // Ignore when it is not the correct resource + if policyRule.Resources[0] != resourceName { + continue + } + + // Add to the corresponding list + if resource.Namespaced { + namespaceScopedRules = append(namespaceScopedRules, policyRule) + } else { + clusterScopedRules = append(clusterScopedRules, policyRule) + } + + break + } + } + + return clusterScopedRules, namespaceScopedRules +} + // GetSyncTime return the spec.synchronization.time as duration, or default time on failures func (r *DynamicClusterRoleReconciler) GetSyncTime(resource *kuberbacv1alpha1.DynamicClusterRole) (syncTime time.Duration, err error) { @@ -540,15 +579,16 @@ func (r *DynamicClusterRoleReconciler) SyncTarget(ctx context.Context, resource return fmt.Errorf("error generating PolicyRulesProcessor: %s", err.Error()) } - // Convert '*' + // Transform '*' symbols with actual things expandedAllowList := policyRulesProcessor.ExpandPolicyRules(resource.Spec.Allow) expandedDenyList := policyRulesProcessor.ExpandPolicyRules(resource.Spec.Deny) - // + // Stretch policy rules to a single resource per item stretchAllowList := policyRulesProcessor.StretchPolicyRules(expandedAllowList) stretchDenyList := policyRulesProcessor.StretchPolicyRules(expandedDenyList) - // + // Craft a map with stretched policy rules. Its keys are created as unique identifiers. + // This is done to increase performance when evaluating the rules. allowMap := policyRulesProcessor.GetMapFromStretchedPolicyRules(stretchAllowList) denyMap := policyRulesProcessor.GetMapFromStretchedPolicyRules(stretchDenyList) @@ -564,6 +604,10 @@ func (r *DynamicClusterRoleReconciler) SyncTarget(ctx context.Context, resource return fmt.Errorf("error evaluating allow and deny maps: %s", err.Error()) } + // Create a list of ClusterRoles to be created. + // We assume always only one ClusterRole, but this will be transformed into two when asked to separate scopes. + clusterRoles := []rbacv1.ClusterRole{} + clusterRoleResource := rbacv1.ClusterRole{ ObjectMeta: metav1.ObjectMeta{ Name: resource.Spec.Target.Name, @@ -573,10 +617,29 @@ func (r *DynamicClusterRoleReconciler) SyncTarget(ctx context.Context, resource Rules: maps.Values(result), // TODO: Implement AggregationRules later } + clusterRoles = append(clusterRoles, clusterRoleResource) - err = r.Client.Update(ctx, &clusterRoleResource) - if err != nil { - err = fmt.Errorf("error updating ClusterRole: %s", err.Error()) + // + if resource.Spec.Target.SeparateScopes { + clusterScopedRules, namespaceScopedRules := policyRulesProcessor.SplitPolicyRules(maps.Values(result)) + + // Assume first ClusterRole as clusterScoped + clusterRoles[0].Rules = clusterScopedRules + clusterRoles[0].Name = resource.Spec.Target.Name + "-cluster" + + // Create a new ClusterRole for namespaceScoped + clusterRoles = append(clusterRoles, *clusterRoleResource.DeepCopy()) + clusterRoles[1].Rules = namespaceScopedRules + clusterRoles[1].Name = resource.Spec.Target.Name + "-namespace" + } + + // + for _, clusterRole := range clusterRoles { + err = r.Client.Update(ctx, &clusterRole) + if err != nil { + err = fmt.Errorf("error updating ClusterRole: %s", err.Error()) + break + } } return err From 8da59857ce273a0e7b2506e14a182c4663764e34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alby=20Hern=C3=A1ndez?= Date: Thu, 8 Aug 2024 01:45:09 +0100 Subject: [PATCH 2/3] fix: Forgot README example --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 916deec..98d1fd4 100644 --- a/README.md +++ b/README.md @@ -112,6 +112,10 @@ spec: annotations: {} labels: {} + # This flag create two separated ClusterRoles: + # one for cluster-wide resources and another for namespace-scoped resources + separateScopes: false + # This is where the allowed policies are expressed # Ref: https://kubernetes.io/docs/reference/access-authn-authz/rbac/ allow: From 495ce956f36bf0ebbf1b6e644a9ee3e3036d813a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alby=20Hern=C3=A1ndez?= Date: Thu, 8 Aug 2024 01:50:28 +0100 Subject: [PATCH 3/3] feat: Add reference annotations to DynamicClusterRole --- internal/controller/dynamicclusterrole_sync.go | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/internal/controller/dynamicclusterrole_sync.go b/internal/controller/dynamicclusterrole_sync.go index f4ec39d..7603d8d 100644 --- a/internal/controller/dynamicclusterrole_sync.go +++ b/internal/controller/dynamicclusterrole_sync.go @@ -608,10 +608,21 @@ func (r *DynamicClusterRoleReconciler) SyncTarget(ctx context.Context, resource // We assume always only one ClusterRole, but this will be transformed into two when asked to separate scopes. clusterRoles := []rbacv1.ClusterRole{} + referenceAnnotations := map[string]string{ + "kuberbac.prosimcorp.com/owner-apiversion": resource.APIVersion, + "kuberbac.prosimcorp.com/owner-kind": resource.Kind, + "kuberbac.prosimcorp.com/owner-name": resource.ObjectMeta.Name, + "kuberbac.prosimcorp.com/owner-namespace": resource.ObjectMeta.Namespace, + } + + if len(resource.Spec.Target.Annotations) == 0 { + resource.Spec.Target.Annotations = map[string]string{} + } + clusterRoleResource := rbacv1.ClusterRole{ ObjectMeta: metav1.ObjectMeta{ Name: resource.Spec.Target.Name, - Annotations: resource.Spec.Target.Annotations, + Annotations: referenceAnnotations, Labels: resource.Spec.Target.Labels, }, Rules: maps.Values(result),