Skip to content

Commit 74c93c7

Browse files
committed
Add client-only helm dry-run to helm applier
Adds a check that makes sure the installer service account has the necessary permissions to manage all the resources in the ClusterExtension payload and returns errors detailing all the missing permissions. This prevents a hung server-connected dry-run getting caught on individual missing get permissions as well returns many other missing required permissions needed for the actual installation or upgrade. Signed-off-by: Tayler Geiger <[email protected]>
1 parent 7f00b13 commit 74c93c7

File tree

3 files changed

+179
-0
lines changed

3 files changed

+179
-0
lines changed

Diff for: cmd/operator-controller/main.go

+3
Original file line numberDiff line numberDiff line change
@@ -407,9 +407,12 @@ func run() error {
407407
crdupgradesafety.NewPreflight(aeClient.CustomResourceDefinitions()),
408408
}
409409

410+
acm := applier.NewAuthClientMapper(clientRestConfigMapper, mgr.GetConfig())
411+
410412
helmApplier := &applier.Helm{
411413
ActionClientGetter: acg,
412414
Preflights: preflights,
415+
AuthClientMapper: acm,
413416
}
414417

415418
cm := contentmanager.NewManager(clientRestConfigMapper, mgr.GetConfig(), mgr.GetRESTMapper())

Diff for: internal/operator-controller/applier/helm.go

