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  }