github.com/argoproj/argo-cd@v1.8.7/util/clusterauth/clusterauth.go (about)

     1  package clusterauth
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"strings"
     7  	"time"
     8  
     9  	jwt "github.com/dgrijalva/jwt-go/v4"
    10  	log "github.com/sirupsen/logrus"
    11  	corev1 "k8s.io/api/core/v1"
    12  	rbacv1 "k8s.io/api/rbac/v1"
    13  	apierr "k8s.io/apimachinery/pkg/api/errors"
    14  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    15  	"k8s.io/apimachinery/pkg/util/wait"
    16  	"k8s.io/client-go/kubernetes"
    17  )
    18  
    19  // ArgoCDManagerServiceAccount is the name of the service account for managing a cluster
    20  const (
    21  	ArgoCDManagerServiceAccount     = "argocd-manager"
    22  	ArgoCDManagerClusterRole        = "argocd-manager-role"
    23  	ArgoCDManagerClusterRoleBinding = "argocd-manager-role-binding"
    24  )
    25  
    26  // ArgoCDManagerPolicyRules are the policies to give argocd-manager
    27  var ArgoCDManagerClusterPolicyRules = []rbacv1.PolicyRule{
    28  	{
    29  		APIGroups: []string{"*"},
    30  		Resources: []string{"*"},
    31  		Verbs:     []string{"*"},
    32  	},
    33  	{
    34  		NonResourceURLs: []string{"*"},
    35  		Verbs:           []string{"*"},
    36  	},
    37  }
    38  
    39  // ArgoCDManagerNamespacePolicyRules are the namespace level policies to give argocd-manager
    40  var ArgoCDManagerNamespacePolicyRules = []rbacv1.PolicyRule{
    41  	{
    42  		APIGroups: []string{"*"},
    43  		Resources: []string{"*"},
    44  		Verbs:     []string{"*"},
    45  	},
    46  }
    47  
    48  // CreateServiceAccount creates a service account in a given namespace
    49  func CreateServiceAccount(
    50  	clientset kubernetes.Interface,
    51  	serviceAccountName string,
    52  	namespace string,
    53  ) error {
    54  	serviceAccount := corev1.ServiceAccount{
    55  		TypeMeta: metav1.TypeMeta{
    56  			APIVersion: "v1",
    57  			Kind:       "ServiceAccount",
    58  		},
    59  		ObjectMeta: metav1.ObjectMeta{
    60  			Name:      serviceAccountName,
    61  			Namespace: namespace,
    62  		},
    63  	}
    64  	_, err := clientset.CoreV1().ServiceAccounts(namespace).Create(context.Background(), &serviceAccount, metav1.CreateOptions{})
    65  	if err != nil {
    66  		if !apierr.IsAlreadyExists(err) {
    67  			return fmt.Errorf("Failed to create service account %q in namespace %q: %v", serviceAccountName, namespace, err)
    68  		}
    69  		log.Infof("ServiceAccount %q already exists in namespace %q", serviceAccountName, namespace)
    70  		return nil
    71  	}
    72  	log.Infof("ServiceAccount %q created in namespace %q", serviceAccountName, namespace)
    73  	return nil
    74  }
    75  
    76  func upsert(kind string, name string, create func() (interface{}, error), update func() (interface{}, error)) error {
    77  	_, err := create()
    78  	if err != nil {
    79  		if !apierr.IsAlreadyExists(err) {
    80  			return fmt.Errorf("Failed to create %s %q: %v", kind, name, err)
    81  		}
    82  		_, err = update()
    83  		if err != nil {
    84  			return fmt.Errorf("Failed to update %s %q: %v", kind, name, err)
    85  		}
    86  		log.Infof("%s %q updated", kind, name)
    87  	} else {
    88  		log.Infof("%s %q created", kind, name)
    89  	}
    90  	return nil
    91  }
    92  
    93  func upsertClusterRole(clientset kubernetes.Interface, name string, rules []rbacv1.PolicyRule) error {
    94  	clusterRole := rbacv1.ClusterRole{
    95  		TypeMeta: metav1.TypeMeta{
    96  			APIVersion: "rbac.authorization.k8s.io/v1",
    97  			Kind:       "ClusterRole",
    98  		},
    99  		ObjectMeta: metav1.ObjectMeta{
   100  			Name: name,
   101  		},
   102  		Rules: rules,
   103  	}
   104  	return upsert("ClusterRole", name, func() (interface{}, error) {
   105  		return clientset.RbacV1().ClusterRoles().Create(context.Background(), &clusterRole, metav1.CreateOptions{})
   106  	}, func() (interface{}, error) {
   107  		return clientset.RbacV1().ClusterRoles().Update(context.Background(), &clusterRole, metav1.UpdateOptions{})
   108  	})
   109  }
   110  
   111  func upsertRole(clientset kubernetes.Interface, name string, namespace string, rules []rbacv1.PolicyRule) error {
   112  	role := rbacv1.Role{
   113  		TypeMeta: metav1.TypeMeta{
   114  			APIVersion: "rbac.authorization.k8s.io/v1",
   115  			Kind:       "Role",
   116  		},
   117  		ObjectMeta: metav1.ObjectMeta{
   118  			Name: name,
   119  		},
   120  		Rules: rules,
   121  	}
   122  	return upsert("Role", fmt.Sprintf("%s/%s", namespace, name), func() (interface{}, error) {
   123  		return clientset.RbacV1().Roles(namespace).Create(context.Background(), &role, metav1.CreateOptions{})
   124  	}, func() (interface{}, error) {
   125  		return clientset.RbacV1().Roles(namespace).Update(context.Background(), &role, metav1.UpdateOptions{})
   126  	})
   127  }
   128  
   129  func upsertClusterRoleBinding(clientset kubernetes.Interface, name string, clusterRoleName string, subject rbacv1.Subject) error {
   130  	roleBinding := rbacv1.ClusterRoleBinding{
   131  		TypeMeta: metav1.TypeMeta{
   132  			APIVersion: "rbac.authorization.k8s.io/v1",
   133  			Kind:       "ClusterRoleBinding",
   134  		},
   135  		ObjectMeta: metav1.ObjectMeta{
   136  			Name: name,
   137  		},
   138  		RoleRef: rbacv1.RoleRef{
   139  			APIGroup: "rbac.authorization.k8s.io",
   140  			Kind:     "ClusterRole",
   141  			Name:     clusterRoleName,
   142  		},
   143  		Subjects: []rbacv1.Subject{subject},
   144  	}
   145  	return upsert("ClusterRoleBinding", name, func() (interface{}, error) {
   146  		return clientset.RbacV1().ClusterRoleBindings().Create(context.Background(), &roleBinding, metav1.CreateOptions{})
   147  	}, func() (interface{}, error) {
   148  		return clientset.RbacV1().ClusterRoleBindings().Update(context.Background(), &roleBinding, metav1.UpdateOptions{})
   149  	})
   150  }
   151  
   152  func upsertRoleBinding(clientset kubernetes.Interface, name string, roleName string, namespace string, subject rbacv1.Subject) error {
   153  	roleBinding := rbacv1.RoleBinding{
   154  		TypeMeta: metav1.TypeMeta{
   155  			APIVersion: "rbac.authorization.k8s.io/v1",
   156  			Kind:       "RoleBinding",
   157  		},
   158  		ObjectMeta: metav1.ObjectMeta{
   159  			Name: name,
   160  		},
   161  		RoleRef: rbacv1.RoleRef{
   162  			APIGroup: "rbac.authorization.k8s.io",
   163  			Kind:     "Role",
   164  			Name:     roleName,
   165  		},
   166  		Subjects: []rbacv1.Subject{subject},
   167  	}
   168  	return upsert("RoleBinding", fmt.Sprintf("%s/%s", namespace, name), func() (interface{}, error) {
   169  		return clientset.RbacV1().RoleBindings(namespace).Create(context.Background(), &roleBinding, metav1.CreateOptions{})
   170  	}, func() (interface{}, error) {
   171  		return clientset.RbacV1().RoleBindings(namespace).Update(context.Background(), &roleBinding, metav1.UpdateOptions{})
   172  	})
   173  }
   174  
   175  // InstallClusterManagerRBAC installs RBAC resources for a cluster manager to operate a cluster. Returns a token
   176  func InstallClusterManagerRBAC(clientset kubernetes.Interface, ns string, namespaces []string) (string, error) {
   177  
   178  	err := CreateServiceAccount(clientset, ArgoCDManagerServiceAccount, ns)
   179  	if err != nil {
   180  		return "", err
   181  	}
   182  
   183  	if len(namespaces) == 0 {
   184  		err = upsertClusterRole(clientset, ArgoCDManagerClusterRole, ArgoCDManagerClusterPolicyRules)
   185  		if err != nil {
   186  			return "", err
   187  		}
   188  
   189  		err = upsertClusterRoleBinding(clientset, ArgoCDManagerClusterRoleBinding, ArgoCDManagerClusterRole, rbacv1.Subject{
   190  			Kind:      rbacv1.ServiceAccountKind,
   191  			Name:      ArgoCDManagerServiceAccount,
   192  			Namespace: ns,
   193  		})
   194  		if err != nil {
   195  			return "", err
   196  		}
   197  	} else {
   198  		for _, namespace := range namespaces {
   199  			err = upsertRole(clientset, ArgoCDManagerClusterRole, namespace, ArgoCDManagerNamespacePolicyRules)
   200  			if err != nil {
   201  				return "", err
   202  			}
   203  
   204  			err = upsertRoleBinding(clientset, ArgoCDManagerClusterRoleBinding, ArgoCDManagerClusterRole, namespace, rbacv1.Subject{
   205  				Kind:      rbacv1.ServiceAccountKind,
   206  				Name:      ArgoCDManagerServiceAccount,
   207  				Namespace: ns,
   208  			})
   209  			if err != nil {
   210  				return "", err
   211  			}
   212  		}
   213  	}
   214  
   215  	return GetServiceAccountBearerToken(clientset, ns, ArgoCDManagerServiceAccount)
   216  }
   217  
   218  // GetServiceAccountBearerToken will attempt to get the provided service account until it
   219  // exists, iterate the secrets associated with it looking for one of type
   220  // kubernetes.io/service-account-token, and return it's token if found.
   221  func GetServiceAccountBearerToken(clientset kubernetes.Interface, ns string, sa string) (string, error) {
   222  	var serviceAccount *corev1.ServiceAccount
   223  	var secret *corev1.Secret
   224  	var err error
   225  	err = wait.Poll(500*time.Millisecond, 30*time.Second, func() (bool, error) {
   226  		serviceAccount, err = clientset.CoreV1().ServiceAccounts(ns).Get(context.Background(), sa, metav1.GetOptions{})
   227  		if err != nil {
   228  			return false, err
   229  		}
   230  		// Scan all secrets looking for one of the correct type:
   231  		for _, oRef := range serviceAccount.Secrets {
   232  			var getErr error
   233  			secret, err = clientset.CoreV1().Secrets(ns).Get(context.Background(), oRef.Name, metav1.GetOptions{})
   234  			if err != nil {
   235  				return false, fmt.Errorf("Failed to retrieve secret %q: %v", oRef.Name, getErr)
   236  			}
   237  			if secret.Type == corev1.SecretTypeServiceAccountToken {
   238  				return true, nil
   239  			}
   240  		}
   241  		return false, nil
   242  	})
   243  	if err != nil {
   244  		return "", fmt.Errorf("Failed to wait for service account secret: %v", err)
   245  	}
   246  	token, ok := secret.Data["token"]
   247  	if !ok {
   248  		return "", fmt.Errorf("Secret %q for service account %q did not have a token", secret.Name, serviceAccount)
   249  	}
   250  	return string(token), nil
   251  }
   252  
   253  // UninstallClusterManagerRBAC removes RBAC resources for a cluster manager to operate a cluster
   254  func UninstallClusterManagerRBAC(clientset kubernetes.Interface) error {
   255  	return UninstallRBAC(clientset, "kube-system", ArgoCDManagerClusterRoleBinding, ArgoCDManagerClusterRole, ArgoCDManagerServiceAccount)
   256  }
   257  
   258  // UninstallRBAC uninstalls RBAC related resources  for a binding, role, and service account
   259  func UninstallRBAC(clientset kubernetes.Interface, namespace, bindingName, roleName, serviceAccount string) error {
   260  	if err := clientset.RbacV1().ClusterRoleBindings().Delete(context.Background(), bindingName, metav1.DeleteOptions{}); err != nil {
   261  		if !apierr.IsNotFound(err) {
   262  			return fmt.Errorf("Failed to delete ClusterRoleBinding: %v", err)
   263  		}
   264  		log.Infof("ClusterRoleBinding %q not found", bindingName)
   265  	} else {
   266  		log.Infof("ClusterRoleBinding %q deleted", bindingName)
   267  	}
   268  
   269  	if err := clientset.RbacV1().ClusterRoles().Delete(context.Background(), roleName, metav1.DeleteOptions{}); err != nil {
   270  		if !apierr.IsNotFound(err) {
   271  			return fmt.Errorf("Failed to delete ClusterRole: %v", err)
   272  		}
   273  		log.Infof("ClusterRole %q not found", roleName)
   274  	} else {
   275  		log.Infof("ClusterRole %q deleted", roleName)
   276  	}
   277  
   278  	if err := clientset.CoreV1().ServiceAccounts(namespace).Delete(context.Background(), serviceAccount, metav1.DeleteOptions{}); err != nil {
   279  		if !apierr.IsNotFound(err) {
   280  			return fmt.Errorf("Failed to delete ServiceAccount: %v", err)
   281  		}
   282  		log.Infof("ServiceAccount %q in namespace %q not found", serviceAccount, namespace)
   283  	} else {
   284  		log.Infof("ServiceAccount %q deleted", serviceAccount)
   285  	}
   286  	return nil
   287  }
   288  
   289  type ServiceAccountClaims struct {
   290  	Sub                string `json:"sub"`
   291  	Iss                string `json:"iss"`
   292  	Namespace          string `json:"kubernetes.io/serviceaccount/namespace"`
   293  	SecretName         string `json:"kubernetes.io/serviceaccount/secret.name"`
   294  	ServiceAccountName string `json:"kubernetes.io/serviceaccount/service-account.name"`
   295  	ServiceAccountUID  string `json:"kubernetes.io/serviceaccount/service-account.uid"`
   296  }
   297  
   298  // Valid satisfies the jwt.Claims interface to enable JWT parsing
   299  func (sac *ServiceAccountClaims) Valid(helper *jwt.ValidationHelper) error {
   300  	return nil
   301  }
   302  
   303  // ParseServiceAccountToken parses a Kubernetes service account token
   304  func ParseServiceAccountToken(token string) (*ServiceAccountClaims, error) {
   305  	parser := &jwt.Parser{
   306  		ValidationHelper: jwt.NewValidationHelper(jwt.WithoutClaimsValidation(), jwt.WithoutAudienceValidation()),
   307  	}
   308  	var claims ServiceAccountClaims
   309  	_, _, err := parser.ParseUnverified(token, &claims)
   310  	if err != nil {
   311  		return nil, fmt.Errorf("Failed to parse service account token: %s", err)
   312  	}
   313  	return &claims, nil
   314  }
   315  
   316  // GenerateNewClusterManagerSecret creates a new secret derived with same metadata as existing one
   317  // and waits until the secret is populated with a bearer token
   318  func GenerateNewClusterManagerSecret(clientset kubernetes.Interface, claims *ServiceAccountClaims) (*corev1.Secret, error) {
   319  	secretsClient := clientset.CoreV1().Secrets(claims.Namespace)
   320  	existingSecret, err := secretsClient.Get(context.Background(), claims.SecretName, metav1.GetOptions{})
   321  	if err != nil {
   322  		return nil, err
   323  	}
   324  	var newSecret corev1.Secret
   325  	secretNameSplit := strings.Split(claims.SecretName, "-")
   326  	if len(secretNameSplit) > 0 {
   327  		secretNameSplit = secretNameSplit[:len(secretNameSplit)-1]
   328  	}
   329  	newSecret.Type = corev1.SecretTypeServiceAccountToken
   330  	newSecret.GenerateName = strings.Join(secretNameSplit, "-") + "-"
   331  	newSecret.Annotations = existingSecret.Annotations
   332  	// We will create an empty secret and let kubernetes populate the data
   333  	newSecret.Data = nil
   334  
   335  	created, err := secretsClient.Create(context.Background(), &newSecret, metav1.CreateOptions{})
   336  	if err != nil {
   337  		return nil, err
   338  	}
   339  
   340  	err = wait.Poll(500*time.Millisecond, 30*time.Second, func() (bool, error) {
   341  		created, err = secretsClient.Get(context.Background(), created.Name, metav1.GetOptions{})
   342  		if err != nil {
   343  			return false, err
   344  		}
   345  		if len(created.Data) == 0 {
   346  			return false, nil
   347  		}
   348  		return true, nil
   349  	})
   350  	if err != nil {
   351  		return nil, fmt.Errorf("Timed out waiting for secret to generate new token")
   352  	}
   353  	return created, nil
   354  }
   355  
   356  // RotateServiceAccountSecrets rotates the entries in the service accounts secrets list
   357  func RotateServiceAccountSecrets(clientset kubernetes.Interface, claims *ServiceAccountClaims, newSecret *corev1.Secret) error {
   358  	// 1. update service account secrets list with new secret name while also removing the old name
   359  	saClient := clientset.CoreV1().ServiceAccounts(claims.Namespace)
   360  	sa, err := saClient.Get(context.Background(), claims.ServiceAccountName, metav1.GetOptions{})
   361  	if err != nil {
   362  		return err
   363  	}
   364  	var newSecretsList []corev1.ObjectReference
   365  	alreadyPresent := false
   366  	for _, objRef := range sa.Secrets {
   367  		if objRef.Name == claims.SecretName {
   368  			continue
   369  		}
   370  		if objRef.Name == newSecret.Name {
   371  			alreadyPresent = true
   372  		}
   373  		newSecretsList = append(newSecretsList, objRef)
   374  	}
   375  	if !alreadyPresent {
   376  		sa.Secrets = append(newSecretsList, corev1.ObjectReference{Name: newSecret.Name})
   377  	}
   378  	_, err = saClient.Update(context.Background(), sa, metav1.UpdateOptions{})
   379  	if err != nil {
   380  		return err
   381  	}
   382  
   383  	// 2. delete existing secret object
   384  	secretsClient := clientset.CoreV1().Secrets(claims.Namespace)
   385  	err = secretsClient.Delete(context.Background(), claims.SecretName, metav1.DeleteOptions{})
   386  	if !apierr.IsNotFound(err) {
   387  		return err
   388  	}
   389  	return nil
   390  }