diff --git a/server/events/command_runner.go b/server/events/command_runner.go index d1fcb72969..41c5fa4973 100644 --- a/server/events/command_runner.go +++ b/server/events/command_runner.go @@ -17,6 +17,7 @@ import ( "errors" "fmt" "strconv" + "strings" "github.com/drmaxgit/go-azuredevops/azuredevops" "github.com/google/go-github/v83/github" @@ -165,7 +166,7 @@ func (c *DefaultCommandRunner) RunAutoplanCommand(baseRepo models.Repo, headRepo return } - ok, err := c.checkUserPermissions(baseRepo, user, "plan") + ok, err := c.checkUserPermissions(baseRepo, &user, "plan") if err != nil { log.Err("Unable to check user permissions: %s", err) return @@ -262,8 +263,62 @@ func (c *DefaultCommandRunner) commentUserDoesNotHavePermissions(baseRepo models } } -// checkUserPermissions checks if the user has permissions to execute the command -func (c *DefaultCommandRunner) checkUserPermissions(repo models.Repo, user models.User, cmdName string) (bool, error) { +// fetchDescendantTeams fetches all descendant team slugs for the given team up to maxDepth +// levels deep using an iterative BFS with a visited set to avoid duplicate API calls and +// handle any cycles in unexpected hierarchy configurations. +func fetchDescendantTeams(fetcher vcs.Client, logger logging.SimpleLogging, repo models.Repo, teamSlug string, maxDepth int) ([]string, error) { + if maxDepth <= 0 { + return nil, nil + } + + type queueItem struct { + slug string + depth int + } + + visited := map[string]struct{}{teamSlug: {}} + queue := []queueItem{{slug: teamSlug, depth: 0}} + var result []string + + for i := 0; i < len(queue); i++ { + current := queue[i] + + if current.depth >= maxDepth { + continue + } + + children, err := fetcher.GetChildTeams(logger, repo, current.slug) + if err != nil { + if current.slug == teamSlug { + return nil, err + } + logger.Warn("Could not fetch child teams for '%s': %s", current.slug, err) + continue + } + + for _, child := range children { + if _, ok := visited[child]; ok { + continue + } + visited[child] = struct{}{} + result = append(result, child) + queue = append(queue, queueItem{slug: child, depth: current.depth + 1}) + } + } + + return result, nil +} + +// checkUserPermissions checks if the user has permissions to execute the command. +// It first checks direct team membership against the allowlist. If that fails, +// it expands each allowlisted team to include all its descendant teams (up to +// 20 levels deep) via GetChildTeams on the VCS client and re-checks. +// Non-GitHub VCS providers return nil from GetChildTeams, so the expansion +// loop is effectively a no-op for them. +// When a match is found via hierarchy, the matched allowlisted parent team is appended to +// user.Teams so that subsequent per-project allowlist checks (which use direct membership +// only) also pass. +func (c *DefaultCommandRunner) checkUserPermissions(repo models.Repo, user *models.User, cmdName string) (bool, error) { if c.TeamAllowlistChecker == nil || !c.TeamAllowlistChecker.HasRules() { // allowlist restriction is not enabled return true, nil @@ -273,15 +328,46 @@ func (c *DefaultCommandRunner) checkUserPermissions(repo models.Repo, user model CommandName: cmdName, Log: c.Logger, Pull: models.PullRequest{}, - User: user, + User: *user, Verbose: false, API: false, } - ok := c.TeamAllowlistChecker.IsCommandAllowedForAnyTeam(ctx, user.Teams, cmdName) - if !ok { - return false, nil + + // Fast path: user is a direct member of an allowlisted team. + if c.TeamAllowlistChecker.IsCommandAllowedForAnyTeam(ctx, user.Teams, cmdName) { + return true, nil + } + + // Slow path: check if the user belongs to a descendant team of any allowlisted team. + const maxHierarchyDepth = 20 + for _, allowedTeam := range c.TeamAllowlistChecker.AllTeams() { + if allowedTeam == "*" { + continue + } + // Only expand teams that actually grant permission for this command. + if !c.TeamAllowlistChecker.IsCommandAllowedForTeam(ctx, allowedTeam, cmdName) { + continue + } + descendants, err := fetchDescendantTeams(c.VCSClient, c.Logger, repo, allowedTeam, maxHierarchyDepth) + if err != nil { + c.Logger.Warn("Could not fetch child teams for '%s': %s", allowedTeam, err) + continue + } + descendantSet := make(map[string]struct{}, len(descendants)) + for _, desc := range descendants { + descendantSet[strings.ToLower(desc)] = struct{}{} + } + for _, userTeam := range user.Teams { + if _, ok := descendantSet[strings.ToLower(userTeam)]; ok { + // Add the matched allowlisted parent team to user.Teams so that + // per-project allowlist filters (which check direct membership) + // also pass for this user. + user.Teams = append(user.Teams, allowedTeam) + return true, nil + } + } } - return true, nil + return false, nil } // checkVarFilesInPlanCommandAllowlisted checks if paths in a 'plan' command are allowlisted. @@ -326,7 +412,7 @@ func (c *DefaultCommandRunner) RunCommentCommand(baseRepo models.Repo, maybeHead return } - ok, err := c.checkUserPermissions(baseRepo, user, cmd.Name.String()) + ok, err := c.checkUserPermissions(baseRepo, &user, cmd.Name.String()) if err != nil { c.Logger.Err("Unable to check user permissions: %s", err) return diff --git a/server/events/command_runner_internal_test.go b/server/events/command_runner_internal_test.go index 8cecfbcadd..4c9ef7a7a6 100644 --- a/server/events/command_runner_internal_test.go +++ b/server/events/command_runner_internal_test.go @@ -4,14 +4,245 @@ package events import ( + "errors" "testing" "github.com/runatlantis/atlantis/server/events/command" "github.com/runatlantis/atlantis/server/events/models" + "github.com/runatlantis/atlantis/server/events/vcs" "github.com/runatlantis/atlantis/server/logging" . "github.com/runatlantis/atlantis/testing" ) +// childTeamVCSClient combines vcs.NotConfiguredVCSClient (satisfying vcs.Client) +// with a configurable team hierarchy map, so tests can exercise GetChildTeams +// without a real VCS connection. +type childTeamVCSClient struct { + vcs.NotConfiguredVCSClient + children map[string][]string + errOn map[string]bool +} + +func (c *childTeamVCSClient) GetChildTeams(_ logging.SimpleLogging, _ models.Repo, teamSlug string) ([]string, error) { + if c.errOn[teamSlug] { + return nil, errors.New("API error for " + teamSlug) + } + return c.children[teamSlug], nil +} + +func TestFetchDescendantTeams(t *testing.T) { + logger := logging.NewNoopLogger(t) + repo := models.Repo{Owner: "test-org"} + + t.Run("leaf team returns empty", func(t *testing.T) { + fetcher := &childTeamVCSClient{children: map[string][]string{}} + result, err := fetchDescendantTeams(fetcher, logger, repo, "leaf-team", 20) + Ok(t, err) + Equals(t, 0, len(result)) + }) + + t.Run("single level of children", func(t *testing.T) { + fetcher := &childTeamVCSClient{children: map[string][]string{ + "parent": {"child-a", "child-b"}, + }} + result, err := fetchDescendantTeams(fetcher, logger, repo, "parent", 20) + Ok(t, err) + Equals(t, []string{"child-a", "child-b"}, result) + }) + + t.Run("multiple levels of nesting", func(t *testing.T) { + fetcher := &childTeamVCSClient{children: map[string][]string{ + "grandparent": {"parent"}, + "parent": {"child"}, + }} + result, err := fetchDescendantTeams(fetcher, logger, repo, "grandparent", 20) + Ok(t, err) + Equals(t, []string{"parent", "child"}, result) + }) + + t.Run("maxDepth=0 returns nothing", func(t *testing.T) { + fetcher := &childTeamVCSClient{children: map[string][]string{ + "parent": {"child"}, + }} + result, err := fetchDescendantTeams(fetcher, logger, repo, "parent", 0) + Ok(t, err) + Equals(t, []string(nil), result) + }) + + t.Run("maxDepth=1 returns only direct children", func(t *testing.T) { + fetcher := &childTeamVCSClient{children: map[string][]string{ + "grandparent": {"parent"}, + "parent": {"child"}, + }} + result, err := fetchDescendantTeams(fetcher, logger, repo, "grandparent", 1) + Ok(t, err) + Equals(t, []string{"parent"}, result) + }) + + t.Run("error at root propagates", func(t *testing.T) { + fetcher := &childTeamVCSClient{ + children: map[string][]string{}, + errOn: map[string]bool{"parent": true}, + } + _, err := fetchDescendantTeams(fetcher, logger, repo, "parent", 20) + Assert(t, err != nil, "expected error to propagate from root team") + }) + + t.Run("error in recursive call is logged and skipped", func(t *testing.T) { + // parent fetches OK; fetching child-a's children errors. + // child-b and its subtree should still be traversed. + fetcher := &childTeamVCSClient{ + children: map[string][]string{ + "parent": {"child-a", "child-b"}, + "child-b": {"grandchild-b"}, + }, + errOn: map[string]bool{"child-a": true}, + } + result, err := fetchDescendantTeams(fetcher, logger, repo, "parent", 20) + Ok(t, err) + // child-a is included (direct child of parent); its subtree is skipped on error. + // child-b and grandchild-b are also traversed successfully. + Equals(t, []string{"child-a", "child-b", "grandchild-b"}, result) + }) + + t.Run("cycle is handled without infinite loop", func(t *testing.T) { + // a -> b -> a forms a cycle; visited set should break it. + fetcher := &childTeamVCSClient{children: map[string][]string{ + "team-a": {"team-b"}, + "team-b": {"team-a"}, + }} + result, err := fetchDescendantTeams(fetcher, logger, repo, "team-a", 20) + Ok(t, err) + Equals(t, []string{"team-b"}, result) + }) +} + +func TestCheckUserPermissions(t *testing.T) { + logger := logging.NewNoopLogger(t) + repo := models.Repo{Owner: "test-org"} + + t.Run("no rules allows everyone", func(t *testing.T) { + cr := &DefaultCommandRunner{ + Logger: logger, + TeamAllowlistChecker: &command.DefaultTeamAllowlistChecker{}, + } + user := models.User{Username: "alice", Teams: []string{"some-team"}} + ok, err := cr.checkUserPermissions(repo, &user, "plan") + Ok(t, err) + Assert(t, ok, "expected allowed when no rules") + }) + + t.Run("fast path: direct team member is allowed", func(t *testing.T) { + checker, _ := command.NewTeamAllowlistChecker("dev-team:plan") + cr := &DefaultCommandRunner{ + Logger: logger, + TeamAllowlistChecker: checker, + VCSClient: &vcs.NotConfiguredVCSClient{}, // GetChildTeams returns nil (no hierarchy support) + } + user := models.User{Username: "alice", Teams: []string{"dev-team"}} + ok, err := cr.checkUserPermissions(repo, &user, "plan") + Ok(t, err) + Assert(t, ok, "expected direct member to be allowed") + }) + + t.Run("fast path: non-member without hierarchy support is rejected", func(t *testing.T) { + checker, _ := command.NewTeamAllowlistChecker("dev-team:plan") + cr := &DefaultCommandRunner{ + Logger: logger, + TeamAllowlistChecker: checker, + VCSClient: &vcs.NotConfiguredVCSClient{}, // GetChildTeams returns nil (no hierarchy support) + } + user := models.User{Username: "alice", Teams: []string{"other-team"}} + ok, err := cr.checkUserPermissions(repo, &user, "plan") + Ok(t, err) + Assert(t, !ok, "expected non-member to be rejected when no hierarchy support") + }) + + hierarchyCases := map[string]struct { + allowlist string + userTeams []string + hierarchy map[string][]string + cmdName string + expectAllow bool + }{ + "slow path: user in direct child team is allowed": { + allowlist: "parent-team:plan", + userTeams: []string{"child-team"}, + hierarchy: map[string][]string{"parent-team": {"child-team"}}, + cmdName: "plan", + expectAllow: true, + }, + "slow path: user in grandchild team is allowed": { + allowlist: "grandparent-team:plan", + userTeams: []string{"grandchild-team"}, + hierarchy: map[string][]string{ + "grandparent-team": {"parent-team"}, + "parent-team": {"grandchild-team"}, + }, + cmdName: "plan", + expectAllow: true, + }, + "slow path: user not in any descendant is rejected": { + allowlist: "parent-team:plan", + userTeams: []string{"unrelated-team"}, + hierarchy: map[string][]string{"parent-team": {"child-team"}}, + cmdName: "plan", + expectAllow: false, + }, + "slow path: user in child team but wrong command is rejected": { + allowlist: "parent-team:apply", + userTeams: []string{"child-team"}, + hierarchy: map[string][]string{"parent-team": {"child-team"}}, + cmdName: "plan", + expectAllow: false, + }, + // *:plan allows everyone including users with no team memberships. + // The fast path handles this via IsCommandAllowedForAnyTeam's zero-team wildcard check. + "fast path: wildcard team rule allows user with no teams": { + allowlist: "*:plan", + userTeams: []string{}, + hierarchy: map[string][]string{}, + cmdName: "plan", + expectAllow: true, + }, + } + + for name, tc := range hierarchyCases { + t.Run(name, func(t *testing.T) { + checker, err := command.NewTeamAllowlistChecker(tc.allowlist) + Ok(t, err) + cr := &DefaultCommandRunner{ + Logger: logger, + TeamAllowlistChecker: checker, + VCSClient: &childTeamVCSClient{children: tc.hierarchy}, + } + user := models.User{Username: "testuser", Teams: tc.userTeams} + ok, checkErr := cr.checkUserPermissions(repo, &user, tc.cmdName) + Ok(t, checkErr) + Equals(t, tc.expectAllow, ok) + }) + } + + t.Run("slow path: matched parent team is appended to user.Teams", func(t *testing.T) { + checker, err := command.NewTeamAllowlistChecker("parent-team:plan") + Ok(t, err) + cr := &DefaultCommandRunner{ + Logger: logger, + TeamAllowlistChecker: checker, + VCSClient: &childTeamVCSClient{ + children: map[string][]string{"parent-team": {"child-team"}}, + }, + } + user := models.User{Username: "alice", Teams: []string{"child-team"}} + ok, checkErr := cr.checkUserPermissions(repo, &user, "plan") + Ok(t, checkErr) + Assert(t, ok, "expected child team member to be allowed") + // The matched parent team should be appended so per-project allowlist checks pass. + Assert(t, len(user.Teams) == 2, "expected user.Teams to contain both child-team and parent-team") + Equals(t, "parent-team", user.Teams[1]) + }) +} + func TestApplyUpdateCommitStatus(t *testing.T) { cases := map[string]struct { cmd command.Name diff --git a/server/events/vcs/azuredevops/client.go b/server/events/vcs/azuredevops/client.go index 0538c7be84..b1d90c17d1 100644 --- a/server/events/vcs/azuredevops/client.go +++ b/server/events/vcs/azuredevops/client.go @@ -454,3 +454,7 @@ func (g *Client) GetCloneURL(_ logging.SimpleLogging, VCSHostType models.VCSHost func (g *Client) GetPullLabels(_ logging.SimpleLogging, _ models.Repo, _ models.PullRequest) ([]string, error) { return nil, fmt.Errorf("not yet implemented") } + +func (g *Client) GetChildTeams(_ logging.SimpleLogging, _ models.Repo, _ string) ([]string, error) { + return nil, nil +} diff --git a/server/events/vcs/bitbucketcloud/client.go b/server/events/vcs/bitbucketcloud/client.go index 68f8d2316a..cdb00b3aac 100644 --- a/server/events/vcs/bitbucketcloud/client.go +++ b/server/events/vcs/bitbucketcloud/client.go @@ -393,3 +393,7 @@ func (b *Client) GetCloneURL(_ logging.SimpleLogging, _ models.VCSHostType, _ st func (b *Client) GetPullLabels(_ logging.SimpleLogging, _ models.Repo, _ models.PullRequest) ([]string, error) { return nil, fmt.Errorf("not yet implemented") } + +func (b *Client) GetChildTeams(_ logging.SimpleLogging, _ models.Repo, _ string) ([]string, error) { + return nil, nil +} diff --git a/server/events/vcs/bitbucketserver/client.go b/server/events/vcs/bitbucketserver/client.go index ea00e4d71f..35cd5827bb 100644 --- a/server/events/vcs/bitbucketserver/client.go +++ b/server/events/vcs/bitbucketserver/client.go @@ -376,3 +376,7 @@ func (b *Client) GetCloneURL(_ logging.SimpleLogging, _ models.VCSHostType, _ st func (b *Client) GetPullLabels(_ logging.SimpleLogging, _ models.Repo, _ models.PullRequest) ([]string, error) { return nil, fmt.Errorf("not yet implemented") } + +func (b *Client) GetChildTeams(_ logging.SimpleLogging, _ models.Repo, _ string) ([]string, error) { + return nil, nil +} diff --git a/server/events/vcs/client.go b/server/events/vcs/client.go index 0fb60778bf..c8d2d281bf 100644 --- a/server/events/vcs/client.go +++ b/server/events/vcs/client.go @@ -53,4 +53,8 @@ type Client interface { // GetPullLabels returns the labels of a pull request GetPullLabels(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest) ([]string, error) + + // GetChildTeams returns the slugs of all teams that are children of the given team. + // Returns nil, nil for VCS providers that don't support team hierarchies. + GetChildTeams(logger logging.SimpleLogging, repo models.Repo, teamSlug string) ([]string, error) } diff --git a/server/events/vcs/gitea/client.go b/server/events/vcs/gitea/client.go index c5431c7e00..c78b4b838d 100644 --- a/server/events/vcs/gitea/client.go +++ b/server/events/vcs/gitea/client.go @@ -506,6 +506,10 @@ func (c *Client) GetPullLabels(logger logging.SimpleLogging, repo models.Repo, p return results, nil } +func (c *Client) GetChildTeams(_ logging.SimpleLogging, _ models.Repo, _ string) ([]string, error) { + return nil, nil +} + func ValidateSignature(payload []byte, signature string, secretKey []byte) error { isValid, err := gitea.VerifyWebhookSignature(string(secretKey), signature, payload) if err != nil { diff --git a/server/events/vcs/github/client.go b/server/events/vcs/github/client.go index a75e0efd39..1ed616804e 100644 --- a/server/events/vcs/github/client.go +++ b/server/events/vcs/github/client.go @@ -1130,6 +1130,52 @@ func (g *Client) GetTeamNamesForUser(logger logging.SimpleLogging, repo models.R return teamNames, nil } +// GetChildTeams returns the slugs of the direct child teams of the given team. +// Use this with fetchDescendantTeams in the command runner to expand an allowlisted team +// to all its descendants, supporting GitHub team hierarchy. +// https://docs.github.com/en/graphql/reference/objects#team +func (g *Client) GetChildTeams(logger logging.SimpleLogging, repo models.Repo, teamSlug string) ([]string, error) { + logger.Debug("Getting child teams for GitHub team '%s'", teamSlug) + orgName := repo.Owner + variables := map[string]any{ + "orgName": githubv4.String(orgName), + "teamSlug": githubv4.String(teamSlug), + "childCursor": (*githubv4.String)(nil), + } + var q struct { + Organization struct { + Team struct { + ChildTeams struct { + Nodes []struct { + Slug string + } + PageInfo struct { + EndCursor githubv4.String + HasNextPage bool + } + } `graphql:"childTeams(first: 100, after: $childCursor)"` + } `graphql:"team(slug: $teamSlug)"` + } `graphql:"organization(login: $orgName)"` + } + var childSlugs []string + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + for { + err := g.v4Client.Query(ctx, &q, variables) + if err != nil { + return nil, err + } + for _, node := range q.Organization.Team.ChildTeams.Nodes { + childSlugs = append(childSlugs, node.Slug) + } + if !q.Organization.Team.ChildTeams.PageInfo.HasNextPage { + break + } + variables["childCursor"] = githubv4.NewString(q.Organization.Team.ChildTeams.PageInfo.EndCursor) + } + return childSlugs, nil +} + // ExchangeCode returns a newly created app's info func (g *Client) ExchangeCode(logger logging.SimpleLogging, code string) (*GithubAppTemporarySecrets, error) { logger.Debug("Exchanging code for app secrets") diff --git a/server/events/vcs/github/client_test.go b/server/events/vcs/github/client_test.go index 132d4edcdb..f94eb0b68d 100644 --- a/server/events/vcs/github/client_test.go +++ b/server/events/vcs/github/client_test.go @@ -1697,6 +1697,140 @@ func TestClient_GetTeamNamesForUser(t *testing.T) { Equals(t, []string{"frontend-developers", "employees"}, teams) } +// TestClient_GetChildTeams verifies that direct child teams of a given team are returned. +func TestClient_GetChildTeams(t *testing.T) { + t.Run("single page", func(t *testing.T) { + logger := logging.NewNoopLogger(t) + resp := `{ + "data":{ + "organization": { + "team": { + "childTeams": { + "nodes": [ + {"slug": "child-team-a"}, + {"slug": "child-team-b"} + ], + "pageInfo": { + "endCursor": "Y3Vyc29yOnYyOpHOAFMoLQ==", + "hasNextPage": false + } + } + } + } + } + }` + testServer := httptest.NewTLSServer( + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.RequestURI { + case "/api/graphql": + w.Write([]byte(resp)) // nolint: errcheck + default: + t.Errorf("got unexpected request at %q", r.RequestURI) + http.Error(w, "not found", http.StatusNotFound) + return + } + })) + defer testServer.Close() + testServerURL, err := url.Parse(testServer.URL) + Ok(t, err) + client, err := github.New(testServerURL.Host, &github.UserCredentials{"user", "pass", ""}, github.Config{}, 0, logger) + Ok(t, err) + defer disableSSLVerification()() + + children, err := client.GetChildTeams( + logger, + models.Repo{Owner: "testrepo"}, + "parent-team", + ) + Ok(t, err) + Equals(t, []string{"child-team-a", "child-team-b"}, children) + }) + + t.Run("multiple pages", func(t *testing.T) { + logger := logging.NewNoopLogger(t) + firstCursor := "cursor-page-1" + firstResp := `{ + "data":{ + "organization": { + "team": { + "childTeams": { + "nodes": [ + {"slug": "child-team-a"}, + {"slug": "child-team-b"} + ], + "pageInfo": { + "endCursor": "cursor-page-1", + "hasNextPage": true + } + } + } + } + } + }` + secondResp := `{ + "data":{ + "organization": { + "team": { + "childTeams": { + "nodes": [ + {"slug": "child-team-c"} + ], + "pageInfo": { + "endCursor": "cursor-page-2", + "hasNextPage": false + } + } + } + } + } + }` + + requestBodies := make([]string, 0, 2) + testServer := httptest.NewTLSServer( + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.RequestURI { + case "/api/graphql": + body, err := io.ReadAll(r.Body) + if err != nil { + t.Errorf("reading request body: %s", err) + http.Error(w, "bad request", http.StatusBadRequest) + return + } + requestBodies = append(requestBodies, string(body)) + if len(requestBodies) == 1 { + w.Write([]byte(firstResp)) // nolint: errcheck + } else { + w.Write([]byte(secondResp)) // nolint: errcheck + } + default: + t.Errorf("got unexpected request at %q", r.RequestURI) + http.Error(w, "not found", http.StatusNotFound) + return + } + })) + defer testServer.Close() + testServerURL, err := url.Parse(testServer.URL) + Ok(t, err) + client, err := github.New(testServerURL.Host, &github.UserCredentials{"user", "pass", ""}, github.Config{}, 0, logger) + Ok(t, err) + defer disableSSLVerification()() + + children, err := client.GetChildTeams( + logger, + models.Repo{Owner: "testrepo"}, + "parent-team", + ) + Ok(t, err) + Equals(t, []string{"child-team-a", "child-team-b", "child-team-c"}, children) + Equals(t, 2, len(requestBodies)) + // First request should not contain the cursor; second should. + Assert(t, !strings.Contains(requestBodies[0], firstCursor), + "expected first request not to include cursor") + Assert(t, strings.Contains(requestBodies[1], firstCursor), + "expected second request to include cursor") + }) +} + func TestClient_DiscardReviews(t *testing.T) { logger := logging.NewNoopLogger(t) type ResponseDef struct { diff --git a/server/events/vcs/gitlab/client.go b/server/events/vcs/gitlab/client.go index 0e02014388..d52d1c8ab2 100644 --- a/server/events/vcs/gitlab/client.go +++ b/server/events/vcs/gitlab/client.go @@ -781,3 +781,7 @@ func (g *Client) GetPullLabels(logger logging.SimpleLogging, repo models.Repo, p return mr.Labels, nil } + +func (g *Client) GetChildTeams(_ logging.SimpleLogging, _ models.Repo, _ string) ([]string, error) { + return nil, nil +} diff --git a/server/events/vcs/mocks/mock_client.go b/server/events/vcs/mocks/mock_client.go index 338308935a..b60e935d95 100644 --- a/server/events/vcs/mocks/mock_client.go +++ b/server/events/vcs/mocks/mock_client.go @@ -117,6 +117,25 @@ func (mock *MockClient) GetModifiedFiles(logger logging.SimpleLogging, repo mode return _ret0, _ret1 } +func (mock *MockClient) GetChildTeams(logger logging.SimpleLogging, repo models.Repo, teamSlug string) ([]string, error) { + if mock == nil { + panic("mock must not be nil. Use myMock := NewMockClient().") + } + _params := []pegomock.Param{logger, repo, teamSlug} + _result := pegomock.GetGenericMockFrom(mock).Invoke("GetChildTeams", _params, []reflect.Type{reflect.TypeOf((*[]string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) + var _ret0 []string + var _ret1 error + if len(_result) != 0 { + if _result[0] != nil { + _ret0 = _result[0].([]string) + } + if _result[1] != nil { + _ret1 = _result[1].(error) + } + } + return _ret0, _ret1 +} + func (mock *MockClient) GetPullLabels(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest) ([]string, error) { if mock == nil { panic("mock must not be nil. Use myMock := NewMockClient().") @@ -547,6 +566,47 @@ func (c *MockClient_GetModifiedFiles_OngoingVerification) GetAllCapturedArgument return } +func (verifier *VerifierMockClient) GetChildTeams(logger logging.SimpleLogging, repo models.Repo, teamSlug string) *MockClient_GetChildTeams_OngoingVerification { + _params := []pegomock.Param{logger, repo, teamSlug} + methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "GetChildTeams", _params, verifier.timeout) + return &MockClient_GetChildTeams_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} +} + +type MockClient_GetChildTeams_OngoingVerification struct { + mock *MockClient + methodInvocations []pegomock.MethodInvocation +} + +func (c *MockClient_GetChildTeams_OngoingVerification) GetCapturedArguments() (logging.SimpleLogging, models.Repo, string) { + logger, repo, teamSlug := c.GetAllCapturedArguments() + return logger[len(logger)-1], repo[len(repo)-1], teamSlug[len(teamSlug)-1] +} + +func (c *MockClient_GetChildTeams_OngoingVerification) GetAllCapturedArguments() (_param0 []logging.SimpleLogging, _param1 []models.Repo, _param2 []string) { + _params := pegomock.GetGenericMockFrom(c.mock).GetInvocationParams(c.methodInvocations) + if len(_params) > 0 { + if len(_params) > 0 { + _param0 = make([]logging.SimpleLogging, len(c.methodInvocations)) + for u, param := range _params[0] { + _param0[u] = param.(logging.SimpleLogging) + } + } + if len(_params) > 1 { + _param1 = make([]models.Repo, len(c.methodInvocations)) + for u, param := range _params[1] { + _param1[u] = param.(models.Repo) + } + } + if len(_params) > 2 { + _param2 = make([]string, len(c.methodInvocations)) + for u, param := range _params[2] { + _param2[u] = param.(string) + } + } + } + return +} + func (verifier *VerifierMockClient) GetPullLabels(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest) *MockClient_GetPullLabels_OngoingVerification { _params := []pegomock.Param{logger, repo, pull} methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "GetPullLabels", _params, verifier.timeout) diff --git a/server/events/vcs/not_configured_vcs_client.go b/server/events/vcs/not_configured_vcs_client.go index 7648ccbce8..29c2216b63 100644 --- a/server/events/vcs/not_configured_vcs_client.go +++ b/server/events/vcs/not_configured_vcs_client.go @@ -78,3 +78,7 @@ func (a *NotConfiguredVCSClient) GetCloneURL(_ logging.SimpleLogging, _ models.V func (a *NotConfiguredVCSClient) GetPullLabels(_ logging.SimpleLogging, _ models.Repo, _ models.PullRequest) ([]string, error) { return nil, a.err() } + +func (a *NotConfiguredVCSClient) GetChildTeams(_ logging.SimpleLogging, _ models.Repo, _ string) ([]string, error) { + return nil, nil +} diff --git a/server/events/vcs/proxy.go b/server/events/vcs/proxy.go index 700bf3798e..f559b4ebe7 100644 --- a/server/events/vcs/proxy.go +++ b/server/events/vcs/proxy.go @@ -116,3 +116,7 @@ func (d *ClientProxy) GetCloneURL(logger logging.SimpleLogging, VCSHostType mode func (d *ClientProxy) GetPullLabels(logger logging.SimpleLogging, repo models.Repo, pull models.PullRequest) ([]string, error) { return d.clients[repo.VCSHost.Type].GetPullLabels(logger, repo, pull) } + +func (d *ClientProxy) GetChildTeams(logger logging.SimpleLogging, repo models.Repo, teamSlug string) ([]string, error) { + return d.clients[repo.VCSHost.Type].GetChildTeams(logger, repo, teamSlug) +}