mirror of
https://github.com/argoproj/argo-cd
synced 2026-04-21 17:07:16 +00:00
876 lines
26 KiB
Go
876 lines
26 KiB
Go
package extension_test
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
|
|
"github.com/sirupsen/logrus/hooks/test"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/mock"
|
|
"github.com/stretchr/testify/require"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
|
|
"github.com/argoproj/argo-cd/v3/util/rbac"
|
|
|
|
"github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
|
|
"github.com/argoproj/argo-cd/v3/server/extension"
|
|
"github.com/argoproj/argo-cd/v3/server/extension/mocks"
|
|
dbmocks "github.com/argoproj/argo-cd/v3/util/db/mocks"
|
|
"github.com/argoproj/argo-cd/v3/util/settings"
|
|
)
|
|
|
|
func TestValidateHeaders(t *testing.T) {
|
|
t.Run("will build RequestResources successfully", func(t *testing.T) {
|
|
// given
|
|
r, err := http.NewRequestWithContext(t.Context(), http.MethodGet, "http://null", http.NoBody)
|
|
require.NoError(t, err, "error initializing request")
|
|
r.Header.Add(extension.HeaderArgoCDApplicationName, "namespace:app-name")
|
|
r.Header.Add(extension.HeaderArgoCDProjectName, "project-name")
|
|
|
|
// when
|
|
rr, err := extension.ValidateHeaders(r)
|
|
|
|
// then
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, rr)
|
|
assert.Equal(t, "namespace", rr.ApplicationNamespace)
|
|
assert.Equal(t, "app-name", rr.ApplicationName)
|
|
assert.Equal(t, "project-name", rr.ProjectName)
|
|
})
|
|
t.Run("will return error if application is malformatted", func(t *testing.T) {
|
|
// given
|
|
r, err := http.NewRequestWithContext(t.Context(), http.MethodGet, "http://null", http.NoBody)
|
|
require.NoError(t, err, "error initializing request")
|
|
r.Header.Add(extension.HeaderArgoCDApplicationName, "no-namespace")
|
|
|
|
// when
|
|
rr, err := extension.ValidateHeaders(r)
|
|
|
|
// then
|
|
require.Error(t, err)
|
|
assert.Nil(t, rr)
|
|
})
|
|
t.Run("will return error if application header is missing", func(t *testing.T) {
|
|
// given
|
|
r, err := http.NewRequestWithContext(t.Context(), http.MethodGet, "http://null", http.NoBody)
|
|
require.NoError(t, err, "error initializing request")
|
|
r.Header.Add(extension.HeaderArgoCDProjectName, "project-name")
|
|
|
|
// when
|
|
rr, err := extension.ValidateHeaders(r)
|
|
|
|
// then
|
|
require.Error(t, err)
|
|
assert.Nil(t, rr)
|
|
})
|
|
t.Run("will return error if project header is missing", func(t *testing.T) {
|
|
// given
|
|
r, err := http.NewRequestWithContext(t.Context(), http.MethodGet, "http://null", http.NoBody)
|
|
require.NoError(t, err, "error initializing request")
|
|
r.Header.Add(extension.HeaderArgoCDApplicationName, "namespace:app-name")
|
|
|
|
// when
|
|
rr, err := extension.ValidateHeaders(r)
|
|
|
|
// then
|
|
require.Error(t, err)
|
|
assert.Nil(t, rr)
|
|
})
|
|
t.Run("will return error if invalid namespace", func(t *testing.T) {
|
|
// given
|
|
r, err := http.NewRequestWithContext(t.Context(), http.MethodGet, "http://null", http.NoBody)
|
|
require.NoError(t, err, "error initializing request")
|
|
r.Header.Add(extension.HeaderArgoCDApplicationName, "bad%namespace:app-name")
|
|
r.Header.Add(extension.HeaderArgoCDProjectName, "project-name")
|
|
|
|
// when
|
|
rr, err := extension.ValidateHeaders(r)
|
|
|
|
// then
|
|
require.Error(t, err)
|
|
assert.Nil(t, rr)
|
|
})
|
|
t.Run("will return error if invalid app name", func(t *testing.T) {
|
|
// given
|
|
r, err := http.NewRequestWithContext(t.Context(), http.MethodGet, "http://null", http.NoBody)
|
|
require.NoError(t, err, "error initializing request")
|
|
r.Header.Add(extension.HeaderArgoCDApplicationName, "namespace:bad@app")
|
|
r.Header.Add(extension.HeaderArgoCDProjectName, "project-name")
|
|
|
|
// when
|
|
rr, err := extension.ValidateHeaders(r)
|
|
|
|
// then
|
|
require.Error(t, err)
|
|
assert.Nil(t, rr)
|
|
})
|
|
t.Run("will return error if invalid project name", func(t *testing.T) {
|
|
// given
|
|
r, err := http.NewRequestWithContext(t.Context(), http.MethodGet, "http://null", http.NoBody)
|
|
require.NoError(t, err, "error initializing request")
|
|
r.Header.Add(extension.HeaderArgoCDApplicationName, "namespace:app")
|
|
r.Header.Add(extension.HeaderArgoCDProjectName, "bad^project")
|
|
|
|
// when
|
|
rr, err := extension.ValidateHeaders(r)
|
|
|
|
// then
|
|
require.Error(t, err)
|
|
assert.Nil(t, rr)
|
|
})
|
|
}
|
|
|
|
func TestRegisterExtensions(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
type fixture struct {
|
|
settingsGetterMock *mocks.SettingsGetter
|
|
manager *extension.Manager
|
|
}
|
|
|
|
setup := func() *fixture {
|
|
settMock := &mocks.SettingsGetter{}
|
|
|
|
logger, _ := test.NewNullLogger()
|
|
logEntry := logger.WithContext(t.Context())
|
|
m := extension.NewManager(logEntry, "", settMock, nil, nil, nil, nil, nil)
|
|
|
|
return &fixture{
|
|
settingsGetterMock: settMock,
|
|
manager: m,
|
|
}
|
|
}
|
|
t.Run("will register extensions successfully", func(t *testing.T) {
|
|
// given
|
|
t.Parallel()
|
|
f := setup()
|
|
settings := &settings.ArgoCDSettings{
|
|
ExtensionConfig: map[string]string{
|
|
"": getExtensionConfigString(),
|
|
"another-ext": getSingleExtensionConfigString(),
|
|
},
|
|
}
|
|
f.settingsGetterMock.EXPECT().Get().Return(settings, nil)
|
|
expectedProxyRegistries := []string{
|
|
"external-backend",
|
|
"some-backend",
|
|
"another-ext",
|
|
}
|
|
|
|
// when
|
|
err := f.manager.RegisterExtensions()
|
|
|
|
// then
|
|
require.NoError(t, err)
|
|
for _, expectedProxyRegistry := range expectedProxyRegistries {
|
|
proxyRegistry, found := f.manager.ProxyRegistry(expectedProxyRegistry)
|
|
assert.True(t, found)
|
|
assert.NotNil(t, proxyRegistry)
|
|
}
|
|
})
|
|
t.Run("will return error if extension config is invalid", func(t *testing.T) {
|
|
// given
|
|
t.Parallel()
|
|
type testCase struct {
|
|
name string
|
|
configYaml string
|
|
}
|
|
cases := []testCase{
|
|
{
|
|
name: "no name",
|
|
configYaml: getExtensionConfigNoName(),
|
|
},
|
|
{
|
|
name: "no service",
|
|
configYaml: getExtensionConfigNoService(),
|
|
},
|
|
{
|
|
name: "no URL",
|
|
configYaml: getExtensionConfigNoURL(),
|
|
},
|
|
{
|
|
name: "invalid name",
|
|
configYaml: getExtensionConfigInvalidName(),
|
|
},
|
|
{
|
|
name: "no header name",
|
|
configYaml: getExtensionConfigNoHeaderName(),
|
|
},
|
|
{
|
|
name: "no header value",
|
|
configYaml: getExtensionConfigNoHeaderValue(),
|
|
},
|
|
}
|
|
|
|
// when
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
// given
|
|
t.Parallel()
|
|
f := setup()
|
|
settings := &settings.ArgoCDSettings{
|
|
ExtensionConfig: map[string]string{
|
|
"": tc.configYaml,
|
|
},
|
|
}
|
|
f.settingsGetterMock.EXPECT().Get().Return(settings, nil)
|
|
|
|
// when
|
|
err := f.manager.RegisterExtensions()
|
|
|
|
// then
|
|
require.Error(t, err, "expected error in test %s but got nil", tc.name)
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestCallExtension(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
type fixture struct {
|
|
mux *http.ServeMux
|
|
appGetterMock *mocks.ApplicationGetter
|
|
settingsGetterMock *mocks.SettingsGetter
|
|
rbacMock *mocks.RbacEnforcer
|
|
projMock *mocks.ProjectGetter
|
|
metricsMock *mocks.ExtensionMetricsRegistry
|
|
userMock *mocks.UserGetter
|
|
manager *extension.Manager
|
|
}
|
|
defaultServerNamespace := "control-plane-ns"
|
|
defaultProjectName := "project-name"
|
|
|
|
setup := func() *fixture {
|
|
appMock := &mocks.ApplicationGetter{}
|
|
settMock := &mocks.SettingsGetter{}
|
|
rbacMock := &mocks.RbacEnforcer{}
|
|
projMock := &mocks.ProjectGetter{}
|
|
metricsMock := &mocks.ExtensionMetricsRegistry{}
|
|
userMock := &mocks.UserGetter{}
|
|
|
|
dbMock := &dbmocks.ArgoDB{}
|
|
dbMock.EXPECT().GetClusterServersByName(mock.Anything, mock.Anything).Return([]string{"cluster1"}, nil).Maybe()
|
|
dbMock.EXPECT().GetCluster(mock.Anything, mock.Anything).Return(&v1alpha1.Cluster{Server: "some-url", Name: "cluster1"}, nil).Maybe()
|
|
|
|
logger, _ := test.NewNullLogger()
|
|
logEntry := logger.WithContext(t.Context())
|
|
m := extension.NewManager(logEntry, defaultServerNamespace, settMock, appMock, projMock, dbMock, rbacMock, userMock)
|
|
m.AddMetricsRegistry(metricsMock)
|
|
|
|
mux := http.NewServeMux()
|
|
extHandler := http.HandlerFunc(m.CallExtension())
|
|
mux.Handle(extension.URLPrefix+"/", extHandler)
|
|
|
|
return &fixture{
|
|
mux: mux,
|
|
appGetterMock: appMock,
|
|
settingsGetterMock: settMock,
|
|
rbacMock: rbacMock,
|
|
projMock: projMock,
|
|
metricsMock: metricsMock,
|
|
userMock: userMock,
|
|
manager: m,
|
|
}
|
|
}
|
|
|
|
getApp := func(destName, destServer, projName string) *v1alpha1.Application {
|
|
return &v1alpha1.Application{
|
|
TypeMeta: metav1.TypeMeta{},
|
|
ObjectMeta: metav1.ObjectMeta{},
|
|
Spec: v1alpha1.ApplicationSpec{
|
|
Destination: v1alpha1.ApplicationDestination{
|
|
Name: destName,
|
|
Server: destServer,
|
|
},
|
|
Project: projName,
|
|
},
|
|
Status: v1alpha1.ApplicationStatus{
|
|
Resources: []v1alpha1.ResourceStatus{
|
|
{
|
|
Group: "apps",
|
|
Version: "v1",
|
|
Kind: "Pod",
|
|
Namespace: "default",
|
|
Name: "some-pod",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
getProjectWithDestinations := func(prjName string, destNames []string, destURLs []string) *v1alpha1.AppProject {
|
|
destinations := []v1alpha1.ApplicationDestination{}
|
|
for _, destName := range destNames {
|
|
destination := v1alpha1.ApplicationDestination{
|
|
Name: destName,
|
|
}
|
|
destinations = append(destinations, destination)
|
|
}
|
|
for _, destURL := range destURLs {
|
|
destination := v1alpha1.ApplicationDestination{
|
|
Server: destURL,
|
|
}
|
|
destinations = append(destinations, destination)
|
|
}
|
|
return &v1alpha1.AppProject{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: prjName,
|
|
},
|
|
Spec: v1alpha1.AppProjectSpec{
|
|
Destinations: destinations,
|
|
},
|
|
}
|
|
}
|
|
|
|
withProject := func(prj *v1alpha1.AppProject, f *fixture) {
|
|
f.projMock.EXPECT().Get(prj.GetName()).Return(prj, nil).Maybe()
|
|
}
|
|
|
|
withMetrics := func(f *fixture) {
|
|
f.metricsMock.EXPECT().IncExtensionRequestCounter(mock.Anything, mock.Anything).Maybe()
|
|
f.metricsMock.EXPECT().ObserveExtensionRequestDuration(mock.Anything, mock.Anything).Maybe()
|
|
}
|
|
|
|
withRbac := func(f *fixture, allowApp, allowExt bool) {
|
|
var appAccessError error
|
|
var extAccessError error
|
|
if !allowApp {
|
|
appAccessError = errors.New("no app permission")
|
|
}
|
|
if !allowExt {
|
|
extAccessError = errors.New("no extension permission")
|
|
}
|
|
f.rbacMock.EXPECT().EnforceErr(mock.Anything, rbac.ResourceApplications, rbac.ActionGet, mock.Anything).Return(appAccessError).Maybe()
|
|
f.rbacMock.EXPECT().EnforceErr(mock.Anything, rbac.ResourceExtensions, rbac.ActionInvoke, mock.Anything).Return(extAccessError).Maybe()
|
|
}
|
|
|
|
withUser := func(f *fixture, userId string, username string, groups []string) {
|
|
f.userMock.EXPECT().GetUserId(mock.Anything).Return(userId).Maybe()
|
|
f.userMock.EXPECT().GetUsername(mock.Anything).Return(username).Maybe()
|
|
f.userMock.EXPECT().GetGroups(mock.Anything).Return(groups).Maybe()
|
|
}
|
|
|
|
withExtensionConfig := func(configYaml string, f *fixture) {
|
|
secrets := make(map[string]string)
|
|
secrets["extension.auth.header"] = "Bearer some-bearer-token"
|
|
secrets["extension.auth.header2"] = "Bearer another-bearer-token"
|
|
|
|
settings := &settings.ArgoCDSettings{
|
|
ExtensionConfig: map[string]string{
|
|
"ephemeral": "services:\n- url: http://some-server.com",
|
|
"": configYaml,
|
|
},
|
|
Secrets: secrets,
|
|
}
|
|
f.settingsGetterMock.EXPECT().Get().Return(settings, nil).Maybe()
|
|
}
|
|
|
|
startTestServer := func(t *testing.T, f *fixture) *httptest.Server {
|
|
t.Helper()
|
|
err := f.manager.RegisterExtensions()
|
|
require.NoError(t, err, "error starting test server")
|
|
return httptest.NewServer(f.mux)
|
|
}
|
|
|
|
startBackendTestSrv := func(response string) *httptest.Server {
|
|
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
for k, v := range r.Header {
|
|
w.Header().Add(k, strings.Join(v, ","))
|
|
}
|
|
fmt.Fprintln(w, response)
|
|
}))
|
|
}
|
|
newExtensionRequest := func(t *testing.T, method, url string) *http.Request {
|
|
t.Helper()
|
|
r, err := http.NewRequestWithContext(t.Context(), method, url, http.NoBody)
|
|
require.NoError(t, err, "error initializing request")
|
|
r.Header.Add(extension.HeaderArgoCDApplicationName, "namespace:app-name")
|
|
r.Header.Add(extension.HeaderArgoCDProjectName, defaultProjectName)
|
|
return r
|
|
}
|
|
|
|
t.Run("will call extension backend successfully", func(t *testing.T) {
|
|
// given
|
|
t.Parallel()
|
|
f := setup()
|
|
backendResponse := "some data"
|
|
backendEndpoint := "some-backend"
|
|
clusterURL := "some-url"
|
|
backendSrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
for k, v := range r.Header {
|
|
w.Header().Add(k, strings.Join(v, ","))
|
|
}
|
|
fmt.Fprintln(w, backendResponse)
|
|
}))
|
|
defer backendSrv.Close()
|
|
withRbac(f, true, true)
|
|
withUser(f, "some-user-id", "some-user", []string{"group1", "group2"})
|
|
withExtensionConfig(getExtensionConfig(backendEndpoint, backendSrv.URL), f)
|
|
ts := startTestServer(t, f)
|
|
defer ts.Close()
|
|
r := newExtensionRequest(t, "Get", fmt.Sprintf("%s/extensions/%s/", ts.URL, backendEndpoint))
|
|
app := getApp("", clusterURL, defaultProjectName)
|
|
proj := getProjectWithDestinations("project-name", nil, []string{clusterURL})
|
|
f.appGetterMock.EXPECT().Get(mock.Anything, mock.Anything).Return(app, nil).Maybe()
|
|
withProject(proj, f)
|
|
var wg sync.WaitGroup
|
|
wg.Add(2)
|
|
f.metricsMock.
|
|
On("IncExtensionRequestCounter", mock.Anything, mock.Anything).
|
|
Run(func(_ mock.Arguments) {
|
|
wg.Done()
|
|
})
|
|
f.metricsMock.
|
|
On("ObserveExtensionRequestDuration", mock.Anything, mock.Anything).
|
|
Run(func(_ mock.Arguments) {
|
|
wg.Done()
|
|
})
|
|
|
|
// when
|
|
resp, err := http.DefaultClient.Do(r)
|
|
|
|
// then
|
|
require.NoError(t, err)
|
|
require.NotNil(t, resp)
|
|
assert.Equal(t, http.StatusOK, resp.StatusCode)
|
|
body, err := io.ReadAll(resp.Body)
|
|
require.NoError(t, err)
|
|
actual := strings.TrimSuffix(string(body), "\n")
|
|
assert.Equal(t, backendResponse, actual)
|
|
assert.Equal(t, defaultServerNamespace, resp.Header.Get(extension.HeaderArgoCDNamespace))
|
|
assert.Equal(t, clusterURL, resp.Header.Get(extension.HeaderArgoCDTargetClusterURL))
|
|
assert.Equal(t, "Bearer some-bearer-token", resp.Header.Get("Authorization"))
|
|
assert.Equal(t, "some-user", resp.Header.Get(extension.HeaderArgoCDUsername))
|
|
assert.Equal(t, "some-user-id", resp.Header.Get(extension.HeaderArgoCDUserId))
|
|
assert.Equal(t, "group1,group2", resp.Header.Get(extension.HeaderArgoCDGroups))
|
|
|
|
// waitgroup is necessary to make sure assertions aren't executed before
|
|
// the goroutine initiated by extension.CallExtension concludes which would
|
|
// lead to flaky test.
|
|
wg.Wait()
|
|
f.metricsMock.AssertCalled(t, "IncExtensionRequestCounter", backendEndpoint, http.StatusOK)
|
|
f.metricsMock.AssertCalled(t, "ObserveExtensionRequestDuration", backendEndpoint, mock.Anything)
|
|
})
|
|
t.Run("proxy will return 404 if extension endpoint not registered", func(t *testing.T) {
|
|
// given
|
|
t.Parallel()
|
|
f := setup()
|
|
withExtensionConfig(getExtensionConfigString(), f)
|
|
withRbac(f, true, true)
|
|
withMetrics(f)
|
|
withUser(f, "some-user-id", "some-user", []string{"group1", "group2"})
|
|
cluster1Name := "cluster1"
|
|
f.appGetterMock.EXPECT().Get("namespace", "app-name").Return(getApp(cluster1Name, "", defaultProjectName), nil).Maybe()
|
|
withProject(getProjectWithDestinations("project-name", []string{cluster1Name}, []string{"some-url"}), f)
|
|
|
|
ts := startTestServer(t, f)
|
|
defer ts.Close()
|
|
nonRegistered := "non-registered"
|
|
r := newExtensionRequest(t, "Get", fmt.Sprintf("%s/extensions/%s/", ts.URL, nonRegistered))
|
|
|
|
// when
|
|
resp, err := http.DefaultClient.Do(r)
|
|
|
|
// then
|
|
require.NoError(t, err)
|
|
require.NotNil(t, resp)
|
|
assert.Equal(t, http.StatusNotFound, resp.StatusCode)
|
|
})
|
|
t.Run("will route requests with 2 backends for the same extension successfully", func(t *testing.T) {
|
|
// given
|
|
t.Parallel()
|
|
f := setup()
|
|
extName := "some-extension"
|
|
|
|
response1 := "response backend 1"
|
|
cluster1Name := "cluster1"
|
|
cluster1URL := "url1"
|
|
beSrv1 := startBackendTestSrv(response1)
|
|
defer beSrv1.Close()
|
|
|
|
response2 := "response backend 2"
|
|
cluster2Name := "cluster2"
|
|
cluster2URL := "url2"
|
|
beSrv2 := startBackendTestSrv(response2)
|
|
defer beSrv2.Close()
|
|
|
|
f.appGetterMock.EXPECT().Get("ns1", "app1").Return(getApp(cluster1Name, "", defaultProjectName), nil).Maybe()
|
|
f.appGetterMock.EXPECT().Get("ns2", "app2").Return(getApp("", cluster2URL, defaultProjectName), nil).Maybe()
|
|
|
|
withRbac(f, true, true)
|
|
withExtensionConfig(getExtensionConfigWith2Backends(extName, beSrv1.URL, cluster1Name, cluster1URL, beSrv2.URL, cluster2Name, cluster2URL), f)
|
|
withProject(getProjectWithDestinations("project-name", []string{cluster1Name}, []string{cluster2URL}), f)
|
|
withMetrics(f)
|
|
withUser(f, "some-user-id", "some-user", []string{"group1", "group2"})
|
|
|
|
ts := startTestServer(t, f)
|
|
defer ts.Close()
|
|
|
|
url := fmt.Sprintf("%s/extensions/%s/", ts.URL, extName)
|
|
req := newExtensionRequest(t, http.MethodGet, url)
|
|
req.Header.Del(extension.HeaderArgoCDApplicationName)
|
|
|
|
req1 := req.Clone(t.Context())
|
|
req1.Header.Add(extension.HeaderArgoCDApplicationName, "ns1:app1")
|
|
req2 := req.Clone(t.Context())
|
|
req2.Header.Add(extension.HeaderArgoCDApplicationName, "ns2:app2")
|
|
|
|
// when
|
|
resp1, err := http.DefaultClient.Do(req1)
|
|
require.NoError(t, err)
|
|
resp2, err := http.DefaultClient.Do(req2)
|
|
require.NoError(t, err)
|
|
|
|
// then
|
|
require.NotNil(t, resp1)
|
|
assert.Equal(t, http.StatusOK, resp1.StatusCode)
|
|
body, err := io.ReadAll(resp1.Body)
|
|
require.NoError(t, err)
|
|
actual := strings.TrimSuffix(string(body), "\n")
|
|
assert.Equal(t, response1, actual)
|
|
assert.Equal(t, "Bearer some-bearer-token", resp1.Header.Get("Authorization"))
|
|
|
|
require.NotNil(t, resp2)
|
|
assert.Equal(t, http.StatusOK, resp2.StatusCode)
|
|
body, err = io.ReadAll(resp2.Body)
|
|
require.NoError(t, err)
|
|
actual = strings.TrimSuffix(string(body), "\n")
|
|
assert.Equal(t, response2, actual)
|
|
assert.Equal(t, "Bearer another-bearer-token", resp2.Header.Get("Authorization"))
|
|
})
|
|
t.Run("will return 401 if sub has no access to get application", func(t *testing.T) {
|
|
// given
|
|
t.Parallel()
|
|
f := setup()
|
|
allowApp := false
|
|
allowExtension := true
|
|
extName := "some-extension"
|
|
withRbac(f, allowApp, allowExtension)
|
|
withExtensionConfig(getExtensionConfig(extName, "http://fake"), f)
|
|
withMetrics(f)
|
|
withUser(f, "some-user-id", "some-user", []string{"group1", "group2"})
|
|
ts := startTestServer(t, f)
|
|
defer ts.Close()
|
|
r := newExtensionRequest(t, "Get", fmt.Sprintf("%s/extensions/%s/", ts.URL, extName))
|
|
f.appGetterMock.EXPECT().Get(mock.Anything, mock.Anything).Return(getApp("", "", defaultProjectName), nil).Maybe()
|
|
|
|
// when
|
|
resp, err := http.DefaultClient.Do(r)
|
|
|
|
// then
|
|
require.NoError(t, err)
|
|
require.NotNil(t, resp)
|
|
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
|
|
})
|
|
t.Run("will return 401 if sub has no access to invoke extension", func(t *testing.T) {
|
|
// given
|
|
t.Parallel()
|
|
f := setup()
|
|
allowApp := true
|
|
allowExtension := false
|
|
extName := "some-extension"
|
|
withRbac(f, allowApp, allowExtension)
|
|
withExtensionConfig(getExtensionConfig(extName, "http://fake"), f)
|
|
withMetrics(f)
|
|
withUser(f, "some-user-id", "some-user", []string{"group1", "group2"})
|
|
ts := startTestServer(t, f)
|
|
defer ts.Close()
|
|
r := newExtensionRequest(t, "Get", fmt.Sprintf("%s/extensions/%s/", ts.URL, extName))
|
|
f.appGetterMock.EXPECT().Get(mock.Anything, mock.Anything).Return(getApp("", "", defaultProjectName), nil).Maybe()
|
|
|
|
// when
|
|
resp, err := http.DefaultClient.Do(r)
|
|
|
|
// then
|
|
require.NoError(t, err)
|
|
require.NotNil(t, resp)
|
|
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
|
|
})
|
|
t.Run("will return 401 if project has no access to target cluster", func(t *testing.T) {
|
|
// given
|
|
t.Parallel()
|
|
f := setup()
|
|
allowApp := true
|
|
allowExtension := true
|
|
extName := "some-extension"
|
|
noCluster := []string{}
|
|
withRbac(f, allowApp, allowExtension)
|
|
withExtensionConfig(getExtensionConfig(extName, "http://fake"), f)
|
|
withMetrics(f)
|
|
withUser(f, "some-user-id", "some-user", []string{"group1", "group2"})
|
|
ts := startTestServer(t, f)
|
|
defer ts.Close()
|
|
r := newExtensionRequest(t, "Get", fmt.Sprintf("%s/extensions/%s/", ts.URL, extName))
|
|
f.appGetterMock.EXPECT().Get(mock.Anything, mock.Anything).Return(getApp("", "", defaultProjectName), nil).Maybe()
|
|
proj := getProjectWithDestinations("project-name", nil, noCluster)
|
|
withProject(proj, f)
|
|
|
|
// when
|
|
resp, err := http.DefaultClient.Do(r)
|
|
|
|
// then
|
|
require.NoError(t, err)
|
|
require.NotNil(t, resp)
|
|
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
|
|
})
|
|
t.Run("will return 401 if project in application does not exist", func(t *testing.T) {
|
|
// given
|
|
t.Parallel()
|
|
f := setup()
|
|
allowApp := true
|
|
allowExtension := true
|
|
extName := "some-extension"
|
|
withRbac(f, allowApp, allowExtension)
|
|
withExtensionConfig(getExtensionConfig(extName, "http://fake"), f)
|
|
withMetrics(f)
|
|
withUser(f, "some-user-id", "some-user", []string{"group1", "group2"})
|
|
ts := startTestServer(t, f)
|
|
defer ts.Close()
|
|
r := newExtensionRequest(t, "Get", fmt.Sprintf("%s/extensions/%s/", ts.URL, extName))
|
|
f.appGetterMock.EXPECT().Get(mock.Anything, mock.Anything).Return(getApp("", "", defaultProjectName), nil).Maybe()
|
|
f.projMock.EXPECT().Get(defaultProjectName).Return(nil, nil).Maybe()
|
|
|
|
// when
|
|
resp, err := http.DefaultClient.Do(r)
|
|
|
|
// then
|
|
require.NoError(t, err)
|
|
require.NotNil(t, resp)
|
|
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
|
|
})
|
|
t.Run("will return 401 if project in application does not match with header", func(t *testing.T) {
|
|
// given
|
|
t.Parallel()
|
|
f := setup()
|
|
allowApp := true
|
|
allowExtension := true
|
|
extName := "some-extension"
|
|
differentProject := "differentProject"
|
|
withRbac(f, allowApp, allowExtension)
|
|
withExtensionConfig(getExtensionConfig(extName, "http://fake"), f)
|
|
withMetrics(f)
|
|
withUser(f, "some-user-id", "some-user", []string{"group1", "group2"})
|
|
ts := startTestServer(t, f)
|
|
defer ts.Close()
|
|
r := newExtensionRequest(t, "Get", fmt.Sprintf("%s/extensions/%s/", ts.URL, extName))
|
|
f.appGetterMock.EXPECT().Get(mock.Anything, mock.Anything).Return(getApp("", "", differentProject), nil).Maybe()
|
|
|
|
// when
|
|
resp, err := http.DefaultClient.Do(r)
|
|
|
|
// then
|
|
require.NoError(t, err)
|
|
require.NotNil(t, resp)
|
|
assert.Equal(t, http.StatusUnauthorized, resp.StatusCode)
|
|
})
|
|
t.Run("will return 401 if application defines name and server destination", func(t *testing.T) {
|
|
// This test is to validate a security risk with malicious application
|
|
// trying to gain access to execute extensions in clusters it doesn't
|
|
// have access.
|
|
|
|
// given
|
|
t.Parallel()
|
|
f := setup()
|
|
extName := "some-extension"
|
|
maliciousName := "srv1"
|
|
destinationServer := "some-valid-server"
|
|
|
|
f.appGetterMock.EXPECT().Get("ns1", "app1").Return(getApp(maliciousName, destinationServer, defaultProjectName), nil).Maybe()
|
|
|
|
withRbac(f, true, true)
|
|
withExtensionConfig(getExtensionConfigWith2Backends(extName, "url1", "cluster1Name", "cluster1URL", "url2", "cluster2Name", "cluster2URL"), f)
|
|
withProject(getProjectWithDestinations("project-name", nil, []string{"srv1", destinationServer}), f)
|
|
withMetrics(f)
|
|
withUser(f, "some-user-id", "some-user", []string{"group1", "group2"})
|
|
|
|
ts := startTestServer(t, f)
|
|
defer ts.Close()
|
|
|
|
url := fmt.Sprintf("%s/extensions/%s/", ts.URL, extName)
|
|
req := newExtensionRequest(t, http.MethodGet, url)
|
|
req.Header.Del(extension.HeaderArgoCDApplicationName)
|
|
req1 := req.Clone(t.Context())
|
|
req1.Header.Add(extension.HeaderArgoCDApplicationName, "ns1:app1")
|
|
|
|
// when
|
|
resp1, err := http.DefaultClient.Do(req1)
|
|
require.NoError(t, err)
|
|
|
|
// then
|
|
require.NotNil(t, resp1)
|
|
assert.Equal(t, http.StatusUnauthorized, resp1.StatusCode)
|
|
body, err := io.ReadAll(resp1.Body)
|
|
require.NoError(t, err)
|
|
actual := strings.TrimSuffix(string(body), "\n")
|
|
assert.Equal(t, "Unauthorized extension request", actual)
|
|
})
|
|
t.Run("will return 400 if no extension name is provided", func(t *testing.T) {
|
|
// given
|
|
t.Parallel()
|
|
f := setup()
|
|
allowApp := true
|
|
allowExtension := true
|
|
extName := "some-extension"
|
|
differentProject := "differentProject"
|
|
withRbac(f, allowApp, allowExtension)
|
|
withExtensionConfig(getExtensionConfig(extName, "http://fake"), f)
|
|
withMetrics(f)
|
|
withUser(f, "some-user-id", "some-user", []string{"group1", "group2"})
|
|
ts := startTestServer(t, f)
|
|
defer ts.Close()
|
|
r := newExtensionRequest(t, "Get", ts.URL+"/extensions/")
|
|
f.appGetterMock.EXPECT().Get(mock.Anything, mock.Anything).Return(getApp("", "", differentProject), nil).Maybe()
|
|
|
|
// when
|
|
resp, err := http.DefaultClient.Do(r)
|
|
|
|
// then
|
|
require.NoError(t, err)
|
|
require.NotNil(t, resp)
|
|
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
|
})
|
|
}
|
|
|
|
func getExtensionConfig(name, url string) string {
|
|
cfg := `
|
|
extensions:
|
|
- name: %s
|
|
backend:
|
|
services:
|
|
- url: %s
|
|
headers:
|
|
- name: Authorization
|
|
value: '$extension.auth.header'
|
|
`
|
|
return fmt.Sprintf(cfg, name, url)
|
|
}
|
|
|
|
func getExtensionConfigWith2Backends(name, url1, clus1Name, clus1URL, url2, clus2Name, clus2URL string) string {
|
|
cfg := `
|
|
extensions:
|
|
- name: %s
|
|
backend:
|
|
services:
|
|
- url: %s
|
|
headers:
|
|
- name: Authorization
|
|
value: '$extension.auth.header'
|
|
cluster:
|
|
name: %s
|
|
server: %s
|
|
- url: %s
|
|
headers:
|
|
- name: Authorization
|
|
value: '$extension.auth.header2'
|
|
cluster:
|
|
name: %s
|
|
server: %s
|
|
- url: http://test.com
|
|
cluster:
|
|
name: cl1
|
|
- url: http://test2.com
|
|
cluster:
|
|
name: cl2
|
|
`
|
|
return fmt.Sprintf(cfg, name, url1, clus1Name, clus1URL, url2, clus2Name, clus2URL)
|
|
}
|
|
|
|
func getExtensionConfigString() string {
|
|
return `
|
|
extensions:
|
|
- name: external-backend
|
|
backend:
|
|
connectionTimeout: 10s
|
|
keepAlive: 11s
|
|
idleConnectionTimeout: 12s
|
|
maxIdleConnections: 30
|
|
services:
|
|
- url: https://httpbin.org
|
|
headers:
|
|
- name: some-header
|
|
value: '$some.secret.ref'
|
|
- name: some-backend
|
|
backend:
|
|
services:
|
|
- url: http://localhost:7777
|
|
`
|
|
}
|
|
|
|
func getSingleExtensionConfigString() string {
|
|
return `
|
|
connectionTimeout: 10s
|
|
keepAlive: 11s
|
|
idleConnectionTimeout: 12s
|
|
maxIdleConnections: 30
|
|
services:
|
|
- url: http://localhost:7777
|
|
`
|
|
}
|
|
|
|
func getExtensionConfigNoService() string {
|
|
return `
|
|
extensions:
|
|
- backend:
|
|
connectionTimeout: 2s
|
|
`
|
|
}
|
|
|
|
func getExtensionConfigNoName() string {
|
|
return `
|
|
extensions:
|
|
- backend:
|
|
services:
|
|
- url: https://httpbin.org
|
|
`
|
|
}
|
|
|
|
func getExtensionConfigInvalidName() string {
|
|
return `
|
|
extensions:
|
|
- name: invalid/name
|
|
backend:
|
|
services:
|
|
- url: https://httpbin.org
|
|
`
|
|
}
|
|
|
|
func getExtensionConfigNoURL() string {
|
|
return `
|
|
extensions:
|
|
- name: some-backend
|
|
backend:
|
|
services:
|
|
- cluster: some-cluster
|
|
`
|
|
}
|
|
|
|
func getExtensionConfigNoHeaderName() string {
|
|
return `
|
|
extensions:
|
|
- name: some-extension
|
|
backend:
|
|
services:
|
|
- url: https://httpbin.org
|
|
headers:
|
|
- value: '$some.secret.key'
|
|
`
|
|
}
|
|
|
|
func getExtensionConfigNoHeaderValue() string {
|
|
return `
|
|
extensions:
|
|
- name: some-extension
|
|
backend:
|
|
services:
|
|
- url: https://httpbin.org
|
|
headers:
|
|
- name: some-header-name
|
|
`
|
|
}
|