Skip to content

[CXH-2123] - Add provisioning for the Enterprise Owner role - GitHub Connector - #194

Open
mateoHernandez123 wants to merge 22 commits into
mainfrom
mateoHernandez123/github-enterprise-owner-provisioning
Open

mateoHernandez123 wants to merge 22 commits into
mainfrom
mateoHernandez123/github-enterprise-owner-provisioning

Conversation

@mateoHernandez123

@mateoHernandez123 mateoHernandez123 commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

Description

  • Bug fix
  • New feature

Adds Grant and Revoke for the built-in Enterprise Owner role under GitHub App authentication, which CXH-2123 asks for on behalf of DoorDash: they want that role requestable and time-bound from C1.

The ticket's implementation note pointed at a GraphQL mutation called updateEnterpriseOwnerMembership. That mutation does not exist, and the real model is more involved: GitHub has no single operation that assigns Owner. A member becomes one by accepting an invitation, and GitHub rejects that invitation outright when the user already holds an enterprise administrator role such as Billing manager. An installation token cannot read that prior role — Enterprise.ownerInfo resolves to null — so the connector refuses that case with FailedPrecondition rather than promoting in place: Revoke could only demote to UNAFFILIATED, discarding a role the grant never gave. I corrected the ticket description with what the API actually offers.

Contrary to the original support thread, this does not require a personal access token. It works with a GitHub App installed on both the enterprise account and the organization.

Sync:

  • Enterprise roles (enterprise_role) — under GitHub App authentication the connector now lists the built-in Owner role and emits its grants. Owners are read from Organization.enterpriseOwners with the organization installation token; Enterprise.members(role: OWNER) looks like the right field but returns owners of organizations inside the enterprise, not owners of the enterprise account. Under PAT authentication the resource type is unchanged.
  • Pending invitations are emitted as grants on the same assigned entitlement as accepted owners. C1 has no pending state for a grant, so an invitee is indistinguishable from a real owner until they accept or the invitation lapses. That is a deliberate trade: emitting nothing would leave the request invisible in C1 for up to seven days with no record that it was made. An access review or offboarding sweep will count an invitee as holding Owner — documented in docs/docs-info.md and in the enterprise connector docs.
  • Every other resource type is an unchanged surface.

Provisioning:

  • Grant/Revoke Enterprise Owner (NEW) — Grant invites. A user GitHub refuses to invite, because they already hold an administrator role, is rejected rather than promoted in place, since the prior role is unreadable and Revoke would discard it. Revoke clears both states rather than treating them as alternatives: it demotes an active owner to UNAFFILIATED, which keeps their enterprise membership instead of evicting them, and cancels an unaccepted invitation.
  • Idempotency: GrantAlreadyExists when the user already holds the role or already has a pending invitation, GrantAlreadyRevoked when neither is present. A NOT_FOUND from either mutation is treated as success, because it means the requested state is already in place.
  • Both operations verify the resulting state and refuse to report a grant or revoke that GitHub did not apply.

What C1 shows for each state:

C1 has no pending state for a grant — it either exists or it does not — so an invitation and an accepted owner map onto the same grant:

State in GitHub What C1 shows
Invited, not accepted yet Owner grant
Invitation accepted, active owner Owner grant, same grant ID
Invitation cancelled or lapsed Grant disappears on the next sync
Never invited No grant

The grant ID being stable across the second row matters: if it changed on acceptance, C1 would read the transition as a revoke followed by a new grant and would corrupt the history exactly where it is most useful. Nothing tracks an expiry — GitHub stops resolving an invitation once it is accepted, cancelled or expired, so it simply stops being emitted.

Every row was exercised against a live GitHub Enterprise Cloud account with the app installed on both levels, driven end to end through a full ConductorOne stack: granting to an org member created the invitation and C1 kept the grant across the following sync, a teammate accepting it turned them into an active owner under the same grant ID, and revoking cleared each state through its own mutation. Also covered live: re-granting while an invitation is pending returns GrantAlreadyExists without sending a second invitation, revoking with nothing to revoke reports success rather than an error, and revoking an active owner leaves their enterprise membership intact.

Auth:

Enterprise Owner provisioning is opt-in: --enable-enterprise-owner-provisioning, off by default. While it is off the connector behaves exactly as it did before this change, so upgrading cannot break an existing deployment. Turning it on requires GitHub App authentication with the app installed on both the enterprise account and the organization, and the Enterprise → People: Read and write permission.

