Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 3 additions & 5 deletions pkg/utils/test_util.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,15 @@ import (
"os"
"strings"

"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/apimachinery/pkg/util/yaml"
"k8s.io/client-go/kubernetes/scheme"

"github.com/onsi/gomega/format"
v1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/serializer"
"k8s.io/apimachinery/pkg/util/yaml"
"k8s.io/client-go/kubernetes/scheme"
"k8s.io/client-go/tools/record"
)

Expand Down Expand Up @@ -63,7 +62,6 @@ func GetObjectFromManifest(relativeFilePath string, obj runtime.Object) error {
if err != nil {
return err
}

return GetObjectFromRawExtension(fileRaw, obj)
}

Expand Down
8 changes: 8 additions & 0 deletions pkg/webhook/add_fleetresourcehandler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package webhook

import "go.goms.io/fleet/pkg/webhook/fleetresourcehandler"

func init() {
// AddToManagerFuncs is a list of functions to create webhook and add them to a manager.
AddToManagerFuncs = append(AddToManagerFuncs, fleetresourcehandler.Add)
}
89 changes: 89 additions & 0 deletions pkg/webhook/fleetresourcehandler/fleetresourcehandler_webhook.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package fleetresourcehandler

import (
"context"
"fmt"
"net/http"
"regexp"

admissionv1 "k8s.io/api/admission/v1"
v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/klog/v2"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/manager"
"sigs.k8s.io/controller-runtime/pkg/webhook"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"

"go.goms.io/fleet/pkg/webhook/validation"
)

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"
)

// Add registers the webhook for K8s bulit-in object types.
func Add(mgr manager.Manager) error {
hookServer := mgr.GetWebhookServer()
hookServer.Register(ValidationPath, &webhook.Admission{Handler: &fleetResourceValidator{Client: mgr.GetClient()}})
return nil
}

type fleetResourceValidator struct {
Client client.Client
decoder *admission.Decoder
}

func (v *fleetResourceValidator) Handle(_ context.Context, req admission.Request) admission.Response {
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())
response = v.handleCRD(req)
default:
klog.V(2).InfoS("resource is not monitored by fleet resource validator webhook", "GVK", req.Kind.String())
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
}

func (v *fleetResourceValidator) handleCRD(req admission.Request) admission.Response {
var crd v1.CustomResourceDefinition
if req.Operation == admissionv1.Delete {
// req.Object is not populated for delete: https://github.com/kubernetes-sigs/controller-runtime/issues/1762.
if err := v.decoder.DecodeRaw(req.OldObject, &crd); err != nil {
klog.ErrorS(err, "failed to decode old request object for delete operation", "userName", req.UserInfo.Username, "groups", req.UserInfo.Groups)
return admission.Errored(http.StatusBadRequest, err)
}
} else {
if err := v.decoder.Decode(req, &crd); err != nil {
klog.ErrorS(err, "failed to decode request object for create/update operation", "userName", req.UserInfo.Username, "groups", req.UserInfo.Groups)
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(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))
}

func createCRDGVK() metav1.GroupVersionKind {
return metav1.GroupVersionKind{
Group: v1.SchemeGroupVersion.Group,
Version: v1.SchemeGroupVersion.Version,
Kind: crdKind,
}
}

func (v *fleetResourceValidator) InjectDecoder(d *admission.Decoder) error {
v.decoder = d
return nil
}
12 changes: 12 additions & 0 deletions pkg/webhook/validation/crdvalidation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package validation

import "k8s.io/utils/strings/slices"

var (
validObjectGroups = []string{"networking.fleet.azure.com", "fleet.azure.com", "multicluster.x-k8s.io", "placement.karavel.io"}
)

// CheckCRDGroup checks to see if the input CRD group is a fleet CRD group.
func CheckCRDGroup(group string) bool {
return slices.Contains(validObjectGroups, group)
}
17 changes: 17 additions & 0 deletions pkg/webhook/validation/uservalidation.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package validation

import (
authenticationv1 "k8s.io/api/authentication/v1"
"k8s.io/utils/strings/slices"
)

const (
mastersGroup = "system:masters"
)

// TODO:(Arvindthiru) Get valid usernames as flag and allow those usernames.

// ValidateUserForCRD checks to see if user is authenticated to make a request to modify fleet CRDs.
func ValidateUserForCRD(userInfo authenticationv1.UserInfo) bool {
return slices.Contains(userInfo.Groups, mastersGroup)
}
53 changes: 53 additions & 0 deletions pkg/webhook/validation/uservalidation_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package validation

import (
"context"
"testing"

"github.com/crossplane/crossplane-runtime/pkg/test"
"github.com/stretchr/testify/assert"
v1 "k8s.io/api/authentication/v1"
"sigs.k8s.io/controller-runtime/pkg/client"

"go.goms.io/fleet/pkg/utils"
)

func TestValidateUserForCRD(t *testing.T) {
testCases := map[string]struct {
client client.Client
userInfo v1.UserInfo
wantResult bool
}{
"allow user in system:masters group": {
client: &test.MockClient{
MockList: func(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error {
return nil
},
},
userInfo: v1.UserInfo{
Username: "test-user",
Groups: []string{"system:masters"},
},
wantResult: true,
},
"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
},
},
userInfo: v1.UserInfo{
Username: "test-user",
Groups: []string{"test-group"},
},
wantResult: false,
},
}

