Skip to content

Commit

Permalink
v1beta1 MC guard rail
Browse files Browse the repository at this point in the history
  • Loading branch information
Arvindthiru committed Oct 16, 2023
1 parent aa54ef3 commit efeefef
Show file tree
Hide file tree
Showing 9 changed files with 587 additions and 16 deletions.
1 change: 0 additions & 1 deletion examples/fleet_v1beta1_membercluster.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ kind: MemberCluster
metadata:
name: kind-member-testing-v1beta1
spec:
state: Join
identity:
name: member-agent-sa
kind: ServiceAccount
Expand Down
37 changes: 29 additions & 8 deletions pkg/webhook/fleetresourcehandler/fleetresourcehandler_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
workv1alpha1 "sigs.k8s.io/work-api/pkg/apis/v1alpha1"

clusterv1beta1 "go.goms.io/fleet/apis/cluster/v1beta1"
fleetv1alpha1 "go.goms.io/fleet/apis/v1alpha1"
"go.goms.io/fleet/pkg/utils"
"go.goms.io/fleet/pkg/webhook/validation"
Expand All @@ -35,12 +36,13 @@ const (
)

var (
crdGVK = metav1.GroupVersionKind{Group: v1.SchemeGroupVersion.Group, Version: v1.SchemeGroupVersion.Version, Kind: "CustomResourceDefinition"}
mcGVK = metav1.GroupVersionKind{Group: fleetv1alpha1.GroupVersion.Group, Version: fleetv1alpha1.GroupVersion.Version, Kind: "MemberCluster"}
imcGVK = metav1.GroupVersionKind{Group: fleetv1alpha1.GroupVersion.Group, Version: fleetv1alpha1.GroupVersion.Version, Kind: "InternalMemberCluster"}
namespaceGVK = metav1.GroupVersionKind{Group: corev1.SchemeGroupVersion.Group, Version: corev1.SchemeGroupVersion.Version, Kind: "Namespace"}
workGVK = metav1.GroupVersionKind{Group: workv1alpha1.GroupVersion.Group, Version: workv1alpha1.GroupVersion.Version, Kind: "Work"}
eventGVK = metav1.GroupVersionKind{Group: corev1.SchemeGroupVersion.Group, Version: corev1.SchemeGroupVersion.Version, Kind: "Event"}
crdGVK = metav1.GroupVersionKind{Group: v1.SchemeGroupVersion.Group, Version: v1.SchemeGroupVersion.Version, Kind: "CustomResourceDefinition"}
v1Alpha1MCGVK = metav1.GroupVersionKind{Group: fleetv1alpha1.GroupVersion.Group, Version: fleetv1alpha1.GroupVersion.Version, Kind: "MemberCluster"}
mcGVK = metav1.GroupVersionKind{Group: clusterv1beta1.GroupVersion.Group, Version: clusterv1beta1.GroupVersion.Version, Kind: "MemberCluster"}
imcGVK = metav1.GroupVersionKind{Group: fleetv1alpha1.GroupVersion.Group, Version: fleetv1alpha1.GroupVersion.Version, Kind: "InternalMemberCluster"}
namespaceGVK = metav1.GroupVersionKind{Group: corev1.SchemeGroupVersion.Group, Version: corev1.SchemeGroupVersion.Version, Kind: "Namespace"}
workGVK = metav1.GroupVersionKind{Group: workv1alpha1.GroupVersion.Group, Version: workv1alpha1.GroupVersion.Version, Kind: "Work"}
eventGVK = metav1.GroupVersionKind{Group: corev1.SchemeGroupVersion.Group, Version: corev1.SchemeGroupVersion.Version, Kind: "Event"}
)

// Add registers the webhook for K8s built-in object types.
Expand Down Expand Up @@ -70,6 +72,9 @@ func (v *fleetResourceValidator) Handle(ctx context.Context, req admission.Reque
case req.Kind == crdGVK:
klog.V(2).InfoS("handling CRD resource", "GVK", crdGVK, "namespacedName", namespacedName, "operation", req.Operation, "subResource", req.SubResource)
response = v.handleCRD(req)
case req.Kind == v1Alpha1MCGVK:
klog.V(2).InfoS("handling v1alpha1 member cluster resource", "GVK", v1Alpha1MCGVK, "namespacedName", namespacedName, "operation", req.Operation, "subResource", req.SubResource)
response = v.handleV1Alpha1MemberCluster(req)
case req.Kind == mcGVK:
klog.V(2).InfoS("handling member cluster resource", "GVK", mcGVK, "namespacedName", namespacedName, "operation", req.Operation, "subResource", req.SubResource)
response = v.handleMemberCluster(req)
Expand Down Expand Up @@ -107,8 +112,8 @@ func (v *fleetResourceValidator) handleCRD(req admission.Request) admission.Resp
return validation.ValidateUserForFleetCRD(req, v.whiteListedUsers, group)
}

