From 31237637bdd66fcf0380d97f8f415dbc8fc8a97d Mon Sep 17 00:00:00 2001 From: Pierre Guinoiseau Date: Tue, 28 Nov 2023 17:05:37 +1300 Subject: [PATCH] feat: add support for GitLab groups --- cmd/server.go | 12 ++++ runatlantis.io/docs/server-configuration.md | 14 +++++ .../docs/server-side-repo-config.md | 20 +++--- server/core/config/valid/policies.go | 14 +++++ server/core/config/valid/policies_test.go | 63 +++++++++++++++++++ server/events/command_runner.go | 21 +++++-- server/events/command_runner_test.go | 8 +-- server/events/project_command_runner.go | 2 +- server/events/project_command_runner_test.go | 2 +- server/events/team_allowlist_checker.go | 13 ++++ server/events/team_allowlist_checker_test.go | 8 +++ server/events/vcs/azuredevops_client.go | 2 +- server/events/vcs/bitbucketcloud/client.go | 2 +- server/events/vcs/bitbucketserver/client.go | 2 +- server/events/vcs/client.go | 2 +- server/events/vcs/github_client.go | 2 +- server/events/vcs/github_client_test.go | 3 +- server/events/vcs/gitlab_client.go | 35 ++++++++++- server/events/vcs/gitlab_client_test.go | 45 ++++++++++++- server/events/vcs/mocks/mock_client.go | 13 ++-- .../events/vcs/not_configured_vcs_client.go | 2 +- server/events/vcs/proxy.go | 4 +- server/server.go | 7 ++- server/user_config.go | 1 + 24 files changed, 256 insertions(+), 41 deletions(-) diff --git a/cmd/server.go b/cmd/server.go index 8d9ebd2a54..d26624f46a 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -93,6 +93,7 @@ const ( GHOrganizationFlag = "gh-org" GHWebhookSecretFlag = "gh-webhook-secret" // nolint: gosec GHAllowMergeableBypassApply = "gh-allow-mergeable-bypass-apply" // nolint: gosec + GitlabGroupAllowlistFlag = "gitlab-group-allowlist" GitlabHostnameFlag = "gitlab-hostname" GitlabTokenFlag = "gitlab-token" GitlabUserFlag = "gitlab-user" @@ -319,6 +320,17 @@ var stringFlags = map[string]stringFlag{ "This means that an attacker could spoof calls to Atlantis and cause it to perform malicious actions. " + "Should be specified via the ATLANTIS_GH_WEBHOOK_SECRET environment variable.", }, + GitlabGroupAllowlistFlag: { + description: "Comma separated list of key-value pairs representing the GitLab groups and the operations that " + + "the members of a particular group are allowed to perform. " + + "The format is {group}:{command},{group}:{command}. " + + "Valid values for 'command' are 'plan', 'apply' and '*', e.g. 'myorg/dev:plan,myorg/ops:apply,myorg/devops:*'" + + "This example gives the users from the 'myorg/dev' GitLab group the permissions to execute the 'plan' command, " + + "the 'myorg/ops' group the permissions to execute the 'apply' command, " + + "and allows the 'myorg/devops' group to perform any operation. If this argument is not provided, the default value (*:*) " + + "will be used and the default behavior will be to not check permissions " + + "and to allow users from any group to perform any operation.", + }, GitlabHostnameFlag: { description: "Hostname of your GitLab Enterprise installation. If using gitlab.com, no need to set.", defaultValue: DefaultGitlabHostname, diff --git a/runatlantis.io/docs/server-configuration.md b/runatlantis.io/docs/server-configuration.md index 6781abb149..498c487dab 100644 --- a/runatlantis.io/docs/server-configuration.md +++ b/runatlantis.io/docs/server-configuration.md @@ -597,6 +597,20 @@ and set `--autoplan-modules` to `false`. This means that an attacker could spoof calls to Atlantis and cause it to perform malicious actions. ::: +### `--gitlab-group-allowlist` + ```bash + atlantis server --gitlab-group-allowlist="myorg/mygroup:plan, myorg/secteam:apply, myorg/devops:apply, myorg/devops:import" + # or + ATLANTIS_GITLAB_GROUP_ALLOWLIST="myorg/mygroup:plan, myorg/secteam:apply, myorg/devops:apply, myorg/devops:import" + ``` + Comma-separated list of GitLab groups and permission pairs. + + By default, any group can plan and apply. + + ::: warning NOTE + Atlantis needs to be able to view the listed group members, inaccessible or non-existent groups are silently ignored. + ::: + ### `--gitlab-hostname` ```bash atlantis server --gitlab-hostname="my.gitlab.enterprise.com" diff --git a/runatlantis.io/docs/server-side-repo-config.md b/runatlantis.io/docs/server-side-repo-config.md index 178d2afe56..5ade38024a 100644 --- a/runatlantis.io/docs/server-side-repo-config.md +++ b/runatlantis.io/docs/server-side-repo-config.md @@ -536,18 +536,18 @@ If you set a workflow with the key `default`, it will override this. ### Policies -| Key | Type | Default | Required | Description | -|------------------------|-----------------|---------|-----------|----------------------------------------------------------| -| conftest_version | string | none | no | conftest version to run all policy sets | -| owners | Owners(#Owners) | none | yes | owners that can approve failing policies | -| approve_count | int | 1 | no | number of approvals required to bypass failing policies. | -| policy_sets | []PolicySet | none | yes | set of policies to run on a plan output | +| Key | Type | Default | Required | Description | +|------------------------|-----------------|---------|-----------|---------------------------------------------------------| +| conftest_version | string | none | no | conftest version to run all policy sets | +| owners | Owners(#Owners) | none | yes | owners that can approve failing policies | +| approve_count | int | 1 | no | number of approvals required to bypass failing policies | +| policy_sets | []PolicySet | none | yes | set of policies to run on a plan output | ### Owners -| Key | Type | Default | Required | Description | -|-------------|-------------------|---------|------------|---------------------------------------------------------| -| users | []string | none | no | list of github users that can approve failing policies | -| teams | []string | none | no | list of github teams that can approve failing policies | +| Key | Type | Default | Required | Description | +|-------------|-------------------|---------|------------|-------------------------------------------------------------------------| +| users | []string | none | no | list of GitHub or GitLab users that can approve failing policies | +| teams | []string | none | no | list of GitHub teams or GitLab groups that can approve failing policies | ### PolicySet diff --git a/server/core/config/valid/policies.go b/server/core/config/valid/policies.go index 8fb6cfdc91..0d76578948 100644 --- a/server/core/config/valid/policies.go +++ b/server/core/config/valid/policies.go @@ -1,6 +1,7 @@ package valid import ( + "slices" "strings" version "github.com/hashicorp/go-version" @@ -66,3 +67,16 @@ func (o *PolicyOwners) IsOwner(username string, userTeams []string) bool { return false } + +// Return all owner teams from all policy sets +func (p *PolicySets) AllTeams() []string { + teams := p.Owners.Teams + for _, policySet := range p.PolicySets { + for _, team := range policySet.Owners.Teams { + if !slices.Contains(teams, team) { + teams = append(teams, team) + } + } + } + return teams +} diff --git a/server/core/config/valid/policies_test.go b/server/core/config/valid/policies_test.go index c575a4585a..5147dd8686 100644 --- a/server/core/config/valid/policies_test.go +++ b/server/core/config/valid/policies_test.go @@ -120,3 +120,66 @@ func TestPoliciesConfig_IsOwners(t *testing.T) { }) } } + +func TestPoliciesConfig_AllTeams(t *testing.T) { + cases := []struct { + description string + input valid.PolicySets + expResult []string + }{ + { + description: "has only top-level team owner", + input: valid.PolicySets{ + Owners: valid.PolicyOwners{ + Teams: []string{ + "team1", + }, + }, + }, + expResult: []string{"team1"}, + }, + { + description: "has only policy-level team owner", + input: valid.PolicySets{ + PolicySets: []valid.PolicySet{ + { + Name: "policy1", + Owners: valid.PolicyOwners{ + Teams: []string{ + "team2", + }, + }, + }, + }, + }, + expResult: []string{"team2"}, + }, + { + description: "has both top-level and policy-level team owners", + input: valid.PolicySets{ + Owners: valid.PolicyOwners{ + Teams: []string{ + "team1", + }, + }, + PolicySets: []valid.PolicySet{ + { + Name: "policy1", + Owners: valid.PolicyOwners{ + Teams: []string{ + "team2", + }, + }, + }, + }, + }, + expResult: []string{"team1", "team2"}, + }, + } + for _, c := range cases { + t.Run(c.description, func(t *testing.T) { + result := c.input.AllTeams() + Equals(t, c.expResult, result) + }) + } +} diff --git a/server/events/command_runner.go b/server/events/command_runner.go index c6bbe29bf9..fb00732667 100644 --- a/server/events/command_runner.go +++ b/server/events/command_runner.go @@ -126,7 +126,8 @@ type DefaultCommandRunner struct { PreWorkflowHooksCommandRunner PreWorkflowHooksCommandRunner PostWorkflowHooksCommandRunner PostWorkflowHooksCommandRunner PullStatusFetcher PullStatusFetcher - TeamAllowlistChecker *TeamAllowlistChecker + GitHubTeamAllowlistChecker *TeamAllowlistChecker + GitLabGroupAllowlistChecker *TeamAllowlistChecker VarFileAllowlistChecker *VarFileAllowlistChecker CommitStatusUpdater CommitStatusUpdater } @@ -238,15 +239,27 @@ 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) { - if c.TeamAllowlistChecker == nil || !c.TeamAllowlistChecker.HasRules() { + var teamAllowListChecker *TeamAllowlistChecker + + switch repo.VCSHost.Type { + case models.Github: + teamAllowListChecker = c.GitHubTeamAllowlistChecker + case models.Gitlab: + teamAllowListChecker = c.GitLabGroupAllowlistChecker + default: + // allowlist restriction is not supported + return true, nil + } + + if teamAllowListChecker == nil || !teamAllowListChecker.HasRules() { // allowlist restriction is not enabled return true, nil } - teams, err := c.VCSClient.GetTeamNamesForUser(repo, user) + teams, err := c.VCSClient.GetTeamNamesForUser(repo, user, teamAllowListChecker.AllTeamsForCommand(cmdName)) if err != nil { return false, err } - ok := c.TeamAllowlistChecker.IsCommandAllowedForAnyTeam(teams, cmdName) + ok := teamAllowListChecker.IsCommandAllowedForAnyTeam(teams, cmdName) if !ok { return false, nil } diff --git a/server/events/command_runner_test.go b/server/events/command_runner_test.go index b99c6b1933..0fcfecd787 100644 --- a/server/events/command_runner_test.go +++ b/server/events/command_runner_test.go @@ -298,7 +298,7 @@ func TestRunCommentCommand_TeamAllowListChecker(t *testing.T) { t.Run("nil checker", func(t *testing.T) { vcsClient := setup(t) // by default these are false so don't need to reset - ch.TeamAllowlistChecker = nil + ch.GitHubTeamAllowlistChecker = nil var pull github.PullRequest modelPull := models.PullRequest{ BaseRepo: testdata.GithubRepo, @@ -308,14 +308,14 @@ func TestRunCommentCommand_TeamAllowListChecker(t *testing.T) { When(eventParsing.ParseGithubPull(&pull)).ThenReturn(modelPull, modelPull.BaseRepo, testdata.GithubRepo, nil) ch.RunCommentCommand(testdata.GithubRepo, nil, nil, testdata.User, testdata.Pull.Num, &events.CommentCommand{Name: command.Plan}) - vcsClient.VerifyWasCalled(Never()).GetTeamNamesForUser(testdata.GithubRepo, testdata.User) + vcsClient.VerifyWasCalled(Never()).GetTeamNamesForUser(testdata.GithubRepo, testdata.User, []string{}) vcsClient.VerifyWasCalledOnce().CreateComment(testdata.GithubRepo, modelPull.Num, "Ran Plan for 0 projects:", "plan") }) t.Run("no rules", func(t *testing.T) { vcsClient := setup(t) // by default these are false so don't need to reset - ch.TeamAllowlistChecker = &events.TeamAllowlistChecker{} + ch.GitHubTeamAllowlistChecker = &events.TeamAllowlistChecker{} var pull github.PullRequest modelPull := models.PullRequest{ BaseRepo: testdata.GithubRepo, @@ -325,7 +325,7 @@ func TestRunCommentCommand_TeamAllowListChecker(t *testing.T) { When(eventParsing.ParseGithubPull(&pull)).ThenReturn(modelPull, modelPull.BaseRepo, testdata.GithubRepo, nil) ch.RunCommentCommand(testdata.GithubRepo, nil, nil, testdata.User, testdata.Pull.Num, &events.CommentCommand{Name: command.Plan}) - vcsClient.VerifyWasCalled(Never()).GetTeamNamesForUser(testdata.GithubRepo, testdata.User) + vcsClient.VerifyWasCalled(Never()).GetTeamNamesForUser(testdata.GithubRepo, testdata.User, []string{}) vcsClient.VerifyWasCalledOnce().CreateComment(testdata.GithubRepo, modelPull.Num, "Ran Plan for 0 projects:", "plan") }) } diff --git a/server/events/project_command_runner.go b/server/events/project_command_runner.go index 736b8ac31b..aa86793530 100644 --- a/server/events/project_command_runner.go +++ b/server/events/project_command_runner.go @@ -343,7 +343,7 @@ func (p *DefaultProjectCommandRunner) doApprovePolicies(ctx command.ProjectConte // Only query the users team membership if any teams have been configured as owners on any policy set(s). if policySetCfg.HasTeamOwners() { // A convenient way to access vcsClient. Not sure if best way. - userTeams, err := p.VcsClient.GetTeamNamesForUser(ctx.Pull.BaseRepo, ctx.User) + userTeams, err := p.VcsClient.GetTeamNamesForUser(ctx.Pull.BaseRepo, ctx.User, policySetCfg.AllTeams()) if err != nil { ctx.Log.Err("unable to get team membership for user: %s", err) return nil, "", err diff --git a/server/events/project_command_runner_test.go b/server/events/project_command_runner_test.go index 786b67088e..2626041f19 100644 --- a/server/events/project_command_runner_test.go +++ b/server/events/project_command_runner_test.go @@ -1231,7 +1231,7 @@ func TestDefaultProjectCommandRunner_ApprovePolicies(t *testing.T) { } modelPull := models.PullRequest{BaseRepo: testdata.GithubRepo, State: models.OpenPullState, Num: testdata.Pull.Num} - When(runner.VcsClient.GetTeamNamesForUser(testdata.GithubRepo, testdata.User)).ThenReturn(c.userTeams, nil) + When(runner.VcsClient.GetTeamNamesForUser(testdata.GithubRepo, testdata.User, c.policySetCfg.AllTeams())).ThenReturn(c.userTeams, nil) ctx := command.ProjectContext{ User: testdata.User, Log: logging.NewNoopLogger(t), diff --git a/server/events/team_allowlist_checker.go b/server/events/team_allowlist_checker.go index 01e7bed73c..d8a21078ae 100644 --- a/server/events/team_allowlist_checker.go +++ b/server/events/team_allowlist_checker.go @@ -71,3 +71,16 @@ func (checker *TeamAllowlistChecker) IsCommandAllowedForAnyTeam(teams []string, } return false } + +// AllTeams returns all teams listed in the rule for a command +func (checker *TeamAllowlistChecker) AllTeamsForCommand(command string) []string { + var teamNames []string + for _, rule := range checker.rules { + for key, value := range rule { + if strings.EqualFold(value, command) { + teamNames = append(teamNames, key) + } + } + } + return teamNames +} diff --git a/server/events/team_allowlist_checker_test.go b/server/events/team_allowlist_checker_test.go index b389b49ae0..0a062eaecf 100644 --- a/server/events/team_allowlist_checker_test.go +++ b/server/events/team_allowlist_checker_test.go @@ -41,3 +41,11 @@ func TestIsCommandAllowedForAnyTeam(t *testing.T) { Equals(t, true, checker.IsCommandAllowedForAnyTeam(teams, `unlock`)) Equals(t, false, checker.IsCommandAllowedForAnyTeam(teams, `noop`)) } + +func TestAllTeamsForCommand(t *testing.T) { + allowlist := `bob:plan, dave:apply, connie:plan, connie:apply` + checker, err := events.NewTeamAllowlistChecker(allowlist) + Ok(t, err) + Equals(t, []string{"bob", "connie"}, checker.AllTeamsForCommand(`plan`)) + Equals(t, []string{"dave", "connie"}, checker.AllTeamsForCommand(`apply`)) +} diff --git a/server/events/vcs/azuredevops_client.go b/server/events/vcs/azuredevops_client.go index 429380d3f8..14ea20cf2c 100644 --- a/server/events/vcs/azuredevops_client.go +++ b/server/events/vcs/azuredevops_client.go @@ -390,7 +390,7 @@ func SplitAzureDevopsRepoFullName(repoFullName string) (owner string, project st } // GetTeamNamesForUser returns the names of the teams or groups that the user belongs to (in the organization the repository belongs to). -func (g *AzureDevopsClient) GetTeamNamesForUser(repo models.Repo, user models.User) ([]string, error) { //nolint: revive +func (g *AzureDevopsClient) GetTeamNamesForUser(repo models.Repo, user models.User, configuredTeams []string) ([]string, error) { //nolint: revive return nil, nil } diff --git a/server/events/vcs/bitbucketcloud/client.go b/server/events/vcs/bitbucketcloud/client.go index 13462a7a51..d0d9f1fc11 100644 --- a/server/events/vcs/bitbucketcloud/client.go +++ b/server/events/vcs/bitbucketcloud/client.go @@ -263,7 +263,7 @@ func (b *Client) makeRequest(method string, path string, reqBody io.Reader) ([]b } // GetTeamNamesForUser returns the names of the teams or groups that the user belongs to (in the organization the repository belongs to). -func (b *Client) GetTeamNamesForUser(_ models.Repo, _ models.User) ([]string, error) { +func (b *Client) GetTeamNamesForUser(_ models.Repo, _ models.User, _ []string) ([]string, error) { return nil, nil } diff --git a/server/events/vcs/bitbucketserver/client.go b/server/events/vcs/bitbucketserver/client.go index 9012c35e58..8c2f4fad09 100644 --- a/server/events/vcs/bitbucketserver/client.go +++ b/server/events/vcs/bitbucketserver/client.go @@ -347,7 +347,7 @@ func (b *Client) makeRequest(method string, path string, reqBody io.Reader) ([]b } // GetTeamNamesForUser returns the names of the teams or groups that the user belongs to (in the organization the repository belongs to). -func (b *Client) GetTeamNamesForUser(_ models.Repo, _ models.User) ([]string, error) { +func (b *Client) GetTeamNamesForUser(_ models.Repo, _ models.User, _ []string) ([]string, error) { return nil, nil } diff --git a/server/events/vcs/client.go b/server/events/vcs/client.go index dd2e489f6f..0ead1d389b 100644 --- a/server/events/vcs/client.go +++ b/server/events/vcs/client.go @@ -41,7 +41,7 @@ type Client interface { DiscardReviews(repo models.Repo, pull models.PullRequest) error MergePull(pull models.PullRequest, pullOptions models.PullRequestOptions) error MarkdownPullLink(pull models.PullRequest) (string, error) - GetTeamNamesForUser(repo models.Repo, user models.User) ([]string, error) + GetTeamNamesForUser(repo models.Repo, user models.User, configuredTeams []string) ([]string, error) // GetFileContent a repository file content from VCS (which support fetch a single file from repository) // The first return value indicates whether the repo contains a file or not diff --git a/server/events/vcs/github_client.go b/server/events/vcs/github_client.go index 73cce78909..ed2ed58e1f 100644 --- a/server/events/vcs/github_client.go +++ b/server/events/vcs/github_client.go @@ -638,7 +638,7 @@ func (g *GithubClient) MarkdownPullLink(pull models.PullRequest) (string, error) // GetTeamNamesForUser returns the names of the teams or groups that the user belongs to (in the organization the repository belongs to). // https://docs.github.com/en/graphql/reference/objects#organization -func (g *GithubClient) GetTeamNamesForUser(repo models.Repo, user models.User) ([]string, error) { +func (g *GithubClient) GetTeamNamesForUser(repo models.Repo, user models.User, _ []string) ([]string, error) { orgName := repo.Owner variables := map[string]interface{}{ "orgName": githubv4.String(orgName), diff --git a/server/events/vcs/github_client_test.go b/server/events/vcs/github_client_test.go index 4f2f8fd3e7..eb1843f42a 100644 --- a/server/events/vcs/github_client_test.go +++ b/server/events/vcs/github_client_test.go @@ -1274,7 +1274,8 @@ func TestGithubClient_GetTeamNamesForUser(t *testing.T) { Owner: "testrepo", }, models.User{ Username: "testuser", - }) + }, + []string{}) Ok(t, err) Equals(t, []string{"Frontend Developers", "frontend-developers", "Employees", "employees"}, teams) } diff --git a/server/events/vcs/gitlab_client.go b/server/events/vcs/gitlab_client.go index c23205a18f..14e6384214 100644 --- a/server/events/vcs/gitlab_client.go +++ b/server/events/vcs/gitlab_client.go @@ -519,9 +519,38 @@ func MustConstraint(constraint string) version.Constraints { return c } -// GetTeamNamesForUser returns the names of the teams or groups that the user belongs to (in the organization the repository belongs to). -func (g *GitlabClient) GetTeamNamesForUser(_ models.Repo, _ models.User) ([]string, error) { - return nil, nil +// GetTeamNamesForUser returns the names of the GitLab groups that the user belongs to. +// The user membership is checked in each group from configuredTeams, groups +// that the Atlantis user doesn't have access to are silently ignored. +func (g *GitlabClient) GetTeamNamesForUser(repo models.Repo, user models.User, configuredTeams []string) ([]string, error) { + var teamNames []string + + users, resp, err := g.Client.Users.ListUsers(&gitlab.ListUsersOptions{Username: &user.Username}) + if resp.StatusCode == http.StatusNotFound { + return teamNames, nil + } + if err != nil { + return nil, errors.Wrapf(err, "GET /users returned: %d", resp.StatusCode) + } else if len(users) == 0 { + return nil, errors.Wrap(err, "GET /users returned no user") + } else if len(users) > 1 { + // Theoretically impossible, just being extra safe + return nil, errors.Wrap(err, "GET /users returned more than 1 user") + } + userID := users[0].ID + for _, groupName := range configuredTeams { + membership, resp, err := g.Client.GroupMembers.GetGroupMember(groupName, userID) + if resp.StatusCode == http.StatusNotFound || resp.StatusCode == http.StatusForbidden { + continue + } + if err != nil { + return nil, errors.Wrapf(err, "GET /groups/%s/members/%d returned: %d", groupName, userID, resp.StatusCode) + } + if resp.StatusCode == http.StatusOK && membership.State == "active" { + teamNames = append(teamNames, groupName) + } + } + return teamNames, nil } // GetFileContent a repository file content from VCS (which support fetch a single file from repository) diff --git a/server/events/vcs/gitlab_client_test.go b/server/events/vcs/gitlab_client_test.go index bae22a6ed7..22f6f712ca 100644 --- a/server/events/vcs/gitlab_client_test.go +++ b/server/events/vcs/gitlab_client_test.go @@ -634,7 +634,7 @@ func TestGitlabClient_HideOldComments(t *testing.T) { Equals(t, summaryFooter, gotNotePutCalls[1].comment[2]) } -func TestGithubClient_GetPullLabels(t *testing.T) { +func TestGitlabClient_GetPullLabels(t *testing.T) { var mergeSuccessWithLabel = strings.ReplaceAll(mergeSuccess, `"labels":[]`, `"labels":["work in progress"]`) testServer := httptest.NewServer( http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -665,7 +665,7 @@ func TestGithubClient_GetPullLabels(t *testing.T) { Equals(t, []string{"work in progress"}, labels) } -func TestGithubClient_GetPullLabels_EmptyResponse(t *testing.T) { +func TestGitlabClient_GetPullLabels_EmptyResponse(t *testing.T) { testServer := httptest.NewServer( http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.RequestURI { @@ -695,6 +695,45 @@ func TestGithubClient_GetPullLabels_EmptyResponse(t *testing.T) { Equals(t, 0, len(labels)) } +// GetTeamNamesForUser returns the names of the GitLab groups that the user belongs to. +func TestGitlabClient_GetTeamNamesForUser(t *testing.T) { + configuredTeams := []string{"someorg/group1", "someorg/group2", "someorg/group3", "someorg/group4"} + testServer := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.RequestURI { + case "/api/v4/users?username=testuser": + w.WriteHeader(http.StatusOK) + w.Write([]byte(userSuccess)) // nolint: errcheck + case "/api/v4/groups/someorg%2Fgroup1/members/123", "/api/v4/groups/someorg%2Fgroup2/members/123": + w.WriteHeader(http.StatusOK) + w.Write([]byte(groupMembershipSuccess)) // nolint: errcheck + case "/api/v4/groups/someorg%2Fgroup3/members/123": + http.Error(w, "forbidden", http.StatusForbidden) + case "/api/v4/groups/someorg%2Fgroup4/members/123": + http.Error(w, "not found", http.StatusNotFound) + default: + t.Errorf("got unexpected request at %q", r.RequestURI) + http.Error(w, "not found", http.StatusNotFound) + } + })) + internalClient, err := gitlab.NewClient("token", gitlab.WithBaseURL(testServer.URL)) + Ok(t, err) + client := &GitlabClient{ + Client: internalClient, + Version: nil, + logger: logging.NewNoopLogger(t), + } + + teams, err := client.GetTeamNamesForUser(models.Repo{ + Owner: "someorg", + }, models.User{ + Username: "testuser", + }, + configuredTeams) + Ok(t, err) + Equals(t, []string{"someorg/group1", "someorg/group2"}, teams) +} + var mergeSuccess = `{"id":22461274,"iid":13,"project_id":4580910,"title":"Update main.tf","description":"","state":"merged","created_at":"2019-01-15T18:27:29.375Z","updated_at":"2019-01-25T17:28:01.437Z","merged_by":{"id":1755902,"name":"Luke Kysow","username":"lkysow","state":"active","avatar_url":"https://secure.gravatar.com/avatar/25fd57e71590fe28736624ff24d41c5f?s=80\u0026d=identicon","web_url":"https://gitlab.com/lkysow"},"merged_at":"2019-01-25T17:28:01.459Z","closed_by":null,"closed_at":null,"target_branch":"patch-1","source_branch":"patch-1-merger","upvotes":0,"downvotes":0,"author":{"id":1755902,"name":"Luke Kysow","username":"lkysow","state":"active","avatar_url":"https://secure.gravatar.com/avatar/25fd57e71590fe28736624ff24d41c5f?s=80\u0026d=identicon","web_url":"https://gitlab.com/lkysow"},"assignee":null,"source_project_id":4580910,"target_project_id":4580910,"labels":[],"work_in_progress":false,"milestone":null,"merge_when_pipeline_succeeds":false,"merge_status":"can_be_merged","detailed_merge_status":"mergeable","sha":"cb86d70f464632bdfbe1bb9bc0f2f9d847a774a0","merge_commit_sha":"c9b336f1c71d3e64810b8cfa2abcfab232d6bff6","user_notes_count":0,"discussion_locked":null,"should_remove_source_branch":null,"force_remove_source_branch":false,"web_url":"https://gitlab.com/lkysow/atlantis-example/merge_requests/13","time_stats":{"time_estimate":0,"total_time_spent":0,"human_time_estimate":null,"human_total_time_spent":null},"squash":false,"subscribed":true,"changes_count":"1","latest_build_started_at":null,"latest_build_finished_at":null,"first_deployed_to_production_at":null,"pipeline":null,"diff_refs":{"base_sha":"67cb91d3f6198189f433c045154a885784ba6977","head_sha":"cb86d70f464632bdfbe1bb9bc0f2f9d847a774a0","start_sha":"67cb91d3f6198189f433c045154a885784ba6977"},"merge_error":null,"approvals_before_merge":null}` //nolint:gosec // No credentials var pipelineSuccess = `{"id": 22461274,"iid": 13,"project_id": 4580910,"title": "Update main.tf","description": "","state": "opened","created_at": "2019-01-15T18:27:29.375Z","updated_at": "2019-01-25T17:28:01.437Z","merged_by": null,"merged_at": null,"closed_by": null,"closed_at": null,"target_branch": "patch-1","source_branch": "patch-1-merger","user_notes_count": 0,"upvotes": 0,"downvotes": 0,"author": {"id": 1755902,"name": "Luke Kysow","username": "lkysow","state": "active","avatar_url": "https://secure.gravatar.com/avatar/25fd57e71590fe28736624ff24d41c5f?s=80\u0026d=identicon","web_url": "https://gitlab.com/lkysow"},"assignee": null,"reviewers": [],"source_project_id": 4580910,"target_project_id": 4580910,"labels": [],"work_in_progress": false,"milestone": null,"merge_when_pipeline_succeeds": false,"merge_status": "can_be_merged","detailed_merge_status": "mergeable","sha": "cb86d70f464632bdfbe1bb9bc0f2f9d847a774a0","merge_commit_sha": null,"squash_commit_sha": null,"discussion_locked": null,"should_remove_source_branch": null,"force_remove_source_branch": true,"reference": "!13","references": {"short": "!13","relative": "!13","full": "lkysow/atlantis-example!13"},"web_url": "https://gitlab.com/lkysow/atlantis-example/merge_requests/13","time_stats": {"time_estimate": 0,"total_time_spent": 0,"human_time_estimate": null,"human_total_time_spent": null},"squash": true,"task_completion_status": {"count": 0,"completed_count": 0},"has_conflicts": false,"blocking_discussions_resolved": true,"approvals_before_merge": null,"subscribed": false,"changes_count": "1","latest_build_started_at": "2019-01-15T18:27:29.375Z","latest_build_finished_at": "2019-01-25T17:28:01.437Z","first_deployed_to_production_at": null,"pipeline": {"id": 488598,"sha": "67cb91d3f6198189f433c045154a885784ba6977","ref": "patch-1-merger","status": "success","created_at": "2019-01-15T18:27:29.375Z","updated_at": "2019-01-25T17:28:01.437Z","web_url": "https://gitlab.com/lkysow/atlantis-example/-/pipelines/488598"},"head_pipeline": {"id": 488598,"sha": "67cb91d3f6198189f433c045154a885784ba6977","ref": "patch-1-merger","status": "success","created_at": "2019-01-15T18:27:29.375Z","updated_at": "2019-01-25T17:28:01.437Z","web_url": "https://gitlab.com/lkysow/atlantis-example/-/pipelines/488598","before_sha": "0000000000000000000000000000000000000000","tag": false,"yaml_errors": null,"user": {"id": 1755902,"name": "Luke Kysow","username": "lkysow","state": "active","avatar_url": "https://secure.gravatar.com/avatar/25fd57e71590fe28736624ff24d41c5f?s=80\u0026d=identicon","web_url": "https://gitlab.com/lkysow"},"started_at": "2019-01-15T18:27:29.375Z","finished_at": "2019-01-25T17:28:01.437Z","committed_at": null,"duration": 31,"coverage": null,"detailed_status": {"icon": "status_success","text": "passed","label": "passed","group": "success","tooltip": "passed","has_details": true,"details_path": "/lkysow/atlantis-example/-/pipelines/488598","illustration": null,"favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png"}},"diff_refs": {"base_sha": "67cb91d3f6198189f433c045154a885784ba6977","head_sha": "cb86d70f464632bdfbe1bb9bc0f2f9d847a774a0","start_sha": "67cb91d3f6198189f433c045154a885784ba6977"},"merge_error": null,"first_contribution": false,"user": {"can_merge": true}}` //nolint:gosec // No credentials var detailedMergeStatusCiMustPass = `{"id": 22461274,"iid": 13,"project_id": 4580910,"title": "Update main.tf","description": "","state": "opened","created_at": "2019-01-15T18:27:29.375Z","updated_at": "2019-01-25T17:28:01.437Z","merged_by": null,"merged_at": null,"closed_by": null,"closed_at": null,"target_branch": "patch-1","source_branch": "patch-1-merger","user_notes_count": 0,"upvotes": 0,"downvotes": 0,"author": {"id": 1755902,"name": "Luke Kysow","username": "lkysow","state": "active","avatar_url": "https://secure.gravatar.com/avatar/25fd57e71590fe28736624ff24d41c5f?s=80\u0026d=identicon","web_url": "https://gitlab.com/lkysow"},"assignee": null,"reviewers": [],"source_project_id": 4580910,"target_project_id": 4580910,"labels": [],"work_in_progress": false,"milestone": null,"merge_when_pipeline_succeeds": false,"merge_status": "can_be_merged","detailed_merge_status": "ci_must_pass","sha": "cb86d70f464632bdfbe1bb9bc0f2f9d847a774a0","merge_commit_sha": null,"squash_commit_sha": null,"discussion_locked": null,"should_remove_source_branch": null,"force_remove_source_branch": true,"reference": "!13","references": {"short": "!13","relative": "!13","full": "lkysow/atlantis-example!13"},"web_url": "https://gitlab.com/lkysow/atlantis-example/merge_requests/13","time_stats": {"time_estimate": 0,"total_time_spent": 0,"human_time_estimate": null,"human_total_time_spent": null},"squash": true,"task_completion_status": {"count": 0,"completed_count": 0},"has_conflicts": false,"blocking_discussions_resolved": true,"approvals_before_merge": null,"subscribed": false,"changes_count": "1","latest_build_started_at": "2019-01-15T18:27:29.375Z","latest_build_finished_at": "2019-01-25T17:28:01.437Z","first_deployed_to_production_at": null,"pipeline": {"id": 488598,"sha": "67cb91d3f6198189f433c045154a885784ba6977","ref": "patch-1-merger","status": "success","created_at": "2019-01-15T18:27:29.375Z","updated_at": "2019-01-25T17:28:01.437Z","web_url": "https://gitlab.com/lkysow/atlantis-example/-/pipelines/488598"},"head_pipeline": {"id": 488598,"sha": "67cb91d3f6198189f433c045154a885784ba6977","ref": "patch-1-merger","status": "success","created_at": "2019-01-15T18:27:29.375Z","updated_at": "2019-01-25T17:28:01.437Z","web_url": "https://gitlab.com/lkysow/atlantis-example/-/pipelines/488598","before_sha": "0000000000000000000000000000000000000000","tag": false,"yaml_errors": null,"user": {"id": 1755902,"name": "Luke Kysow","username": "lkysow","state": "active","avatar_url": "https://secure.gravatar.com/avatar/25fd57e71590fe28736624ff24d41c5f?s=80\u0026d=identicon","web_url": "https://gitlab.com/lkysow"},"started_at": "2019-01-15T18:27:29.375Z","finished_at": "2019-01-25T17:28:01.437Z","committed_at": null,"duration": 31,"coverage": null,"detailed_status": {"icon": "status_success","text": "passed","label": "passed","group": "success","tooltip": "passed","has_details": true,"details_path": "/lkysow/atlantis-example/-/pipelines/488598","illustration": null,"favicon": "/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png"}},"diff_refs": {"base_sha": "67cb91d3f6198189f433c045154a885784ba6977","head_sha": "cb86d70f464632bdfbe1bb9bc0f2f9d847a774a0","start_sha": "67cb91d3f6198189f433c045154a885784ba6977"},"merge_error": null,"first_contribution": false,"user": {"can_merge": true}}` //nolint:gosec // No credentials @@ -702,3 +741,5 @@ var projectSuccess = `{"id": 4580910,"description": "","name": "atlantis-example var changesPending = `{"id":8312,"iid":102,"target_branch":"main","source_branch":"TestBranch","project_id":3771,"title":"Update somefile.yaml","state":"opened","created_at":"2023-03-14T13:43:17.895Z","updated_at":"2023-03-14T13:43:17.895Z","upvotes":0,"downvotes":0,"author":{"id":1755902,"name":"Luke Kysow","username":"lkysow","state":"active","avatar_url":"https://secure.gravatar.com/avatar/25fd57e71590fe28736624ff24d41c5f?s=80\\u0026d=identicon","web_url":"https://gitlab.com/lkysow"},"assignee":null,"assignees":[],"reviewers":[],"source_project_id":3771,"target_project_id":3771,"labels":"","description":"","draft":false,"work_in_progress":false,"milestone":null,"merge_when_pipeline_succeeds":false,"detailed_merge_status":"checking","merge_error":"","merged_by":null,"merged_at":null,"closed_by":null,"closed_at":null,"subscribed":false,"sha":"cb86d70f464632bdfbe1bb9bc0f2f9d847a774a0","merge_commit_sha":"","squash_commit_sha":"","user_notes_count":0,"changes_count":"","should_remove_source_branch":false,"force_remove_source_branch":true,"allow_collaboration":false,"web_url":"https://gitlab.com/lkysow/atlantis-example/merge_requests/13","references":{"short":"!13","relative":"!13","full":"lkysow/atlantis-example!13"},"discussion_locked":false,"changes":[],"user":{"can_merge":true},"time_stats":{"human_time_estimate":"","human_total_time_spent":"","time_estimate":0,"total_time_spent":0},"squash":false,"pipeline":null,"head_pipeline":null,"diff_refs":{"base_sha":"","head_sha":"","start_sha":""},"diverged_commits_count":0,"rebase_in_progress":false,"approvals_before_merge":0,"reference":"!13","first_contribution":false,"task_completion_status":{"count":0,"completed_count":0},"has_conflicts":false,"blocking_discussions_resolved":true,"overflow":false,"merge_status":"checking"}` //nolint:gosec // No credentials var changesAvailable = `{"id":8312,"iid":102,"target_branch":"main","source_branch":"TestBranch","project_id":3771,"title":"Update somefile.yaml","state":"opened","created_at":"2023-03-14T13:43:17.895Z","updated_at":"2023-03-14T13:43:59.978Z","upvotes":0,"downvotes":0,"author":{"id":1755902,"name":"Luke Kysow","username":"lkysow","state":"active","avatar_url":"https://secure.gravatar.com/avatar/25fd57e71590fe28736624ff24d41c5f?s=80\\u0026d=identicon","web_url":"https://gitlab.com/lkysow"},"assignee":null,"assignees":[],"reviewers":[],"source_project_id":3771,"target_project_id":3771,"labels":[],"description":"","draft":false,"work_in_progress":false,"milestone":null,"merge_when_pipeline_succeeds":false,"detailed_merge_status":"not_approved","merge_error":"","merged_by":null,"merged_at":null,"closed_by":null,"closed_at":null,"subscribed":false,"sha":"cb86d70f464632bdfbe1bb9bc0f2f9d847a774a0","merge_commit_sha":null,"squash_commit_sha":null,"user_notes_count":0,"changes_count":"1","should_remove_source_branch":null,"force_remove_source_branch":true,"allow_collaboration":false,"web_url":"https://gitlab.com/lkysow/atlantis-example/merge_requests/13","references":{"short":"!13","relative":"!13","full":"lkysow/atlantis-example!13"},"discussion_locked":null,"changes":[{"old_path":"somefile.yaml","new_path":"somefile.yaml","a_mode":"100644","b_mode":"100644","diff":"--- a/somefile.yaml\\ +++ b/somefile.yaml\\ @@ -1 +1 @@\\ -gud\\ +good","new_file":false,"renamed_file":false,"deleted_file":false}],"user":{"can_merge":true},"time_stats":{"human_time_estimate":null,"human_total_time_spent":null,"time_estimate":0,"total_time_spent":0},"squash":false,"pipeline":null,"head_pipeline":null,"diff_refs":{"base_sha":"67cb91d3f6198189f433c045154a885784ba6977","head_sha":"cb86d70f464632bdfbe1bb9bc0f2f9d847a774a0","start_sha":"67cb91d3f6198189f433c045154a885784ba6977"},"approvals_before_merge":null,"reference":"!13","task_completion_status":{"count":0,"completed_count":0},"has_conflicts":false,"blocking_discussions_resolved":true,"overflow":false,"merge_status":"can_be_merged"}` //nolint:gosec // No credentials var headPipelineNotAvailable = `{"id": 22461274,"iid": 13,"project_id": 4580910,"title": "Update main.tf","description": "","state": "opened","created_at": "2019-01-15T18:27:29.375Z","updated_at": "2019-01-25T17:28:01.437Z","merged_by": null,"merged_at": null,"closed_by": null,"closed_at": null,"target_branch": "patch-1","source_branch": "patch-1-merger","user_notes_count": 0,"upvotes": 0,"downvotes": 0,"author": {"id": 1755902,"name": "Luke Kysow","username": "lkysow","state": "active","avatar_url": "https://secure.gravatar.com/avatar/25fd57e71590fe28736624ff24d41c5f?s=80\u0026d=identicon","web_url": "https://gitlab.com/lkysow"},"assignee": null,"reviewers": [],"source_project_id": 4580910,"target_project_id": 4580910,"labels": [],"work_in_progress": false,"milestone": null,"merge_when_pipeline_succeeds": false,"merge_status": "can_be_merged","detailed_merge_status": "mergeable","sha": "cb86d70f464632bdfbe1bb9bc0f2f9d847a774a0","merge_commit_sha": null,"squash_commit_sha": null,"discussion_locked": null,"should_remove_source_branch": null,"force_remove_source_branch": true,"reference": "!13","references": {"short": "!13","relative": "!13","full": "lkysow/atlantis-example!13"},"web_url": "https://gitlab.com/lkysow/atlantis-example/merge_requests/13","time_stats": {"time_estimate": 0,"total_time_spent": 0,"human_time_estimate": null,"human_total_time_spent": null},"squash": true,"task_completion_status": {"count": 0,"completed_count": 0},"has_conflicts": false,"blocking_discussions_resolved": true,"approvals_before_merge": null,"subscribed": false,"changes_count": "1","latest_build_started_at": "2019-01-15T18:27:29.375Z","latest_build_finished_at": "2019-01-25T17:28:01.437Z","first_deployed_to_production_at": null,"pipeline": {"id": 488598,"sha": "67cb91d3f6198189f433c045154a885784ba6977","ref": "patch-1-merger","status": "success","created_at": "2019-01-15T18:27:29.375Z","updated_at": "2019-01-25T17:28:01.437Z","web_url": "https://gitlab.com/lkysow/atlantis-example/-/pipelines/488598"},"head_pipeline": null,"diff_refs": {"base_sha": "67cb91d3f6198189f433c045154a885784ba6977","head_sha": "cb86d70f464632bdfbe1bb9bc0f2f9d847a774a0","start_sha": "67cb91d3f6198189f433c045154a885784ba6977"},"merge_error": null,"first_contribution": false,"user": {"can_merge": true}}` //nolint:gosec // No credentials +var groupMembershipSuccess = `{"access_level":50,"created_at":"2023-11-28T01:23:45.789Z","created_by":{"id":456,"username":"someone","name":"Someone","state":"active","locked":false,"avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/456/avatar.png","web_url":"https://gitlab.com/someone"},"expires_at":null,"id":123,"username":"testuser","name":"Test User","state":"active","locked":false,"avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/123/avatar.png","web_url":"https://gitlab.com/testuser","membership_state":"active"}` //nolint:gosec // No credentials +var userSuccess = `[{"id":123,"username":"testuser","name":"Test User","state":"active","locked":false,"avatar_url":"https://gitlab.com/uploads/-/system/user/avatar/123/avatar.png","web_url":"https://gitlab.com/testuser"}]` //nolint:gosec // No credentials diff --git a/server/events/vcs/mocks/mock_client.go b/server/events/vcs/mocks/mock_client.go index 7583e22fac..8561698be6 100644 --- a/server/events/vcs/mocks/mock_client.go +++ b/server/events/vcs/mocks/mock_client.go @@ -4,10 +4,11 @@ package mocks import ( - pegomock "github.com/petergtz/pegomock/v4" - models "github.com/runatlantis/atlantis/server/events/models" "reflect" "time" + + pegomock "github.com/petergtz/pegomock/v4" + models "github.com/runatlantis/atlantis/server/events/models" ) type MockClient struct { @@ -135,11 +136,11 @@ func (mock *MockClient) GetPullLabels(repo models.Repo, pull models.PullRequest) return ret0, ret1 } -func (mock *MockClient) GetTeamNamesForUser(repo models.Repo, user models.User) ([]string, error) { +func (mock *MockClient) GetTeamNamesForUser(repo models.Repo, user models.User, configuredTeams []string) ([]string, error) { if mock == nil { panic("mock must not be nil. Use myMock := NewMockClient().") } - params := []pegomock.Param{repo, user} + params := []pegomock.Param{repo, user, configuredTeams} result := pegomock.GetGenericMockFrom(mock).Invoke("GetTeamNamesForUser", params, []reflect.Type{reflect.TypeOf((*[]string)(nil)).Elem(), reflect.TypeOf((*error)(nil)).Elem()}) var ret0 []string var ret1 error @@ -517,8 +518,8 @@ func (c *MockClient_GetPullLabels_OngoingVerification) GetAllCapturedArguments() return } -func (verifier *VerifierMockClient) GetTeamNamesForUser(repo models.Repo, user models.User) *MockClient_GetTeamNamesForUser_OngoingVerification { - params := []pegomock.Param{repo, user} +func (verifier *VerifierMockClient) GetTeamNamesForUser(repo models.Repo, user models.User, configuredTeams []string) *MockClient_GetTeamNamesForUser_OngoingVerification { + params := []pegomock.Param{repo, user, configuredTeams} methodInvocations := pegomock.GetGenericMockFrom(verifier.mock).Verify(verifier.inOrderContext, verifier.invocationCountMatcher, "GetTeamNamesForUser", params, verifier.timeout) return &MockClient_GetTeamNamesForUser_OngoingVerification{mock: verifier.mock, methodInvocations: methodInvocations} } diff --git a/server/events/vcs/not_configured_vcs_client.go b/server/events/vcs/not_configured_vcs_client.go index c916195f3b..0ba55ed917 100644 --- a/server/events/vcs/not_configured_vcs_client.go +++ b/server/events/vcs/not_configured_vcs_client.go @@ -59,7 +59,7 @@ func (a *NotConfiguredVCSClient) MarkdownPullLink(_ models.PullRequest) (string, func (a *NotConfiguredVCSClient) err() error { return fmt.Errorf("atlantis was not configured to support repos from %s", a.Host.String()) } -func (a *NotConfiguredVCSClient) GetTeamNamesForUser(_ models.Repo, _ models.User) ([]string, error) { +func (a *NotConfiguredVCSClient) GetTeamNamesForUser(_ models.Repo, _ models.User, _ []string) ([]string, error) { return nil, a.err() } diff --git a/server/events/vcs/proxy.go b/server/events/vcs/proxy.go index 25637bcd0f..1c2fb26637 100644 --- a/server/events/vcs/proxy.go +++ b/server/events/vcs/proxy.go @@ -92,8 +92,8 @@ func (d *ClientProxy) MarkdownPullLink(pull models.PullRequest) (string, error) return d.clients[pull.BaseRepo.VCSHost.Type].MarkdownPullLink(pull) } -func (d *ClientProxy) GetTeamNamesForUser(repo models.Repo, user models.User) ([]string, error) { - return d.clients[repo.VCSHost.Type].GetTeamNamesForUser(repo, user) +func (d *ClientProxy) GetTeamNamesForUser(repo models.Repo, user models.User, configuredTeams []string) ([]string, error) { + return d.clients[repo.VCSHost.Type].GetTeamNamesForUser(repo, user, configuredTeams) } func (d *ClientProxy) GetFileContent(pull models.PullRequest, fileName string) (bool, []byte, error) { diff --git a/server/server.go b/server/server.go index 8d05a4633b..0eea510695 100644 --- a/server/server.go +++ b/server/server.go @@ -786,6 +786,10 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { if err != nil { return nil, err } + gitlabGroupAllowlistChecker, err := events.NewTeamAllowlistChecker(userConfig.GitlabGroupAllowlist) + if err != nil { + return nil, err + } varFileAllowlistChecker, err := events.NewVarFileAllowlistChecker(userConfig.VarFileAllowlist) if err != nil { return nil, err @@ -812,7 +816,8 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { PreWorkflowHooksCommandRunner: preWorkflowHooksCommandRunner, PostWorkflowHooksCommandRunner: postWorkflowHooksCommandRunner, PullStatusFetcher: backend, - TeamAllowlistChecker: githubTeamAllowlistChecker, + GitHubTeamAllowlistChecker: githubTeamAllowlistChecker, + GitLabGroupAllowlistChecker: gitlabGroupAllowlistChecker, VarFileAllowlistChecker: varFileAllowlistChecker, CommitStatusUpdater: commitStatusUpdater, } diff --git a/server/user_config.go b/server/user_config.go index 3c533d1b83..d439e32f7d 100644 --- a/server/user_config.go +++ b/server/user_config.go @@ -58,6 +58,7 @@ type UserConfig struct { GithubAppSlug string `mapstructure:"gh-app-slug"` GithubTeamAllowlist string `mapstructure:"gh-team-allowlist"` GitlabHostname string `mapstructure:"gitlab-hostname"` + GitlabGroupAllowlist string `mapstructure:"gitlab-group-allowlist"` GitlabToken string `mapstructure:"gitlab-token"` GitlabUser string `mapstructure:"gitlab-user"` GitlabWebhookSecret string `mapstructure:"gitlab-webhook-secret"`