mirror of
https://github.com/argoproj/argo-cd.git
synced 2026-02-20 01:28:45 +01:00
1336 lines
47 KiB
Go
1336 lines
47 KiB
Go
package session
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"encoding/pem"
|
|
stderrors "errors"
|
|
"fmt"
|
|
"io"
|
|
"math"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/golang-jwt/jwt/v5"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/status"
|
|
corev1 "k8s.io/api/core/v1"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
"k8s.io/client-go/kubernetes/fake"
|
|
|
|
"github.com/argoproj/argo-cd/v3/common"
|
|
appv1 "github.com/argoproj/argo-cd/v3/pkg/apis/application/v1alpha1"
|
|
apps "github.com/argoproj/argo-cd/v3/pkg/client/clientset/versioned/fake"
|
|
"github.com/argoproj/argo-cd/v3/pkg/client/listers/application/v1alpha1"
|
|
"github.com/argoproj/argo-cd/v3/test"
|
|
"github.com/argoproj/argo-cd/v3/util"
|
|
"github.com/argoproj/argo-cd/v3/util/cache"
|
|
"github.com/argoproj/argo-cd/v3/util/crypto"
|
|
jwtutil "github.com/argoproj/argo-cd/v3/util/jwt"
|
|
"github.com/argoproj/argo-cd/v3/util/oidc"
|
|
"github.com/argoproj/argo-cd/v3/util/password"
|
|
"github.com/argoproj/argo-cd/v3/util/settings"
|
|
utiltest "github.com/argoproj/argo-cd/v3/util/test"
|
|
)
|
|
|
|
func getProjLister(objects ...runtime.Object) v1alpha1.AppProjectNamespaceLister {
|
|
return test.NewFakeProjListerFromInterface(apps.NewSimpleClientset(objects...).ArgoprojV1alpha1().AppProjects("argocd"))
|
|
}
|
|
|
|
func getKubeClient(t *testing.T, pass string, enabled bool, capabilities ...settings.AccountCapability) *fake.Clientset {
|
|
t.Helper()
|
|
const defaultSecretKey = "Hello, world!"
|
|
|
|
bcrypt, err := password.HashPassword(pass)
|
|
require.NoError(t, err)
|
|
if len(capabilities) == 0 {
|
|
capabilities = []settings.AccountCapability{settings.AccountCapabilityLogin, settings.AccountCapabilityApiKey}
|
|
}
|
|
var capabilitiesStr []string
|
|
for i := range capabilities {
|
|
capabilitiesStr = append(capabilitiesStr, string(capabilities[i]))
|
|
}
|
|
|
|
return fake.NewClientset(&corev1.ConfigMap{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "argocd-cm",
|
|
Namespace: "argocd",
|
|
Labels: map[string]string{
|
|
"app.kubernetes.io/part-of": "argocd",
|
|
},
|
|
},
|
|
Data: map[string]string{
|
|
"admin": strings.Join(capabilitiesStr, ","),
|
|
"admin.enabled": strconv.FormatBool(enabled),
|
|
},
|
|
}, &corev1.Secret{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "argocd-secret",
|
|
Namespace: "argocd",
|
|
},
|
|
Data: map[string][]byte{
|
|
"admin.password": []byte(bcrypt),
|
|
"server.secretkey": []byte(defaultSecretKey),
|
|
},
|
|
})
|
|
}
|
|
|
|
func newSessionManager(settingsMgr *settings.SettingsManager, projectLister v1alpha1.AppProjectNamespaceLister, storage UserStateStorage) *SessionManager {
|
|
mgr := NewSessionManager(settingsMgr, projectLister, "", nil, storage)
|
|
mgr.verificationDelayNoiseEnabled = false
|
|
return mgr
|
|
}
|
|
|
|
func TestSessionManager_AdminToken(t *testing.T) {
|
|
redisClient, closer := test.NewInMemoryRedis()
|
|
defer closer()
|
|
|
|
settingsMgr := settings.NewSettingsManager(t.Context(), getKubeClient(t, "pass", true), "argocd")
|
|
mgr := newSessionManager(settingsMgr, getProjLister(), NewUserStateStorage(redisClient))
|
|
|
|
token, err := mgr.Create("admin:login", 0, "123")
|
|
require.NoError(t, err, "Could not create token")
|
|
|
|
claims, newToken, err := mgr.Parse(token)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, newToken)
|
|
|
|
mapClaims := *(claims.(*jwt.MapClaims))
|
|
subject := mapClaims["sub"].(string)
|
|
if subject != "admin" {
|
|
t.Errorf("Token claim subject %q does not match expected subject %q.", subject, "admin")
|
|
}
|
|
}
|
|
|
|
func TestSessionManager_AdminToken_ExpiringSoon(t *testing.T) {
|
|
redisClient, closer := test.NewInMemoryRedis()
|
|
defer closer()
|
|
|
|
settingsMgr := settings.NewSettingsManager(t.Context(), getKubeClient(t, "pass", true), "argocd")
|
|
mgr := newSessionManager(settingsMgr, getProjLister(), NewUserStateStorage(redisClient))
|
|
|
|
token, err := mgr.Create("admin:login", int64(autoRegenerateTokenDuration.Seconds()-1), "123")
|
|
require.NoError(t, err)
|
|
|
|
// verify new token is generated is login token is expiring soon
|
|
_, newToken, err := mgr.Parse(token)
|
|
require.NoError(t, err)
|
|
assert.NotEmpty(t, newToken)
|
|
|
|
// verify that new token is valid and for the same user
|
|
claims, _, err := mgr.Parse(newToken)
|
|
require.NoError(t, err)
|
|
|
|
mapClaims := *(claims.(*jwt.MapClaims))
|
|
subject := mapClaims["sub"].(string)
|
|
assert.Equal(t, "admin", subject)
|
|
}
|
|
|
|
func TestSessionManager_AdminToken_Revoked(t *testing.T) {
|
|
redisClient, closer := test.NewInMemoryRedis()
|
|
defer closer()
|
|
|
|
settingsMgr := settings.NewSettingsManager(t.Context(), getKubeClient(t, "pass", true), "argocd")
|
|
storage := NewUserStateStorage(redisClient)
|
|
|
|
mgr := newSessionManager(settingsMgr, getProjLister(), storage)
|
|
|
|
token, err := mgr.Create("admin:login", 0, "123")
|
|
require.NoError(t, err)
|
|
|
|
err = storage.RevokeToken(t.Context(), "123", time.Hour)
|
|
require.NoError(t, err)
|
|
|
|
_, _, err = mgr.Parse(token)
|
|
assert.EqualError(t, err, "token is revoked, please re-login")
|
|
}
|
|
|
|
func TestSessionManager_AdminToken_Deactivated(t *testing.T) {
|
|
settingsMgr := settings.NewSettingsManager(t.Context(), getKubeClient(t, "pass", false), "argocd")
|
|
mgr := newSessionManager(settingsMgr, getProjLister(), NewUserStateStorage(nil))
|
|
|
|
token, err := mgr.Create("admin:login", 0, "abc")
|
|
require.NoError(t, err, "Could not create token")
|
|
|
|
_, _, err = mgr.Parse(token)
|
|
assert.ErrorContains(t, err, "account admin is disabled")
|
|
}
|
|
|
|
func TestSessionManager_AdminToken_LoginCapabilityDisabled(t *testing.T) {
|
|
settingsMgr := settings.NewSettingsManager(t.Context(), getKubeClient(t, "pass", true, settings.AccountCapabilityLogin), "argocd")
|
|
mgr := newSessionManager(settingsMgr, getProjLister(), NewUserStateStorage(nil))
|
|
|
|
token, err := mgr.Create("admin", 0, "abc")
|
|
require.NoError(t, err, "Could not create token")
|
|
|
|
_, _, err = mgr.Parse(token)
|
|
assert.ErrorContains(t, err, "account admin does not have 'apiKey' capability")
|
|
}
|
|
|
|
func TestSessionManager_ProjectToken(t *testing.T) {
|
|
settingsMgr := settings.NewSettingsManager(t.Context(), getKubeClient(t, "pass", true), "argocd")
|
|
|
|
t.Run("Valid Token", func(t *testing.T) {
|
|
proj := appv1.AppProject{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "default",
|
|
Namespace: "argocd",
|
|
},
|
|
Spec: appv1.AppProjectSpec{Roles: []appv1.ProjectRole{{Name: "test"}}},
|
|
Status: appv1.AppProjectStatus{JWTTokensByRole: map[string]appv1.JWTTokens{
|
|
"test": {
|
|
Items: []appv1.JWTToken{{ID: "abc", IssuedAt: time.Now().Unix(), ExpiresAt: 0}},
|
|
},
|
|
}},
|
|
}
|
|
mgr := newSessionManager(settingsMgr, getProjLister(&proj), NewUserStateStorage(nil))
|
|
|
|
jwtToken, err := mgr.Create("proj:default:test", 100, "abc")
|
|
require.NoError(t, err)
|
|
|
|
claims, _, err := mgr.Parse(jwtToken)
|
|
require.NoError(t, err)
|
|
|
|
mapClaims, err := jwtutil.MapClaims(claims)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, "proj:default:test", mapClaims["sub"])
|
|
})
|
|
|
|
t.Run("Token Revoked", func(t *testing.T) {
|
|
proj := appv1.AppProject{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "default",
|
|
Namespace: "argocd",
|
|
},
|
|
Spec: appv1.AppProjectSpec{Roles: []appv1.ProjectRole{{Name: "test"}}},
|
|
}
|
|
|
|
mgr := newSessionManager(settingsMgr, getProjLister(&proj), NewUserStateStorage(nil))
|
|
|
|
jwtToken, err := mgr.Create("proj:default:test", 10, "")
|
|
require.NoError(t, err)
|
|
|
|
_, _, err = mgr.Parse(jwtToken)
|
|
assert.ErrorContains(t, err, "does not exist in project 'default'")
|
|
})
|
|
}
|
|
|
|
type tokenVerifierMock struct {
|
|
claims jwt.Claims
|
|
err error
|
|
}
|
|
|
|
func (tm *tokenVerifierMock) VerifyToken(_ string) (jwt.Claims, string, error) {
|
|
if tm.claims == nil {
|
|
return nil, "", tm.err
|
|
}
|
|
return tm.claims, "", tm.err
|
|
}
|
|
|
|
func strPointer(str string) *string {
|
|
return &str
|
|
}
|
|
|
|
func TestSessionManager_WithAuthMiddleware(t *testing.T) {
|
|
handlerFunc := func() func(http.ResponseWriter, *http.Request) {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
t.Helper()
|
|
w.WriteHeader(http.StatusOK)
|
|
|
|
contextClaims := r.Context().Value("claims")
|
|
if contextClaims != nil {
|
|
var gotClaims jwt.MapClaims
|
|
var ok bool
|
|
if gotClaims, ok = contextClaims.(jwt.MapClaims); !ok {
|
|
if tmpClaims, ok := contextClaims.(*jwt.MapClaims); ok && tmpClaims != nil {
|
|
gotClaims = *tmpClaims
|
|
}
|
|
}
|
|
jsonClaims, err := json.Marshal(gotClaims)
|
|
require.NoError(t, err, "erorr marshalling claims set by AuthMiddleware")
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, err = w.Write(jsonClaims)
|
|
require.NoError(t, err, "error writing response: %s", err)
|
|
} else {
|
|
w.Header().Set("Content-Type", "application/text")
|
|
_, err := w.Write([]byte("Ok"))
|
|
require.NoError(t, err, "error writing response: %s", err)
|
|
}
|
|
}
|
|
}
|
|
type testCase struct {
|
|
name string
|
|
authDisabled bool
|
|
ssoEnabled bool
|
|
cookieHeader bool
|
|
verifiedClaims *jwt.MapClaims
|
|
verifyTokenErr error
|
|
userInfoCacheClaims *jwt.MapClaims
|
|
expectedStatusCode int
|
|
expectedResponseBody *string
|
|
}
|
|
|
|
cases := []testCase{
|
|
{
|
|
name: "will authenticate successfully",
|
|
authDisabled: false,
|
|
ssoEnabled: false,
|
|
cookieHeader: true,
|
|
verifiedClaims: &jwt.MapClaims{},
|
|
verifyTokenErr: nil,
|
|
userInfoCacheClaims: nil,
|
|
expectedStatusCode: http.StatusOK,
|
|
expectedResponseBody: strPointer("{}"),
|
|
},
|
|
{
|
|
name: "will be noop if auth is disabled",
|
|
authDisabled: true,
|
|
ssoEnabled: false,
|
|
cookieHeader: false,
|
|
verifiedClaims: nil,
|
|
verifyTokenErr: nil,
|
|
userInfoCacheClaims: nil,
|
|
expectedStatusCode: http.StatusOK,
|
|
expectedResponseBody: strPointer("Ok"),
|
|
},
|
|
{
|
|
name: "will return 400 if no cookie header",
|
|
authDisabled: false,
|
|
ssoEnabled: false,
|
|
cookieHeader: false,
|
|
verifiedClaims: &jwt.MapClaims{},
|
|
verifyTokenErr: nil,
|
|
userInfoCacheClaims: nil,
|
|
expectedStatusCode: http.StatusBadRequest,
|
|
expectedResponseBody: nil,
|
|
},
|
|
{
|
|
name: "will return 401 verify token fails",
|
|
authDisabled: false,
|
|
ssoEnabled: false,
|
|
cookieHeader: true,
|
|
verifiedClaims: &jwt.MapClaims{},
|
|
verifyTokenErr: stderrors.New("token error"),
|
|
userInfoCacheClaims: nil,
|
|
expectedStatusCode: http.StatusUnauthorized,
|
|
expectedResponseBody: nil,
|
|
},
|
|
{
|
|
name: "will return 200 if claims are nil",
|
|
authDisabled: false,
|
|
ssoEnabled: false,
|
|
cookieHeader: true,
|
|
verifiedClaims: nil,
|
|
verifyTokenErr: nil,
|
|
userInfoCacheClaims: nil,
|
|
expectedStatusCode: http.StatusOK,
|
|
expectedResponseBody: strPointer("null"),
|
|
},
|
|
{
|
|
name: "will return 401 if sso is enabled but userinfo response not working",
|
|
authDisabled: false,
|
|
ssoEnabled: true,
|
|
cookieHeader: true,
|
|
verifiedClaims: nil,
|
|
verifyTokenErr: nil,
|
|
userInfoCacheClaims: nil, // indicates that the userinfo response will not work since cache is empty and userinfo endpoint not rechable
|
|
expectedStatusCode: http.StatusUnauthorized,
|
|
expectedResponseBody: strPointer("Invalid session"),
|
|
},
|
|
{
|
|
name: "will return 200 if sso is enabled and userinfo response from cache is valid",
|
|
authDisabled: false,
|
|
ssoEnabled: true,
|
|
cookieHeader: true,
|
|
verifiedClaims: &jwt.MapClaims{"sub": "randomUser", "exp": float64(time.Now().Add(5 * time.Minute).Unix())},
|
|
verifyTokenErr: nil,
|
|
userInfoCacheClaims: &jwt.MapClaims{"sub": "randomUser", "groups": []string{"superusers"}, "exp": float64(time.Now().Add(5 * time.Minute).Unix())},
|
|
expectedStatusCode: http.StatusOK,
|
|
expectedResponseBody: strPointer("\"groups\":[\"superusers\"]"),
|
|
},
|
|
}
|
|
for _, tc := range cases {
|
|
tc := tc
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
// given
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/", handlerFunc())
|
|
tm := &tokenVerifierMock{
|
|
claims: tc.verifiedClaims,
|
|
err: tc.verifyTokenErr,
|
|
}
|
|
clientApp := &oidc.ClientApp{} // all testcases need at least the empty struct for the function to work
|
|
if tc.ssoEnabled {
|
|
userInfoCache := cache.NewInMemoryCache(24 * time.Hour)
|
|
signature, err := util.MakeSignature(32)
|
|
require.NoError(t, err, "failed creating signature for settings object")
|
|
cdSettings := &settings.ArgoCDSettings{
|
|
ServerSignature: signature,
|
|
OIDCConfigRAW: `
|
|
issuer: http://localhost:63231
|
|
enableUserInfoGroups: true
|
|
userInfoPath: /`,
|
|
}
|
|
clientApp, err = oidc.NewClientApp(cdSettings, "", nil, "/argo-cd", userInfoCache)
|
|
require.NoError(t, err, "failed creating clientapp")
|
|
|
|
// prepopulate the cache with claims to return for a userinfo call
|
|
encryptionKey, err := cdSettings.GetServerEncryptionKey()
|
|
require.NoError(t, err, "failed obtaining encryption key from settings")
|
|
// set fake accessToken for GetUserInfo to not return early (can be the same for all cases)
|
|
encAccessToken, err := crypto.Encrypt([]byte("123456"), encryptionKey)
|
|
require.NoError(t, err, "failed encrypting dummy access token")
|
|
err = userInfoCache.Set(&cache.Item{
|
|
Key: oidc.FormatAccessTokenCacheKey("randomUser"),
|
|
Object: encAccessToken,
|
|
})
|
|
require.NoError(t, err, "failed setting item to in-memory cache")
|
|
|
|
// set cacheClaims to in-memory cache to let GetUserInfo return early with this information
|
|
if tc.userInfoCacheClaims != nil {
|
|
cacheClaims, err := json.Marshal(tc.userInfoCacheClaims)
|
|
require.NoError(t, err)
|
|
encCacheClaims, err := crypto.Encrypt([]byte(cacheClaims), encryptionKey)
|
|
require.NoError(t, err, "failed encrypting cache Claims")
|
|
err = userInfoCache.Set(&cache.Item{
|
|
Key: oidc.FormatUserInfoResponseCacheKey("randomUser"),
|
|
Object: encCacheClaims,
|
|
})
|
|
require.NoError(t, err, "failed setting item to in-memory cache")
|
|
}
|
|
}
|
|
ts := httptest.NewServer(WithAuthMiddleware(tc.authDisabled, tc.ssoEnabled, clientApp, tm, mux))
|
|
defer ts.Close()
|
|
req, err := http.NewRequestWithContext(t.Context(), http.MethodGet, ts.URL, http.NoBody)
|
|
require.NoErrorf(t, err, "error creating request: %s", err)
|
|
if tc.cookieHeader {
|
|
req.Header.Add("Cookie", "argocd.token=123456")
|
|
}
|
|
|
|
// when
|
|
resp, err := http.DefaultClient.Do(req)
|
|
|
|
// then
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, resp)
|
|
assert.Equal(t, tc.expectedStatusCode, resp.StatusCode)
|
|
if tc.expectedResponseBody != nil {
|
|
body, err := io.ReadAll(resp.Body)
|
|
require.NoError(t, err)
|
|
actual := strings.TrimSuffix(string(body), "\n")
|
|
assert.Contains(t, actual, *tc.expectedResponseBody)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
var (
|
|
loggedOutContext = context.Background()
|
|
//nolint:staticcheck
|
|
loggedInContext = context.WithValue(context.Background(), "claims", &jwt.MapClaims{"iss": "qux", "sub": "foo", "email": "bar", "groups": []string{"baz"}})
|
|
//nolint:staticcheck
|
|
loggedInContextFederated = context.WithValue(context.Background(), "claims", &jwt.MapClaims{"iss": "qux", "sub": "not-foo", "email": "bar", "groups": []string{"baz"}, "federated_claims": map[string]any{"user_id": "foo"}})
|
|
)
|
|
|
|
func TestIss(t *testing.T) {
|
|
assert.Empty(t, Iss(loggedOutContext))
|
|
assert.Equal(t, "qux", Iss(loggedInContext))
|
|
assert.Equal(t, "qux", Iss(loggedInContextFederated))
|
|
}
|
|
|
|
func TestLoggedIn(t *testing.T) {
|
|
assert.False(t, LoggedIn(loggedOutContext))
|
|
assert.True(t, LoggedIn(loggedInContext))
|
|
assert.True(t, LoggedIn(loggedInContextFederated))
|
|
}
|
|
|
|
func TestUsername(t *testing.T) {
|
|
assert.Empty(t, Username(loggedOutContext))
|
|
assert.Equal(t, "bar", Username(loggedInContext))
|
|
assert.Equal(t, "bar", Username(loggedInContextFederated))
|
|
}
|
|
|
|
func TestGetUserIdentifier(t *testing.T) {
|
|
assert.Empty(t, GetUserIdentifier(loggedOutContext))
|
|
assert.Equal(t, "foo", GetUserIdentifier(loggedInContext))
|
|
assert.Equal(t, "foo", GetUserIdentifier(loggedInContextFederated))
|
|
}
|
|
|
|
func TestGroups(t *testing.T) {
|
|
assert.Empty(t, Groups(loggedOutContext, []string{"groups"}))
|
|
assert.Equal(t, []string{"baz"}, Groups(loggedInContext, []string{"groups"}))
|
|
}
|
|
|
|
func TestVerifyUsernamePassword(t *testing.T) {
|
|
const password = "password"
|
|
|
|
for _, tc := range []struct {
|
|
name string
|
|
disabled bool
|
|
userName string
|
|
password string
|
|
expected error
|
|
}{
|
|
{
|
|
name: "Success if userName and password is correct",
|
|
disabled: false,
|
|
userName: common.ArgoCDAdminUsername,
|
|
password: password,
|
|
expected: nil,
|
|
},
|
|
{
|
|
name: "Return error if password is empty",
|
|
disabled: false,
|
|
userName: common.ArgoCDAdminUsername,
|
|
password: "",
|
|
expected: status.Errorf(codes.Unauthenticated, blankPasswordError),
|
|
},
|
|
{
|
|
name: "Return error if password is not correct",
|
|
disabled: false,
|
|
userName: common.ArgoCDAdminUsername,
|
|
password: "foo",
|
|
expected: status.Errorf(codes.Unauthenticated, invalidLoginError),
|
|
},
|
|
{
|
|
name: "Return error if disableAdmin is true",
|
|
disabled: true,
|
|
userName: common.ArgoCDAdminUsername,
|
|
password: password,
|
|
expected: status.Errorf(codes.Unauthenticated, accountDisabled, "admin"),
|
|
},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
settingsMgr := settings.NewSettingsManager(t.Context(), getKubeClient(t, password, !tc.disabled), "argocd")
|
|
|
|
mgr := newSessionManager(settingsMgr, getProjLister(), NewUserStateStorage(nil))
|
|
|
|
err := mgr.VerifyUsernamePassword(tc.userName, tc.password)
|
|
|
|
if tc.expected == nil {
|
|
require.NoError(t, err)
|
|
} else {
|
|
assert.EqualError(t, err, tc.expected.Error())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCacheValueGetters(t *testing.T) {
|
|
t.Run("Default values", func(t *testing.T) {
|
|
mlf := getMaxLoginFailures()
|
|
assert.Equal(t, defaultMaxLoginFailures, mlf)
|
|
|
|
mcs := getMaximumCacheSize()
|
|
assert.Equal(t, defaultMaxCacheSize, mcs)
|
|
})
|
|
|
|
t.Run("Valid environment overrides", func(t *testing.T) {
|
|
t.Setenv(envLoginMaxFailCount, "5")
|
|
t.Setenv(envLoginMaxCacheSize, "5")
|
|
|
|
mlf := getMaxLoginFailures()
|
|
assert.Equal(t, 5, mlf)
|
|
|
|
mcs := getMaximumCacheSize()
|
|
assert.Equal(t, 5, mcs)
|
|
})
|
|
|
|
t.Run("Invalid environment overrides", func(t *testing.T) {
|
|
t.Setenv(envLoginMaxFailCount, "invalid")
|
|
t.Setenv(envLoginMaxCacheSize, "invalid")
|
|
|
|
mlf := getMaxLoginFailures()
|
|
assert.Equal(t, defaultMaxLoginFailures, mlf)
|
|
|
|
mcs := getMaximumCacheSize()
|
|
assert.Equal(t, defaultMaxCacheSize, mcs)
|
|
})
|
|
|
|
t.Run("Less than allowed in environment overrides", func(t *testing.T) {
|
|
t.Setenv(envLoginMaxFailCount, "-1")
|
|
t.Setenv(envLoginMaxCacheSize, "-1")
|
|
|
|
mlf := getMaxLoginFailures()
|
|
assert.Equal(t, defaultMaxLoginFailures, mlf)
|
|
|
|
mcs := getMaximumCacheSize()
|
|
assert.Equal(t, defaultMaxCacheSize, mcs)
|
|
})
|
|
|
|
t.Run("Greater than allowed in environment overrides", func(t *testing.T) {
|
|
t.Setenv(envLoginMaxFailCount, strconv.Itoa(math.MaxInt32+1))
|
|
t.Setenv(envLoginMaxCacheSize, strconv.Itoa(math.MaxInt32+1))
|
|
|
|
mlf := getMaxLoginFailures()
|
|
assert.Equal(t, defaultMaxLoginFailures, mlf)
|
|
|
|
mcs := getMaximumCacheSize()
|
|
assert.Equal(t, defaultMaxCacheSize, mcs)
|
|
})
|
|
}
|
|
|
|
func TestLoginRateLimiter(t *testing.T) {
|
|
settingsMgr := settings.NewSettingsManager(t.Context(), getKubeClient(t, "password", true), "argocd")
|
|
storage := NewUserStateStorage(nil)
|
|
|
|
mgr := newSessionManager(settingsMgr, getProjLister(), storage)
|
|
|
|
t.Run("Test login delay valid user", func(t *testing.T) {
|
|
for i := 0; i < getMaxLoginFailures(); i++ {
|
|
err := mgr.VerifyUsernamePassword("admin", "wrong")
|
|
require.Error(t, err)
|
|
}
|
|
|
|
// The 11th time should fail even if password is right
|
|
{
|
|
err := mgr.VerifyUsernamePassword("admin", "password")
|
|
require.Error(t, err)
|
|
}
|
|
|
|
storage.attempts = map[string]LoginAttempts{}
|
|
// Failed counter should have been reset, should validate immediately
|
|
{
|
|
err := mgr.VerifyUsernamePassword("admin", "password")
|
|
require.NoError(t, err)
|
|
}
|
|
})
|
|
|
|
t.Run("Test login delay invalid user", func(t *testing.T) {
|
|
for i := 0; i < getMaxLoginFailures(); i++ {
|
|
err := mgr.VerifyUsernamePassword("invalid", "wrong")
|
|
require.Error(t, err)
|
|
}
|
|
|
|
err := mgr.VerifyUsernamePassword("invalid", "wrong")
|
|
require.Error(t, err)
|
|
})
|
|
}
|
|
|
|
func TestMaxUsernameLength(t *testing.T) {
|
|
username := ""
|
|
for i := 0; i < maxUsernameLength+1; i++ {
|
|
username += "a"
|
|
}
|
|
settingsMgr := settings.NewSettingsManager(t.Context(), getKubeClient(t, "password", true), "argocd")
|
|
mgr := newSessionManager(settingsMgr, getProjLister(), NewUserStateStorage(nil))
|
|
err := mgr.VerifyUsernamePassword(username, "password")
|
|
assert.ErrorContains(t, err, fmt.Sprintf(usernameTooLongError, maxUsernameLength))
|
|
}
|
|
|
|
func TestMaxCacheSize(t *testing.T) {
|
|
settingsMgr := settings.NewSettingsManager(t.Context(), getKubeClient(t, "password", true), "argocd")
|
|
mgr := newSessionManager(settingsMgr, getProjLister(), NewUserStateStorage(nil))
|
|
|
|
invalidUsers := []string{"invalid1", "invalid2", "invalid3", "invalid4", "invalid5", "invalid6", "invalid7"}
|
|
// Temporarily decrease max cache size
|
|
t.Setenv(envLoginMaxCacheSize, "5")
|
|
|
|
for _, user := range invalidUsers {
|
|
err := mgr.VerifyUsernamePassword(user, "password")
|
|
require.Error(t, err)
|
|
}
|
|
|
|
assert.Len(t, mgr.GetLoginFailures(), 5)
|
|
}
|
|
|
|
func TestFailedAttemptsExpiry(t *testing.T) {
|
|
settingsMgr := settings.NewSettingsManager(t.Context(), getKubeClient(t, "password", true), "argocd")
|
|
mgr := newSessionManager(settingsMgr, getProjLister(), NewUserStateStorage(nil))
|
|
|
|
invalidUsers := []string{"invalid1", "invalid2", "invalid3", "invalid4", "invalid5", "invalid6", "invalid7"}
|
|
|
|
t.Setenv(envLoginFailureWindowSeconds, "1")
|
|
|
|
for _, user := range invalidUsers {
|
|
err := mgr.VerifyUsernamePassword(user, "password")
|
|
require.Error(t, err)
|
|
}
|
|
|
|
time.Sleep(2 * time.Second)
|
|
|
|
err := mgr.VerifyUsernamePassword("invalid8", "password")
|
|
require.Error(t, err)
|
|
assert.Len(t, mgr.GetLoginFailures(), 1)
|
|
}
|
|
|
|
func getKubeClientWithConfig(config map[string]string, secretConfig map[string][]byte) *fake.Clientset {
|
|
mergedSecretConfig := map[string][]byte{
|
|
"server.secretkey": []byte("Hello, world!"),
|
|
}
|
|
for key, value := range secretConfig {
|
|
mergedSecretConfig[key] = value
|
|
}
|
|
|
|
return fake.NewClientset(&corev1.ConfigMap{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "argocd-cm",
|
|
Namespace: "argocd",
|
|
Labels: map[string]string{
|
|
"app.kubernetes.io/part-of": "argocd",
|
|
},
|
|
},
|
|
Data: config,
|
|
}, &corev1.Secret{
|
|
ObjectMeta: metav1.ObjectMeta{
|
|
Name: "argocd-secret",
|
|
Namespace: "argocd",
|
|
},
|
|
Data: mergedSecretConfig,
|
|
})
|
|
}
|
|
|
|
func TestSessionManager_VerifyToken(t *testing.T) {
|
|
oidcTestServer := utiltest.GetOIDCTestServer(t, nil)
|
|
t.Cleanup(oidcTestServer.Close)
|
|
|
|
dexTestServer := utiltest.GetDexTestServer(t)
|
|
t.Cleanup(dexTestServer.Close)
|
|
|
|
t.Run("RS512 is supported", func(t *testing.T) {
|
|
dexConfig := map[string]string{
|
|
"url": "",
|
|
"oidc.config": fmt.Sprintf(`
|
|
name: Test
|
|
issuer: %s
|
|
clientID: xxx
|
|
clientSecret: yyy
|
|
requestedScopes: ["oidc"]`, oidcTestServer.URL),
|
|
}
|
|
|
|
settingsMgr := settings.NewSettingsManager(t.Context(), getKubeClientWithConfig(dexConfig, nil), "argocd")
|
|
mgr := NewSessionManager(settingsMgr, getProjLister(), "", nil, NewUserStateStorage(nil))
|
|
mgr.verificationDelayNoiseEnabled = false
|
|
// Use test server's client to avoid TLS issues.
|
|
mgr.client = oidcTestServer.Client()
|
|
|
|
claims := jwt.RegisteredClaims{Audience: jwt.ClaimStrings{"test-client"}, Subject: "admin", ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24))}
|
|
claims.Issuer = oidcTestServer.URL
|
|
token := jwt.NewWithClaims(jwt.SigningMethodRS512, claims)
|
|
key, err := jwt.ParseRSAPrivateKeyFromPEM(utiltest.PrivateKey)
|
|
require.NoError(t, err)
|
|
tokenString, err := token.SignedString(key)
|
|
require.NoError(t, err)
|
|
|
|
_, _, err = mgr.VerifyToken(tokenString)
|
|
assert.NotContains(t, err.Error(), "oidc: id token signed with unsupported algorithm")
|
|
})
|
|
|
|
t.Run("oidcConfig.rootCA is respected", func(t *testing.T) {
|
|
cert := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: oidcTestServer.TLS.Certificates[0].Certificate[0]})
|
|
|
|
dexConfig := map[string]string{
|
|
"url": "",
|
|
"oidc.config": fmt.Sprintf(`
|
|
name: Test
|
|
issuer: %s
|
|
clientID: xxx
|
|
clientSecret: yyy
|
|
requestedScopes: ["oidc"]
|
|
rootCA: |
|
|
%s
|
|
`, oidcTestServer.URL, strings.ReplaceAll(string(cert), "\n", "\n ")),
|
|
}
|
|
|
|
settingsMgr := settings.NewSettingsManager(t.Context(), getKubeClientWithConfig(dexConfig, nil), "argocd")
|
|
mgr := NewSessionManager(settingsMgr, getProjLister(), "", nil, NewUserStateStorage(nil))
|
|
mgr.verificationDelayNoiseEnabled = false
|
|
|
|
claims := jwt.RegisteredClaims{Audience: jwt.ClaimStrings{"test-client"}, Subject: "admin", ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24))}
|
|
claims.Issuer = oidcTestServer.URL
|
|
token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)
|
|
key, err := jwt.ParseRSAPrivateKeyFromPEM(utiltest.PrivateKey)
|
|
require.NoError(t, err)
|
|
tokenString, err := token.SignedString(key)
|
|
require.NoError(t, err)
|
|
|
|
_, _, err = mgr.VerifyToken(tokenString)
|
|
// If the root CA is being respected, we won't get this error. The error message is environment-dependent, so
|
|
// we check for either of the error messages associated with a failed cert check.
|
|
assert.NotContains(t, err.Error(), "certificate is not trusted")
|
|
assert.NotContains(t, err.Error(), "certificate signed by unknown authority")
|
|
})
|
|
|
|
t.Run("OIDC provider is Dex, TLS is configured", func(t *testing.T) {
|
|
dexConfig := map[string]string{
|
|
"url": dexTestServer.URL,
|
|
"dex.config": `connectors:
|
|
- type: github
|
|
name: GitHub
|
|
config:
|
|
clientID: aabbccddeeff00112233
|
|
clientSecret: aabbccddeeff00112233`,
|
|
}
|
|
|
|
// This is not actually used in the test. The test only calls the OIDC test server. But a valid cert/key pair
|
|
// must be set to test VerifyToken's behavior when Argo CD is configured with TLS enabled.
|
|
secretConfig := map[string][]byte{
|
|
"tls.crt": utiltest.Cert,
|
|
"tls.key": utiltest.PrivateKey,
|
|
}
|
|
|
|
settingsMgr := settings.NewSettingsManager(t.Context(), getKubeClientWithConfig(dexConfig, secretConfig), "argocd")
|
|
mgr := NewSessionManager(settingsMgr, getProjLister(), dexTestServer.URL, nil, NewUserStateStorage(nil))
|
|
mgr.verificationDelayNoiseEnabled = false
|
|
|
|
claims := jwt.RegisteredClaims{Audience: jwt.ClaimStrings{"test-client"}, Subject: "admin", ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24))}
|
|
claims.Issuer = dexTestServer.URL + "/api/dex"
|
|
token := jwt.NewWithClaims(jwt.SigningMethodRS512, claims)
|
|
key, err := jwt.ParseRSAPrivateKeyFromPEM(utiltest.PrivateKey)
|
|
require.NoError(t, err)
|
|
tokenString, err := token.SignedString(key)
|
|
require.NoError(t, err)
|
|
|
|
_, _, err = mgr.VerifyToken(tokenString)
|
|
require.Error(t, err)
|
|
assert.ErrorIs(t, err, common.ErrTokenVerification)
|
|
})
|
|
|
|
t.Run("OIDC provider is external, TLS is configured", func(t *testing.T) {
|
|
dexConfig := map[string]string{
|
|
"url": "",
|
|
"oidc.config": fmt.Sprintf(`
|
|
name: Test
|
|
issuer: %s
|
|
clientID: xxx
|
|
clientSecret: yyy
|
|
requestedScopes: ["oidc"]`, oidcTestServer.URL),
|
|
}
|
|
|
|
// This is not actually used in the test. The test only calls the OIDC test server. But a valid cert/key pair
|
|
// must be set to test VerifyToken's behavior when Argo CD is configured with TLS enabled.
|
|
secretConfig := map[string][]byte{
|
|
"tls.crt": utiltest.Cert,
|
|
"tls.key": utiltest.PrivateKey,
|
|
}
|
|
|
|
settingsMgr := settings.NewSettingsManager(t.Context(), getKubeClientWithConfig(dexConfig, secretConfig), "argocd")
|
|
mgr := NewSessionManager(settingsMgr, getProjLister(), "", nil, NewUserStateStorage(nil))
|
|
mgr.verificationDelayNoiseEnabled = false
|
|
|
|
claims := jwt.RegisteredClaims{Audience: jwt.ClaimStrings{"test-client"}, Subject: "admin", ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24))}
|
|
claims.Issuer = oidcTestServer.URL
|
|
token := jwt.NewWithClaims(jwt.SigningMethodRS512, claims)
|
|
key, err := jwt.ParseRSAPrivateKeyFromPEM(utiltest.PrivateKey)
|
|
require.NoError(t, err)
|
|
tokenString, err := token.SignedString(key)
|
|
require.NoError(t, err)
|
|
|
|
_, _, err = mgr.VerifyToken(tokenString)
|
|
require.Error(t, err)
|
|
assert.ErrorIs(t, err, common.ErrTokenVerification)
|
|
})
|
|
|
|
t.Run("OIDC provider is Dex, TLS is configured", func(t *testing.T) {
|
|
dexConfig := map[string]string{
|
|
"url": dexTestServer.URL,
|
|
"dex.config": `connectors:
|
|
- type: github
|
|
name: GitHub
|
|
config:
|
|
clientID: aabbccddeeff00112233
|
|
clientSecret: aabbccddeeff00112233`,
|
|
}
|
|
|
|
// This is not actually used in the test. The test only calls the OIDC test server. But a valid cert/key pair
|
|
// must be set to test VerifyToken's behavior when Argo CD is configured with TLS enabled.
|
|
secretConfig := map[string][]byte{
|
|
"tls.crt": utiltest.Cert,
|
|
"tls.key": utiltest.PrivateKey,
|
|
}
|
|
|
|
settingsMgr := settings.NewSettingsManager(t.Context(), getKubeClientWithConfig(dexConfig, secretConfig), "argocd")
|
|
mgr := NewSessionManager(settingsMgr, getProjLister(), dexTestServer.URL, nil, NewUserStateStorage(nil))
|
|
mgr.verificationDelayNoiseEnabled = false
|
|
|
|
claims := jwt.RegisteredClaims{Audience: jwt.ClaimStrings{"test-client"}, Subject: "admin", ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24))}
|
|
claims.Issuer = dexTestServer.URL + "/api/dex"
|
|
token := jwt.NewWithClaims(jwt.SigningMethodRS512, claims)
|
|
key, err := jwt.ParseRSAPrivateKeyFromPEM(utiltest.PrivateKey)
|
|
require.NoError(t, err)
|
|
tokenString, err := token.SignedString(key)
|
|
require.NoError(t, err)
|
|
|
|
_, _, err = mgr.VerifyToken(tokenString)
|
|
require.Error(t, err)
|
|
assert.ErrorIs(t, err, common.ErrTokenVerification)
|
|
})
|
|
|
|
t.Run("OIDC provider is external, TLS is configured, OIDCTLSInsecureSkipVerify is true", func(t *testing.T) {
|
|
dexConfig := map[string]string{
|
|
"url": "",
|
|
"oidc.config": fmt.Sprintf(`
|
|
name: Test
|
|
issuer: %s
|
|
clientID: xxx
|
|
clientSecret: yyy
|
|
requestedScopes: ["oidc"]`, oidcTestServer.URL),
|
|
"oidc.tls.insecure.skip.verify": "true",
|
|
}
|
|
|
|
// This is not actually used in the test. The test only calls the OIDC test server. But a valid cert/key pair
|
|
// must be set to test VerifyToken's behavior when Argo CD is configured with TLS enabled.
|
|
secretConfig := map[string][]byte{
|
|
"tls.crt": utiltest.Cert,
|
|
"tls.key": utiltest.PrivateKey,
|
|
}
|
|
|
|
settingsMgr := settings.NewSettingsManager(t.Context(), getKubeClientWithConfig(dexConfig, secretConfig), "argocd")
|
|
mgr := NewSessionManager(settingsMgr, getProjLister(), "", nil, NewUserStateStorage(nil))
|
|
mgr.verificationDelayNoiseEnabled = false
|
|
|
|
claims := jwt.RegisteredClaims{Audience: jwt.ClaimStrings{"test-client"}, Subject: "admin", ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24))}
|
|
claims.Issuer = oidcTestServer.URL
|
|
token := jwt.NewWithClaims(jwt.SigningMethodRS512, claims)
|
|
key, err := jwt.ParseRSAPrivateKeyFromPEM(utiltest.PrivateKey)
|
|
require.NoError(t, err)
|
|
tokenString, err := token.SignedString(key)
|
|
require.NoError(t, err)
|
|
|
|
_, _, err = mgr.VerifyToken(tokenString)
|
|
assert.NotContains(t, err.Error(), "certificate is not trusted")
|
|
assert.NotContains(t, err.Error(), "certificate signed by unknown authority")
|
|
})
|
|
|
|
t.Run("OIDC provider is external, TLS is not configured, OIDCTLSInsecureSkipVerify is true", func(t *testing.T) {
|
|
dexConfig := map[string]string{
|
|
"url": "",
|
|
"oidc.config": fmt.Sprintf(`
|
|
name: Test
|
|
issuer: %s
|
|
clientID: xxx
|
|
clientSecret: yyy
|
|
requestedScopes: ["oidc"]`, oidcTestServer.URL),
|
|
"oidc.tls.insecure.skip.verify": "true",
|
|
}
|
|
|
|
settingsMgr := settings.NewSettingsManager(t.Context(), getKubeClientWithConfig(dexConfig, nil), "argocd")
|
|
mgr := NewSessionManager(settingsMgr, getProjLister(), "", nil, NewUserStateStorage(nil))
|
|
mgr.verificationDelayNoiseEnabled = false
|
|
|
|
claims := jwt.RegisteredClaims{Audience: jwt.ClaimStrings{"test-client"}, Subject: "admin", ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24))}
|
|
claims.Issuer = oidcTestServer.URL
|
|
token := jwt.NewWithClaims(jwt.SigningMethodRS512, claims)
|
|
key, err := jwt.ParseRSAPrivateKeyFromPEM(utiltest.PrivateKey)
|
|
require.NoError(t, err)
|
|
tokenString, err := token.SignedString(key)
|
|
require.NoError(t, err)
|
|
|
|
_, _, err = mgr.VerifyToken(tokenString)
|
|
// This is the error thrown when the test server's certificate _is_ being verified.
|
|
assert.NotContains(t, err.Error(), "certificate is not trusted")
|
|
assert.NotContains(t, err.Error(), "certificate signed by unknown authority")
|
|
})
|
|
|
|
t.Run("OIDC provider is external, audience is not specified", func(t *testing.T) {
|
|
config := map[string]string{
|
|
"url": "",
|
|
"oidc.config": fmt.Sprintf(`
|
|
name: Test
|
|
issuer: %s
|
|
clientID: xxx
|
|
clientSecret: yyy
|
|
requestedScopes: ["oidc"]`, oidcTestServer.URL),
|
|
"oidc.tls.insecure.skip.verify": "true", // This isn't what we're testing.
|
|
}
|
|
|
|
// This is not actually used in the test. The test only calls the OIDC test server. But a valid cert/key pair
|
|
// must be set to test VerifyToken's behavior when Argo CD is configured with TLS enabled.
|
|
secretConfig := map[string][]byte{
|
|
"tls.crt": utiltest.Cert,
|
|
"tls.key": utiltest.PrivateKey,
|
|
}
|
|
|
|
settingsMgr := settings.NewSettingsManager(t.Context(), getKubeClientWithConfig(config, secretConfig), "argocd")
|
|
mgr := NewSessionManager(settingsMgr, getProjLister(), "", nil, NewUserStateStorage(nil))
|
|
mgr.verificationDelayNoiseEnabled = false
|
|
|
|
claims := jwt.RegisteredClaims{Subject: "admin", ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24))}
|
|
claims.Issuer = oidcTestServer.URL
|
|
token := jwt.NewWithClaims(jwt.SigningMethodRS512, claims)
|
|
key, err := jwt.ParseRSAPrivateKeyFromPEM(utiltest.PrivateKey)
|
|
require.NoError(t, err)
|
|
tokenString, err := token.SignedString(key)
|
|
require.NoError(t, err)
|
|
|
|
_, _, err = mgr.VerifyToken(tokenString)
|
|
require.Error(t, err)
|
|
})
|
|
|
|
t.Run("OIDC provider is external, audience is not specified, absent audience is allowed", func(t *testing.T) {
|
|
config := map[string]string{
|
|
"url": "",
|
|
"oidc.config": fmt.Sprintf(`
|
|
name: Test
|
|
issuer: %s
|
|
clientID: xxx
|
|
clientSecret: yyy
|
|
requestedScopes: ["oidc"]
|
|
skipAudienceCheckWhenTokenHasNoAudience: true`, oidcTestServer.URL),
|
|
"oidc.tls.insecure.skip.verify": "true", // This isn't what we're testing.
|
|
}
|
|
|
|
// This is not actually used in the test. The test only calls the OIDC test server. But a valid cert/key pair
|
|
// must be set to test VerifyToken's behavior when Argo CD is configured with TLS enabled.
|
|
secretConfig := map[string][]byte{
|
|
"tls.crt": utiltest.Cert,
|
|
"tls.key": utiltest.PrivateKey,
|
|
}
|
|
|
|
settingsMgr := settings.NewSettingsManager(t.Context(), getKubeClientWithConfig(config, secretConfig), "argocd")
|
|
mgr := NewSessionManager(settingsMgr, getProjLister(), "", nil, NewUserStateStorage(nil))
|
|
mgr.verificationDelayNoiseEnabled = false
|
|
|
|
claims := jwt.RegisteredClaims{Subject: "admin", ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24))}
|
|
claims.Issuer = oidcTestServer.URL
|
|
token := jwt.NewWithClaims(jwt.SigningMethodRS512, claims)
|
|
key, err := jwt.ParseRSAPrivateKeyFromPEM(utiltest.PrivateKey)
|
|
require.NoError(t, err)
|
|
tokenString, err := token.SignedString(key)
|
|
require.NoError(t, err)
|
|
|
|
_, _, err = mgr.VerifyToken(tokenString)
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
t.Run("OIDC provider is external, audience is not specified but is required", func(t *testing.T) {
|
|
config := map[string]string{
|
|
"url": "",
|
|
"oidc.config": fmt.Sprintf(`
|
|
name: Test
|
|
issuer: %s
|
|
clientID: xxx
|
|
clientSecret: yyy
|
|
requestedScopes: ["oidc"]
|
|
skipAudienceCheckWhenTokenHasNoAudience: false`, oidcTestServer.URL),
|
|
"oidc.tls.insecure.skip.verify": "true", // This isn't what we're testing.
|
|
}
|
|
|
|
// This is not actually used in the test. The test only calls the OIDC test server. But a valid cert/key pair
|
|
// must be set to test VerifyToken's behavior when Argo CD is configured with TLS enabled.
|
|
secretConfig := map[string][]byte{
|
|
"tls.crt": utiltest.Cert,
|
|
"tls.key": utiltest.PrivateKey,
|
|
}
|
|
|
|
settingsMgr := settings.NewSettingsManager(t.Context(), getKubeClientWithConfig(config, secretConfig), "argocd")
|
|
mgr := NewSessionManager(settingsMgr, getProjLister(), "", nil, NewUserStateStorage(nil))
|
|
mgr.verificationDelayNoiseEnabled = false
|
|
|
|
claims := jwt.RegisteredClaims{Subject: "admin", ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24))}
|
|
claims.Issuer = oidcTestServer.URL
|
|
token := jwt.NewWithClaims(jwt.SigningMethodRS512, claims)
|
|
key, err := jwt.ParseRSAPrivateKeyFromPEM(utiltest.PrivateKey)
|
|
require.NoError(t, err)
|
|
tokenString, err := token.SignedString(key)
|
|
require.NoError(t, err)
|
|
|
|
_, _, err = mgr.VerifyToken(tokenString)
|
|
require.Error(t, err)
|
|
assert.ErrorIs(t, err, common.ErrTokenVerification)
|
|
})
|
|
|
|
t.Run("OIDC provider is external, audience is client ID, no allowed list specified", func(t *testing.T) {
|
|
config := map[string]string{
|
|
"url": "",
|
|
"oidc.config": fmt.Sprintf(`
|
|
name: Test
|
|
issuer: %s
|
|
clientID: xxx
|
|
clientSecret: yyy
|
|
requestedScopes: ["oidc"]`, oidcTestServer.URL),
|
|
"oidc.tls.insecure.skip.verify": "true", // This isn't what we're testing.
|
|
}
|
|
|
|
// This is not actually used in the test. The test only calls the OIDC test server. But a valid cert/key pair
|
|
// must be set to test VerifyToken's behavior when Argo CD is configured with TLS enabled.
|
|
secretConfig := map[string][]byte{
|
|
"tls.crt": utiltest.Cert,
|
|
"tls.key": utiltest.PrivateKey,
|
|
}
|
|
|
|
settingsMgr := settings.NewSettingsManager(t.Context(), getKubeClientWithConfig(config, secretConfig), "argocd")
|
|
mgr := NewSessionManager(settingsMgr, getProjLister(), "", nil, NewUserStateStorage(nil))
|
|
mgr.verificationDelayNoiseEnabled = false
|
|
|
|
claims := jwt.RegisteredClaims{Audience: jwt.ClaimStrings{"xxx"}, Subject: "admin", ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24))}
|
|
claims.Issuer = oidcTestServer.URL
|
|
token := jwt.NewWithClaims(jwt.SigningMethodRS512, claims)
|
|
key, err := jwt.ParseRSAPrivateKeyFromPEM(utiltest.PrivateKey)
|
|
require.NoError(t, err)
|
|
tokenString, err := token.SignedString(key)
|
|
require.NoError(t, err)
|
|
|
|
_, _, err = mgr.VerifyToken(tokenString)
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
t.Run("OIDC provider is external, audience is in allowed list", func(t *testing.T) {
|
|
config := map[string]string{
|
|
"url": "",
|
|
"oidc.config": fmt.Sprintf(`
|
|
name: Test
|
|
issuer: %s
|
|
clientID: xxx
|
|
clientSecret: yyy
|
|
requestedScopes: ["oidc"]
|
|
allowedAudiences:
|
|
- something`, oidcTestServer.URL),
|
|
"oidc.tls.insecure.skip.verify": "true", // This isn't what we're testing.
|
|
}
|
|
|
|
// This is not actually used in the test. The test only calls the OIDC test server. But a valid cert/key pair
|
|
// must be set to test VerifyToken's behavior when Argo CD is configured with TLS enabled.
|
|
secretConfig := map[string][]byte{
|
|
"tls.crt": utiltest.Cert,
|
|
"tls.key": utiltest.PrivateKey,
|
|
}
|
|
|
|
settingsMgr := settings.NewSettingsManager(t.Context(), getKubeClientWithConfig(config, secretConfig), "argocd")
|
|
mgr := NewSessionManager(settingsMgr, getProjLister(), "", nil, NewUserStateStorage(nil))
|
|
mgr.verificationDelayNoiseEnabled = false
|
|
|
|
claims := jwt.RegisteredClaims{Audience: jwt.ClaimStrings{"something"}, Subject: "admin", ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24))}
|
|
claims.Issuer = oidcTestServer.URL
|
|
token := jwt.NewWithClaims(jwt.SigningMethodRS512, claims)
|
|
key, err := jwt.ParseRSAPrivateKeyFromPEM(utiltest.PrivateKey)
|
|
require.NoError(t, err)
|
|
tokenString, err := token.SignedString(key)
|
|
require.NoError(t, err)
|
|
|
|
_, _, err = mgr.VerifyToken(tokenString)
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
t.Run("OIDC provider is external, audience is not in allowed list", func(t *testing.T) {
|
|
config := map[string]string{
|
|
"url": "",
|
|
"oidc.config": fmt.Sprintf(`
|
|
name: Test
|
|
issuer: %s
|
|
clientID: xxx
|
|
clientSecret: yyy
|
|
requestedScopes: ["oidc"]
|
|
allowedAudiences:
|
|
- something-else`, oidcTestServer.URL),
|
|
"oidc.tls.insecure.skip.verify": "true", // This isn't what we're testing.
|
|
}
|
|
|
|
// This is not actually used in the test. The test only calls the OIDC test server. But a valid cert/key pair
|
|
// must be set to test VerifyToken's behavior when Argo CD is configured with TLS enabled.
|
|
secretConfig := map[string][]byte{
|
|
"tls.crt": utiltest.Cert,
|
|
"tls.key": utiltest.PrivateKey,
|
|
}
|
|
|
|
settingsMgr := settings.NewSettingsManager(t.Context(), getKubeClientWithConfig(config, secretConfig), "argocd")
|
|
mgr := NewSessionManager(settingsMgr, getProjLister(), "", nil, NewUserStateStorage(nil))
|
|
mgr.verificationDelayNoiseEnabled = false
|
|
|
|
claims := jwt.RegisteredClaims{Audience: jwt.ClaimStrings{"something"}, Subject: "admin", ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24))}
|
|
claims.Issuer = oidcTestServer.URL
|
|
token := jwt.NewWithClaims(jwt.SigningMethodRS512, claims)
|
|
key, err := jwt.ParseRSAPrivateKeyFromPEM(utiltest.PrivateKey)
|
|
require.NoError(t, err)
|
|
tokenString, err := token.SignedString(key)
|
|
require.NoError(t, err)
|
|
|
|
_, _, err = mgr.VerifyToken(tokenString)
|
|
require.Error(t, err)
|
|
assert.ErrorIs(t, err, common.ErrTokenVerification)
|
|
})
|
|
|
|
t.Run("OIDC provider is external, audience is not client ID, and there is no allow list", func(t *testing.T) {
|
|
config := map[string]string{
|
|
"url": "",
|
|
"oidc.config": fmt.Sprintf(`
|
|
name: Test
|
|
issuer: %s
|
|
clientID: xxx
|
|
clientSecret: yyy
|
|
requestedScopes: ["oidc"]`, oidcTestServer.URL),
|
|
"oidc.tls.insecure.skip.verify": "true", // This isn't what we're testing.
|
|
}
|
|
|
|
// This is not actually used in the test. The test only calls the OIDC test server. But a valid cert/key pair
|
|
// must be set to test VerifyToken's behavior when Argo CD is configured with TLS enabled.
|
|
secretConfig := map[string][]byte{
|
|
"tls.crt": utiltest.Cert,
|
|
"tls.key": utiltest.PrivateKey,
|
|
}
|
|
|
|
settingsMgr := settings.NewSettingsManager(t.Context(), getKubeClientWithConfig(config, secretConfig), "argocd")
|
|
mgr := NewSessionManager(settingsMgr, getProjLister(), "", nil, NewUserStateStorage(nil))
|
|
mgr.verificationDelayNoiseEnabled = false
|
|
|
|
claims := jwt.RegisteredClaims{Audience: jwt.ClaimStrings{"something"}, Subject: "admin", ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24))}
|
|
claims.Issuer = oidcTestServer.URL
|
|
token := jwt.NewWithClaims(jwt.SigningMethodRS512, claims)
|
|
key, err := jwt.ParseRSAPrivateKeyFromPEM(utiltest.PrivateKey)
|
|
require.NoError(t, err)
|
|
tokenString, err := token.SignedString(key)
|
|
require.NoError(t, err)
|
|
|
|
_, _, err = mgr.VerifyToken(tokenString)
|
|
require.Error(t, err)
|
|
assert.ErrorIs(t, err, common.ErrTokenVerification)
|
|
})
|
|
|
|
t.Run("OIDC provider is external, audience is specified, but allow list is empty", func(t *testing.T) {
|
|
config := map[string]string{
|
|
"url": "",
|
|
"oidc.config": fmt.Sprintf(`
|
|
name: Test
|
|
issuer: %s
|
|
clientID: xxx
|
|
clientSecret: yyy
|
|
requestedScopes: ["oidc"]
|
|
allowedAudiences: []`, oidcTestServer.URL),
|
|
"oidc.tls.insecure.skip.verify": "true", // This isn't what we're testing.
|
|
}
|
|
|
|
// This is not actually used in the test. The test only calls the OIDC test server. But a valid cert/key pair
|
|
// must be set to test VerifyToken's behavior when Argo CD is configured with TLS enabled.
|
|
secretConfig := map[string][]byte{
|
|
"tls.crt": utiltest.Cert,
|
|
"tls.key": utiltest.PrivateKey,
|
|
}
|
|
|
|
settingsMgr := settings.NewSettingsManager(t.Context(), getKubeClientWithConfig(config, secretConfig), "argocd")
|
|
mgr := NewSessionManager(settingsMgr, getProjLister(), "", nil, NewUserStateStorage(nil))
|
|
mgr.verificationDelayNoiseEnabled = false
|
|
|
|
claims := jwt.RegisteredClaims{Audience: jwt.ClaimStrings{"something"}, Subject: "admin", ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24))}
|
|
claims.Issuer = oidcTestServer.URL
|
|
token := jwt.NewWithClaims(jwt.SigningMethodRS512, claims)
|
|
key, err := jwt.ParseRSAPrivateKeyFromPEM(utiltest.PrivateKey)
|
|
require.NoError(t, err)
|
|
tokenString, err := token.SignedString(key)
|
|
require.NoError(t, err)
|
|
|
|
_, _, err = mgr.VerifyToken(tokenString)
|
|
require.Error(t, err)
|
|
assert.ErrorIs(t, err, common.ErrTokenVerification)
|
|
})
|
|
|
|
// Make sure the logic works to allow any of the allowed audiences, not just the first one.
|
|
t.Run("OIDC provider is external, audience is specified, actual audience isn't the first allowed audience", func(t *testing.T) {
|
|
config := map[string]string{
|
|
"url": "",
|
|
"oidc.config": fmt.Sprintf(`
|
|
name: Test
|
|
issuer: %s
|
|
clientID: xxx
|
|
clientSecret: yyy
|
|
requestedScopes: ["oidc"]
|
|
allowedAudiences: ["aud-a", "aud-b"]`, oidcTestServer.URL),
|
|
"oidc.tls.insecure.skip.verify": "true", // This isn't what we're testing.
|
|
}
|
|
|
|
// This is not actually used in the test. The test only calls the OIDC test server. But a valid cert/key pair
|
|
// must be set to test VerifyToken's behavior when Argo CD is configured with TLS enabled.
|
|
secretConfig := map[string][]byte{
|
|
"tls.crt": utiltest.Cert,
|
|
"tls.key": utiltest.PrivateKey,
|
|
}
|
|
|
|
settingsMgr := settings.NewSettingsManager(t.Context(), getKubeClientWithConfig(config, secretConfig), "argocd")
|
|
mgr := NewSessionManager(settingsMgr, getProjLister(), "", nil, NewUserStateStorage(nil))
|
|
mgr.verificationDelayNoiseEnabled = false
|
|
|
|
claims := jwt.RegisteredClaims{Audience: jwt.ClaimStrings{"aud-b"}, Subject: "admin", ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24))}
|
|
claims.Issuer = oidcTestServer.URL
|
|
token := jwt.NewWithClaims(jwt.SigningMethodRS512, claims)
|
|
key, err := jwt.ParseRSAPrivateKeyFromPEM(utiltest.PrivateKey)
|
|
require.NoError(t, err)
|
|
tokenString, err := token.SignedString(key)
|
|
require.NoError(t, err)
|
|
|
|
_, _, err = mgr.VerifyToken(tokenString)
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
t.Run("OIDC provider is external, audience is not specified, token is signed with the wrong key", func(t *testing.T) {
|
|
config := map[string]string{
|
|
"url": "",
|
|
"oidc.config": fmt.Sprintf(`
|
|
name: Test
|
|
issuer: %s
|
|
clientID: xxx
|
|
clientSecret: yyy
|
|
requestedScopes: ["oidc"]`, oidcTestServer.URL),
|
|
"oidc.tls.insecure.skip.verify": "true", // This isn't what we're testing.
|
|
}
|
|
|
|
// This is not actually used in the test. The test only calls the OIDC test server. But a valid cert/key pair
|
|
// must be set to test VerifyToken's behavior when Argo CD is configured with TLS enabled.
|
|
secretConfig := map[string][]byte{
|
|
"tls.crt": utiltest.Cert,
|
|
"tls.key": utiltest.PrivateKey,
|
|
}
|
|
|
|
settingsMgr := settings.NewSettingsManager(t.Context(), getKubeClientWithConfig(config, secretConfig), "argocd")
|
|
mgr := NewSessionManager(settingsMgr, getProjLister(), "", nil, NewUserStateStorage(nil))
|
|
mgr.verificationDelayNoiseEnabled = false
|
|
|
|
claims := jwt.RegisteredClaims{Subject: "admin", ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour * 24))}
|
|
claims.Issuer = oidcTestServer.URL
|
|
token := jwt.NewWithClaims(jwt.SigningMethodRS512, claims)
|
|
key, err := jwt.ParseRSAPrivateKeyFromPEM(utiltest.PrivateKey2)
|
|
require.NoError(t, err)
|
|
tokenString, err := token.SignedString(key)
|
|
require.NoError(t, err)
|
|
|
|
_, _, err = mgr.VerifyToken(tokenString)
|
|
require.Error(t, err)
|
|
assert.ErrorIs(t, err, common.ErrTokenVerification)
|
|
})
|
|
}
|
|
|
|
func Test_PickFailureAttemptWhenOverflowed(t *testing.T) {
|
|
t.Run("Not pick admin user from the queue", func(t *testing.T) {
|
|
failures := map[string]LoginAttempts{
|
|
"admin": {
|
|
FailCount: 1,
|
|
},
|
|
"test2": {
|
|
FailCount: 1,
|
|
},
|
|
}
|
|
|
|
// inside pickRandomNonAdminLoginFailure, it uses random, so we need to test it multiple times
|
|
for i := 0; i < 1000; i++ {
|
|
user := pickRandomNonAdminLoginFailure(failures, "test")
|
|
assert.Equal(t, "test2", *user)
|
|
}
|
|
})
|
|
|
|
t.Run("Not pick admin user and current user from the queue", func(t *testing.T) {
|
|
failures := map[string]LoginAttempts{
|
|
"test": {
|
|
FailCount: 1,
|
|
},
|
|
"admin": {
|
|
FailCount: 1,
|
|
},
|
|
"test2": {
|
|
FailCount: 1,
|
|
},
|
|
}
|
|
|
|
// inside pickRandomNonAdminLoginFailure, it uses random, so we need to test it multiple times
|
|
for i := 0; i < 1000; i++ {
|
|
user := pickRandomNonAdminLoginFailure(failures, "test")
|
|
assert.Equal(t, "test2", *user)
|
|
}
|
|
})
|
|
}
|