Once enabled, a missing enterprise installation fails the sync rather than emitting no owners. That is deliberate: C1 deletes every resource of a type that a completed sync did not report, so finishing the sync while reading nothing would silently drop the Owner role and every grant on it — and GitHub answers 404 for an uninstalled app, a revoked permission and a slug typo alike, so the connector cannot tell them apart. Failing keeps the sync from completing, so nothing is deleted. The flag is what confines that failure to operators who asked for the capability.

Only one enterprise can be served per connector under App authentication, because owners are read through the single configured organization and an organization belongs to exactly one enterprise. A configuration naming several is rejected with an explanatory error. The PAT path still accepts a list.

Architecture highlights:

  • Pending invitations are not enumerable. Enterprise.ownerInfo.pendingAdminInvitations is the only connection GitHub offers and it is null for installation tokens, so the sync resolves invitations by asking about the enterprise members, aliasing up to 100 logins into one request — measured at a single rate-limit point. An invitation addressed to somebody outside the enterprise is therefore invisible to the sync; invitations created from C1 are always visible, because C1 grants to a user it has already synced.
  • Grants() walks owners and invitations as two phases of one page token via pagination.Bag.
  • A GraphQL transport classifies the errors GitHub returns alongside an HTTP 200, so a rate limit reaches the SDK as a retryable Unavailable. It is layered only on the enterprise clients: the shared GraphQL client is untouched because user.go detects enterprise SAML by matching that error's text. The aliased batch deliberately bypasses it, since it always carries NOT_FOUND entries, and filters those out before classifying the rest — leaving them in would let them claim the code for the whole response, and NOT_FOUND is the one code the SDK downgrades to a warning.
  • customclient now resolves URLs against the go-github client's BaseURL and escapes each path segment, so these endpoints follow --instance-url instead of hardcoding api.github.com.
  • The GraphQL endpoint derivation that existed in three places is now one helper.
  • Not fixed here: the license resource type still cannot sync under GitHub App authentication, because GitHub does not offer the enterprise_administration permission to Apps. Pre-existing and unrelated to this change, but it means that type has to stay disabled when running the App path with enterprises configured.

Useful links:

@linear-code

linear-code Bot commented Sep 22, 2026

Copy link
Copy Markdown

CXH-2123

Comment thread pkg/connector/enterprise_role.go
Comment thread pkg/connector/connector.go Outdated
Comment thread pkg/customclient/client.go
Comment thread pkg/connector/enterprise_role.go
@github-actions

github-actions Bot commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor
Superseded — see the current review report for commit 8943329e11c5

Connector PR Review: [CXH-2123] - Add provisioning for the Enterprise Owner role - GitHub Connector

Blocking Issues: 0 | Suggestions: 3 | Threads Resolved: 0
Criteria: Criteria status: loaded .claude/skills/ci-review.md from trusted base 1b8c8f878539.
Review mode: full
View review run

Review Summary

When --enable-enterprise-owner-provisioning is set under GitHub App auth, the connector now builds per-enterprise GraphQL clients lazily. With those clients it syncs the built-in Owner role (owners, then pending invitations resolved through one aliased batch, as two phases of one pagination.Bag token) and registers a separate enterpriseRoleProvisioner that adds Grant and Revoke. Grant invites and refuses in-place promotion; Revoke demotes to UNAFFILIATED and cancels the invitation. Both re-read OwnerState to confirm the change landed. PAT and flag-off paths register the read-only syncer plus license as before. I scanned the full diff for security and correctness and re-checked all 19 prior findings against the current code. I applied the repo criteria: provisioning entity sources (principal → login, entitlement resource → enterprise, no ParentResourceId use), idempotency annotations, gRPC codes on provisioning errors (E2/E3), log levels (A), and the breaking-change gate (BP1/BP5). The new behavior is opt-in and off by default, so it is not an ungated breaking change. go.mod/go.sum are unchanged.

Security Issues

None found. The aliased invitation query passes logins only as GraphQL variables, and the customclient path segments are escaped.

Correctness Issues

None found.

Suggestions

  • Prior — still present (confidence: medium) pkg/connector/enterprise_role.go:438-445: Organization.enterpriseOwners and Enterprise.members cover the whole enterprise account, but user only syncs members of the configured org. An owner from another org in the enterprise gets a grant whose principal was never synced. The author chose to document this in docs/docs-info.md:272 rather than filter, so it stays open as a known limitation.
  • Prior — still present (confidence: medium) pkg/config/config.go:461-464: EnterprisesField and enable-enterprise-owner-provisioning now appear in this connector's GitHub App form, but the App setup steps in docs/connector.mdx (~lines 292-317) don't mention either one (D4). The author declined because enterprise setup is documented in the GitHub Enterprise connector. That argument is weaker now that both fields show up in this connector's own form.
  • Prior — still present (confidence: low) pkg/connector/connector.go:314: when consumed-licenses fails under App auth (a guaranteed later license sync failure with the flag off), the connector logs it at Debug, while repo criteria L1 calls for Warn. The Debug level is inherited from main, and the author declined because of a team-level rule against Warn.

