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

     1  package account
     2  
     3  import (
     4  	"context"
     5  	"testing"
     6  	"time"
     7  
     8  	"github.com/golang-jwt/jwt/v5"
     9  	"github.com/stretchr/testify/assert"
    10  	"github.com/stretchr/testify/require"
    11  	"google.golang.org/grpc/codes"
    12  	"google.golang.org/grpc/status"
    13  	corev1 "k8s.io/api/core/v1"
    14  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    15  	"k8s.io/client-go/kubernetes/fake"
    16  
    17  	"github.com/argoproj/argo-cd/v3/common"
    18  	"github.com/argoproj/argo-cd/v3/pkg/apiclient/account"
    19  	sessionpkg "github.com/argoproj/argo-cd/v3/pkg/apiclient/session"
    20  	"github.com/argoproj/argo-cd/v3/server/session"
    21  	"github.com/argoproj/argo-cd/v3/test"
    22  	"github.com/argoproj/argo-cd/v3/util/password"
    23  	"github.com/argoproj/argo-cd/v3/util/rbac"
    24  	sessionutil "github.com/argoproj/argo-cd/v3/util/session"
    25  	"github.com/argoproj/argo-cd/v3/util/settings"
    26  )
    27  
    28  const (
    29  	testNamespace = "default"
    30  )
    31  
    32  // return an AccountServer which returns fake data
    33  func newTestAccountServer(t *testing.T, ctx context.Context, opts ...func(cm *corev1.ConfigMap, secret *corev1.Secret)) (*Server, *session.Server) {
    34  	t.Helper()
    35  	return newTestAccountServerExt(t, ctx, func(_ jwt.Claims, _ ...any) bool {
    36  		return true
    37  	}, opts...)
    38  }
    39  
    40  func newTestAccountServerExt(t *testing.T, ctx context.Context, enforceFn rbac.ClaimsEnforcerFunc, opts ...func(cm *corev1.ConfigMap, secret *corev1.Secret)) (*Server, *session.Server) {
    41  	t.Helper()
    42  	bcrypt, err := password.HashPassword("oldpassword")
    43  	require.NoError(t, err)
    44  	cm := &corev1.ConfigMap{
    45  		ObjectMeta: metav1.ObjectMeta{
    46  			Name:      "argocd-cm",
    47  			Namespace: testNamespace,
    48  			Labels: map[string]string{
    49  				"app.kubernetes.io/part-of": "argocd",
    50  			},
    51  		},
    52  		Data: map[string]string{},
    53  	}
    54  	secret := &corev1.Secret{
    55  		ObjectMeta: metav1.ObjectMeta{
    56  			Name:      "argocd-secret",
    57  			Namespace: testNamespace,
    58  		},
    59  		Data: map[string][]byte{
    60  			"admin.password":   []byte(bcrypt),
    61  			"server.secretkey": []byte("test"),
    62  		},
    63  	}
    64  	for i := range opts {
    65  		opts[i](cm, secret)
    66  	}
    67  	kubeclientset := fake.NewClientset(cm, secret)
    68  	settingsMgr := settings.NewSettingsManager(ctx, kubeclientset, testNamespace)
    69  	sessionMgr := sessionutil.NewSessionManager(settingsMgr, test.NewFakeProjLister(), "", nil, sessionutil.NewUserStateStorage(nil))
    70  	enforcer := rbac.NewEnforcer(kubeclientset, testNamespace, common.ArgoCDRBACConfigMapName, nil)
    71  	enforcer.SetClaimsEnforcerFunc(enforceFn)
    72  
    73  	return NewServer(sessionMgr, settingsMgr, enforcer), session.NewServer(sessionMgr, settingsMgr, nil, nil, nil)
    74  }
    75  
    76  func getAdminAccount(mgr *settings.SettingsManager) (*settings.Account, error) {
    77  	accounts, err := mgr.GetAccounts()
    78  	if err != nil {
    79  		return nil, err
    80  	}
    81  	adminAccount := accounts[common.ArgoCDAdminUsername]
    82  	return &adminAccount, nil
    83  }
    84  
    85  func adminContext(ctx context.Context) context.Context {
    86  	//nolint:staticcheck
    87  	return context.WithValue(ctx, "claims", &jwt.RegisteredClaims{Subject: "admin", Issuer: sessionutil.SessionManagerClaimsIssuer})
    88  }
    89  
    90  func ssoAdminContext(ctx context.Context, iat time.Time) context.Context {
    91  	//nolint:staticcheck
    92  	return context.WithValue(ctx, "claims", &jwt.RegisteredClaims{
    93  		Subject:  "admin",
    94  		Issuer:   "https://myargocdhost.com/api/dex",
    95  		IssuedAt: jwt.NewNumericDate(iat),
    96  	})
    97  }
    98  
    99  func projTokenContext(ctx context.Context) context.Context {
   100  	//nolint:staticcheck
   101  	return context.WithValue(ctx, "claims", &jwt.RegisteredClaims{
   102  		Subject: "proj:demo:deployer",
   103  		Issuer:  sessionutil.SessionManagerClaimsIssuer,
   104  	})
   105  }
   106  
   107  func TestUpdatePassword(t *testing.T) {
   108  	accountServer, sessionServer := newTestAccountServer(t, t.Context())
   109  	ctx := adminContext(t.Context())
   110  	var err error
   111  
   112  	// ensure password is not allowed to be updated if given bad password
   113  	_, err = accountServer.UpdatePassword(ctx, &account.UpdatePasswordRequest{CurrentPassword: "badpassword", NewPassword: "newpassword"})
   114  	require.Error(t, err)
   115  	require.NoError(t, accountServer.sessionMgr.VerifyUsernamePassword("admin", "oldpassword"))
   116  	require.Error(t, accountServer.sessionMgr.VerifyUsernamePassword("admin", "newpassword"))
   117  	// verify old password works
   118  	_, err = sessionServer.Create(ctx, &sessionpkg.SessionCreateRequest{Username: "admin", Password: "oldpassword"})
   119  	require.NoError(t, err)
   120  	// verify new password doesn't
   121  	_, err = sessionServer.Create(ctx, &sessionpkg.SessionCreateRequest{Username: "admin", Password: "newpassword"})
   122  	require.Error(t, err)
   123  
   124  	// ensure password can be updated with valid password and immediately be used
   125  	adminAccount, err := getAdminAccount(accountServer.settingsMgr)
   126  	require.NoError(t, err)
   127  	prevHash := adminAccount.PasswordHash
   128  	_, err = accountServer.UpdatePassword(ctx, &account.UpdatePasswordRequest{CurrentPassword: "oldpassword", NewPassword: "newpassword"})
   129  	require.NoError(t, err)
   130  	adminAccount, err = getAdminAccount(accountServer.settingsMgr)
   131  	require.NoError(t, err)
   132  	assert.NotEqual(t, prevHash, adminAccount.PasswordHash)
   133  	require.NoError(t, accountServer.sessionMgr.VerifyUsernamePassword("admin", "newpassword"))
   134  	require.Error(t, accountServer.sessionMgr.VerifyUsernamePassword("admin", "oldpassword"))
   135  	// verify old password is invalid
   136  	_, err = sessionServer.Create(ctx, &sessionpkg.SessionCreateRequest{Username: "admin", Password: "oldpassword"})
   137  	require.Error(t, err)
   138  	// verify new password works
   139  	_, err = sessionServer.Create(ctx, &sessionpkg.SessionCreateRequest{Username: "admin", Password: "newpassword"})
   140  	require.NoError(t, err)
   141  }
   142  
   143  func TestUpdatePassword_AdminUpdatesAnotherUser(t *testing.T) {
   144  	accountServer, sessionServer := newTestAccountServer(t, t.Context(), func(cm *corev1.ConfigMap, _ *corev1.Secret) {
   145  		cm.Data["accounts.anotherUser"] = "login"
   146  	})
   147  	ctx := adminContext(t.Context())
   148  
   149  	_, err := accountServer.UpdatePassword(ctx, &account.UpdatePasswordRequest{CurrentPassword: "oldpassword", NewPassword: "newpassword", Name: "anotherUser"})
   150  	require.NoError(t, err)
   151  
   152  	_, err = sessionServer.Create(ctx, &sessionpkg.SessionCreateRequest{Username: "anotherUser", Password: "newpassword"})
   153  	require.NoError(t, err)
   154  }
   155  
   156  func TestUpdatePassword_DoesNotHavePermissions(t *testing.T) {
   157  	enforcer := func(_ jwt.Claims, _ ...any) bool {
   158  		return false
   159  	}
   160  
   161  	t.Run("LocalAccountUpdatesAnotherAccount", func(t *testing.T) {
   162  		accountServer, _ := newTestAccountServerExt(t, t.Context(), enforcer, func(cm *corev1.ConfigMap, _ *corev1.Secret) {
   163  			cm.Data["accounts.anotherUser"] = "login"
   164  		})
   165  		ctx := adminContext(t.Context())
   166  		_, err := accountServer.UpdatePassword(ctx, &account.UpdatePasswordRequest{CurrentPassword: "oldpassword", NewPassword: "newpassword", Name: "anotherUser"})
   167  		assert.ErrorContains(t, err, "permission denied")
   168  	})
   169  
   170  	t.Run("SSOAccountWithTheSameName", func(t *testing.T) {
   171  		accountServer, _ := newTestAccountServerExt(t, t.Context(), enforcer)
   172  		ctx := ssoAdminContext(t.Context(), time.Now())
   173  		_, err := accountServer.UpdatePassword(ctx, &account.UpdatePasswordRequest{CurrentPassword: "oldpassword", NewPassword: "newpassword", Name: "admin"})
   174  		assert.ErrorContains(t, err, "permission denied")
   175  	})
   176  }
   177  
   178  func TestUpdatePassword_ProjectToken(t *testing.T) {
   179  	accountServer, _ := newTestAccountServer(t, t.Context(), func(cm *corev1.ConfigMap, _ *corev1.Secret) {
   180  		cm.Data["accounts.anotherUser"] = "login"
   181  	})
   182  	ctx := projTokenContext(t.Context())
   183  	_, err := accountServer.UpdatePassword(ctx, &account.UpdatePasswordRequest{CurrentPassword: "oldpassword", NewPassword: "newpassword"})
   184  	assert.ErrorContains(t, err, "password can only be changed for local users")
   185  }
   186  
   187  func TestUpdatePassword_OldSSOToken(t *testing.T) {
   188  	accountServer, _ := newTestAccountServer(t, t.Context(), func(cm *corev1.ConfigMap, _ *corev1.Secret) {
   189  		cm.Data["accounts.anotherUser"] = "login"
   190  	})
   191  	ctx := ssoAdminContext(t.Context(), time.Now().Add(-2*common.ChangePasswordSSOTokenMaxAge))
   192  
   193  	_, err := accountServer.UpdatePassword(ctx, &account.UpdatePasswordRequest{CurrentPassword: "oldpassword", NewPassword: "newpassword", Name: "anotherUser"})
   194  	require.Error(t, err)
   195  }
   196  
   197  func TestUpdatePassword_SSOUserUpdatesAnotherUser(t *testing.T) {
   198  	accountServer, sessionServer := newTestAccountServer(t, t.Context(), func(cm *corev1.ConfigMap, _ *corev1.Secret) {
   199  		cm.Data["accounts.anotherUser"] = "login"
   200  	})
   201  	ctx := ssoAdminContext(t.Context(), time.Now())
   202  
   203  	_, err := accountServer.UpdatePassword(ctx, &account.UpdatePasswordRequest{CurrentPassword: "oldpassword", NewPassword: "newpassword", Name: "anotherUser"})
   204  	require.NoError(t, err)
   205  
   206  	_, err = sessionServer.Create(ctx, &sessionpkg.SessionCreateRequest{Username: "anotherUser", Password: "newpassword"})
   207  	require.NoError(t, err)
   208  }
   209  
   210  func TestListAccounts_NoAccountsConfigured(t *testing.T) {
   211  	ctx := adminContext(t.Context())
   212  
   213  	accountServer, _ := newTestAccountServer(t, ctx)
   214  	resp, err := accountServer.ListAccounts(ctx, &account.ListAccountRequest{})
   215  	require.NoError(t, err)
   216  	assert.Len(t, resp.Items, 1)
   217  }
   218  
   219  func TestListAccounts_AccountsAreConfigured(t *testing.T) {
   220  	ctx := adminContext(t.Context())
   221  	accountServer, _ := newTestAccountServer(t, ctx, func(cm *corev1.ConfigMap, _ *corev1.Secret) {
   222  		cm.Data["accounts.account1"] = "apiKey"
   223  		cm.Data["accounts.account2"] = "login, apiKey"
   224  		cm.Data["accounts.account2.enabled"] = "false"
   225  	})
   226  
   227  	resp, err := accountServer.ListAccounts(ctx, &account.ListAccountRequest{})
   228  	require.NoError(t, err)
   229  	assert.Len(t, resp.Items, 3)
   230  	assert.ElementsMatch(t, []*account.Account{
   231  		{Name: "admin", Capabilities: []string{"login"}, Enabled: true},
   232  		{Name: "account1", Capabilities: []string{"apiKey"}, Enabled: true},
   233  		{Name: "account2", Capabilities: []string{"login", "apiKey"}, Enabled: false},
   234  	}, resp.Items)
   235  }
   236  
   237  func TestGetAccount(t *testing.T) {
   238  	ctx := adminContext(t.Context())
   239  	accountServer, _ := newTestAccountServer(t, ctx, func(cm *corev1.ConfigMap, _ *corev1.Secret) {
   240  		cm.Data["accounts.account1"] = "apiKey"
   241  	})
   242  
   243  	t.Run("ExistingAccount", func(t *testing.T) {
   244  		acc, err := accountServer.GetAccount(ctx, &account.GetAccountRequest{Name: "account1"})
   245  		require.NoError(t, err)
   246  
   247  		assert.Equal(t, "account1", acc.Name)
   248  	})
   249  
   250  	t.Run("NonExistingAccount", func(t *testing.T) {
   251  		_, err := accountServer.GetAccount(ctx, &account.GetAccountRequest{Name: "bad-name"})
   252  		require.Error(t, err)
   253  		assert.Equal(t, codes.NotFound, status.Code(err))
   254  	})
   255  }
   256  
   257  func TestCreateToken_SuccessfullyCreated(t *testing.T) {
   258  	ctx := adminContext(t.Context())
   259  	accountServer, _ := newTestAccountServer(t, ctx, func(cm *corev1.ConfigMap, _ *corev1.Secret) {
   260  		cm.Data["accounts.account1"] = "apiKey"
   261  	})
   262  
   263  	_, err := accountServer.CreateToken(ctx, &account.CreateTokenRequest{Name: "account1"})
   264  	require.NoError(t, err)
   265  
   266  	acc, err := accountServer.GetAccount(ctx, &account.GetAccountRequest{Name: "account1"})
   267  	require.NoError(t, err)
   268  
   269  	assert.Len(t, acc.Tokens, 1)
   270  }
   271  
   272  func TestCreateToken_DoesNotHaveCapability(t *testing.T) {
   273  	ctx := adminContext(t.Context())
   274  	accountServer, _ := newTestAccountServer(t, ctx, func(cm *corev1.ConfigMap, _ *corev1.Secret) {
   275  		cm.Data["accounts.account1"] = "login"
   276  	})
   277  
   278  	_, err := accountServer.CreateToken(ctx, &account.CreateTokenRequest{Name: "account1"})
   279  	require.Error(t, err)
   280  }
   281  
   282  func TestCreateToken_UserSpecifiedID(t *testing.T) {
   283  	ctx := adminContext(t.Context())
   284  	accountServer, _ := newTestAccountServer(t, ctx, func(cm *corev1.ConfigMap, _ *corev1.Secret) {
   285  		cm.Data["accounts.account1"] = "apiKey"
   286  	})
   287  
   288  	_, err := accountServer.CreateToken(ctx, &account.CreateTokenRequest{Name: "account1", Id: "test"})
   289  	require.NoError(t, err)
   290  
   291  	_, err = accountServer.CreateToken(ctx, &account.CreateTokenRequest{Name: "account1", Id: "test"})
   292  	require.ErrorContains(t, err, "failed to update account with new token:")
   293  	assert.ErrorContains(t, err, "account already has token with id 'test'")
   294  }
   295  
   296  func TestDeleteToken_SuccessfullyRemoved(t *testing.T) {
   297  	ctx := adminContext(t.Context())
   298  	accountServer, _ := newTestAccountServer(t, ctx, func(cm *corev1.ConfigMap, secret *corev1.Secret) {
   299  		cm.Data["accounts.account1"] = "apiKey"
   300  		secret.Data["accounts.account1.tokens"] = []byte(`[{"id":"123","iat":1583789194,"exp":1583789194}]`)
   301  	})
   302  
   303  	_, err := accountServer.DeleteToken(ctx, &account.DeleteTokenRequest{Name: "account1", Id: "123"})
   304  	require.NoError(t, err)
   305  
   306  	acc, err := accountServer.GetAccount(ctx, &account.GetAccountRequest{Name: "account1"})
   307  	require.NoError(t, err)
   308  
   309  	assert.Empty(t, acc.Tokens)
   310  }
   311  
   312  func TestCanI_GetLogsAllow(t *testing.T) {
   313  	accountServer, _ := newTestAccountServer(t, t.Context(), func(_ *corev1.ConfigMap, _ *corev1.Secret) {
   314  	})
   315  
   316  	ctx := projTokenContext(t.Context())
   317  	resp, err := accountServer.CanI(ctx, &account.CanIRequest{Resource: "logs", Action: "get", Subresource: ""})
   318  	require.NoError(t, err)
   319  	assert.Equal(t, "yes", resp.Value)
   320  }
   321  
   322  func TestCanI_GetLogsDeny(t *testing.T) {
   323  	enforcer := func(_ jwt.Claims, _ ...any) bool {
   324  		return false
   325  	}
   326  
   327  	accountServer, _ := newTestAccountServerExt(t, t.Context(), enforcer, func(_ *corev1.ConfigMap, _ *corev1.Secret) {
   328  	})
   329  
   330  	ctx := projTokenContext(t.Context())
   331  	resp, err := accountServer.CanI(ctx, &account.CanIRequest{Resource: "logs", Action: "get", Subresource: "*/*"})
   332  	require.NoError(t, err)
   333  	assert.Equal(t, "no", resp.Value)
   334  }