Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 82 additions & 1 deletion cmd/lfx-v1-sync-helper/auth0_identity_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ type fakeAuth0Users struct {
// created tracks users passed to Create.
created []*management.User

// updateErr is returned by Update.
updateErr error
// updated tracks (id, user) pairs passed to Update.
updated []*management.User

// linkErr is returned by Link.
linkErr error
// linked tracks (primaryID, provider, userID) tuples.
Expand Down Expand Up @@ -94,7 +99,11 @@ func (f *fakeAuth0Users) Create(_ context.Context, u *management.User, _ ...mana
return nil
}

func (f *fakeAuth0Users) Update(_ context.Context, _ string, _ *management.User, _ ...management.RequestOption) error {
func (f *fakeAuth0Users) Update(_ context.Context, _ string, u *management.User, _ ...management.RequestOption) error {
if f.updateErr != nil {
return f.updateErr
}
f.updated = append(f.updated, u)
return nil
}

Expand Down Expand Up @@ -186,6 +195,78 @@ func TestLinkEmailIdentity(t *testing.T) {
}
})

t.Run("email matches primary account's own email is skipped", func(t *testing.T) {
fake := &fakeAuth0Users{
users: map[string]*management.User{
"auth0|primary": {
ID: auth0.String("auth0|primary"),
Email: auth0.String("alt@example.com"),
},
},
}
cleanup := setupLinkTest(t, fake)
defer cleanup()

err := linkEmailIdentity(context.Background(), "auth0|primary", "alt@example.com")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(fake.created) != 0 {
t.Errorf("expected no create calls, got %d", len(fake.created))
}
if len(fake.linked) != 0 {
t.Errorf("expected no link calls, got %d", len(fake.linked))
}
})

t.Run("email matches primary account's own email case-insensitive is skipped", func(t *testing.T) {
fake := &fakeAuth0Users{
users: map[string]*management.User{
"auth0|primary": {
ID: auth0.String("auth0|primary"),
Email: auth0.String("Alt@Example.COM"),
},
},
}
cleanup := setupLinkTest(t, fake)
defer cleanup()

err := linkEmailIdentity(context.Background(), "auth0|primary", "alt@example.com")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(fake.created) != 0 {
t.Errorf("expected no create calls, got %d", len(fake.created))
}
if len(fake.linked) != 0 {
t.Errorf("expected no link calls, got %d", len(fake.linked))
}
})

t.Run("blocked primary account is skipped", func(t *testing.T) {
fake := &fakeAuth0Users{
users: map[string]*management.User{
"auth0|primary": {
ID: auth0.String("auth0|primary"),
Blocked: auth0.Bool(true),
},
},
}
cleanup := setupLinkTest(t, fake)
defer cleanup()

err := linkEmailIdentity(context.Background(), "auth0|primary", "alt@example.com")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(fake.created) != 0 {
t.Errorf("expected no create calls, got %d", len(fake.created))
}
if len(fake.linked) != 0 {
t.Errorf("expected no link calls, got %d", len(fake.linked))
}
})

t.Run("already linked case-insensitive", func(t *testing.T) {
fake := &fakeAuth0Users{
users: map[string]*management.User{
Expand Down
27 changes: 27 additions & 0 deletions cmd/lfx-v1-sync-helper/auth0_mgmt.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,14 @@ func syncProfileToAuth0(ctx context.Context, auth0UserID string, v1Data map[stri
return fmt.Errorf("failed to read Auth0 user %s: %w", auth0UserID, err)
}

// Blocked accounts are treated as inactive/deprovisioned: don't push new
// profile data to them. This is a no-op skip, not an error.
if existing.GetBlocked() {
logger.With("auth0_user_id", auth0UserID).
WarnContext(ctx, "Auth0 user is blocked, skipping profile sync")
return nil
Comment on lines +178 to +181
}

// Start from existing user_metadata (or empty map) for diffing.
existingMetadata := make(map[string]interface{})
if existing.UserMetadata != nil {
Expand Down Expand Up @@ -225,6 +233,25 @@ func linkEmailIdentity(ctx context.Context, primaryAuth0ID, email string) error
if err != nil {
return fmt.Errorf("failed to read primary user %s: %w", primaryAuth0ID, err)
}

// Blocked accounts are treated as inactive/deprovisioned: don't link new
// secondary identities to them. This is a no-op skip, not an error.
if primaryUser.GetBlocked() {
logger.With("auth0_user_id", primaryAuth0ID, "email", email).
WarnContext(ctx, "Auth0 user is blocked, skipping email link")
return nil
Comment on lines +239 to +242
}

// If the email matches the primary account's own login email, there is
// nothing to link: the account already has this email as its top-level
// identity. Linking it as a secondary "email" identity would just create
// a redundant duplicate.
if strings.EqualFold(primaryUser.GetEmail(), email) {
logger.With("auth0_user_id", primaryAuth0ID, "email", email).
WarnContext(ctx, "email matches primary account's own email, skipping link")
return nil
Comment on lines +249 to +252
}

for _, identity := range primaryUser.Identities {
if identity.GetProvider() == "email" && identity.GetConnection() == "email" {
if profileEmail, _ := identity.GetProfileData()["email"].(string); strings.EqualFold(profileEmail, email) {
Expand Down
24 changes: 24 additions & 0 deletions cmd/lfx-v1-sync-helper/auth0_mgmt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,32 @@ import (
"fmt"
"net"
"testing"

"github.com/auth0/go-auth0"
"github.com/auth0/go-auth0/management"
)

func TestSyncProfileToAuth0Blocked(t *testing.T) {
fake := &fakeAuth0Users{
users: map[string]*management.User{
"auth0|blocked": {
ID: auth0.String("auth0|blocked"),
Blocked: auth0.Bool(true),
},
},
}
cleanup := setupLinkTest(t, fake)
defer cleanup()

err := syncProfileToAuth0(context.Background(), "auth0|blocked", map[string]any{"title": "Engineer"})
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(fake.updated) != 0 {
t.Errorf("expected no update calls, got %d", len(fake.updated))
}
}

func TestBuildAuth0Metadata(t *testing.T) {
tests := []struct {
name string
Expand Down
Loading