for testName, testCase := range testCases {
t.Run(testName, func(t *testing.T) {
gotResult := ValidateUserForCRD(testCase.userInfo)
assert.Equal(t, testCase.wantResult, gotResult, utils.TestCaseMsg, testName)
})
}
}
58 changes: 47 additions & 11 deletions pkg/webhook/webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@ import (
"time"

admv1 "k8s.io/api/admissionregistration/v1"
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"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/klog/v2"
Expand All @@ -33,6 +35,7 @@ import (
fleetv1alpha1 "go.goms.io/fleet/apis/v1alpha1"
"go.goms.io/fleet/cmd/hubagent/options"
"go.goms.io/fleet/pkg/webhook/clusterresourceplacement"
"go.goms.io/fleet/pkg/webhook/fleetresourcehandler"
"go.goms.io/fleet/pkg/webhook/pod"
"go.goms.io/fleet/pkg/webhook/replicaset"
)
Expand All @@ -42,6 +45,14 @@ const (
FleetWebhookKeyFileName = "tls.key"
FleetWebhookCfgName = "fleet-validating-webhook-configuration"
FleetWebhookSvcName = "fleetwebhook"

crdResourceName = "customresourcedefinitions"
replicaSetResourceName = "replicasets"
podResourceName = "pods"
)

var (
admissionReviewVersions = []string{admv1.SchemeGroupVersion.Version, admv1beta1.SchemeGroupVersion.Version}
)

var AddToManagerFuncs []func(manager.Manager) error
Expand Down Expand Up @@ -120,17 +131,17 @@ func (w *Config) createFleetWebhookConfiguration(ctx context.Context) error {
ClientConfig: w.createClientConfig(corev1.Pod{}),
FailurePolicy: &failPolicy,
SideEffects: &sideEffortsNone,
AdmissionReviewVersions: []string{"v1", "v1beta1"},
AdmissionReviewVersions: admissionReviewVersions,

Rules: []admv1.RuleWithOperations{
{
Operations: []admv1.OperationType{
admv1.Create,
},
Rule: admv1.Rule{
APIGroups: []string{""},
APIVersions: []string{"v1"},
Resources: []string{"pods"},
APIGroups: []string{corev1.SchemeGroupVersion.Group},
APIVersions: []string{corev1.SchemeGroupVersion.Version},
Resources: []string{podResourceName},
Scope: &namespacedScope,
},
},
Expand All @@ -141,7 +152,7 @@ func (w *Config) createFleetWebhookConfiguration(ctx context.Context) error {
ClientConfig: w.createClientConfig(fleetv1alpha1.ClusterResourcePlacement{}),
FailurePolicy: &failPolicy,
SideEffects: &sideEffortsNone,
AdmissionReviewVersions: []string{"v1", "v1beta1"},
AdmissionReviewVersions: admissionReviewVersions,

Rules: []admv1.RuleWithOperations{
{
Expand All @@ -150,8 +161,8 @@ func (w *Config) createFleetWebhookConfiguration(ctx context.Context) error {
admv1.Update,
},
Rule: admv1.Rule{
APIGroups: []string{"fleet.azure.com"},
APIVersions: []string{"v1alpha1"},
APIGroups: []string{fleetv1alpha1.GroupVersion.Group},
APIVersions: []string{fleetv1alpha1.GroupVersion.Version},
Resources: []string{fleetv1alpha1.ClusterResourcePlacementResource},
Scope: &clusterScope,
},
Expand All @@ -163,21 +174,43 @@ func (w *Config) createFleetWebhookConfiguration(ctx context.Context) error {
ClientConfig: w.createClientConfig(appsv1.ReplicaSet{}),
FailurePolicy: &failPolicy,
SideEffects: &sideEffortsNone,
AdmissionReviewVersions: []string{"v1", "v1beta1"},
AdmissionReviewVersions: admissionReviewVersions,
Rules: []admv1.RuleWithOperations{
{
Operations: []admv1.OperationType{
admv1.Create,
},
Rule: admv1.Rule{
APIGroups: []string{"apps"},
APIVersions: []string{"v1"},
Resources: []string{"replicasets"},
APIGroups: []string{appsv1.SchemeGroupVersion.Group},
APIVersions: []string{appsv1.SchemeGroupVersion.Version},
Resources: []string{replicaSetResourceName},
Scope: &namespacedScope,
},
},
},
},
{
Name: "fleet.customresourcedefinition.validating",
ClientConfig: w.createClientConfig(v1.CustomResourceDefinition{}),
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,
},
},
},
},
},
}

Expand Down Expand Up @@ -225,6 +258,9 @@ func (w *Config) createClientConfig(webhookInterface interface{}) admv1.WebhookC
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)
}

config := admv1.WebhookClientConfig{
Expand Down
Loading