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 }