Skip to content

feat(github): support team hierarchy in GH_TEAM_ALLOWLIST#6365

Open
hussein-mimi wants to merge 20 commits intorunatlantis:mainfrom
hussein-mimi:feat/github-team-hierarchy-allowlist
Open

feat(github): support team hierarchy in GH_TEAM_ALLOWLIST#6365
hussein-mimi wants to merge 20 commits intorunatlantis:mainfrom
hussein-mimi:feat/github-team-hierarchy-allowlist

Conversation

@hussein-mimi
Copy link
Copy Markdown

@hussein-mimi hussein-mimi commented Apr 5, 2026

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 has parent-team → ❌ rejected
After: User in child-team → allowlist has parent-team → ✅ authorized

How it works

  • Added GetChildTeams(logger, repo, teamSlug) to the GitHub client — queries the GitHub GraphQL API for direct child teams of a given team slug (with pagination).
  • In checkUserPermissions, after the fast path (direct team membership, zero extra API calls) fails, a slow path kicks in:
    1. For each allowlisted team that grants the requested command, recursively fetch all descendant teams via GetChildTeams (up to 20 levels deep to prevent infinite loops).
    2. Check if the user's direct teams appear in this expanded set.
  • Non-GitHub VCS clients are unaffected — the expansion uses a duck-typed childTeamFetcher interface, so no changes to the shared Client interface or any other VCS provider.

Test plan

  • TestClient_GetTeamNamesForUser — existing test, still passes unchanged
  • TestClient_GetChildTeams — new test verifying GetChildTeams returns direct children from a mocked GraphQL response
  • go build ./server/events/... — clean build
  • Manual test: add a parent team to ATLANTIS_GH_TEAM_ALLOWLIST, comment as a user who is only in a child team, verify they are now authorized

🤖 Generated with Claude Code

Copilot AI review requested due to automatic review settings April 5, 2026 13:06
@dosubot dosubot Bot added feature New functionality/enhancement go Pull requests that update Go code provider/github labels Apr 5, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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 childTeamFetcher interface 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

Comment thread server/events/command_runner.go Outdated
@hussein-mimi hussein-mimi force-pushed the feat/github-team-hierarchy-allowlist branch from 73d098f to bb0b23d Compare April 7, 2026 16:39
@hussein-mimi hussein-mimi force-pushed the feat/github-team-hierarchy-allowlist branch from bb0b23d to b92451e Compare April 7, 2026 16:42
@hussein-mimi hussein-mimi requested a review from Copilot April 8, 2026 06:50
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.

@hussein-mimi hussein-mimi force-pushed the feat/github-team-hierarchy-allowlist branch from 6d9cb59 to 6dab523 Compare April 9, 2026 08:29
@hussein-mimi hussein-mimi marked this pull request as draft April 9, 2026 12:51
@hussein-mimi hussein-mimi marked this pull request as ready for review April 9, 2026 12:54
hussein-mimi and others added 4 commits April 9, 2026 16:00
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>
@hussein-mimi hussein-mimi force-pushed the feat/github-team-hierarchy-allowlist branch from 77c3ffd to f57331f Compare April 9, 2026 13:00
@hussein-mimi hussein-mimi requested a review from Copilot April 9, 2026 13:01
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.

Comment on lines +119 to +130

// 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
}
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.

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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

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.

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.

Nice! Is childTeamFetcher still needed then?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

@lukemassa i didn't notice your last question, i let AI handling it, can you continue review ?

hussein-mimi and others added 2 commits April 12, 2026 11:22
…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>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 14 out of 14 changed files in this pull request and generated 2 comments.

Comment thread server/events/command_runner.go Outdated
Comment thread server/events/command_runner_internal_test.go
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>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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 childTeamFetcher without changing the shared Client interface, but this diff adds GetChildTeams to vcs.Client, forcing all VCS providers (and the proxy/mocks) to implement it. If the intent is truly duck-typing, keep GetChildTeams off vcs.Client and 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.

Comment thread server/events/command_runner.go Outdated
Comment thread server/events/vcs/github/client_test.go Outdated
…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>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 14 out of 14 changed files in this pull request and generated 3 comments.

Comment thread server/events/command_runner.go
Comment thread server/events/vcs/github/client_test.go Outdated
Comment thread server/events/vcs/github/client_test.go Outdated
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>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 14 out of 14 changed files in this pull request and generated 3 comments.

Comment thread server/events/command_runner.go
Comment thread server/events/command_runner.go
Comment thread server/events/command_runner_internal_test.go
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>
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 14 out of 14 changed files in this pull request and generated 1 comment.

Comment thread server/events/command_runner.go
@hussein-mimi
Copy link
Copy Markdown
Author

@lukemassa can you take a look ?

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated 1 comment.

Comment thread server/events/command_runner.go
hussein-mimi and others added 4 commits April 21, 2026 18:22
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

build Relating to how we build Atlantis feature New functionality/enhancement go Pull requests that update Go code provider/bitbucket provider/github website

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ATLANTIS_GH_TEAM_ALLOWLIST does not support nested team membership

3 participants