diff --git a/runatlantis.io/docs/access-credentials.md b/runatlantis.io/docs/access-credentials.md index 05d320b50..3a40bcef1 100644 --- a/runatlantis.io/docs/access-credentials.md +++ b/runatlantis.io/docs/access-credentials.md @@ -57,6 +57,8 @@ GitHub App needs these permissions. These are automatically set when a GitHub ap ::: tip NOTE Since v0.19.7, a new permission for `Administration` has been added. If you have already created a GitHub app, updating Atlantis to v0.19.7 will not automatically add this permission, so you will need to set it manually. + +Since v0.22.3, a new permission for `Members` has been added, which is required for features that apply permissions to an organizations team members rather than individual users. Like the `Administration` permission above, updating Atlantis will not automatically add this permission, so if you wish to use features that rely on checking team membership you will need to add this manually. ::: | Type | Access | @@ -69,6 +71,7 @@ Since v0.19.7, a new permission for `Administration` has been added. If you have | Metadata | Read-only (default) | | Pull requests | Read and write | | Webhooks | Read and write | +| Members | Read-only | ### GitLab - Follow: [https://docs.gitlab.com/ce/user/profile/personal_access_tokens.html#create-a-personal-access-token](https://docs.gitlab.com/ce/user/profile/personal_access_tokens.html#create-a-personal-access-token) diff --git a/runatlantis.io/docs/server-side-repo-config.md b/runatlantis.io/docs/server-side-repo-config.md index 0c9a902af..d4a77f9f5 100644 --- a/runatlantis.io/docs/server-side-repo-config.md +++ b/runatlantis.io/docs/server-side-repo-config.md @@ -521,7 +521,8 @@ If you set a workflow with the key `default`, it will override this. ### Owners | Key | Type | Default | Required | Description | |-------------|-------------------|---------|------------|---------------------------------------------------------| -| users | []string | none | yes | list of github users that can approve failing policies | +| 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 | ### PolicySet diff --git a/server/controllers/events/events_controller_e2e_test.go b/server/controllers/events/events_controller_e2e_test.go index 62b59ef65..3371a1c12 100644 --- a/server/controllers/events/events_controller_e2e_test.go +++ b/server/controllers/events/events_controller_e2e_test.go @@ -1222,6 +1222,7 @@ func setupE2E(t *testing.T, repoDir string, opt setupOption) (events_controllers dbUpdater, silenceNoProjects, false, + e2eVCSClient, ) unlockCommandRunner := events.NewUnlockCommandRunner( diff --git a/server/controllers/github_app_controller.go b/server/controllers/github_app_controller.go index 14753a38f..06a160413 100644 --- a/server/controllers/github_app_controller.go +++ b/server/controllers/github_app_controller.go @@ -121,6 +121,7 @@ func (g *GithubAppController) New(w http.ResponseWriter, r *http.Request) { "repository_hooks": "write", "statuses": "write", "administration": "read", + "members": "read", }, } diff --git a/server/core/config/raw/policies.go b/server/core/config/raw/policies.go index 33fb78ee1..270e314a4 100644 --- a/server/core/config/raw/policies.go +++ b/server/core/config/raw/policies.go @@ -40,6 +40,7 @@ func (p PolicySets) ToValid() valid.PolicySets { type PolicyOwners struct { Users []string `yaml:"users,omitempty" json:"users,omitempty"` + Teams []string `yaml:"teams,omitempty" json:"teams,omitempty"` } func (o PolicyOwners) ToValid() valid.PolicyOwners { @@ -48,6 +49,10 @@ func (o PolicyOwners) ToValid() valid.PolicyOwners { if len(o.Users) > 0 { policyOwners.Users = o.Users } + + if len(o.Teams) > 0 { + policyOwners.Teams = o.Teams + } return policyOwners } diff --git a/server/core/config/raw/policies_test.go b/server/core/config/raw/policies_test.go index 53b599d1b..7987d7bf5 100644 --- a/server/core/config/raw/policies_test.go +++ b/server/core/config/raw/policies_test.go @@ -180,6 +180,9 @@ func TestPolicySets_ToValid(t *testing.T) { Users: []string{ "test", }, + Teams: []string{ + "testteam", + }, }, PolicySets: []raw.PolicySet{ { @@ -199,6 +202,7 @@ func TestPolicySets_ToValid(t *testing.T) { Version: version, Owners: valid.PolicyOwners{ Users: []string{"test"}, + Teams: []string{"testteam"}, }, PolicySets: []valid.PolicySet{ { diff --git a/server/core/config/valid/policies.go b/server/core/config/valid/policies.go index 7eafd6185..48d166cea 100644 --- a/server/core/config/valid/policies.go +++ b/server/core/config/valid/policies.go @@ -22,6 +22,7 @@ type PolicySets struct { type PolicyOwners struct { Users []string + Teams []string } type PolicySet struct { @@ -35,12 +36,24 @@ func (p *PolicySets) HasPolicies() bool { return len(p.PolicySets) > 0 } -func (p *PolicySets) IsOwner(username string) bool { +func (p *PolicySets) HasTeamOwners() bool { + return len(p.Owners.Teams) > 0 +} + +func (p *PolicySets) IsOwner(username string, userTeams []string) bool { for _, uname := range p.Owners.Users { if strings.EqualFold(uname, username) { return true } } + for _, orgTeamName := range p.Owners.Teams { + for _, userTeamName := range userTeams { + if strings.EqualFold(orgTeamName, userTeamName) { + return true + } + } + } + return false } diff --git a/server/events/approve_policies_command_runner.go b/server/events/approve_policies_command_runner.go index 7e9a56e1a..1d1056681 100644 --- a/server/events/approve_policies_command_runner.go +++ b/server/events/approve_policies_command_runner.go @@ -5,6 +5,7 @@ import ( "github.com/runatlantis/atlantis/server/events/command" "github.com/runatlantis/atlantis/server/events/models" + "github.com/runatlantis/atlantis/server/events/vcs" ) func NewApprovePoliciesCommandRunner( @@ -15,6 +16,7 @@ func NewApprovePoliciesCommandRunner( dbUpdater *DBUpdater, SilenceNoProjects bool, silenceVCSStatusNoProjects bool, + vcsClient vcs.Client, ) *ApprovePoliciesCommandRunner { return &ApprovePoliciesCommandRunner{ commitStatusUpdater: commitStatusUpdater, @@ -24,6 +26,7 @@ func NewApprovePoliciesCommandRunner( dbUpdater: dbUpdater, SilenceNoProjects: SilenceNoProjects, silenceVCSStatusNoProjects: silenceVCSStatusNoProjects, + vcsClient: vcsClient, } } @@ -37,6 +40,7 @@ type ApprovePoliciesCommandRunner struct { // are found SilenceNoProjects bool silenceVCSStatusNoProjects bool + vcsClient vcs.Client } func (a *ApprovePoliciesCommandRunner) Run(ctx *command.Context, cmd *CommentCommand) { @@ -91,9 +95,23 @@ func (a *ApprovePoliciesCommandRunner) buildApprovePolicyCommandResults(ctx *com // Check if vcs user is in the owner list of the PolicySets. All projects // share the same Owners list at this time so no reason to iterate over each // project. - if len(prjCmds) > 0 && !prjCmds[0].PolicySets.IsOwner(ctx.User.Username) { - result.Error = fmt.Errorf("contact policy owners to approve failing policies") - return + if len(prjCmds) > 0 { + teams := []string{} + + // Only query the users team membership if any teams have been configured as owners. + if prjCmds[0].PolicySets.HasTeamOwners() { + userTeams, err := a.vcsClient.GetTeamNamesForUser(ctx.Pull.BaseRepo, ctx.User) + if err != nil { + ctx.Log.Err("unable to get team membership for user: %s", err) + return + } + teams = append(teams, userTeams...) + } + + if !prjCmds[0].PolicySets.IsOwner(ctx.User.Username, teams) { + result.Error = fmt.Errorf("contact policy owners to approve failing policies") + return + } } var prjResults []command.ProjectResult diff --git a/server/events/approve_policies_command_runner_test.go b/server/events/approve_policies_command_runner_test.go index 8f7fa06dd..2fc526892 100644 --- a/server/events/approve_policies_command_runner_test.go +++ b/server/events/approve_policies_command_runner_test.go @@ -20,16 +20,41 @@ func TestApproveCommandRunner_IsOwner(t *testing.T) { cases := []struct { Description string OwnerUsers []string + OwnerTeams []string // Teams configured as owners + UserTeams []string // Teams the user is a member of ExpComment string }{ { Description: "When user is not an owner, approval fails", OwnerUsers: []string{}, + OwnerTeams: []string{}, ExpComment: "**Approve Policies Error**\n```\ncontact policy owners to approve failing policies\n```\n", }, { Description: "When user is an owner, approval succeeds", OwnerUsers: []string{fixtures.User.Username}, + OwnerTeams: []string{}, + ExpComment: "Approved Policies for 1 projects:\n\n1. dir: `` workspace: ``\n\n\n", + }, + { + Description: "When user is an owner via team membership, approval succeeds", + OwnerUsers: []string{}, + OwnerTeams: []string{"SomeTeam"}, + UserTeams: []string{"SomeTeam"}, + ExpComment: "Approved Policies for 1 projects:\n\n1. dir: `` workspace: ``\n\n\n", + }, + { + Description: "When user belongs to a team not configured as a owner, approval fails", + OwnerUsers: []string{}, + OwnerTeams: []string{"SomeTeam"}, + UserTeams: []string{"SomeOtherTeam}"}, + ExpComment: "**Approve Policies Error**\n```\ncontact policy owners to approve failing policies\n```\n", + }, + { + Description: "When user is an owner but not a team member, approval succeeds", + OwnerUsers: []string{fixtures.User.Username}, + OwnerTeams: []string{"SomeTeam"}, + UserTeams: []string{"SomeOtherTeam"}, ExpComment: "Approved Policies for 1 projects:\n\n1. dir: `` workspace: ``\n\n\n", }, } @@ -57,10 +82,12 @@ func TestApproveCommandRunner_IsOwner(t *testing.T) { PolicySets: valid.PolicySets{ Owners: valid.PolicyOwners{ Users: c.OwnerUsers, + Teams: c.OwnerTeams, }, }, }, }, nil) + When(vcsClient.GetTeamNamesForUser(fixtures.GithubRepo, fixtures.User)).ThenReturn(c.UserTeams, nil) approvePoliciesCommandRunner.Run(ctx, &events.CommentCommand{Name: command.ApprovePolicies}) diff --git a/server/events/command_runner_test.go b/server/events/command_runner_test.go index c2c830c6a..62019b2bc 100644 --- a/server/events/command_runner_test.go +++ b/server/events/command_runner_test.go @@ -182,6 +182,7 @@ func setup(t *testing.T, options ...func(testConfig *TestConfig)) *vcsmocks.Mock dbUpdater, testConfig.SilenceNoProjects, false, + vcsClient, ) unlockCommandRunner = events.NewUnlockCommandRunner( diff --git a/server/server.go b/server/server.go index 5b6158e73..0a87d3807 100644 --- a/server/server.go +++ b/server/server.go @@ -714,6 +714,7 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { dbUpdater, userConfig.SilenceNoProjects, userConfig.SilenceVCSStatusNoPlans, + vcsClient, ) unlockCommandRunner := events.NewUnlockCommandRunner(