-
Notifications
You must be signed in to change notification settings - Fork 1.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: add GitHub team allowlist configuration option #1694
Changes from all commits
dbbf507
f22d951
fbcd74f
23c5cd9
ac049ea
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
package events | ||
|
||
import ( | ||
"strings" | ||
) | ||
|
||
// Wildcard matches all teams and all commands | ||
const wildcard = "*" | ||
|
||
// mapOfStrings is an alias for map[string]string | ||
type mapOfStrings map[string]string | ||
|
||
// TeamAllowlistChecker implements checking the teams and the operations that the members | ||
// of a particular team are allowed to perform | ||
type TeamAllowlistChecker struct { | ||
rules []mapOfStrings | ||
} | ||
|
||
// NewTeamAllowlistChecker constructs a new checker | ||
func NewTeamAllowlistChecker(allowlist string) (*TeamAllowlistChecker, error) { | ||
var rules []mapOfStrings | ||
pairs := strings.Split(allowlist, ",") | ||
if pairs[0] != "" { | ||
for _, pair := range pairs { | ||
values := strings.Split(pair, ":") | ||
team := strings.TrimSpace(values[0]) | ||
command := strings.TrimSpace(values[1]) | ||
m := mapOfStrings{team: command} | ||
rules = append(rules, m) | ||
} | ||
} | ||
return &TeamAllowlistChecker{ | ||
rules: rules, | ||
}, nil | ||
} | ||
|
||
// IsCommandAllowedForTeam returns true if the team is allowed to execute the command | ||
// and false otherwise. | ||
func (checker *TeamAllowlistChecker) IsCommandAllowedForTeam(team string, command string) bool { | ||
for _, rule := range checker.rules { | ||
for key, value := range rule { | ||
if (key == wildcard || strings.EqualFold(key, team)) && (value == wildcard || strings.EqualFold(value, command)) { | ||
return true | ||
} | ||
} | ||
} | ||
return false | ||
} | ||
|
||
// IsCommandAllowedForAnyTeam returns true if any of the teams is allowed to execute the command | ||
// and false otherwise. | ||
func (checker *TeamAllowlistChecker) IsCommandAllowedForAnyTeam(teams []string, command string) bool { | ||
if len(teams) == 0 { | ||
for _, rule := range checker.rules { | ||
for key, value := range rule { | ||
if (key == wildcard) && (value == wildcard || strings.EqualFold(value, command)) { | ||
return true | ||
} | ||
} | ||
} | ||
} else { | ||
for _, t := range teams { | ||
if checker.IsCommandAllowedForTeam(t, command) { | ||
return true | ||
} | ||
} | ||
} | ||
return false | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
package events_test | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/runatlantis/atlantis/server/events" | ||
. "github.com/runatlantis/atlantis/testing" | ||
) | ||
|
||
func TestNewTeamAllowListChecker(t *testing.T) { | ||
allowlist := `bob:plan, dave:apply` | ||
_, err := events.NewTeamAllowlistChecker(allowlist) | ||
Ok(t, err) | ||
} | ||
|
||
func TestIsCommandAllowedForTeam(t *testing.T) { | ||
allowlist := `bob:plan, dave:apply, connie:plan, connie:apply` | ||
checker, err := events.NewTeamAllowlistChecker(allowlist) | ||
Ok(t, err) | ||
Equals(t, true, checker.IsCommandAllowedForTeam("connie", "plan")) | ||
Equals(t, true, checker.IsCommandAllowedForTeam("connie", "apply")) | ||
Equals(t, true, checker.IsCommandAllowedForTeam("dave", "apply")) | ||
Equals(t, true, checker.IsCommandAllowedForTeam("bob", "plan")) | ||
Equals(t, false, checker.IsCommandAllowedForTeam("bob", "apply")) | ||
} | ||
|
||
func TestIsCommandAllowedForAnyTeam(t *testing.T) { | ||
allowlist := `alpha:plan,beta:release,*:unlock,nobody:*` | ||
teams := []string{`alpha`, `beta`} | ||
checker, err := events.NewTeamAllowlistChecker(allowlist) | ||
Ok(t, err) | ||
Equals(t, true, checker.IsCommandAllowedForAnyTeam(teams, `plan`)) | ||
Equals(t, true, checker.IsCommandAllowedForAnyTeam(teams, `release`)) | ||
Equals(t, true, checker.IsCommandAllowedForAnyTeam(teams, `unlock`)) | ||
Equals(t, false, checker.IsCommandAllowedForAnyTeam(teams, `noop`)) | ||
} | ||
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -454,6 +454,35 @@ func (g *GithubClient) MarkdownPullLink(pull models.PullRequest) (string, error) | |
return fmt.Sprintf("#%d", pull.Num), nil | ||
} | ||
|
||
// GetTeamNamesForUser returns the names of the teams or groups that the user belongs to (in the organization the repository belongs to). | ||
// https://developer.github.com/v3/teams/members/#get-team-membership | ||
func (g *GithubClient) GetTeamNamesForUser(repo models.Repo, user models.User) ([]string, error) { | ||
var teamNames []string | ||
opts := &github.ListOptions{} | ||
org := repo.Owner | ||
for { | ||
teams, resp, err := g.client.Teams.ListTeams(g.ctx, org, opts) | ||
if err != nil { | ||
return nil, errors.Wrap(err, "retrieving GitHub teams") | ||
} | ||
for _, t := range teams { | ||
membership, _, err := g.client.Teams.GetTeamMembershipBySlug(g.ctx, org, *t.Slug, user.Username) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What about the rate limiting? Wound't it be better to use the GraphQL so we could just pull the orgs teams of a user? https://docs.github.com/en/rest/overview/resources-in-the-rest-api#rate-limiting There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 I haven't worked with the GraphQL API, but it does seem like some calls could be saved that way There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @paulerickson Thanks for the answer I could try to switch this feature to use GraphQL :) |
||
if err != nil { | ||
g.logger.Err("Failed to get team membership from GitHub: %s", err) | ||
} else if membership != nil { | ||
if *membership.State == "active" && (*membership.Role == "member" || *membership.Role == "maintainer") { | ||
teamNames = append(teamNames, t.GetName()) | ||
} | ||
} | ||
} | ||
if resp.NextPage == 0 { | ||
break | ||
} | ||
opts.Page = resp.NextPage | ||
} | ||
return teamNames, nil | ||
} | ||
|
||
// ExchangeCode returns a newly created app's info | ||
func (g *GithubClient) ExchangeCode(code string) (*GithubAppTemporarySecrets, error) { | ||
ctx := context.Background() | ||
|
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's add some tests for wildcard as well.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added in 23c5cd9