Skip to content

Commit 3c247ee

Browse files
perdasilvaPer Goncalves da Silva
andauthored
Add preflight checks to Boxcutter applier (#2443)
Signed-off-by: Per Goncalves da Silva <pegoncal@redhat.com> Co-authored-by: Per Goncalves da Silva <pegoncal@redhat.com>
1 parent 46e41b2 commit 3c247ee

File tree

12 files changed

+662
-166
lines changed

12 files changed

+662
-166
lines changed

cmd/operator-controller/main.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -598,7 +598,12 @@ func (c *boxcutterReconcilerConfigurator) Configure(ceReconciler *controllers.Cl
598598
return err
599599
}
600600

601-
// TODO: add support for preflight checks
601+
// determine if PreAuthorizer should be enabled based on feature gate
602+
var preAuth authorization.PreAuthorizer
603+
if features.OperatorControllerFeatureGate.Enabled(features.PreflightPermissions) {
604+
preAuth = authorization.NewRBACPreAuthorizer(c.mgr.GetClient())
605+
}
606+
602607
// TODO: better scheme handling - which types do we want to support?
603608
_ = apiextensionsv1.AddToScheme(c.mgr.GetScheme())
604609
rg := &applier.SimpleRevisionGenerator{
@@ -610,6 +615,7 @@ func (c *boxcutterReconcilerConfigurator) Configure(ceReconciler *controllers.Cl
610615
Scheme: c.mgr.GetScheme(),
611616
RevisionGenerator: rg,
612617
Preflights: c.preflights,
618+
PreAuthorizer: preAuth,
613619
FieldOwner: fmt.Sprintf("%s/clusterextension-controller", fieldOwnerPrefix),
614620
}
615621
revisionStatesGetter := &controllers.BoxcutterRevisionStatesGetter{Reader: c.mgr.GetClient()}

internal/operator-controller/applier/boxcutter.go

Lines changed: 80 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
package applier
22

33
import (
4+
"bytes"
45
"cmp"
56
"context"
67
"errors"
78
"fmt"
9+
"io"
810
"io/fs"
911
"maps"
1012
"slices"
@@ -16,6 +18,9 @@ import (
1618
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1719
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
1820
"k8s.io/apimachinery/pkg/runtime"
21+
"k8s.io/apiserver/pkg/authentication/user"
22+
"k8s.io/apiserver/pkg/authorization/authorizer"
23+
"k8s.io/cli-runtime/pkg/printers"
1924
"sigs.k8s.io/controller-runtime/pkg/client"
2025
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
2126
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
@@ -25,6 +30,7 @@ import (
2530
helmclient "github.com/operator-framework/helm-operator-plugins/pkg/client"
2631

2732
ocv1 "github.com/operator-framework/operator-controller/api/v1"
33+
"github.com/operator-framework/operator-controller/internal/operator-controller/authorization"
2834
"github.com/operator-framework/operator-controller/internal/operator-controller/labels"
2935
"github.com/operator-framework/operator-controller/internal/shared/util/cache"
3036
)
@@ -283,28 +289,27 @@ type Boxcutter struct {
283289
Scheme *runtime.Scheme
284290
RevisionGenerator ClusterExtensionRevisionGenerator
285291
Preflights []Preflight
292+
PreAuthorizer authorization.PreAuthorizer
286293
FieldOwner string
287294
}
288295

289-
func (bc *Boxcutter) getObjects(rev *ocv1.ClusterExtensionRevision) []client.Object {
290-
var objs []client.Object
291-
for _, phase := range rev.Spec.Phases {
292-
for _, phaseObject := range phase.Objects {
293-
objs = append(objs, &phaseObject.Object)
294-
}
295-
}
296-
return objs
297-
}
298-
299-
func (bc *Boxcutter) createOrUpdate(ctx context.Context, obj client.Object) error {
300-
if obj.GetObjectKind().GroupVersionKind().Empty() {
301-
gvk, err := apiutil.GVKForObject(obj, bc.Scheme)
296+
// createOrUpdate creates or updates the revision object. PreAuthorization checks are performed to ensure the
297+
// user has sufficient permissions to manage the revision and its resources
298+
func (bc *Boxcutter) createOrUpdate(ctx context.Context, user user.Info, rev *ocv1.ClusterExtensionRevision) error {
299+
if rev.GetObjectKind().GroupVersionKind().Empty() {
300+
gvk, err := apiutil.GVKForObject(rev, bc.Scheme)
302301
if err != nil {
303302
return err
304303
}
305-
obj.GetObjectKind().SetGroupVersionKind(gvk)
304+
rev.GetObjectKind().SetGroupVersionKind(gvk)
306305
}
307-
return bc.Client.Patch(ctx, obj, client.Apply, client.FieldOwner(bc.FieldOwner), client.ForceOwnership)
306+
307+
// Run auth preflight checks
308+
if err := bc.runPreAuthorizationChecks(ctx, user, rev); err != nil {
309+
return err
310+
}
311+
312+
return bc.Client.Patch(ctx, rev, client.Apply, client.FieldOwner(bc.FieldOwner), client.ForceOwnership)
308313
}
309314

310315
func (bc *Boxcutter) Apply(ctx context.Context, contentFS fs.FS, ext *ocv1.ClusterExtension, objectLabels, revisionAnnotations map[string]string) error {
@@ -333,7 +338,7 @@ func (bc *Boxcutter) Apply(ctx context.Context, contentFS fs.FS, ext *ocv1.Clust
333338
desiredRevision.Spec.Revision = currentRevision.Spec.Revision
334339
desiredRevision.Name = currentRevision.Name
335340

336-
err := bc.createOrUpdate(ctx, desiredRevision)
341+
err := bc.createOrUpdate(ctx, getUserInfo(ext), desiredRevision)
337342
switch {
338343
case apierrors.IsInvalid(err):
339344
// We could not update the current revision due to trying to update an immutable field.
@@ -348,7 +353,7 @@ func (bc *Boxcutter) Apply(ctx context.Context, contentFS fs.FS, ext *ocv1.Clust
348353
}
349354

350355
// Preflights
351-
plainObjs := bc.getObjects(desiredRevision)
356+
plainObjs := getObjects(desiredRevision)
352357
for _, preflight := range bc.Preflights {
353358
if shouldSkipPreflight(ctx, preflight, ext, state) {
354359
continue
@@ -383,14 +388,31 @@ func (bc *Boxcutter) Apply(ctx context.Context, contentFS fs.FS, ext *ocv1.Clust
383388
return fmt.Errorf("garbage collecting old revisions: %w", err)
384389
}
385390

386-
if err := bc.createOrUpdate(ctx, desiredRevision); err != nil {
391+
if err := bc.createOrUpdate(ctx, getUserInfo(ext), desiredRevision); err != nil {
387392
return fmt.Errorf("creating new Revision: %w", err)
388393
}
389394
}
390395

391396
return nil
392397
}
393398

399+
// runPreAuthorizationChecks runs PreAuthorization checks if the PreAuthorizer is set. An error will be returned if
400+
// the ClusterExtension service account does not have the necessary permissions to manage the revision's resources
401+
func (bc *Boxcutter) runPreAuthorizationChecks(ctx context.Context, user user.Info, rev *ocv1.ClusterExtensionRevision) error {
402+
if bc.PreAuthorizer == nil {
403+
return nil
404+
}
405+
406+
// collect the revision manifests
407+
manifestReader, err := revisionManifestReader(rev)
408+
if err != nil {
409+
return err
410+
}
411+
412+
// run preauthorization check
413+
return formatPreAuthorizerOutput(bc.PreAuthorizer.PreAuthorize(ctx, user, manifestReader, revisionManagementPerms(rev)))
414+
}
415+
394416
// garbageCollectOldRevisions deletes archived revisions beyond ClusterExtensionRevisionRetentionLimit.
395417
// Active revisions are never deleted. revisionList must be sorted oldest to newest.
396418
func (bc *Boxcutter) garbageCollectOldRevisions(ctx context.Context, revisionList []ocv1.ClusterExtensionRevision) error {
@@ -449,3 +471,43 @@ func splitManifestDocuments(file string) []string {
449471
}
450472
return docs
451473
}
474+
475+
// getObjects returns a slice of all objects in the revision
476+
func getObjects(rev *ocv1.ClusterExtensionRevision) []client.Object {
477+
var objs []client.Object
478+
for _, phase := range rev.Spec.Phases {
479+
for _, phaseObject := range phase.Objects {
480+
objs = append(objs, &phaseObject.Object)
481+
}
482+
}
483+
return objs
484+
}
485+
486+
// revisionManifestReader returns an io.Reader containing all manifests in the revision
487+
func revisionManifestReader(rev *ocv1.ClusterExtensionRevision) (io.Reader, error) {
488+
printer := printers.YAMLPrinter{}
489+
buf := new(bytes.Buffer)
490+
for _, obj := range getObjects(rev) {
491+
buf.WriteString("---\n")
492+
if err := printer.PrintObj(obj, buf); err != nil {
493+
return nil, err
494+
}
495+
}
496+
return buf, nil
497+
}
498+
499+
func revisionManagementPerms(rev *ocv1.ClusterExtensionRevision) func(user.Info) []authorizer.AttributesRecord {
500+
return func(user user.Info) []authorizer.AttributesRecord {
501+
return []authorizer.AttributesRecord{
502+
{
503+
User: user,
504+
Name: rev.Name,
505+
APIGroup: ocv1.GroupVersion.Group,
506+
APIVersion: ocv1.GroupVersion.Version,
507+
Resource: "clusterextensionrevisions/finalizers",
508+
ResourceRequest: true,
509+
Verb: "update",
510+
},
511+
}
512+
}
513+
}

0 commit comments

Comments
 (0)