@@ -11,6 +11,7 @@ import (
11
11
"sync"
12
12
"time"
13
13
14
+ "github.com/cenkalti/backoff/v5"
14
15
"k8s.io/klog/v2"
15
16
16
17
"github.com/jetstack/preflight/pkg/internal/cyberark/servicediscovery"
@@ -233,32 +234,34 @@ func (c *Client) LoginUsernamePassword(ctx context.Context, username string, pas
233
234
}
234
235
}()
235
236
236
- c .tokenCacheMutex .Lock ()
237
- defer c .tokenCacheMutex .Unlock ()
237
+ operation := func () (any , error ) {
238
+ advanceRequestBody , err := c .doStartAuthentication (ctx , username )
239
+ if err != nil {
240
+ return struct {}{}, err
241
+ }
238
242
239
- advanceRequestBody , err := c .doStartAuthentication (ctx , username )
240
- if err != nil {
241
- return err
243
+ // NB: We explicitly pass advanceRequestBody by value here so that when we add the password
244
+ // in doAdvanceAuthentication we don't create a copy of the password slice elsewhere.
245
+ err = c .doAdvanceAuthentication (ctx , username , & password , advanceRequestBody )
246
+ if err != nil {
247
+ return struct {}{}, err
248
+ }
249
+
250
+ return struct {}{}, nil
242
251
}
243
252
244
- // We can't skip AdvanceAuthentication so we need to add the password to the body
245
- // and send it off to complete the login process
246
- advanceRequestBody .Answer = string (password )
253
+ backoffPolicy := backoff .NewConstantBackOff (10 * time .Second )
247
254
248
- err = c .doAdvanceAuthentication (ctx , username , & advanceRequestBody )
249
- if err != nil {
250
- return err
251
- }
255
+ _ , err := backoff .Retry (ctx , operation , backoff .WithBackOff (backoffPolicy ))
252
256
253
- return nil
257
+ return err
254
258
}
255
259
256
260
// doStartAuthentication performs the initial request to start the login process using a username and password.
257
261
// It returns a partially initialized advanceAuthenticationRequestBody ready to send to the server to complete
258
- // the login. To avoid copying passwords, this function doesn't set the password in the Answer field, and
259
- // callers must set that field before making the request to AdvanceAuthentication.
262
+ // the login. As this function doesn't have access to the password, it must be added to the returned request body
263
+ // by the caller before being used as a request to AdvanceAuthentication.
260
264
// See https://api-docs.cyberark.com/docs/identity-api-reference/authentication-and-authorization/operations/create-a-security-start-authentication
261
- // This function assumes that tokenCacheMutex has already been acquired by the caller.
262
265
func (c * Client ) doStartAuthentication (ctx context.Context , username string ) (advanceAuthenticationRequestBody , error ) {
263
266
response := advanceAuthenticationRequestBody {}
264
267
@@ -287,16 +290,7 @@ func (c *Client) doStartAuthentication(ctx context.Context, username string) (ad
287
290
return response , fmt .Errorf ("failed to initialise request to Identity endpoint %s: %s" , endpoint , err )
288
291
}
289
292
290
- // From the docs:
291
- // Your request header must contain X-IDAP-NATIVE-CLIENT:true to indicate that an application is invoking
292
- // the CyberArk Identity endpoint, and
293
- // Content-Type: application/json to indicate that the body is in JSON format.
294
- // Experimentally, it seems the X-IDAP-NATIVE-CLIENT is not required but we'll follow the docs.
295
- // The "canonicalheader" warns us that the IDAP-NATIVE-CLIENT header isn't canonical, but we silence it here
296
- // since we want to exactly match the docs.
297
- request .Header .Set ("Content-Type" , "application/json" )
298
- request .Header .Set ("X-IDAP-NATIVE-CLIENT" , "true" ) //nolint: canonicalheader
299
- version .SetUserAgent (request )
293
+ setIdentityHeaders (request )
300
294
301
295
httpResponse , err := c .client .Do (request )
302
296
if err != nil {
@@ -306,7 +300,14 @@ func (c *Client) doStartAuthentication(ctx context.Context, username string) (ad
306
300
defer httpResponse .Body .Close ()
307
301
308
302
if httpResponse .StatusCode != 200 {
309
- return response , fmt .Errorf ("got unexpected status code %s from request to start authentication in CyberArk Identity API" , httpResponse .Status )
303
+ err := fmt .Errorf ("got unexpected status code %s from request to start authentication in CyberArk Identity API" , httpResponse .Status )
304
+ if httpResponse .StatusCode >= 500 || httpResponse .StatusCode < 400 {
305
+ return response , err
306
+ }
307
+
308
+ // If we got a 4xx error, we shouldn't retry
309
+ return response , backoff .Permanent (err )
310
+
310
311
}
311
312
312
313
startAuthResponse := startAuthenticationResponseBody {}
@@ -374,32 +375,31 @@ func (c *Client) doStartAuthentication(ctx context.Context, username string) (ad
374
375
return response , nil
375
376
}
376
377
377
- func (c * Client ) doAdvanceAuthentication (ctx context.Context , username string , requestBody * advanceAuthenticationRequestBody ) error {
378
+ // doAdvanceAuthentication performs the second step of the login process, sending the password to the server
379
+ // and receiving a token in response.
380
+ func (c * Client ) doAdvanceAuthentication (ctx context.Context , username string , password * []byte , requestBody advanceAuthenticationRequestBody ) error {
381
+ if password == nil {
382
+ return backoff .Permanent (fmt .Errorf ("password must not be nil; this is a programming error" ))
383
+ }
384
+
385
+ requestBody .Answer = string (* password )
386
+
378
387
bodyJSON , err := json .Marshal (requestBody )
379
388
if err != nil {
380
- return fmt .Errorf ("failed to marshal JSON for request to AdvanceAuthentication endpoint: %s" , err )
389
+ return backoff . Permanent ( fmt .Errorf ("failed to marshal JSON for request to AdvanceAuthentication endpoint: %s" , err ) )
381
390
}
382
391
383
392
endpoint , err := url .JoinPath (c .endpoint , "Security" , "AdvanceAuthentication" )
384
393
if err != nil {
385
- return fmt .Errorf ("failed to create URL for request to CyberArk Identity AdvanceAuthentication: %s" , err )
394
+ return backoff . Permanent ( fmt .Errorf ("failed to create URL for request to CyberArk Identity AdvanceAuthentication: %s" , err ) )
386
395
}
387
396
388
397
request , err := http .NewRequestWithContext (ctx , http .MethodPost , endpoint , bytes .NewReader (bodyJSON ))
389
398
if err != nil {
390
399
return fmt .Errorf ("failed to initialise request to Identity endpoint %s: %s" , endpoint , err )
391
400
}
392
401
393
- // From the docs:
394
- // Your request header must contain X-IDAP-NATIVE-CLIENT:true to indicate that an application is invoking
395
- // the CyberArk Identity endpoint, and
396
- // Content-Type: application/json to indicate that the body is in JSON format.
397
- // Experimentally, it seems the X-IDAP-NATIVE-CLIENT is not required but we'll follow the docs.
398
- // The "canonicalheader" warns us that the IDAP-NATIVE-CLIENT header isn't canonical, but we silence it here
399
- // since we want to exactly match the docs.
400
- request .Header .Set ("Content-Type" , "application/json" )
401
- request .Header .Set ("X-IDAP-NATIVE-CLIENT" , "true" ) //nolint: canonicalheader
402
- version .SetUserAgent (request )
402
+ setIdentityHeaders (request )
403
403
404
404
httpResponse , err := c .client .Do (request )
405
405
if err != nil {
@@ -411,7 +411,13 @@ func (c *Client) doAdvanceAuthentication(ctx context.Context, username string, r
411
411
// Important: Even login failures can produce a 200 status code, so this
412
412
// check won't catch all failures
413
413
if httpResponse .StatusCode != 200 {
414
- return fmt .Errorf ("got unexpected status code %s from request to advance authentication in CyberArk Identity API" , httpResponse .Status )
414
+ err := fmt .Errorf ("got unexpected status code %s from request to advance authentication in CyberArk Identity API" , httpResponse .Status )
415
+ if httpResponse .StatusCode >= 500 || httpResponse .StatusCode < 400 {
416
+ return err
417
+ }
418
+
419
+ // If we got a 4xx error, we shouldn't retry
420
+ return backoff .Permanent (err )
415
421
}
416
422
417
423
advanceAuthResponse := advanceAuthenticationResponseBody {}
@@ -430,12 +436,32 @@ func (c *Client) doAdvanceAuthentication(ctx context.Context, username string, r
430
436
}
431
437
432
438
if advanceAuthResponse .Result .Summary != SummaryLoginSuccess {
433
- return fmt .Errorf ("got a %s response from AdvanceAuthentication; this implies that the user account %s requires MFA, which is not supported. Try unlocking MFA for this user" , advanceAuthResponse .Result .Summary , username )
439
+ // IF MFA was enabled and we got here, there's probably nothing to be gained from a retry
440
+ // and the best thing to do is fail now so the user can fix MFA settings.
441
+ return backoff .Permanent (fmt .Errorf ("got a %s response from AdvanceAuthentication; this implies that the user account %s requires MFA, which is not supported. Try unlocking MFA for this user" , advanceAuthResponse .Result .Summary , username ))
434
442
}
435
443
436
444
klog .FromContext (ctx ).Info ("successfully completed AdvanceAuthentication request to CyberArk Identity; login complete" , "username" , username )
437
445
446
+ c .tokenCacheMutex .Lock ()
447
+
438
448
c .tokenCache [username ] = token (advanceAuthResponse .Result .Token )
439
449
450
+ c .tokenCacheMutex .Unlock ()
451
+
440
452
return nil
441
453
}
454
+
455
+ // setIdentityHeaders sets the headers required for requests to the CyberArk Identity API.
456
+ // From the docs:
457
+ // Your request header must contain X-IDAP-NATIVE-CLIENT:true to indicate that an application is invoking
458
+ // the CyberArk Identity endpoint, and
459
+ // Content-Type: application/json to indicate that the body is in JSON format.
460
+ // Experimentally, it seems the X-IDAP-NATIVE-CLIENT is not required but we'll follow the docs.
461
+ func setIdentityHeaders (r * http.Request ) {
462
+ // The "canonicalheader" linter warns us that the IDAP-NATIVE-CLIENT header isn't canonical, but we silence it here
463
+ // since we want to exactly match the docs.
464
+ r .Header .Set ("Content-Type" , "application/json" )
465
+ r .Header .Set ("X-IDAP-NATIVE-CLIENT" , "true" ) //nolint: canonicalheader
466
+ version .SetUserAgent (r )
467
+ }
0 commit comments