Skip to content

Commit 187312f

Browse files
authored
feat: auto respect rbac for discovery/sync (#532)
* feat: respect rbac for resource exclusions Signed-off-by: Soumya Ghosh Dastidar <[email protected]> * feat: use list call to check for permissions Signed-off-by: Soumya Ghosh Dastidar <[email protected]> * feat: updated implementation to handle different levels of rbac check Signed-off-by: Soumya Ghosh Dastidar <[email protected]> * feat: fixed linter error Signed-off-by: Soumya Ghosh Dastidar <[email protected]> * feat: resolve review comments Signed-off-by: Soumya Ghosh Dastidar <[email protected]> --------- Signed-off-by: Soumya Ghosh Dastidar <[email protected]>
1 parent ed7c77a commit 187312f

File tree

3 files changed

+150
-27
lines changed

3 files changed

+150
-27
lines changed

go.mod

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@ require (
77
github.com/evanphx/json-patch v4.12.0+incompatible
88
github.com/go-logr/logr v1.2.2
99
github.com/golang/mock v1.6.0
10+
github.com/google/gnostic v0.5.7-v3refs
1011
github.com/spf13/cobra v1.5.0
1112
github.com/stretchr/testify v1.7.0
1213
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
14+
google.golang.org/protobuf v1.27.1
1315
k8s.io/api v0.24.2
1416
k8s.io/apiextensions-apiserver v0.24.2
1517
k8s.io/apimachinery v0.24.2
@@ -42,7 +44,6 @@ require (
4244
github.com/gogo/protobuf v1.3.2 // indirect
4345
github.com/golang/protobuf v1.5.2 // indirect
4446
github.com/google/btree v1.0.1 // indirect
45-
github.com/google/gnostic v0.5.7-v3refs // indirect
4647
github.com/google/go-cmp v0.5.5 // indirect
4748
github.com/google/gofuzz v1.1.0 // indirect
4849
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
@@ -78,7 +79,6 @@ require (
7879
golang.org/x/text v0.3.7 // indirect
7980
golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect
8081
google.golang.org/appengine v1.6.7 // indirect
81-
google.golang.org/protobuf v1.27.1 // indirect
8282
gopkg.in/inf.v0 v0.9.1 // indirect
8383
gopkg.in/yaml.v2 v2.4.0 // indirect
8484
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect

pkg/cache/cluster.go

Lines changed: 136 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@ import (
1111

1212
"github.com/go-logr/logr"
1313
"golang.org/x/sync/semaphore"
14+
authorizationv1 "k8s.io/api/authorization/v1"
1415
v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
1516
"k8s.io/apimachinery/pkg/api/errors"
17+
k8sErrors "k8s.io/apimachinery/pkg/api/errors"
1618
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1719
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
1820
"k8s.io/apimachinery/pkg/runtime"
@@ -22,6 +24,8 @@ import (
2224
"k8s.io/apimachinery/pkg/util/wait"
2325
"k8s.io/apimachinery/pkg/watch"
2426
"k8s.io/client-go/dynamic"
27+
"k8s.io/client-go/kubernetes"
28+
authType1 "k8s.io/client-go/kubernetes/typed/authorization/v1"
2529
"k8s.io/client-go/rest"
2630
"k8s.io/client-go/tools/cache"
2731
"k8s.io/client-go/tools/pager"
@@ -55,6 +59,15 @@ const (
5559
defaultListSemaphoreWeight = 50
5660
)
5761

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+
5871
type apiMeta struct {
5972
namespaced bool
6073
watchCancel context.CancelFunc
@@ -208,6 +221,8 @@ type clusterCache struct {
208221
eventHandlers map[uint64]OnEventHandler
209222
openAPISchema openapi.Resources
210223
gvkParser *managedfields.GvkParser
224+
225+
respectRBAC int
211226
}
212227

213228
type clusterCacheSync struct {
@@ -462,6 +477,10 @@ func (c *clusterCache) startMissingWatches() error {
462477
if err != nil {
463478
return err
464479
}
480+
clientset, err := kubernetes.NewForConfig(c.config)
481+
if err != nil {
482+
return err
483+
}
465484
namespacedResources := make(map[schema.GroupKind]bool)
466485
for i := range apis {
467486
api := apis[i]
@@ -470,8 +489,25 @@ func (c *clusterCache) startMissingWatches() error {
470489
ctx, cancel := context.WithCancel(context.Background())
471490
c.apisMeta[api.GroupKind] = &apiMeta{namespaced: api.Meta.Namespaced, watchCancel: cancel}
472491

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)
475511
return nil
476512
})
477513
if err != nil {
@@ -530,6 +566,29 @@ func (c *clusterCache) listResources(ctx context.Context, resClient dynamic.Reso
530566
return resourceVersion, callback(listPager)
531567
}
532568

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+
533592
func (c *clusterCache) watchEvents(ctx context.Context, api kube.APIResourceInfo, resClient dynamic.ResourceInterface, ns string, resourceVersion string) {
534593
kube.RetryUntilSucceed(ctx, watchResourcesRetryTimeout, fmt.Sprintf("watch %s on %s", api.GroupKind, c.config.Host), c.log, func() (err error) {
535594
defer func() {
@@ -540,30 +599,10 @@ func (c *clusterCache) watchEvents(ctx context.Context, api kube.APIResourceInfo
540599

541600
// load API initial state if no resource version provided
542601
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)
560603
if err != nil {
561604
return err
562605
}
563-
564-
c.lock.Lock()
565-
c.replaceResourceCache(api.GroupKind, items, ns)
566-
c.lock.Unlock()
567606
}
568607

569608
w, err := watchutil.NewRetryWatcher(resourceVersion, &cache.ListWatch{
@@ -687,7 +726,7 @@ func (c *clusterCache) processApi(client dynamic.Interface, api kube.APIResource
687726
resClient := client.Resource(api.GroupVersionResource)
688727
switch {
689728
// 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):
691730
return callback(resClient, "")
692731
// if manage some namespaces and resource is namespaced
693732
case len(c.namespaces) != 0 && api.Meta.Namespaced:
@@ -702,6 +741,56 @@ func (c *clusterCache) processApi(client dynamic.Interface, api kube.APIResource
702741
return nil
703742
}
704743

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+
705794
func (c *clusterCache) sync() error {
706795
c.log.Info("Start syncing cluster")
707796

@@ -748,6 +837,10 @@ func (c *clusterCache) sync() error {
748837
if err != nil {
749838
return err
750839
}
840+
clientset, err := kubernetes.NewForConfig(config)
841+
if err != nil {
842+
return err
843+
}
751844
lock := sync.Mutex{}
752845
err = kube.RunAllAsync(len(apis), func(i int) error {
753846
api := apis[i]
@@ -773,7 +866,25 @@ func (c *clusterCache) sync() error {
773866
})
774867
})
775868
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)
777888
}
778889

779890
go c.watchEvents(ctx, api, resClient, ns, resourceVersion)

pkg/cache/settings.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,3 +158,15 @@ func SetRetryOptions(maxRetries int32, useBackoff bool, retryFunc ListRetryFunc)
158158
cache.listRetryFunc = retryFunc
159159
}
160160
}
161+
162+
// SetRespectRBAC allows to set whether to respect the controller rbac in list/watches
163+
func SetRespectRBAC(respectRBAC int) UpdateSettingsFunc {
164+
return func(cache *clusterCache) {
165+
// if invalid value is provided disable respect rbac
166+
if respectRBAC < RespectRbacDisabled || respectRBAC > RespectRbacStrict {
167+
cache.respectRBAC = RespectRbacDisabled
168+
} else {
169+
cache.respectRBAC = respectRBAC
170+
}
171+
}
172+
}

0 commit comments

Comments
 (0)