Skip to content

Commit 8eb4f3e

Browse files
pkg/controller: label RBAC with content hash (#3034)
When a CSV is processed, it is assumed that the InstallPlan has already run, or that a user that's creating a CSV as their entrypoint into the system has otherwise met all the preconditions for the CSV to exist. As part of validating these preconditions, the CSV logic today uses cluster-scoped listers for all RBAC resources. sets up an in-memory authorizer for these, and queries the CSV install strategie's permissions against those. We would like to restrict the amount of memory OLM uses, and part of that is not caching the world. For the above approach to work, all RBAC objects fulfilling CSV permission preconditions would need to be labelled. In the case that a user is creating a CSV manually, this will not be the case. We can use the SubjectAccessReview API to check for the presence of permissions without caching the world, but since a PolicyRule has slices of verbs, resources, subjects, etc and the SAR endpoint accepts but one of each, there will be (in the general case) a combinatorical explosion of calls to issue enough SARs to cover the full set of permissions. Therefore, we need to limit the amount of times we take that action. A simple optimization is to check for permissions created directly by OLM, as that's by far the most common entrypoint into the system (a user creates a Subscription, that triggers an InstallPlan, which creates the RBAC). As OLM chose to name the RBAC objects with random strings of characters, it's not possible to look at a list of permissions in a CSV and know which resources OLM would have created. Therefore, this PR adds a label to all relevant RBAC resources with the hash of their content. We already have the name of the CSV, but since CSV content is ostensibly mutable, this is not enough. Signed-off-by: Steve Kuznetsov <[email protected]>
1 parent 46f4f89 commit 8eb4f3e

File tree

6 files changed

+183
-8
lines changed

6 files changed

+183
-8
lines changed

pkg/controller/operators/catalog/operator.go

+26-3
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,6 @@ type Operator struct {
108108
client versioned.Interface
109109
dynamicClient dynamic.Interface
110110
lister operatorlister.OperatorLister
111-
k8sLabelQueueSets map[schema.GroupVersionResource]workqueue.RateLimitingInterface
112111
catsrcQueueSet *queueinformer.ResourceQueueSet
113112
subQueueSet *queueinformer.ResourceQueueSet
114113
ipQueueSet *queueinformer.ResourceQueueSet
@@ -202,7 +201,6 @@ func NewOperator(ctx context.Context, kubeconfigPath string, clock utilclock.Clo
202201
lister: lister,
203202
namespace: operatorNamespace,
204203
recorder: eventRecorder,
205-
k8sLabelQueueSets: map[schema.GroupVersionResource]workqueue.RateLimitingInterface{},
206204
catsrcQueueSet: queueinformer.NewEmptyResourceQueueSet(),
207205
subQueueSet: queueinformer.NewEmptyResourceQueueSet(),
208206
ipQueueSet: queueinformer.NewEmptyResourceQueueSet(),
@@ -386,11 +384,12 @@ func NewOperator(ctx context.Context, kubeconfigPath string, clock utilclock.Clo
386384
if canFilter {
387385
return nil
388386
}
389-
op.k8sLabelQueueSets[gvr] = workqueue.NewRateLimitingQueueWithConfig(workqueue.DefaultControllerRateLimiter(), workqueue.RateLimitingQueueConfig{
387+
queue := workqueue.NewRateLimitingQueueWithConfig(workqueue.DefaultControllerRateLimiter(), workqueue.RateLimitingQueueConfig{
390388
Name: gvr.String(),
391389
})
392390
queueInformer, err := queueinformer.NewQueueInformer(
393391
ctx,
392+
queueinformer.WithQueue(queue),
394393
queueinformer.WithLogger(op.logger),
395394
queueinformer.WithInformer(informer),
396395
queueinformer.WithSyncer(sync.ToSyncer()),
@@ -416,6 +415,18 @@ func NewOperator(ctx context.Context, kubeconfigPath string, clock utilclock.Clo
416415
)); err != nil {
417416
return nil, err
418417
}
418+
if err := labelObjects(rolesgvk, roleInformer.Informer(), labeller.ContentHashLabeler[*rbacv1.Role, *rbacv1applyconfigurations.RoleApplyConfiguration](
419+
ctx, op.logger, labeller.ContentHashFilter,
420+
func(role *rbacv1.Role) (string, error) {
421+
return resolver.PolicyRuleHashLabelValue(role.Rules)
422+
},
423+
rbacv1applyconfigurations.Role,
424+
func(namespace string, ctx context.Context, cfg *rbacv1applyconfigurations.RoleApplyConfiguration, opts metav1.ApplyOptions) (*rbacv1.Role, error) {
425+
return op.opClient.KubernetesInterface().RbacV1().Roles(namespace).Apply(ctx, cfg, opts)
426+
},
427+
)); err != nil {
428+
return nil, err
429+
}
419430

420431
// Wire RoleBindings
421432
roleBindingInformer := k8sInformerFactory.Rbac().V1().RoleBindings()
@@ -432,6 +443,18 @@ func NewOperator(ctx context.Context, kubeconfigPath string, clock utilclock.Clo
432443
)); err != nil {
433444
return nil, err
434445
}
446+
if err := labelObjects(rolebindingsgvk, roleBindingInformer.Informer(), labeller.ContentHashLabeler[*rbacv1.RoleBinding, *rbacv1applyconfigurations.RoleBindingApplyConfiguration](
447+
ctx, op.logger, labeller.ContentHashFilter,
448+
func(roleBinding *rbacv1.RoleBinding) (string, error) {
449+
return resolver.RoleReferenceAndSubjectHashLabelValue(roleBinding.RoleRef, roleBinding.Subjects)
450+
},
451+
rbacv1applyconfigurations.RoleBinding,
452+
func(namespace string, ctx context.Context, cfg *rbacv1applyconfigurations.RoleBindingApplyConfiguration, opts metav1.ApplyOptions) (*rbacv1.RoleBinding, error) {
453+
return op.opClient.KubernetesInterface().RbacV1().RoleBindings(namespace).Apply(ctx, cfg, opts)
454+
},
455+
)); err != nil {
456+
return nil, err
457+
}
435458

436459
// Wire ServiceAccounts
437460
serviceAccountInformer := k8sInformerFactory.Core().V1().ServiceAccounts()

pkg/controller/operators/labeller/filters.go

+16
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ import (
2121
"github.com/operator-framework/operator-lifecycle-manager/pkg/controller/operators/internal/alongside"
2222
)
2323

24+
func ContentHashFilter(object metav1.Object) bool {
25+
return HasOLMOwnerRef(object) && !hasHashLabel(object)
26+
}
27+
2428
func Filter(gvr schema.GroupVersionResource) func(metav1.Object) bool {
2529
if f, ok := filters[gvr]; ok {
2630
return f
@@ -80,6 +84,18 @@ func Validate(ctx context.Context, logger *logrus.Logger, metadataClient metadat
8084
allFilters[batchv1.SchemeGroupVersion.WithResource("jobs")] = JobFilter(func(namespace, name string) (metav1.Object, error) {
8185
return metadataClient.Resource(corev1.SchemeGroupVersion.WithResource("configmaps")).Namespace(namespace).Get(ctx, name, metav1.GetOptions{})
8286
})
87+
88+
for _, gvr := range []schema.GroupVersionResource{
89+
rbacv1.SchemeGroupVersion.WithResource("roles"),
90+
rbacv1.SchemeGroupVersion.WithResource("rolebindings"),
91+
rbacv1.SchemeGroupVersion.WithResource("clusterroles"),
92+
rbacv1.SchemeGroupVersion.WithResource("clusterrolebindings"),
93+
} {
94+
previous := allFilters[gvr]
95+
allFilters[gvr] = func(object metav1.Object) bool {
96+
return previous != nil && previous(object) && ContentHashFilter(object)
97+
}
98+
}
8399
for gvr, filter := range allFilters {
84100
gvr, filter := gvr, filter
85101
g.Go(func() error {
+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package labeller
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/operator-framework/api/pkg/operators/v1alpha1"
8+
"github.com/operator-framework/operator-lifecycle-manager/pkg/controller/registry/resolver"
9+
"github.com/operator-framework/operator-lifecycle-manager/pkg/lib/ownerutil"
10+
"github.com/operator-framework/operator-lifecycle-manager/pkg/lib/queueinformer"
11+
"github.com/sirupsen/logrus"
12+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
13+
)
14+
15+
func hasHashLabel(obj metav1.Object) bool {
16+
_, ok := obj.GetLabels()[resolver.ContentHashLabelKey]
17+
return ok
18+
}
19+
20+
func ContentHashLabeler[T metav1.Object, A ApplyConfig[A]](
21+
ctx context.Context,
22+
logger *logrus.Logger,
23+
check func(metav1.Object) bool,
24+
hasher func(object T) (string, error),
25+
applyConfigFor func(name, namespace string) A,
26+
apply func(namespace string, ctx context.Context, cfg A, opts metav1.ApplyOptions) (T, error),
27+
) queueinformer.LegacySyncHandler {
28+
return func(obj interface{}) error {
29+
cast, ok := obj.(T)
30+
if !ok {
31+
err := fmt.Errorf("wrong type %T, expected %T: %#v", obj, new(T), obj)
32+
logger.WithError(err).Error("casting failed")
33+
return fmt.Errorf("casting failed: %w", err)
34+
}
35+
36+
if _, _, ok := ownerutil.GetOwnerByKindLabel(cast, v1alpha1.ClusterServiceVersionKind); !ok {
37+
return nil
38+
}
39+
40+
if !check(cast) || hasHashLabel(cast) {
41+
return nil
42+
}
43+
44+
hash, err := hasher(cast)
45+
if err != nil {
46+
return fmt.Errorf("failed to calculate hash: %w", err)
47+
}
48+
49+
cfg := applyConfigFor(cast.GetName(), cast.GetNamespace())
50+
cfg.WithLabels(map[string]string{
51+
resolver.ContentHashLabelKey: hash,
52+
})
53+
_, err = apply(cast.GetNamespace(), ctx, cfg, metav1.ApplyOptions{})
54+
return err
55+
}
56+
}

pkg/controller/operators/olm/operator.go

+30-3
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,6 @@ type Operator struct {
7878
opClient operatorclient.ClientInterface
7979
client versioned.Interface
8080
lister operatorlister.OperatorLister
81-
k8sLabelQueueSets map[schema.GroupVersionResource]workqueue.RateLimitingInterface
8281
protectedCopiedCSVNamespaces map[string]struct{}
8382
copiedCSVLister metadatalister.Lister
8483
ogQueueSet *queueinformer.ResourceQueueSet
@@ -162,7 +161,6 @@ func newOperatorWithConfig(ctx context.Context, config *operatorConfig) (*Operat
162161
resolver: config.strategyResolver,
163162
apiReconciler: config.apiReconciler,
164163
lister: lister,
165-
k8sLabelQueueSets: map[schema.GroupVersionResource]workqueue.RateLimitingInterface{},
166164
recorder: eventRecorder,
167165
apiLabeler: config.apiLabeler,
168166
csvIndexers: map[string]cache.Indexer{},
@@ -453,11 +451,12 @@ func newOperatorWithConfig(ctx context.Context, config *operatorConfig) (*Operat
453451
if canFilter {
454452
return nil
455453
}
456-
op.k8sLabelQueueSets[gvr] = workqueue.NewRateLimitingQueueWithConfig(workqueue.DefaultControllerRateLimiter(), workqueue.RateLimitingQueueConfig{
454+
queue := workqueue.NewRateLimitingQueueWithConfig(workqueue.DefaultControllerRateLimiter(), workqueue.RateLimitingQueueConfig{
457455
Name: gvr.String(),
458456
})
459457
queueInformer, err := queueinformer.NewQueueInformer(
460458
ctx,
459+
queueinformer.WithQueue(queue),
461460
queueinformer.WithLogger(op.logger),
462461
queueinformer.WithInformer(informer),
463462
queueinformer.WithSyncer(sync.ToSyncer()),
@@ -557,6 +556,20 @@ func newOperatorWithConfig(ctx context.Context, config *operatorConfig) (*Operat
557556
)); err != nil {
558557
return nil, err
559558
}
559+
if err := labelObjects(clusterrolesgvk, clusterRoleInformer.Informer(), labeller.ContentHashLabeler[*rbacv1.ClusterRole, *rbacv1applyconfigurations.ClusterRoleApplyConfiguration](
560+
ctx, op.logger, labeller.ContentHashFilter,
561+
func(clusterRole *rbacv1.ClusterRole) (string, error) {
562+
return resolver.PolicyRuleHashLabelValue(clusterRole.Rules)
563+
},
564+
func(name, _ string) *rbacv1applyconfigurations.ClusterRoleApplyConfiguration {
565+
return rbacv1applyconfigurations.ClusterRole(name)
566+
},
567+
func(_ string, ctx context.Context, cfg *rbacv1applyconfigurations.ClusterRoleApplyConfiguration, opts metav1.ApplyOptions) (*rbacv1.ClusterRole, error) {
568+
return op.opClient.KubernetesInterface().RbacV1().ClusterRoles().Apply(ctx, cfg, opts)
569+
},
570+
)); err != nil {
571+
return nil, err
572+
}
560573

561574
clusterRoleBindingInformer := k8sInformerFactory.Rbac().V1().ClusterRoleBindings()
562575
informersByNamespace[metav1.NamespaceAll].ClusterRoleBindingInformer = clusterRoleBindingInformer
@@ -586,6 +599,20 @@ func newOperatorWithConfig(ctx context.Context, config *operatorConfig) (*Operat
586599
)); err != nil {
587600
return nil, err
588601
}
602+
if err := labelObjects(clusterrolebindingssgvk, clusterRoleBindingInformer.Informer(), labeller.ContentHashLabeler[*rbacv1.ClusterRoleBinding, *rbacv1applyconfigurations.ClusterRoleBindingApplyConfiguration](
603+
ctx, op.logger, labeller.ContentHashFilter,
604+
func(clusterRoleBinding *rbacv1.ClusterRoleBinding) (string, error) {
605+
return resolver.RoleReferenceAndSubjectHashLabelValue(clusterRoleBinding.RoleRef, clusterRoleBinding.Subjects)
606+
},
607+
func(name, _ string) *rbacv1applyconfigurations.ClusterRoleBindingApplyConfiguration {
608+
return rbacv1applyconfigurations.ClusterRoleBinding(name)
609+
},
610+
func(_ string, ctx context.Context, cfg *rbacv1applyconfigurations.ClusterRoleBindingApplyConfiguration, opts metav1.ApplyOptions) (*rbacv1.ClusterRoleBinding, error) {
611+
return op.opClient.KubernetesInterface().RbacV1().ClusterRoleBindings().Apply(ctx, cfg, opts)
612+
},
613+
)); err != nil {
614+
return nil, err
615+
}
589616

590617
// register namespace queueinformer
591618
namespaceInformer := k8sInformerFactory.Core().V1().Namespaces()

pkg/controller/registry/resolver/rbac.go

+54
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
package resolver
22

33
import (
4+
"crypto/sha256"
5+
"encoding/json"
46
"fmt"
57
"hash/fnv"
8+
"math/big"
69

710
corev1 "k8s.io/api/core/v1"
811
rbacv1 "k8s.io/api/rbac/v1"
@@ -62,6 +65,37 @@ func (o *OperatorPermissions) AddClusterRoleBinding(clusterRoleBinding *rbacv1.C
6265
o.ClusterRoleBindings = append(o.ClusterRoleBindings, clusterRoleBinding)
6366
}
6467

68+
const ContentHashLabelKey = "olm.permissions.hash"
69+
70+
func PolicyRuleHashLabelValue(rules []rbacv1.PolicyRule) (string, error) {
71+
raw, err := json.Marshal(rules)
72+
if err != nil {
73+
return "", err
74+
}
75+
return toBase62(sha256.Sum224(raw)), nil
76+
}
77+
78+
func RoleReferenceAndSubjectHashLabelValue(roleRef rbacv1.RoleRef, subjects []rbacv1.Subject) (string, error) {
79+
var container = struct {
80+
RoleRef rbacv1.RoleRef
81+
Subjects []rbacv1.Subject
82+
}{
83+
RoleRef: roleRef,
84+
Subjects: subjects,
85+
}
86+
raw, err := json.Marshal(&container)
87+
if err != nil {
88+
return "", err
89+
}
90+
return toBase62(sha256.Sum224(raw)), nil
91+
}
92+
93+
func toBase62(hash [28]byte) string {
94+
var i big.Int
95+
i.SetBytes(hash[:])
96+
return i.Text(62)
97+
}
98+
6599
func RBACForClusterServiceVersion(csv *v1alpha1.ClusterServiceVersion) (map[string]*OperatorPermissions, error) {
66100
permissions := map[string]*OperatorPermissions{}
67101

@@ -100,6 +134,11 @@ func RBACForClusterServiceVersion(csv *v1alpha1.ClusterServiceVersion) (map[stri
100134
},
101135
Rules: permission.Rules,
102136
}
137+
hash, err := PolicyRuleHashLabelValue(permission.Rules)
138+
if err != nil {
139+
return nil, fmt.Errorf("failed to hash permission rules: %w", err)
140+
}
141+
role.Labels[ContentHashLabelKey] = hash
103142
permissions[permission.ServiceAccountName].AddRole(role)
104143

105144
// Create RoleBinding
@@ -120,6 +159,11 @@ func RBACForClusterServiceVersion(csv *v1alpha1.ClusterServiceVersion) (map[stri
120159
Namespace: csv.GetNamespace(),
121160
}},
122161
}
162+
hash, err = RoleReferenceAndSubjectHashLabelValue(roleBinding.RoleRef, roleBinding.Subjects)
163+
if err != nil {
164+
return nil, fmt.Errorf("failed to hash binding content: %w", err)
165+
}
166+
roleBinding.Labels[ContentHashLabelKey] = hash
123167
permissions[permission.ServiceAccountName].AddRoleBinding(roleBinding)
124168
}
125169

@@ -142,6 +186,11 @@ func RBACForClusterServiceVersion(csv *v1alpha1.ClusterServiceVersion) (map[stri
142186
},
143187
Rules: permission.Rules,
144188
}
189+
hash, err := PolicyRuleHashLabelValue(permission.Rules)
190+
if err != nil {
191+
return nil, fmt.Errorf("failed to hash permission rules: %w", err)
192+
}
193+
role.Labels[ContentHashLabelKey] = hash
145194
permissions[permission.ServiceAccountName].AddClusterRole(role)
146195

147196
// Create ClusterRoleBinding
@@ -162,6 +211,11 @@ func RBACForClusterServiceVersion(csv *v1alpha1.ClusterServiceVersion) (map[stri
162211
Namespace: csv.GetNamespace(),
163212
}},
164213
}
214+
hash, err = RoleReferenceAndSubjectHashLabelValue(roleBinding.RoleRef, roleBinding.Subjects)
215+
if err != nil {
216+
return nil, fmt.Errorf("failed to hash binding content: %w", err)
217+
}
218+
roleBinding.Labels[ContentHashLabelKey] = hash
165219
permissions[permission.ServiceAccountName].AddClusterRoleBinding(roleBinding)
166220
}
167221
return permissions, nil

pkg/package-server/provider/registry_test.go

+1-2
Original file line numberDiff line numberDiff line change
@@ -787,7 +787,7 @@ func TestRegistryProviderList(t *testing.T) {
787787
globalNS: "ns",
788788
requestNamespace: "wisconsin",
789789
expectedErr: "",
790-
expected: &operators.PackageManifestList{Items: []operators.PackageManifest{}},
790+
expected: &operators.PackageManifestList{},
791791
},
792792
{
793793
name: "PackagesFound",
@@ -1230,7 +1230,6 @@ func TestRegistryProviderList(t *testing.T) {
12301230
} else {
12311231
require.Nil(t, err)
12321232
}
1233-
12341233
require.Equal(t, len(test.expected.Items), len(packageManifestList.Items))
12351234
require.ElementsMatch(t, test.expected.Items, packageManifestList.Items)
12361235
})

0 commit comments

Comments
 (0)