Skip to content

Commit dd63342

Browse files
author
Per Goncalves da Silva
committed
Add preflight checks to Boxcutter applier
Signed-off-by: Per Goncalves da Silva <pegoncal@redhat.com>
1 parent b08a054 commit dd63342

11 files changed

Lines changed: 626 additions & 171 deletions

File tree

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
)
@@ -279,28 +285,27 @@ type Boxcutter struct {
279285
Scheme *runtime.Scheme
280286
RevisionGenerator ClusterExtensionRevisionGenerator
281287
Preflights []Preflight
288+
PreAuthorizer authorization.PreAuthorizer
282289
FieldOwner string
283290
}
284291

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

306311
func (bc *Boxcutter) Apply(ctx context.Context, contentFS fs.FS, ext *ocv1.ClusterExtension, objectLabels, revisionAnnotations map[string]string) error {
@@ -329,7 +334,7 @@ func (bc *Boxcutter) Apply(ctx context.Context, contentFS fs.FS, ext *ocv1.Clust
329334
desiredRevision.Spec.Revision = currentRevision.Spec.Revision
330335
desiredRevision.Name = currentRevision.Name
331336

332-
err := bc.createOrUpdate(ctx, desiredRevision)
337+
err := bc.createOrUpdate(ctx, getUserInfo(ext), desiredRevision)
333338
switch {
334339
case apierrors.IsInvalid(err):
335340
// We could not update the current revision due to trying to update an immutable field.
@@ -344,7 +349,7 @@ func (bc *Boxcutter) Apply(ctx context.Context, contentFS fs.FS, ext *ocv1.Clust
344349
}
345350

346351
// Preflights
347-
plainObjs := bc.getObjects(desiredRevision)
352+
plainObjs := getObjects(desiredRevision)
348353
for _, preflight := range bc.Preflights {
349354
if shouldSkipPreflight(ctx, preflight, ext, state) {
350355
continue
@@ -379,14 +384,31 @@ func (bc *Boxcutter) Apply(ctx context.Context, contentFS fs.FS, ext *ocv1.Clust
379384
return fmt.Errorf("garbage collecting old revisions: %w", err)
380385
}
381386

382-
if err := bc.createOrUpdate(ctx, desiredRevision); err != nil {
387+
if err := bc.createOrUpdate(ctx, getUserInfo(ext), desiredRevision); err != nil {
383388
return fmt.Errorf("creating new Revision: %w", err)
384389
}
385390
}
386391

387392
return nil
388393
}
389394

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

0 commit comments

Comments
 (0)