Skip to content

Commit

Permalink
feat: Webhook for Role/RoleBinding (#425)
Browse files Browse the repository at this point in the history
  • Loading branch information
Arvindthiru authored Jul 14, 2023
1 parent 16e9b4d commit 36b3987
Show file tree
Hide file tree
Showing 9 changed files with 460 additions and 224 deletions.
77 changes: 61 additions & 16 deletions pkg/webhook/fleetresourcehandler/fleetresourcehandler_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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.
Expand All @@ -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 <plural>.<group>.
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.
Expand All @@ -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,
Expand All @@ -115,10 +141,29 @@ func createCRDGVK() metav1.GroupVersionKind {
}
}

// createMemberClusterGVK returns GVK for member cluster.
func createMemberClusterGVK() metav1.GroupVersionKind {
return metav1.GroupVersionKind{
Group: fleetv1alpha1.GroupVersion.Group,
Version: fleetv1alpha1.GroupVersion.Version,
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,
}
}
12 changes: 0 additions & 12 deletions pkg/webhook/validation/crdvalidation.go

This file was deleted.

72 changes: 50 additions & 22 deletions pkg/webhook/validation/uservalidation.go
Original file line number Diff line number Diff line change
@@ -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)
}
Loading

0 comments on commit 36b3987

Please sign in to comment.