github.com/argoproj/argo-cd@v1.8.7/util/session/sessionmanager_test.go (about)

     1  package session
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"math"
     7  	"os"
     8  	"strconv"
     9  	"strings"
    10  	"testing"
    11  	"time"
    12  
    13  	"github.com/dgrijalva/jwt-go/v4"
    14  	"github.com/stretchr/testify/assert"
    15  	"github.com/stretchr/testify/require"
    16  	"google.golang.org/grpc/codes"
    17  	"google.golang.org/grpc/status"
    18  	corev1 "k8s.io/api/core/v1"
    19  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    20  	"k8s.io/apimachinery/pkg/runtime"
    21  	"k8s.io/client-go/kubernetes/fake"
    22  
    23  	"github.com/argoproj/argo-cd/common"
    24  	appv1 "github.com/argoproj/argo-cd/pkg/apis/application/v1alpha1"
    25  	apps "github.com/argoproj/argo-cd/pkg/client/clientset/versioned/fake"
    26  	"github.com/argoproj/argo-cd/pkg/client/listers/application/v1alpha1"
    27  	"github.com/argoproj/argo-cd/test"
    28  	"github.com/argoproj/argo-cd/util/errors"
    29  	"github.com/argoproj/argo-cd/util/password"
    30  	"github.com/argoproj/argo-cd/util/settings"
    31  )
    32  
    33  func getProjLister(objects ...runtime.Object) v1alpha1.AppProjectNamespaceLister {
    34  	return test.NewFakeProjListerFromInterface(apps.NewSimpleClientset(objects...).ArgoprojV1alpha1().AppProjects("argocd"))
    35  }
    36  
    37  func getKubeClient(pass string, enabled bool, capabilities ...settings.AccountCapability) *fake.Clientset {
    38  	const defaultSecretKey = "Hello, world!"
    39  
    40  	bcrypt, err := password.HashPassword(pass)
    41  	errors.CheckError(err)
    42  	if len(capabilities) == 0 {
    43  		capabilities = []settings.AccountCapability{settings.AccountCapabilityLogin, settings.AccountCapabilityApiKey}
    44  	}
    45  	var capabilitiesStr []string
    46  	for i := range capabilities {
    47  		capabilitiesStr = append(capabilitiesStr, string(capabilities[i]))
    48  	}
    49  
    50  	return fake.NewSimpleClientset(&corev1.ConfigMap{
    51  		ObjectMeta: metav1.ObjectMeta{
    52  			Name:      "argocd-cm",
    53  			Namespace: "argocd",
    54  			Labels: map[string]string{
    55  				"app.kubernetes.io/part-of": "argocd",
    56  			},
    57  		},
    58  		Data: map[string]string{
    59  			"admin":         strings.Join(capabilitiesStr, ","),
    60  			"admin.enabled": strconv.FormatBool(enabled),
    61  		},
    62  	}, &corev1.Secret{
    63  		ObjectMeta: metav1.ObjectMeta{
    64  			Name:      "argocd-secret",
    65  			Namespace: "argocd",
    66  		},
    67  		Data: map[string][]byte{
    68  			"admin.password":   []byte(bcrypt),
    69  			"server.secretkey": []byte(defaultSecretKey),
    70  		},
    71  	})
    72  }
    73  
    74  func newSessionManager(settingsMgr *settings.SettingsManager, projectLister v1alpha1.AppProjectNamespaceLister, storage UserStateStorage) *SessionManager {
    75  	mgr := NewSessionManager(settingsMgr, projectLister, "", storage)
    76  	mgr.verificationDelayNoiseEnabled = false
    77  	return mgr
    78  }
    79  
    80  func TestSessionManager_AdminToken(t *testing.T) {
    81  	const (
    82  		defaultSubject = "admin"
    83  	)
    84  	settingsMgr := settings.NewSettingsManager(context.Background(), getKubeClient("pass", true), "argocd")
    85  	mgr := newSessionManager(settingsMgr, getProjLister(), NewInMemoryUserStateStorage())
    86  
    87  	token, err := mgr.Create(defaultSubject, 0, "")
    88  	if err != nil {
    89  		t.Errorf("Could not create token: %v", err)
    90  	}
    91  
    92  	claims, err := mgr.Parse(token)
    93  	if err != nil {
    94  		t.Errorf("Could not parse token: %v", err)
    95  	}
    96  
    97  	mapClaims := *(claims.(*jwt.MapClaims))
    98  	subject := mapClaims["sub"].(string)
    99  	if subject != "admin" {
   100  		t.Errorf("Token claim subject \"%s\" does not match expected subject \"%s\".", subject, defaultSubject)
   101  	}
   102  }
   103  
   104  func TestSessionManager_AdminToken_Deactivated(t *testing.T) {
   105  	settingsMgr := settings.NewSettingsManager(context.Background(), getKubeClient("pass", false), "argocd")
   106  	mgr := newSessionManager(settingsMgr, getProjLister(), NewInMemoryUserStateStorage())
   107  
   108  	token, err := mgr.Create("admin", 0, "")
   109  	if err != nil {
   110  		t.Errorf("Could not create token: %v", err)
   111  	}
   112  
   113  	_, err = mgr.Parse(token)
   114  	require.Error(t, err)
   115  	assert.Contains(t, err.Error(), "account admin is disabled")
   116  }
   117  
   118  func TestSessionManager_AdminToken_LoginCapabilityDisabled(t *testing.T) {
   119  	settingsMgr := settings.NewSettingsManager(context.Background(), getKubeClient("pass", true, settings.AccountCapabilityLogin), "argocd")
   120  	mgr := newSessionManager(settingsMgr, getProjLister(), NewInMemoryUserStateStorage())
   121  
   122  	token, err := mgr.Create("admin", 0, "abc")
   123  	if err != nil {
   124  		t.Errorf("Could not create token: %v", err)
   125  	}
   126  
   127  	_, err = mgr.Parse(token)
   128  	require.Error(t, err)
   129  	assert.Contains(t, err.Error(), "account admin does not have 'apiKey' capability")
   130  }
   131  
   132  func TestSessionManager_ProjectToken(t *testing.T) {
   133  	settingsMgr := settings.NewSettingsManager(context.Background(), getKubeClient("pass", true), "argocd")
   134  
   135  	t.Run("Valid Token", func(t *testing.T) {
   136  		proj := appv1.AppProject{
   137  			ObjectMeta: metav1.ObjectMeta{
   138  				Name:      "default",
   139  				Namespace: "argocd",
   140  			},
   141  			Spec: appv1.AppProjectSpec{Roles: []appv1.ProjectRole{{Name: "test"}}},
   142  			Status: appv1.AppProjectStatus{JWTTokensByRole: map[string]appv1.JWTTokens{
   143  				"test": {
   144  					Items: []appv1.JWTToken{{ID: "abc", IssuedAt: time.Now().Unix(), ExpiresAt: 0}},
   145  				},
   146  			}},
   147  		}
   148  		mgr := newSessionManager(settingsMgr, getProjLister(&proj), NewInMemoryUserStateStorage())
   149  
   150  		jwtToken, err := mgr.Create("proj:default:test", 100, "abc")
   151  		require.NoError(t, err)
   152  
   153  		_, err = mgr.Parse(jwtToken)
   154  		assert.NoError(t, err)
   155  	})
   156  
   157  	t.Run("Token Revoked", func(t *testing.T) {
   158  		proj := appv1.AppProject{
   159  			ObjectMeta: metav1.ObjectMeta{
   160  				Name:      "default",
   161  				Namespace: "argocd",
   162  			},
   163  			Spec: appv1.AppProjectSpec{Roles: []appv1.ProjectRole{{Name: "test"}}},
   164  		}
   165  
   166  		mgr := newSessionManager(settingsMgr, getProjLister(&proj), NewInMemoryUserStateStorage())
   167  
   168  		jwtToken, err := mgr.Create("proj:default:test", 10, "")
   169  		require.NoError(t, err)
   170  
   171  		_, err = mgr.Parse(jwtToken)
   172  		require.Error(t, err)
   173  
   174  		assert.Contains(t, err.Error(), "does not exist in project 'default'")
   175  	})
   176  }
   177  
   178  var loggedOutContext = context.Background()
   179  
   180  // nolint:staticcheck
   181  var loggedInContext = context.WithValue(context.Background(), "claims", &jwt.MapClaims{"iss": "qux", "sub": "foo", "email": "bar", "groups": []string{"baz"}})
   182  
   183  func TestIss(t *testing.T) {
   184  	assert.Empty(t, Iss(loggedOutContext))
   185  	assert.Equal(t, "qux", Iss(loggedInContext))
   186  }
   187  func TestLoggedIn(t *testing.T) {
   188  	assert.False(t, LoggedIn(loggedOutContext))
   189  	assert.True(t, LoggedIn(loggedInContext))
   190  }
   191  
   192  func TestUsername(t *testing.T) {
   193  	assert.Empty(t, Username(loggedOutContext))
   194  	assert.Equal(t, "bar", Username(loggedInContext))
   195  }
   196  
   197  func TestSub(t *testing.T) {
   198  	assert.Empty(t, Sub(loggedOutContext))
   199  	assert.Equal(t, "foo", Sub(loggedInContext))
   200  }
   201  
   202  func TestGroups(t *testing.T) {
   203  	assert.Empty(t, Groups(loggedOutContext, []string{"groups"}))
   204  	assert.Equal(t, []string{"baz"}, Groups(loggedInContext, []string{"groups"}))
   205  }
   206  
   207  func TestVerifyUsernamePassword(t *testing.T) {
   208  	const password = "password"
   209  
   210  	for _, tc := range []struct {
   211  		name     string
   212  		disabled bool
   213  		userName string
   214  		password string
   215  		expected error
   216  	}{
   217  		{
   218  			name:     "Success if userName and password is correct",
   219  			disabled: false,
   220  			userName: common.ArgoCDAdminUsername,
   221  			password: password,
   222  			expected: nil,
   223  		},
   224  		{
   225  			name:     "Return error if password is empty",
   226  			disabled: false,
   227  			userName: common.ArgoCDAdminUsername,
   228  			password: "",
   229  			expected: status.Errorf(codes.Unauthenticated, blankPasswordError),
   230  		},
   231  		{
   232  			name:     "Return error if password is not correct",
   233  			disabled: false,
   234  			userName: common.ArgoCDAdminUsername,
   235  			password: "foo",
   236  			expected: status.Errorf(codes.Unauthenticated, invalidLoginError),
   237  		},
   238  		{
   239  			name:     "Return error if disableAdmin is true",
   240  			disabled: true,
   241  			userName: common.ArgoCDAdminUsername,
   242  			password: password,
   243  			expected: status.Errorf(codes.Unauthenticated, accountDisabled, "admin"),
   244  		},
   245  	} {
   246  		t.Run(tc.name, func(t *testing.T) {
   247  			settingsMgr := settings.NewSettingsManager(context.Background(), getKubeClient(password, !tc.disabled), "argocd")
   248  
   249  			mgr := newSessionManager(settingsMgr, getProjLister(), NewInMemoryUserStateStorage())
   250  
   251  			err := mgr.VerifyUsernamePassword(tc.userName, tc.password)
   252  
   253  			if tc.expected == nil {
   254  				assert.Nil(t, err)
   255  			} else {
   256  				assert.EqualError(t, err, tc.expected.Error())
   257  			}
   258  		})
   259  	}
   260  }
   261  
   262  func TestCacheValueGetters(t *testing.T) {
   263  	t.Run("Default values", func(t *testing.T) {
   264  		mlf := getMaxLoginFailures()
   265  		assert.Equal(t, defaultMaxLoginFailures, mlf)
   266  
   267  		mcs := getMaximumCacheSize()
   268  		assert.Equal(t, defaultMaxCacheSize, mcs)
   269  	})
   270  
   271  	t.Run("Valid environment overrides", func(t *testing.T) {
   272  		os.Setenv(envLoginMaxFailCount, "5")
   273  		os.Setenv(envLoginMaxCacheSize, "5")
   274  
   275  		mlf := getMaxLoginFailures()
   276  		assert.Equal(t, 5, mlf)
   277  
   278  		mcs := getMaximumCacheSize()
   279  		assert.Equal(t, 5, mcs)
   280  
   281  		os.Setenv(envLoginMaxFailCount, "")
   282  		os.Setenv(envLoginMaxCacheSize, "")
   283  	})
   284  
   285  	t.Run("Invalid environment overrides", func(t *testing.T) {
   286  		os.Setenv(envLoginMaxFailCount, "invalid")
   287  		os.Setenv(envLoginMaxCacheSize, "invalid")
   288  
   289  		mlf := getMaxLoginFailures()
   290  		assert.Equal(t, defaultMaxLoginFailures, mlf)
   291  
   292  		mcs := getMaximumCacheSize()
   293  		assert.Equal(t, defaultMaxCacheSize, mcs)
   294  
   295  		os.Setenv(envLoginMaxFailCount, "")
   296  		os.Setenv(envLoginMaxCacheSize, "")
   297  	})
   298  
   299  	t.Run("Less than allowed in environment overrides", func(t *testing.T) {
   300  		os.Setenv(envLoginMaxFailCount, "-1")
   301  		os.Setenv(envLoginMaxCacheSize, "-1")
   302  
   303  		mlf := getMaxLoginFailures()
   304  		assert.Equal(t, defaultMaxLoginFailures, mlf)
   305  
   306  		mcs := getMaximumCacheSize()
   307  		assert.Equal(t, defaultMaxCacheSize, mcs)
   308  
   309  		os.Setenv(envLoginMaxFailCount, "")
   310  		os.Setenv(envLoginMaxCacheSize, "")
   311  	})
   312  
   313  	t.Run("Greater than allowed in environment overrides", func(t *testing.T) {
   314  		os.Setenv(envLoginMaxFailCount, fmt.Sprintf("%d", math.MaxInt32+1))
   315  		os.Setenv(envLoginMaxCacheSize, fmt.Sprintf("%d", math.MaxInt32+1))
   316  
   317  		mlf := getMaxLoginFailures()
   318  		assert.Equal(t, defaultMaxLoginFailures, mlf)
   319  
   320  		mcs := getMaximumCacheSize()
   321  		assert.Equal(t, defaultMaxCacheSize, mcs)
   322  
   323  		os.Setenv(envLoginMaxFailCount, "")
   324  		os.Setenv(envLoginMaxCacheSize, "")
   325  	})
   326  
   327  }
   328  
   329  func TestLoginRateLimiter(t *testing.T) {
   330  	settingsMgr := settings.NewSettingsManager(context.Background(), getKubeClient("password", true), "argocd")
   331  	storage := NewInMemoryUserStateStorage()
   332  
   333  	mgr := newSessionManager(settingsMgr, getProjLister(), storage)
   334  
   335  	t.Run("Test login delay valid user", func(t *testing.T) {
   336  		for i := 0; i < getMaxLoginFailures(); i++ {
   337  			err := mgr.VerifyUsernamePassword("admin", "wrong")
   338  			assert.Error(t, err)
   339  		}
   340  
   341  		// The 11th time should fail even if password is right
   342  		{
   343  			err := mgr.VerifyUsernamePassword("admin", "password")
   344  			assert.Error(t, err)
   345  		}
   346  
   347  		storage.attempts = map[string]LoginAttempts{}
   348  		// Failed counter should have been reset, should validate immediately
   349  		{
   350  			err := mgr.VerifyUsernamePassword("admin", "password")
   351  			assert.NoError(t, err)
   352  		}
   353  	})
   354  
   355  	t.Run("Test login delay invalid user", func(t *testing.T) {
   356  		for i := 0; i < getMaxLoginFailures(); i++ {
   357  			err := mgr.VerifyUsernamePassword("invalid", "wrong")
   358  			assert.Error(t, err)
   359  		}
   360  
   361  		err := mgr.VerifyUsernamePassword("invalid", "wrong")
   362  		assert.Error(t, err)
   363  	})
   364  }
   365  
   366  func TestMaxUsernameLength(t *testing.T) {
   367  	username := ""
   368  	for i := 0; i < maxUsernameLength+1; i++ {
   369  		username += "a"
   370  	}
   371  	settingsMgr := settings.NewSettingsManager(context.Background(), getKubeClient("password", true), "argocd")
   372  	mgr := newSessionManager(settingsMgr, getProjLister(), NewInMemoryUserStateStorage())
   373  	err := mgr.VerifyUsernamePassword(username, "password")
   374  	assert.Error(t, err)
   375  	assert.Contains(t, err.Error(), fmt.Sprintf(usernameTooLongError, maxUsernameLength))
   376  }
   377  
   378  func TestMaxCacheSize(t *testing.T) {
   379  	settingsMgr := settings.NewSettingsManager(context.Background(), getKubeClient("password", true), "argocd")
   380  	mgr := newSessionManager(settingsMgr, getProjLister(), NewInMemoryUserStateStorage())
   381  
   382  	invalidUsers := []string{"invalid1", "invalid2", "invalid3", "invalid4", "invalid5", "invalid6", "invalid7"}
   383  	// Temporarily decrease max cache size
   384  	os.Setenv(envLoginMaxCacheSize, "5")
   385  
   386  	for _, user := range invalidUsers {
   387  		err := mgr.VerifyUsernamePassword(user, "password")
   388  		assert.Error(t, err)
   389  	}
   390  
   391  	assert.Len(t, mgr.GetLoginFailures(), 5)
   392  }
   393  
   394  func TestFailedAttemptsExpiry(t *testing.T) {
   395  	settingsMgr := settings.NewSettingsManager(context.Background(), getKubeClient("password", true), "argocd")
   396  	mgr := newSessionManager(settingsMgr, getProjLister(), NewInMemoryUserStateStorage())
   397  
   398  	invalidUsers := []string{"invalid1", "invalid2", "invalid3", "invalid4", "invalid5", "invalid6", "invalid7"}
   399  
   400  	os.Setenv(envLoginFailureWindowSeconds, "1")
   401  
   402  	for _, user := range invalidUsers {
   403  		err := mgr.VerifyUsernamePassword(user, "password")
   404  		assert.Error(t, err)
   405  	}
   406  
   407  	time.Sleep(2 * time.Second)
   408  
   409  	err := mgr.VerifyUsernamePassword("invalid8", "password")
   410  	assert.Error(t, err)
   411  	assert.Len(t, mgr.GetLoginFailures(), 1)
   412  
   413  	os.Setenv(envLoginFailureWindowSeconds, "")
   414  }