Skip to content

Commit 27659b8

Browse files
committed
Add support for ClusterRole AggregationRule during bundle generation
ClusterRoles that use an AggregationRule often do not have any rules defined directly. Instead, their rules are aggregated from other ClusterRoles that match the AggregationRule’s label selector. The existing generator logic only included rules from ClusterRoles that were explicitly bound via ClusterRoleBindings to the ServiceAccounts used by Deployments. Since ClusterRoles with an AggregationRule typically lack direct rule definitions, the resulting permission bundle ended up being empty. This update adds support for handling ClusterRoles that use an AggregationRule. Signed-off-by: Maciej Zimnoch <[email protected]>
1 parent bbee393 commit 27659b8

File tree

3 files changed

+177
-12
lines changed

3 files changed

+177
-12
lines changed
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# entries is a list of entries to include in
2+
# release notes and/or the migration guide
3+
entries:
4+
- description: >
5+
The bundle generator now includes policy rules from ClusterRoles originating through AggregationRule.
6+
Previously, only policy rules of ClusterRoles explicitly bound to ServiceAccounts via ClusterRoleBindings were included.
7+
With this update, the generator also aggregates and applies rules from matching ClusterRoles,
8+
ensuring proper RBAC coverage.
9+
10+
11+
# kind is one of:
12+
# - addition
13+
# - change
14+
# - deprecation
15+
# - removal
16+
# - bugfix
17+
kind: addition
18+
19+
# Is this a breaking change?
20+
breaking: false
21+
22+
# NOTE: ONLY USE `pull_request_override` WHEN ADDING THIS
23+
# FILE FOR A PREVIOUSLY MERGED PULL_REQUEST!
24+
#
25+
# The generator auto-detects the PR number from the commit
26+
# message in which this file was originally added.
27+
#
28+
# What is the pull request number (without the "#")?
29+
# pull_request_override: 0
30+
31+
32+
# Migration can be defined to automatically add a section to
33+
# the migration guide. This is required for breaking changes.
34+
# migration:
35+
# header: Header text for the migration section
36+
# body: |
37+
# Body of the migration section. This should be formatted as markdown and can
38+
# span multiple lines.
39+
#
40+
# Using the YAML string '|' operator means that newlines in this string will
41+
# be honored and interpretted as newlines in the rendered markdown.

internal/generate/clusterserviceversion/clusterserviceversion_updaters.go

