Skip to content

Sync outside collaborators as part of user List#128

Open
aldevv wants to merge 1 commit intomainfrom
add-outside-collaborators-to-user-sync
Open

Sync outside collaborators as part of user List#128
aldevv wants to merge 1 commit intomainfrom
add-outside-collaborators-to-user-sync

Conversation

@aldevv
Copy link
Contributor

@aldevv aldevv commented Mar 9, 2026

Summary

  • Outside collaborators are now synced as part of the user List alongside org members
  • Outside collaborators are marked in their profile to distinguish them from regular org members
image

Summary by CodeRabbit

  • New Features

    • Added support for discovering and enumerating outside collaborators within organizations
    • Enhanced user profiles with SAML identity enrichment and additional email data
    • Implemented two-phase pagination workflow for organization member listing
  • Tests

    • Added comprehensive test coverage for outside collaborator enumeration
    • Updated existing tests to support new user profile parameters and pagination flow

@aldevv aldevv requested a review from a team March 9, 2026 19:37
@coderabbitai
Copy link

coderabbitai bot commented Mar 9, 2026

📝 Walkthrough

Walkthrough

Added two-phase user listing (org members then outside collaborators), extended userResource to include an isOutsideCollaborator flag, added pagination helpers, SAML enrichment via GraphQL, and test/mock support for outside-collaborators.

Changes

Cohort / File(s) Summary
Pagination Helpers
pkg/connector/helpers.go
Added isOutsideCollaboratorPhase and membersNextPageToken to manage two-phase pagination state and transitions.
Core User Logic & API
pkg/connector/user.go
Extended userResource signature with isOutsideCollaborator flag; added SAML enrichment (enrichUserWithSAML), detail fetch (fetchUserDetails), resource builders (buildUserResource, buildUserResources), listOutsideCollaboratorsPage, selectRateLimit, and refactored List to support two-phase traversal and combined rate-limit annotations.
Call-site Updates (Production)
pkg/connector/org.go, pkg/connector/org_role.go, pkg/connector/repository.go, pkg/connector/team.go
Updated all userResource call sites to pass an additional false boolean argument for the new isOutsideCollaborator parameter.
Call-site Updates (Tests)
pkg/connector/org_role_test.go, pkg/connector/org_test.go, pkg/connector/repository_test.go, pkg/connector/team_test.go
Updated test call sites to pass the new boolean argument to userResource.
User Tests
pkg/connector/user_test.go
Updated TestUsersList expectations for two-phase pagination and added TestUsersListWithOutsideCollaborators to validate member → outside-collaborator flow.
Mock Endpoint Pattern
test/mocks/endpointpattern.go
Added GetOrgsOutsideCollaboratorsByOrg endpoint pattern for the outside collaborators API.
Mock GitHub
test/mocks/github.go
Added outsideCollaboratorsByOrg storage, getOutsideCollaborators handler, methods AddUser and AddOutsideCollaborator, and wired the new endpoint into the mock server.

Sequence Diagram(s)

sequenceDiagram
  participant Client as Client
  participant Connector as Connector (List)
  participant Cache as orgCache
  participant REST as GitHub REST API
  participant GQL as GitHub GraphQL API

  rect rgba(200,200,255,0.5)
  Client->>Connector: List(org)
  Connector->>Cache: getOrgName(orgID)
  Cache-->>Connector: orgName
  Connector->>REST: GET /orgs/{org}/members (page=1)
  REST-->>Connector: members(page=1), nextPageToken
  Connector->>Connector: buildUserResources(members) (may call fetchUserDetails)
  Connector->>GQL: query SAML identities (per-user, optional)
  GQL-->>Connector: saml emails / rate-limit info
  Connector-->>Client: resources, NextPageToken (encodes members→outside phase)
  end

  rect rgba(200,255,200,0.5)
  Client->>Connector: List(org, NextPageToken)
  Connector->>REST: GET /orgs/{org}/outside_collaborators (page=1)
  REST-->>Connector: outside collaborators (page=1), no nextPage
  Connector->>Connector: buildUserResources(outside collaborators, isOutsideCollaborator=true)
  Connector-->>Client: resources, NextPageToken (empty)
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 Two-phase hop from member to guest,
I fetch SAML secrets and merge the best,
Tokens flip—then back they cease,
Outside friends join, and rate limits peace,
A tiny rabbit cheers this new feast! 🎉

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 28.57% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Sync outside collaborators as part of user List' directly and clearly summarizes the main change: extending the user List operation to include outside collaborators in addition to regular org members.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch add-outside-collaborators-to-user-sync

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link
Contributor

github-actions bot commented Mar 9, 2026

