github.com/argoproj/argo-cd/v2@v2.10.9/server/extension/extension_test.go (about) 1 package extension_test 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "io" 8 "net/http" 9 "net/http/httptest" 10 "strings" 11 "testing" 12 13 "github.com/sirupsen/logrus/hooks/test" 14 "github.com/stretchr/testify/assert" 15 "github.com/stretchr/testify/mock" 16 "github.com/stretchr/testify/require" 17 "k8s.io/apimachinery/pkg/apis/meta/v1" 18 19 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" 20 "github.com/argoproj/argo-cd/v2/server/extension" 21 "github.com/argoproj/argo-cd/v2/server/extension/mocks" 22 "github.com/argoproj/argo-cd/v2/server/rbacpolicy" 23 "github.com/argoproj/argo-cd/v2/util/settings" 24 ) 25 26 func TestValidateHeaders(t *testing.T) { 27 t.Run("will build RequestResources successfully", func(t *testing.T) { 28 // given 29 r, err := http.NewRequest("Get", "http://null", nil) 30 if err != nil { 31 t.Fatalf("error initializing request: %s", err) 32 } 33 r.Header.Add(extension.HeaderArgoCDApplicationName, "namespace:app-name") 34 r.Header.Add(extension.HeaderArgoCDProjectName, "project-name") 35 36 // when 37 rr, err := extension.ValidateHeaders(r) 38 39 // then 40 require.NoError(t, err) 41 assert.NotNil(t, rr) 42 assert.Equal(t, "namespace", rr.ApplicationNamespace) 43 assert.Equal(t, "app-name", rr.ApplicationName) 44 assert.Equal(t, "project-name", rr.ProjectName) 45 }) 46 t.Run("will return error if application is malformatted", func(t *testing.T) { 47 // given 48 r, err := http.NewRequest("Get", "http://null", nil) 49 if err != nil { 50 t.Fatalf("error initializing request: %s", err) 51 } 52 r.Header.Add(extension.HeaderArgoCDApplicationName, "no-namespace") 53 54 // when 55 rr, err := extension.ValidateHeaders(r) 56 57 // then 58 assert.Error(t, err) 59 assert.Nil(t, rr) 60 }) 61 t.Run("will return error if application header is missing", func(t *testing.T) { 62 // given 63 r, err := http.NewRequest("Get", "http://null", nil) 64 if err != nil { 65 t.Fatalf("error initializing request: %s", err) 66 } 67 r.Header.Add(extension.HeaderArgoCDProjectName, "project-name") 68 69 // when 70 rr, err := extension.ValidateHeaders(r) 71 72 // then 73 assert.Error(t, err) 74 assert.Nil(t, rr) 75 }) 76 t.Run("will return error if project header is missing", func(t *testing.T) { 77 // given 78 r, err := http.NewRequest("Get", "http://null", nil) 79 if err != nil { 80 t.Fatalf("error initializing request: %s", err) 81 } 82 r.Header.Add(extension.HeaderArgoCDApplicationName, "namespace:app-name") 83 84 // when 85 rr, err := extension.ValidateHeaders(r) 86 87 // then 88 assert.Error(t, err) 89 assert.Nil(t, rr) 90 }) 91 t.Run("will return error if invalid namespace", func(t *testing.T) { 92 // given 93 r, err := http.NewRequest("Get", "http://null", nil) 94 if err != nil { 95 t.Fatalf("error initializing request: %s", err) 96 } 97 r.Header.Add(extension.HeaderArgoCDApplicationName, "bad%namespace:app-name") 98 r.Header.Add(extension.HeaderArgoCDProjectName, "project-name") 99 100 // when 101 rr, err := extension.ValidateHeaders(r) 102 103 // then 104 assert.Error(t, err) 105 assert.Nil(t, rr) 106 }) 107 t.Run("will return error if invalid app name", func(t *testing.T) { 108 // given 109 r, err := http.NewRequest("Get", "http://null", nil) 110 if err != nil { 111 t.Fatalf("error initializing request: %s", err) 112 } 113 r.Header.Add(extension.HeaderArgoCDApplicationName, "namespace:bad@app") 114 r.Header.Add(extension.HeaderArgoCDProjectName, "project-name") 115 116 // when 117 rr, err := extension.ValidateHeaders(r) 118 119 // then 120 assert.Error(t, err) 121 assert.Nil(t, rr) 122 }) 123 t.Run("will return error if invalid project name", func(t *testing.T) { 124 // given 125 r, err := http.NewRequest("Get", "http://null", nil) 126 if err != nil { 127 t.Fatalf("error initializing request: %s", err) 128 } 129 r.Header.Add(extension.HeaderArgoCDApplicationName, "namespace:app") 130 r.Header.Add(extension.HeaderArgoCDProjectName, "bad^project") 131 132 // when 133 rr, err := extension.ValidateHeaders(r) 134 135 // then 136 assert.Error(t, err) 137 assert.Nil(t, rr) 138 }) 139 } 140 141 func TestRegisterExtensions(t *testing.T) { 142 type fixture struct { 143 settingsGetterMock *mocks.SettingsGetter 144 manager *extension.Manager 145 } 146 147 setup := func() *fixture { 148 settMock := &mocks.SettingsGetter{} 149 150 logger, _ := test.NewNullLogger() 151 logEntry := logger.WithContext(context.Background()) 152 m := extension.NewManager(logEntry, settMock, nil, nil, nil) 153 154 return &fixture{ 155 settingsGetterMock: settMock, 156 manager: m, 157 } 158 } 159 t.Run("will register extensions successfully", func(t *testing.T) { 160 // given 161 t.Parallel() 162 f := setup() 163 settings := &settings.ArgoCDSettings{ 164 ExtensionConfig: getExtensionConfigString(), 165 } 166 f.settingsGetterMock.On("Get", mock.Anything).Return(settings, nil) 167 expectedProxyRegistries := []string{ 168 "external-backend", 169 "some-backend"} 170 171 // when 172 err := f.manager.RegisterExtensions() 173 174 // then 175 require.NoError(t, err) 176 for _, expectedProxyRegistry := range expectedProxyRegistries { 177 proxyRegistry, found := f.manager.ProxyRegistry(expectedProxyRegistry) 178 assert.True(t, found) 179 assert.NotNil(t, proxyRegistry) 180 } 181 182 }) 183 t.Run("will return error if extension config is invalid", func(t *testing.T) { 184 // given 185 t.Parallel() 186 type testCase struct { 187 name string 188 configYaml string 189 } 190 cases := []testCase{ 191 { 192 name: "no config", 193 configYaml: "", 194 }, 195 { 196 name: "no name", 197 configYaml: getExtensionConfigNoName(), 198 }, 199 { 200 name: "no service", 201 configYaml: getExtensionConfigNoService(), 202 }, 203 { 204 name: "no URL", 205 configYaml: getExtensionConfigNoURL(), 206 }, 207 { 208 name: "invalid name", 209 configYaml: getExtensionConfigInvalidName(), 210 }, 211 { 212 name: "no header name", 213 configYaml: getExtensionConfigNoHeaderName(), 214 }, 215 { 216 name: "no header value", 217 configYaml: getExtensionConfigNoHeaderValue(), 218 }, 219 } 220 221 // when 222 for _, tc := range cases { 223 tc := tc 224 t.Run(tc.name, func(t *testing.T) { 225 // given 226 t.Parallel() 227 f := setup() 228 settings := &settings.ArgoCDSettings{ 229 ExtensionConfig: tc.configYaml, 230 } 231 f.settingsGetterMock.On("Get", mock.Anything).Return(settings, nil) 232 233 // when 234 err := f.manager.RegisterExtensions() 235 236 // then 237 assert.Error(t, err) 238 }) 239 } 240 }) 241 } 242 243 func TestCallExtension(t *testing.T) { 244 type fixture struct { 245 mux *http.ServeMux 246 appGetterMock *mocks.ApplicationGetter 247 settingsGetterMock *mocks.SettingsGetter 248 rbacMock *mocks.RbacEnforcer 249 projMock *mocks.ProjectGetter 250 manager *extension.Manager 251 } 252 defaultProjectName := "project-name" 253 254 setup := func() *fixture { 255 appMock := &mocks.ApplicationGetter{} 256 settMock := &mocks.SettingsGetter{} 257 rbacMock := &mocks.RbacEnforcer{} 258 projMock := &mocks.ProjectGetter{} 259 260 logger, _ := test.NewNullLogger() 261 logEntry := logger.WithContext(context.Background()) 262 m := extension.NewManager(logEntry, settMock, appMock, projMock, rbacMock) 263 264 mux := http.NewServeMux() 265 extHandler := http.HandlerFunc(m.CallExtension()) 266 mux.Handle(fmt.Sprintf("%s/", extension.URLPrefix), extHandler) 267 268 return &fixture{ 269 mux: mux, 270 appGetterMock: appMock, 271 settingsGetterMock: settMock, 272 rbacMock: rbacMock, 273 projMock: projMock, 274 manager: m, 275 } 276 } 277 278 getApp := func(destName, destServer, projName string) *v1alpha1.Application { 279 return &v1alpha1.Application{ 280 TypeMeta: v1.TypeMeta{}, 281 ObjectMeta: v1.ObjectMeta{}, 282 Spec: v1alpha1.ApplicationSpec{ 283 Destination: v1alpha1.ApplicationDestination{ 284 Name: destName, 285 Server: destServer, 286 }, 287 Project: projName, 288 }, 289 Status: v1alpha1.ApplicationStatus{ 290 Resources: []v1alpha1.ResourceStatus{ 291 { 292 Group: "apps", 293 Version: "v1", 294 Kind: "Pod", 295 Namespace: "default", 296 Name: "some-pod", 297 }, 298 }, 299 }, 300 } 301 } 302 303 getProjectWithDestinations := func(prjName string, destNames []string, destURLs []string) *v1alpha1.AppProject { 304 destinations := []v1alpha1.ApplicationDestination{} 305 for _, destName := range destNames { 306 destination := v1alpha1.ApplicationDestination{ 307 Name: destName, 308 } 309 destinations = append(destinations, destination) 310 } 311 for _, destURL := range destURLs { 312 destination := v1alpha1.ApplicationDestination{ 313 Server: destURL, 314 } 315 destinations = append(destinations, destination) 316 } 317 return &v1alpha1.AppProject{ 318 ObjectMeta: v1.ObjectMeta{ 319 Name: prjName, 320 }, 321 Spec: v1alpha1.AppProjectSpec{ 322 Destinations: destinations, 323 }, 324 } 325 } 326 327 withProject := func(prj *v1alpha1.AppProject, f *fixture) { 328 f.projMock.On("Get", prj.GetName()).Return(prj, nil) 329 } 330 331 withRbac := func(f *fixture, allowApp, allowExt bool) { 332 var appAccessError error 333 var extAccessError error 334 if !allowApp { 335 appAccessError = errors.New("no app permission") 336 } 337 if !allowExt { 338 extAccessError = errors.New("no extension permission") 339 } 340 f.rbacMock.On("EnforceErr", mock.Anything, rbacpolicy.ResourceApplications, rbacpolicy.ActionGet, mock.Anything).Return(appAccessError) 341 f.rbacMock.On("EnforceErr", mock.Anything, rbacpolicy.ResourceExtensions, rbacpolicy.ActionInvoke, mock.Anything).Return(extAccessError) 342 } 343 344 withExtensionConfig := func(configYaml string, f *fixture) { 345 secrets := make(map[string]string) 346 secrets["extension.auth.header"] = "Bearer some-bearer-token" 347 secrets["extension.auth.header2"] = "Bearer another-bearer-token" 348 349 settings := &settings.ArgoCDSettings{ 350 ExtensionConfig: configYaml, 351 Secrets: secrets, 352 } 353 f.settingsGetterMock.On("Get", mock.Anything).Return(settings, nil) 354 } 355 356 startTestServer := func(t *testing.T, f *fixture) *httptest.Server { 357 t.Helper() 358 err := f.manager.RegisterExtensions() 359 if err != nil { 360 t.Fatalf("error starting test server: %s", err) 361 } 362 return httptest.NewServer(f.mux) 363 } 364 365 startBackendTestSrv := func(response string) *httptest.Server { 366 return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 367 for k, v := range r.Header { 368 w.Header().Add(k, strings.Join(v, ",")) 369 } 370 fmt.Fprintln(w, response) 371 })) 372 373 } 374 newExtensionRequest := func(t *testing.T, method, url string) *http.Request { 375 t.Helper() 376 r, err := http.NewRequest(method, url, nil) 377 if err != nil { 378 t.Fatalf("error initializing request: %s", err) 379 } 380 r.Header.Add(extension.HeaderArgoCDApplicationName, "namespace:app-name") 381 r.Header.Add(extension.HeaderArgoCDProjectName, defaultProjectName) 382 return r 383 } 384 385 t.Run("will call extension backend successfully", func(t *testing.T) { 386 // given 387 t.Parallel() 388 f := setup() 389 backendResponse := "some data" 390 backendEndpoint := "some-backend" 391 clusterName := "clusterName" 392 clusterURL := "clusterURL" 393 backendSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 394 for k, v := range r.Header { 395 w.Header().Add(k, strings.Join(v, ",")) 396 } 397 fmt.Fprintln(w, backendResponse) 398 })) 399 defer backendSrv.Close() 400 withRbac(f, true, true) 401 withExtensionConfig(getExtensionConfig(backendEndpoint, backendSrv.URL), f) 402 ts := startTestServer(t, f) 403 defer ts.Close() 404 r := newExtensionRequest(t, "Get", fmt.Sprintf("%s/extensions/%s/", ts.URL, backendEndpoint)) 405 app := getApp(clusterName, clusterURL, defaultProjectName) 406 proj := getProjectWithDestinations("project-name", nil, []string{clusterURL}) 407 f.appGetterMock.On("Get", mock.Anything, mock.Anything).Return(app, nil) 408 withProject(proj, f) 409 410 // when 411 resp, err := http.DefaultClient.Do(r) 412 413 // then 414 require.NoError(t, err) 415 require.NotNil(t, resp) 416 assert.Equal(t, http.StatusOK, resp.StatusCode) 417 body, err := io.ReadAll(resp.Body) 418 require.NoError(t, err) 419 actual := strings.TrimSuffix(string(body), "\n") 420 assert.Equal(t, backendResponse, actual) 421 assert.Equal(t, clusterURL, resp.Header.Get(extension.HeaderArgoCDTargetClusterURL)) 422 assert.Equal(t, "Bearer some-bearer-token", resp.Header.Get("Authorization")) 423 }) 424 t.Run("proxy will return 404 if extension endpoint not registered", func(t *testing.T) { 425 // given 426 t.Parallel() 427 f := setup() 428 withExtensionConfig(getExtensionConfigString(), f) 429 withRbac(f, true, true) 430 cluster1Name := "cluster1" 431 f.appGetterMock.On("Get", "namespace", "app-name").Return(getApp(cluster1Name, "", defaultProjectName), nil) 432 withProject(getProjectWithDestinations("project-name", []string{cluster1Name}, []string{"some-url"}), f) 433 434 ts := startTestServer(t, f) 435 defer ts.Close() 436 nonRegistered := "non-registered" 437 r := newExtensionRequest(t, "Get", fmt.Sprintf("%s/extensions/%s/", ts.URL, nonRegistered)) 438 439 // when 440 resp, err := http.DefaultClient.Do(r) 441 442 // then 443 require.NoError(t, err) 444 require.NotNil(t, resp) 445 assert.Equal(t, http.StatusNotFound, resp.StatusCode) 446 }) 447 t.Run("will route requests with 2 backends for the same extension successfully", func(t *testing.T) { 448 // given 449 t.Parallel() 450 f := setup() 451 extName := "some-extension" 452 453 response1 := "response backend 1" 454 cluster1Name := "cluster1" 455 beSrv1 := startBackendTestSrv(response1) 456 defer beSrv1.Close() 457 458 response2 := "response backend 2" 459 cluster2URL := "cluster2" 460 beSrv2 := startBackendTestSrv(response2) 461 defer beSrv2.Close() 462 463 f.appGetterMock.On("Get", "ns1", "app1").Return(getApp(cluster1Name, "", defaultProjectName), nil) 464 f.appGetterMock.On("Get", "ns2", "app2").Return(getApp("", cluster2URL, defaultProjectName), nil) 465 466 withRbac(f, true, true) 467 withExtensionConfig(getExtensionConfigWith2Backends(extName, beSrv1.URL, cluster1Name, beSrv2.URL, cluster2URL), f) 468 withProject(getProjectWithDestinations("project-name", []string{cluster1Name}, []string{cluster2URL}), f) 469 470 ts := startTestServer(t, f) 471 defer ts.Close() 472 473 url := fmt.Sprintf("%s/extensions/%s/", ts.URL, extName) 474 req := newExtensionRequest(t, http.MethodGet, url) 475 req.Header.Del(extension.HeaderArgoCDApplicationName) 476 477 req1 := req.Clone(context.Background()) 478 req1.Header.Add(extension.HeaderArgoCDApplicationName, "ns1:app1") 479 req2 := req.Clone(context.Background()) 480 req2.Header.Add(extension.HeaderArgoCDApplicationName, "ns2:app2") 481 482 // when 483 resp1, err := http.DefaultClient.Do(req1) 484 require.NoError(t, err) 485 resp2, err := http.DefaultClient.Do(req2) 486 require.NoError(t, err) 487 488 // then 489 require.NotNil(t, resp1) 490 assert.Equal(t, http.StatusOK, resp1.StatusCode) 491 body, err := io.ReadAll(resp1.Body) 492 require.NoError(t, err) 493 actual := strings.TrimSuffix(string(body), "\n") 494 assert.Equal(t, response1, actual) 495 assert.Equal(t, "Bearer some-bearer-token", resp1.Header.Get("Authorization")) 496 497 require.NotNil(t, resp2) 498 assert.Equal(t, http.StatusOK, resp2.StatusCode) 499 body, err = io.ReadAll(resp2.Body) 500 require.NoError(t, err) 501 actual = strings.TrimSuffix(string(body), "\n") 502 assert.Equal(t, response2, actual) 503 assert.Equal(t, "Bearer another-bearer-token", resp2.Header.Get("Authorization")) 504 }) 505 t.Run("will return 401 if sub has no access to get application", func(t *testing.T) { 506 // given 507 t.Parallel() 508 f := setup() 509 allowApp := false 510 allowExtension := true 511 extName := "some-extension" 512 withRbac(f, allowApp, allowExtension) 513 withExtensionConfig(getExtensionConfig(extName, "http://fake"), f) 514 ts := startTestServer(t, f) 515 defer ts.Close() 516 r := newExtensionRequest(t, "Get", fmt.Sprintf("%s/extensions/%s/", ts.URL, extName)) 517 f.appGetterMock.On("Get", mock.Anything, mock.Anything).Return(getApp("", "", defaultProjectName), nil) 518 519 // when 520 resp, err := http.DefaultClient.Do(r) 521 522 // then 523 require.NoError(t, err) 524 require.NotNil(t, resp) 525 assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) 526 }) 527 t.Run("will return 401 if sub has no access to invoke extension", func(t *testing.T) { 528 // given 529 t.Parallel() 530 f := setup() 531 allowApp := true 532 allowExtension := false 533 extName := "some-extension" 534 withRbac(f, allowApp, allowExtension) 535 withExtensionConfig(getExtensionConfig(extName, "http://fake"), f) 536 ts := startTestServer(t, f) 537 defer ts.Close() 538 r := newExtensionRequest(t, "Get", fmt.Sprintf("%s/extensions/%s/", ts.URL, extName)) 539 f.appGetterMock.On("Get", mock.Anything, mock.Anything).Return(getApp("", "", defaultProjectName), nil) 540 541 // when 542 resp, err := http.DefaultClient.Do(r) 543 544 // then 545 require.NoError(t, err) 546 require.NotNil(t, resp) 547 assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) 548 }) 549 t.Run("will return 401 if project has no access to target cluster", func(t *testing.T) { 550 // given 551 t.Parallel() 552 f := setup() 553 allowApp := true 554 allowExtension := true 555 extName := "some-extension" 556 noCluster := []string{} 557 withRbac(f, allowApp, allowExtension) 558 withExtensionConfig(getExtensionConfig(extName, "http://fake"), f) 559 ts := startTestServer(t, f) 560 defer ts.Close() 561 r := newExtensionRequest(t, "Get", fmt.Sprintf("%s/extensions/%s/", ts.URL, extName)) 562 f.appGetterMock.On("Get", mock.Anything, mock.Anything).Return(getApp("", "", defaultProjectName), nil) 563 proj := getProjectWithDestinations("project-name", nil, noCluster) 564 withProject(proj, f) 565 566 // when 567 resp, err := http.DefaultClient.Do(r) 568 569 // then 570 require.NoError(t, err) 571 require.NotNil(t, resp) 572 assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) 573 }) 574 t.Run("will return 401 if project in application does not exist", func(t *testing.T) { 575 // given 576 t.Parallel() 577 f := setup() 578 allowApp := true 579 allowExtension := true 580 extName := "some-extension" 581 withRbac(f, allowApp, allowExtension) 582 withExtensionConfig(getExtensionConfig(extName, "http://fake"), f) 583 ts := startTestServer(t, f) 584 defer ts.Close() 585 r := newExtensionRequest(t, "Get", fmt.Sprintf("%s/extensions/%s/", ts.URL, extName)) 586 f.appGetterMock.On("Get", mock.Anything, mock.Anything).Return(getApp("", "", defaultProjectName), nil) 587 f.projMock.On("Get", defaultProjectName).Return(nil, nil) 588 589 // when 590 resp, err := http.DefaultClient.Do(r) 591 592 // then 593 require.NoError(t, err) 594 require.NotNil(t, resp) 595 assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) 596 }) 597 t.Run("will return 401 if project in application does not match with header", func(t *testing.T) { 598 // given 599 t.Parallel() 600 f := setup() 601 allowApp := true 602 allowExtension := true 603 extName := "some-extension" 604 differentProject := "differentProject" 605 withRbac(f, allowApp, allowExtension) 606 withExtensionConfig(getExtensionConfig(extName, "http://fake"), f) 607 ts := startTestServer(t, f) 608 defer ts.Close() 609 r := newExtensionRequest(t, "Get", fmt.Sprintf("%s/extensions/%s/", ts.URL, extName)) 610 f.appGetterMock.On("Get", mock.Anything, mock.Anything).Return(getApp("", "", differentProject), nil) 611 612 // when 613 resp, err := http.DefaultClient.Do(r) 614 615 // then 616 require.NoError(t, err) 617 require.NotNil(t, resp) 618 assert.Equal(t, http.StatusUnauthorized, resp.StatusCode) 619 }) 620 t.Run("will return 400 if application defines name and server destination", func(t *testing.T) { 621 // This test is to validate a security risk with malicious application 622 // trying to gain access to execute extensions in clusters it doesn't 623 // have access. 624 625 // given 626 t.Parallel() 627 f := setup() 628 extName := "some-extension" 629 maliciousName := "srv1" 630 destinationServer := "some-valid-server" 631 632 f.appGetterMock.On("Get", "ns1", "app1").Return(getApp(maliciousName, destinationServer, defaultProjectName), nil) 633 634 withRbac(f, true, true) 635 withExtensionConfig(getExtensionConfigWith2Backends(extName, "url1", "clusterName", "url2", "clusterURL"), f) 636 withProject(getProjectWithDestinations("project-name", nil, []string{"srv1", destinationServer}), f) 637 638 ts := startTestServer(t, f) 639 defer ts.Close() 640 641 url := fmt.Sprintf("%s/extensions/%s/", ts.URL, extName) 642 req := newExtensionRequest(t, http.MethodGet, url) 643 req.Header.Del(extension.HeaderArgoCDApplicationName) 644 req1 := req.Clone(context.Background()) 645 req1.Header.Add(extension.HeaderArgoCDApplicationName, "ns1:app1") 646 647 // when 648 resp1, err := http.DefaultClient.Do(req1) 649 require.NoError(t, err) 650 651 // then 652 require.NotNil(t, resp1) 653 assert.Equal(t, http.StatusBadRequest, resp1.StatusCode) 654 body, err := io.ReadAll(resp1.Body) 655 require.NoError(t, err) 656 actual := strings.TrimSuffix(string(body), "\n") 657 assert.Equal(t, "invalid extension", actual) 658 }) 659 t.Run("will return 400 if no extension name is provided", func(t *testing.T) { 660 // given 661 t.Parallel() 662 f := setup() 663 allowApp := true 664 allowExtension := true 665 extName := "some-extension" 666 differentProject := "differentProject" 667 withRbac(f, allowApp, allowExtension) 668 withExtensionConfig(getExtensionConfig(extName, "http://fake"), f) 669 ts := startTestServer(t, f) 670 defer ts.Close() 671 r := newExtensionRequest(t, "Get", fmt.Sprintf("%s/extensions/", ts.URL)) 672 f.appGetterMock.On("Get", mock.Anything, mock.Anything).Return(getApp("", "", differentProject), nil) 673 674 // when 675 resp, err := http.DefaultClient.Do(r) 676 677 // then 678 require.NoError(t, err) 679 require.NotNil(t, resp) 680 assert.Equal(t, http.StatusBadRequest, resp.StatusCode) 681 }) 682 } 683 684 func getExtensionConfig(name, url string) string { 685 cfg := ` 686 extensions: 687 - name: %s 688 backend: 689 services: 690 - url: %s 691 headers: 692 - name: Authorization 693 value: '$extension.auth.header' 694 ` 695 return fmt.Sprintf(cfg, name, url) 696 } 697 698 func getExtensionConfigWith2Backends(name, url1, clusName, url2, clusURL string) string { 699 cfg := ` 700 extensions: 701 - name: %s 702 backend: 703 services: 704 - url: %s 705 headers: 706 - name: Authorization 707 value: '$extension.auth.header' 708 cluster: 709 name: %s 710 - url: %s 711 headers: 712 - name: Authorization 713 value: '$extension.auth.header2' 714 cluster: 715 server: %s 716 ` 717 // second extension is configured with the cluster url rather 718 // than the cluster name so we can validate that both use-cases 719 // are working 720 return fmt.Sprintf(cfg, name, url1, clusName, url2, clusURL) 721 } 722 723 func getExtensionConfigString() string { 724 return ` 725 extensions: 726 - name: external-backend 727 backend: 728 connectionTimeout: 10s 729 keepAlive: 11s 730 idleConnectionTimeout: 12s 731 maxIdleConnections: 30 732 services: 733 - url: https://httpbin.org 734 headers: 735 - name: some-header 736 value: '$some.secret.ref' 737 - name: some-backend 738 backend: 739 services: 740 - url: http://localhost:7777 741 ` 742 } 743 744 func getExtensionConfigNoService() string { 745 return ` 746 extensions: 747 - backend: 748 connectionTimeout: 2s 749 ` 750 } 751 func getExtensionConfigNoName() string { 752 return ` 753 extensions: 754 - backend: 755 services: 756 - url: https://httpbin.org 757 ` 758 } 759 func getExtensionConfigInvalidName() string { 760 return ` 761 extensions: 762 - name: invalid/name 763 backend: 764 services: 765 - url: https://httpbin.org 766 ` 767 } 768 769 func getExtensionConfigNoURL() string { 770 return ` 771 extensions: 772 - name: some-backend 773 backend: 774 services: 775 - cluster: some-cluster 776 ` 777 } 778 779 func getExtensionConfigNoHeaderName() string { 780 return ` 781 extensions: 782 - name: some-extension 783 backend: 784 services: 785 - url: https://httpbin.org 786 headers: 787 - value: '$some.secret.key' 788 ` 789 } 790 791 func getExtensionConfigNoHeaderValue() string { 792 return ` 793 extensions: 794 - name: some-extension 795 backend: 796 services: 797 - url: https://httpbin.org 798 headers: 799 - name: some-header-name 800 ` 801 }