Skip to content

Commit 32df2be

Browse files
authored
Merge pull request #132 from linuxfoundation/lfxv2-2662-email-sync-edge-cases
2 parents 148b159 + 6295096 commit 32df2be

4 files changed

Lines changed: 310 additions & 55 deletions

File tree

cmd/lfx-v1-sync-helper/backfill_email_profile.go

Lines changed: 93 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -257,31 +257,71 @@ func backfillEmailsForUser(ctx context.Context, auth0UserID, username string, dr
257257
return fmt.Errorf("parsing email SFIDs for %s: %w", userSfid, err)
258258
}
259259

260+
// Single pass: collect candidate (non-primary, active, verified) emails to
261+
// link, while also counting "qualifying" emails (active and either
262+
// verified or primary) across ALL of the user's alternate email rows. If
263+
// only one email qualifies, that row is a lone alternate_email__c acting
264+
// as a de-facto primary — v1 lazy-sync may not have created/synced the
265+
// primary row yet — so it should not be linked as a secondary identity,
266+
// even if it was collected as a link candidate (see LFXV2-2662; this
267+
// mirrors auth0-db-sync.js's heuristic).
268+
type emailCandidate struct {
269+
emailSfid string
270+
email string
271+
}
272+
var candidates []emailCandidate
273+
qualifying := 0
274+
sawPrimary := false
260275
for _, emailSfid := range emailSfids {
261276
email, isPrimary, isVerified, isActive, err := getAlternateEmailDetailsFn(ctx, emailSfid)
262277
if err != nil {
263-
logger.With("error", err, "email_sfid", emailSfid, "auth0_user_id", auth0UserID).
264-
Warn("failed to get alternate email details, skipping")
265-
result.emailsSkipped++
266-
continue
278+
// A read failure makes the qualifying count unreliable, and
279+
// guessing either way (sole vs. non-sole) risks a wrong link
280+
// decision, so abort rather than silently proceed. This user
281+
// will be retried on the next run (the cursor doesn't advance
282+
// past them); a persistently-bad row requires a manual data fix
283+
// to unblock, which is preferable to silently mislinking.
284+
return fmt.Errorf("getting alternate email details for %s (auth0 user %s): %w", emailSfid, auth0UserID, err)
285+
}
286+
if isActive && (isVerified || isPrimary) {
287+
qualifying++
288+
if isPrimary {
289+
sawPrimary = true
290+
}
267291
}
268292
if isPrimary || !isActive || !isVerified || email == "" {
269293
result.emailsSkipped++
270294
continue
271295
}
296+
candidates = append(candidates, emailCandidate{emailSfid: emailSfid, email: email})
297+
}
272298

