github.com/argoproj/argo-cd/v3@v3.2.1/util/clusterauth/clusterauth_test.go (about)

     1  package clusterauth
     2  
     3  import (
     4  	"errors"
     5  	"os"
     6  	"testing"
     7  	"time"
     8  
     9  	"github.com/golang-jwt/jwt/v5"
    10  	"github.com/stretchr/testify/assert"
    11  	"github.com/stretchr/testify/require"
    12  	corev1 "k8s.io/api/core/v1"
    13  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    14  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    15  	"k8s.io/apimachinery/pkg/runtime"
    16  	"k8s.io/apimachinery/pkg/runtime/schema"
    17  	"k8s.io/client-go/kubernetes/fake"
    18  	kubetesting "k8s.io/client-go/testing"
    19  	"sigs.k8s.io/yaml"
    20  )
    21  
    22  const (
    23  	testToken              = "eyJhbGciOiJSUzI1NiIsImtpZCI6IiJ9.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJrdWJlLXN5c3RlbSIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VjcmV0Lm5hbWUiOiJhcmdvY2QtbWFuYWdlci10b2tlbi10ajc5ciIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50Lm5hbWUiOiJhcmdvY2QtbWFuYWdlciIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VydmljZS1hY2NvdW50LnVpZCI6IjkxZGQzN2NmLThkOTItMTFlOS1hMDkxLWQ2NWYyYWU3ZmE4ZCIsInN1YiI6InN5c3RlbTpzZXJ2aWNlYWNjb3VudDprdWJlLXN5c3RlbTphcmdvY2QtbWFuYWdlciJ9.ytZjt2pDV8-A7DBMR06zQ3wt9cuVEfq262TQw7sdra-KRpDpMPnziMhc8bkwvgW-LGhTWUh5iu1y-1QhEx6mtbCt7vQArlBRxfvM5ys6ClFkplzq5c2TtZ7EzGSD0Up7tdxuG9dvR6TGXYdfFcG779yCdZo2H48sz5OSJfdEriduMEY1iL5suZd3ebOoVi1fGflmqFEkZX6SvxkoArl5mtNP6TvZ1eTcn64xh4ws152hxio42E-eSnl_CET4tpB5vgP5BVlSKW2xB7w2GJxqdETA5LJRI_OilY77dTOp8cMr_Ck3EOeda3zHfh4Okflg8rZFEeAuJYahQNeAILLkcA"
    24  	testBearerTokenTimeout = 5 * time.Second
    25  )
    26  
    27  var testClaims = ServiceAccountClaims{
    28  	"kube-system",
    29  	"argocd-manager-token-tj79r",
    30  	"argocd-manager",
    31  	"91dd37cf-8d92-11e9-a091-d65f2ae7fa8d",
    32  	jwt.RegisteredClaims{
    33  		Subject: "system:serviceaccount:kube-system:argocd-manager",
    34  		Issuer:  "kubernetes/serviceaccount",
    35  	},
    36  }
    37  
    38  func newServiceAccount(t *testing.T) *corev1.ServiceAccount {
    39  	t.Helper()
    40  	saBytes, err := os.ReadFile("./testdata/argocd-manager-sa.yaml")
    41  	require.NoError(t, err)
    42  	var sa corev1.ServiceAccount
    43  	err = yaml.Unmarshal(saBytes, &sa)
    44  	require.NoError(t, err)
    45  	return &sa
    46  }
    47  
    48  func newServiceAccountSecret(t *testing.T) *corev1.Secret {
    49  	t.Helper()
    50  	secretBytes, err := os.ReadFile("./testdata/argocd-manager-sa-token.yaml")
    51  	require.NoError(t, err)
    52  	var secret corev1.Secret
    53  	err = yaml.Unmarshal(secretBytes, &secret)
    54  	require.NoError(t, err)
    55  	return &secret
    56  }
    57  
    58  func TestParseServiceAccountToken(t *testing.T) {
    59  	claims, err := ParseServiceAccountToken(testToken)
    60  	require.NoError(t, err)
    61  	assert.Equal(t, testClaims, *claims)
    62  }
    63  
    64  func TestCreateServiceAccount(t *testing.T) {
    65  	ns := &corev1.Namespace{
    66  		ObjectMeta: metav1.ObjectMeta{
    67  			Name: "kube-system",
    68  		},
    69  	}
    70  	sa := &corev1.ServiceAccount{
    71  		TypeMeta: metav1.TypeMeta{
    72  			APIVersion: "v1",
    73  			Kind:       "ServiceAccount",
    74  		},
    75  		ObjectMeta: metav1.ObjectMeta{
    76  			Name:      "argocd-manager",
    77  			Namespace: "kube-system",
    78  		},
    79  	}
    80  
    81  	t.Run("New SA", func(t *testing.T) {
    82  		cs := fake.NewClientset(ns)
    83  		err := CreateServiceAccount(cs, "argocd-manager", "kube-system")
    84  		require.NoError(t, err)
    85  		rsa, err := cs.CoreV1().ServiceAccounts("kube-system").Get(t.Context(), "argocd-manager", metav1.GetOptions{})
    86  		require.NoError(t, err)
    87  		assert.NotNil(t, rsa)
    88  	})
    89  
    90  	t.Run("SA exists already", func(t *testing.T) {
    91  		cs := fake.NewClientset(ns, sa)
    92  		err := CreateServiceAccount(cs, "argocd-manager", "kube-system")
    93  		require.NoError(t, err)
    94  		rsa, err := cs.CoreV1().ServiceAccounts("kube-system").Get(t.Context(), "argocd-manager", metav1.GetOptions{})
    95  		require.NoError(t, err)
    96  		assert.NotNil(t, rsa)
    97  	})
    98  
    99  	t.Run("Invalid namespace", func(t *testing.T) {
   100  		cs := fake.NewClientset()
   101  		err := CreateServiceAccount(cs, "argocd-manager", "invalid")
   102  		require.NoError(t, err)
   103  		rsa, err := cs.CoreV1().ServiceAccounts("invalid").Get(t.Context(), "argocd-manager", metav1.GetOptions{})
   104  		require.NoError(t, err)
   105  		assert.NotNil(t, rsa)
   106  	})
   107  }
   108  
   109  func _MockK8STokenController(objects kubetesting.ObjectTracker) kubetesting.ReactionFunc {
   110  	return (func(action kubetesting.Action) (bool, runtime.Object, error) {
   111  		secret, ok := action.(kubetesting.CreateAction).GetObject().(*corev1.Secret)
   112  		if !ok {
   113  			return false, nil, nil
   114  		}
   115  		_, err := objects.Get(schema.GroupVersionResource{Version: "v1", Resource: "serviceaccounts"},
   116  			secret.Namespace,
   117  			secret.Annotations[corev1.ServiceAccountNameKey],
   118  			metav1.GetOptions{})
   119  		if err != nil {
   120  			return false, nil, nil
   121  		}
   122  		if secret.Data == nil {
   123  			secret.Data = map[string][]byte{}
   124  		}
   125  		if secret.Data[corev1.ServiceAccountTokenKey] == nil {
   126  			secret.Data[corev1.ServiceAccountTokenKey] = []byte(testToken)
   127  		}
   128  		return false, secret, nil
   129  	})
   130  }
   131  
   132  func TestInstallClusterManagerRBAC(t *testing.T) {
   133  	ns := &corev1.Namespace{
   134  		ObjectMeta: metav1.ObjectMeta{
   135  			Name: "test",
   136  		},
   137  	}
   138  	legacyAutoSecret := &corev1.Secret{
   139  		ObjectMeta: metav1.ObjectMeta{
   140  			Name:      "sa-secret",
   141  			Namespace: "test",
   142  		},
   143  		Type: corev1.SecretTypeServiceAccountToken,
   144  		Data: map[string][]byte{
   145  			"token": []byte("foobar"),
   146  		},
   147  	}
   148  	sa := &corev1.ServiceAccount{
   149  		ObjectMeta: metav1.ObjectMeta{
   150  			Name:      ArgoCDManagerServiceAccount,
   151  			Namespace: "test",
   152  		},
   153  		Secrets: []corev1.ObjectReference{
   154  			{
   155  				Kind:            legacyAutoSecret.GetObjectKind().GroupVersionKind().Kind,
   156  				APIVersion:      legacyAutoSecret.APIVersion,
   157  				Name:            legacyAutoSecret.GetName(),
   158  				Namespace:       legacyAutoSecret.GetNamespace(),
   159  				UID:             legacyAutoSecret.GetUID(),
   160  				ResourceVersion: legacyAutoSecret.GetResourceVersion(),
   161  			},
   162  		},
   163  	}
   164  	longLivedSecret := &corev1.Secret{
   165  		ObjectMeta: metav1.ObjectMeta{
   166  			Name:      sa.Name + SATokenSecretSuffix,
   167  			Namespace: "test",
   168  			Annotations: map[string]string{
   169  				corev1.ServiceAccountNameKey: sa.Name,
   170  			},
   171  		},
   172  		Type: corev1.SecretTypeServiceAccountToken,
   173  		Data: map[string][]byte{
   174  			"token": []byte("barfoo"),
   175  		},
   176  	}
   177  
   178  	t.Run("Cluster Scope - Success", func(t *testing.T) {
   179  		cs := fake.NewClientset(ns, legacyAutoSecret, sa)
   180  		cs.PrependReactor("create", "secrets", _MockK8STokenController(cs.Tracker()))
   181  		token, err := InstallClusterManagerRBAC(cs, "test", nil, testBearerTokenTimeout)
   182  		require.NoError(t, err)
   183  		assert.Equal(t, testToken, token)
   184  	})
   185  
   186  	t.Run("Cluster Scope - Missing data in secret", func(t *testing.T) {
   187  		nsecret := legacyAutoSecret.DeepCopy()
   188  		nsecret.Data = make(map[string][]byte)
   189  		cs := fake.NewClientset(ns, nsecret, sa)
   190  		token, err := InstallClusterManagerRBAC(cs, "test", nil, testBearerTokenTimeout)
   191  		require.Error(t, err)
   192  		assert.Empty(t, token)
   193  	})
   194  
   195  	t.Run("Namespace Scope - Success", func(t *testing.T) {
   196  		cs := fake.NewClientset(ns, sa, longLivedSecret)
   197  		cs.PrependReactor("create", "secrets", _MockK8STokenController(cs.Tracker()))
   198  		token, err := InstallClusterManagerRBAC(cs, "test", []string{"nsa"}, testBearerTokenTimeout)
   199  		require.NoError(t, err)
   200  		assert.Equal(t, "barfoo", token)
   201  	})
   202  
   203  	t.Run("Namespace Scope - Missing data in secret", func(t *testing.T) {
   204  		nsecret := legacyAutoSecret.DeepCopy()
   205  		nsecret.Data = make(map[string][]byte)
   206  		cs := fake.NewClientset(ns, nsecret, sa)
   207  		token, err := InstallClusterManagerRBAC(cs, "test", []string{"nsa"}, testBearerTokenTimeout)
   208  		require.Error(t, err)
   209  		assert.Empty(t, token)
   210  	})
   211  }
   212  
   213  func TestUninstallClusterManagerRBAC(t *testing.T) {
   214  	t.Run("Success", func(t *testing.T) {
   215  		cs := fake.NewClientset(newServiceAccountSecret(t))
   216  		err := UninstallClusterManagerRBAC(cs)
   217  		require.NoError(t, err)
   218  	})
   219  }
   220  
   221  func TestGenerateNewClusterManagerSecret(t *testing.T) {
   222  	kubeclientset := fake.NewClientset(newServiceAccountSecret(t))
   223  	kubeclientset.ReactionChain = nil
   224  
   225  	generatedSecret := newServiceAccountSecret(t)
   226  	generatedSecret.Name = "argocd-manager-token-abc123"
   227  	generatedSecret.Data = map[string][]byte{
   228  		"token": []byte("fake-token"),
   229  	}
   230  
   231  	kubeclientset.AddReactor("*", "secrets", func(_ kubetesting.Action) (handled bool, ret runtime.Object, err error) {
   232  		return true, generatedSecret, nil
   233  	})
   234  
   235  	created, err := GenerateNewClusterManagerSecret(kubeclientset, &testClaims)
   236  	require.NoError(t, err)
   237  	assert.Equal(t, "argocd-manager-token-abc123", created.Name)
   238  	assert.Equal(t, "fake-token", string(created.Data["token"]))
   239  }
   240  
   241  func TestRotateServiceAccountSecrets(t *testing.T) {
   242  	generatedSecret := newServiceAccountSecret(t)
   243  	generatedSecret.Name = "argocd-manager-token-abc123"
   244  	generatedSecret.Data = map[string][]byte{
   245  		"token": []byte("fake-token"),
   246  	}
   247  
   248  	kubeclientset := fake.NewClientset(newServiceAccount(t), newServiceAccountSecret(t), generatedSecret)
   249  
   250  	err := RotateServiceAccountSecrets(kubeclientset, &testClaims, generatedSecret)
   251  	require.NoError(t, err)
   252  
   253  	// Verify service account references new secret and old secret is deleted
   254  	saClient := kubeclientset.CoreV1().ServiceAccounts(testClaims.Namespace)
   255  	sa, err := saClient.Get(t.Context(), testClaims.ServiceAccountName, metav1.GetOptions{})
   256  	require.NoError(t, err)
   257  	assert.Equal(t, []corev1.ObjectReference{
   258  		{
   259  			Name: "argocd-manager-token-abc123",
   260  		},
   261  	}, sa.Secrets)
   262  	secretsClient := kubeclientset.CoreV1().Secrets(testClaims.Namespace)
   263  	_, err = secretsClient.Get(t.Context(), testClaims.SecretName, metav1.GetOptions{})
   264  	assert.True(t, apierrors.IsNotFound(err))
   265  }
   266  
   267  func TestGetServiceAccountBearerToken(t *testing.T) {
   268  	sa := newServiceAccount(t)
   269  	tokenSecret := newServiceAccountSecret(t)
   270  	dockercfgSecret := &corev1.Secret{
   271  		ObjectMeta: metav1.ObjectMeta{
   272  			Name:      "argocd-manager-dockercfg-d8j66",
   273  			Namespace: "kube-system",
   274  		},
   275  		Type: corev1.SecretTypeDockercfg,
   276  		// Skipping data, doesn't really matter.
   277  	}
   278  	sa.Secrets = []corev1.ObjectReference{
   279  		{
   280  			Name:      dockercfgSecret.Name,
   281  			Namespace: dockercfgSecret.Namespace,
   282  		},
   283  	}
   284  	kubeclientset := fake.NewClientset(sa, dockercfgSecret, tokenSecret)
   285  
   286  	token, err := GetServiceAccountBearerToken(kubeclientset, "kube-system", sa.Name, testBearerTokenTimeout)
   287  	require.NoError(t, err)
   288  	assert.Equal(t, testToken, token)
   289  }
   290  
   291  func Test_getOrCreateServiceAccountTokenSecret_NoSecretForSA(t *testing.T) {
   292  	ns := &corev1.Namespace{
   293  		ObjectMeta: metav1.ObjectMeta{
   294  			Name: "kube-system",
   295  		},
   296  	}
   297  	sa := &corev1.ServiceAccount{
   298  		ObjectMeta: metav1.ObjectMeta{
   299  			Name:      ArgoCDManagerServiceAccount,
   300  			Namespace: ns.Name,
   301  		},
   302  	}
   303  	manualSecret := &corev1.Secret{
   304  		ObjectMeta: metav1.ObjectMeta{
   305  			Name:      ArgoCDManagerServiceAccount + SATokenSecretSuffix,
   306  			Namespace: ns.Name,
   307  			Annotations: map[string]string{
   308  				corev1.ServiceAccountNameKey: sa.Name,
   309  			},
   310  		},
   311  		Type: corev1.SecretTypeServiceAccountToken,
   312  	}
   313  
   314  	assertOnlyOneTokenExists := func(t *testing.T, cs *fake.Clientset) {
   315  		t.Helper()
   316  		got, err := getOrCreateServiceAccountTokenSecret(cs, ArgoCDManagerServiceAccount, ns.Name)
   317  		require.NoError(t, err)
   318  		assert.Equal(t, ArgoCDManagerServiceAccount+SATokenSecretSuffix, got)
   319  
   320  		list, err := cs.Tracker().List(schema.GroupVersionResource{Version: "v1", Resource: "secrets"},
   321  			schema.GroupVersionKind{Version: "v1", Kind: "Secret"}, ns.Name, metav1.ListOptions{})
   322  		require.NoError(t, err)
   323  		secretList, ok := list.(*corev1.SecretList)
   324  		require.True(t, ok)
   325  		assert.Len(t, secretList.Items, 1)
   326  		obj, err := cs.Tracker().Get(schema.GroupVersionResource{Version: "v1", Resource: "serviceaccounts"},
   327  			ns.Name, ArgoCDManagerServiceAccount)
   328  		require.NoError(t, err, "ServiceAccount %s not found but was expected to be found", ArgoCDManagerServiceAccount)
   329  
   330  		assert.Empty(t, obj.(*corev1.ServiceAccount).Secrets, 0)
   331  	}
   332  	t.Run("Token secret exists", func(t *testing.T) {
   333  		cs := fake.NewClientset(ns, sa, manualSecret)
   334  		assertOnlyOneTokenExists(t, cs)
   335  	})
   336  
   337  	t.Run("Token secret does not exist", func(t *testing.T) {
   338  		cs := fake.NewClientset(ns, sa)
   339  		assertOnlyOneTokenExists(t, cs)
   340  	})
   341  
   342  	t.Run("Error on secret creation", func(t *testing.T) {
   343  		cs := fake.NewClientset(ns, sa)
   344  		cs.PrependReactor("create", "secrets", func(kubetesting.Action) (handled bool, ret runtime.Object, err error) {
   345  			return true, &corev1.Secret{}, errors.New("testing error case")
   346  		})
   347  		got, err := getOrCreateServiceAccountTokenSecret(cs, ArgoCDManagerServiceAccount, ns.Name)
   348  		require.Error(t, err)
   349  		assert.Empty(t, got)
   350  	})
   351  }
   352  
   353  func Test_getOrCreateServiceAccountTokenSecret_SAHasSecret(t *testing.T) {
   354  	ns := &corev1.Namespace{
   355  		ObjectMeta: metav1.ObjectMeta{
   356  			Name: "kube-system",
   357  		},
   358  	}
   359  
   360  	secret := &corev1.Secret{
   361  		ObjectMeta: metav1.ObjectMeta{
   362  			Name:      "sa-secret",
   363  			Namespace: ns.Name,
   364  		},
   365  		Type: corev1.SecretTypeServiceAccountToken,
   366  		Data: map[string][]byte{
   367  			"token": []byte("foobar"),
   368  		},
   369  	}
   370  
   371  	saWithSecret := &corev1.ServiceAccount{
   372  		ObjectMeta: metav1.ObjectMeta{
   373  			Name:      ArgoCDManagerServiceAccount,
   374  			Namespace: ns.Name,
   375  		},
   376  		Secrets: []corev1.ObjectReference{
   377  			{
   378  				Kind:            secret.GetObjectKind().GroupVersionKind().Kind,
   379  				APIVersion:      secret.APIVersion,
   380  				Name:            secret.GetName(),
   381  				Namespace:       secret.GetNamespace(),
   382  				UID:             secret.GetUID(),
   383  				ResourceVersion: secret.GetResourceVersion(),
   384  			},
   385  		},
   386  	}
   387  
   388  	cs := fake.NewClientset(ns, saWithSecret, secret)
   389  
   390  	got, err := getOrCreateServiceAccountTokenSecret(cs, ArgoCDManagerServiceAccount, ns.Name)
   391  	require.NoError(t, err)
   392  	assert.Equal(t, ArgoCDManagerServiceAccount+SATokenSecretSuffix, got)
   393  
   394  	obj, err := cs.Tracker().Get(schema.GroupVersionResource{Version: "v1", Resource: "serviceaccounts"},
   395  		ns.Name, ArgoCDManagerServiceAccount)
   396  	require.NoError(t, err, "ServiceAccount %s not found but was expected to be found", ArgoCDManagerServiceAccount)
   397  
   398  	sa := obj.(*corev1.ServiceAccount)
   399  	assert.Len(t, sa.Secrets, 1)
   400  
   401  	// Adding if statement to prevent case where secret not found
   402  	// since accessing name by first index.
   403  	if len(sa.Secrets) != 0 {
   404  		assert.Equal(t, "sa-secret", sa.Secrets[0].Name)
   405  	}
   406  }