github.com/argoproj/argo-cd/v3@v3.2.1/server/server_test.go (about)

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