Support team owners for policies (#2953)

This commit is contained in:
Darren Worrall
2023-01-10 22:53:05 +00:00
committed by GitHub
parent 87f9f9a6f4
commit 7746655eb1
11 changed files with 80 additions and 5 deletions

View File

@@ -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)

View File

@@ -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

View File

@@ -1222,6 +1222,7 @@ func setupE2E(t *testing.T, repoDir string, opt setupOption) (events_controllers
dbUpdater,
silenceNoProjects,
false,
e2eVCSClient,
)
unlockCommandRunner := events.NewUnlockCommandRunner(

View File

@@ -121,6 +121,7 @@ func (g *GithubAppController) New(w http.ResponseWriter, r *http.Request) {
"repository_hooks": "write",
"statuses": "write",
"administration": "read",
"members": "read",
},
}

View File

@@ -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
}

View File

@@ -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{
{

View File

@@ -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
}

View File

@@ -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

View File

@@ -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})

View File

@@ -182,6 +182,7 @@ func setup(t *testing.T, options ...func(testConfig *TestConfig)) *vcsmocks.Mock
dbUpdater,
testConfig.SilenceNoProjects,
false,
vcsClient,
)
unlockCommandRunner = events.NewUnlockCommandRunner(

View File

@@ -714,6 +714,7 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
dbUpdater,
userConfig.SilenceNoProjects,
userConfig.SilenceVCSStatusNoPlans,
vcsClient,
)
unlockCommandRunner := events.NewUnlockCommandRunner(