Skip to content

Commit b295770

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 dc20dfb commit b295770

7 files changed

Lines changed: 421 additions & 112 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: 79 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"context"
66
"errors"
77
"fmt"
8+
"io"
89
"io/fs"
910
"maps"
1011
"slices"
@@ -16,6 +17,8 @@ import (
1617
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1718
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
1819
"k8s.io/apimachinery/pkg/runtime"
20+
"k8s.io/apiserver/pkg/authentication/user"
21+
"k8s.io/apiserver/pkg/authorization/authorizer"
1922
"sigs.k8s.io/controller-runtime/pkg/client"
2023
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
2124
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
@@ -25,6 +28,7 @@ import (
2528
helmclient "github.com/operator-framework/helm-operator-plugins/pkg/client"
2629

2730
ocv1 "github.com/operator-framework/operator-controller/api/v1"
31+
"github.com/operator-framework/operator-controller/internal/operator-controller/authorization"
2832
"github.com/operator-framework/operator-controller/internal/operator-controller/labels"
2933
"github.com/operator-framework/operator-controller/internal/shared/util/cache"
3034
)
@@ -279,28 +283,25 @@ type Boxcutter struct {
279283
Scheme *runtime.Scheme
280284
RevisionGenerator ClusterExtensionRevisionGenerator
281285
Preflights []Preflight
286+
PreAuthorizer authorization.PreAuthorizer
282287
FieldOwner string
283288
}
284289

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)
290+
func (bc *Boxcutter) createOrUpdateRevisionWithPreAuthorization(ctx context.Context, ext *ocv1.ClusterExtension, rev *ocv1.ClusterExtensionRevision) error {
291+
if rev.GetObjectKind().GroupVersionKind().Empty() {
292+
gvk, err := apiutil.GVKForObject(rev, bc.Scheme)
298293
if err != nil {
299294
return err
300295
}
301-
obj.GetObjectKind().SetGroupVersionKind(gvk)
296+
rev.GetObjectKind().SetGroupVersionKind(gvk)
302297
}
303-
return bc.Client.Patch(ctx, obj, client.Apply, client.FieldOwner(bc.FieldOwner), client.ForceOwnership)
298+
299+
// Run auth preflight checks
300+
if err := bc.runPreAuthorizationChecks(ctx, ext, rev); err != nil {
301+
return err
302+
}
303+
304+
return bc.Client.Patch(ctx, rev, client.Apply, client.FieldOwner(bc.FieldOwner), client.ForceOwnership)
304305
}
305306

306307
func (bc *Boxcutter) Apply(ctx context.Context, contentFS fs.FS, ext *ocv1.ClusterExtension, objectLabels, revisionAnnotations map[string]string) error {
@@ -329,7 +330,7 @@ func (bc *Boxcutter) Apply(ctx context.Context, contentFS fs.FS, ext *ocv1.Clust
329330
desiredRevision.Spec.Revision = currentRevision.Spec.Revision
330331
desiredRevision.Name = currentRevision.Name
331332

332-
err := bc.createOrUpdate(ctx, desiredRevision)
333+
err := bc.createOrUpdateRevisionWithPreAuthorization(ctx, ext, desiredRevision)
333334
switch {
334335
case apierrors.IsInvalid(err):
335336
// We could not update the current revision due to trying to update an immutable field.
@@ -344,7 +345,7 @@ func (bc *Boxcutter) Apply(ctx context.Context, contentFS fs.FS, ext *ocv1.Clust
344345
}
345346

346347
// Preflights
347-
plainObjs := bc.getObjects(desiredRevision)
348+
plainObjs := getObjects(desiredRevision)
348349
for _, preflight := range bc.Preflights {
349350
if shouldSkipPreflight(ctx, preflight, ext, state) {
350351
continue
@@ -379,14 +380,34 @@ func (bc *Boxcutter) Apply(ctx context.Context, contentFS fs.FS, ext *ocv1.Clust
379380
return fmt.Errorf("garbage collecting old revisions: %w", err)
380381
}
381382

382-
if err := bc.createOrUpdate(ctx, desiredRevision); err != nil {
383+
if err := bc.createOrUpdateRevisionWithPreAuthorization(ctx, ext, desiredRevision); err != nil {
383384
return fmt.Errorf("creating new Revision: %w", err)
384385
}
385386
}
386387

387388
return nil
388389
}
389390

391+
// runPreAuthorizationChecks runs PreAuthorization checks if the PreAuthorizer is set. An error will be returned if
392+
// the ClusterExtension service account does not have the necessary permissions to manage the revision's resources
393+
func (bc *Boxcutter) runPreAuthorizationChecks(ctx context.Context, ext *ocv1.ClusterExtension, rev *ocv1.ClusterExtensionRevision) error {
394+
if bc.PreAuthorizer == nil {
395+
return nil
396+
}
397+
398+
// collect the revision manifests
399+
manifestReader, err := revisionManifestReader(rev)
400+
if err != nil {
401+
return err
402+
}
403+
404+
// extract user to check permissions against
405+
manifestManager := getUserInfo(ext)
406+
407+
// run preauthorization check
408+
return formatPreAuthorizerOutput(bc.PreAuthorizer.PreAuthorize(ctx, manifestManager, manifestReader, clusterExtensionRevisionManagementPermissions(manifestManager, rev)...))
409+
}
410+
390411
// garbageCollectOldRevisions deletes archived revisions beyond ClusterExtensionRevisionRetentionLimit.
391412
// Active revisions are never deleted. revisionList must be sorted oldest to newest.
392413
func (bc *Boxcutter) garbageCollectOldRevisions(ctx context.Context, revisionList []ocv1.ClusterExtensionRevision) error {
@@ -445,3 +466,43 @@ func splitManifestDocuments(file string) []string {
445466
}
446467
return docs
447468
}
469+
470+
// getObjects returns a slice of all objects in the revision
471+
func getObjects(rev *ocv1.ClusterExtensionRevision) []client.Object {
472+
var objs []client.Object
473+
for _, phase := range rev.Spec.Phases {
474+
for _, phaseObject := range phase.Objects {
475+
objs = append(objs, &phaseObject.Object)
476+
}
477+
}
478+
return objs
479+
}
480+
481+
// revisionManifestReader returns an io.Reader containing all manifests in the revision
482+
func revisionManifestReader(rev *ocv1.ClusterExtensionRevision) (io.Reader, error) {
483+
var manifestBuilder strings.Builder
484+
for _, obj := range getObjects(rev) {
485+
objBytes, err := yaml.Marshal(obj)
486+
if err != nil {
487+
return nil, fmt.Errorf("error generating revision manifest: %w", err)
488+
}
489+
manifestBuilder.WriteString("---\n")
490+
manifestBuilder.WriteString(string(objBytes))
491+
manifestBuilder.WriteString("\n")
492+
}
493+
return strings.NewReader(manifestBuilder.String()), nil
494+
}
495+
496+
func clusterExtensionRevisionManagementPermissions(manifestManager user.Info, rev *ocv1.ClusterExtensionRevision) []authorizer.AttributesRecord {
497+
return []authorizer.AttributesRecord{
498+
{
499+
User: manifestManager,
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+
}

0 commit comments

Comments
 (0)