@@ -16,17 +16,24 @@ import (
1616 "helm.sh/helm/v3/pkg/release"
1717 "helm.sh/helm/v3/pkg/storage/driver"
1818 corev1 "k8s.io/api/core/v1"
19+ v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1920 "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
2021 apimachyaml "k8s.io/apimachinery/pkg/util/yaml"
22+ "k8s.io/apiserver/pkg/authorization/authorizer"
23+ "k8s.io/client-go/rest"
2124 "sigs.k8s.io/controller-runtime/pkg/client"
2225 "sigs.k8s.io/controller-runtime/pkg/log"
2326
2427 helmclient "github.com/operator-framework/helm-operator-plugins/pkg/client"
28+ authv1 "k8s.io/api/authorization/v1"
29+ rbacv1 "k8s.io/api/rbac/v1"
30+ authorizationv1client "k8s.io/client-go/kubernetes/typed/authorization/v1"
2531
2632 ocv1 "github.com/operator-framework/operator-controller/api/v1"
2733 "github.com/operator-framework/operator-controller/internal/rukpak/convert"
2834 "github.com/operator-framework/operator-controller/internal/rukpak/preflights/crdupgradesafety"
2935 "github.com/operator-framework/operator-controller/internal/rukpak/util"
36+ rbacauthorizer "k8s.io/kubernetes/plugin/pkg/auth/authorizer/rbac"
3037)
3138
3239const (
@@ -52,9 +59,38 @@ type Preflight interface {
5259 Upgrade (context.Context , * release.Release ) error
5360}
5461
62+ type RestConfigMapper func (context.Context , client.Object , * rest.Config ) (* rest.Config , error )
63+
64+ type AuthClientMapper struct {
65+ rcm RestConfigMapper
66+ baseCfg * rest.Config
67+ }
68+
69+ func (acm * AuthClientMapper ) GetAuthenticationClient (ctx context.Context , ext * ocv1.ClusterExtension ) (* authorizationv1client.AuthorizationV1Client , error ) {
70+ authcfg , err := acm .rcm (ctx , ext , acm .baseCfg )
71+ if err != nil {
72+ return nil , err
73+ }
74+
75+ authclient , err := authorizationv1client .NewForConfig (authcfg )
76+ if err != nil {
77+ return nil , err
78+ }
79+
80+ return authclient , nil
81+ }
82+
5583type Helm struct {
5684 ActionClientGetter helmclient.ActionClientGetter
5785 Preflights []Preflight
86+ AuthClientMapper AuthClientMapper
87+ }
88+
89+ func NewAuthClientMapper (rcm RestConfigMapper , baseCfg * rest.Config ) AuthClientMapper {
90+ return AuthClientMapper {
91+ rcm : rcm ,
92+ baseCfg : baseCfg ,
93+ }
5894}
5995
6096// shouldSkipPreflight is a helper to determine if the preflight check is CRDUpgradeSafety AND
@@ -93,6 +129,16 @@ func (h *Helm) Apply(ctx context.Context, contentFS fs.FS, ext *ocv1.ClusterExte
93129 labels : objectLabels ,
94130 }
95131
132+ authclient , err := h .AuthClientMapper .GetAuthenticationClient (ctx , ext )
133+ if err != nil {
134+ return nil , "" , err
135+ }
136+
137+ err = h .checkGetPermissions (ctx , authclient , ac , ext , chrt , values , post )
138+ if err != nil {
139+ return nil , "" , err
140+ }
141+
96142 rel , desiredRel , state , err := h .getReleaseState (ac , ext , chrt , values , post )
97143 if err != nil {
98144 return nil , "" , err
@@ -151,8 +197,101 @@ func (h *Helm) Apply(ctx context.Context, contentFS fs.FS, ext *ocv1.ClusterExte
151197 return relObjects , state , nil
152198}
153199
200+ // re-org this to take in just the resulting manifest so I can use it for checking both the client-only and server-connnected dry-runs
201+ func (h * Helm ) checkGetPermissions (ctx context.Context , authcl * authorizationv1client.AuthorizationV1Client , cl helmclient.ActionInterface , ext * ocv1.ClusterExtension , chrt * chart.Chart , values chartutil.Values , post postrender.PostRenderer ) error {
202+ // client-only dry run
203+ // plainObjs, err := convert.Convert(chrt, "", []string)
204+ clientDryRunRelease , err := cl .Install (ext .GetName (), ext .Spec .Namespace , chrt , values , func (i * action.Install ) error {
205+ i .DryRun = true
206+ i .DryRunOption = "client"
207+ return nil
208+ }, helmclient .AppendInstallPostRenderer (post ))
209+ if err != nil {
210+ return err
211+ }
212+ objects , err := util .ManifestObjects (strings .NewReader (clientDryRunRelease .Manifest ), fmt .Sprintf ("%s-release-manifest" , clientDryRunRelease .Name ))
213+
214+ if err != nil {
215+ return err
216+ }
217+
218+ ssrr := & authv1.SelfSubjectRulesReview {
219+ Spec : authv1.SelfSubjectRulesReviewSpec {
220+ Namespace : ext .Spec .Namespace ,
221+ },
222+ }
223+
224+ ssrr , err = authcl .SelfSubjectRulesReviews ().Create (ctx , ssrr , v1.CreateOptions {})
225+ if err != nil {
226+ return err
227+ }
228+
229+ rules := []rbacv1.PolicyRule {}
230+ for _ , rule := range ssrr .Status .ResourceRules {
231+ rules = append (rules , rbacv1.PolicyRule {
232+ Verbs : rule .Verbs ,
233+ APIGroups : rule .APIGroups ,
234+ Resources : rule .Resources ,
235+ ResourceNames : rule .ResourceNames ,
236+ })
237+ }
238+
239+ for _ , rule := range ssrr .Status .NonResourceRules {
240+ rules = append (rules , rbacv1.PolicyRule {
241+ Verbs : rule .Verbs ,
242+ NonResourceURLs : rule .NonResourceURLs ,
243+ })
244+ }
245+
246+ resAttrs := []authorizer.AttributesRecord {}
247+ errs := []error {}
248+
249+ for _ , o := range objects {
250+ resAttrs = append (resAttrs , authorizer.AttributesRecord {
251+ Namespace : o .GetNamespace (),
252+ Verb : "get" ,
253+ APIGroup : o .GetObjectKind ().GroupVersionKind ().Group ,
254+ Resource : sanitizeResourceName (o .GetObjectKind ().GroupVersionKind ().Kind ),
255+ Name : o .GetName (),
256+ ResourceRequest : true ,
257+ })
258+ }
259+
260+ for _ , resAttr := range resAttrs {
261+ if ! rbacauthorizer .RulesAllow (resAttr , rules ... ) {
262+ errs = append (errs , fmt .Errorf ("serviceaccount %s cannot get %s in namespace %s" ,
263+ ext .Spec .ServiceAccount .Name ,
264+ resAttr .Resource ,
265+ resAttr .Namespace ))
266+ }
267+ }
268+ if len (errs ) > 0 {
269+ errs = append ([]error {fmt .Errorf ("installer service account %s is missing required get permissions" , ext .Spec .ServiceAccount .Name )}, errs ... )
270+ }
271+
272+ return errors .Join (errs ... )
273+
274+ // for _, o := range objects {
275+ // ssar := &authv1.SelfSubjectAccessReview{
276+ // Spec: authv1.SelfSubjectAccessReviewSpec{
277+ // ResourceAttributes: &authv1.ResourceAttributes{
278+ // Namespace: ext.Spec.Namespace,
279+ // Verb: "get",
280+ // Resource: o.GetObjectKind().GroupVersionKind().Kind,
281+ // Group: o.GetObjectKind().GroupVersionKind().Group,
282+ // },
283+ // },
284+ // }
285+ // ssar, err = authcl.SelfSubjectAccessReviews().Create(ctx, ssar, v1.CreateOptions{})
286+ // if err != nil {
287+ // return err
288+ // }
289+ // }
290+ }
291+
154292func (h * Helm ) getReleaseState (cl helmclient.ActionInterface , ext * ocv1.ClusterExtension , chrt * chart.Chart , values chartutil.Values , post postrender.PostRenderer ) (* release.Release , * release.Release , string , error ) {
155293 currentRelease , err := cl .Get (ext .GetName ())
294+
156295 if errors .Is (err , driver .ErrReleaseNotFound ) {
157296 desiredRelease , err := cl .Install (ext .GetName (), ext .Spec .Namespace , chrt , values , func (i * action.Install ) error {
158297 i .DryRun = true
@@ -186,6 +325,11 @@ func (h *Helm) getReleaseState(cl helmclient.ActionInterface, ext *ocv1.ClusterE
186325 return currentRelease , desiredRelease , relState , nil
187326}
188327
328+ // RulesAllow() checks expects resource names to be lowercase and plural, there's probably a better way to do this
329+ func sanitizeResourceName (resourceName string ) string {
330+ return strings .ToLower (resourceName ) + "s"
331+ }
332+
189333type postrenderer struct {
190334 labels map [string ]string
191335 cascade postrender.PostRenderer
0 commit comments