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
306307func (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.
392413func (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