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 }