Skip to content

Commit 7746655

Browse files
authored
Support team owners for policies (#2953)
1 parent 87f9f9a commit 7746655

11 files changed

+80
-5
lines changed

runatlantis.io/docs/access-credentials.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ GitHub App needs these permissions. These are automatically set when a GitHub ap
5757

5858
::: tip NOTE
5959
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.
60+
61+
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.
6062
:::
6163

6264
| Type | Access |
@@ -69,6 +71,7 @@ Since v0.19.7, a new permission for `Administration` has been added. If you have
6971
| Metadata | Read-only (default) |
7072
| Pull requests | Read and write |
7173
| Webhooks | Read and write |
74+
| Members | Read-only |
7275

7376
### GitLab
7477
- 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)

runatlantis.io/docs/server-side-repo-config.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -521,7 +521,8 @@ If you set a workflow with the key `default`, it will override this.
521521
### Owners
522522
| Key | Type | Default | Required | Description |
523523
|-------------|-------------------|---------|------------|---------------------------------------------------------|
524-
| users | []string | none | yes | list of github users that can approve failing policies |
524+
| users | []string | none | no | list of github users that can approve failing policies |
525+
| teams | []string | none | no | list of github teams that can approve failing policies |
525526

526527
### PolicySet
527528

server/controllers/events/events_controller_e2e_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1222,6 +1222,7 @@ func setupE2E(t *testing.T, repoDir string, opt setupOption) (events_controllers
12221222
dbUpdater,
12231223
silenceNoProjects,
12241224
false,
1225+
e2eVCSClient,
12251226
)
12261227

12271228
unlockCommandRunner := events.NewUnlockCommandRunner(

server/controllers/github_app_controller.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ func (g *GithubAppController) New(w http.ResponseWriter, r *http.Request) {
121121
"repository_hooks": "write",
122122
"statuses": "write",
123123
"administration": "read",
124+
"members": "read",
124125
},
125126
}
126127

