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 }