PR Review: Sync outside collaborators as part of user List

  • Read changed files and analyze code
  • Post inline comments
  • Post review summary

View job run

}
}

ur, err := userResource(ctx, u, userEmail, extraEmails, false)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: buildUserResource always hardcodes isOutsideCollaborator: false, which is correct since it's only called from the members phase. Worth a brief comment to make this explicit, since the outside collaborator path builds resources directly in listOutsideCollaboratorsPage.

(No action needed — just noting for future readers.)

annotations.WithRateLimiting(graphqlRateLimit)

var annos annotations.Annotations
annos.WithRateLimiting(rlDesc)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: The return value of annos.WithRateLimiting(rlDesc) is discarded here (same pattern as line 316). If WithRateLimiting returns an error, it would be silently ignored. I see this is the existing convention in this file, but worth confirming WithRateLimiting doesn't return a meaningful error.

@aldevv aldevv force-pushed the add-outside-collaborators-to-user-sync branch from 8ad076d to 039600c Compare March 9, 2026 19:40
@github-actions
Copy link
Contributor

github-actions bot commented Mar 9, 2026

Claude finished @aldevv's task in 2m 37s —— View job


PR Review: Sync outside collaborators as part of user List

Blocking Issues: 0 | Suggestions: 2

Security Issues

None found.

Correctness Issues

None found.

Suggestions

  • pkg/connector/helpers.go:119-121isOutsideCollaboratorPhase calls bag.Current() without a nil guard. Safe today due to parsePageToken always pushing a state, but a defensive check would make this less fragile. (comment)
  • pkg/connector/user.go:342-350selectRateLimit is a nice addition; minor note on when the aggregation matters most. (comment)

Notes

  • Two-phase pagination (members → outside collaborators) is correctly implemented with proper bag.Pop() / bag.Push() transitions and terminal "" token when done (R8). No infinite loop or early termination risk.
  • All callers of userResource correctly updated with the new isOutsideCollaborator parameter. No breaking changes to resource IDs, entitlement slugs, or parent hierarchy (B1-B8).
  • Code extraction into fetchUserDetails, enrichUserWithSAML, buildUserResource improves readability without changing behavior.
  • SAML enrichment is correctly skipped for outside collaborators — they are external to the org and don't have org-level SAML identities.
  • docs/connector.mdx does not need updating — "Accounts" sync capability already covers both members and outside collaborators.
  • The annos.WithRateLimiting() discarded return value (lines 250, 316) was already flagged in the existing review.
  • Test coverage is good: TestUsersList validates both phases across all SAML variants, and TestUsersListWithOutsideCollaborators verifies seeded collaborators are returned in phase 2.

  • Read changed files and analyze code
  • Post inline comments
  • Post review summary