299+
if qualifying <= 1 {
300+
logger.With("user_sfid", userSfid, "auth0_user_id", auth0UserID).
301+
Debug("sole qualifying alternate email, treating as de-facto primary, skipping link candidates")
302+
result.emailsSkipped += len(candidates)
303+
return nil
304+
}
305+
if !sawPrimary {
306+
// More than one row qualifies but none is flagged primary: which one
307+
// is the de-facto primary is genuinely ambiguous, so abort rather
308+
// than linking all of them as secondary identities.
309+
return fmt.Errorf("user %s (auth0 user %s) has %d qualifying alternate emails and none is flagged primary; cannot determine de-facto primary", userSfid, auth0UserID, qualifying)
310+
}
311+
312+
for _, c := range candidates {
273313
if dryRun {
274314
logger.With(
275315
"auth0_user_id", auth0UserID,
276-
"email", email,
277-
"email_sfid", emailSfid,
316+
"email", c.email,
317+
"email_sfid", c.emailSfid,
278318
).Info("[dry-run] would link alternate email to Auth0 user")
279319
result.emailsLinked++
280320
continue
281321
}
282322

283-
if err := linkEmailIdentityFn(ctx, auth0UserID, email); err != nil {
284-
return fmt.Errorf("linking email %s: %w", email, err)
323+
if err := linkEmailIdentityFn(ctx, auth0UserID, c.email); err != nil {
324+
return fmt.Errorf("linking email %s: %w", c.email, err)
285325
}
286326
result.emailsLinked++
287327
}
@@ -474,11 +514,36 @@ func syncSingleUser(ctx context.Context, username string, dryRun bool) error {
474514
return fmt.Errorf("parsing email SFIDs: %w", err)
475515
}
476516

517+
// Single pass: collect candidate (non-primary, active, verified) emails to
518+
// link, while also counting "qualifying" emails (active and either
519+
// verified or primary) across ALL of the user's alternate email rows. If
520+
// only one email qualifies, that row is a lone alternate_email__c acting
521+
// as a de-facto primary — v1 lazy-sync may not have created/synced the
522+
// primary row yet — so it should not be linked as a secondary identity,
523+
// even if it was collected as a link candidate (see LFXV2-2662; this
524+
// mirrors auth0-db-sync.js's heuristic).
525+
type emailCandidate struct {
526+
emailSfid string
527+
email string
528+
}
529+
var candidates []emailCandidate
530+
qualifying := 0
531+
sawPrimary := false
477532
for _, emailSfid := range emailSfids {
478533
email, isPrimary, isVerified, isActive, err := getAlternateEmailDetailsFn(ctx, emailSfid)
479534
if err != nil {
480-
logger.With("error", err, "email_sfid", emailSfid).Warn("failed to get email details, skipping")
481-
continue
535+
// A read failure makes the qualifying count unreliable, and
536+
// guessing either way (sole vs. non-sole) risks a wrong link
537+
// decision, so abort rather than silently proceed. Re-run
538+
// --sync-user for this user once the underlying read problem
539+
// (or bad data) is resolved.
540+
return fmt.Errorf("getting alternate email details for %s (auth0 user %s): %w", emailSfid, auth0UserID, err)
541+
}
542+
if isActive && (isVerified || isPrimary) {
543+
qualifying++
544+
if isPrimary {
545+
sawPrimary = true
546+
}
482547
}
483548
if isPrimary || !isActive {
484549
continue
@@ -491,18 +556,32 @@ func syncSingleUser(ctx context.Context, username string, dryRun bool) error {
491556
if email == "" {
492557
continue
493558
}
559+
candidates = append(candidates, emailCandidate{emailSfid: emailSfid, email: email})
560+
}
561+
562+
if qualifying <= 1 {
563+
logger.With("user_sfid", userSfid, "auth0_user_id", auth0UserID).
564+
Debug("sole qualifying alternate email, treating as de-facto primary, skipping link candidates")
565+
candidates = nil
566+
} else if !sawPrimary {
567+
// More than one row qualifies but none is flagged primary: which one
568+
// is the de-facto primary is genuinely ambiguous, so abort rather
569+
// than linking all of them as secondary identities.
570+
return fmt.Errorf("user %s (auth0 user %s) has %d qualifying alternate emails and none is flagged primary; cannot determine de-facto primary", userSfid, auth0UserID, qualifying)
571+
}
494572

573+
for _, c := range candidates {
495574
if dryRun {
496-
logger.With("auth0_user_id", auth0UserID, "email", email).
575+
logger.With("auth0_user_id", auth0UserID, "email", c.email).
497576
Info("[dry-run] would link alternate email to Auth0 user")
498577
continue
499578
}
500579

501-
if err := linkEmailIdentityFn(ctx, auth0UserID, email); err != nil {
502-
logger.With("error", err, "auth0_user_id", auth0UserID, "email", email).
580+
if err := linkEmailIdentityFn(ctx, auth0UserID, c.email); err != nil {
581+
logger.With("error", err, "auth0_user_id", auth0UserID, "email", c.email).
503582
Warn("failed to link email, skipping")
504583
} else {
505-
logger.With("auth0_user_id", auth0UserID, "email", email).
584+
logger.With("auth0_user_id", auth0UserID, "email", c.email).
506585
Info("linked email identity")
507586
}
508587
}

0 commit comments

Comments
 (0)