@@ -11,6 +11,7 @@ import (
1111 "sync"
1212 "time"
1313
14+ "github.com/cenkalti/backoff/v5"
1415 "k8s.io/klog/v2"
1516
1617 "github.com/jetstack/preflight/pkg/internal/cyberark/servicediscovery"
@@ -233,32 +234,34 @@ func (c *Client) LoginUsernamePassword(ctx context.Context, username string, pas
233234 }
234235 }()
235236
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+ }
238242
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
242251 }
243252
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 )
247254
248- err = c .doAdvanceAuthentication (ctx , username , & advanceRequestBody )
249- if err != nil {
250- return err
251- }
255+ _ , err := backoff .Retry (ctx , operation , backoff .WithBackOff (backoffPolicy ))
252256
253- return nil
257+ return err
254258}
255259
256260// doStartAuthentication performs the initial request to start the login process using a username and password.
257261// 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.
260264// 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.
262265func (c * Client ) doStartAuthentication (ctx context.Context , username string ) (advanceAuthenticationRequestBody , error ) {
263266 response := advanceAuthenticationRequestBody {}
264267
@@ -287,16 +290,7 @@ func (c *Client) doStartAuthentication(ctx context.Context, username string) (ad
287290 return response , fmt .Errorf ("failed to initialise request to Identity endpoint %s: %s" , endpoint , err )
288291 }
289292
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 )
300294
301295 httpResponse , err := c .client .Do (request )
302296 if err != nil {
@@ -306,7 +300,14 @@ func (c *Client) doStartAuthentication(ctx context.Context, username string) (ad
306300 defer httpResponse .Body .Close ()
307301
308302 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+
310311 }
311312
312313 startAuthResponse := startAuthenticationResponseBody {}
@@ -374,32 +375,31 @@ func (c *Client) doStartAuthentication(ctx context.Context, username string) (ad
374375 return response , nil
375376}
376377
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+
378387 bodyJSON , err := json .Marshal (requestBody )
379388 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 ) )
381390 }
382391
383392 endpoint , err := url .JoinPath (c .endpoint , "Security" , "AdvanceAuthentication" )
384393 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 ) )
386395 }
387396
388397 request , err := http .NewRequestWithContext (ctx , http .MethodPost , endpoint , bytes .NewReader (bodyJSON ))
389398 if err != nil {
390399 return fmt .Errorf ("failed to initialise request to Identity endpoint %s: %s" , endpoint , err )
391400 }
392401
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 )
403403
404404 httpResponse , err := c .client .Do (request )
405405 if err != nil {
@@ -411,7 +411,13 @@ func (c *Client) doAdvanceAuthentication(ctx context.Context, username string, r
411411 // Important: Even login failures can produce a 200 status code, so this
412412 // check won't catch all failures
413413 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 )
415421 }
416422
417423 advanceAuthResponse := advanceAuthenticationResponseBody {}
@@ -430,12 +436,32 @@ func (c *Client) doAdvanceAuthentication(ctx context.Context, username string, r
430436 }
431437
432438 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 ))
434442 }
435443
436444 klog .FromContext (ctx ).Info ("successfully completed AdvanceAuthentication request to CyberArk Identity; login complete" , "username" , username )
437445
446+ c .tokenCacheMutex .Lock ()
447+
438448 c .tokenCache [username ] = token (advanceAuthResponse .Result .Token )
439449
450+ c .tokenCacheMutex .Unlock ()
451+
440452 return nil
441453}
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