github.com/argoproj/argo-cd/v2@v2.10.9/server/server_test.go (about)

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