Comment on lines +342 to +350
func selectRateLimit(rest, graphql *v2.RateLimitDescription) *v2.RateLimitDescription {
if rest == nil {
return graphql
}
if graphql != nil && graphql.Remaining < rest.Remaining {
return graphql
}
return rest
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: The selectRateLimit function picks the most constrained rate limit, which is a nice improvement over the previous approach. However, the two phases (members vs outside collaborators) hit the same REST API quota, so the rate limit from the last response already reflects cumulative usage. This aggregation is more useful when comparing REST vs GraphQL quotas (as done in the members phase). Here it's fine — just noting for clarity.

Comment on lines +119 to +121
func isOutsideCollaboratorPhase(bag *pagination.Bag) bool {
return bag.Current().ResourceTypeID == outsideCollaboratorPhase
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Suggestion: bag.Current() is called without a nil guard. This is safe today because parsePageToken always pushes a state when bag.Current() is nil, but it's a subtle precondition. A defensive nil check (or a comment documenting the precondition) would make this less fragile if the call chain ever changes.

Suggested change
func isOutsideCollaboratorPhase(bag *pagination.Bag) bool {
return bag.Current().ResourceTypeID == outsideCollaboratorPhase
}
func isOutsideCollaboratorPhase(bag *pagination.Bag) bool {
current := bag.Current()
return current != nil && current.ResourceTypeID == outsideCollaboratorPhase
}

Copy link
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blocking issues found.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
pkg/connector/repository.go (1)

171-177: ⚠️ Potential issue | 🟠 Major

Hard-coding false on line 211 conflicts with Affiliation: "all" on line 172.

The collaborator list request uses Affiliation: "all", which per GitHub documentation returns outside collaborators along with organization members. However, line 211 hard-codes isOutsideCollaborator: false for all returned users, causing the flag to be incorrect for any outside collaborators in the result. This creates inconsistency with the canonical user resource state. Either detect and pass the correct isOutsideCollaborator value for each user, or use a filtered affiliation parameter that excludes the types you don't want.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/connector/repository.go` around lines 171 - 177, The code sets
ListCollaboratorsOptions{Affiliation: "all"} but later hard-codes
isOutsideCollaborator = false; either make the affiliation narrower or compute
the flag per-user: if you intend to exclude outside collaborators, change
Affiliation from "all" to "direct"; otherwise, remove the hard-coded
isOutsideCollaborator and set it per returned collaborator by querying the
collaborator metadata (or calling the GitHub API/isOutsideCollaborator helper)
when mapping results—update the mapping in repository.go where
isOutsideCollaborator is assigned so it reflects the actual collaborator type
instead of always false.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@pkg/connector/user.go`:
- Around line 42-47: The profile map currently converts user.GetID() by
narrowing to int then strconv.Itoa, which can truncate large GitHub IDs on
32-bit systems; replace that conversion by calling
strconv.FormatInt(user.GetID(), 10) (or fmt.Sprintf("%d", user.GetID())) so the
ID is converted from its int64 form without narrowing—update the "user_id" value
in the profile map in pkg/connector/user.go (where user.GetID() is used) to use
strconv.FormatInt(user.GetID(), 10).
- Around line 154-169: samlIdent.NameId is being assumed to be a valid email;
instead initialize primaryEmail and setPrimary only after validating NameId with
isEmail. Replace the unconditional assignment of primaryEmail = samlIdent.NameId
and setPrimary = primaryEmail != "" with logic that checks
isEmail(samlIdent.NameId) first (if valid, set primaryEmail and
setPrimary=true), otherwise leave primaryEmail empty and setPrimary=false so the
subsequent loop over samlIdent.Emails can select the first valid email into
primaryEmail and append others to extraEmails; keep references to samlIdent,
NameId, primaryEmail, setPrimary, isEmail, and extraEmails unchanged.
- Around line 144-148: The RateLimitDescription created in pkg/connector/user.go
(rlDesc of type v2.RateLimitDescription) does not set the Status field; update
its construction to set Status to v2.RateLimitStatus_STATUS_OK when q.RateLimit
indicates available budget (e.g., Remaining > 0) and to
v2.RateLimitStatus_STATUS_OVERLIMIT when budget is exhausted (e.g., Remaining <=
0 or a corresponding flag on q.RateLimit), matching the pattern used in
extractRateLimitData, rateLimitDescriptionFromRate, and
rateLimitDescriptionFromRetryAfter so selectRateLimit returns a usable status.

---

Outside diff comments:
In `@pkg/connector/repository.go`:
- Around line 171-177: The code sets ListCollaboratorsOptions{Affiliation:
"all"} but later hard-codes isOutsideCollaborator = false; either make the
affiliation narrower or compute the flag per-user: if you intend to exclude
outside collaborators, change Affiliation from "all" to "direct"; otherwise,
remove the hard-coded isOutsideCollaborator and set it per returned collaborator
by querying the collaborator metadata (or calling the GitHub
API/isOutsideCollaborator helper) when mapping results—update the mapping in
repository.go where isOutsideCollaborator is assigned so it reflects the actual
collaborator type instead of always false.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 270a32ab-7990-4dee-bf3e-b7676dd1fb04

📥 Commits

Reviewing files that changed from the base of the PR and between 8077bc3 and 8ad076d.

📒 Files selected for processing (13)
  • pkg/connector/helpers.go
  • pkg/connector/org.go
  • pkg/connector/org_role.go
  • pkg/connector/org_role_test.go
  • pkg/connector/org_test.go
  • pkg/connector/repository.go
  • pkg/connector/repository_test.go
  • pkg/connector/team.go
  • pkg/connector/team_test.go
  • pkg/connector/user.go
  • pkg/connector/user_test.go
  • test/mocks/endpointpattern.go
  • test/mocks/github.go

Comment on lines 42 to +47
profile := map[string]interface{}{
"first_name": firstName,
"last_name": lastName,
"login": user.GetLogin(),
"user_id": strconv.Itoa(int(user.GetID())),
"first_name": firstName,
"last_name": lastName,
"login": user.GetLogin(),
"user_id": strconv.Itoa(int(user.GetID())),
"is_outside_collaborator": isOutsideCollaborator,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Use FormatInt instead of narrowing the GitHub ID to int.

Line 46 narrows user.GetID() to int before stringifying it. Go's int is architecture-sized, non-constant integer conversions truncate to the destination width, and strconv.FormatInt already accepts int64 directly, so this can corrupt large GitHub IDs on 32-bit builds. (go.dev)

Suggested fix
 	profile := map[string]interface{}{
 		"first_name":              firstName,
 		"last_name":               lastName,
 		"login":                   user.GetLogin(),
-		"user_id":                 strconv.Itoa(int(user.GetID())),
+		"user_id":                 strconv.FormatInt(user.GetID(), 10),
 		"is_outside_collaborator": isOutsideCollaborator,
 	}

As per coding guidelines, "Use %d or strconv for numeric IDs instead of %v or %g to avoid scientific notation with large IDs (e.g., 1.23e+15)".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/connector/user.go` around lines 42 - 47, The profile map currently
converts user.GetID() by narrowing to int then strconv.Itoa, which can truncate
large GitHub IDs on 32-bit systems; replace that conversion by calling
strconv.FormatInt(user.GetID(), 10) (or fmt.Sprintf("%d", user.GetID())) so the
ID is converted from its int64 form without narrowing—update the "user_id" value
in the profile map in pkg/connector/user.go (where user.GetID() is used) to use
strconv.FormatInt(user.GetID(), 10).

Comment on lines +144 to +148
rlDesc := &v2.RateLimitDescription{
Limit: int64(q.RateLimit.Limit),
Remaining: int64(q.RateLimit.Remaining),
ResetAt: timestamppb.New(q.RateLimit.ResetAt.Time),
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

find . -name "user.go" -path "*/connector/*" | head -5

Repository: ConductorOne/baton-github

Length of output: 92


🏁 Script executed:

sed -n '140,155n' ./pkg/connector/user.go

Repository: ConductorOne/baton-github

Length of output: 51


🏁 Script executed:

cat -n ./pkg/connector/user.go | sed -n '140,155p'

Repository: ConductorOne/baton-github

Length of output: 635


🏁 Script executed:

find . -name "*.proto" | xargs grep -l "RateLimitDescription" | head -5

Repository: ConductorOne/baton-github

Length of output: 51


🏁 Script executed:

cat -n ./pkg/connector/user.go | head -30

Repository: ConductorOne/baton-github

Length of output: 1241


🏁 Script executed:

rg "RateLimitDescription" ./pkg/connector/ -A 5 -B 2

Repository: ConductorOne/baton-github

Length of output: 6491


🏁 Script executed:

rg "func selectRateLimit" ./pkg/connector/ -A 10

Repository: ConductorOne/baton-github

Length of output: 587


Populate Status on the GraphQL rate-limit descriptor.

RateLimitDescription.Status defaults to STATUS_UNSPECIFIED (0). This constructor only fills Limit, Remaining, and ResetAt, so selectRateLimit can return a GraphQL descriptor with no usable status even when the GraphQL budget is exhausted. The codebase consistently sets Status to STATUS_OK or STATUS_OVERLIMIT in other rate limit creation helpers (extractRateLimitData, rateLimitDescriptionFromRate, rateLimitDescriptionFromRetryAfter); this location should follow the same pattern.

Suggested fix
 	rlDesc := &v2.RateLimitDescription{
+		Status:    v2.RateLimitDescription_STATUS_OK,
 		Limit:     int64(q.RateLimit.Limit),
 		Remaining: int64(q.RateLimit.Remaining),
 		ResetAt:   timestamppb.New(q.RateLimit.ResetAt.Time),
 	}
+	if q.RateLimit.Remaining <= 0 {
+		rlDesc.Status = v2.RateLimitDescription_STATUS_OVERLIMIT
+	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
rlDesc := &v2.RateLimitDescription{
Limit: int64(q.RateLimit.Limit),
Remaining: int64(q.RateLimit.Remaining),
ResetAt: timestamppb.New(q.RateLimit.ResetAt.Time),
}
rlDesc := &v2.RateLimitDescription{
Status: v2.RateLimitDescription_STATUS_OK,
Limit: int64(q.RateLimit.Limit),
Remaining: int64(q.RateLimit.Remaining),
ResetAt: timestamppb.New(q.RateLimit.ResetAt.Time),
}
if q.RateLimit.Remaining <= 0 {
rlDesc.Status = v2.RateLimitDescription_STATUS_OVERLIMIT
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/connector/user.go` around lines 144 - 148, The RateLimitDescription
created in pkg/connector/user.go (rlDesc of type v2.RateLimitDescription) does
not set the Status field; update its construction to set Status to
v2.RateLimitStatus_STATUS_OK when q.RateLimit indicates available budget (e.g.,
Remaining > 0) and to v2.RateLimitStatus_STATUS_OVERLIMIT when budget is
exhausted (e.g., Remaining <= 0 or a corresponding flag on q.RateLimit),
matching the pattern used in extractRateLimitData, rateLimitDescriptionFromRate,
and rateLimitDescriptionFromRetryAfter so selectRateLimit returns a usable
status.

Comment on lines +154 to +169
samlIdent := q.Organization.SamlIdentityProvider.ExternalIdentities.Edges[0].Node.SamlIdentity
primaryEmail := samlIdent.NameId
setPrimary := primaryEmail != ""

var extraEmails []string
for _, email := range samlIdent.Emails {
if !isEmail(email.Value) {
continue
}
if !setPrimary {
primaryEmail = email.Value
setPrimary = true
} else {
extraEmails = append(extraEmails, email.Value)
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's check the file exists and read the relevant lines
cat -n pkg/connector/user.go | sed -n '140,180p'

Repository: ConductorOne/baton-github

Length of output: 1577


🏁 Script executed:

# Let's also find the isEmail function definition
rg "func isEmail" -A 5 pkg/connector/

Repository: ConductorOne/baton-github

Length of output: 445


🏁 Script executed:

# Check the file structure to understand the broader context
head -50 pkg/connector/user.go

Repository: ConductorOne/baton-github

Length of output: 1545


🏁 Script executed:

# Search for tests related to this SAML identity logic
rg "getSAMLIdentityUserAndEmail|samlIdent|NameId" -t go pkg/connector/

Repository: ConductorOne/baton-github

Length of output: 352


🏁 Script executed:

# Look for any test files that test this function
fd -e _test.go pkg/connector/ | xargs grep -l "SAML\|samlIdent" 2>/dev/null || echo "No test matches found"

Repository: ConductorOne/baton-github

Length of output: 469


🏁 Script executed:

# Check the function signature and any comments around it
cat -n pkg/connector/user.go | sed -n '100,155p'

Repository: ConductorOne/baton-github

Length of output: 2716


Validate NameId before using it as the primary email.

Line 155 treats samlIdent.NameId as an email without validation, but SAML NameID is not guaranteed to be an email format. GitHub's SAML documentation notes that NameID is "typically" (not always) an email. The current logic causes an inconsistency: if NameId is non-empty but not a valid email, setPrimary becomes true (line 156), which prevents any valid email from samlIdent.Emails from being assigned as primary—they get added to extraEmails instead.

Additionally, the loop validates emails from samlIdent.Emails using isEmail() (line 160), but NameId receives no such validation. Add the same validation for NameId:

Suggested fix
 	samlIdent := q.Organization.SamlIdentityProvider.ExternalIdentities.Edges[0].Node.SamlIdentity
-	primaryEmail := samlIdent.NameId
-	setPrimary := primaryEmail != ""
+	primaryEmail := ""
+	setPrimary := false
+	if isEmail(samlIdent.NameId) {
+		primaryEmail = samlIdent.NameId
+		setPrimary = true
+	}
 
 	var extraEmails []string
 	for _, email := range samlIdent.Emails {
 		if !isEmail(email.Value) {
 			continue
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
samlIdent := q.Organization.SamlIdentityProvider.ExternalIdentities.Edges[0].Node.SamlIdentity
primaryEmail := samlIdent.NameId
setPrimary := primaryEmail != ""
var extraEmails []string
for _, email := range samlIdent.Emails {
if !isEmail(email.Value) {
continue
}
if !setPrimary {
primaryEmail = email.Value
setPrimary = true
} else {
extraEmails = append(extraEmails, email.Value)
}
}
samlIdent := q.Organization.SamlIdentityProvider.ExternalIdentities.Edges[0].Node.SamlIdentity
primaryEmail := ""
setPrimary := false
if isEmail(samlIdent.NameId) {
primaryEmail = samlIdent.NameId
setPrimary = true
}
var extraEmails []string
for _, email := range samlIdent.Emails {
if !isEmail(email.Value) {
continue
}
if !setPrimary {
primaryEmail = email.Value
setPrimary = true
} else {
extraEmails = append(extraEmails, email.Value)
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/connector/user.go` around lines 154 - 169, samlIdent.NameId is being
assumed to be a valid email; instead initialize primaryEmail and setPrimary only
after validating NameId with isEmail. Replace the unconditional assignment of
primaryEmail = samlIdent.NameId and setPrimary = primaryEmail != "" with logic
that checks isEmail(samlIdent.NameId) first (if valid, set primaryEmail and
setPrimary=true), otherwise leave primaryEmail empty and setPrimary=false so the
subsequent loop over samlIdent.Emails can select the first valid email into
primaryEmail and append others to extraEmails; keep references to samlIdent,
NameId, primaryEmail, setPrimary, isEmail, and extraEmails unchanged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant