diff --git a/pkg/webhook/fleetresourcehandler/fleetresourcehandler_webhook.go b/pkg/webhook/fleetresourcehandler/fleetresourcehandler_webhook.go index bc9650b03..77dbb3a83 100644 --- a/pkg/webhook/fleetresourcehandler/fleetresourcehandler_webhook.go +++ b/pkg/webhook/fleetresourcehandler/fleetresourcehandler_webhook.go @@ -7,9 +7,11 @@ import ( "regexp" admissionv1 "k8s.io/api/admission/v1" + rbacv1 "k8s.io/api/rbac/v1" v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" "k8s.io/klog/v2" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/manager" @@ -23,9 +25,11 @@ import ( const ( // ValidationPath is the webhook service path which admission requests are routed to for validating custom resource definition resources. ValidationPath = "/validate-v1-fleetresourcehandler" - groupMatch = `^[^.]*\.(.*)` crdKind = "CustomResourceDefinition" memberClusterKind = "MemberCluster" + roleKind = "Role" + roleBindingKind = "RoleBinding" + groupMatch = `^[^.]*\.(.*)` ) // Add registers the webhook for K8s bulit-in object types. @@ -41,51 +45,71 @@ type fleetResourceValidator struct { decoder *admission.Decoder } -func (v *fleetResourceValidator) Handle(ctx context.Context, req admission.Request) admission.Response { +// Handle receives the request then allows/denies the request to modify fleet resources. +func (v *fleetResourceValidator) Handle(_ context.Context, req admission.Request) admission.Response { + namespacedName := types.NamespacedName{Name: req.Name, Namespace: req.Namespace} var response admission.Response if req.Operation == admissionv1.Create || req.Operation == admissionv1.Update || req.Operation == admissionv1.Delete { switch req.Kind { case createCRDGVK(): - klog.V(2).InfoS("handling CRD resource", "GVK", createCRDGVK()) + klog.V(2).InfoS("handling CRD resource", "GVK", createCRDGVK(), "namespacedName", namespacedName) response = v.handleCRD(req) case createMemberClusterGVK(): - klog.V(2).InfoS("handling Member cluster resource", "GVK", createMemberClusterGVK()) - response = v.handleMemberCluster(ctx, req) + klog.V(2).InfoS("handling Member cluster resource", "GVK", createMemberClusterGVK(), "namespacedName", namespacedName) + response = v.handleMemberCluster(req) + case createRoleGVK(): + klog.V(2).InfoS("handling Role resource", "GVK", createRoleGVK(), "namespacedName", namespacedName) + response = v.handleRole(req) + case createRoleBindingGVK(): + klog.V(2).InfoS("handling Role binding resource", "GVK", createRoleBindingGVK(), "namespacedName", namespacedName) + response = v.handleRoleBinding(req) default: - klog.V(2).InfoS("resource is not monitored by fleet resource validator webhook", "GVK", req.Kind.String()) + klog.V(2).InfoS("resource is not monitored by fleet resource validator webhook", "GVK", req.Kind.String(), "namespacedName", namespacedName) response = admission.Allowed(fmt.Sprintf("user: %s in groups: %v is allowed to modify resource with GVK: %s", req.UserInfo.Username, req.UserInfo.Groups, req.Kind.String())) } } return response } +// handleCRD allows/denies the request to modify CRD object after validation. func (v *fleetResourceValidator) handleCRD(req admission.Request) admission.Response { var crd v1.CustomResourceDefinition if err := v.decodeRequestObject(req, &crd); err != nil { return admission.Errored(http.StatusBadRequest, err) } - // This regex works because every CRD name in kubernetes follows this pattern .. group := regexp.MustCompile(groupMatch).FindStringSubmatch(crd.Name)[1] - if validation.CheckCRDGroup(group) && !validation.ValidateUserForCRD(v.whiteListedUsers, req.UserInfo) { - return admission.Denied(fmt.Sprintf("failed to validate user: %s in groups: %v to modify fleet CRD: %s", req.UserInfo.Username, req.UserInfo.Groups, crd.Name)) - } - return admission.Allowed(fmt.Sprintf("user: %s in groups: %v is allowed to modify CRD: %s", req.UserInfo.Username, req.UserInfo.Groups, crd.Name)) + return validation.ValidateUserForFleetCRD(group, types.NamespacedName{Name: crd.Name, Namespace: crd.Namespace}, v.whiteListedUsers, req.UserInfo) } -func (v *fleetResourceValidator) handleMemberCluster(ctx context.Context, req admission.Request) admission.Response { +// handleMemberCluster allows/denies the request to modify member cluster object after validation. +func (v *fleetResourceValidator) handleMemberCluster(req admission.Request) admission.Response { var mc fleetv1alpha1.MemberCluster if err := v.decodeRequestObject(req, &mc); err != nil { return admission.Errored(http.StatusBadRequest, err) } + return validation.ValidateUserForFleetCR(mc.Kind, types.NamespacedName{Name: mc.Name, Namespace: mc.Namespace}, v.whiteListedUsers, req.UserInfo) +} + +// handleRole allows/denies the request to modify role after validation. +func (v *fleetResourceValidator) handleRole(req admission.Request) admission.Response { + var role rbacv1.Role + if err := v.decodeRequestObject(req, &role); err != nil { + return admission.Errored(http.StatusBadRequest, err) + } + return validation.ValidateUserForResource(role.Kind, types.NamespacedName{Name: role.Name, Namespace: role.Namespace}, v.whiteListedUsers, req.UserInfo) +} - if !validation.ValidateUserForFleetCR(ctx, v.client, v.whiteListedUsers, req.UserInfo) { - return admission.Denied(fmt.Sprintf("failed to validate user: %s in groups: %v to modify member cluster CR: %s", req.UserInfo.Username, req.UserInfo.Groups, mc.Name)) +// handleRoleBinding allows/denies the request to modify role after validation. +func (v *fleetResourceValidator) handleRoleBinding(req admission.Request) admission.Response { + var rb rbacv1.RoleBinding + if err := v.decodeRequestObject(req, &rb); err != nil { + return admission.Errored(http.StatusBadRequest, err) } - klog.V(2).InfoS("user in groups is allowed to modify member cluster CR", "user", req.UserInfo.Username, "groups", req.UserInfo.Groups) - return admission.Allowed(fmt.Sprintf("user: %s in groups: %v is allowed to modify member cluster: %s", req.UserInfo.Username, req.UserInfo.Groups, mc.Name)) + return validation.ValidateUserForResource(rb.Kind, types.NamespacedName{Name: rb.Name, Namespace: rb.Namespace}, v.whiteListedUsers, req.UserInfo) } +// decodeRequestObject decodes the request object into the passed runtime object. func (v *fleetResourceValidator) decodeRequestObject(req admission.Request, obj runtime.Object) error { if req.Operation == admissionv1.Delete { // req.Object is not populated for delete: https://github.com/kubernetes-sigs/controller-runtime/issues/1762. @@ -102,11 +126,13 @@ func (v *fleetResourceValidator) decodeRequestObject(req admission.Request, obj return nil } +// InjectDecoder injects the decoder into fleetResourceValidator. func (v *fleetResourceValidator) InjectDecoder(d *admission.Decoder) error { v.decoder = d return nil } +// createCRDGVK returns GVK for CRD. func createCRDGVK() metav1.GroupVersionKind { return metav1.GroupVersionKind{ Group: v1.SchemeGroupVersion.Group, @@ -115,6 +141,7 @@ func createCRDGVK() metav1.GroupVersionKind { } } +// createMemberClusterGVK returns GVK for member cluster. func createMemberClusterGVK() metav1.GroupVersionKind { return metav1.GroupVersionKind{ Group: fleetv1alpha1.GroupVersion.Group, @@ -122,3 +149,21 @@ func createMemberClusterGVK() metav1.GroupVersionKind { Kind: memberClusterKind, } } + +// createRoleGVK return GVK for role. +func createRoleGVK() metav1.GroupVersionKind { + return metav1.GroupVersionKind{ + Group: rbacv1.SchemeGroupVersion.Group, + Version: rbacv1.SchemeGroupVersion.Version, + Kind: roleKind, + } +} + +// createRoleBindingGVK returns GVK for role binding. +func createRoleBindingGVK() metav1.GroupVersionKind { + return metav1.GroupVersionKind{ + Group: rbacv1.SchemeGroupVersion.Group, + Version: rbacv1.SchemeGroupVersion.Version, + Kind: roleBindingKind, + } +} diff --git a/pkg/webhook/validation/crdvalidation.go b/pkg/webhook/validation/crdvalidation.go deleted file mode 100644 index 4749cbdb3..000000000 --- a/pkg/webhook/validation/crdvalidation.go +++ /dev/null @@ -1,12 +0,0 @@ -package validation - -import "k8s.io/utils/strings/slices" - -var ( - validObjectGroups = []string{"networking.fleet.azure.com", "fleet.azure.com", "multicluster.x-k8s.io", "placement.azure.com"} -) - -// CheckCRDGroup checks to see if the input CRD group is a fleet CRD group. -func CheckCRDGroup(group string) bool { - return slices.Contains(validObjectGroups, group) -} diff --git a/pkg/webhook/validation/uservalidation.go b/pkg/webhook/validation/uservalidation.go index 41a88d99d..f47fdae96 100644 --- a/pkg/webhook/validation/uservalidation.go +++ b/pkg/webhook/validation/uservalidation.go @@ -1,43 +1,71 @@ package validation import ( - "context" + "fmt" authenticationv1 "k8s.io/api/authentication/v1" + "k8s.io/apimachinery/pkg/types" "k8s.io/klog/v2" "k8s.io/utils/strings/slices" - "sigs.k8s.io/controller-runtime/pkg/client" - - fleetv1alpha1 "go.goms.io/fleet/apis/v1alpha1" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" ) const ( - mastersGroup = "system:masters" + mastersGroup = "system:masters" + serviceAccountsGroup = "system:serviceaccounts" + + crdAllowedFormat = "user: %s in groups: %v is allowed to modify fleet CRD: %+v" + crdDeniedFormat = "user: %s in groups: %v is not allowed to modify fleet CRD: %+v" + fleetResourceAllowedFormat = "user: %s in groups: %v is allowed to modify fleet resource %s: %+v" + fleetResourceDeniedFormat = "user: %s in groups: %v is not allowed to modify fleet resource %s: %+v" ) -// ValidateUserForCRD checks to see if user is authenticated to make a request to modify fleet CRDs. -func ValidateUserForCRD(whiteListedUsers []string, userInfo authenticationv1.UserInfo) bool { - return isMasterGroupUserOrWhiteListedUser(whiteListedUsers, userInfo) -} +var ( + fleetCRDGroups = []string{"networking.fleet.azure.com", "fleet.azure.com", "multicluster.x-k8s.io", "placement.azure.com"} +) -// ValidateUserForFleetCR checks to see if user is authenticated to make a request to modify Fleet CRs. -func ValidateUserForFleetCR(ctx context.Context, client client.Client, whiteListedUsers []string, userInfo authenticationv1.UserInfo) bool { - if isMasterGroupUserOrWhiteListedUser(whiteListedUsers, userInfo) { - return true +// ValidateUserForFleetCRD checks to see if user is not allowed to modify fleet CRDs. +func ValidateUserForFleetCRD(group string, namespacedName types.NamespacedName, whiteListedUsers []string, userInfo authenticationv1.UserInfo) admission.Response { + if checkCRDGroup(group) && !isMasterGroupUserOrWhiteListedUser(whiteListedUsers, userInfo) { + klog.V(2).InfoS("user in groups is not allowed to modify CRD", "user", userInfo.Username, "groups", userInfo.Groups, "namespacedName", namespacedName) + return admission.Denied(fmt.Sprintf(crdDeniedFormat, userInfo.Username, userInfo.Groups, namespacedName)) } - var memberClusterList fleetv1alpha1.MemberClusterList - if err := client.List(ctx, &memberClusterList); err != nil { - klog.V(2).ErrorS(err, "failed to list member clusters") - return false + klog.V(2).InfoS("user in groups is allowed to modify CRD", "user", userInfo.Username, "groups", userInfo.Groups, "namespacedName", namespacedName) + return admission.Allowed(fmt.Sprintf(crdAllowedFormat, userInfo.Username, userInfo.Groups, namespacedName)) +} + +// ValidateUserForFleetCR checks to see if user is allowed to make a request to modify fleet CRs. +func ValidateUserForFleetCR(resKind string, namespacedName types.NamespacedName, whiteListedUsers []string, userInfo authenticationv1.UserInfo) admission.Response { + var response admission.Response + // TODO(Arvindthiru): this switch will be expanded for all fleet CR validations. + switch resKind { + case "MemberCluster": + response = ValidateUserForResource(resKind, namespacedName, whiteListedUsers, userInfo) } - identities := make([]string, len(memberClusterList.Items)) - for i := range memberClusterList.Items { - identities = append(identities, memberClusterList.Items[i].Spec.Identity.Name) + return response +} + +// ValidateUserForResource checks to see if user is allowed to modify argued fleet resource. +func ValidateUserForResource(resKind string, namespacedName types.NamespacedName, whiteListedUsers []string, userInfo authenticationv1.UserInfo) admission.Response { + if isMasterGroupUserOrWhiteListedUser(whiteListedUsers, userInfo) || isUserAuthenticatedServiceAccount(userInfo) { + klog.V(2).InfoS("user in groups is allowed to modify fleet resource", "user", userInfo.Username, "groups", userInfo.Groups, "kind", resKind, "namespacedName", namespacedName) + return admission.Allowed(fmt.Sprintf(fleetResourceAllowedFormat, userInfo.Username, userInfo.Groups, resKind, namespacedName)) } - // this ensures will allow all member agents are validated. - return slices.Contains(identities, userInfo.Username) + klog.V(2).InfoS("user in groups is not allowed to modify fleet resource", "user", userInfo.Username, "groups", userInfo.Groups, "kind", resKind, "namespacedName", namespacedName) + return admission.Denied(fmt.Sprintf(fleetResourceDeniedFormat, userInfo.Username, userInfo.Groups, resKind, namespacedName)) } +// isMasterGroupUserOrWhiteListedUser returns true is user belongs to white listed users or user belongs to system:masters group. func isMasterGroupUserOrWhiteListedUser(whiteListedUsers []string, userInfo authenticationv1.UserInfo) bool { return slices.Contains(whiteListedUsers, userInfo.Username) || slices.Contains(userInfo.Groups, mastersGroup) } + +// isUserAuthenticatedServiceAccount returns true if user is a valid service account. +func isUserAuthenticatedServiceAccount(userInfo authenticationv1.UserInfo) bool { + return slices.Contains(userInfo.Groups, serviceAccountsGroup) +} + +// checkCRDGroup returns true if the input CRD group is a fleet CRD group. +func checkCRDGroup(group string) bool { + return slices.Contains(fleetCRDGroups, group) +} diff --git a/pkg/webhook/validation/uservalidation_test.go b/pkg/webhook/validation/uservalidation_test.go index 0ecaf05ab..910a60f4b 100644 --- a/pkg/webhook/validation/uservalidation_test.go +++ b/pkg/webhook/validation/uservalidation_test.go @@ -1,122 +1,123 @@ package validation import ( - "context" + "fmt" "testing" - "github.com/crossplane/crossplane-runtime/pkg/test" "github.com/stretchr/testify/assert" v1 "k8s.io/api/authentication/v1" - rbacv1 "k8s.io/api/rbac/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "sigs.k8s.io/controller-runtime/pkg/client" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" - fleetv1alpha1 "go.goms.io/fleet/apis/v1alpha1" "go.goms.io/fleet/pkg/utils" ) -func TestValidateUserForCRD(t *testing.T) { +func TestValidateUserForResource(t *testing.T) { testCases := map[string]struct { - userInfo v1.UserInfo + resKind string + namespacedName types.NamespacedName whiteListedUsers []string - wantResult bool + userInfo v1.UserInfo + wantResponse admission.Response }{ "allow user in system:masters group": { userInfo: v1.UserInfo{ Username: "test-user", - Groups: []string{"system:masters"}, + Groups: []string{mastersGroup}, }, - wantResult: true, + resKind: "Role", + namespacedName: types.NamespacedName{Name: "test-role", Namespace: "test-namespace"}, + wantResponse: admission.Allowed(fmt.Sprintf(fleetResourceAllowedFormat, "test-user", []string{mastersGroup}, "Role", types.NamespacedName{Name: "test-role", Namespace: "test-namespace"})), }, "allow white listed user not in system:masters group": { userInfo: v1.UserInfo{ Username: "test-user", + Groups: []string{"test-group"}, }, + resKind: "RoleBinding", + namespacedName: types.NamespacedName{Name: "test-role-binding", Namespace: "test-namespace"}, whiteListedUsers: []string{"test-user"}, - wantResult: true, + wantResponse: admission.Allowed(fmt.Sprintf(fleetResourceAllowedFormat, "test-user", []string{"test-group"}, "RoleBinding", types.NamespacedName{Name: "test-role-binding", Namespace: "test-namespace"})), + }, + "allow valid service account": { + userInfo: v1.UserInfo{ + Username: "test-user", + Groups: []string{serviceAccountsGroup}, + }, + resKind: "RoleBinding", + namespacedName: types.NamespacedName{Name: "test-role-binding", Namespace: "test-namespace"}, + wantResponse: admission.Allowed(fmt.Sprintf(fleetResourceAllowedFormat, "test-user", []string{serviceAccountsGroup}, "RoleBinding", types.NamespacedName{Name: "test-role-binding", Namespace: "test-namespace"})), }, "fail to validate user with invalid username, groups": { userInfo: v1.UserInfo{ Username: "test-user", Groups: []string{"test-group"}, }, - wantResult: false, + resKind: "Role", + namespacedName: types.NamespacedName{Name: "test-role", Namespace: "test-namespace"}, + wantResponse: admission.Denied(fmt.Sprintf(fleetResourceDeniedFormat, "test-user", []string{"test-group"}, "Role", types.NamespacedName{Name: "test-role", Namespace: "test-namespace"})), }, } for testName, testCase := range testCases { t.Run(testName, func(t *testing.T) { - gotResult := ValidateUserForCRD(testCase.whiteListedUsers, testCase.userInfo) - assert.Equal(t, testCase.wantResult, gotResult, utils.TestCaseMsg, testName) + gotResult := ValidateUserForResource(testCase.resKind, testCase.namespacedName, testCase.whiteListedUsers, testCase.userInfo) + assert.Equal(t, testCase.wantResponse, gotResult, utils.TestCaseMsg, testName) }) } } -func TestValidateUserForFleetCR(t *testing.T) { +func TestValidateUserForFleetCRD(t *testing.T) { testCases := map[string]struct { - client client.Client + group string + namespacedName types.NamespacedName whiteListedUsers []string userInfo v1.UserInfo - wantResult bool + wantResponse admission.Response }{ - "allow use in system:masters group": { + "allow user in system:masters group to modify other CRD": { + group: "other-group", + namespacedName: types.NamespacedName{Name: "test-crd"}, userInfo: v1.UserInfo{ Username: "test-user", Groups: []string{"system:masters"}, }, - wantResult: true, + wantResponse: admission.Allowed(fmt.Sprintf(crdAllowedFormat, "test-user", []string{mastersGroup}, types.NamespacedName{Name: "test-crd"})), }, - "allow white listed user not in system:masters group": { + "allow user in system:masters group to modify fleet CRD": { + group: "fleet.azure.com", + namespacedName: types.NamespacedName{Name: "memberclusters.fleet.azure.com"}, userInfo: v1.UserInfo{ Username: "test-user", + Groups: []string{"system:masters"}, }, - whiteListedUsers: []string{"test-user"}, - wantResult: true, + wantResponse: admission.Allowed(fmt.Sprintf(crdAllowedFormat, "test-user", []string{mastersGroup}, types.NamespacedName{Name: "memberclusters.fleet.azure.com"})), }, - "allow member cluster identity": { - client: &test.MockClient{ - MockList: func(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error { - o := list.(*fleetv1alpha1.MemberClusterList) - *o = fleetv1alpha1.MemberClusterList{ - Items: []fleetv1alpha1.MemberCluster{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "test-member-cluster", - }, - Spec: fleetv1alpha1.MemberClusterSpec{ - Identity: rbacv1.Subject{ - Name: "member-cluster-identity", - }, - }, - }, - }, - } - return nil - }, - }, + "allow white listed user to modify fleet CRD": { + group: "fleet.azure.com", + namespacedName: types.NamespacedName{Name: "memberclusters.fleet.azure.com"}, userInfo: v1.UserInfo{ - Username: "member-cluster-identity", + Username: "test-user", + Groups: []string{"test-group"}, }, - wantResult: true, + whiteListedUsers: []string{"test-user"}, + wantResponse: admission.Allowed(fmt.Sprintf(crdAllowedFormat, "test-user", []string{"test-group"}, types.NamespacedName{Name: "memberclusters.fleet.azure.com"})), }, - "fail to validate user with invalid username, groups": { - client: &test.MockClient{ - MockList: func(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error { - return nil - }, - }, + "deny user not in system:masters group to modify fleet CRD": { + group: "fleet.azure.com", + namespacedName: types.NamespacedName{Name: "memberclusters.fleet.azure.com"}, userInfo: v1.UserInfo{ Username: "test-user", Groups: []string{"test-group"}, }, - wantResult: false, + wantResponse: admission.Denied(fmt.Sprintf(crdDeniedFormat, "test-user", []string{"test-group"}, types.NamespacedName{Name: "memberclusters.fleet.azure.com"})), }, } for testName, testCase := range testCases { t.Run(testName, func(t *testing.T) { - gotResult := ValidateUserForFleetCR(context.Background(), testCase.client, testCase.whiteListedUsers, testCase.userInfo) - assert.Equal(t, testCase.wantResult, gotResult, utils.TestCaseMsg, testName) + gotResult := ValidateUserForFleetCRD(testCase.group, testCase.namespacedName, testCase.whiteListedUsers, testCase.userInfo) + assert.Equal(t, testCase.wantResponse, gotResult, utils.TestCaseMsg, testName) }) } } diff --git a/pkg/webhook/webhook.go b/pkg/webhook/webhook.go index ee99349dc..38affa5ea 100644 --- a/pkg/webhook/webhook.go +++ b/pkg/webhook/webhook.go @@ -24,7 +24,8 @@ import ( admv1beta1 "k8s.io/api/admissionregistration/v1beta1" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" - v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + rbacv1 "k8s.io/api/rbac/v1" + apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/klog/v2" @@ -32,6 +33,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/manager" + fleetv1beta1 "go.goms.io/fleet/apis/placement/v1beta1" fleetv1alpha1 "go.goms.io/fleet/apis/v1alpha1" "go.goms.io/fleet/cmd/hubagent/options" "go.goms.io/fleet/pkg/webhook/clusterresourceplacement" @@ -50,6 +52,8 @@ const ( memberClusterResourceName = "memberclusters" replicaSetResourceName = "replicasets" podResourceName = "pods" + roleResourceName = "roles" + roleBindingResourceName = "rolebindings" ) var ( @@ -119,6 +123,22 @@ func (w *Config) createFleetWebhookConfiguration(ctx context.Context) error { namespacedScope := admv1.NamespacedScope clusterScope := admv1.ClusterScope + namespaceSelector := &metav1.LabelSelector{ + MatchExpressions: []metav1.LabelSelectorRequirement{ + { + Key: fleetv1beta1.FleetResourceLabelKey, + Operator: metav1.LabelSelectorOpIn, + Values: []string{"true"}, + }, + }, + } + + CUDOperations := []admv1.OperationType{ + admv1.Create, + admv1.Update, + admv1.Delete, + } + whCfg := admv1.ValidatingWebhookConfiguration{ ObjectMeta: metav1.ObjectMeta{ Name: FleetWebhookCfgName, @@ -129,7 +149,7 @@ func (w *Config) createFleetWebhookConfiguration(ctx context.Context) error { Webhooks: []admv1.ValidatingWebhook{ { Name: "fleet.pod.validating", - ClientConfig: w.createClientConfig(corev1.Pod{}), + ClientConfig: w.createClientConfig(pod.ValidationPath), FailurePolicy: &failPolicy, SideEffects: &sideEffortsNone, AdmissionReviewVersions: admissionReviewVersions, @@ -139,18 +159,13 @@ func (w *Config) createFleetWebhookConfiguration(ctx context.Context) error { Operations: []admv1.OperationType{ admv1.Create, }, - Rule: admv1.Rule{ - APIGroups: []string{corev1.SchemeGroupVersion.Group}, - APIVersions: []string{corev1.SchemeGroupVersion.Version}, - Resources: []string{podResourceName}, - Scope: &namespacedScope, - }, + Rule: createRule([]string{corev1.SchemeGroupVersion.Group}, []string{corev1.SchemeGroupVersion.Version}, []string{podResourceName}, &namespacedScope), }, }, }, { Name: "fleet.clusterresourceplacement.validating", - ClientConfig: w.createClientConfig(fleetv1alpha1.ClusterResourcePlacement{}), + ClientConfig: w.createClientConfig(clusterresourceplacement.ValidationPath), FailurePolicy: &failPolicy, SideEffects: &sideEffortsNone, AdmissionReviewVersions: admissionReviewVersions, @@ -161,18 +176,13 @@ func (w *Config) createFleetWebhookConfiguration(ctx context.Context) error { admv1.Create, admv1.Update, }, - Rule: admv1.Rule{ - APIGroups: []string{fleetv1alpha1.GroupVersion.Group}, - APIVersions: []string{fleetv1alpha1.GroupVersion.Version}, - Resources: []string{fleetv1alpha1.ClusterResourcePlacementResource}, - Scope: &clusterScope, - }, + Rule: createRule([]string{fleetv1alpha1.GroupVersion.Group}, []string{fleetv1alpha1.GroupVersion.Version}, []string{fleetv1alpha1.ClusterResourcePlacementResource}, &clusterScope), }, }, }, { Name: "fleet.replicaset.validating", - ClientConfig: w.createClientConfig(appsv1.ReplicaSet{}), + ClientConfig: w.createClientConfig(replicaset.ValidationPath), FailurePolicy: &failPolicy, SideEffects: &sideEffortsNone, AdmissionReviewVersions: admissionReviewVersions, @@ -181,59 +191,51 @@ func (w *Config) createFleetWebhookConfiguration(ctx context.Context) error { Operations: []admv1.OperationType{ admv1.Create, }, - Rule: admv1.Rule{ - APIGroups: []string{appsv1.SchemeGroupVersion.Group}, - APIVersions: []string{appsv1.SchemeGroupVersion.Version}, - Resources: []string{replicaSetResourceName}, - Scope: &namespacedScope, - }, + Rule: createRule([]string{appsv1.SchemeGroupVersion.Group}, []string{appsv1.SchemeGroupVersion.Version}, []string{replicaSetResourceName}, &namespacedScope), }, }, }, { Name: "fleet.customresourcedefinition.validating", - ClientConfig: w.createClientConfig(v1.CustomResourceDefinition{}), + ClientConfig: w.createClientConfig(fleetresourcehandler.ValidationPath), FailurePolicy: &failPolicy, SideEffects: &sideEffortsNone, AdmissionReviewVersions: admissionReviewVersions, Rules: []admv1.RuleWithOperations{ { - Operations: []admv1.OperationType{ - admv1.Create, - admv1.Update, - admv1.Delete, - }, - Rule: admv1.Rule{ - APIGroups: []string{v1.SchemeGroupVersion.Group}, - APIVersions: []string{v1.SchemeGroupVersion.Version}, - Resources: []string{crdResourceName}, - Scope: &clusterScope, - }, + Operations: CUDOperations, + Rule: createRule([]string{apiextensionsv1.SchemeGroupVersion.Group}, []string{apiextensionsv1.SchemeGroupVersion.Version}, []string{crdResourceName}, &clusterScope), }, }, }, { Name: "fleet.membercluster.validating", - ClientConfig: w.createClientConfig(fleetv1alpha1.MemberCluster{}), + ClientConfig: w.createClientConfig(fleetresourcehandler.ValidationPath), FailurePolicy: &failPolicy, SideEffects: &sideEffortsNone, AdmissionReviewVersions: admissionReviewVersions, Rules: []admv1.RuleWithOperations{ { - Operations: []admv1.OperationType{ - admv1.Create, - admv1.Update, - admv1.Delete, - }, - Rule: admv1.Rule{ - APIGroups: []string{fleetv1alpha1.GroupVersion.Group}, - APIVersions: []string{fleetv1alpha1.GroupVersion.Version}, - Resources: []string{memberClusterResourceName}, - Scope: &clusterScope, - }, + Operations: CUDOperations, + Rule: createRule([]string{fleetv1alpha1.GroupVersion.Group}, []string{fleetv1alpha1.GroupVersion.Version}, []string{memberClusterResourceName}, &clusterScope), }, }, }, + { + Name: "fleet.namespacedresources.validating", + ClientConfig: w.createClientConfig(fleetresourcehandler.ValidationPath), + FailurePolicy: &failPolicy, + SideEffects: &sideEffortsNone, + AdmissionReviewVersions: admissionReviewVersions, + NamespaceSelector: namespaceSelector, + Rules: []admv1.RuleWithOperations{ + { + Operations: CUDOperations, + Rule: createRule([]string{rbacv1.SchemeGroupVersion.Group}, []string{rbacv1.SchemeGroupVersion.Version}, []string{roleResourceName, roleBindingResourceName}, &namespacedScope), + }, + // TODO (Arvindthiru): Add Rules for pods, services, configmaps, secrets, deployments and replicasets + }, + }, }, } @@ -264,31 +266,14 @@ func (w *Config) createFleetWebhookConfiguration(ctx context.Context) error { } // createClientConfig generates the client configuration with either service ref or URL for the argued interface. -func (w *Config) createClientConfig(webhookInterface interface{}) admv1.WebhookClientConfig { +func (w *Config) createClientConfig(validationPath string) admv1.WebhookClientConfig { serviceRef := admv1.ServiceReference{ Namespace: w.serviceNamespace, Name: FleetWebhookSvcName, Port: pointer.Int32(w.servicePort), } - var serviceEndpoint string - switch webhookInterface.(type) { - case corev1.Pod: - serviceEndpoint = w.serviceURL + pod.ValidationPath - serviceRef.Path = pointer.String(pod.ValidationPath) - case fleetv1alpha1.ClusterResourcePlacement: - serviceEndpoint = w.serviceURL + clusterresourceplacement.ValidationPath - serviceRef.Path = pointer.String(clusterresourceplacement.ValidationPath) - case appsv1.ReplicaSet: - serviceEndpoint = w.serviceURL + replicaset.ValidationPath - serviceRef.Path = pointer.String(replicaset.ValidationPath) - case v1.CustomResourceDefinition: - serviceEndpoint = w.serviceURL + fleetresourcehandler.ValidationPath - serviceRef.Path = pointer.String(fleetresourcehandler.ValidationPath) - case fleetv1alpha1.MemberCluster: - serviceEndpoint = w.serviceURL + fleetresourcehandler.ValidationPath - serviceRef.Path = pointer.String(fleetresourcehandler.ValidationPath) - } - + serviceEndpoint := w.serviceURL + validationPath + serviceRef.Path = pointer.String(validationPath) config := admv1.WebhookClientConfig{ CABundle: w.caPEM, } @@ -474,3 +459,13 @@ func bindWebhookConfigToFleetSystem(ctx context.Context, k8Client client.Client, validatingWebhookConfig.OwnerReferences = []metav1.OwnerReference{ownerRef} return nil } + +// createRule returns a admission rule using the arguments passed. +func createRule(apiGroups, apiResources, resources []string, scopeType *admv1.ScopeType) admv1.Rule { + return admv1.Rule{ + APIGroups: apiGroups, + APIVersions: apiResources, + Resources: resources, + Scope: scopeType, + } +} diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index ce427a188..4ad923b15 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -228,13 +228,13 @@ var _ = BeforeSuite(func() { } testutils.CheckMemberClusterStatus(ctx, *HubCluster, &types.NamespacedName{Name: mc.Name}, wantMCStatus, mcStatusCmpOptions) - By("create cluster role binding and cluster roles for webhook e2e") - testutils.CreateClusterRoleAndClusterRoleBindingsForWebHookE2E(ctx, HubCluster) + By("create resources for webhook e2e") + testutils.CreateResourcesForWebHookE2E(ctx, HubCluster, memberNamespace.Name) }) var _ = AfterSuite(func() { - By("delete cluster role binding and cluster roles for webhook e2e") - testutils.DeleteClusterRoleAndClusterRoleBindingForWebHookE2E(ctx, HubCluster) + By("delete resources created for webhook e2e") + testutils.DeleteResourcesForWebHookE2E(ctx, HubCluster, memberNamespace.Name) By("update member cluster in the hub cluster") Expect(HubCluster.KubeClient.Get(ctx, types.NamespacedName{Name: mc.Name}, mc)).Should(Succeed(), "Failed to retrieve member cluster %s in %s cluster", mc.Name, HubCluster.ClusterName) diff --git a/test/e2e/utils/helper.go b/test/e2e/utils/helper.go index 1368cca63..9063572aa 100644 --- a/test/e2e/utils/helper.go +++ b/test/e2e/utils/helper.go @@ -15,7 +15,6 @@ import ( "github.com/onsi/gomega" corev1 "k8s.io/api/core/v1" rbacv1 "k8s.io/api/rbac/v1" - v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -33,10 +32,10 @@ import ( ) const ( - crdClusterRole = "CRD-cluster-role" - crdClusterRoleBinding = "CRD-role-binding" - crClusterRole = "CR-cluster-role" - crClusterRoleBinding = "CR-cluster-role-binding" + testClusterRole = "wh-test-cluster-role" + testClusterRoleBinding = "wh-test-cluster-role-binding" + testRole = "wh-test-role" + testRoleBinding = "wh-test-role-binding" ) var ( @@ -196,14 +195,15 @@ func GenerateCRDObjectFromFile(cluster framework.Cluster, fs embed.FS, filepath return obj, gvk, mapping.Resource } -func CreateClusterRoleAndClusterRoleBindingsForWebHookE2E(ctx context.Context, hubCluster *framework.Cluster) { +// CreateResourcesForWebHookE2E create resources required for Webhook E2E. +func CreateResourcesForWebHookE2E(ctx context.Context, hubCluster *framework.Cluster, memberNamespace string) { cr := rbacv1.ClusterRole{ ObjectMeta: metav1.ObjectMeta{ - Name: crdClusterRole, + Name: testClusterRole, }, Rules: []rbacv1.PolicyRule{ { - APIGroups: []string{v1.SchemeGroupVersion.Group}, + APIGroups: []string{"*"}, Verbs: []string{"*"}, Resources: []string{"*"}, }, @@ -211,11 +211,11 @@ func CreateClusterRoleAndClusterRoleBindingsForWebHookE2E(ctx context.Context, h } gomega.Eventually(func() error { return hubCluster.KubeClient.Create(ctx, &cr) - }, PollTimeout, PollInterval).Should(gomega.Succeed(), "failed to create cluster role %s to modify CRDs", cr.Name) + }, PollTimeout, PollInterval).Should(gomega.Succeed(), "failed to create cluster role %s for webhook E2E", cr.Name) crb := rbacv1.ClusterRoleBinding{ ObjectMeta: metav1.ObjectMeta{ - Name: crdClusterRoleBinding, + Name: testClusterRoleBinding, }, Subjects: []rbacv1.Subject{ { @@ -227,33 +227,35 @@ func CreateClusterRoleAndClusterRoleBindingsForWebHookE2E(ctx context.Context, h RoleRef: rbacv1.RoleRef{ APIGroup: rbacv1.GroupName, Kind: "ClusterRole", - Name: crdClusterRole, + Name: testClusterRole, }, } gomega.Eventually(func() error { return hubCluster.KubeClient.Create(ctx, &crb) - }, PollTimeout, PollInterval).Should(gomega.Succeed(), "failed to create cluster role binding %s to modify CRDs", crb.Name) + }, PollTimeout, PollInterval).Should(gomega.Succeed(), "failed to create cluster role binding %s for webhook E2E", crb.Name) - cr = rbacv1.ClusterRole{ + r := rbacv1.Role{ ObjectMeta: metav1.ObjectMeta{ - Name: crClusterRole, + Name: testRole, + Namespace: memberNamespace, }, Rules: []rbacv1.PolicyRule{ { - APIGroups: []string{fleetv1alpha1.GroupVersion.Group}, Verbs: []string{"*"}, + APIGroups: []string{"*"}, Resources: []string{"*"}, }, }, } gomega.Eventually(func() error { - return hubCluster.KubeClient.Create(ctx, &cr) - }, PollTimeout, PollInterval).Should(gomega.Succeed(), "failed to create cluster role %s to modify CRDs", cr.Name) + return hubCluster.KubeClient.Create(ctx, &r) + }, PollTimeout, PollInterval).Should(gomega.Succeed(), "failed to create role %s for webhook E2E", r.Name) - crb = rbacv1.ClusterRoleBinding{ + rb := rbacv1.RoleBinding{ ObjectMeta: metav1.ObjectMeta{ - Name: crClusterRoleBinding, + Name: testRoleBinding, + Namespace: memberNamespace, }, Subjects: []rbacv1.Subject{ { @@ -264,40 +266,43 @@ func CreateClusterRoleAndClusterRoleBindingsForWebHookE2E(ctx context.Context, h }, RoleRef: rbacv1.RoleRef{ APIGroup: rbacv1.GroupName, - Kind: "ClusterRole", - Name: crClusterRole, + Kind: "Role", + Name: testRole, }, } gomega.Eventually(func() error { - return hubCluster.KubeClient.Create(ctx, &crb) - }, PollTimeout, PollInterval).Should(gomega.Succeed(), "failed to create cluster role binding %s to modify CRDs", crb.Name) + return hubCluster.KubeClient.Create(ctx, &rb) + }, PollTimeout, PollInterval).Should(gomega.Succeed(), "failed to create role binding %s for webhook E2E", rb.Name) } -func DeleteClusterRoleAndClusterRoleBindingForWebHookE2E(ctx context.Context, hubCluster *framework.Cluster) { - crb := rbacv1.ClusterRoleBinding{ +// DeleteResourcesForWebHookE2E deletes resources created for Webhook E2E. +func DeleteResourcesForWebHookE2E(ctx context.Context, hubCluster *framework.Cluster, memberNamespace string) { + rb := rbacv1.RoleBinding{ ObjectMeta: metav1.ObjectMeta{ - Name: crClusterRoleBinding, + Name: testRoleBinding, + Namespace: memberNamespace, }, } - gomega.Expect(hubCluster.KubeClient.Delete(ctx, &crb)).Should(gomega.Succeed()) + gomega.Expect(hubCluster.KubeClient.Delete(ctx, &rb)).Should(gomega.Succeed()) - cr := rbacv1.ClusterRole{ + r := rbacv1.Role{ ObjectMeta: metav1.ObjectMeta{ - Name: crClusterRole, + Name: testRole, + Namespace: memberNamespace, }, } - gomega.Expect(hubCluster.KubeClient.Delete(ctx, &cr)).Should(gomega.Succeed()) + gomega.Expect(hubCluster.KubeClient.Delete(ctx, &r)).Should(gomega.Succeed()) - crb = rbacv1.ClusterRoleBinding{ + crb := rbacv1.ClusterRoleBinding{ ObjectMeta: metav1.ObjectMeta{ - Name: crdClusterRoleBinding, + Name: testClusterRoleBinding, }, } gomega.Expect(hubCluster.KubeClient.Delete(ctx, &crb)).Should(gomega.Succeed()) - cr = rbacv1.ClusterRole{ + cr := rbacv1.ClusterRole{ ObjectMeta: metav1.ObjectMeta{ - Name: crdClusterRole, + Name: testClusterRole, }, } gomega.Expect(hubCluster.KubeClient.Delete(ctx, &cr)).Should(gomega.Succeed()) diff --git a/test/e2e/webhook_test.go b/test/e2e/webhook_test.go index 596576779..96af93692 100644 --- a/test/e2e/webhook_test.go +++ b/test/e2e/webhook_test.go @@ -38,12 +38,14 @@ var ( ) const ( - testUser = "test-user" - testKey = "test-key" - testValue = "test-value" - - crdStatusErrFormat = `failed to validate user: %s in groups: %v to modify fleet CRD: %s` - mcStatusErrFormat = `failed to validate user: %s in groups: %v to modify member cluster CR: %s` + testUser = "test-user" + testKey = "test-key" + testValue = "test-value" + testRole = "wh-test-role" + testRoleBinding = "wh-test-role-binding" + + crdStatusErrFormat = `user: %s in groups: %v is not allowed to modify fleet CRD: %+v` + resourceStatusErrFormat = `user: %s in groups: %v is not allowed to modify fleet resource %s: %+v` ) var _ = Describe("Fleet's Hub cluster webhook tests", func() { @@ -484,7 +486,7 @@ var _ = Describe("Fleet's CRD Resource Handler webhook tests", func() { err := HubCluster.ImpersonateKubeClient.Create(ctx, &crd) var statusErr *k8sErrors.StatusError Expect(errors.As(err, &statusErr)).To(BeTrue(), fmt.Sprintf("Create CRD call produced error %s. Error type wanted is %s.", reflect.TypeOf(err), reflect.TypeOf(&k8sErrors.StatusError{}))) - Expect(string(statusErr.Status().Reason)).Should(Equal(fmt.Sprintf(crdStatusErrFormat, testUser, testGroups, crd.Name))) + Expect(string(statusErr.Status().Reason)).Should(Equal(fmt.Sprintf(crdStatusErrFormat, testUser, testGroups, types.NamespacedName{Name: crd.Name}))) }) It("should deny UPDATE operation on Fleet CRD for user not in system:masters group", func() { @@ -500,7 +502,7 @@ var _ = Describe("Fleet's CRD Resource Handler webhook tests", func() { err := HubCluster.ImpersonateKubeClient.Update(ctx, &crd) var statusErr *k8sErrors.StatusError Expect(errors.As(err, &statusErr)).To(BeTrue(), fmt.Sprintf("Update CRD call produced error %s. Error type wanted is %s.", reflect.TypeOf(err), reflect.TypeOf(&k8sErrors.StatusError{}))) - Expect(string(statusErr.Status().Reason)).Should(Equal(fmt.Sprintf(crdStatusErrFormat, testUser, testGroups, crd.Name))) + Expect(string(statusErr.Status().Reason)).Should(Equal(fmt.Sprintf(crdStatusErrFormat, testUser, testGroups, types.NamespacedName{Name: crd.Name}))) }) It("should deny DELETE operation on Fleet CRD for user not in system:masters group", func() { @@ -513,10 +515,10 @@ var _ = Describe("Fleet's CRD Resource Handler webhook tests", func() { err := HubCluster.ImpersonateKubeClient.Delete(ctx, &crd) var statusErr *k8sErrors.StatusError Expect(errors.As(err, &statusErr)).To(BeTrue(), fmt.Sprintf("Delete CRD call produced error %s. Error type wanted is %s.", reflect.TypeOf(err), reflect.TypeOf(&k8sErrors.StatusError{}))) - Expect(string(statusErr.Status().Reason)).Should(Equal(fmt.Sprintf(crdStatusErrFormat, testUser, testGroups, crd.Name))) + Expect(string(statusErr.Status().Reason)).Should(Equal(fmt.Sprintf(crdStatusErrFormat, testUser, testGroups, types.NamespacedName{Name: crd.Name}))) }) - It("should allow UPDATE operation on Fleet CRDs if user in system:masters group", func() { + It("should allow UPDATE operation on Fleet CRDs even if user in system:masters group", func() { var crd v1.CustomResourceDefinition Expect(HubCluster.KubeClient.Get(ctx, types.NamespacedName{Name: "memberclusters.fleet.azure.com"}, &crd)).Should(Succeed()) @@ -570,7 +572,7 @@ var _ = Describe("Fleet's CR Resource Handler webhook tests", func() { err := HubCluster.ImpersonateKubeClient.Create(ctx, &mc) var statusErr *k8sErrors.StatusError Expect(errors.As(err, &statusErr)).To(BeTrue(), fmt.Sprintf("Create member cluster call produced error %s. Error type wanted is %s.", reflect.TypeOf(err), reflect.TypeOf(&k8sErrors.StatusError{}))) - Expect(string(statusErr.Status().Reason)).Should(Equal(fmt.Sprintf(mcStatusErrFormat, testUser, testGroups, mc.Name))) + Expect(string(statusErr.Status().Reason)).Should(Equal(fmt.Sprintf(resourceStatusErrFormat, testUser, testGroups, "MemberCluster", types.NamespacedName{Name: mc.Name}))) }) It("should deny UPDATE operation on member cluster CR for user not in system:masters group", func() { @@ -584,7 +586,7 @@ var _ = Describe("Fleet's CR Resource Handler webhook tests", func() { err := HubCluster.ImpersonateKubeClient.Update(ctx, &mc) var statusErr *k8sErrors.StatusError Expect(errors.As(err, &statusErr)).To(BeTrue(), fmt.Sprintf("Update member cluster call produced error %s. Error type wanted is %s.", reflect.TypeOf(err), reflect.TypeOf(&k8sErrors.StatusError{}))) - Expect(string(statusErr.Status().Reason)).Should(Equal(fmt.Sprintf(mcStatusErrFormat, testUser, testGroups, mc.Name))) + Expect(string(statusErr.Status().Reason)).Should(Equal(fmt.Sprintf(resourceStatusErrFormat, testUser, testGroups, "MemberCluster", types.NamespacedName{Name: mc.Name}))) }) It("should deny DELETE operation on member cluster CR for user not in system:masters group", func() { @@ -598,19 +600,19 @@ var _ = Describe("Fleet's CR Resource Handler webhook tests", func() { err := HubCluster.ImpersonateKubeClient.Delete(ctx, &mc) var statusErr *k8sErrors.StatusError Expect(errors.As(err, &statusErr)).To(BeTrue(), fmt.Sprintf("Delete member cluster call produced error %s. Error type wanted is %s.", reflect.TypeOf(err), reflect.TypeOf(&k8sErrors.StatusError{}))) - Expect(string(statusErr.Status().Reason)).Should(Equal(fmt.Sprintf(mcStatusErrFormat, testUser, testGroups, mc.Name))) + Expect(string(statusErr.Status().Reason)).Should(Equal(fmt.Sprintf(resourceStatusErrFormat, testUser, testGroups, "MemberCluster", types.NamespacedName{Name: mc.Name}))) }) It("should allow update operation on member cluster CR for user in system:masters group", func() { var mc fleetv1alpha1.MemberCluster Expect(HubCluster.KubeClient.Get(ctx, types.NamespacedName{Name: MemberCluster.ClusterName}, &mc)).Should(Succeed()) - By("update labels in CRD") + By("update labels in member cluster") labels := make(map[string]string) labels[testKey] = testValue mc.SetLabels(labels) - By("expecting denial of operation UPDATE of CRD") + By("expecting successful UPDATE of member cluster") // The user associated with KubeClient is kubernetes-admin in groups: [system:masters, system:authenticated] Expect(HubCluster.KubeClient.Update(ctx, &mc)).To(Succeed()) @@ -618,6 +620,173 @@ var _ = Describe("Fleet's CR Resource Handler webhook tests", func() { labels = mc.GetLabels() delete(labels, testKey) mc.SetLabels(labels) + + By("expecting successful UPDATE of member cluster") + // The user associated with KubeClient is kubernetes-admin in groups: [system:masters, system:authenticated] + Expect(HubCluster.KubeClient.Update(ctx, &mc)).To(Succeed()) + }) + }) +}) + +var _ = Describe("Fleet's Namespaced Resource Handler webhook tests", func() { + Context("Role & Role binding validation webhook", func() { + It("should deny CREATE operation on role for user not in system:masters group", func() { + r := rbacv1.Role{ + ObjectMeta: metav1.ObjectMeta{ + Name: testRole, + Namespace: memberNamespace.Name, + }, + Rules: []rbacv1.PolicyRule{ + { + Verbs: []string{"*"}, + APIGroups: []string{rbacv1.SchemeGroupVersion.Group}, + Resources: []string{"*"}, + }, + }, + } + + By("expecting denial of operation CREATE of role") + err := HubCluster.ImpersonateKubeClient.Create(ctx, &r) + var statusErr *k8sErrors.StatusError + Expect(errors.As(err, &statusErr)).To(BeTrue(), fmt.Sprintf("Create role call produced error %s. Error type wanted is %s.", reflect.TypeOf(err), reflect.TypeOf(&k8sErrors.StatusError{}))) + fmt.Println(string(statusErr.Status().Reason)) + Expect(string(statusErr.Status().Reason)).Should(Equal(fmt.Sprintf(resourceStatusErrFormat, testUser, testGroups, "Role", types.NamespacedName{Name: r.Name, Namespace: r.Namespace}))) + }) + + It("should deny UPDATE operation on role for user not in system:masters group", func() { + var r rbacv1.Role + Expect(HubCluster.KubeClient.Get(ctx, types.NamespacedName{Name: testRole, Namespace: memberNamespace.Name}, &r)).Should(Succeed()) + + By("update role") + labels := make(map[string]string) + labels[testKey] = testValue + r.SetLabels(labels) + + By("expecting denial of operation UPDATE of role") + err := HubCluster.ImpersonateKubeClient.Update(ctx, &r) + var statusErr *k8sErrors.StatusError + Expect(errors.As(err, &statusErr)).To(BeTrue(), fmt.Sprintf("Update role call produced error %s. Error type wanted is %s.", reflect.TypeOf(err), reflect.TypeOf(&k8sErrors.StatusError{}))) + Expect(string(statusErr.Status().Reason)).Should(Equal(fmt.Sprintf(resourceStatusErrFormat, testUser, testGroups, "Role", types.NamespacedName{Name: r.Name, Namespace: r.Namespace}))) + }) + + It("should deny DELETE operation on role for user not in system:masters group", func() { + r := rbacv1.Role{ + ObjectMeta: metav1.ObjectMeta{ + Name: testRole, + Namespace: memberNamespace.Name, + }, + } + + By("expecting denial of operation DELETE of role") + err := HubCluster.ImpersonateKubeClient.Delete(ctx, &r) + var statusErr *k8sErrors.StatusError + Expect(errors.As(err, &statusErr)).To(BeTrue(), fmt.Sprintf("Delete role call produced error %s. Error type wanted is %s.", reflect.TypeOf(err), reflect.TypeOf(&k8sErrors.StatusError{}))) + Expect(string(statusErr.Status().Reason)).Should(Equal(fmt.Sprintf(resourceStatusErrFormat, testUser, testGroups, "Role", types.NamespacedName{Name: r.Name, Namespace: r.Namespace}))) + }) + + It("should allow update operation on role for user in system:masters group", func() { + var r rbacv1.Role + Expect(HubCluster.KubeClient.Get(ctx, types.NamespacedName{Name: testRole, Namespace: memberNamespace.Name}, &r)).Should(Succeed()) + + By("update labels in Role") + labels := make(map[string]string) + labels[testKey] = testValue + r.SetLabels(labels) + + By("expecting successful UPDATE of role") + // The user associated with KubeClient is kubernetes-admin in groups: [system:masters, system:authenticated] + Expect(HubCluster.KubeClient.Update(ctx, &r)).To(Succeed()) + + By("remove new label added for test") + labels = mc.GetLabels() + delete(labels, testKey) + mc.SetLabels(labels) + + By("expecting successful UPDATE of role") + // The user associated with KubeClient is kubernetes-admin in groups: [system:masters, system:authenticated] + Expect(HubCluster.KubeClient.Update(ctx, &r)).To(Succeed()) + }) + + It("should deny CREATE operation on role binding for user not in system:masters group", func() { + rb := rbacv1.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: testRoleBinding, + Namespace: memberNamespace.Name, + }, + Subjects: []rbacv1.Subject{ + { + APIGroup: rbacv1.GroupName, + Kind: "User", + Name: testUser, + }, + }, + RoleRef: rbacv1.RoleRef{ + APIGroup: rbacv1.GroupName, + Kind: "Role", + Name: testRole, + }, + } + + By("expecting denial of operation CREATE of role binding") + err := HubCluster.ImpersonateKubeClient.Create(ctx, &rb) + var statusErr *k8sErrors.StatusError + Expect(errors.As(err, &statusErr)).To(BeTrue(), fmt.Sprintf("Create role binding call produced error %s. Error type wanted is %s.", reflect.TypeOf(err), reflect.TypeOf(&k8sErrors.StatusError{}))) + fmt.Println(string(statusErr.Status().Reason)) + Expect(string(statusErr.Status().Reason)).Should(Equal(fmt.Sprintf(resourceStatusErrFormat, testUser, testGroups, "RoleBinding", types.NamespacedName{Name: rb.Name, Namespace: rb.Namespace}))) + }) + + It("should deny UPDATE operation on role binding for user not in system:masters group", func() { + var rb rbacv1.RoleBinding + Expect(HubCluster.KubeClient.Get(ctx, types.NamespacedName{Name: testRoleBinding, Namespace: memberNamespace.Name}, &rb)).Should(Succeed()) + + By("update role") + labels := make(map[string]string) + labels[testKey] = testValue + rb.SetLabels(labels) + + By("expecting denial of operation UPDATE of role binding") + err := HubCluster.ImpersonateKubeClient.Update(ctx, &rb) + var statusErr *k8sErrors.StatusError + Expect(errors.As(err, &statusErr)).To(BeTrue(), fmt.Sprintf("Update role binding call produced error %s. Error type wanted is %s.", reflect.TypeOf(err), reflect.TypeOf(&k8sErrors.StatusError{}))) + Expect(string(statusErr.Status().Reason)).Should(Equal(fmt.Sprintf(resourceStatusErrFormat, testUser, testGroups, "RoleBinding", types.NamespacedName{Name: rb.Name, Namespace: rb.Namespace}))) + }) + + It("should deny DELETE operation on role binding for user not in system:masters group", func() { + rb := rbacv1.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: testRoleBinding, + Namespace: memberNamespace.Name, + }, + } + + By("expecting denial of operation DELETE of role binding") + err := HubCluster.ImpersonateKubeClient.Delete(ctx, &rb) + var statusErr *k8sErrors.StatusError + Expect(errors.As(err, &statusErr)).To(BeTrue(), fmt.Sprintf("Delete role binding call produced error %s. Error type wanted is %s.", reflect.TypeOf(err), reflect.TypeOf(&k8sErrors.StatusError{}))) + Expect(string(statusErr.Status().Reason)).Should(Equal(fmt.Sprintf(resourceStatusErrFormat, testUser, testGroups, "RoleBinding", types.NamespacedName{Name: rb.Name, Namespace: rb.Namespace}))) + }) + + It("should allow update operation on role binding for user in system:masters group", func() { + var rb rbacv1.RoleBinding + Expect(HubCluster.KubeClient.Get(ctx, types.NamespacedName{Name: testRoleBinding, Namespace: memberNamespace.Name}, &rb)).Should(Succeed()) + + By("update labels in Role Binding") + labels := make(map[string]string) + labels[testKey] = testValue + rb.SetLabels(labels) + + By("expecting successful UPDATE of role binding") + // The user associated with KubeClient is kubernetes-admin in groups: [system:masters, system:authenticated] + Expect(HubCluster.KubeClient.Update(ctx, &rb)).To(Succeed()) + + By("remove new label added for test") + labels = mc.GetLabels() + delete(labels, testKey) + mc.SetLabels(labels) + + By("expecting successful UPDATE of role binding") + // The user associated with KubeClient is kubernetes-admin in groups: [system:masters, system:authenticated] + Expect(HubCluster.KubeClient.Update(ctx, &rb)).To(Succeed()) }) }) }) diff --git a/test/e2e/work_api_e2e_test.go b/test/e2e/work_api_e2e_test.go index e0677fa1c..d21448176 100644 --- a/test/e2e/work_api_e2e_test.go +++ b/test/e2e/work_api_e2e_test.go @@ -1,3 +1,8 @@ +/* +Copyright (c) Microsoft Corporation. +Licensed under the MIT license. +*/ + package e2e import (