feat(github): support team hierarchy in GH_TEAM_ALLOWLIST#6365
feat(github): support team hierarchy in GH_TEAM_ALLOWLIST#6365hussein-mimi wants to merge 20 commits intorunatlantis:mainfrom
Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds support for GitHub team hierarchy in the Atlantis allowlist. When a parent team is added to ATLANTIS_GH_TEAM_ALLOWLIST, users who are members of descendant (child/grandchild/etc.) teams are now correctly authorized. The implementation uses a duck-typed interface to support team hierarchies for VCS clients that support them (like GitHub), while remaining compatible with VCS clients that don't support this feature.
Changes:
- Added
GetChildTeams()method to the GitHub client that queries the GraphQL API for direct child teams with pagination support - Added
childTeamFetcherinterface for VCS clients supporting team hierarchies - Added
fetchDescendantTeams()recursive function with depth limiting to expand teams to all descendants - Updated
checkUserPermissions()with a two-path approach: fast path for direct membership, slow path for hierarchical expansion - Added test for
GetChildTeams()method
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| server/events/vcs/github/client.go | Added GetChildTeams() method to query GitHub's GraphQL API for direct child teams with pagination |
| server/events/vcs/github/client_test.go | Added TestClient_GetChildTeams() to verify the method correctly parses GraphQL responses |
| server/events/command_runner.go | Added childTeamFetcher interface, fetchDescendantTeams() function, and updated checkUserPermissions() to support team hierarchy expansion with a two-path authorization check |
73d098f to
bb0b23d
Compare
bb0b23d to
b92451e
Compare
6d9cb59 to
6dab523
Compare
When a parent team is added to ATLANTIS_GH_TEAM_ALLOWLIST, users who are members of any descendant (child/grandchild) team are now correctly authorized, instead of being rejected. The fix adds GetChildTeams to the GitHub client, which fetches direct child teams via GraphQL. In checkUserPermissions, after the fast-path direct-membership check fails, each allowlisted team is expanded to all its descendants (up to 20 levels deep) using recursive GetChildTeams calls. The user's direct teams are then checked against this expanded set. Non-GitHub VCS clients are unaffected. Fixes runatlantis#6107 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: hussein-mimi <hussein.mimi@harri.com>
…ions hierarchy Addresses the missing test coverage flagged by Copilot and code review: - TestFetchDescendantTeams: leaf team, single/multi-level nesting, maxDepth cutoff at 0 and 1, error propagation at root, and soft-fail on recursive errors while sibling subtrees continue - TestCheckUserPermissions: nil checker, direct member fast path, non-member rejection without hierarchy support, and table-driven slow-path cases (child team allowed, grandchild team allowed, unrelated team rejected, wrong command rejected, wildcard rule with no-team user) Also adds childTeamVCSClient test helper that satisfies both vcs.Client and childTeamFetcher, enabling the slow path to be exercised without a real VCS connection. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: hussein-mimi <hussein.mimi@harri.com>
…ntedGithubClient The team hierarchy slow path in checkUserPermissions asserts c.VCSClient against childTeamFetcher, but c.VCSClient is always a *ClientProxy wrapping an *InstrumentedGithubClient. Neither type implemented GetChildTeams, so the assertion always failed and hierarchy expansion was silently skipped. Add GetChildTeams to both ClientProxy and InstrumentedGithubClient so the interface is satisfied and calls are correctly delegated to the underlying *github.Client. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: hussein-mimi <hussein.mimi@harri.com>
When a user belongs to a child team of an allowlisted team, the global checkUserPermissions passes via the hierarchy slow path, but the per-project filter in buildAllCommandsByCfg/buildProjectCommandCtxs only does a direct team membership check. This caused child-team members to always see "Ran Plan for 0 projects:" even though the command-level check allowed them through. Fix by changing checkUserPermissions to accept *models.User and appending the matched allowlisted parent team to user.Teams when a hierarchy match is found. This ensures subsequent per-project allowlist checks (which use direct membership) also pass. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: hussein-mimi <hussein.mimi@harri.com>
77c3ffd to
f57331f
Compare
|
|
||
| // GetChildTeams delegates to the underlying VCS client if it supports fetching child teams | ||
| // (e.g. GitHub). Returns nil, nil for VCS providers that don't support team hierarchies. | ||
| func (d *ClientProxy) GetChildTeams(logger logging.SimpleLogging, repo models.Repo, teamSlug string) ([]string, error) { | ||
| type childTeamFetcher interface { | ||
| GetChildTeams(logging.SimpleLogging, models.Repo, string) ([]string, error) | ||
| } | ||
| if fetcher, ok := d.clients[repo.VCSHost.Type].(childTeamFetcher); ok { | ||
| return fetcher.GetChildTeams(logger, repo, teamSlug) | ||
| } | ||
| return nil, nil | ||
| } |
There was a problem hiding this comment.
Instead of using anonymous types and type assertion throughout, I think the preferred way to do this would be for GetChildTeams() should be added to the Client, then a stub added to all VCSs that returns nil. That way it's more explicit what's going on here and we don't rely on the indirection of the proxy, and it's more clear how to implement this feature on other VCSs.
There was a problem hiding this comment.
Good call — done in b1cd614. GetChildTeams is now a first-class method on the Client interface, with nil, nil stubs added to all non-GitHub providers (GitLab, Bitbucket Cloud/Server, Azure DevOps, Gitea, and NotConfiguredVCSClient). The proxy and instrumented client both delegate directly now, no more anonymous interfaces or type assertions.
There was a problem hiding this comment.
Nice! Is childTeamFetcher still needed then?
There was a problem hiding this comment.
Good catch — removed in 5b6cd44. childTeamFetcher was a strict subset of vcs.Client (which already has GetChildTeams), so the type assertion always succeeded and it served no selection purpose. fetchDescendantTeams now takes vcs.Client directly, and the separate mockChildTeamFetcher test double is merged into childTeamVCSClient so there's one test helper instead of two.
There was a problem hiding this comment.
@lukemassa i didn't notice your last question, i let AI handling it, can you continue review ?
…on-GitHub providers Addresses PR review feedback: instead of using anonymous types and type assertions in the proxy and instrumented client, GetChildTeams is now a first-class method on the Client interface. All non-GitHub VCS providers return nil, nil as they don't support team hierarchies. Signed-off-by: hussein-mimi <hussein.mimi@harri.com> Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The debian-security repo updated openssh to u9 which requires a matching openssh-client version, causing a dependency conflict with the previously pinned u7 version. Signed-off-by: hussein-mimi <hussein.mimi@harri.com> Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace len-only assertion with exact slice comparison so the test catches wrong teams or duplicates, not just wrong counts. Signed-off-by: hussein-mimi <hussein.mimi@harri.com> Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 14 out of 14 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (1)
server/events/vcs/client.go:1
- The PR description says the hierarchy support is implemented via a duck-typed
childTeamFetcherwithout changing the sharedClientinterface, but this diff addsGetChildTeamstovcs.Client, forcing all VCS providers (and the proxy/mocks) to implement it. If the intent is truly duck-typing, keepGetChildTeamsoffvcs.Clientand only implement it on the GitHub client (and adjust call sites to type-assert to the extra interface); otherwise, update the PR description to reflect this breaking interface change.
// Copyright 2017 HootSuite Media Inc.
…missions Since GetChildTeams is now on the vcs.Client interface (per reviewer request), the type assertion c.VCSClient.(childTeamFetcher) always succeeds, causing the slow path to execute for all VCS providers even when they just return nil. Remove the assertion and call c.VCSClient directly; non-GitHub providers return nil from GetChildTeams so fetchDescendantTeams produces empty results and the loop is a no-op. Keep childTeamFetcher as a narrow interface for fetchDescendantTeams so tests can pass a mock without implementing all of vcs.Client. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: hussein-mimi <hussein.mimi@harri.com>
Add a 'multiple pages' subtest to TestClient_GetChildTeams that verifies cursor-based pagination works correctly: the first GraphQL request has no cursor, the second includes the endCursor from the first page, and results from both pages are accumulated. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: hussein-mimi <hussein.mimi@harri.com>
Verify that a cyclic hierarchy (a -> b -> a) terminates and produces deduplicated results thanks to the visited set. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: hussein-mimi <hussein.mimi@harri.com>
|
@lukemassa can you take a look ? |
GetChildTeams is already on vcs.Client, so childTeamFetcher was a strict subset of it — the type assertion always succeeded and the interface served no selection purpose. fetchDescendantTeams now takes vcs.Client directly, and the separate mockChildTeamFetcher test double is merged into childTeamVCSClient so there is one test helper instead of two. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: hussein-mimi <hussein.mimi@harri.com>
Summary
Closes #6107
When a parent team is added to
ATLANTIS_GH_TEAM_ALLOWLIST, users who are members of any descendant (child/grandchild/etc.) team are now correctly authorized, instead of being rejected.Before: User in
child-team→ allowlist hasparent-team→ ❌ rejectedAfter: User in
child-team→ allowlist hasparent-team→ ✅ authorizedHow it works
GetChildTeams(logger, repo, teamSlug)to the GitHub client — queries the GitHub GraphQL API for direct child teams of a given team slug (with pagination).checkUserPermissions, after the fast path (direct team membership, zero extra API calls) fails, a slow path kicks in:GetChildTeams(up to 20 levels deep to prevent infinite loops).childTeamFetcherinterface, so no changes to the sharedClientinterface or any other VCS provider.Test plan
TestClient_GetTeamNamesForUser— existing test, still passes unchangedTestClient_GetChildTeams— new test verifyingGetChildTeamsreturns direct children from a mocked GraphQL responsego build ./server/events/...— clean buildATLANTIS_GH_TEAM_ALLOWLIST, comment as a user who is only in a child team, verify they are now authorized🤖 Generated with Claude Code