github.com/argoproj/argo-cd/v2@v2.10.9/server/server_test.go (about) 1 package server 2 3 import ( 4 "context" 5 "fmt" 6 "io" 7 "net/http" 8 "net/http/httptest" 9 "net/url" 10 "os" 11 "path/filepath" 12 "strings" 13 "testing" 14 "time" 15 16 "github.com/golang-jwt/jwt/v4" 17 log "github.com/sirupsen/logrus" 18 "github.com/stretchr/testify/assert" 19 "github.com/stretchr/testify/require" 20 "google.golang.org/grpc/metadata" 21 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 22 "k8s.io/client-go/kubernetes/fake" 23 "sigs.k8s.io/yaml" 24 25 "github.com/argoproj/argo-cd/v2/common" 26 "github.com/argoproj/argo-cd/v2/pkg/apiclient" 27 "github.com/argoproj/argo-cd/v2/pkg/apiclient/session" 28 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" 29 apps "github.com/argoproj/argo-cd/v2/pkg/client/clientset/versioned/fake" 30 "github.com/argoproj/argo-cd/v2/reposerver/apiclient/mocks" 31 servercache "github.com/argoproj/argo-cd/v2/server/cache" 32 "github.com/argoproj/argo-cd/v2/server/rbacpolicy" 33 "github.com/argoproj/argo-cd/v2/test" 34 "github.com/argoproj/argo-cd/v2/util/assets" 35 "github.com/argoproj/argo-cd/v2/util/cache" 36 cacheutil "github.com/argoproj/argo-cd/v2/util/cache" 37 appstatecache "github.com/argoproj/argo-cd/v2/util/cache/appstate" 38 "github.com/argoproj/argo-cd/v2/util/oidc" 39 "github.com/argoproj/argo-cd/v2/util/rbac" 40 settings_util "github.com/argoproj/argo-cd/v2/util/settings" 41 testutil "github.com/argoproj/argo-cd/v2/util/test" 42 ) 43 44 type FakeArgoCDServer struct { 45 *ArgoCDServer 46 TmpAssetsDir string 47 } 48 49 func fakeServer(t *testing.T) (*FakeArgoCDServer, func()) { 50 cm := test.NewFakeConfigMap() 51 secret := test.NewFakeSecret() 52 kubeclientset := fake.NewSimpleClientset(cm, secret) 53 appClientSet := apps.NewSimpleClientset() 54 redis, closer := test.NewInMemoryRedis() 55 port, err := test.GetFreePort() 56 mockRepoClient := &mocks.Clientset{RepoServerServiceClient: &mocks.RepoServerServiceClient{}} 57 tmpAssetsDir := t.TempDir() 58 59 if err != nil { 60 panic(err) 61 } 62 63 argoCDOpts := ArgoCDServerOpts{ 64 ListenPort: port, 65 Namespace: test.FakeArgoCDNamespace, 66 KubeClientset: kubeclientset, 67 AppClientset: appClientSet, 68 Insecure: true, 69 DisableAuth: true, 70 XFrameOptions: "sameorigin", 71 ContentSecurityPolicy: "frame-ancestors 'self';", 72 Cache: servercache.NewCache( 73 appstatecache.NewCache( 74 cacheutil.NewCache(cacheutil.NewInMemoryCache(1*time.Hour)), 75 1*time.Minute, 76 ), 77 1*time.Minute, 78 1*time.Minute, 79 1*time.Minute, 80 ), 81 RedisClient: redis, 82 RepoClientset: mockRepoClient, 83 StaticAssetsDir: tmpAssetsDir, 84 } 85 srv := NewServer(context.Background(), argoCDOpts) 86 fakeSrv := &FakeArgoCDServer{srv, tmpAssetsDir} 87 return fakeSrv, closer 88 } 89 90 func TestEnforceProjectToken(t *testing.T) { 91 projectName := "testProj" 92 roleName := "testRole" 93 subFormat := "proj:%s:%s" 94 policyTemplate := "p, %s, applications, get, %s/%s, %s" 95 defaultObject := "*" 96 defaultEffect := "allow" 97 defaultTestObject := fmt.Sprintf("%s/%s", projectName, "test") 98 defaultIssuedAt := int64(1) 99 defaultSub := fmt.Sprintf(subFormat, projectName, roleName) 100 defaultPolicy := fmt.Sprintf(policyTemplate, defaultSub, projectName, defaultObject, defaultEffect) 101 defaultId := "testId" 102 103 role := v1alpha1.ProjectRole{Name: roleName, Policies: []string{defaultPolicy}, JWTTokens: []v1alpha1.JWTToken{{IssuedAt: defaultIssuedAt}, {ID: defaultId}}} 104 105 jwtTokenByRole := make(map[string]v1alpha1.JWTTokens) 106 jwtTokenByRole[roleName] = v1alpha1.JWTTokens{Items: []v1alpha1.JWTToken{{IssuedAt: defaultIssuedAt}, {ID: defaultId}}} 107 108 existingProj := v1alpha1.AppProject{ 109 ObjectMeta: metav1.ObjectMeta{Name: projectName, Namespace: test.FakeArgoCDNamespace}, 110 Spec: v1alpha1.AppProjectSpec{ 111 Roles: []v1alpha1.ProjectRole{role}, 112 }, 113 Status: v1alpha1.AppProjectStatus{JWTTokensByRole: jwtTokenByRole}, 114 } 115 cm := test.NewFakeConfigMap() 116 secret := test.NewFakeSecret() 117 kubeclientset := fake.NewSimpleClientset(cm, secret) 118 mockRepoClient := &mocks.Clientset{RepoServerServiceClient: &mocks.RepoServerServiceClient{}} 119 120 t.Run("TestEnforceProjectTokenSuccessful", func(t *testing.T) { 121 s := NewServer(context.Background(), ArgoCDServerOpts{Namespace: test.FakeArgoCDNamespace, KubeClientset: kubeclientset, AppClientset: apps.NewSimpleClientset(&existingProj), RepoClientset: mockRepoClient}) 122 cancel := test.StartInformer(s.projInformer) 123 defer cancel() 124 claims := jwt.MapClaims{"sub": defaultSub, "iat": defaultIssuedAt} 125 assert.True(t, s.enf.Enforce(claims, "projects", "get", existingProj.ObjectMeta.Name)) 126 assert.True(t, s.enf.Enforce(claims, "applications", "get", defaultTestObject)) 127 }) 128 129 t.Run("TestEnforceProjectTokenWithDiffCreateAtFailure", func(t *testing.T) { 130 s := NewServer(context.Background(), ArgoCDServerOpts{Namespace: test.FakeArgoCDNamespace, KubeClientset: kubeclientset, AppClientset: apps.NewSimpleClientset(&existingProj), RepoClientset: mockRepoClient}) 131 diffCreateAt := defaultIssuedAt + 1 132 claims := jwt.MapClaims{"sub": defaultSub, "iat": diffCreateAt} 133 assert.False(t, s.enf.Enforce(claims, "applications", "get", defaultTestObject)) 134 }) 135 136 t.Run("TestEnforceProjectTokenIncorrectSubFormatFailure", func(t *testing.T) { 137 s := NewServer(context.Background(), ArgoCDServerOpts{Namespace: test.FakeArgoCDNamespace, KubeClientset: kubeclientset, AppClientset: apps.NewSimpleClientset(&existingProj), RepoClientset: mockRepoClient}) 138 invalidSub := "proj:test" 139 claims := jwt.MapClaims{"sub": invalidSub, "iat": defaultIssuedAt} 140 assert.False(t, s.enf.Enforce(claims, "applications", "get", defaultTestObject)) 141 }) 142 143 t.Run("TestEnforceProjectTokenNoTokenFailure", func(t *testing.T) { 144 s := NewServer(context.Background(), ArgoCDServerOpts{Namespace: test.FakeArgoCDNamespace, KubeClientset: kubeclientset, AppClientset: apps.NewSimpleClientset(&existingProj), RepoClientset: mockRepoClient}) 145 nonExistentToken := "fake-token" 146 invalidSub := fmt.Sprintf(subFormat, projectName, nonExistentToken) 147 claims := jwt.MapClaims{"sub": invalidSub, "iat": defaultIssuedAt} 148 assert.False(t, s.enf.Enforce(claims, "applications", "get", defaultTestObject)) 149 }) 150 151 t.Run("TestEnforceProjectTokenNotJWTTokenFailure", func(t *testing.T) { 152 proj := existingProj.DeepCopy() 153 proj.Spec.Roles[0].JWTTokens = nil 154 s := NewServer(context.Background(), ArgoCDServerOpts{Namespace: test.FakeArgoCDNamespace, KubeClientset: kubeclientset, AppClientset: apps.NewSimpleClientset(proj), RepoClientset: mockRepoClient}) 155 claims := jwt.MapClaims{"sub": defaultSub, "iat": defaultIssuedAt} 156 assert.False(t, s.enf.Enforce(claims, "applications", "get", defaultTestObject)) 157 }) 158 159 t.Run("TestEnforceProjectTokenExplicitDeny", func(t *testing.T) { 160 denyApp := "testDenyApp" 161 allowPolicy := fmt.Sprintf(policyTemplate, defaultSub, projectName, defaultObject, defaultEffect) 162 denyPolicy := fmt.Sprintf(policyTemplate, defaultSub, projectName, denyApp, "deny") 163 role := v1alpha1.ProjectRole{Name: roleName, Policies: []string{allowPolicy, denyPolicy}, JWTTokens: []v1alpha1.JWTToken{{IssuedAt: defaultIssuedAt}}} 164 proj := existingProj.DeepCopy() 165 proj.Spec.Roles[0] = role 166 167 s := NewServer(context.Background(), ArgoCDServerOpts{Namespace: test.FakeArgoCDNamespace, KubeClientset: kubeclientset, AppClientset: apps.NewSimpleClientset(proj), RepoClientset: mockRepoClient}) 168 cancel := test.StartInformer(s.projInformer) 169 defer cancel() 170 claims := jwt.MapClaims{"sub": defaultSub, "iat": defaultIssuedAt} 171 allowedObject := fmt.Sprintf("%s/%s", projectName, "test") 172 denyObject := fmt.Sprintf("%s/%s", projectName, denyApp) 173 assert.True(t, s.enf.Enforce(claims, "applications", "get", allowedObject)) 174 assert.False(t, s.enf.Enforce(claims, "applications", "get", denyObject)) 175 }) 176 177 t.Run("TestEnforceProjectTokenWithIdSuccessful", func(t *testing.T) { 178 s := NewServer(context.Background(), ArgoCDServerOpts{Namespace: test.FakeArgoCDNamespace, KubeClientset: kubeclientset, AppClientset: apps.NewSimpleClientset(&existingProj), RepoClientset: mockRepoClient}) 179 cancel := test.StartInformer(s.projInformer) 180 defer cancel() 181 claims := jwt.MapClaims{"sub": defaultSub, "jti": defaultId} 182 assert.True(t, s.enf.Enforce(claims, "projects", "get", existingProj.ObjectMeta.Name)) 183 assert.True(t, s.enf.Enforce(claims, "applications", "get", defaultTestObject)) 184 }) 185 186 t.Run("TestEnforceProjectTokenWithInvalidIdFailure", func(t *testing.T) { 187 s := NewServer(context.Background(), ArgoCDServerOpts{Namespace: test.FakeArgoCDNamespace, KubeClientset: kubeclientset, AppClientset: apps.NewSimpleClientset(&existingProj), RepoClientset: mockRepoClient}) 188 invalidId := "invalidId" 189 claims := jwt.MapClaims{"sub": defaultSub, "jti": defaultId} 190 res := s.enf.Enforce(claims, "applications", "get", invalidId) 191 assert.False(t, res) 192 }) 193 194 } 195 196 func TestEnforceClaims(t *testing.T) { 197 kubeclientset := fake.NewSimpleClientset(test.NewFakeConfigMap()) 198 enf := rbac.NewEnforcer(kubeclientset, test.FakeArgoCDNamespace, common.ArgoCDConfigMapName, nil) 199 _ = enf.SetBuiltinPolicy(assets.BuiltinPolicyCSV) 200 rbacEnf := rbacpolicy.NewRBACPolicyEnforcer(enf, test.NewFakeProjLister()) 201 enf.SetClaimsEnforcerFunc(rbacEnf.EnforceClaims) 202 policy := ` 203 g, org2:team2, role:admin 204 g, bob, role:admin 205 ` 206 _ = enf.SetUserPolicy(policy) 207 allowed := []jwt.Claims{ 208 jwt.MapClaims{"groups": []string{"org1:team1", "org2:team2"}}, 209 jwt.RegisteredClaims{Subject: "admin"}, 210 } 211 for _, c := range allowed { 212 if !assert.True(t, enf.Enforce(c, "applications", "delete", "foo/obj")) { 213 log.Errorf("%v: expected true, got false", c) 214 } 215 } 216 217 disallowed := []jwt.Claims{ 218 jwt.MapClaims{"groups": []string{"org3:team3"}}, 219 jwt.RegisteredClaims{Subject: "nobody"}, 220 } 221 for _, c := range disallowed { 222 if !assert.False(t, enf.Enforce(c, "applications", "delete", "foo/obj")) { 223 log.Errorf("%v: expected true, got false", c) 224 } 225 } 226 } 227 228 func TestDefaultRoleWithClaims(t *testing.T) { 229 kubeclientset := fake.NewSimpleClientset() 230 enf := rbac.NewEnforcer(kubeclientset, test.FakeArgoCDNamespace, common.ArgoCDConfigMapName, nil) 231 _ = enf.SetBuiltinPolicy(assets.BuiltinPolicyCSV) 232 rbacEnf := rbacpolicy.NewRBACPolicyEnforcer(enf, test.NewFakeProjLister()) 233 enf.SetClaimsEnforcerFunc(rbacEnf.EnforceClaims) 234 claims := jwt.MapClaims{"groups": []string{"org1:team1", "org2:team2"}} 235 236 assert.False(t, enf.Enforce(claims, "applications", "get", "foo/bar")) 237 // after setting the default role to be the read-only role, this should now pass 238 enf.SetDefaultRole("role:readonly") 239 assert.True(t, enf.Enforce(claims, "applications", "get", "foo/bar")) 240 } 241 242 func TestEnforceNilClaims(t *testing.T) { 243 kubeclientset := fake.NewSimpleClientset(test.NewFakeConfigMap()) 244 enf := rbac.NewEnforcer(kubeclientset, test.FakeArgoCDNamespace, common.ArgoCDConfigMapName, nil) 245 _ = enf.SetBuiltinPolicy(assets.BuiltinPolicyCSV) 246 rbacEnf := rbacpolicy.NewRBACPolicyEnforcer(enf, test.NewFakeProjLister()) 247 enf.SetClaimsEnforcerFunc(rbacEnf.EnforceClaims) 248 assert.False(t, enf.Enforce(nil, "applications", "get", "foo/obj")) 249 enf.SetDefaultRole("role:readonly") 250 assert.True(t, enf.Enforce(nil, "applications", "get", "foo/obj")) 251 } 252 253 func TestInitializingExistingDefaultProject(t *testing.T) { 254 cm := test.NewFakeConfigMap() 255 secret := test.NewFakeSecret() 256 kubeclientset := fake.NewSimpleClientset(cm, secret) 257 defaultProj := &v1alpha1.AppProject{ 258 ObjectMeta: metav1.ObjectMeta{Name: v1alpha1.DefaultAppProjectName, Namespace: test.FakeArgoCDNamespace}, 259 Spec: v1alpha1.AppProjectSpec{}, 260 } 261 appClientSet := apps.NewSimpleClientset(defaultProj) 262 263 mockRepoClient := &mocks.Clientset{RepoServerServiceClient: &mocks.RepoServerServiceClient{}} 264 265 argoCDOpts := ArgoCDServerOpts{ 266 Namespace: test.FakeArgoCDNamespace, 267 KubeClientset: kubeclientset, 268 AppClientset: appClientSet, 269 RepoClientset: mockRepoClient, 270 } 271 272 argocd := NewServer(context.Background(), argoCDOpts) 273 assert.NotNil(t, argocd) 274 275 proj, err := appClientSet.ArgoprojV1alpha1().AppProjects(test.FakeArgoCDNamespace).Get(context.Background(), v1alpha1.DefaultAppProjectName, metav1.GetOptions{}) 276 assert.Nil(t, err) 277 assert.NotNil(t, proj) 278 assert.Equal(t, proj.Name, v1alpha1.DefaultAppProjectName) 279 } 280 281 func TestInitializingNotExistingDefaultProject(t *testing.T) { 282 cm := test.NewFakeConfigMap() 283 secret := test.NewFakeSecret() 284 kubeclientset := fake.NewSimpleClientset(cm, secret) 285 appClientSet := apps.NewSimpleClientset() 286 mockRepoClient := &mocks.Clientset{RepoServerServiceClient: &mocks.RepoServerServiceClient{}} 287 288 argoCDOpts := ArgoCDServerOpts{ 289 Namespace: test.FakeArgoCDNamespace, 290 KubeClientset: kubeclientset, 291 AppClientset: appClientSet, 292 RepoClientset: mockRepoClient, 293 } 294 295 argocd := NewServer(context.Background(), argoCDOpts) 296 assert.NotNil(t, argocd) 297 298 proj, err := appClientSet.ArgoprojV1alpha1().AppProjects(test.FakeArgoCDNamespace).Get(context.Background(), v1alpha1.DefaultAppProjectName, metav1.GetOptions{}) 299 assert.Nil(t, err) 300 assert.NotNil(t, proj) 301 assert.Equal(t, proj.Name, v1alpha1.DefaultAppProjectName) 302 } 303 304 func TestEnforceProjectGroups(t *testing.T) { 305 projectName := "testProj" 306 roleName := "testRole" 307 subFormat := "proj:%s:%s" 308 policyTemplate := "p, %s, applications, get, %s/%s, %s" 309 groupName := "my-org:my-team" 310 311 defaultObject := "*" 312 defaultEffect := "allow" 313 defaultTestObject := fmt.Sprintf("%s/%s", projectName, "test") 314 defaultIssuedAt := int64(1) 315 defaultSub := fmt.Sprintf(subFormat, projectName, roleName) 316 defaultPolicy := fmt.Sprintf(policyTemplate, defaultSub, projectName, defaultObject, defaultEffect) 317 318 existingProj := v1alpha1.AppProject{ 319 ObjectMeta: metav1.ObjectMeta{ 320 Name: projectName, 321 Namespace: test.FakeArgoCDNamespace, 322 }, 323 Spec: v1alpha1.AppProjectSpec{ 324 Roles: []v1alpha1.ProjectRole{ 325 { 326 Name: roleName, 327 Policies: []string{defaultPolicy}, 328 Groups: []string{ 329 groupName, 330 }, 331 }, 332 }, 333 }, 334 } 335 mockRepoClient := &mocks.Clientset{RepoServerServiceClient: &mocks.RepoServerServiceClient{}} 336 kubeclientset := fake.NewSimpleClientset(test.NewFakeConfigMap(), test.NewFakeSecret()) 337 s := NewServer(context.Background(), ArgoCDServerOpts{Namespace: test.FakeArgoCDNamespace, KubeClientset: kubeclientset, AppClientset: apps.NewSimpleClientset(&existingProj), RepoClientset: mockRepoClient}) 338 cancel := test.StartInformer(s.projInformer) 339 defer cancel() 340 claims := jwt.MapClaims{ 341 "iat": defaultIssuedAt, 342 "groups": []string{groupName}, 343 } 344 assert.True(t, s.enf.Enforce(claims, "projects", "get", existingProj.ObjectMeta.Name)) 345 assert.True(t, s.enf.Enforce(claims, "applications", "get", defaultTestObject)) 346 assert.False(t, s.enf.Enforce(claims, "clusters", "get", "test")) 347 348 // now remove the group and make sure it fails 349 log.Println(existingProj.ProjectPoliciesString()) 350 existingProj.Spec.Roles[0].Groups = nil 351 log.Println(existingProj.ProjectPoliciesString()) 352 _, _ = s.AppClientset.ArgoprojV1alpha1().AppProjects(test.FakeArgoCDNamespace).Update(context.Background(), &existingProj, metav1.UpdateOptions{}) 353 time.Sleep(100 * time.Millisecond) // this lets the informer get synced 354 assert.False(t, s.enf.Enforce(claims, "projects", "get", existingProj.ObjectMeta.Name)) 355 assert.False(t, s.enf.Enforce(claims, "applications", "get", defaultTestObject)) 356 assert.False(t, s.enf.Enforce(claims, "clusters", "get", "test")) 357 } 358 359 func TestRevokedToken(t *testing.T) { 360 projectName := "testProj" 361 roleName := "testRole" 362 subFormat := "proj:%s:%s" 363 policyTemplate := "p, %s, applications, get, %s/%s, %s" 364 defaultObject := "*" 365 defaultEffect := "allow" 366 defaultTestObject := fmt.Sprintf("%s/%s", projectName, "test") 367 defaultIssuedAt := int64(1) 368 defaultSub := fmt.Sprintf(subFormat, projectName, roleName) 369 defaultPolicy := fmt.Sprintf(policyTemplate, defaultSub, projectName, defaultObject, defaultEffect) 370 kubeclientset := fake.NewSimpleClientset(test.NewFakeConfigMap(), test.NewFakeSecret()) 371 mockRepoClient := &mocks.Clientset{RepoServerServiceClient: &mocks.RepoServerServiceClient{}} 372 373 jwtTokenByRole := make(map[string]v1alpha1.JWTTokens) 374 jwtTokenByRole[roleName] = v1alpha1.JWTTokens{Items: []v1alpha1.JWTToken{{IssuedAt: defaultIssuedAt}}} 375 376 existingProj := v1alpha1.AppProject{ 377 ObjectMeta: metav1.ObjectMeta{ 378 Name: projectName, 379 Namespace: test.FakeArgoCDNamespace, 380 }, 381 Spec: v1alpha1.AppProjectSpec{ 382 Roles: []v1alpha1.ProjectRole{ 383 { 384 Name: roleName, 385 Policies: []string{defaultPolicy}, 386 JWTTokens: []v1alpha1.JWTToken{ 387 { 388 IssuedAt: defaultIssuedAt, 389 }, 390 }, 391 }, 392 }, 393 }, 394 Status: v1alpha1.AppProjectStatus{ 395 JWTTokensByRole: jwtTokenByRole, 396 }, 397 } 398 399 s := NewServer(context.Background(), ArgoCDServerOpts{Namespace: test.FakeArgoCDNamespace, KubeClientset: kubeclientset, AppClientset: apps.NewSimpleClientset(&existingProj), RepoClientset: mockRepoClient}) 400 cancel := test.StartInformer(s.projInformer) 401 defer cancel() 402 claims := jwt.MapClaims{"sub": defaultSub, "iat": defaultIssuedAt} 403 assert.True(t, s.enf.Enforce(claims, "projects", "get", existingProj.ObjectMeta.Name)) 404 assert.True(t, s.enf.Enforce(claims, "applications", "get", defaultTestObject)) 405 } 406 407 func TestCertsAreNotGeneratedInInsecureMode(t *testing.T) { 408 s, closer := fakeServer(t) 409 defer closer() 410 assert.True(t, s.Insecure) 411 assert.Nil(t, s.settings.Certificate) 412 } 413 414 func TestAuthenticate(t *testing.T) { 415 type testData struct { 416 test string 417 user string 418 errorMsg string 419 anonymousEnabled bool 420 } 421 var tests = []testData{ 422 { 423 test: "TestNoSessionAnonymousDisabled", 424 errorMsg: "no session information", 425 anonymousEnabled: false, 426 }, 427 { 428 test: "TestSessionPresent", 429 user: "admin:login", 430 anonymousEnabled: false, 431 }, 432 { 433 test: "TestSessionNotPresentAnonymousEnabled", 434 anonymousEnabled: true, 435 }, 436 } 437 438 for _, testData := range tests { 439 t.Run(testData.test, func(t *testing.T) { 440 cm := test.NewFakeConfigMap() 441 if testData.anonymousEnabled { 442 cm.Data["users.anonymous.enabled"] = "true" 443 } 444 secret := test.NewFakeSecret() 445 kubeclientset := fake.NewSimpleClientset(cm, secret) 446 appClientSet := apps.NewSimpleClientset() 447 mockRepoClient := &mocks.Clientset{RepoServerServiceClient: &mocks.RepoServerServiceClient{}} 448 argoCDOpts := ArgoCDServerOpts{ 449 Namespace: test.FakeArgoCDNamespace, 450 KubeClientset: kubeclientset, 451 AppClientset: appClientSet, 452 RepoClientset: mockRepoClient, 453 } 454 argocd := NewServer(context.Background(), argoCDOpts) 455 ctx := context.Background() 456 if testData.user != "" { 457 token, err := argocd.sessionMgr.Create(testData.user, 0, "abc") 458 assert.NoError(t, err) 459 ctx = metadata.NewIncomingContext(context.Background(), metadata.Pairs(apiclient.MetaDataTokenKey, token)) 460 } 461 462 _, err := argocd.Authenticate(ctx) 463 if testData.errorMsg != "" { 464 assert.Errorf(t, err, testData.errorMsg) 465 } else { 466 assert.NoError(t, err) 467 } 468 469 }) 470 } 471 } 472 473 func dexMockHandler(t *testing.T, url string) func(http.ResponseWriter, *http.Request) { 474 return func(w http.ResponseWriter, r *http.Request) { 475 w.Header().Set("Content-Type", "application/json") 476 switch r.RequestURI { 477 case "/api/dex/.well-known/openid-configuration": 478 _, err := io.WriteString(w, fmt.Sprintf(` 479 { 480 "issuer": "%[1]s/api/dex", 481 "authorization_endpoint": "%[1]s/api/dex/auth", 482 "token_endpoint": "%[1]s/api/dex/token", 483 "jwks_uri": "%[1]s/api/dex/keys", 484 "userinfo_endpoint": "%[1]s/api/dex/userinfo", 485 "device_authorization_endpoint": "%[1]s/api/dex/device/code", 486 "grant_types_supported": [ 487 "authorization_code", 488 "refresh_token", 489 "urn:ietf:params:oauth:grant-type:device_code" 490 ], 491 "response_types_supported": [ 492 "code" 493 ], 494 "subject_types_supported": [ 495 "public" 496 ], 497 "id_token_signing_alg_values_supported": [ 498 "RS256", "HS256" 499 ], 500 "code_challenge_methods_supported": [ 501 "S256", 502 "plain" 503 ], 504 "scopes_supported": [ 505 "openid", 506 "email", 507 "groups", 508 "profile", 509 "offline_access" 510 ], 511 "token_endpoint_auth_methods_supported": [ 512 "client_secret_basic", 513 "client_secret_post" 514 ], 515 "claims_supported": [ 516 "iss", 517 "sub", 518 "aud", 519 "iat", 520 "exp", 521 "email", 522 "email_verified", 523 "locale", 524 "name", 525 "preferred_username", 526 "at_hash" 527 ] 528 }`, url)) 529 if err != nil { 530 t.Fail() 531 } 532 default: 533 w.WriteHeader(http.StatusNotFound) 534 } 535 } 536 } 537 538 func getTestServer(t *testing.T, anonymousEnabled bool, withFakeSSO bool, useDexForSSO bool, additionalOIDCConfig settings_util.OIDCConfig) (argocd *ArgoCDServer, oidcURL string) { 539 cm := test.NewFakeConfigMap() 540 if anonymousEnabled { 541 cm.Data["users.anonymous.enabled"] = "true" 542 } 543 ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 544 // Start with a placeholder. We need the server URL before setting up the real handler. 545 })) 546 ts.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 547 dexMockHandler(t, ts.URL)(w, r) 548 }) 549 oidcServer := ts 550 if !useDexForSSO { 551 oidcServer = testutil.GetOIDCTestServer(t) 552 } 553 if withFakeSSO { 554 cm.Data["url"] = ts.URL 555 if useDexForSSO { 556 cm.Data["dex.config"] = ` 557 connectors: 558 # OIDC 559 - type: OIDC 560 id: oidc 561 name: OIDC 562 config: 563 issuer: https://auth.example.gom 564 clientID: test-client 565 clientSecret: $dex.oidc.clientSecret` 566 } else { 567 // override required oidc config fields but keep other configs as passed in 568 additionalOIDCConfig.Name = "Okta" 569 additionalOIDCConfig.Issuer = oidcServer.URL 570 additionalOIDCConfig.ClientID = "argo-cd" 571 additionalOIDCConfig.ClientSecret = "$oidc.okta.clientSecret" 572 oidcConfigString, err := yaml.Marshal(additionalOIDCConfig) 573 require.NoError(t, err) 574 cm.Data["oidc.config"] = string(oidcConfigString) 575 // Avoid bothering with certs for local tests. 576 cm.Data["oidc.tls.insecure.skip.verify"] = "true" 577 } 578 } 579 secret := test.NewFakeSecret() 580 kubeclientset := fake.NewSimpleClientset(cm, secret) 581 appClientSet := apps.NewSimpleClientset() 582 mockRepoClient := &mocks.Clientset{RepoServerServiceClient: &mocks.RepoServerServiceClient{}} 583 argoCDOpts := ArgoCDServerOpts{ 584 Namespace: test.FakeArgoCDNamespace, 585 KubeClientset: kubeclientset, 586 AppClientset: appClientSet, 587 RepoClientset: mockRepoClient, 588 } 589 if withFakeSSO && useDexForSSO { 590 argoCDOpts.DexServerAddr = ts.URL 591 } 592 argocd = NewServer(context.Background(), argoCDOpts) 593 var err error 594 argocd.ssoClientApp, err = oidc.NewClientApp(argocd.settings, argocd.DexServerAddr, argocd.DexTLSConfig, argocd.BaseHRef, cache.NewInMemoryCache(24*time.Hour)) 595 require.NoError(t, err) 596 return argocd, oidcServer.URL 597 } 598 599 func TestGetClaims(t *testing.T) { 600 601 defaultExpiry := jwt.NewNumericDate(time.Now().Add(time.Hour * 24)) 602 defaultExpiryUnix := float64(defaultExpiry.Unix()) 603 604 type testData struct { 605 test string 606 claims jwt.MapClaims 607 expectedErrorContains string 608 expectedClaims jwt.MapClaims 609 expectNewToken bool 610 additionalOIDCConfig settings_util.OIDCConfig 611 } 612 var tests = []testData{ 613 { 614 test: "GetClaims", 615 claims: jwt.MapClaims{ 616 "aud": "argo-cd", 617 "exp": defaultExpiry, 618 "sub": "randomUser", 619 }, 620 expectedErrorContains: "", 621 expectedClaims: jwt.MapClaims{ 622 "aud": "argo-cd", 623 "exp": defaultExpiryUnix, 624 "sub": "randomUser", 625 }, 626 expectNewToken: false, 627 additionalOIDCConfig: settings_util.OIDCConfig{}, 628 }, 629 { 630 // note: a passing test with user info groups can never be achieved since the user never logged in properly 631 // therefore the oidcClient's cache contains no accessToken for the user info endpoint 632 // and since the oidcClient cache is unexported (for good reasons) we can't mock this behaviour 633 test: "GetClaimsWithUserInfoGroupsEnabled", 634 claims: jwt.MapClaims{ 635 "aud": common.ArgoCDClientAppID, 636 "exp": defaultExpiry, 637 "sub": "randomUser", 638 }, 639 expectedErrorContains: "invalid session", 640 expectedClaims: jwt.MapClaims{ 641 "aud": common.ArgoCDClientAppID, 642 "exp": defaultExpiryUnix, 643 "sub": "randomUser", 644 }, 645 expectNewToken: false, 646 additionalOIDCConfig: settings_util.OIDCConfig{ 647 EnableUserInfoGroups: true, 648 UserInfoPath: "/userinfo", 649 UserInfoCacheExpiration: "5m", 650 }, 651 }, 652 } 653 654 for _, testData := range tests { 655 testDataCopy := testData 656 657 t.Run(testDataCopy.test, func(t *testing.T) { 658 t.Parallel() 659 660 // Must be declared here to avoid race. 661 ctx := context.Background() //nolint:ineffassign,staticcheck 662 663 argocd, oidcURL := getTestServer(t, false, true, false, testDataCopy.additionalOIDCConfig) 664 665 // create new JWT and store it on the context to simulate an incoming request 666 testDataCopy.claims["iss"] = oidcURL 667 testDataCopy.expectedClaims["iss"] = oidcURL 668 token := jwt.NewWithClaims(jwt.SigningMethodRS512, testDataCopy.claims) 669 key, err := jwt.ParseRSAPrivateKeyFromPEM(testutil.PrivateKey) 670 require.NoError(t, err) 671 tokenString, err := token.SignedString(key) 672 require.NoError(t, err) 673 ctx = metadata.NewIncomingContext(context.Background(), metadata.Pairs(apiclient.MetaDataTokenKey, tokenString)) 674 675 gotClaims, newToken, err := argocd.getClaims(ctx) 676 677 // Note: testutil.oidcMockHandler currently doesn't implement reissuing expired tokens 678 // so newToken will always be empty 679 if testDataCopy.expectNewToken { 680 assert.NotEmpty(t, newToken) 681 } 682 if testDataCopy.expectedClaims == nil { 683 assert.Nil(t, gotClaims) 684 } else { 685 assert.Equal(t, testDataCopy.expectedClaims, gotClaims) 686 } 687 if testDataCopy.expectedErrorContains != "" { 688 assert.ErrorContains(t, err, testDataCopy.expectedErrorContains, "getClaims should have thrown an error and return an error") 689 } else { 690 assert.NoError(t, err) 691 } 692 }) 693 } 694 } 695 696 func TestAuthenticate_3rd_party_JWTs(t *testing.T) { 697 // Marshaling single strings to strings is typical, so we test for this relatively common behavior. 698 jwt.MarshalSingleStringAsArray = false 699 700 type testData struct { 701 test string 702 anonymousEnabled bool 703 claims jwt.RegisteredClaims 704 expectedErrorContains string 705 expectedClaims interface{} 706 useDex bool 707 } 708 var tests = []testData{ 709 // Dex 710 { 711 test: "anonymous disabled, no audience", 712 anonymousEnabled: false, 713 claims: jwt.RegisteredClaims{ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24))}, 714 expectedErrorContains: common.TokenVerificationError, 715 expectedClaims: nil, 716 }, 717 { 718 test: "anonymous enabled, no audience", 719 anonymousEnabled: true, 720 claims: jwt.RegisteredClaims{}, 721 expectedErrorContains: "", 722 expectedClaims: "", 723 }, 724 { 725 test: "anonymous disabled, unexpired token, admin claim", 726 anonymousEnabled: false, 727 claims: jwt.RegisteredClaims{Audience: jwt.ClaimStrings{common.ArgoCDClientAppID}, Subject: "admin", ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24))}, 728 expectedErrorContains: common.TokenVerificationError, 729 expectedClaims: nil, 730 }, 731 { 732 test: "anonymous enabled, unexpired token, admin claim", 733 anonymousEnabled: true, 734 claims: jwt.RegisteredClaims{Audience: jwt.ClaimStrings{common.ArgoCDClientAppID}, Subject: "admin", ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24))}, 735 expectedErrorContains: "", 736 expectedClaims: "", 737 }, 738 { 739 test: "anonymous disabled, expired token, admin claim", 740 anonymousEnabled: false, 741 claims: jwt.RegisteredClaims{Audience: jwt.ClaimStrings{common.ArgoCDClientAppID}, Subject: "admin", ExpiresAt: jwt.NewNumericDate(time.Now())}, 742 expectedErrorContains: common.TokenVerificationError, 743 expectedClaims: jwt.RegisteredClaims{Issuer: "sso"}, 744 }, 745 { 746 test: "anonymous enabled, expired token, admin claim", 747 anonymousEnabled: true, 748 claims: jwt.RegisteredClaims{Audience: jwt.ClaimStrings{common.ArgoCDClientAppID}, Subject: "admin", ExpiresAt: jwt.NewNumericDate(time.Now())}, 749 expectedErrorContains: "", 750 expectedClaims: "", 751 }, 752 { 753 test: "anonymous disabled, unexpired token, admin claim, incorrect audience", 754 anonymousEnabled: false, 755 claims: jwt.RegisteredClaims{Audience: jwt.ClaimStrings{"incorrect-audience"}, Subject: "admin", ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24))}, 756 expectedErrorContains: common.TokenVerificationError, 757 expectedClaims: nil, 758 }, 759 // External OIDC (not bundled Dex) 760 { 761 test: "external OIDC: anonymous disabled, no audience", 762 anonymousEnabled: false, 763 claims: jwt.RegisteredClaims{ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24))}, 764 useDex: true, 765 expectedErrorContains: common.TokenVerificationError, 766 expectedClaims: nil, 767 }, 768 { 769 test: "external OIDC: anonymous enabled, no audience", 770 anonymousEnabled: true, 771 claims: jwt.RegisteredClaims{}, 772 useDex: true, 773 expectedErrorContains: "", 774 expectedClaims: "", 775 }, 776 { 777 test: "external OIDC: anonymous disabled, unexpired token, admin claim", 778 anonymousEnabled: false, 779 claims: jwt.RegisteredClaims{Audience: jwt.ClaimStrings{common.ArgoCDClientAppID}, Subject: "admin", ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24))}, 780 useDex: true, 781 expectedErrorContains: common.TokenVerificationError, 782 expectedClaims: nil, 783 }, 784 { 785 test: "external OIDC: anonymous enabled, unexpired token, admin claim", 786 anonymousEnabled: true, 787 claims: jwt.RegisteredClaims{Audience: jwt.ClaimStrings{common.ArgoCDClientAppID}, Subject: "admin", ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24))}, 788 useDex: true, 789 expectedErrorContains: "", 790 expectedClaims: "", 791 }, 792 { 793 test: "external OIDC: anonymous disabled, expired token, admin claim", 794 anonymousEnabled: false, 795 claims: jwt.RegisteredClaims{Audience: jwt.ClaimStrings{common.ArgoCDClientAppID}, Subject: "admin", ExpiresAt: jwt.NewNumericDate(time.Now())}, 796 useDex: true, 797 expectedErrorContains: common.TokenVerificationError, 798 expectedClaims: jwt.RegisteredClaims{Issuer: "sso"}, 799 }, 800 { 801 test: "external OIDC: anonymous enabled, expired token, admin claim", 802 anonymousEnabled: true, 803 claims: jwt.RegisteredClaims{Audience: jwt.ClaimStrings{common.ArgoCDClientAppID}, Subject: "admin", ExpiresAt: jwt.NewNumericDate(time.Now())}, 804 useDex: true, 805 expectedErrorContains: "", 806 expectedClaims: "", 807 }, 808 { 809 test: "external OIDC: anonymous disabled, unexpired token, admin claim, incorrect audience", 810 anonymousEnabled: false, 811 claims: jwt.RegisteredClaims{Audience: jwt.ClaimStrings{"incorrect-audience"}, Subject: "admin", ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24))}, 812 useDex: true, 813 expectedErrorContains: common.TokenVerificationError, 814 expectedClaims: nil, 815 }, 816 } 817 818 for _, testData := range tests { 819 testDataCopy := testData 820 821 t.Run(testDataCopy.test, func(t *testing.T) { 822 t.Parallel() 823 824 // Must be declared here to avoid race. 825 ctx := context.Background() //nolint:ineffassign,staticcheck 826 827 argocd, oidcURL := getTestServer(t, testDataCopy.anonymousEnabled, true, testDataCopy.useDex, settings_util.OIDCConfig{}) 828 829 if testDataCopy.useDex { 830 testDataCopy.claims.Issuer = fmt.Sprintf("%s/api/dex", oidcURL) 831 } else { 832 testDataCopy.claims.Issuer = oidcURL 833 } 834 token := jwt.NewWithClaims(jwt.SigningMethodHS256, testDataCopy.claims) 835 tokenString, err := token.SignedString([]byte("key")) 836 require.NoError(t, err) 837 ctx = metadata.NewIncomingContext(context.Background(), metadata.Pairs(apiclient.MetaDataTokenKey, tokenString)) 838 839 ctx, err = argocd.Authenticate(ctx) 840 claims := ctx.Value("claims") 841 if testDataCopy.expectedClaims == nil { 842 assert.Nil(t, claims) 843 } else { 844 assert.Equal(t, testDataCopy.expectedClaims, claims) 845 } 846 if testDataCopy.expectedErrorContains != "" { 847 assert.ErrorContains(t, err, testDataCopy.expectedErrorContains, "Authenticate should have thrown an error and blocked the request") 848 } else { 849 assert.NoError(t, err) 850 } 851 }) 852 } 853 } 854 855 func TestAuthenticate_no_request_metadata(t *testing.T) { 856 type testData struct { 857 test string 858 anonymousEnabled bool 859 expectedErrorContains string 860 expectedClaims interface{} 861 } 862 var tests = []testData{ 863 { 864 test: "anonymous disabled", 865 anonymousEnabled: false, 866 expectedErrorContains: "no session information", 867 expectedClaims: nil, 868 }, 869 { 870 test: "anonymous enabled", 871 anonymousEnabled: true, 872 expectedErrorContains: "", 873 expectedClaims: "", 874 }, 875 } 876 877 for _, testData := range tests { 878 testDataCopy := testData 879 880 t.Run(testDataCopy.test, func(t *testing.T) { 881 t.Parallel() 882 883 argocd, _ := getTestServer(t, testDataCopy.anonymousEnabled, true, true, settings_util.OIDCConfig{}) 884 ctx := context.Background() 885 886 ctx, err := argocd.Authenticate(ctx) 887 claims := ctx.Value("claims") 888 assert.Equal(t, testDataCopy.expectedClaims, claims) 889 if testDataCopy.expectedErrorContains != "" { 890 assert.ErrorContains(t, err, testDataCopy.expectedErrorContains, "Authenticate should have thrown an error and blocked the request") 891 } else { 892 assert.NoError(t, err) 893 } 894 }) 895 } 896 } 897 898 func TestAuthenticate_no_SSO(t *testing.T) { 899 type testData struct { 900 test string 901 anonymousEnabled bool 902 expectedErrorMessage string 903 expectedClaims interface{} 904 } 905 var tests = []testData{ 906 { 907 test: "anonymous disabled", 908 anonymousEnabled: false, 909 expectedErrorMessage: "SSO is not configured", 910 expectedClaims: nil, 911 }, 912 { 913 test: "anonymous enabled", 914 anonymousEnabled: true, 915 expectedErrorMessage: "", 916 expectedClaims: "", 917 }, 918 } 919 920 for _, testData := range tests { 921 testDataCopy := testData 922 923 t.Run(testDataCopy.test, func(t *testing.T) { 924 t.Parallel() 925 926 // Must be declared here to avoid race. 927 ctx := context.Background() //nolint:ineffassign,staticcheck 928 929 argocd, dexURL := getTestServer(t, testDataCopy.anonymousEnabled, false, true, settings_util.OIDCConfig{}) 930 token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.RegisteredClaims{Issuer: fmt.Sprintf("%s/api/dex", dexURL)}) 931 tokenString, err := token.SignedString([]byte("key")) 932 require.NoError(t, err) 933 ctx = metadata.NewIncomingContext(context.Background(), metadata.Pairs(apiclient.MetaDataTokenKey, tokenString)) 934 935 ctx, err = argocd.Authenticate(ctx) 936 claims := ctx.Value("claims") 937 assert.Equal(t, testDataCopy.expectedClaims, claims) 938 if testDataCopy.expectedErrorMessage != "" { 939 assert.ErrorContains(t, err, testDataCopy.expectedErrorMessage, "Authenticate should have thrown an error and blocked the request") 940 } else { 941 assert.NoError(t, err) 942 } 943 }) 944 } 945 } 946 947 func TestAuthenticate_bad_request_metadata(t *testing.T) { 948 type testData struct { 949 test string 950 anonymousEnabled bool 951 metadata metadata.MD 952 expectedErrorMessage string 953 expectedClaims interface{} 954 } 955 var tests = []testData{ 956 { 957 test: "anonymous disabled, empty metadata", 958 anonymousEnabled: false, 959 metadata: metadata.MD{}, 960 expectedErrorMessage: "no session information", 961 expectedClaims: nil, 962 }, 963 { 964 test: "anonymous enabled, empty metadata", 965 anonymousEnabled: true, 966 metadata: metadata.MD{}, 967 expectedErrorMessage: "", 968 expectedClaims: "", 969 }, 970 { 971 test: "anonymous disabled, empty tokens", 972 anonymousEnabled: false, 973 metadata: metadata.MD{apiclient.MetaDataTokenKey: []string{}}, 974 expectedErrorMessage: "no session information", 975 expectedClaims: nil, 976 }, 977 { 978 test: "anonymous enabled, empty tokens", 979 anonymousEnabled: true, 980 metadata: metadata.MD{apiclient.MetaDataTokenKey: []string{}}, 981 expectedErrorMessage: "", 982 expectedClaims: "", 983 }, 984 { 985 test: "anonymous disabled, bad tokens", 986 anonymousEnabled: false, 987 metadata: metadata.Pairs(apiclient.MetaDataTokenKey, "bad"), 988 expectedErrorMessage: "token contains an invalid number of segments", 989 expectedClaims: nil, 990 }, 991 { 992 test: "anonymous enabled, bad tokens", 993 anonymousEnabled: true, 994 metadata: metadata.Pairs(apiclient.MetaDataTokenKey, "bad"), 995 expectedErrorMessage: "", 996 expectedClaims: "", 997 }, 998 { 999 test: "anonymous disabled, bad auth header", 1000 anonymousEnabled: false, 1001 metadata: metadata.MD{"authorization": []string{"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZG1pbiJ9.TGGTTHuuGpEU8WgobXxkrBtW3NiR3dgw5LR-1DEW3BQ"}}, 1002 expectedErrorMessage: common.TokenVerificationError, 1003 expectedClaims: nil, 1004 }, 1005 { 1006 test: "anonymous enabled, bad auth header", 1007 anonymousEnabled: true, 1008 metadata: metadata.MD{"authorization": []string{"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZG1pbiJ9.TGGTTHuuGpEU8WgobXxkrBtW3NiR3dgw5LR-1DEW3BQ"}}, 1009 expectedErrorMessage: "", 1010 expectedClaims: "", 1011 }, 1012 { 1013 test: "anonymous disabled, bad auth cookie", 1014 anonymousEnabled: false, 1015 metadata: metadata.MD{"grpcgateway-cookie": []string{"argocd.token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZG1pbiJ9.TGGTTHuuGpEU8WgobXxkrBtW3NiR3dgw5LR-1DEW3BQ"}}, 1016 expectedErrorMessage: common.TokenVerificationError, 1017 expectedClaims: nil, 1018 }, 1019 { 1020 test: "anonymous enabled, bad auth cookie", 1021 anonymousEnabled: true, 1022 metadata: metadata.MD{"grpcgateway-cookie": []string{"argocd.token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZG1pbiJ9.TGGTTHuuGpEU8WgobXxkrBtW3NiR3dgw5LR-1DEW3BQ"}}, 1023 expectedErrorMessage: "", 1024 expectedClaims: "", 1025 }, 1026 } 1027 1028 for _, testData := range tests { 1029 testDataCopy := testData 1030 1031 t.Run(testDataCopy.test, func(t *testing.T) { 1032 t.Parallel() 1033 1034 // Must be declared here to avoid race. 1035 ctx := context.Background() //nolint:ineffassign,staticcheck 1036 1037 argocd, _ := getTestServer(t, testDataCopy.anonymousEnabled, true, true, settings_util.OIDCConfig{}) 1038 ctx = metadata.NewIncomingContext(context.Background(), testDataCopy.metadata) 1039 1040 ctx, err := argocd.Authenticate(ctx) 1041 claims := ctx.Value("claims") 1042 assert.Equal(t, testDataCopy.expectedClaims, claims) 1043 if testDataCopy.expectedErrorMessage != "" { 1044 assert.ErrorContains(t, err, testDataCopy.expectedErrorMessage, "Authenticate should have thrown an error and blocked the request") 1045 } else { 1046 assert.NoError(t, err) 1047 } 1048 }) 1049 } 1050 } 1051 1052 func Test_getToken(t *testing.T) { 1053 token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" 1054 t.Run("Empty", func(t *testing.T) { 1055 assert.Empty(t, getToken(metadata.New(map[string]string{}))) 1056 }) 1057 t.Run("Token", func(t *testing.T) { 1058 assert.Equal(t, token, getToken(metadata.New(map[string]string{"token": token}))) 1059 }) 1060 t.Run("Authorisation", func(t *testing.T) { 1061 assert.Empty(t, getToken(metadata.New(map[string]string{"authorization": "Bearer invalid"}))) 1062 assert.Equal(t, token, getToken(metadata.New(map[string]string{"authorization": "Bearer " + token}))) 1063 }) 1064 t.Run("Cookie", func(t *testing.T) { 1065 assert.Empty(t, getToken(metadata.New(map[string]string{"grpcgateway-cookie": "argocd.token=invalid"}))) 1066 assert.Equal(t, token, getToken(metadata.New(map[string]string{"grpcgateway-cookie": "argocd.token=" + token}))) 1067 }) 1068 } 1069 1070 func TestTranslateGrpcCookieHeader(t *testing.T) { 1071 argoCDOpts := ArgoCDServerOpts{ 1072 Namespace: test.FakeArgoCDNamespace, 1073 KubeClientset: fake.NewSimpleClientset(test.NewFakeConfigMap(), test.NewFakeSecret()), 1074 AppClientset: apps.NewSimpleClientset(), 1075 RepoClientset: &mocks.Clientset{RepoServerServiceClient: &mocks.RepoServerServiceClient{}}, 1076 } 1077 argocd := NewServer(context.Background(), argoCDOpts) 1078 1079 t.Run("TokenIsNotEmpty", func(t *testing.T) { 1080 recorder := httptest.NewRecorder() 1081 err := argocd.translateGrpcCookieHeader(context.Background(), recorder, &session.SessionResponse{ 1082 Token: "xyz", 1083 }) 1084 assert.NoError(t, err) 1085 assert.Equal(t, "argocd.token=xyz; path=/; SameSite=lax; httpOnly; Secure", recorder.Result().Header.Get("Set-Cookie")) 1086 assert.Equal(t, 1, len(recorder.Result().Cookies())) 1087 }) 1088 1089 t.Run("TokenIsLongerThan4093", func(t *testing.T) { 1090 recorder := httptest.NewRecorder() 1091 err := argocd.translateGrpcCookieHeader(context.Background(), recorder, &session.SessionResponse{ 1092 Token: "abc.xyz." + strings.Repeat("x", 4093), 1093 }) 1094 assert.NoError(t, err) 1095 assert.Regexp(t, "argocd.token=.*; path=/; SameSite=lax; httpOnly; Secure", recorder.Result().Header.Get("Set-Cookie")) 1096 assert.Equal(t, 2, len(recorder.Result().Cookies())) 1097 }) 1098 1099 t.Run("TokenIsEmpty", func(t *testing.T) { 1100 recorder := httptest.NewRecorder() 1101 err := argocd.translateGrpcCookieHeader(context.Background(), recorder, &session.SessionResponse{ 1102 Token: "", 1103 }) 1104 assert.NoError(t, err) 1105 assert.Equal(t, "", recorder.Result().Header.Get("Set-Cookie")) 1106 }) 1107 1108 } 1109 1110 func TestInitializeDefaultProject_ProjectDoesNotExist(t *testing.T) { 1111 argoCDOpts := ArgoCDServerOpts{ 1112 Namespace: test.FakeArgoCDNamespace, 1113 KubeClientset: fake.NewSimpleClientset(test.NewFakeConfigMap(), test.NewFakeSecret()), 1114 AppClientset: apps.NewSimpleClientset(), 1115 RepoClientset: &mocks.Clientset{RepoServerServiceClient: &mocks.RepoServerServiceClient{}}, 1116 } 1117 1118 err := initializeDefaultProject(argoCDOpts) 1119 if !assert.NoError(t, err) { 1120 return 1121 } 1122 1123 proj, err := argoCDOpts.AppClientset.ArgoprojV1alpha1(). 1124 AppProjects(test.FakeArgoCDNamespace).Get(context.Background(), v1alpha1.DefaultAppProjectName, metav1.GetOptions{}) 1125 1126 if !assert.NoError(t, err) { 1127 return 1128 } 1129 1130 assert.Equal(t, proj.Spec, v1alpha1.AppProjectSpec{ 1131 SourceRepos: []string{"*"}, 1132 Destinations: []v1alpha1.ApplicationDestination{{Server: "*", Namespace: "*"}}, 1133 ClusterResourceWhitelist: []metav1.GroupKind{{Group: "*", Kind: "*"}}, 1134 }) 1135 } 1136 1137 func TestInitializeDefaultProject_ProjectAlreadyInitialized(t *testing.T) { 1138 existingDefaultProject := v1alpha1.AppProject{ 1139 ObjectMeta: metav1.ObjectMeta{ 1140 Name: v1alpha1.DefaultAppProjectName, 1141 Namespace: test.FakeArgoCDNamespace, 1142 }, 1143 Spec: v1alpha1.AppProjectSpec{ 1144 SourceRepos: []string{"some repo"}, 1145 Destinations: []v1alpha1.ApplicationDestination{{Server: "some cluster", Namespace: "*"}}, 1146 }, 1147 } 1148 1149 argoCDOpts := ArgoCDServerOpts{ 1150 Namespace: test.FakeArgoCDNamespace, 1151 KubeClientset: fake.NewSimpleClientset(test.NewFakeConfigMap(), test.NewFakeSecret()), 1152 AppClientset: apps.NewSimpleClientset(&existingDefaultProject), 1153 RepoClientset: &mocks.Clientset{RepoServerServiceClient: &mocks.RepoServerServiceClient{}}, 1154 } 1155 1156 err := initializeDefaultProject(argoCDOpts) 1157 if !assert.NoError(t, err) { 1158 return 1159 } 1160 1161 proj, err := argoCDOpts.AppClientset.ArgoprojV1alpha1(). 1162 AppProjects(test.FakeArgoCDNamespace).Get(context.Background(), v1alpha1.DefaultAppProjectName, metav1.GetOptions{}) 1163 1164 if !assert.NoError(t, err) { 1165 return 1166 } 1167 1168 assert.Equal(t, proj.Spec, existingDefaultProject.Spec) 1169 } 1170 1171 func TestOIDCConfigChangeDetection_SecretsChanged(t *testing.T) { 1172 //Given 1173 rawOIDCConfig, err := yaml.Marshal(&settings_util.OIDCConfig{ 1174 ClientID: "$k8ssecret:clientid", 1175 ClientSecret: "$k8ssecret:clientsecret"}) 1176 assert.NoError(t, err, "no error expected when marshalling OIDC config") 1177 1178 originalSecrets := map[string]string{"k8ssecret:clientid": "argocd", "k8ssecret:clientsecret": "sharedargooauthsecret"} 1179 1180 argoSettings := settings_util.ArgoCDSettings{OIDCConfigRAW: string(rawOIDCConfig), Secrets: originalSecrets} 1181 1182 originalOIDCConfig := argoSettings.OIDCConfig() 1183 1184 assert.Equal(t, originalOIDCConfig.ClientID, originalSecrets["k8ssecret:clientid"], "expected ClientID be replaced by secret value") 1185 assert.Equal(t, originalOIDCConfig.ClientSecret, originalSecrets["k8ssecret:clientsecret"], "expected ClientSecret be replaced by secret value") 1186 1187 //When 1188 newSecrets := map[string]string{"k8ssecret:clientid": "argocd", "k8ssecret:clientsecret": "a!Better!Secret"} 1189 argoSettings.Secrets = newSecrets 1190 result := checkOIDCConfigChange(originalOIDCConfig, &argoSettings) 1191 1192 //Then 1193 assert.Equal(t, result, true, "secrets have changed, expect interpolated OIDCConfig to change") 1194 } 1195 1196 func TestOIDCConfigChangeDetection_ConfigChanged(t *testing.T) { 1197 //Given 1198 rawOIDCConfig, err := yaml.Marshal(&settings_util.OIDCConfig{ 1199 Name: "argocd", 1200 ClientID: "$k8ssecret:clientid", 1201 ClientSecret: "$k8ssecret:clientsecret"}) 1202 1203 assert.NoError(t, err, "no error expected when marshalling OIDC config") 1204 1205 originalSecrets := map[string]string{"k8ssecret:clientid": "argocd", "k8ssecret:clientsecret": "sharedargooauthsecret"} 1206 1207 argoSettings := settings_util.ArgoCDSettings{OIDCConfigRAW: string(rawOIDCConfig), Secrets: originalSecrets} 1208 1209 originalOIDCConfig := argoSettings.OIDCConfig() 1210 1211 assert.Equal(t, originalOIDCConfig.ClientID, originalSecrets["k8ssecret:clientid"], "expected ClientID be replaced by secret value") 1212 assert.Equal(t, originalOIDCConfig.ClientSecret, originalSecrets["k8ssecret:clientsecret"], "expected ClientSecret be replaced by secret value") 1213 1214 //When 1215 newRawOICDConfig, err := yaml.Marshal(&settings_util.OIDCConfig{ 1216 Name: "cat", 1217 ClientID: "$k8ssecret:clientid", 1218 ClientSecret: "$k8ssecret:clientsecret"}) 1219 1220 assert.NoError(t, err, "no error expected when marshalling OIDC config") 1221 argoSettings.OIDCConfigRAW = string(newRawOICDConfig) 1222 result := checkOIDCConfigChange(originalOIDCConfig, &argoSettings) 1223 1224 //Then 1225 assert.Equal(t, result, true, "no error expected since OICD config created") 1226 } 1227 1228 func TestOIDCConfigChangeDetection_ConfigCreated(t *testing.T) { 1229 //Given 1230 argoSettings := settings_util.ArgoCDSettings{OIDCConfigRAW: ""} 1231 originalOIDCConfig := argoSettings.OIDCConfig() 1232 1233 //When 1234 newRawOICDConfig, err := yaml.Marshal(&settings_util.OIDCConfig{ 1235 Name: "cat", 1236 ClientID: "$k8ssecret:clientid", 1237 ClientSecret: "$k8ssecret:clientsecret"}) 1238 assert.NoError(t, err, "no error expected when marshalling OIDC config") 1239 newSecrets := map[string]string{"k8ssecret:clientid": "argocd", "k8ssecret:clientsecret": "sharedargooauthsecret"} 1240 argoSettings.OIDCConfigRAW = string(newRawOICDConfig) 1241 argoSettings.Secrets = newSecrets 1242 result := checkOIDCConfigChange(originalOIDCConfig, &argoSettings) 1243 1244 //Then 1245 assert.Equal(t, result, true, "no error expected since new OICD config created") 1246 } 1247 1248 func TestOIDCConfigChangeDetection_ConfigDeleted(t *testing.T) { 1249 //Given 1250 rawOIDCConfig, err := yaml.Marshal(&settings_util.OIDCConfig{ 1251 ClientID: "$k8ssecret:clientid", 1252 ClientSecret: "$k8ssecret:clientsecret"}) 1253 assert.NoError(t, err, "no error expected when marshalling OIDC config") 1254 1255 originalSecrets := map[string]string{"k8ssecret:clientid": "argocd", "k8ssecret:clientsecret": "sharedargooauthsecret"} 1256 1257 argoSettings := settings_util.ArgoCDSettings{OIDCConfigRAW: string(rawOIDCConfig), Secrets: originalSecrets} 1258 1259 originalOIDCConfig := argoSettings.OIDCConfig() 1260 1261 assert.Equal(t, originalOIDCConfig.ClientID, originalSecrets["k8ssecret:clientid"], "expected ClientID be replaced by secret value") 1262 assert.Equal(t, originalOIDCConfig.ClientSecret, originalSecrets["k8ssecret:clientsecret"], "expected ClientSecret be replaced by secret value") 1263 1264 //When 1265 argoSettings.OIDCConfigRAW = "" 1266 argoSettings.Secrets = make(map[string]string) 1267 result := checkOIDCConfigChange(originalOIDCConfig, &argoSettings) 1268 1269 //Then 1270 assert.Equal(t, result, true, "no error expected since OICD config deleted") 1271 } 1272 1273 func TestOIDCConfigChangeDetection_NoChange(t *testing.T) { 1274 //Given 1275 rawOIDCConfig, err := yaml.Marshal(&settings_util.OIDCConfig{ 1276 ClientID: "$k8ssecret:clientid", 1277 ClientSecret: "$k8ssecret:clientsecret"}) 1278 assert.NoError(t, err, "no error expected when marshalling OIDC config") 1279 1280 originalSecrets := map[string]string{"k8ssecret:clientid": "argocd", "k8ssecret:clientsecret": "sharedargooauthsecret"} 1281 1282 argoSettings := settings_util.ArgoCDSettings{OIDCConfigRAW: string(rawOIDCConfig), Secrets: originalSecrets} 1283 1284 originalOIDCConfig := argoSettings.OIDCConfig() 1285 1286 assert.Equal(t, originalOIDCConfig.ClientID, originalSecrets["k8ssecret:clientid"], "expected ClientID be replaced by secret value") 1287 assert.Equal(t, originalOIDCConfig.ClientSecret, originalSecrets["k8ssecret:clientsecret"], "expected ClientSecret be replaced by secret value") 1288 1289 //When 1290 result := checkOIDCConfigChange(originalOIDCConfig, &argoSettings) 1291 1292 //Then 1293 assert.Equal(t, result, false, "no error since no config change") 1294 } 1295 1296 func TestIsMainJsBundle(t *testing.T) { 1297 testCases := []struct { 1298 name string 1299 url string 1300 isMainJsBundle bool 1301 }{ 1302 { 1303 name: "localhost with valid main bundle", 1304 url: "https://localhost:8080/main.e4188e5adc97bbfc00c3.js", 1305 isMainJsBundle: true, 1306 }, 1307 { 1308 name: "localhost and deep path with valid main bundle", 1309 url: "https://localhost:8080/some/argo-cd-instance/main.e4188e5adc97bbfc00c3.js", 1310 isMainJsBundle: true, 1311 }, 1312 { 1313 name: "font file", 1314 url: "https://localhost:8080/assets/fonts/google-fonts/Heebo-Bols.woff2", 1315 isMainJsBundle: false, 1316 }, 1317 { 1318 name: "no dot after main", 1319 url: "https://localhost:8080/main/e4188e5adc97bbfc00c3.js", 1320 isMainJsBundle: false, 1321 }, 1322 { 1323 name: "wrong extension character", 1324 url: "https://localhost:8080/main.e4188e5adc97bbfc00c3/js", 1325 isMainJsBundle: false, 1326 }, 1327 { 1328 name: "wrong hash length", 1329 url: "https://localhost:8080/main.e4188e5adc97bbfc00c3abcdefg.js", 1330 isMainJsBundle: false, 1331 }, 1332 } 1333 for _, testCase := range testCases { 1334 testCaseCopy := testCase 1335 t.Run(testCaseCopy.name, func(t *testing.T) { 1336 t.Parallel() 1337 testUrl, _ := url.Parse(testCaseCopy.url) 1338 isMainJsBundle := isMainJsBundle(testUrl) 1339 assert.Equal(t, testCaseCopy.isMainJsBundle, isMainJsBundle) 1340 }) 1341 } 1342 } 1343 1344 func TestCacheControlHeaders(t *testing.T) { 1345 testCases := []struct { 1346 name string 1347 filename string 1348 createFile bool 1349 expectedStatus int 1350 expectedCacheControlHeaders []string 1351 }{ 1352 { 1353 name: "file exists", 1354 filename: "exists.html", 1355 createFile: true, 1356 expectedStatus: 200, 1357 expectedCacheControlHeaders: nil, 1358 }, 1359 { 1360 name: "file does not exist", 1361 filename: "missing.html", 1362 createFile: false, 1363 expectedStatus: 404, 1364 expectedCacheControlHeaders: nil, 1365 }, 1366 { 1367 name: "main js bundle exists", 1368 filename: "main.e4188e5adc97bbfc00c3.js", 1369 createFile: true, 1370 expectedStatus: 200, 1371 expectedCacheControlHeaders: []string{"public, max-age=31536000, immutable"}, 1372 }, 1373 { 1374 name: "main js bundle does not exists", 1375 filename: "main.e4188e5adc97bbfc00c0.js", 1376 createFile: false, 1377 expectedStatus: 404, 1378 expectedCacheControlHeaders: []string{"no-cache"}, 1379 }, 1380 } 1381 1382 for _, testCase := range testCases { 1383 t.Run(testCase.name, func(t *testing.T) { 1384 argocd, closer := fakeServer(t) 1385 defer closer() 1386 1387 handler := argocd.newStaticAssetsHandler() 1388 1389 rr := httptest.NewRecorder() 1390 req := httptest.NewRequest("", fmt.Sprintf("/%s", testCase.filename), nil) 1391 1392 fp := filepath.Join(argocd.TmpAssetsDir, testCase.filename) 1393 1394 if testCase.createFile { 1395 tmpFile, err := os.Create(fp) 1396 assert.NoError(t, err) 1397 err = tmpFile.Close() 1398 assert.NoError(t, err) 1399 } 1400 1401 handler(rr, req) 1402 1403 assert.Equal(t, testCase.expectedStatus, rr.Code) 1404 1405 cacheControl := rr.Result().Header["Cache-Control"] 1406 assert.Equal(t, testCase.expectedCacheControlHeaders, cacheControl) 1407 }) 1408 } 1409 } 1410 func TestReplaceBaseHRef(t *testing.T) { 1411 testCases := []struct { 1412 name string 1413 data string 1414 expected string 1415 replaceWith string 1416 }{ 1417 { 1418 name: "non-root basepath", 1419 data: `<!DOCTYPE html> 1420 <html lang="en"> 1421 1422 <head> 1423 <meta charset="UTF-8"> 1424 <title>Argo CD</title> 1425 <base href="/"> 1426 <meta name="viewport" content="width=device-width, initial-scale=1"> 1427 <link rel='icon' type='image/png' href='assets/favicon/favicon-32x32.png' sizes='32x32'/> 1428 <link rel='icon' type='image/png' href='assets/favicon/favicon-16x16.png' sizes='16x16'/> 1429 <link href="assets/fonts.css" rel="stylesheet"> 1430 </head> 1431 1432 <body> 1433 <noscript> 1434 <p> 1435 Your browser does not support JavaScript. Please enable JavaScript to view the site. 1436 Alternatively, Argo CD can be used with the <a href="https://argoproj.github.io/argo-cd/cli_installation/">Argo CD CLI</a>. 1437 </p> 1438 </noscript> 1439 <div id="app"></div> 1440 </body> 1441 1442 </html>`, 1443 expected: `<!DOCTYPE html> 1444 <html lang="en"> 1445 1446 <head> 1447 <meta charset="UTF-8"> 1448 <title>Argo CD</title> 1449 <base href="/path1/path2/path3/"> 1450 <meta name="viewport" content="width=device-width, initial-scale=1"> 1451 <link rel='icon' type='image/png' href='assets/favicon/favicon-32x32.png' sizes='32x32'/> 1452 <link rel='icon' type='image/png' href='assets/favicon/favicon-16x16.png' sizes='16x16'/> 1453 <link href="assets/fonts.css" rel="stylesheet"> 1454 </head> 1455 1456 <body> 1457 <noscript> 1458 <p> 1459 Your browser does not support JavaScript. Please enable JavaScript to view the site. 1460 Alternatively, Argo CD can be used with the <a href="https://argoproj.github.io/argo-cd/cli_installation/">Argo CD CLI</a>. 1461 </p> 1462 </noscript> 1463 <div id="app"></div> 1464 </body> 1465 1466 </html>`, 1467 replaceWith: `<base href="/path1/path2/path3/">`, 1468 }, 1469 { 1470 name: "root basepath", 1471 data: `<!DOCTYPE html> 1472 <html lang="en"> 1473 1474 <head> 1475 <meta charset="UTF-8"> 1476 <title>Argo CD</title> 1477 <base href="/any/path/test/"> 1478 <meta name="viewport" content="width=device-width, initial-scale=1"> 1479 <link rel='icon' type='image/png' href='assets/favicon/favicon-32x32.png' sizes='32x32'/> 1480 <link rel='icon' type='image/png' href='assets/favicon/favicon-16x16.png' sizes='16x16'/> 1481 <link href="assets/fonts.css" rel="stylesheet"> 1482 </head> 1483 1484 <body> 1485 <noscript> 1486 <p> 1487 Your browser does not support JavaScript. Please enable JavaScript to view the site. 1488 Alternatively, Argo CD can be used with the <a href="https://argoproj.github.io/argo-cd/cli_installation/">Argo CD CLI</a>. 1489 </p> 1490 </noscript> 1491 <div id="app"></div> 1492 </body> 1493 1494 </html>`, 1495 expected: `<!DOCTYPE html> 1496 <html lang="en"> 1497 1498 <head> 1499 <meta charset="UTF-8"> 1500 <title>Argo CD</title> 1501 <base href="/"> 1502 <meta name="viewport" content="width=device-width, initial-scale=1"> 1503 <link rel='icon' type='image/png' href='assets/favicon/favicon-32x32.png' sizes='32x32'/> 1504 <link rel='icon' type='image/png' href='assets/favicon/favicon-16x16.png' sizes='16x16'/> 1505 <link href="assets/fonts.css" rel="stylesheet"> 1506 </head> 1507 1508 <body> 1509 <noscript> 1510 <p> 1511 Your browser does not support JavaScript. Please enable JavaScript to view the site. 1512 Alternatively, Argo CD can be used with the <a href="https://argoproj.github.io/argo-cd/cli_installation/">Argo CD CLI</a>. 1513 </p> 1514 </noscript> 1515 <div id="app"></div> 1516 </body> 1517 1518 </html>`, 1519 replaceWith: `<base href="/">`, 1520 }, 1521 } 1522 for _, testCase := range testCases { 1523 t.Run(testCase.name, func(t *testing.T) { 1524 result := replaceBaseHRef(testCase.data, testCase.replaceWith) 1525 assert.Equal(t, testCase.expected, result) 1526 }) 1527 } 1528 } 1529 1530 func Test_enforceContentTypes(t *testing.T) { 1531 getBaseHandler := func(t *testing.T, allow bool) http.Handler { 1532 return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { 1533 assert.True(t, allow, "http handler was hit when it should have been blocked by content type enforcement") 1534 writer.WriteHeader(200) 1535 }) 1536 } 1537 1538 t.Parallel() 1539 1540 t.Run("GET - not providing a content type, should still succeed", func(t *testing.T) { 1541 handler := enforceContentTypes(getBaseHandler(t, true), []string{"application/json"}).(http.HandlerFunc) 1542 req := httptest.NewRequest("GET", "/", nil) 1543 w := httptest.NewRecorder() 1544 handler(w, req) 1545 resp := w.Result() 1546 assert.Equal(t, 200, resp.StatusCode) 1547 }) 1548 1549 t.Run("POST", func(t *testing.T) { 1550 handler := enforceContentTypes(getBaseHandler(t, true), []string{"application/json"}).(http.HandlerFunc) 1551 req := httptest.NewRequest("POST", "/", nil) 1552 w := httptest.NewRecorder() 1553 handler(w, req) 1554 resp := w.Result() 1555 assert.Equal(t, 415, resp.StatusCode, "didn't provide a content type, should have gotten an error") 1556 1557 req = httptest.NewRequest("POST", "/", nil) 1558 req.Header = map[string][]string{"Content-Type": {"application/json"}} 1559 w = httptest.NewRecorder() 1560 handler(w, req) 1561 resp = w.Result() 1562 assert.Equal(t, 200, resp.StatusCode, "should have passed, since an allowed content type was provided") 1563 1564 req = httptest.NewRequest("POST", "/", nil) 1565 req.Header = map[string][]string{"Content-Type": {"not-allowed"}} 1566 w = httptest.NewRecorder() 1567 handler(w, req) 1568 resp = w.Result() 1569 assert.Equal(t, 415, resp.StatusCode, "should not have passed, since a disallowed content type was provided") 1570 }) 1571 }