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