feat: support enterprise SAML email sync via consumed licenses API#136
feat: support enterprise SAML email sync via consumed licenses API#136madison-c-evans wants to merge 3 commits intomainfrom
Conversation
|
Claude finished @madison-c-evans's task in 1m 49s —— View job PR Review: Enterprise SAML Email Sync
Overall: This is a well-structured refactor that replaces a fragile, reactive SAML detection path with an upfront, per-org state machine. The batched session store usage is a nice improvement. A few items to consider: Issues1. The old case samlStateEnterprise:
key := enterpriseSAMLKeyPrefix + strings.ToLower(ghUser.GetLogin())
if samlEmail, ok := enterpriseSAMLEmails[key]; ok {
userEmail = samlEmail // no isEmail() check
}SAML NameIDs aren't guaranteed to be email addresses (they can be opaque identifiers). The old code had 2. The 3. The Just noting: the PR description says "fetched once, shared across orgs via session store" — the fetch is guarded per-struct-instance, not per-session. If the SDK creates multiple Positive observations
Nits
|
ac1174d to
29ac121
Compare
|
Claude finished @madison-c-evans's task in 1m 48s —— View job PR Review: Enterprise SAML Email Sync
Overall: This is a clean refactor that replaces a fragile, reactive SAML fallback path with a well-structured per-org state machine ( Issues1. The old case samlStateEnterprise:
key := enterpriseSAMLKeyPrefix + strings.ToLower(ghUser.GetLogin())
if samlEmail, ok := enterpriseSAMLEmails[key]; ok {
userEmail = samlEmail // no isEmail() check
}SAML NameIDs aren't guaranteed to be email addresses — they can be opaque identifiers. The 2. The 3. At if q.Organization.SamlIdentityProvider.Id == "" {
if len(u.enterprises) > 0 {
// → samlStateEnterprise
}
}This means any org without SAML configured will trigger the consumed-licenses API fetch when enterprises are configured, even if SAML isn't set up at the enterprise level either. This is probably acceptable since the API will simply return users without SAML data, but it's a behavioral change from the old code that's worth calling out — the old code would have left 4. Error handling change in the user loop (low, but important to verify) The old code caught the enterprise SAML error inside the per-user GraphQL loop and gracefully fell back mid-iteration. The new code at err = u.graphqlClient.Query(ctx, &q, variables)
if err != nil {
return nil, nil, err // hard fail, no fallback
}This is correct because Positive observations
Nit
|
Rework the enterprise SAML fallback to correctly detect enterprise-managed SAML across multiple GitHub API behaviors: - Empty SAML provider ID + enterprises configured → enterprise SAML - SAML provider exists but SSO URL contains /enterprises/ → enterprise SAML - Explicit "SAML identity provider is disabled" error → enterprise SAML - SAML state is now cached per-org (not globally) to support mixed configs Refactor session store usage to avoid reads/writes inside loops: - fetchAndStoreEnterpriseSAML aggregates all mappings then writes once with SetManyJSON, plus a key index for bulk reads - loadEnterpriseSAMLEmails bulk-reads via GetManyJSON into a local map - User loop does plain Go map lookups with zero session calls Remove dead code: ensureEnterpriseSAMLLoaded, getEnterpriseSAMLEmail, enterpriseSAMLFetchedKey, unreachable enterprise fallback in the samlStateOrgEnabled case, and redundant err==nil guards. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
29ac121 to
49ce0a7
Compare
|
Claude finished @madison-c-evans's task in 1m 19s —— View job PR Review: Enterprise SAML Email Sync
Overall: This is a well-structured refactor. The Issues1. In This works correctly only if 2. At This is probably acceptable given the PR's goal, but worth noting in case it causes unexpected consumed-licenses API calls for non-SAML enterprise setups. 3. The PR description says "fetched once, shared across orgs via session store." The fetch is guarded by 4. Per-user GraphQL errors in At err = u.graphqlClient.Query(ctx, &q, variables)
if err != nil {
return nil, nil, err // hard fail
}This is correct because Positive observations
Nits
|
| }, nil | ||
| } | ||
|
|
||
| func apiTokenBuilder(client *github.Client, hasSAMLEnabled *bool, orgCache *orgNameCache) *apiTokenResourceType { |
There was a problem hiding this comment.
is hasSamlEnabled useless today?
There was a problem hiding this comment.
removed in 9a926e8. It was actually dead in apiTokenBuilder even before this PR (accepted but never stored or used). On the user side, it's replaced by the per-org samlStates map and checkOrgSAML
There was a problem hiding this comment.
Just to be clear have we recently introduced something the customer will have to change in order to continue to have their sync succeed? If so we cannot ship without customer success's help.
- Add missing samlStateUnknown case to exhaustive switch - Break long error line to stay under 200-char limit - Remove unused //nolint:gosec directive in mocks - Remove dead hasSamlEnabled from user test (addresses PR review) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
Claude finished @madison-c-evans's task in 1m 37s —— View job PR Review: Enterprise SAML Email Sync
Overall: This is a well-executed refactor. The Issues1. At if q.Organization.SamlIdentityProvider.Id == "" {
if len(u.enterprises) > 0 {
// → samlStateEnterprise
}
}This means any org without SAML configured will trigger the consumed-licenses API fetch whenever enterprises exist, even if SAML isn't set up at the enterprise level either. The old code would have set In practice, the API returns users without SAML data (so it's safe), but orgs with no SAML at all will now lose the REST API public email they previously got. Consider whether the 2. Per-user GraphQL errors in At err = u.graphqlClient.Query(ctx, &q, variables)
if err != nil {
return nil, nil, err
}This is correct because 3. At if strings.Contains(ssoUrl, "/enterprises/") {
if len(u.enterprises) > 0 {
u.samlStates[orgName] = samlStateEnterprise
} else {
u.samlStates[orgName] = samlStateDisabled
}
return u.samlStates[orgName], nil
}4. At Addressing the review commentRe: @Bencheng21's question on Yes, the Positive observations
Nit
|
| }, nil | ||
| } | ||
|
|
||
| func apiTokenBuilder(client *github.Client, hasSAMLEnabled *bool, orgCache *orgNameCache) *apiTokenResourceType { |
There was a problem hiding this comment.
Just to be clear have we recently introduced something the customer will have to change in order to continue to have their sync succeed? If so we cannot ship without customer success's help.
pkg/connector/connector.go
Outdated
| if len(gh.enterprises) > 0 { | ||
| _, _, err := gh.customClient.ListEnterpriseConsumedLicenses(ctx, gh.enterprises[0], 1) | ||
| if err != nil { | ||
| return nil, uhttp.WrapErrors( |
There was a problem hiding this comment.
This is likely incorrect. We should look at what the error is and only wrap with codes.PermissionDenied when we are sure it is actually a permission denied error and not a context cancelled or anything else. An err could be anything.
There was a problem hiding this comment.
re: Just to be clear have we recently introduced something the customer will have to change in order to continue to have their sync succeed? If so we cannot ship without customer success's help.
Upon further review, the new validateAppCredentials enterprise license probe hard-fails. I think best route is downgrade the validation to a warning. If consumed-licenses fails in validateAppCredentials, log a warning and skip enterprise SAML entirely (set a flag like enterpriseSAMLAvailable = false). Sync continues without SAML emails, matching old behavior. The fetchAndStoreEnterpriseSAML path would check this flag and skip
…cessible Instead of hard-failing during Validate when the consumed-licenses API returns an error (e.g., PermissionDenied for GitHub App installations), log a warning and skip enterprise SAML enrichment and enterprise role sync. This prevents breaking existing customers who have enterprises configured but lack enterprise admin scope. - Add enterpriseLicensesAvailable flag set during Validate - Gate enterpriseRoleBuilder and enterprise SAML on the flag - Gracefully fall back to REST API emails in user sync if fetchAndStoreEnterpriseSAML fails Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
Claude finished @madison-c-evans's task in 1m 32s —— View job PR Review: Enterprise SAML Email Sync
Overall: The latest commit ( Issues1. The // enterprise_role.go:63
return uhttp.WrapErrors(codes.PermissionDenied, fmt.Sprintf("baton-github: error listing enterprise consumed licenses for %s", enterprise), err)This has the same problem btipling identified in 2. SSO URL branches can be combined (low — readability)
if strings.Contains(ssoUrl, "/enterprises/") && len(u.enterprises) > 0 {
// → samlStateEnterprise
}
if strings.Contains(ssoUrl, "/enterprises/") && len(u.enterprises) == 0 {
// → samlStateDisabled
}These can be combined into a single block: if strings.Contains(ssoUrl, "/enterprises/") {
if len(u.enterprises) > 0 {
u.samlStates[orgName] = samlStateEnterprise
} else {
u.samlStates[orgName] = samlStateDisabled
}
return u.samlStates[orgName], nil
}3. At Positive observations
Re: btipling's review comments
|
Summary
Resolves CXH-1180
SetManyJSON) and reads (GetManyJSON) — zero session calls inside user loopsensureEnterpriseSAMLLoaded,getEnterpriseSAMLEmail, unreachable fallback pathsTest plan
mevans-testenterprise,mevans-test-org1andmevans-test-org2)go build ./...andgo test ./...pass🤖 Generated with Claude Code