server/core/config/raw/policies.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ func (p PolicySets) ToValid() valid.PolicySets {
4040

4141
type PolicyOwners struct {
4242
Users []string `yaml:"users,omitempty" json:"users,omitempty"`
43+
Teams []string `yaml:"teams,omitempty" json:"teams,omitempty"`
4344
}
4445

4546
func (o PolicyOwners) ToValid() valid.PolicyOwners {
@@ -48,6 +49,10 @@ func (o PolicyOwners) ToValid() valid.PolicyOwners {
4849
if len(o.Users) > 0 {
4950
policyOwners.Users = o.Users
5051
}
52+
53+
if len(o.Teams) > 0 {
54+
policyOwners.Teams = o.Teams
55+
}
5156
return policyOwners
5257
}
5358

server/core/config/raw/policies_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,9 @@ func TestPolicySets_ToValid(t *testing.T) {
180180
Users: []string{
181181
"test",
182182
},
183+
Teams: []string{
184+
"testteam",
185+
},
183186
},
184187
PolicySets: []raw.PolicySet{
185188
{
@@ -199,6 +202,7 @@ func TestPolicySets_ToValid(t *testing.T) {
199202
Version: version,
200203
Owners: valid.PolicyOwners{
201204
Users: []string{"test"},
205+
Teams: []string{"testteam"},
202206
},
203207
PolicySets: []valid.PolicySet{
204208
{

server/core/config/valid/policies.go

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ type PolicySets struct {
2222

2323
type PolicyOwners struct {
2424
Users []string
25+
Teams []string
2526
}
2627

2728
type PolicySet struct {
@@ -35,12 +36,24 @@ func (p *PolicySets) HasPolicies() bool {
3536
return len(p.PolicySets) > 0
3637
}
3738

38-
func (p *PolicySets) IsOwner(username string) bool {
39+
func (p *PolicySets) HasTeamOwners() bool {
40+
return len(p.Owners.Teams) > 0
41+
}
42+
43+
func (p *PolicySets) IsOwner(username string, userTeams []string) bool {
3944
for _, uname := range p.Owners.Users {
4045
if strings.EqualFold(uname, username) {
4146
return true
4247
}
4348
}
4449

50+
for _, orgTeamName := range p.Owners.Teams {
51+
for _, userTeamName := range userTeams {
52+
if strings.EqualFold(orgTeamName, userTeamName) {
53+
return true
54+
}
55+
}
56+
}
57+
4558
return false
4659
}

server/events/approve_policies_command_runner.go

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55

66
"github.com/runatlantis/atlantis/server/events/command"
77
"github.com/runatlantis/atlantis/server/events/models"
8+
"github.com/runatlantis/atlantis/server/events/vcs"
89
)
910

1011
func NewApprovePoliciesCommandRunner(
@@ -15,6 +16,7 @@ func NewApprovePoliciesCommandRunner(
1516
dbUpdater *DBUpdater,
1617
SilenceNoProjects bool,
1718
silenceVCSStatusNoProjects bool,
19+
vcsClient vcs.Client,
1820
) *ApprovePoliciesCommandRunner {
1921
return &ApprovePoliciesCommandRunner{
2022
commitStatusUpdater: commitStatusUpdater,
@@ -24,6 +26,7 @@ func NewApprovePoliciesCommandRunner(
2426
dbUpdater: dbUpdater,
2527
SilenceNoProjects: SilenceNoProjects,
2628
silenceVCSStatusNoProjects: silenceVCSStatusNoProjects,
29+
vcsClient: vcsClient,
2730
}
2831
}
2932

@@ -37,6 +40,7 @@ type ApprovePoliciesCommandRunner struct {
3740
// are found
3841
SilenceNoProjects bool
3942
silenceVCSStatusNoProjects bool
43+
vcsClient vcs.Client
4044
}
4145

4246
func (a *ApprovePoliciesCommandRunner) Run(ctx *command.Context, cmd *CommentCommand) {
@@ -91,9 +95,23 @@ func (a *ApprovePoliciesCommandRunner) buildApprovePolicyCommandResults(ctx *com
9195
// Check if vcs user is in the owner list of the PolicySets. All projects
9296
// share the same Owners list at this time so no reason to iterate over each
9397
// project.
94-
if len(prjCmds) > 0 && !prjCmds[0].PolicySets.IsOwner(ctx.User.Username) {
95-
result.Error = fmt.Errorf("contact policy owners to approve failing policies")
96-
return
98+
if len(prjCmds) > 0 {
99+
teams := []string{}
100+
101+
// Only query the users team membership if any teams have been configured as owners.
102+
if prjCmds[0].PolicySets.HasTeamOwners() {
103+
userTeams, err := a.vcsClient.GetTeamNamesForUser(ctx.Pull.BaseRepo, ctx.User)
104+
if err != nil {
105+
ctx.Log.Err("unable to get team membership for user: %s", err)
106+
return
107+
}
108+
teams = append(teams, userTeams...)
109+
}
110+
111+
if !prjCmds[0].PolicySets.IsOwner(ctx.User.Username, teams) {
112+
result.Error = fmt.Errorf("contact policy owners to approve failing policies")
113+
return
114+
}
97115
}
98116

99117
var prjResults []command.ProjectResult

server/events/approve_policies_command_runner_test.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,41 @@ func TestApproveCommandRunner_IsOwner(t *testing.T) {
2020
cases := []struct {
2121
Description string
2222
OwnerUsers []string
23+
OwnerTeams []string // Teams configured as owners
24+
UserTeams []string // Teams the user is a member of
2325
ExpComment string
2426
}{
2527
{
2628
Description: "When user is not an owner, approval fails",
2729
OwnerUsers: []string{},
30+
OwnerTeams: []string{},
2831
ExpComment: "**Approve Policies Error**\n```\ncontact policy owners to approve failing policies\n```\n",
2932
},
3033
{
3134
Description: "When user is an owner, approval succeeds",
3235
OwnerUsers: []string{fixtures.User.Username},
36+
OwnerTeams: []string{},
37+
ExpComment: "Approved Policies for 1 projects:\n\n1. dir: `` workspace: ``\n\n\n",
38+
},
39+
{
40+
Description: "When user is an owner via team membership, approval succeeds",
41+
OwnerUsers: []string{},
42+
OwnerTeams: []string{"SomeTeam"},
43+
UserTeams: []string{"SomeTeam"},
44+
ExpComment: "Approved Policies for 1 projects:\n\n1. dir: `` workspace: ``\n\n\n",
45+
},
46+
{
47+
Description: "When user belongs to a team not configured as a owner, approval fails",
48+
OwnerUsers: []string{},
49+
OwnerTeams: []string{"SomeTeam"},
50+
UserTeams: []string{"SomeOtherTeam}"},
51+
ExpComment: "**Approve Policies Error**\n```\ncontact policy owners to approve failing policies\n```\n",
52+
},
53+
{
54+
Description: "When user is an owner but not a team member, approval succeeds",
55+
OwnerUsers: []string{fixtures.User.Username},
56+
OwnerTeams: []string{"SomeTeam"},
57+
UserTeams: []string{"SomeOtherTeam"},
3358
ExpComment: "Approved Policies for 1 projects:\n\n1. dir: `` workspace: ``\n\n\n",
3459
},
3560
}
@@ -57,10 +82,12 @@ func TestApproveCommandRunner_IsOwner(t *testing.T) {
5782
PolicySets: valid.PolicySets{
5883
Owners: valid.PolicyOwners{
5984
Users: c.OwnerUsers,
85+
Teams: c.OwnerTeams,
6086
},
6187
},
6288
},
6389
}, nil)
90+
When(vcsClient.GetTeamNamesForUser(fixtures.GithubRepo, fixtures.User)).ThenReturn(c.UserTeams, nil)
6491

6592
approvePoliciesCommandRunner.Run(ctx, &events.CommentCommand{Name: command.ApprovePolicies})
6693

server/events/command_runner_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ func setup(t *testing.T, options ...func(testConfig *TestConfig)) *vcsmocks.Mock
182182
dbUpdater,
183183
testConfig.SilenceNoProjects,
184184
false,
185+
vcsClient,
185186
)
186187

187188
unlockCommandRunner = events.NewUnlockCommandRunner(

0 commit comments

Comments
 (0)