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
286291func (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
311312func (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.
416443func (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