+64
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,15 @@ import (
1818
corev1 "k8s.io/api/core/v1"
1919
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
2020
apimachyaml "k8s.io/apimachinery/pkg/util/yaml"
21+
"k8s.io/client-go/rest"
2122
"sigs.k8s.io/controller-runtime/pkg/client"
2223
"sigs.k8s.io/controller-runtime/pkg/log"
2324

2425
helmclient "github.com/operator-framework/helm-operator-plugins/pkg/client"
26+
authorizationv1client "k8s.io/client-go/kubernetes/typed/authorization/v1"
2527

2628
ocv1 "github.com/operator-framework/operator-controller/api/v1"
29+
"github.com/operator-framework/operator-controller/internal/operator-controller/authorization"
2730
"github.com/operator-framework/operator-controller/internal/operator-controller/features"
2831
"github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/convert"
2932
"github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/preflights/crdupgradesafety"
@@ -53,9 +56,38 @@ type Preflight interface {
5356
Upgrade(context.Context, *release.Release) error
5457
}
5558

59+
type RestConfigMapper func(context.Context, client.Object, *rest.Config) (*rest.Config, error)
60+
61+
type AuthClientMapper struct {
62+
rcm RestConfigMapper
63+
baseCfg *rest.Config
64+
}
65+
66+
func (acm *AuthClientMapper) GetAuthenticationClient(ctx context.Context, ext *ocv1.ClusterExtension) (*authorizationv1client.AuthorizationV1Client, error) {
67+
authcfg, err := acm.rcm(ctx, ext, acm.baseCfg)
68+
if err != nil {
69+
return nil, err
70+
}
71+
72+
authclient, err := authorizationv1client.NewForConfig(authcfg)
73+
if err != nil {
74+
return nil, err
75+
}
76+
77+
return authclient, nil
78+
}
79+
5680
type Helm struct {
5781
ActionClientGetter helmclient.ActionClientGetter
5882
Preflights []Preflight
83+
AuthClientMapper AuthClientMapper
84+
}
85+
86+
func NewAuthClientMapper(rcm RestConfigMapper, baseCfg *rest.Config) AuthClientMapper {
87+
return AuthClientMapper{
88+
rcm: rcm,
89+
baseCfg: baseCfg,
90+
}
5991
}
6092

6193
// shouldSkipPreflight is a helper to determine if the preflight check is CRDUpgradeSafety AND
@@ -79,7 +111,21 @@ func shouldSkipPreflight(ctx context.Context, preflight Preflight, ext *ocv1.Clu
79111
}
80112

81113
func (h *Helm) Apply(ctx context.Context, contentFS fs.FS, ext *ocv1.ClusterExtension, objectLabels map[string]string, storageLabels map[string]string) ([]client.Object, string, error) {
114+
115+
if features.OperatorControllerFeatureGate.Enabled(features.PreflightPermissions) {
116+
authclient, err := h.AuthClientMapper.GetAuthenticationClient(ctx, ext)
117+
if err != nil {
118+
return nil, "", err
119+
}
120+
121+
err = h.checkContentPermissions(ctx, contentFS, authclient, ext)
122+
if err != nil {
123+
return nil, "", err
124+
}
125+
}
126+
82127
chrt, err := convert.RegistryV1ToHelmChart(ctx, contentFS, ext.Spec.Namespace, []string{corev1.NamespaceAll})
128+
83129
if err != nil {
84130
return nil, "", err
85131
}
@@ -152,8 +198,26 @@ func (h *Helm) Apply(ctx context.Context, contentFS fs.FS, ext *ocv1.ClusterExte
152198
return relObjects, state, nil
153199
}
154200

201+
// Check if RBAC allows the installer service account necessary permissions on the objects in the contentFS
202+
func (h *Helm) checkContentPermissions(ctx context.Context, contentFS fs.FS, authcl *authorizationv1client.AuthorizationV1Client, ext *ocv1.ClusterExtension) error {
203+
reg, err := convert.ParseFS(ctx, contentFS)
204+
if err != nil {
205+
return err
206+
}
207+
208+
plain, err := convert.Convert(reg, ext.Spec.Namespace, []string{corev1.NamespaceAll})
209+
if err != nil {
210+
return err
211+
}
212+
213+
err = authorization.CheckObjectPermissions(ctx, authcl, plain.Objects, ext)
214+
215+
return err
216+
}
217+
155218
func (h *Helm) getReleaseState(cl helmclient.ActionInterface, ext *ocv1.ClusterExtension, chrt *chart.Chart, values chartutil.Values, post postrender.PostRenderer) (*release.Release, *release.Release, string, error) {
156219
currentRelease, err := cl.Get(ext.GetName())
220+
157221
if errors.Is(err, driver.ErrReleaseNotFound) {
158222
desiredRelease, err := cl.Install(ext.GetName(), ext.Spec.Namespace, chrt, values, func(i *action.Install) error {
159223
i.DryRun = true
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
package authorization
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"slices"
8+
"strings"
9+
10+
ocv1 "github.com/operator-framework/operator-controller/api/v1"
11+
authv1 "k8s.io/api/authorization/v1"
12+
rbacv1 "k8s.io/api/rbac/v1"
13+
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
14+
authorizationv1client "k8s.io/client-go/kubernetes/typed/authorization/v1"
15+
"sigs.k8s.io/controller-runtime/pkg/client"
16+
)
17+
18+
const (
19+
SelfSubjectRulesReview string = "SelfSubjectRulesReview"
20+
SelfSubjectAccessReview string = "SelfSubjectAccessReview"
21+
)
22+
23+
func CheckObjectPermissions(ctx context.Context, authcl *authorizationv1client.AuthorizationV1Client, objects []client.Object, ext *ocv1.ClusterExtension) error {
24+
ssrr := &authv1.SelfSubjectRulesReview{
25+
Spec: authv1.SelfSubjectRulesReviewSpec{
26+
Namespace: ext.Spec.Namespace,
27+
},
28+
}
29+
30+
ssrr, err := authcl.SelfSubjectRulesReviews().Create(ctx, ssrr, v1.CreateOptions{})
31+
if err != nil {
32+
return err
33+
}
34+
35+
rules := []rbacv1.PolicyRule{}
36+
for _, rule := range ssrr.Status.ResourceRules {
37+
rules = append(rules, rbacv1.PolicyRule{
38+
Verbs: rule.Verbs,
39+
APIGroups: rule.APIGroups,
40+
Resources: rule.Resources,
41+
ResourceNames: rule.ResourceNames,
42+
})
43+
}
44+
45+
for _, rule := range ssrr.Status.NonResourceRules {
46+
rules = append(rules, rbacv1.PolicyRule{
47+
Verbs: rule.Verbs,
48+
NonResourceURLs: rule.NonResourceURLs,
49+
})
50+
}
51+
52+
resAttrs := []authv1.ResourceAttributes{}
53+
namespacedErrs := []error{}
54+
clusterScopedErrs := []error{}
55+
requiredVerbs := []string{"get", "create", "update", "list", "watch", "delete", "patch"}
56+
57+
for _, o := range objects {
58+
for _, verb := range requiredVerbs {
59+
resAttrs = append(resAttrs, authv1.ResourceAttributes{
60+
Namespace: o.GetNamespace(),
61+
Verb: verb,
62+
Resource: sanitizeResourceName(o.GetObjectKind().GroupVersionKind().Kind),
63+
Group: o.GetObjectKind().GroupVersionKind().Group,
64+
Name: o.GetName(),
65+
})
66+
}
67+
}
68+
69+
for _, resAttr := range resAttrs {
70+
if !canI(resAttr, rules) {
71+
if resAttr.Namespace != "" {
72+
namespacedErrs = append(namespacedErrs, fmt.Errorf("cannot %s %s %s in namespace %s",
73+
resAttr.Verb,
74+
strings.TrimSuffix(resAttr.Resource, "s"),
75+
resAttr.Name,
76+
resAttr.Namespace))
77+
continue
78+
}
79+
clusterScopedErrs = append(clusterScopedErrs, fmt.Errorf("cannot %s %s %s",
80+
resAttr.Verb,
81+
strings.TrimSuffix(resAttr.Resource, "s"),
82+
resAttr.Name))
83+
}
84+
}
85+
errs := append(namespacedErrs, clusterScopedErrs...)
86+
if len(errs) > 0 {
87+
errs = append([]error{fmt.Errorf("installer service account %s is missing required permissions", ext.Spec.ServiceAccount.Name)}, errs...)
88+
}
89+
90+
return errors.Join(errs...)
91+
92+
}
93+
94+
// Checks if the rules allow the verb on the GroupVersionKind in resAttr
95+
func canI(resAttr authv1.ResourceAttributes, rules []rbacv1.PolicyRule) bool {
96+
var canI bool
97+
for _, rule := range rules {
98+
if (slices.Contains(rule.APIGroups, resAttr.Group) || slices.Contains(rule.APIGroups, "*")) &&
99+
(slices.Contains(rule.Resources, resAttr.Resource) || slices.Contains(rule.Resources, "*")) &&
100+
(slices.Contains(rule.Verbs, resAttr.Verb) || slices.Contains(rule.Verbs, "*")) &&
101+
(slices.Contains(rule.ResourceNames, resAttr.Name) || len(rule.ResourceNames) == 0) {
102+
canI = true
103+
break
104+
}
105+
}
106+
return canI
107+
}
108+
109+
// SelfSubjectRulesReview formats the resource names as lowercase and plural
110+
func sanitizeResourceName(resourceName string) string {
111+
return strings.ToLower(resourceName) + "s"
112+
}

0 commit comments

Comments
 (0)