Skip to content

Commit 40a2645

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 40a2645

File tree

11 files changed

+612
-159
lines changed

11 files changed

+612
-159
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
@@ -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,27 @@ 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+
// createOrUpdate creates or updates the revision object. PreAuthorization checks are performed to ensure the
291+
// manifestManager has sufficient permissions to manage the revision and its resources
292+
func (bc *Boxcutter) createOrUpdate(ctx context.Context, manifestManager user.Info, rev *ocv1.ClusterExtensionRevision) error {
293+
if rev.GetObjectKind().GroupVersionKind().Empty() {
294+
gvk, err := apiutil.GVKForObject(rev, bc.Scheme)
298295
if err != nil {
299296
return err
300297
}
301-
obj.GetObjectKind().SetGroupVersionKind(gvk)
298+
rev.GetObjectKind().SetGroupVersionKind(gvk)
302299
}
303-
return bc.Client.Patch(ctx, obj, client.Apply, client.FieldOwner(bc.FieldOwner), client.ForceOwnership)
300+
301+
// Run auth preflight checks
302+
if err := bc.runPreAuthorizationChecks(ctx, manifestManager, rev); err != nil {
303+
return err
304+
}
305+
306+
return bc.Client.Patch(ctx, rev, client.Apply, client.FieldOwner(bc.FieldOwner), client.ForceOwnership)
304307
}
305308

306309
func (bc *Boxcutter) Apply(ctx context.Context, contentFS fs.FS, ext *ocv1.ClusterExtension, objectLabels, revisionAnnotations map[string]string) error {
@@ -329,7 +332,7 @@ func (bc *Boxcutter) Apply(ctx context.Context, contentFS fs.FS, ext *ocv1.Clust
329332
desiredRevision.Spec.Revision = currentRevision.Spec.Revision
330333
desiredRevision.Name = currentRevision.Name
331334

332-
err := bc.createOrUpdate(ctx, desiredRevision)
335+
err := bc.createOrUpdate(ctx, getUserInfo(ext), desiredRevision)
333336
switch {
334337
case apierrors.IsInvalid(err):
335338
// We could not update the current revision due to trying to update an immutable field.
@@ -344,7 +347,7 @@ func (bc *Boxcutter) Apply(ctx context.Context, contentFS fs.FS, ext *ocv1.Clust
344347
}
345348

346349
// Preflights
347-
plainObjs := bc.getObjects(desiredRevision)
350+
plainObjs := getObjects(desiredRevision)
348351
for _, preflight := range bc.Preflights {
349352
if shouldSkipPreflight(ctx, preflight, ext, state) {
350353
continue
@@ -379,14 +382,31 @@ func (bc *Boxcutter) Apply(ctx context.Context, contentFS fs.FS, ext *ocv1.Clust
379382
return fmt.Errorf("garbage collecting old revisions: %w", err)
380383
}
381384

382-
if err := bc.createOrUpdate(ctx, desiredRevision); err != nil {
385+
if err := bc.createOrUpdate(ctx, getUserInfo(ext), desiredRevision); err != nil {
383386
return fmt.Errorf("creating new Revision: %w", err)
384387
}
385388
}
386389

387390
return nil
388391
}
389392

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