Resolved prior findings

  • Provision capability advertised under PAT (2 threads): fixed. The capability now comes from the separate enterpriseRoleProvisioner type (pkg/connector/enterprise_role.go:284-301), which ResourceSyncers registers only when newEnterpriseRoleClients != nil (pkg/connector/connector.go:501-510).
  • appTokenRefresher held the per-RPC ctx: fixed. It now captures connectorCtx (pkg/connector/connector.go:583), which the refresher and newGitHubAppHTTPClient use.
  • Nil BaseHttpClient from uhttp.NewBaseHttpClient: fixed. Both construction sites nil-check it (pkg/connector/connector.go:661-664, pkg/connector/enterprise_administrator_client.go:106-109).
  • OwnerState fell through its page bound and reported "not an owner": fixed. It now returns codes.Internal when it hits the bound (pkg/connector/enterprise_administrator_client.go:620-624).
  • clients() error aborting the whole sync contradicted the code comment: fixed. Failing the sync is now the documented intent, and the comments at pkg/connector/enterprise_role.go:54-57/65-74 and pkg/connector/connector.go:573-578 state it and give the C1 deletion reason.
  • Unbounded installation walk; 404 skip logged at Debug; hard startup failure with two enterprises; memoized empty or permanent results; isPermanentError missing transient errors (outdated threads): obsolete.
    • listEnterpriseInstallations, isPermanentError and enterpriseClientsSet no longer exist.
    • Discovery is now one direct GetEnterpriseInstallation call per enterprise. A 404 becomes an explicit FailedPrecondition, and the multi-enterprise check is lazy and opt-in only (pkg/connector/connector.go:652-677).
    • clients() memoizes only success (pkg/connector/enterprise_role.go:85-95).
  • Stale "documented fallback" comment: fixed at pkg/connector/graphql_transport.go:3487-3490 of the diff; the comment now says Grant refuses a rejected invitation.
  • customclient comment claimed PathEscape blocks ..: fixed. The comment now says escaping covers slashes only (pkg/customclient/client.go endpoint doc).
  • GetEnterpriseInstallation error path did not close the response body: fixed. defer res.Body.Close() now runs before logBody.
Prompt for AI agents
Verify each finding against the current code and only fix it if needed.

## Suggestions

In `pkg/connector/enterprise_role.go`:
- Around line 438-445 (ownerGrants) and 485-495 (pendingInvitationGrants): owners and invitation candidates span the whole enterprise, while the user resource type only syncs members of the configured org, so some grants point at principals the sync never created. Either skip owners whose database ID isn't among the synced org members, or keep the documented limitation in docs/docs-info.md. Don't change behavior silently.

In `pkg/config/config.go`:
- Around line 461-464: the GitHub App field group now includes EnterprisesField and enable-enterprise-owner-provisioning. Add both optional fields to the GitHub App configuration steps in docs/connector.mdx (~lines 292-317), or point to the GitHub Enterprise integration page for them, so the docs match the fields shown in this connector's form.

In `pkg/connector/connector.go`:
- Around line 314: consumed-licenses failing under App auth means the license resource type will fail the sync. Consider logging it at Warn per repo criteria L1, or leave it at Debug if the team logging policy overrides the repo criteria.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

No blocking issues found.

mateoHernandez123 and others added 2 commits September 22, 2026 15:36
@mateoHernandez123
mateoHernandez123 force-pushed the mateoHernandez123/github-enterprise-owner-provisioning branch from 8738a07 to 04872b2 Compare September 22, 2026 18:37
Comment thread pkg/connector/connector.go
Comment thread pkg/connector/enterprise_administrator_client.go
Comment thread pkg/customclient/client.go

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Blocking issues found — see review comments.

Comment thread pkg/connector/connector.go

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

No blocking issues found.

…the path

Co-authored-by: Cursor <cursoragent@cursor.com>
Comment thread pkg/config/config.go

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

No blocking issues found.

@sergiocorral-conductorone sergiocorral-conductorone left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Automated review — draft by Opus, cross-verified by Codex (gpt-5.3-codex-high).

