Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 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: 4 additions & 4 deletions cmd/hubagent/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@

if opts.EnableWebhook {
whiteListedUsers := strings.Split(opts.WhiteListedUsers, ",")
if err := SetupWebhook(mgr, options.WebhookClientConnectionType(opts.WebhookClientConnectionType), opts.WebhookServiceName, whiteListedUsers, opts.EnableGuardRail, opts.EnableV1Beta1APIs); err != nil {
if err := SetupWebhook(mgr, options.WebhookClientConnectionType(opts.WebhookClientConnectionType), opts.WebhookServiceName, whiteListedUsers, opts.EnableGuardRail, opts.EnableV1Beta1APIs, opts.DenyModifyMemberClusterLabels); err != nil {

Check warning on line 174 in cmd/hubagent/main.go

View check run for this annotation

Codecov / codecov/patch

cmd/hubagent/main.go#L174

Added line #L174 was not covered by tests
klog.ErrorS(err, "unable to set up webhook")
exitWithErrorFunc()
}
Expand Down Expand Up @@ -203,9 +203,9 @@
}

// SetupWebhook generates the webhook cert and then set up the webhook configurator.
func SetupWebhook(mgr manager.Manager, webhookClientConnectionType options.WebhookClientConnectionType, webhookServiceName string, whiteListedUsers []string, enableGuardRail, isFleetV1Beta1API bool) error {
func SetupWebhook(mgr manager.Manager, webhookClientConnectionType options.WebhookClientConnectionType, webhookServiceName string, whiteListedUsers []string, enableGuardRail, isFleetV1Beta1API bool, denyModifyMemberClusterLabels bool) error {

Check warning on line 206 in cmd/hubagent/main.go

View check run for this annotation

Codecov / codecov/patch

cmd/hubagent/main.go#L206

Added line #L206 was not covered by tests
// Generate self-signed key and crt files in FleetWebhookCertDir for the webhook server to start.
w, err := webhook.NewWebhookConfig(mgr, webhookServiceName, FleetWebhookPort, &webhookClientConnectionType, FleetWebhookCertDir, enableGuardRail)
w, err := webhook.NewWebhookConfig(mgr, webhookServiceName, FleetWebhookPort, &webhookClientConnectionType, FleetWebhookCertDir, enableGuardRail, denyModifyMemberClusterLabels)

Check warning on line 208 in cmd/hubagent/main.go

View check run for this annotation

Codecov / codecov/patch

cmd/hubagent/main.go#L208

Added line #L208 was not covered by tests
if err != nil {
klog.ErrorS(err, "fail to generate WebhookConfig")
return err
Expand All @@ -214,7 +214,7 @@
klog.ErrorS(err, "unable to add WebhookConfig")
return err
}
if err = webhook.AddToManager(mgr, whiteListedUsers, isFleetV1Beta1API); err != nil {
if err = webhook.AddToManager(mgr, whiteListedUsers, isFleetV1Beta1API, denyModifyMemberClusterLabels); err != nil {

Check warning on line 217 in cmd/hubagent/main.go

View check run for this annotation

Codecov / codecov/patch

cmd/hubagent/main.go#L217

Added line #L217 was not covered by tests
klog.ErrorS(err, "unable to register webhooks to the manager")
return err
}
Expand Down
3 changes: 3 additions & 0 deletions cmd/hubagent/options/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@
EnableStagedUpdateRunAPIs bool
// EnableEvictionAPIs enables to agents to watch the eviction and placement disruption budget CRs.
EnableEvictionAPIs bool
// DenyModifyMemberClusterLabels indicates if the member cluster labels cannot be modified by groups (excluding system:masters)
DenyModifyMemberClusterLabels bool
}

// NewOptions builds an empty options.
Expand Down Expand Up @@ -158,6 +160,7 @@
flags.DurationVar(&o.ForceDeleteWaitTime.Duration, "force-delete-wait-time", 15*time.Minute, "The duration the hub agent waits before force deleting a member cluster.")
flags.BoolVar(&o.EnableStagedUpdateRunAPIs, "enable-staged-update-run-apis", true, "If set, the agents will watch for the ClusterStagedUpdateRun APIs.")
flags.BoolVar(&o.EnableEvictionAPIs, "enable-eviction-apis", true, "If set, the agents will watch for the Eviction and PlacementDisruptionBudget APIs.")
flags.BoolVar(&o.DenyModifyMemberClusterLabels, "deny-modify-member-cluster-labels", false, "If set, users not in the system:masters cannot modify member cluster labels.")

Check warning on line 163 in cmd/hubagent/options/options.go

View check run for this annotation

Codecov / codecov/patch

cmd/hubagent/options/options.go#L163

Added line #L163 was not covered by tests

o.RateLimiterOpts.AddFlags(flags)
}
22 changes: 12 additions & 10 deletions pkg/webhook/fleetresourcehandler/fleetresourcehandler_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,23 +35,25 @@
)

// Add registers the webhook for K8s built-in object types.
func Add(mgr manager.Manager, whiteListedUsers []string, isFleetV1Beta1API bool) error {
func Add(mgr manager.Manager, whiteListedUsers []string, isFleetV1Beta1API bool, denyModifyMemberClusterLabels bool) error {

Check warning on line 38 in pkg/webhook/fleetresourcehandler/fleetresourcehandler_webhook.go

View check run for this annotation

Codecov / codecov/patch

pkg/webhook/fleetresourcehandler/fleetresourcehandler_webhook.go#L38

Added line #L38 was not covered by tests
hookServer := mgr.GetWebhookServer()
handler := &fleetResourceValidator{
client: mgr.GetClient(),
whiteListedUsers: whiteListedUsers,
isFleetV1Beta1API: isFleetV1Beta1API,
decoder: admission.NewDecoder(mgr.GetScheme()),
client: mgr.GetClient(),
whiteListedUsers: whiteListedUsers,
isFleetV1Beta1API: isFleetV1Beta1API,
decoder: admission.NewDecoder(mgr.GetScheme()),
denyModifyMemberClusterLabels: denyModifyMemberClusterLabels,

Check warning on line 45 in pkg/webhook/fleetresourcehandler/fleetresourcehandler_webhook.go

View check run for this annotation

Codecov / codecov/patch

pkg/webhook/fleetresourcehandler/fleetresourcehandler_webhook.go#L41-L45

Added lines #L41 - L45 were not covered by tests
}
hookServer.Register(ValidationPath, &webhook.Admission{Handler: handler})
return nil
}

type fleetResourceValidator struct {
client client.Client
whiteListedUsers []string
isFleetV1Beta1API bool
decoder webhook.AdmissionDecoder
client client.Client
whiteListedUsers []string
isFleetV1Beta1API bool
decoder webhook.AdmissionDecoder
denyModifyMemberClusterLabels bool
}

// Handle receives the request then allows/denies the request to modify fleet resources.
Expand Down Expand Up @@ -133,7 +135,7 @@
}
isFleetMC := utils.IsFleetAnnotationPresent(oldMC.Annotations)
if isFleetMC {
return validation.ValidateFleetMemberClusterUpdate(currentMC, oldMC, req, v.whiteListedUsers)
return validation.ValidateFleetMemberClusterUpdate(currentMC, oldMC, req, v.whiteListedUsers, v.denyModifyMemberClusterLabels)
}
return validation.ValidatedUpstreamMemberClusterUpdate(currentMC, oldMC, req, v.whiteListedUsers)
}
Expand Down
183 changes: 183 additions & 0 deletions pkg/webhook/fleetresourcehandler/fleetresourcehandler_webhook_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -634,6 +634,189 @@ 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": {
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,
denyModifyMemberClusterLabels: true,
},
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": {
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,
denyModifyMemberClusterLabels: true,
},
wantResponse: admission.Denied(fmt.Sprintf(validation.DeniedModifyMemberClusterLabels)),
},
"deny label modification by fleet agent when flag is set to 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,
denyModifyMemberClusterLabels: true,
},
wantResponse: admission.Denied(fmt.Sprintf(validation.DeniedModifyMemberClusterLabels)),
},
"allow label modification by non-RP client when flag is set to 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,
denyModifyMemberClusterLabels: false,
},
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 {
Expand Down
30 changes: 21 additions & 9 deletions pkg/webhook/validation/uservalidation.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,11 @@ const (
aksSupportUser = "aks-support"
serviceAccountFmt = "system:serviceaccount:fleet-system:%s"

allowedModifyResource = "user in groups is allowed to modify resource"
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"
allowedModifyResource = "user in groups is allowed to modify resource"
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"
DeniedModifyMemberClusterLabels = "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"
Expand All @@ -62,7 +63,7 @@ func ValidateUserForFleetCRD(req admission.Request, whiteListedUsers []string, g
func ValidateUserForResource(req admission.Request, whiteListedUsers []string) admission.Response {
namespacedName := types.NamespacedName{Name: req.Name, Namespace: req.Namespace}
userInfo := req.UserInfo
if isAdminGroupUserOrWhiteListedUser(whiteListedUsers, userInfo) || isUserAuthenticatedServiceAccount(userInfo) || isUserKubeScheduler(userInfo) || isUserKubeControllerManager(userInfo) || isNodeGroupUser(userInfo) || isAKSSupportUser(userInfo) {
if isAdminGroupUserOrWhiteListedUser(whiteListedUsers, userInfo) || isUserAuthenticatedServiceAccount(userInfo) || isUserKubeScheduler(userInfo) || isUserKubeControllerManager(userInfo) || isUserInGroup(userInfo, nodeGroup) || isAKSSupportUser(userInfo) {
klog.V(3).InfoS(allowedModifyResource, "user", userInfo.Username, "groups", userInfo.Groups, "operation", req.Operation, "GVK", req.RequestKind, "subResource", req.SubResource, "namespacedName", namespacedName)
return admission.Allowed(fmt.Sprintf(ResourceAllowedFormat, userInfo.Username, utils.GenerateGroupString(userInfo.Groups), req.Operation, req.RequestKind, req.SubResource, namespacedName))
}
Expand Down Expand Up @@ -93,7 +94,7 @@ func ValidateV1Alpha1MemberClusterUpdate(currentMC, oldMC fleetv1alpha1.MemberCl
}

// ValidateFleetMemberClusterUpdate checks to see if user had updated the fleet member cluster resource and allows/denies the request.
func ValidateFleetMemberClusterUpdate(currentMC, oldMC clusterv1beta1.MemberCluster, req admission.Request, whiteListedUsers []string) admission.Response {
func ValidateFleetMemberClusterUpdate(currentMC, oldMC clusterv1beta1.MemberCluster, req admission.Request, whiteListedUsers []string, denyModifyMemberClusterLabels bool) admission.Response {
namespacedName := types.NamespacedName{Name: currentMC.GetName()}
userInfo := req.UserInfo
if areAllFleetAnnotationsRemoved(currentMC.Annotations, oldMC.Annotations) {
Expand All @@ -107,6 +108,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 denyModifyMemberClusterLabels {
isLabelUpdated := isMapFieldUpdated(currentMC.GetLabels(), oldMC.GetLabels())
if isLabelUpdated && !isUserInGroup(userInfo, mastersGroup) {
klog.V(2).InfoS(DeniedModifyMemberClusterLabels, "user", userInfo.Username, "groups", userInfo.Groups, "operation", req.Operation, "GVK", req.RequestKind, "subResource", req.SubResource, "namespacedName", namespacedName)
return admission.Denied(DeniedModifyMemberClusterLabels)
}
}

isAnnotationUpdated := isFleetAnnotationUpdated(currentMC.Annotations, oldMC.Annotations)
if isObjUpdated || isAnnotationUpdated {
return ValidateUserForResource(req, whiteListedUsers)
Expand Down Expand Up @@ -162,9 +174,9 @@ func isAKSSupportUser(userInfo authenticationv1.UserInfo) bool {
return userInfo.Username == aksSupportUser
}

// isNodeGroupUser returns true if user belongs to system:nodes group.
func isNodeGroupUser(userInfo authenticationv1.UserInfo) bool {
return slices.Contains(userInfo.Groups, nodeGroup)
// isUserInGroup returns true if user belongs to the specified groupName.
func isUserInGroup(userInfo authenticationv1.UserInfo, groupName string) bool {
return slices.Contains(userInfo.Groups, groupName)
}

// isMemberClusterMapFieldUpdated return true if member cluster label is updated.
Expand Down
Loading
Loading