Lines changed: 73 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"encoding/json"
1919
"errors"
2020
"fmt"
21+
"slices"
2122
"sort"
2223
"strings"
2324
"time"
@@ -31,6 +32,9 @@ import (
3132
corev1 "k8s.io/api/core/v1"
3233
rbacv1 "k8s.io/api/rbac/v1"
3334
apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
35+
"k8s.io/apimachinery/pkg/api/equality"
36+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
37+
"k8s.io/apimachinery/pkg/labels"
3438
"sigs.k8s.io/controller-runtime/pkg/client"
3539

3640
"github.com/operator-framework/operator-sdk/internal/generate/collector"
@@ -57,8 +61,14 @@ func apply(c *collector.Manifests, csv *operatorsv1alpha1.ClusterServiceVersion,
5761
switch strategy.StrategyName {
5862
case operatorsv1alpha1.InstallStrategyNameDeployment:
5963
inPerms, inCPerms, _ := c.SplitCSVPermissionsObjects(extraSAs)
60-
applyRoles(c, inPerms, &strategy.StrategySpec, extraSAs)
61-
applyClusterRoles(c, inCPerms, &strategy.StrategySpec, extraSAs)
64+
err := applyRoles(c, inPerms, &strategy.StrategySpec, extraSAs)
65+
if err != nil {
66+
return fmt.Errorf("can't apply roles: %w", err)
67+
}
68+
err = applyClusterRoles(c, inCPerms, &strategy.StrategySpec, extraSAs)
69+
if err != nil {
70+
return fmt.Errorf("can't apply cluster roles: %w", err)
71+
}
6272
applyDeployments(c, &strategy.StrategySpec)
6373
}
6474
csv.Spec.InstallStrategy = strategy
@@ -88,7 +98,7 @@ const defaultServiceAccountName = "default"
8898

8999
// applyRoles applies Roles to strategy's permissions field by combining Roles bound to ServiceAccounts
90100
// into one set of permissions.
91-
func applyRoles(c *collector.Manifests, objs []client.Object, strategy *operatorsv1alpha1.StrategyDetailsDeployment, extraSAs []string) { //nolint:dupl
101+
func applyRoles(c *collector.Manifests, objs []client.Object, strategy *operatorsv1alpha1.StrategyDetailsDeployment, extraSAs []string) error { //nolint:dupl
92102
roleSet := make(map[string]rbacv1.Role)
93103
cRoleSet := make(map[string]rbacv1.ClusterRole)
94104
for i := range objs {
@@ -120,8 +130,16 @@ func applyRoles(c *collector.Manifests, objs []client.Object, strategy *operator
120130
hasRole = has
121131
case "ClusterRole":
122132
role, has := cRoleSet[binding.RoleRef.Name]
123-
rules = role.Rules
133+
124134
hasRole = has
135+
if has {
136+
var err error
137+
rules, err = getClusterRoleRules(role, c.ClusterRoles)
138+
if err != nil {
139+
return fmt.Errorf("can't get ClusterRole rules: %w", err)
140+
}
141+
}
142+
125143
default:
126144
continue
127145
}
@@ -143,11 +161,13 @@ func applyRoles(c *collector.Manifests, objs []client.Object, strategy *operator
143161
return perms[i].ServiceAccountName < perms[j].ServiceAccountName
144162
})
145163
strategy.Permissions = perms
164+
165+
return nil
146166
}
147167

148168
// applyClusterRoles applies ClusterRoles to strategy's clusterPermissions field by combining ClusterRoles
149169
// bound to ServiceAccounts into one set of clusterPermissions.
150-
func applyClusterRoles(c *collector.Manifests, objs []client.Object, strategy *operatorsv1alpha1.StrategyDetailsDeployment, extraSAs []string) { //nolint:dupl
170+
func applyClusterRoles(c *collector.Manifests, objs []client.Object, strategy *operatorsv1alpha1.StrategyDetailsDeployment, extraSAs []string) error { //nolint:dupl
151171
roleSet := make(map[string]rbacv1.ClusterRole)
152172
for i := range objs {
153173
switch t := objs[i].(type) {
@@ -166,7 +186,12 @@ func applyClusterRoles(c *collector.Manifests, objs []client.Object, strategy *o
166186
continue
167187
}
168188
if role, hasRole := roleSet[binding.RoleRef.Name]; hasRole {
169-
perm.Rules = append(perm.Rules, role.Rules...)
189+
rules, err := getClusterRoleRules(role, c.ClusterRoles)
190+
if err != nil {
191+
return fmt.Errorf("can't get ClusterRole rules: %w", err)
192+
}
193+
194+
perm.Rules = append(perm.Rules, rules...)
170195
saToPermissions[subject.Name] = perm
171196
}
172197
}
@@ -183,6 +208,48 @@ func applyClusterRoles(c *collector.Manifests, objs []client.Object, strategy *o
183208
return perms[i].ServiceAccountName < perms[j].ServiceAccountName
184209
})
185210
strategy.ClusterPermissions = perms
211+
212+
return nil
213+
}
214+
215+
// getClusterRoleRules returns all PolicyRules for a given ClusterRole, including rules from aggregated ClusterRoles
216+
// as specified by the AggregationRule, ensuring no duplicate rules are added.
217+
func getClusterRoleRules(clusterRole rbacv1.ClusterRole, clusterRoles []rbacv1.ClusterRole) ([]rbacv1.PolicyRule, error) {
218+
rules := make([]rbacv1.PolicyRule, 0, len(clusterRole.Rules))
219+
rules = append(rules, clusterRole.Rules...)
220+
221+
if clusterRole.AggregationRule == nil {
222+
return rules, nil
223+
}
224+
225+
for _, crSelector := range clusterRole.AggregationRule.ClusterRoleSelectors {
226+
labelSelector, err := metav1.LabelSelectorAsSelector(&crSelector)
227+
if err != nil {
228+
return nil, fmt.Errorf("can't create label selector from ClusterRole %q AggregationRule: %w", clusterRole.Name, err)
229+
}
230+
231+
for _, cr := range clusterRoles {
232+
if cr.Name == clusterRole.Name {
233+
continue
234+
}
235+
236+
if !labelSelector.Matches(labels.Set(cr.Labels)) {
237+
continue
238+
}
239+
240+
for _, rule := range cr.Rules {
241+
ruleExists := slices.ContainsFunc(rules, func(r rbacv1.PolicyRule) bool {
242+
return equality.Semantic.DeepEqual(rule, r)
243+
})
244+
245+
if !ruleExists {
246+
rules = append(rules, rule)
247+
}
248+
}
249+
}
250+
}
251+
252+
return rules, nil
186253
}
187254

188255
// initPermissionSet initializes a map of ServiceAccount name to permissions, which are empty.

internal/generate/clusterserviceversion/clusterserviceversion_updaters_test.go

Lines changed: 63 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
rbacv1 "k8s.io/api/rbac/v1"
2525
apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
2626
apiextv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
27+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2728
"k8s.io/apimachinery/pkg/labels"
2829
"sigs.k8s.io/controller-runtime/pkg/client"
2930

@@ -65,6 +66,9 @@ var _ = Describe("apply functions", func() {
6566
saName1 = "service-account-1"
6667
roleName1 = "role-1"
6768
cRoleName1 = "cluster-role-1"
69+
cRoleName2 = "cluster-role-2"
70+
cRoleName3 = "cluster-role-3"
71+
cRoleName4 = "cluster-role-4"
6872
)
6973

7074
BeforeEach(func() {
@@ -79,7 +83,8 @@ var _ = Describe("apply functions", func() {
7983
rules := []rbacv1.PolicyRule{{Verbs: []string{"create"}}}
8084
perms := []client.Object{newRole(roleName1, rules...)}
8185
c.RoleBindings = []rbacv1.RoleBinding{newRoleBinding("role-binding", newRoleRef(roleName1), newServiceAccountSubject(saName1))}
82-
applyRoles(c, perms, strategy, nil)
86+
err := applyRoles(c, perms, strategy, nil)
87+
Expect(err).NotTo(HaveOccurred())
8388
Expect(strategy.Permissions).To(Equal([]operatorsv1alpha1.StrategyDeploymentPermissions{
8489
{ServiceAccountName: saName1, Rules: rules},
8590
}))
@@ -90,7 +95,55 @@ var _ = Describe("apply functions", func() {
9095
rules := []rbacv1.PolicyRule{{Verbs: []string{"create"}}}
9196
perms := []client.Object{newClusterRole(cRoleName1, rules...)}
9297
c.ClusterRoleBindings = []rbacv1.ClusterRoleBinding{newClusterRoleBinding("cluster-role-binding", newClusterRoleRef(cRoleName1), newServiceAccountSubject(saName1))}
93-
applyClusterRoles(c, perms, strategy, nil)
98+
err := applyClusterRoles(c, perms, strategy, nil)
99+
Expect(err).NotTo(HaveOccurred())
100+
Expect(strategy.ClusterPermissions).To(Equal([]operatorsv1alpha1.StrategyDeploymentPermissions{
101+
{ServiceAccountName: saName1, Rules: rules},
102+
}))
103+
})
104+
It("adds rules from aggregated ClusterRoles eliminating duplicates to the CSV deployment strategy", func() {
105+
c.Deployments = []appsv1.Deployment{newDeploymentWithServiceAccount(depName1, saName1)}
106+
c.ServiceAccounts = []corev1.ServiceAccount{newServiceAccount(saName1)}
107+
rules := []rbacv1.PolicyRule{{Verbs: []string{"create"}}, {Verbs: []string{"update"}}}
108+
var emptyRules []rbacv1.PolicyRule
109+
perms := []client.Object{
110+
func() *rbacv1.ClusterRole {
111+
cr := newClusterRole(cRoleName1, emptyRules...)
112+
cr.AggregationRule = &rbacv1.AggregationRule{
113+
ClusterRoleSelectors: []metav1.LabelSelector{
114+
{
115+
MatchLabels: map[string]string{
116+
"aggregate-to-cluster-role-1": "true",
117+
},
118+
},
119+
},
120+
}
121+
return cr
122+
}(),
123+
func() *rbacv1.ClusterRole {
124+
cr := newClusterRole(cRoleName2, rules...)
125+
cr.Labels = map[string]string{
126+
"aggregate-to-cluster-role-1": "true",
127+
}
128+
return cr
129+
}(),
130+
func() *rbacv1.ClusterRole {
131+
cr := newClusterRole(cRoleName3, rules...)
132+
cr.Labels = map[string]string{
133+
"aggregate-to-cluster-role-1": "true",
134+
}
135+
return cr
136+
}(),
137+
// ClusterRole not bound to any ServiceAccount, nor matching any ClusterRule AggregationRule,
138+
// it shouldn't land in strategy ClusterPermissions.
139+
newClusterRole(cRoleName4, []rbacv1.PolicyRule{{Verbs: []string{"delete"}}}...),
140+
}
141+
for _, cr := range perms {
142+
c.ClusterRoles = append(c.ClusterRoles, *cr.(*rbacv1.ClusterRole))
143+
}
144+
c.ClusterRoleBindings = []rbacv1.ClusterRoleBinding{newClusterRoleBinding("cluster-role-binding", newClusterRoleRef(cRoleName1), newServiceAccountSubject(saName1))}
145+
err := applyClusterRoles(c, perms, strategy, nil)
146+
Expect(err).NotTo(HaveOccurred())
94147
Expect(strategy.ClusterPermissions).To(Equal([]operatorsv1alpha1.StrategyDeploymentPermissions{
95148
{ServiceAccountName: saName1, Rules: rules},
96149
}))
@@ -128,8 +181,10 @@ var _ = Describe("apply functions", func() {
128181
newClusterRoleBinding("cluster-role-binding-2", newClusterRoleRef(cRoleName2), newServiceAccountSubject(extraSAName)),
129182
newClusterRoleBinding("cluster-role-binding-3", newClusterRoleRef(cRoleName3), newServiceAccountSubject(extraSAName)),
130183
}
131-
applyRoles(c, perms, strategy, []string{extraSAName})
132-
applyClusterRoles(c, cperms, strategy, []string{extraSAName})
184+
err := applyRoles(c, perms, strategy, []string{extraSAName})
185+
Expect(err).NotTo(HaveOccurred())
186+
err = applyClusterRoles(c, cperms, strategy, []string{extraSAName})
187+
Expect(err).NotTo(HaveOccurred())
133188
Expect(strategy.Permissions).To(Equal([]operatorsv1alpha1.StrategyDeploymentPermissions{
134189
{ServiceAccountName: saName1, Rules: rules},
135190
{ServiceAccountName: extraSAName, Rules: rules},
@@ -146,14 +201,16 @@ var _ = Describe("apply functions", func() {
146201
c.Deployments = []appsv1.Deployment{newDeploymentWithServiceAccount(depName1, saName1)}
147202
c.ServiceAccounts = []corev1.ServiceAccount{newServiceAccount(saName1)}
148203
c.RoleBindings = []rbacv1.RoleBinding{newRoleBinding("role-binding", newRoleRef(roleName1), newServiceAccountSubject(saName1))}
149-
applyRoles(c, nil, strategy, nil)
204+
err := applyRoles(c, nil, strategy, nil)
205+
Expect(err).NotTo(HaveOccurred())
150206
Expect(strategy.Permissions).To(Equal([]operatorsv1alpha1.StrategyDeploymentPermissions{}))
151207
})
152208
It("adds no ClusterPermissions to the CSV deployment strategy", func() {
153209
c.Deployments = []appsv1.Deployment{newDeploymentWithServiceAccount(depName1, saName1)}
154210
c.ServiceAccounts = []corev1.ServiceAccount{newServiceAccount(saName1)}
155211
c.ClusterRoleBindings = []rbacv1.ClusterRoleBinding{newClusterRoleBinding("cluster-role-binding", newClusterRoleRef(cRoleName1), newServiceAccountSubject(saName1))}
156-
applyClusterRoles(c, nil, strategy, nil)
212+
err := applyClusterRoles(c, nil, strategy, nil)
213+
Expect(err).NotTo(HaveOccurred())
157214
Expect(strategy.ClusterPermissions).To(Equal([]operatorsv1alpha1.StrategyDeploymentPermissions{}))
158215
})
159216
})

0 commit comments

Comments
 (0)