Skip to content

Commit c633902

Browse files
committed
feat: improve password handling, add retries
Signed-off-by: Ashley Davis <[email protected]>
1 parent 7c580cd commit c633902

File tree

2 files changed

+83
-56
lines changed

2 files changed

+83
-56
lines changed

pkg/internal/cyberark/identity/advance_authentication_test.go

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,16 @@ import (
1111
func Test_IdentityAdvanceAuthentication(t *testing.T) {
1212
tests := map[string]struct {
1313
username string
14-
advanceBody *advanceAuthenticationRequestBody
14+
password []byte
15+
advanceBody advanceAuthenticationRequestBody
1516

1617
expectedError error
1718
}{
1819
"success": {
1920
username: successUser,
20-
advanceBody: &advanceAuthenticationRequestBody{
21+
password: []byte(successPassword),
22+
advanceBody: advanceAuthenticationRequestBody{
2123
Action: ActionAnswer,
22-
Answer: successPassword,
2324
MechanismID: successMechanismID,
2425
SessionID: successSessionID,
2526
TenantID: "foo",
@@ -30,9 +31,9 @@ func Test_IdentityAdvanceAuthentication(t *testing.T) {
3031
},
3132
"incorrect password": {
3233
username: successUser,
33-
advanceBody: &advanceAuthenticationRequestBody{
34+
password: []byte("foo"),
35+
advanceBody: advanceAuthenticationRequestBody{
3436
Action: ActionAnswer,
35-
Answer: "foo",
3637
MechanismID: successMechanismID,
3738
SessionID: successSessionID,
3839
TenantID: "foo",
@@ -43,9 +44,9 @@ func Test_IdentityAdvanceAuthentication(t *testing.T) {
4344
},
4445
"bad action": {
4546
username: successUser,
46-
advanceBody: &advanceAuthenticationRequestBody{
47+
password: []byte(successPassword),
48+
advanceBody: advanceAuthenticationRequestBody{
4749
Action: "foo",
48-
Answer: successPassword,
4950
MechanismID: successMechanismID,
5051
SessionID: successSessionID,
5152
TenantID: "foo",
@@ -56,9 +57,9 @@ func Test_IdentityAdvanceAuthentication(t *testing.T) {
5657
},
5758
"bad mechanism id": {
5859
username: successUser,
59-
advanceBody: &advanceAuthenticationRequestBody{
60+
password: []byte(successPassword),
61+
advanceBody: advanceAuthenticationRequestBody{
6062
Action: ActionAnswer,
61-
Answer: successPassword,
6263
MechanismID: "foo",
6364
SessionID: successSessionID,
6465
TenantID: "foo",
@@ -69,9 +70,9 @@ func Test_IdentityAdvanceAuthentication(t *testing.T) {
6970
},
7071
"bad session id": {
7172
username: successUser,
72-
advanceBody: &advanceAuthenticationRequestBody{
73+
password: []byte(successPassword),
74+
advanceBody: advanceAuthenticationRequestBody{
7375
Action: ActionAnswer,
74-
Answer: successPassword,
7576
MechanismID: successMechanismID,
7677
SessionID: "foo",
7778
TenantID: "foo",
@@ -82,9 +83,9 @@ func Test_IdentityAdvanceAuthentication(t *testing.T) {
8283
},
8384
"persistant login not set": {
8485
username: successUser,
85-
advanceBody: &advanceAuthenticationRequestBody{
86+
password: []byte(successPassword),
87+
advanceBody: advanceAuthenticationRequestBody{
8688
Action: ActionAnswer,
87-
Answer: successPassword,
8889
MechanismID: successMechanismID,
8990
SessionID: successSessionID,
9091
TenantID: "foo",
@@ -113,7 +114,7 @@ func Test_IdentityAdvanceAuthentication(t *testing.T) {
113114
return
114115
}
115116

116-
err = client.doAdvanceAuthentication(ctx, testSpec.username, testSpec.advanceBody)
117+
err = client.doAdvanceAuthentication(ctx, testSpec.username, &testSpec.password, testSpec.advanceBody)
117118
if testSpec.expectedError != err {
118119
if testSpec.expectedError == nil {
119120
t.Errorf("didn't expect an error but got %v", err)

pkg/internal/cyberark/identity/identity.go

Lines changed: 68 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -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.
262265
func (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

Comments
 (0)