// handleMemberCluster allows/denies the request to modify member cluster object after validation.
func (v *fleetResourceValidator) handleMemberCluster(req admission.Request) admission.Response {
// handleV1Alpha1MemberCluster allows/denies the request to modify v1alpha1 member cluster object after validation.
func (v *fleetResourceValidator) handleV1Alpha1MemberCluster(req admission.Request) admission.Response {
var currentMC fleetv1alpha1.MemberCluster
if err := v.decodeRequestObject(req, &currentMC); err != nil {
return admission.Errored(http.StatusBadRequest, err)
Expand All @@ -123,6 +128,22 @@ func (v *fleetResourceValidator) handleMemberCluster(req admission.Request) admi
return validation.ValidateUserForResource(req, v.whiteListedUsers)
}

// handleMemberCluster allows/denies the request to modify member cluster object after validation.
func (v *fleetResourceValidator) handleMemberCluster(req admission.Request) admission.Response {
var currentMC clusterv1beta1.MemberCluster
if err := v.decodeRequestObject(req, &currentMC); err != nil {
return admission.Errored(http.StatusBadRequest, err)
}
if req.Operation == admissionv1.Update {
var oldMC clusterv1beta1.MemberCluster
if err := v.decoder.DecodeRaw(req.OldObject, &oldMC); err != nil {
return admission.Errored(http.StatusBadRequest, err)
}
return validation.ValidateMemberClusterUpdate(&currentMC, &oldMC, req, v.whiteListedUsers)
}
return validation.ValidateUserForResource(req, v.whiteListedUsers)
}

// handleFleetMemberNamespacedResource allows/denies the request to modify object after validation.
func (v *fleetResourceValidator) handleFleetMemberNamespacedResource(ctx context.Context, req admission.Request) admission.Response {
var response admission.Response
Expand Down
292 changes: 290 additions & 2 deletions pkg/webhook/fleetresourcehandler/fleetresourcehandler_webhook_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"

clusterv1beta1 "go.goms.io/fleet/apis/cluster/v1beta1"
fleetv1alpha1 "go.goms.io/fleet/apis/v1alpha1"
"go.goms.io/fleet/pkg/utils"
)
Expand Down Expand Up @@ -107,7 +108,7 @@ func TestHandleCRD(t *testing.T) {
}
}

func TestHandleMemberCluster(t *testing.T) {
func TestHandleV1Alpha1MemberCluster(t *testing.T) {
MCObject := &fleetv1alpha1.MemberCluster{
TypeMeta: metav1.TypeMeta{
Kind: "MemberCluster",
Expand Down Expand Up @@ -179,6 +180,293 @@ func TestHandleMemberCluster(t *testing.T) {
decoder, err := admission.NewDecoder(scheme)
assert.Nil(t, err)

testCases := map[string]struct {
req admission.Request
resourceValidator fleetResourceValidator
wantResponse admission.Response
}{
"allow create MC for user in system:masters group": {
req: admission.Request{
AdmissionRequest: admissionv1.AdmissionRequest{
Name: "test-mc",
Object: runtime.RawExtension{
Raw: labelUpdatedMCObjectBytes,
Object: labelUpdatedMCObject,
},
UserInfo: authenticationv1.UserInfo{
Username: "test-user",
Groups: []string{"system:masters"},
},
RequestKind: &v1Alpha1MCGVK,
Operation: admissionv1.Create,
},
},
resourceValidator: fleetResourceValidator{
decoder: decoder,
},
wantResponse: admission.Allowed(fmt.Sprintf(resourceAllowedFormat, "test-user", []string{"system:masters"}, admissionv1.Create, &v1Alpha1MCGVK, "", types.NamespacedName{Name: "test-mc"})),
},
"allow any user to modify MC labels": {
req: admission.Request{
AdmissionRequest: admissionv1.AdmissionRequest{
Name: "test-mc",
Object: runtime.RawExtension{
Raw: labelUpdatedMCObjectBytes,
Object: labelUpdatedMCObject,
},
OldObject: runtime.RawExtension{
Raw: MCObjectBytes,
Object: MCObject,
},
UserInfo: authenticationv1.UserInfo{
Username: "test-user",
Groups: []string{"test-group"},
},
RequestKind: &v1Alpha1MCGVK,
Operation: admissionv1.Update,
},
},
resourceValidator: fleetResourceValidator{
decoder: decoder,
},
wantResponse: admission.Allowed(fmt.Sprintf(resourceAllowedFormat, "test-user", []string{"test-group"}, admissionv1.Update, &v1Alpha1MCGVK, "", types.NamespacedName{Name: "test-mc"})),
},
"allow any user to modify MC annotations": {
req: admission.Request{
AdmissionRequest: admissionv1.AdmissionRequest{
Name: "test-mc",
Object: runtime.RawExtension{
Raw: annotationUpdatedMCObjectBytes,
Object: annotationUpdatedMCObject,
},
OldObject: runtime.RawExtension{
Raw: MCObjectBytes,
Object: MCObject,
},
UserInfo: authenticationv1.UserInfo{
Username: "test-user",
Groups: []string{"test-group"},
},
RequestKind: &v1Alpha1MCGVK,
Operation: admissionv1.Update,
},
},
resourceValidator: fleetResourceValidator{
decoder: decoder,
},
wantResponse: admission.Allowed(fmt.Sprintf(resourceAllowedFormat, "test-user", []string{"test-group"}, admissionv1.Update, &v1Alpha1MCGVK, "", types.NamespacedName{Name: "test-mc"})),
},
"allow system:masters group user to modify MC spec": {
req: admission.Request{
AdmissionRequest: admissionv1.AdmissionRequest{
Name: "test-mc",
Object: runtime.RawExtension{
Raw: specUpdatedMCObjectBytes,
Object: specUpdatedMCObject,
},
OldObject: runtime.RawExtension{
Raw: MCObjectBytes,
Object: MCObject,
},
UserInfo: authenticationv1.UserInfo{
Username: "test-user",
Groups: []string{"system:masters"},
},
RequestKind: &v1Alpha1MCGVK,
Operation: admissionv1.Update,
},
},
resourceValidator: fleetResourceValidator{
decoder: decoder,
},
wantResponse: admission.Allowed(fmt.Sprintf(resourceAllowedFormat, "test-user", []string{"system:masters"}, admissionv1.Update, &v1Alpha1MCGVK, "", types.NamespacedName{Name: "test-mc"})),
},
"allow system:masters group user to modify MC status": {
req: admission.Request{
AdmissionRequest: admissionv1.AdmissionRequest{
Name: "test-mc",
Object: runtime.RawExtension{
Raw: statusUpdatedMCObjectBytes,
Object: statusUpdatedMCObject,
},
OldObject: runtime.RawExtension{
Raw: MCObjectBytes,
Object: MCObject,
},
UserInfo: authenticationv1.UserInfo{
Username: "test-user",
Groups: []string{"system:masters"},
},
RequestKind: &v1Alpha1MCGVK,
Operation: admissionv1.Update,
SubResource: "status",
},
},
resourceValidator: fleetResourceValidator{
decoder: decoder,
},
wantResponse: admission.Allowed(fmt.Sprintf(resourceAllowedFormat, "test-user", []string{"system:masters"}, admissionv1.Update, &v1Alpha1MCGVK, "status", types.NamespacedName{Name: "test-mc"})),
},
"allow whitelisted user to modify MC status": {
req: admission.Request{
AdmissionRequest: admissionv1.AdmissionRequest{
Name: "test-mc",
Object: runtime.RawExtension{
Raw: statusUpdatedMCObjectBytes,
Object: statusUpdatedMCObject,
},
OldObject: runtime.RawExtension{
Raw: MCObjectBytes,
Object: MCObject,
},
UserInfo: authenticationv1.UserInfo{
Username: "test-user",
Groups: []string{"test-group"},
},
RequestKind: &v1Alpha1MCGVK,
Operation: admissionv1.Update,
SubResource: "status",
},
},
resourceValidator: fleetResourceValidator{
decoder: decoder,
whiteListedUsers: []string{"test-user"},
},
wantResponse: admission.Allowed(fmt.Sprintf(resourceAllowedFormat, "test-user", []string{"test-group"}, admissionv1.Update, &v1Alpha1MCGVK, "status", types.NamespacedName{Name: "test-mc"})),
},
"deny update of member cluster spec by non system:masters group": {
req: admission.Request{
AdmissionRequest: admissionv1.AdmissionRequest{
Name: "test-mc",
Object: runtime.RawExtension{
Raw: specUpdatedMCObjectBytes,
Object: specUpdatedMCObject,
},
OldObject: runtime.RawExtension{
Raw: MCObjectBytes,
Object: MCObject,
},
UserInfo: authenticationv1.UserInfo{
Username: "test-user",
Groups: []string{"test-group"},
},
RequestKind: &v1Alpha1MCGVK,
Operation: admissionv1.Update,
},
},
resourceValidator: fleetResourceValidator{
decoder: decoder,
},
wantResponse: admission.Denied(fmt.Sprintf(resourceDeniedFormat, "test-user", []string{"test-group"}, admissionv1.Update, &v1Alpha1MCGVK, "", types.NamespacedName{Name: "test-mc"})),
},
"deny update of member cluster spec by non whitelisted user ": {
req: admission.Request{
AdmissionRequest: admissionv1.AdmissionRequest{
Name: "test-mc",
Object: runtime.RawExtension{
Raw: specUpdatedMCObjectBytes,
Object: specUpdatedMCObject,
},
OldObject: runtime.RawExtension{
Raw: MCObjectBytes,
Object: MCObject,
},
UserInfo: authenticationv1.UserInfo{
Username: "test-user",
Groups: []string{"test-group"},
},
RequestKind: &v1Alpha1MCGVK,
Operation: admissionv1.Update,
},
},
resourceValidator: fleetResourceValidator{
decoder: decoder,
whiteListedUsers: []string{"test-user1"},
},
wantResponse: admission.Denied(fmt.Sprintf(resourceDeniedFormat, "test-user", []string{"test-group"}, admissionv1.Update, &v1Alpha1MCGVK, "", types.NamespacedName{Name: "test-mc"})),
},
}

for testName, testCase := range testCases {
t.Run(testName, func(t *testing.T) {
gotResult := testCase.resourceValidator.handleV1Alpha1MemberCluster(testCase.req)
assert.Equal(t, testCase.wantResponse, gotResult, utils.TestCaseMsg, testName)
})
}
}

func TestHandleMemberCluster(t *testing.T) {
MCObject := &clusterv1beta1.MemberCluster{
TypeMeta: metav1.TypeMeta{
Kind: "MemberCluster",
},
ObjectMeta: metav1.ObjectMeta{
Name: "test-mc",
},
}
labelUpdatedMCObject := &clusterv1beta1.MemberCluster{
TypeMeta: metav1.TypeMeta{
Kind: "MemberCluster",
},
ObjectMeta: metav1.ObjectMeta{
Name: "test-mc",
Labels: map[string]string{"test-key": "test-value"},
},
}
annotationUpdatedMCObject := &clusterv1beta1.MemberCluster{
TypeMeta: metav1.TypeMeta{
Kind: "MemberCluster",
},
ObjectMeta: metav1.ObjectMeta{
Name: "test-mc",
Annotations: map[string]string{"test-key": "test-value"},
},
}
specUpdatedMCObject := &clusterv1beta1.MemberCluster{
TypeMeta: metav1.TypeMeta{
Kind: "MemberCluster",
},
ObjectMeta: metav1.ObjectMeta{
Name: "test-mc",
},
Spec: clusterv1beta1.MemberClusterSpec{
HeartbeatPeriodSeconds: 30,
},
}
statusUpdatedMCObject := &clusterv1beta1.MemberCluster{
TypeMeta: metav1.TypeMeta{
Kind: "MemberCluster",
},
ObjectMeta: metav1.ObjectMeta{
Name: "test-mc",
},
Status: clusterv1beta1.MemberClusterStatus{
Conditions: []metav1.Condition{
{
Type: string(fleetv1alpha1.ConditionTypeMemberClusterReadyToJoin),
Status: metav1.ConditionTrue,
},
},
},
}

MCObjectBytes, err := json.Marshal(MCObject)
assert.Nil(t, err)
labelUpdatedMCObjectBytes, err := json.Marshal(labelUpdatedMCObject)
assert.Nil(t, err)
annotationUpdatedMCObjectBytes, err := json.Marshal(annotationUpdatedMCObject)
assert.Nil(t, err)
specUpdatedMCObjectBytes, err := json.Marshal(specUpdatedMCObject)
assert.Nil(t, err)
statusUpdatedMCObjectBytes, err := json.Marshal(statusUpdatedMCObject)
assert.Nil(t, err)

scheme := runtime.NewScheme()
err = fleetv1alpha1.AddToScheme(scheme)
assert.Nil(t, err)
decoder, err := admission.NewDecoder(scheme)
assert.Nil(t, err)

testCases := map[string]struct {
req admission.Request
resourceValidator fleetResourceValidator
Expand Down Expand Up @@ -388,7 +676,7 @@ func TestHandleMemberCluster(t *testing.T) {

for testName, testCase := range testCases {
t.Run(testName, func(t *testing.T) {
gotResult := testCase.resourceValidator.handleMemberCluster(testCase.req)
gotResult := testCase.resourceValidator.handleV1Alpha1MemberCluster(testCase.req)
assert.Equal(t, testCase.wantResponse, gotResult, utils.TestCaseMsg, testName)
})
}
Expand Down
Loading

0 comments on commit efeefef

Please sign in to comment.