diff --git a/client/rbac_filter.go b/client/rbac_filter.go index 051aeecf..0ab701d2 100644 --- a/client/rbac_filter.go +++ b/client/rbac_filter.go @@ -23,6 +23,7 @@ import ( "strings" "github.com/emicklei/go-restful/v3" + kerrors "github.com/katanomi/pkg/errors" authnv1 "k8s.io/api/authentication/v1" authv1 "k8s.io/api/authorization/v1" "k8s.io/apiserver/pkg/authentication/user" @@ -30,7 +31,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" // metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - kerrors "github.com/katanomi/pkg/errors" + "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime/schema" ) @@ -63,6 +64,7 @@ func DynamicSubjectReviewFilter(ctx context.Context, resourceAttGetter ResourceA kerrors.HandleError(req, resp, err) return } + reqCtx := req.Request.Context() log := logging.FromContext(reqCtx).With( "resource", resourceAtt.Resource, @@ -70,7 +72,6 @@ func DynamicSubjectReviewFilter(ctx context.Context, resourceAttGetter ResourceA "verb", resourceAtt.Verb, ) reqCtx = logging.WithLogger(reqCtx, log) - review := makeSelfSubjectAccessReview(resourceAtt) var clt client.Client if clientGetter, ok := resourceAttGetter.(SubjectAccessReviewClientGetter); ok { @@ -81,12 +82,14 @@ func DynamicSubjectReviewFilter(ctx context.Context, resourceAttGetter ResourceA return } } + if clt == nil { clt = Client(reqCtx) } - err = postSubjectAccessReview(reqCtx, clt, review) + + err = RequestSubjectAccessReview(reqCtx, clt, resourceAtt) if err != nil { - log.Debugw("error verifying user permissions", "err", err, "review", review.GetObject()) + log.Debugw("error verifying user permissions", "err", err) kerrors.HandleError(req, resp, err) return } @@ -94,6 +97,12 @@ func DynamicSubjectReviewFilter(ctx context.Context, resourceAttGetter ResourceA } } +// RequestSubjectAccessReview request the SubjectAccessReview resource to check whether it has permission. +func RequestSubjectAccessReview(ctx context.Context, clt client.Client, resourceAtt authv1.ResourceAttributes) error { + review := makeSelfSubjectAccessReview(resourceAtt) + return postSubjectAccessReview(ctx, clt, review) +} + // SubjectReviewFilterForResource makes a self subject review based a configuration already present inside the // request context using the user's bearer token // also, it makes a subject review based on Impersonate User info in request header diff --git a/multicluster/types.go b/multicluster/types.go index f493be84..7d52057f 100644 --- a/multicluster/types.go +++ b/multicluster/types.go @@ -17,6 +17,7 @@ limitations under the License. package multicluster import ( + authv1 "k8s.io/api/authorization/v1" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -172,3 +173,21 @@ type ClusterCondition struct { // +optional Message string `json:"message,omitempty" protobuf:"bytes,6,opt,name=message"` } + +// ClusterList represents a list of clusters +type ClusterList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + + Items []Cluster `json:"items"` +} + +// ClusterResourceAttributes returns a ResourceAttribute object to be used in a filter +func ClusterResourceAttributes(verb string) authv1.ResourceAttributes { + return authv1.ResourceAttributes{ + Group: ClusterRegistryGroupVersion.Group, + Version: ClusterRegistryGroupVersion.Version, + Resource: ClusterGVR.Resource, + Verb: verb, + } +} diff --git a/resource/doc.go b/resource/doc.go new file mode 100644 index 00000000..ac5ead5a --- /dev/null +++ b/resource/doc.go @@ -0,0 +1,18 @@ +/* +Copyright 2023 The Katanomi Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package resource provides some common operations on ResourceRequirements resources. +package resource diff --git a/resource/resources.go b/resource/resources.go new file mode 100644 index 00000000..17267951 --- /dev/null +++ b/resource/resources.go @@ -0,0 +1,59 @@ +/* +Copyright 2023 The Katanomi Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package resource + +import ( + corev1 "k8s.io/api/core/v1" +) + +// SumResources sums many ResourceRequirements resources. +// Note: No operations are performed on Claims. only the sum of Limits and Requests is completed. +func SumResources(resources ...corev1.ResourceRequirements) corev1.ResourceRequirements { + if len(resources) == 0 { + return corev1.ResourceRequirements{} + } + + sumLimits := make([]corev1.ResourceList, 0, len(resources)) + sumRequests := make([]corev1.ResourceList, 0, len(resources)) + for _, item := range resources { + sumLimits = append(sumLimits, item.Limits) + sumRequests = append(sumRequests, item.Requests) + } + + return corev1.ResourceRequirements{ + Limits: SumResourceList(sumLimits...), + Requests: SumResourceList(sumRequests...), + } +} + +// SumResourceList sums many ResourceList resources. +func SumResourceList(list ...corev1.ResourceList) corev1.ResourceList { + result := corev1.ResourceList{} + for _, item := range list { + for key, itemQuantity := range item { + tmpQuantity := itemQuantity + if v, ok := result[key]; ok { + v.Add(itemQuantity) + result[key] = v + } else { + result[key] = tmpQuantity + } + } + } + + return result +} diff --git a/resource/resources_test.go b/resource/resources_test.go new file mode 100644 index 00000000..919cc4e1 --- /dev/null +++ b/resource/resources_test.go @@ -0,0 +1,151 @@ +/* +Copyright 2023 The Katanomi Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package resource + +import ( + "fmt" + "testing" + + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" +) + +func TestSumResources(t *testing.T) { + tests := map[string]struct { + param1 corev1.ResourceRequirements + param2 corev1.ResourceRequirements + want corev1.ResourceRequirements + }{ + "param1 and param2 is empty": {}, + "param1 is empty": { + param2: corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("2"), + corev1.ResourceMemory: resource.MustParse("2Gi"), + }, + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("2"), + corev1.ResourceMemory: resource.MustParse("2Gi"), + }, + }, + want: corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("2"), + corev1.ResourceMemory: resource.MustParse("2Gi"), + }, + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("2"), + corev1.ResourceMemory: resource.MustParse("2Gi"), + }, + }, + }, + "param2 is empty": { + param1: corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("2"), + corev1.ResourceMemory: resource.MustParse("2Gi"), + }, + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("2"), + corev1.ResourceMemory: resource.MustParse("2Gi"), + }, + }, + want: corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("2"), + corev1.ResourceMemory: resource.MustParse("2Gi"), + }, + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("2"), + corev1.ResourceMemory: resource.MustParse("2Gi"), + }, + }, + }, + "param1 and param2 with value": { + param2: corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("2"), + corev1.ResourceMemory: resource.MustParse("2Gi"), + }, + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("2"), + corev1.ResourceMemory: resource.MustParse("2Gi"), + }, + }, + param1: corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("2"), + corev1.ResourceMemory: resource.MustParse("2Gi"), + }, + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("2"), + corev1.ResourceMemory: resource.MustParse("2Gi"), + }, + }, + want: corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("4"), + corev1.ResourceMemory: resource.MustParse("4Gi"), + }, + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("4"), + corev1.ResourceMemory: resource.MustParse("4Gi"), + }, + }, + }, + "just has cpu or memory": { + param2: corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("2"), + corev1.ResourceMemory: resource.MustParse("2Gi"), + }, + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("2"), + corev1.ResourceMemory: resource.MustParse("2Gi"), + }, + }, + param1: corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("4"), + }, + Requests: corev1.ResourceList{ + corev1.ResourceMemory: resource.MustParse("4Gi"), + }, + }, + want: corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("6"), + corev1.ResourceMemory: resource.MustParse("2Gi"), + }, + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("2"), + corev1.ResourceMemory: resource.MustParse("6Gi"), + }, + }, + }, + } + + g := NewGomegaWithT(t) + for name, tt := range tests { + got := SumResources(tt.param1, tt.param2) + g.Expect(got.Limits.Cpu().Value()).Should(Equal(tt.want.Limits.Cpu().Value()), fmt.Sprintf("%s: Limits.Cpu", name)) + g.Expect(got.Limits.Memory().Value()).Should(Equal(tt.want.Limits.Memory().Value()), fmt.Sprintf("%s: Limits.Memory", name)) + g.Expect(got.Requests.Cpu().Value()).Should(Equal(tt.want.Requests.Cpu().Value()), fmt.Sprintf("%s: Requests.Cpu", name)) + g.Expect(got.Requests.Memory().Value()).Should(Equal(tt.want.Requests.Memory().Value()), fmt.Sprintf("%s: Requests.Memory", name)) + } +}