Skip to content

Commit 53a9309

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 347be32 commit 53a9309

7 files changed

Lines changed: 413 additions & 115 deletions

File tree

cmd/operator-controller/main.go

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

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+
601607
// TODO: add support for preflight checks
602608
// TODO: better scheme handling - which types do we want to support?
603609
_ = apiextensionsv1.AddToScheme(c.mgr.GetScheme())
@@ -610,6 +616,7 @@ func (c *boxcutterReconcilerConfigurator) Configure(ceReconciler *controllers.Cl
610616
Scheme: c.mgr.GetScheme(),
611617
RevisionGenerator: rg,
612618
Preflights: c.preflights,
619+
PreAuthorizer: preAuth,
613620
FieldOwner: fmt.Sprintf("%s/clusterextension-controller", fieldOwnerPrefix),
614621
}
615622
revisionStatesGetter := &controllers.BoxcutterRevisionStatesGetter{Reader: c.mgr.GetClient()}

internal/operator-controller/applier/boxcutter.go

Lines changed: 85 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"
@@ -17,6 +18,8 @@ import (
1718
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1819
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
1920
"k8s.io/apimachinery/pkg/runtime"
21+
"k8s.io/apiserver/pkg/authentication/user"
22+
"k8s.io/apiserver/pkg/authorization/authorizer"
2023
"sigs.k8s.io/controller-runtime/pkg/client"
2124
"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
2225
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
@@ -26,6 +29,7 @@ import (
2629
helmclient "github.com/operator-framework/helm-operator-plugins/pkg/client"
2730

2831
ocv1 "github.com/operator-framework/operator-controller/api/v1"
32+
"github.com/operator-framework/operator-controller/internal/operator-controller/authorization"
2933
"github.com/operator-framework/operator-controller/internal/operator-controller/labels"
3034
"github.com/operator-framework/operator-controller/internal/shared/util/cache"
3135
)
@@ -280,32 +284,29 @@ type Boxcutter struct {
280284
Scheme *runtime.Scheme
281285
RevisionGenerator ClusterExtensionRevisionGenerator
282286
Preflights []Preflight
287+
PreAuthorizer authorization.PreAuthorizer
283288
FieldOwner string
284289
}
285290

286291
func (bc *Boxcutter) Apply(ctx context.Context, contentFS fs.FS, ext *ocv1.ClusterExtension, objectLabels, revisionAnnotations map[string]string) (bool, string, error) {
287292
return bc.apply(ctx, contentFS, ext, objectLabels, revisionAnnotations)
288293
}
289294

290-
func (bc *Boxcutter) getObjects(rev *ocv1.ClusterExtensionRevision) []client.Object {
291-
var objs []client.Object
292-
for _, phase := range rev.Spec.Phases {
293-
for _, phaseObject := range phase.Objects {
294-
objs = append(objs, &phaseObject.Object)
295-
}
296-
}
297-
return objs
298-
}
299-
300-
func (bc *Boxcutter) createOrUpdate(ctx context.Context, obj client.Object) error {
301-
if obj.GetObjectKind().GroupVersionKind().Empty() {
302-
gvk, err := apiutil.GVKForObject(obj, bc.Scheme)
295+
func (bc *Boxcutter) createOrUpdateRevisionWithPreAuthorization(ctx context.Context, ext *ocv1.ClusterExtension, rev *ocv1.ClusterExtensionRevision) error {
296+
if rev.GetObjectKind().GroupVersionKind().Empty() {
297+
gvk, err := apiutil.GVKForObject(rev, bc.Scheme)
303298
if err != nil {
304299
return err
305300
}
306-
obj.GetObjectKind().SetGroupVersionKind(gvk)
301+
rev.GetObjectKind().SetGroupVersionKind(gvk)
307302
}
308-
return bc.Client.Patch(ctx, obj, client.Apply, client.FieldOwner(bc.FieldOwner), client.ForceOwnership)
303+
304+
// Run auth preflight checks
305+
if err := bc.runPreAuthorizationChecks(ctx, ext, rev); err != nil {
306+
return err
307+
}
308+
309+
return bc.Client.Patch(ctx, rev, client.Apply, client.FieldOwner(bc.FieldOwner), client.ForceOwnership)
309310
}
310311

311312
func (bc *Boxcutter) apply(ctx context.Context, contentFS fs.FS, ext *ocv1.ClusterExtension, objectLabels, revisionAnnotations map[string]string) (bool, string, error) {
@@ -334,7 +335,7 @@ func (bc *Boxcutter) apply(ctx context.Context, contentFS fs.FS, ext *ocv1.Clust
334335
desiredRevision.Spec.Revision = currentRevision.Spec.Revision
335336
desiredRevision.Name = currentRevision.Name
336337

337-
err := bc.createOrUpdate(ctx, desiredRevision)
338+
err := bc.createOrUpdateRevisionWithPreAuthorization(ctx, ext, desiredRevision)
338339
switch {
339340
case apierrors.IsInvalid(err):
340341
// We could not update the current revision due to trying to update an immutable field.
@@ -349,7 +350,7 @@ func (bc *Boxcutter) apply(ctx context.Context, contentFS fs.FS, ext *ocv1.Clust
349350
}
350351

351352
// Preflights
352-
plainObjs := bc.getObjects(desiredRevision)
353+
plainObjs := getObjects(desiredRevision)
353354
for _, preflight := range bc.Preflights {
354355
if shouldSkipPreflight(ctx, preflight, ext, state) {
355356
continue
@@ -384,7 +385,7 @@ func (bc *Boxcutter) apply(ctx context.Context, contentFS fs.FS, ext *ocv1.Clust
384385
return false, "", fmt.Errorf("garbage collecting old revisions: %w", err)
385386
}
386387

387-
if err := bc.createOrUpdate(ctx, desiredRevision); err != nil {
388+
if err := bc.createOrUpdateRevisionWithPreAuthorization(ctx, ext, desiredRevision); err != nil {
388389
return false, "", fmt.Errorf("creating new Revision: %w", err)
389390
}
390391
currentRevision = desiredRevision
@@ -411,6 +412,32 @@ func (bc *Boxcutter) apply(ctx context.Context, contentFS fs.FS, ext *ocv1.Clust
411412
return true, "", nil
412413
}
413414

415+
// runPreAuthorizationChecks runs PreAuthorization checks if the PreAuthorizer is set. An error will be returned if
416+
// the ClusterExtension service account does not have the necessary permissions to manage to the revision's resources
417+
func (bc *Boxcutter) runPreAuthorizationChecks(ctx context.Context, ext *ocv1.ClusterExtension, rev *ocv1.ClusterExtensionRevision) error {
418+
if bc.PreAuthorizer == nil {
419+
return nil
420+
}
421+
422+
// collect the revision manifests
423+
manifestReader, err := revisionManifestReader(rev)
424+
if err != nil {
425+
return err
426+
}
427+
428+
// extract user to check permissions against
429+
manifestManager := getUserInfo(ext)
430+
431+
// collect additional permissions required by OLM to manage the bundle manifests
432+
managementPerms := slices.Concat(
433+
clusterExtensionManagementPermissions(manifestManager, ext),
434+
clusterExtensionRevisionManagementPermissions(manifestManager, rev),
435+
)
436+
437+
// run preauthorization check
438+
return formatPreAuthorizerOutput(bc.PreAuthorizer.PreAuthorize(ctx, manifestManager, manifestReader, managementPerms...))
439+
}
440+
414441
// garbageCollectOldRevisions deletes archived revisions beyond ClusterExtensionRevisionRetentionLimit.
415442
// Active revisions are never deleted. revisionList must be sorted oldest to newest.
416443
func (bc *Boxcutter) garbageCollectOldRevisions(ctx context.Context, revisionList []ocv1.ClusterExtensionRevision) error {
@@ -469,3 +496,43 @@ func splitManifestDocuments(file string) []string {
469496
}
470497
return docs
471498
}
499+
500+
// getObjects returns a slice of all objects in the revision
501+
func getObjects(rev *ocv1.ClusterExtensionRevision) []client.Object {
502+
var objs []client.Object
503+
for _, phase := range rev.Spec.Phases {
504+
for _, phaseObject := range phase.Objects {
505+
objs = append(objs, &phaseObject.Object)
506+
}
507+
}
508+
return objs
509+
}
510+
511+
// revisionMenifestReader returns an io.Reader containing all manifests in the revision
512+
func revisionManifestReader(rev *ocv1.ClusterExtensionRevision) (io.Reader, error) {
513+
var manifestBuilder strings.Builder
514+
for _, obj := range getObjects(rev) {
515+
objBytes, err := yaml.Marshal(obj)
516+
if err != nil {
517+
return nil, fmt.Errorf("error generating revision manifest: %w", err)
518+
}
519+
manifestBuilder.WriteString("---\n")
520+
manifestBuilder.WriteString(string(objBytes))
521+
manifestBuilder.WriteString("\n")
522+
}
523+
return strings.NewReader(manifestBuilder.String()), nil
524+
}
525+
526+
func clusterExtensionRevisionManagementPermissions(manifestManager user.Info, rev *ocv1.ClusterExtensionRevision) []authorizer.AttributesRecord {
527+
return []authorizer.AttributesRecord{
528+
{
529+
User: manifestManager,
530+
Name: rev.Name,
531+
APIGroup: ocv1.GroupVersion.Group,
532+
APIVersion: ocv1.GroupVersion.Version,
533+
Resource: "clusterextensionrevisions/finalizers",
534+
ResourceRequest: true,
535+
Verb: "update",
536+
},
537+
}
538+
}

0 commit comments

Comments
 (0)