tooling/templatize/pkg/aks/admin.go (141 lines of code) (raw):

// Copyright 2025 Microsoft Corporation // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package aks import ( "context" "errors" "fmt" "os" "time" "github.com/Azure/azure-sdk-for-go/sdk/azcore" "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" armauthorization "github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v3" "github.com/google/uuid" auth "github.com/microsoft/kiota-authentication-azure-go" msgraphsdk "github.com/microsoftgraph/msgraph-sdk-go" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" "k8s.io/client-go/tools/clientcmd" "github.com/Azure/ARO-HCP/tooling/templatize/pkg/azauth" ) const ( clusterAdminRoleID = "b1ff04bb-8a4e-4dc4-8eb5-8693973ce19b" // Azure Kubernetes Service RBAC Cluster Admin ) type ClusterAdminAssignmentOptions struct { Timeout time.Duration CheckFrequency time.Duration } func EnsureClusterAdmin(ctx context.Context, kubeconfigPath, subscriptionID, resourceGroupName, aksClusterName string, options *ClusterAdminAssignmentOptions) error { if options == nil { options = &ClusterAdminAssignmentOptions{ Timeout: time.Duration(2 * time.Minute), CheckFrequency: time.Duration(5 * time.Second), } } // Get the current user's object ID userObjectID, err := getCurrentUserObjectID(ctx) if err != nil { return fmt.Errorf("failed to get current user object ID: %w", err) } // Assign the Azure Kubernetes Service RBAC Cluster Admin role to the current user err = assignClusterAdminRBACRole(ctx, subscriptionID, resourceGroupName, aksClusterName, userObjectID, clusterAdminRoleID) if err != nil { return fmt.Errorf("failed to assign cluster admin role: %w", err) } // Validate assignment err = CheckClusterAdminPermissions(ctx, kubeconfigPath) if err == nil { return nil } // Wait for role assignment to be effective fmt.Println("Wait for role assignment to be effective") timeout := time.After(options.Timeout) ticker := time.NewTicker(options.CheckFrequency) defer ticker.Stop() for { select { case <-ctx.Done(): return ctx.Err() case <-timeout: return fmt.Errorf("timed out waiting for role assignment to be effective") case <-ticker.C: err = CheckClusterAdminPermissions(ctx, kubeconfigPath) if err == nil { fmt.Println("Cluster admin permissions are now effective") return nil } fmt.Println("Waiting for role assignment to be effective...") } } } func CheckClusterAdminPermissions(ctx context.Context, kubeconfigPath string) error { clientset, err := createKubeClient(kubeconfigPath) if err != nil { return fmt.Errorf("failed to create Kubernetes client: %w", err) } // Implement the logic to test cluster admin permissions // by checking if the user can list pods in the default namespace _, err = clientset.CoreV1().Pods("default").List(ctx, metav1.ListOptions{}) if err != nil { return fmt.Errorf("failed to list pods in the default namespace: %w", err) } return nil } func getCurrentUserObjectID(ctx context.Context) (string, error) { if os.Getenv("PRINCIPAL_ID") != "" { return os.Getenv("PRINCIPAL_ID"), nil } // Create a Graph client using Azure Credentials cred, err := azauth.GetAzureTokenCredentials() if err != nil { return "", fmt.Errorf("failed to obtain a credential: %w", err) } authProvider, err := auth.NewAzureIdentityAuthenticationProviderWithScopes(cred, []string{"https://graph.microsoft.com/.default"}) if err != nil { return "", err } adapter, err := msgraphsdk.NewGraphRequestAdapter(authProvider) if err != nil { return "", err } client := msgraphsdk.NewGraphServiceClient(adapter) // Get the current user user, err := client.Me().Get(ctx, nil) if err != nil { return "", fmt.Errorf("failed to get current user: %w", err) } // Extract the user ID userID := user.GetId() if userID == nil { return "", fmt.Errorf("user ID is nil") } return *userID, nil } func assignClusterAdminRBACRole(ctx context.Context, subscriptionID, resourceGroupName, aksClusterName, userObjectID, roleID string) error { // Create a new Azure identity client cred, err := azauth.GetAzureTokenCredentials() if err != nil { return fmt.Errorf("failed to obtain a credential: %w", err) } // Create a new role assignments client client, err := armauthorization.NewRoleAssignmentsClient(subscriptionID, cred, nil) if err != nil { return fmt.Errorf("failed to create role assignments client: %w", err) } aksID := fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.ContainerService/managedClusters/%s", subscriptionID, resourceGroupName, aksClusterName) roleDefinitionID := fmt.Sprintf("/subscriptions/%s/providers/Microsoft.Authorization/roleDefinitions/%s", subscriptionID, roleID) // Define the role assignment parameters parameters := armauthorization.RoleAssignmentCreateParameters{ Properties: &armauthorization.RoleAssignmentProperties{ RoleDefinitionID: to.Ptr(roleDefinitionID), PrincipalID: to.Ptr(userObjectID), }, } // Create the role assignment _, err = client.Create(ctx, aksID, uuid.New().String(), parameters, nil) if err != nil { var respErr *azcore.ResponseError if errors.As(err, &respErr) && respErr.ErrorCode == "RoleAssignmentExists" { // we could check if the roleassignment exists upfront but even when // the role exists, checking for it is not always reliably detect it // so there is no point why we should check. all our users have // permissions to create such role assignments anyways return nil } return fmt.Errorf("failed to create role assignment: %w", err) } fmt.Println("Azure Kubernetes Service RBAC Cluster Admin role assignment created successfully") return nil } func createKubeClient(kubeconfigPath string) (*kubernetes.Clientset, error) { // Load the kubeconfig file config, err := clientcmd.BuildConfigFromFlags("", kubeconfigPath) if err != nil { return nil, fmt.Errorf("failed to load kubeconfig file: %w", err) } // Create the Kubernetes client clientset, err := kubernetes.NewForConfig(config) if err != nil { return nil, fmt.Errorf("failed to create Kubernetes client: %w", err) } return clientset, nil }