Adds opt-in (--enable-enterprise-owner-provisioning) Grant/Revoke for the built-in Enterprise Owner role under GitHub App auth, via GraphQL (inviteEnterpriseAdmin with fallback to updateEnterpriseAdministratorRole, demote to UNAFFILIATED / cancelEnterpriseAdminInvitation on revoke), plus App-path sync of owners and pending invitations. Linked ticket CXH-2123 found; both itemized work items (GraphQL client calls, Grant/Revoke on the enterprise role builder) are covered, and the ticket's implementation notes match the code. Idempotency is handled well — state is read before and after each mutation, GrantAlreadyExists/GrantAlreadyRevoked are returned correctly, NOT_FOUND on revoke counts as success, paging is bounded, errors are wrapped, rate limits annotated, CI is green.

The concerns are about deployability and blast radius, not the mutations themselves:

  • High: turning the feature on requires --enterprises under App auth, which also registers the license syncer — and that syncer hard-fails under App auth, so the documented setup for this feature doesn't actually sync.
  • Medium: Grant/Revoke also gets advertised on enterprise_role for existing PAT deployments that can never provision it — every request would fail with FailedPrecondition.
  • Medium, low-confidence: the pending-invitation pass scans the whole enterprise membership (not just the configured org) and can emit grants for principals the sync never created.
  • Low: a rejected InviteOwner error is only Debug-logged before falling through to UpdateRole, losing the real rejection reason from the operator-visible error.
  • Low: a code comment claims a resource-type failure doesn't fail the whole sync, but the fail-closed behavior this PR depends on for safety requires exactly that — the comment contradicts the actual (correct) guarantee and invites a future regression.

Verification: 4 confirmed, 1 uncertain (kept, tagged low-confidence) — no new majors from the verifier.

Comment thread pkg/connector/connector.go Outdated
Comment thread pkg/connector/enterprise_role.go Outdated
Comment thread pkg/connector/enterprise_role.go
Comment thread pkg/connector/enterprise_role.go
Comment thread pkg/connector/connector.go Outdated
… be used

Co-authored-by: Cursor <cursoragent@cursor.com>
Comment thread pkg/connector/connector.go

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

No blocking issues found.

Co-authored-by: Cursor <cursoragent@cursor.com>

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

No blocking issues found.

… are opted into

Co-authored-by: Cursor <cursoragent@cursor.com>
Comment thread pkg/connector/connector.go Outdated
Comment thread docs/docs-info.md Outdated
Comment thread docs/docs-info.md Outdated

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

No blocking issues found.

Co-authored-by: Cursor <cursoragent@cursor.com>

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

No blocking issues found.

… it removed

Co-authored-by: Cursor <cursoragent@cursor.com>

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

No blocking issues found.

@sergiocorral-conductorone sergiocorral-conductorone left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Nightshift automated review — Re-review at e28eb7b (20 commits since our last pass on a5b1304).

Comment thread pkg/config/config.go
Comment thread pkg/connector/enterprise_role.go
Comment thread pkg/connector/enterprise_role.go
Co-authored-by: Cursor <cursoragent@cursor.com>
Comment thread pkg/connector/graphql_transport.go Outdated

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

No blocking issues found.

Co-authored-by: Cursor <cursoragent@cursor.com>

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

No blocking issues found.

@github-actions

github-actions Bot commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Connector PR Review: [CXH-2123] - Add provisioning for the Enterprise Owner role - GitHub Connector

Blocking Issues: 0 | Suggestions: 3 | Threads Resolved: 0
Criteria: Criteria status: loaded .claude/skills/ci-review.md from trusted base 1b8c8f878539.
Review mode: full
View review run

Review Summary

When --enable-enterprise-owner-provisioning is set under GitHub App auth, the connector now builds per-enterprise GraphQL clients lazily. With those clients it syncs the built-in Owner role (owners, then pending invitations resolved through one aliased batch, as two phases of one pagination.Bag token) and registers a separate enterpriseRoleProvisioner that adds Grant and Revoke. Grant invites and refuses in-place promotion; Revoke demotes to UNAFFILIATED and cancels the invitation. Both re-read OwnerState to confirm the change landed. PAT and flag-off paths register the read-only syncer plus license as before. I scanned the full diff for security and correctness and re-checked all 19 prior findings against the current code. I applied the repo criteria: provisioning entity sources (principal → login, entitlement resource → enterprise, no ParentResourceId use), idempotency annotations, gRPC codes on provisioning errors (E2/E3), log levels (A), and the breaking-change gate (BP1/BP5). The new behavior is opt-in and off by default, so it is not an ungated breaking change. go.mod/go.sum are unchanged.

