Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
196 changes: 193 additions & 3 deletions pkg/webhook/fleetresourcehandler/fleetresourcehandler_webhook_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -514,11 +514,13 @@ func TestHandleMemberCluster(t *testing.T) {
assert.Nil(t, err)

testCases := map[string]struct {
req admission.Request
resourceValidator fleetResourceValidator
wantResponse admission.Response
req admission.Request
enableDenyModifiedLabelsFlag bool
resourceValidator fleetResourceValidator
wantResponse admission.Response
}{
"allow create upstream fleet MC by any user": {
enableDenyModifiedLabelsFlag: false,
req: admission.Request{
AdmissionRequest: admissionv1.AdmissionRequest{
Name: "test-mc",
Expand All @@ -539,6 +541,7 @@ func TestHandleMemberCluster(t *testing.T) {
wantResponse: admission.Allowed(allowedMessageMemberCluster),
},
"deny update of fleet MC spec by non whitelisted user": {
enableDenyModifiedLabelsFlag: false,
req: admission.Request{
AdmissionRequest: admissionv1.AdmissionRequest{
Name: "test-mc",
Expand All @@ -563,6 +566,7 @@ func TestHandleMemberCluster(t *testing.T) {
wantResponse: admission.Denied(fmt.Sprintf(validation.ResourceDeniedFormat, "test-user", utils.GenerateGroupString([]string{"test-group"}), admissionv1.Update, &utils.MCMetaGVK, "", types.NamespacedName{Name: "test-mc"})),
},
"allow whitelisted user to modify fleet MC status": {
enableDenyModifiedLabelsFlag: false,
req: admission.Request{
AdmissionRequest: admissionv1.AdmissionRequest{
Name: "test-mc",
Expand All @@ -588,6 +592,7 @@ func TestHandleMemberCluster(t *testing.T) {
wantResponse: admission.Allowed(fmt.Sprintf(validation.ResourceAllowedFormat, "test-user", utils.GenerateGroupString([]string{"test-group"}), admissionv1.Update, &utils.MCMetaGVK, "status", types.NamespacedName{Name: "test-mc"})),
},
"allow whitelisted user to modify upstream MC status": {
enableDenyModifiedLabelsFlag: false,
req: admission.Request{
AdmissionRequest: admissionv1.AdmissionRequest{
Name: "test-mc",
Expand Down Expand Up @@ -615,6 +620,7 @@ func TestHandleMemberCluster(t *testing.T) {
// added as UT since testing this case as an E2E requires
// creating a new user called aks-support in our test environment.
"allow delete for fleet MC by aks-support user": {
enableDenyModifiedLabelsFlag: false,
req: admission.Request{
AdmissionRequest: admissionv1.AdmissionRequest{
Name: "test-mc",
Expand All @@ -634,10 +640,194 @@ func TestHandleMemberCluster(t *testing.T) {
},
wantResponse: admission.Allowed(fmt.Sprintf(validation.ResourceAllowedFormat, "aks-support", utils.GenerateGroupString([]string{"system:authenticated"}), admissionv1.Delete, &utils.MCMetaGVK, "", types.NamespacedName{Name: "test-mc"})),
},

"allow label modification by RP client when flag is set to true": {
enableDenyModifiedLabelsFlag: true,
req: admission.Request{
AdmissionRequest: admissionv1.AdmissionRequest{
Name: "test-mc",
Object: runtime.RawExtension{
Raw: func() []byte {
updatedMC := &clusterv1beta1.MemberCluster{
ObjectMeta: metav1.ObjectMeta{
Name: "test-mc",
Labels: map[string]string{"key1": "value1"},
Annotations: map[string]string{
"fleet.azure.com/cluster-resource-id": "test-cluster-resource-id",
},
},
}
raw, _ := json.Marshal(updatedMC)
return raw
}(),
},
OldObject: runtime.RawExtension{
Raw: fleetMCObjectBytes,
},
UserInfo: authenticationv1.UserInfo{
Username: "aksService",
Groups: []string{"system:masters"},
},
RequestKind: &utils.MCMetaGVK,
Operation: admissionv1.Update,
},
},
resourceValidator: fleetResourceValidator{
decoder: decoder,
},
wantResponse: admission.Allowed(fmt.Sprintf(validation.ResourceAllowedFormat, "aksService", utils.GenerateGroupString([]string{"system:masters"}), admissionv1.Update, &utils.MCMetaGVK, "", types.NamespacedName{Name: "test-mc"})),
},
"deny label modification by non-RP client when flag is set to true": {
enableDenyModifiedLabelsFlag: true,
req: admission.Request{
AdmissionRequest: admissionv1.AdmissionRequest{
Name: "test-mc",
Object: runtime.RawExtension{
Raw: func() []byte {
updatedMC := &clusterv1beta1.MemberCluster{
ObjectMeta: metav1.ObjectMeta{
Name: "test-mc",
Labels: map[string]string{"key1": "value1"},
Annotations: map[string]string{
"fleet.azure.com/cluster-resource-id": "test-cluster-resource-id",
},
},
}
raw, _ := json.Marshal(updatedMC)
return raw
}(),
},
OldObject: runtime.RawExtension{
Raw: func() []byte {
oldMC := &clusterv1beta1.MemberCluster{
ObjectMeta: metav1.ObjectMeta{
Name: "test-mc",
Labels: map[string]string{"key1": "value2"},
Annotations: map[string]string{
"fleet.azure.com/cluster-resource-id": "test-cluster-resource-id",
},
},
}
raw, _ := json.Marshal(oldMC)
return raw
}(),
},
UserInfo: authenticationv1.UserInfo{
Username: "nonRPUser",
Groups: []string{"system:authenticated"},
},
RequestKind: &utils.MCMetaGVK,
Operation: admissionv1.Update,
},
},
resourceValidator: fleetResourceValidator{
decoder: decoder,
},
wantResponse: admission.Denied(fmt.Sprintf(validation.DeniedModifyFleetLabels)),
},
"deny label modification by fleet agent when flag is set to true": {
enableDenyModifiedLabelsFlag: true,
req: admission.Request{
AdmissionRequest: admissionv1.AdmissionRequest{
Name: "test-mc",
Object: runtime.RawExtension{
Raw: func() []byte {
updatedMC := &clusterv1beta1.MemberCluster{
ObjectMeta: metav1.ObjectMeta{
Name: "test-mc",
Labels: map[string]string{"key1": "value1"},
Annotations: map[string]string{
"fleet.azure.com/cluster-resource-id": "test-cluster-resource-id",
},
},
}
raw, _ := json.Marshal(updatedMC)
return raw
}(),
},
OldObject: runtime.RawExtension{
Raw: func() []byte {
oldMC := &clusterv1beta1.MemberCluster{
ObjectMeta: metav1.ObjectMeta{
Name: "test-mc",
Labels: map[string]string{"key1": "value2"},
Annotations: map[string]string{
"fleet.azure.com/cluster-resource-id": "test-cluster-resource-id",
},
},
}
raw, _ := json.Marshal(oldMC)
return raw
}(),
},
UserInfo: authenticationv1.UserInfo{
Username: "system:serviceaccount:fleet-system:hub-agent-sa",
Groups: []string{"system:serviceaccounts"},
},
RequestKind: &utils.MCMetaGVK,
Operation: admissionv1.Update,
},
},
resourceValidator: fleetResourceValidator{
decoder: decoder,
},
wantResponse: admission.Denied(fmt.Sprintf(validation.DeniedModifyFleetLabels)),
},
"allow label modification by non-RP client when flag is set to false": {
enableDenyModifiedLabelsFlag: false,
req: admission.Request{
AdmissionRequest: admissionv1.AdmissionRequest{
Name: "test-mc",
Object: runtime.RawExtension{
Raw: func() []byte {
updatedMC := &clusterv1beta1.MemberCluster{
ObjectMeta: metav1.ObjectMeta{
Name: "test-mc",
Labels: map[string]string{"key1": "value1"},
Annotations: map[string]string{
"fleet.azure.com/cluster-resource-id": "test-cluster-resource-id",
},
},
}
raw, _ := json.Marshal(updatedMC)
return raw
}(),
},
OldObject: runtime.RawExtension{
Raw: func() []byte {
oldMC := &clusterv1beta1.MemberCluster{
ObjectMeta: metav1.ObjectMeta{
Name: "test-mc",
Labels: map[string]string{"key1": "value2"},
Annotations: map[string]string{
"fleet.azure.com/cluster-resource-id": "test-cluster-resource-id",
},
},
}
raw, _ := json.Marshal(oldMC)
return raw
}(),
},
UserInfo: authenticationv1.UserInfo{
Username: "nonRPUser",
Groups: []string{"system:authenticated"},
},
RequestKind: &utils.MCMetaGVK,
Operation: admissionv1.Update,
},
},
resourceValidator: fleetResourceValidator{
decoder: decoder,
},
wantResponse: admission.Allowed(fmt.Sprintf(validation.ResourceAllowedFormat,
"nonRPUser", utils.GenerateGroupString([]string{"system:authenticated"}), admissionv1.Update, &utils.MCMetaGVK, "",
types.NamespacedName{Name: "test-mc"})),
},
}

for testName, testCase := range testCases {
t.Run(testName, func(t *testing.T) {
validation.SetDeniedModifyFleetLabelsEnabled(testCase.enableDenyModifiedLabelsFlag)
gotResult := testCase.resourceValidator.handleMemberCluster(testCase.req)
assert.Equal(t, testCase.wantResponse, gotResult, utils.TestCaseMsg, testName)
})
Expand Down
24 changes: 23 additions & 1 deletion pkg/webhook/validation/uservalidation.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,16 @@ const (
deniedModifyResource = "user in groups is not allowed to modify resource"
deniedAddFleetAnnotation = "no user is allowed to add a fleet pre-fixed annotation to an upstream member cluster"
deniedRemoveFleetAnnotation = "no user is allowed to remove all fleet pre-fixed annotations from a fleet member cluster"
DeniedModifyFleetLabels = "users are not allowed to modify labels through hub cluster directly"

ResourceAllowedFormat = "user: '%s' in '%s' is allowed to %s resource %+v/%s: %+v"
ResourceDeniedFormat = "user: '%s' in '%s' is not allowed to %s resource %+v/%s: %+v"
ResourceAllowedGetMCFailed = "user: '%s' in '%s' is allowed to %s resource %+v/%s: %+v because we failed to get MC"
)

var (
fleetCRDGroups = []string{"networking.fleet.azure.com", "fleet.azure.com", "multicluster.x-k8s.io", "cluster.kubernetes-fleet.io", "placement.kubernetes-fleet.io"}
fleetCRDGroups = []string{"networking.fleet.azure.com", "fleet.azure.com", "multicluster.x-k8s.io", "cluster.kubernetes-fleet.io", "placement.kubernetes-fleet.io"}
DeniedModifyFleetLabelsEnabled = false
)

// ValidateUserForFleetCRD checks to see if user is not allowed to modify fleet CRDs.
Expand Down Expand Up @@ -107,6 +109,17 @@ func ValidateFleetMemberClusterUpdate(currentMC, oldMC clusterv1beta1.MemberClus
if err != nil {
return admission.Denied(err.Error())
}

// users are no longer allowed to modify labels of fleet member cluster through webhook.
// this will be disabled until member labels are accessible through CLI
if DeniedModifyFleetLabelsEnabled {
isLabelUpdated := isMapFieldUpdated(currentMC.GetLabels(), oldMC.GetLabels())
if isLabelUpdated && !isRPClient(userInfo) {
klog.V(2).InfoS(DeniedModifyFleetLabels, "user", userInfo.Username, "groups", userInfo.Groups, "operation", req.Operation, "GVK", req.RequestKind, "subResource", req.SubResource, "namespacedName", namespacedName)
return admission.Denied(DeniedModifyFleetLabels)
}
}

isAnnotationUpdated := isFleetAnnotationUpdated(currentMC.Annotations, oldMC.Annotations)
if isObjUpdated || isAnnotationUpdated {
return ValidateUserForResource(req, whiteListedUsers)
Expand Down Expand Up @@ -167,6 +180,11 @@ func isNodeGroupUser(userInfo authenticationv1.UserInfo) bool {
return slices.Contains(userInfo.Groups, nodeGroup)
}

// isRPClient returns true if user is aksService and belongs to system:masters group.
func isRPClient(userInfo authenticationv1.UserInfo) bool {
return userInfo.Username == "aksService" && slices.Contains(userInfo.Groups, mastersGroup)
}

// isMemberClusterMapFieldUpdated return true if member cluster label is updated.
func isMapFieldUpdated(currentMap, oldMap map[string]string) bool {
return !reflect.DeepEqual(currentMap, oldMap)
Expand Down Expand Up @@ -281,3 +299,7 @@ func ValidateMCIdentity(ctx context.Context, client client.Client, req admission
klog.V(2).InfoS(deniedModifyResource, "user", userInfo.Username, "groups", userInfo.Groups, "operation", req.Operation, "GVK", req.RequestKind, "subResource", req.SubResource, "namespacedName", namespacedName)
return admission.Denied(fmt.Sprintf(ResourceDeniedFormat, userInfo.Username, utils.GenerateGroupString(userInfo.Groups), req.Operation, req.RequestKind, req.SubResource, namespacedName))
}

func SetDeniedModifyFleetLabelsEnabled(enabled bool) {
DeniedModifyFleetLabelsEnabled = enabled
}
Loading
Loading