|
| 1 | +/* |
| 2 | +Copyright 2026 The Kubernetes Authors. |
| 3 | +
|
| 4 | +Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | +you may not use this file except in compliance with the License. |
| 6 | +You may obtain a copy of the License at |
| 7 | +
|
| 8 | + http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | +
|
| 10 | +Unless required by applicable law or agreed to in writing, software |
| 11 | +distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | +See the License for the specific language governing permissions and |
| 14 | +limitations under the License. |
| 15 | +*/ |
| 16 | + |
1 | 17 | package admissionhandler |
2 | 18 |
|
3 | 19 | import ( |
4 | 20 | "context" |
5 | 21 | "encoding/json" |
6 | 22 | "fmt" |
7 | 23 | "regexp" |
| 24 | + "strings" |
8 | 25 |
|
9 | 26 | admissionv1 "k8s.io/api/admission/v1" |
10 | 27 |
|
11 | 28 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" |
12 | 29 | "k8s.io/apimachinery/pkg/labels" |
13 | 30 | "sigs.k8s.io/controller-runtime/pkg/client" |
14 | 31 | cnsoperatorv1alpha1 "sigs.k8s.io/vsphere-csi-driver/v3/pkg/apis/cnsoperator" |
| 32 | + ccV1beta2 "sigs.k8s.io/cluster-api/api/core/v1beta2" |
15 | 33 |
|
16 | 34 | "k8s.io/client-go/rest" |
17 | 35 | cnsfileaccessconfigv1alpha1 "sigs.k8s.io/vsphere-csi-driver/v3/pkg/apis/cnsoperator/cnsfileaccessconfig/v1alpha1" |
@@ -209,33 +227,122 @@ func validateDeleteCnsFileAccessConfig(ctx context.Context, clientConfig *rest.C |
209 | 227 | // isUserAllowedForDeletion returns true if user is either a PVCSI service account or |
210 | 228 | // K8s' namespace-cotnroller. |
211 | 229 | func isUserAllowedForDeletion(username string) (bool, error) { |
212 | | - pvcCsiServiceAccountRegex, err := regexp.Compile(PvCsiServiceAccountregex) |
| 230 | + kubernetesServiceAccount, err := regexp.Compile(KubernetesServiceAccount) |
213 | 231 | if err != nil { |
214 | 232 | return false, err |
215 | 233 | } |
216 | 234 |
|
217 | | - kubernetesServiceAccount, err := regexp.Compile(KubernetesServiceAccount) |
| 235 | + // Check if user is a valid PVCSI service account using the new validation logic |
| 236 | + isPvCSIServiceAccount, err := validatePvCSIServiceAccount(username) |
218 | 237 | if err != nil { |
219 | 238 | return false, err |
220 | 239 | } |
| 240 | + if isPvCSIServiceAccount { |
| 241 | + return true, nil |
| 242 | + } |
221 | 243 |
|
222 | 244 | // Allowed users are : |
223 | | - // 1. PVCSI service account |
| 245 | + // 1. PVCSI service account (checked above using new validation logic) |
224 | 246 | // 2. K8s service account (like namespace-controller or generic-garbage-collector) |
225 | 247 | // 3. K8s admin |
226 | | - if pvcCsiServiceAccountRegex.MatchString(username) || |
227 | | - kubernetesServiceAccount.MatchString(username) || username == KubernetesAdmin { |
| 248 | + if kubernetesServiceAccount.MatchString(username) || username == KubernetesAdmin { |
228 | 249 | return true, nil |
229 | | - |
230 | 250 | } |
231 | 251 |
|
232 | 252 | return false, nil |
233 | 253 | } |
234 | 254 |
|
235 | 255 | func validatePvCSIServiceAccount(username string) (bool, error) { |
236 | | - pvcCsiServiceAccountRegex, err := regexp.Compile(PvCsiServiceAccountregex) |
| 256 | + // Expected format: "system:serviceaccount:namespace:service-account-name" |
| 257 | + // Parse the username to extract namespace and service account name |
| 258 | + const prefix = "system:serviceaccount:" |
| 259 | + if !strings.HasPrefix(username, prefix) { |
| 260 | + return false, nil |
| 261 | + } |
| 262 | + |
| 263 | + remaining := strings.TrimPrefix(username, prefix) |
| 264 | + parts := strings.Split(remaining, ":") |
| 265 | + if len(parts) != 2 { |
| 266 | + return false, nil |
| 267 | + } |
| 268 | + |
| 269 | + namespace := parts[0] |
| 270 | + serviceAccountName := parts[1] |
| 271 | + |
| 272 | + // Check explicit service account names first |
| 273 | + if isExplicitPvCSIServiceAccount(namespace, serviceAccountName) { |
| 274 | + return true, nil |
| 275 | + } |
| 276 | + |
| 277 | + // For vmware-system-csi namespace, construct the expected ProviderServiceAccount name |
| 278 | + // and do exact matching based on the reference implementation |
| 279 | + if namespace == "vmware-system-csi" { |
| 280 | + return validateProviderServiceAccount(serviceAccountName) |
| 281 | + } |
| 282 | + |
| 283 | + return false, nil |
| 284 | +} |
| 285 | + |
| 286 | +// isExplicitPvCSIServiceAccount checks for known explicit service account names |
| 287 | +func isExplicitPvCSIServiceAccount(namespace, serviceAccountName string) bool { |
| 288 | + switch namespace { |
| 289 | + case "vmware-system-csi": |
| 290 | + return serviceAccountName == "vsphere-csi-controller" || serviceAccountName == "vsphere-csi-node" |
| 291 | + case "kube-system": |
| 292 | + return serviceAccountName == "pvcsi" |
| 293 | + default: |
| 294 | + return false |
| 295 | + } |
| 296 | +} |
| 297 | + |
| 298 | +// validateProviderServiceAccount validates if the service account name matches |
| 299 | +// the expected ProviderServiceAccount name pattern based on the reference implementation: |
| 300 | +// https://github-vcf.devops.broadcom.net/vcf/kubernetes-service/blob/0319c5f7c9a4300b0a97296e2b3ad6283fc6bae0/addons/controllers/csi/vspherecsiconfig_controller.go#L326C3-L331C4 |
| 301 | +func validateProviderServiceAccount(serviceAccountName string) (bool, error) { |
| 302 | + // The service account name should follow the pattern: fmt.Sprintf("%s-%s", vsphereCluster.Name, "pvcsi") |
| 303 | + // Instead of using regex patterns, we need to validate against actual cluster names |
| 304 | + |
| 305 | + // Get in-cluster config to access the cluster API |
| 306 | + config, err := rest.InClusterConfig() |
| 307 | + if err != nil { |
| 308 | + // If we can't get config, fall back to basic validation |
| 309 | + return validateBasicProviderServiceAccountPattern(serviceAccountName), nil |
| 310 | + } |
| 311 | + |
| 312 | + // Create a client to access cluster API resources |
| 313 | + k8sClient, err := k8s.NewClient(context.TODO(), config) |
237 | 314 | if err != nil { |
238 | | - return false, err // fail open |
| 315 | + // Fall back to basic validation if we can't create client |
| 316 | + return validateBasicProviderServiceAccountPattern(serviceAccountName), nil |
| 317 | + } |
| 318 | + |
| 319 | + // Get all clusters in the system to validate against actual cluster names |
| 320 | + clusterList := &ccV1beta2.ClusterList{} |
| 321 | + if err := k8sClient.List(context.TODO(), clusterList); err != nil { |
| 322 | + // Fall back to basic validation if we can't list clusters |
| 323 | + return validateBasicProviderServiceAccountPattern(serviceAccountName), nil |
| 324 | + } |
| 325 | + |
| 326 | + // Check if the service account name matches any actual cluster's expected ProviderServiceAccount name |
| 327 | + for _, cluster := range clusterList.Items { |
| 328 | + expectedServiceAccountName := fmt.Sprintf("%s-%s", cluster.Name, "pvcsi") |
| 329 | + if serviceAccountName == expectedServiceAccountName { |
| 330 | + return true, nil |
| 331 | + } |
| 332 | + } |
| 333 | + |
| 334 | + // Service account name doesn't match any existing cluster's expected ProviderServiceAccount name |
| 335 | + return false, nil |
| 336 | +} |
| 337 | + |
| 338 | +// validateBasicProviderServiceAccountPattern provides fallback validation when cluster API is not available |
| 339 | +func validateBasicProviderServiceAccountPattern(serviceAccountName string) bool { |
| 340 | + // Basic validation: must end with -pvcsi, not be empty, and not contain colons (format confusion attack prevention) |
| 341 | + if !strings.HasSuffix(serviceAccountName, "-pvcsi") { |
| 342 | + return false |
239 | 343 | } |
240 | | - return pvcCsiServiceAccountRegex.MatchString(username), nil |
| 344 | + |
| 345 | + clusterName := strings.TrimSuffix(serviceAccountName, "-pvcsi") |
| 346 | + // Cluster name should not be empty and not contain colons |
| 347 | + return len(clusterName) > 0 && !strings.Contains(clusterName, ":") |
241 | 348 | } |
0 commit comments