Security Issues

None found. The aliased invitation query passes logins only as GraphQL variables, and the customclient path segments are escaped.

Correctness Issues

None found.

Suggestions

  • Prior — still present (confidence: medium) pkg/connector/enterprise_role.go:438-445: Organization.enterpriseOwners and Enterprise.members cover the whole enterprise account, but user only syncs members of the configured org. An owner from another org in the enterprise gets a grant whose principal was never synced. The author chose to document this in docs/docs-info.md:272 rather than filter, so it stays open as a known limitation.
  • Prior — still present (confidence: medium) pkg/config/config.go:461-464: EnterprisesField and enable-enterprise-owner-provisioning now appear in this connector's GitHub App form, but the App setup steps in docs/connector.mdx (~lines 292-317) don't mention either one (D4). The author declined because enterprise setup is documented in the GitHub Enterprise connector. That argument is weaker now that both fields show up in this connector's own form.
  • Prior — still present (confidence: low) pkg/connector/connector.go:314: when consumed-licenses fails under App auth (a guaranteed later license sync failure with the flag off), the connector logs it at Debug, while repo criteria L1 calls for Warn. The Debug level is inherited from main, and the author declined because of a team-level rule against Warn.

Resolved prior findings

  • Provision capability advertised under PAT (2 threads): fixed. The capability now comes from the separate enterpriseRoleProvisioner type (pkg/connector/enterprise_role.go:284-301), which ResourceSyncers registers only when newEnterpriseRoleClients != nil (pkg/connector/connector.go:501-510).
  • appTokenRefresher held the per-RPC ctx: fixed. It now captures connectorCtx (pkg/connector/connector.go:583), which the refresher and newGitHubAppHTTPClient use.
  • Nil BaseHttpClient from uhttp.NewBaseHttpClient: fixed. Both construction sites nil-check it (pkg/connector/connector.go:661-664, pkg/connector/enterprise_administrator_client.go:106-109).
  • OwnerState fell through its page bound and reported "not an owner": fixed. It now returns codes.Internal when it hits the bound (pkg/connector/enterprise_administrator_client.go:620-624).
  • clients() error aborting the whole sync contradicted the code comment: fixed. Failing the sync is now the documented intent, and the comments at pkg/connector/enterprise_role.go:54-57/65-74 and pkg/connector/connector.go:573-578 state it and give the C1 deletion reason.
  • Unbounded installation walk; 404 skip logged at Debug; hard startup failure with two enterprises; memoized empty or permanent results; isPermanentError missing transient errors (outdated threads): obsolete.
    • listEnterpriseInstallations, isPermanentError and enterpriseClientsSet no longer exist.
    • Discovery is now one direct GetEnterpriseInstallation call per enterprise. A 404 becomes an explicit FailedPrecondition, and the multi-enterprise check is lazy and opt-in only (pkg/connector/connector.go:652-677).
    • clients() memoizes only success (pkg/connector/enterprise_role.go:85-95).
  • Stale "documented fallback" comment: fixed at pkg/connector/graphql_transport.go:3487-3490 of the diff; the comment now says Grant refuses a rejected invitation.
  • customclient comment claimed PathEscape blocks ..: fixed. The comment now says escaping covers slashes only (pkg/customclient/client.go endpoint doc).
  • GetEnterpriseInstallation error path did not close the response body: fixed. defer res.Body.Close() now runs before logBody.
Prompt for AI agents
Verify each finding against the current code and only fix it if needed.

## Suggestions

In `pkg/connector/enterprise_role.go`:
- Around line 438-445 (ownerGrants) and 485-495 (pendingInvitationGrants): owners and invitation candidates span the whole enterprise, while the user resource type only syncs members of the configured org, so some grants point at principals the sync never created. Either skip owners whose database ID isn't among the synced org members, or keep the documented limitation in docs/docs-info.md. Don't change behavior silently.

In `pkg/config/config.go`:
- Around line 461-464: the GitHub App field group now includes EnterprisesField and enable-enterprise-owner-provisioning. Add both optional fields to the GitHub App configuration steps in docs/connector.mdx (~lines 292-317), or point to the GitHub Enterprise integration page for them, so the docs match the fields shown in this connector's form.

In `pkg/connector/connector.go`:
- Around line 314: consumed-licenses failing under App auth means the license resource type will fail the sync. Consider logging it at Warn per repo criteria L1, or leave it at Debug if the team logging policy overrides the repo criteria.

Reviewed commit: 8943329e11c5

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

No blocking issues found — see the full review report

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.

6 participants