@@ -11,8 +11,10 @@ import (
11
11
12
12
"github.com/go-logr/logr"
13
13
"golang.org/x/sync/semaphore"
14
+ authorizationv1 "k8s.io/api/authorization/v1"
14
15
v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
15
16
"k8s.io/apimachinery/pkg/api/errors"
17
+ k8sErrors "k8s.io/apimachinery/pkg/api/errors"
16
18
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
17
19
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
18
20
"k8s.io/apimachinery/pkg/runtime"
@@ -22,6 +24,8 @@ import (
22
24
"k8s.io/apimachinery/pkg/util/wait"
23
25
"k8s.io/apimachinery/pkg/watch"
24
26
"k8s.io/client-go/dynamic"
27
+ "k8s.io/client-go/kubernetes"
28
+ authType1 "k8s.io/client-go/kubernetes/typed/authorization/v1"
25
29
"k8s.io/client-go/rest"
26
30
"k8s.io/client-go/tools/cache"
27
31
"k8s.io/client-go/tools/pager"
@@ -55,6 +59,15 @@ const (
55
59
defaultListSemaphoreWeight = 50
56
60
)
57
61
62
+ const (
63
+ // RespectRbacDisabled default value for respectRbac
64
+ RespectRbacDisabled = iota
65
+ // RespectRbacNormal checks only api response for forbidden/unauthorized errors
66
+ RespectRbacNormal
67
+ // RespectRbacStrict checks both api response for forbidden/unauthorized errors and SelfSubjectAccessReview
68
+ RespectRbacStrict
69
+ )
70
+
58
71
type apiMeta struct {
59
72
namespaced bool
60
73
watchCancel context.CancelFunc
@@ -208,6 +221,8 @@ type clusterCache struct {
208
221
eventHandlers map [uint64 ]OnEventHandler
209
222
openAPISchema openapi.Resources
210
223
gvkParser * managedfields.GvkParser
224
+
225
+ respectRBAC int
211
226
}
212
227
213
228
type clusterCacheSync struct {
@@ -462,6 +477,10 @@ func (c *clusterCache) startMissingWatches() error {
462
477
if err != nil {
463
478
return err
464
479
}
480
+ clientset , err := kubernetes .NewForConfig (c .config )
481
+ if err != nil {
482
+ return err
483
+ }
465
484
namespacedResources := make (map [schema.GroupKind ]bool )
466
485
for i := range apis {
467
486
api := apis [i ]
@@ -470,8 +489,25 @@ func (c *clusterCache) startMissingWatches() error {
470
489
ctx , cancel := context .WithCancel (context .Background ())
471
490
c .apisMeta [api .GroupKind ] = & apiMeta {namespaced : api .Meta .Namespaced , watchCancel : cancel }
472
491
473
- err = c .processApi (client , api , func (resClient dynamic.ResourceInterface , ns string ) error {
474
- go c .watchEvents (ctx , api , resClient , ns , "" )
492
+ err := c .processApi (client , api , func (resClient dynamic.ResourceInterface , ns string ) error {
493
+ resourceVersion , err := c .loadInitialState (ctx , api , resClient , ns )
494
+ if err != nil && c .isRestrictedResource (err ) {
495
+ keep := false
496
+ if c .respectRBAC == RespectRbacStrict {
497
+ k , permErr := c .checkPermission (ctx , clientset .AuthorizationV1 ().SelfSubjectAccessReviews (), api )
498
+ if permErr != nil {
499
+ return fmt .Errorf ("failed to check permissions for resource %s: %w, original error=%v" , api .GroupKind .String (), permErr , err .Error ())
500
+ }
501
+ keep = k
502
+ }
503
+ // if we are not allowed to list the resource, remove it from the watch list
504
+ if ! keep {
505
+ delete (c .apisMeta , api .GroupKind )
506
+ delete (namespacedResources , api .GroupKind )
507
+ return nil
508
+ }
509
+ }
510
+ go c .watchEvents (ctx , api , resClient , ns , resourceVersion )
475
511
return nil
476
512
})
477
513
if err != nil {
@@ -530,6 +566,29 @@ func (c *clusterCache) listResources(ctx context.Context, resClient dynamic.Reso
530
566
return resourceVersion , callback (listPager )
531
567
}
532
568
569
+ func (c * clusterCache ) loadInitialState (ctx context.Context , api kube.APIResourceInfo , resClient dynamic.ResourceInterface , ns string ) (string , error ) {
570
+ return c .listResources (ctx , resClient , func (listPager * pager.ListPager ) error {
571
+ var items []* Resource
572
+ err := listPager .EachListItem (ctx , metav1.ListOptions {}, func (obj runtime.Object ) error {
573
+ if un , ok := obj .(* unstructured.Unstructured ); ! ok {
574
+ return fmt .Errorf ("object %s/%s has an unexpected type" , un .GroupVersionKind ().String (), un .GetName ())
575
+ } else {
576
+ items = append (items , c .newResource (un ))
577
+ }
578
+ return nil
579
+ })
580
+
581
+ if err != nil {
582
+ return fmt .Errorf ("failed to load initial state of resource %s: %w" , api .GroupKind .String (), err )
583
+ }
584
+
585
+ return runSynced (& c .lock , func () error {
586
+ c .replaceResourceCache (api .GroupKind , items , ns )
587
+ return nil
588
+ })
589
+ })
590
+ }
591
+
533
592
func (c * clusterCache ) watchEvents (ctx context.Context , api kube.APIResourceInfo , resClient dynamic.ResourceInterface , ns string , resourceVersion string ) {
534
593
kube .RetryUntilSucceed (ctx , watchResourcesRetryTimeout , fmt .Sprintf ("watch %s on %s" , api .GroupKind , c .config .Host ), c .log , func () (err error ) {
535
594
defer func () {
@@ -540,30 +599,10 @@ func (c *clusterCache) watchEvents(ctx context.Context, api kube.APIResourceInfo
540
599
541
600
// load API initial state if no resource version provided
542
601
if resourceVersion == "" {
543
- var items []* Resource
544
- resourceVersion , err = c .listResources (ctx , resClient , func (listPager * pager.ListPager ) error {
545
- err := listPager .EachListItem (ctx , metav1.ListOptions {}, func (obj runtime.Object ) error {
546
- if un , ok := obj .(* unstructured.Unstructured ); ! ok {
547
- return fmt .Errorf ("object %s/%s has an unexpected type" , un .GroupVersionKind ().String (), un .GetName ())
548
- } else {
549
- items = append (items , c .newResource (un ))
550
- }
551
- return nil
552
- })
553
-
554
- if err != nil {
555
- return fmt .Errorf ("failed to load initial state of resource %s: %v" , api .GroupKind .String (), err )
556
- }
557
- return nil
558
- })
559
-
602
+ resourceVersion , err = c .loadInitialState (ctx , api , resClient , ns )
560
603
if err != nil {
561
604
return err
562
605
}
563
-
564
- c .lock .Lock ()
565
- c .replaceResourceCache (api .GroupKind , items , ns )
566
- c .lock .Unlock ()
567
606
}
568
607
569
608
w , err := watchutil .NewRetryWatcher (resourceVersion , & cache.ListWatch {
@@ -687,7 +726,7 @@ func (c *clusterCache) processApi(client dynamic.Interface, api kube.APIResource
687
726
resClient := client .Resource (api .GroupVersionResource )
688
727
switch {
689
728
// if manage whole cluster or resource is cluster level and cluster resources enabled
690
- case len (c .namespaces ) == 0 || ! api .Meta .Namespaced && c .clusterResources :
729
+ case len (c .namespaces ) == 0 || ( ! api .Meta .Namespaced && c .clusterResources ) :
691
730
return callback (resClient , "" )
692
731
// if manage some namespaces and resource is namespaced
693
732
case len (c .namespaces ) != 0 && api .Meta .Namespaced :
@@ -702,6 +741,56 @@ func (c *clusterCache) processApi(client dynamic.Interface, api kube.APIResource
702
741
return nil
703
742
}
704
743
744
+ // isRestrictedResource checks if the kube api call is unauthorized or forbidden
745
+ func (c * clusterCache ) isRestrictedResource (err error ) bool {
746
+ return c .respectRBAC != RespectRbacDisabled && (k8sErrors .IsForbidden (err ) || k8sErrors .IsUnauthorized (err ))
747
+ }
748
+
749
+ // checkPermission runs a self subject access review to check if the controller has permissions to list the resource
750
+ func (c * clusterCache ) checkPermission (ctx context.Context , reviewInterface authType1.SelfSubjectAccessReviewInterface , api kube.APIResourceInfo ) (keep bool , err error ) {
751
+ sar := & authorizationv1.SelfSubjectAccessReview {
752
+ Spec : authorizationv1.SelfSubjectAccessReviewSpec {
753
+ ResourceAttributes : & authorizationv1.ResourceAttributes {
754
+ Namespace : "*" ,
755
+ Verb : "list" , // uses list verb to check for permissions
756
+ Resource : api .GroupVersionResource .Resource ,
757
+ },
758
+ },
759
+ }
760
+
761
+ switch {
762
+ // if manage whole cluster or resource is cluster level and cluster resources enabled
763
+ case len (c .namespaces ) == 0 || (! api .Meta .Namespaced && c .clusterResources ):
764
+ resp , err := reviewInterface .Create (ctx , sar , metav1.CreateOptions {})
765
+ if err != nil {
766
+ return false , err
767
+ }
768
+ if resp != nil && resp .Status .Allowed {
769
+ return true , nil
770
+ }
771
+ // unsupported, remove from watch list
772
+ return false , nil
773
+ // if manage some namespaces and resource is namespaced
774
+ case len (c .namespaces ) != 0 && api .Meta .Namespaced :
775
+ for _ , ns := range c .namespaces {
776
+ sar .Spec .ResourceAttributes .Namespace = ns
777
+ resp , err := reviewInterface .Create (ctx , sar , metav1.CreateOptions {})
778
+ if err != nil {
779
+ return false , err
780
+ }
781
+ if resp != nil && resp .Status .Allowed {
782
+ return true , nil
783
+ } else {
784
+ // unsupported, remove from watch list
785
+ return false , nil
786
+ }
787
+ }
788
+ }
789
+ // checkPermission follows the same logic of determining namespace/cluster resource as the processApi function
790
+ // so if neither of the cases match it means the controller will not watch for it so it is safe to return true.
791
+ return true , nil
792
+ }
793
+
705
794
func (c * clusterCache ) sync () error {
706
795
c .log .Info ("Start syncing cluster" )
707
796
@@ -748,6 +837,10 @@ func (c *clusterCache) sync() error {
748
837
if err != nil {
749
838
return err
750
839
}
840
+ clientset , err := kubernetes .NewForConfig (config )
841
+ if err != nil {
842
+ return err
843
+ }
751
844
lock := sync.Mutex {}
752
845
err = kube .RunAllAsync (len (apis ), func (i int ) error {
753
846
api := apis [i ]
@@ -773,7 +866,25 @@ func (c *clusterCache) sync() error {
773
866
})
774
867
})
775
868
if err != nil {
776
- return fmt .Errorf ("failed to load initial state of resource %s: %v" , api .GroupKind .String (), err )
869
+ if c .isRestrictedResource (err ) {
870
+ keep := false
871
+ if c .respectRBAC == RespectRbacStrict {
872
+ k , permErr := c .checkPermission (ctx , clientset .AuthorizationV1 ().SelfSubjectAccessReviews (), api )
873
+ if permErr != nil {
874
+ return fmt .Errorf ("failed to check permissions for resource %s: %w, original error=%v" , api .GroupKind .String (), permErr , err .Error ())
875
+ }
876
+ keep = k
877
+ }
878
+ // if we are not allowed to list the resource, remove it from the watch list
879
+ if ! keep {
880
+ lock .Lock ()
881
+ delete (c .apisMeta , api .GroupKind )
882
+ delete (c .namespacedResources , api .GroupKind )
883
+ lock .Unlock ()
884
+ return nil
885
+ }
886
+ }
887
+ return fmt .Errorf ("failed to load initial state of resource %s: %w" , api .GroupKind .String (), err )
777
888
}
778
889
779
890
go c .watchEvents (ctx , api , resClient , ns , resourceVersion )
0 commit comments