Skip to content
This repository has been archived by the owner on Jan 11, 2022. It is now read-only.

Commit

Permalink
Merge pull request #1 from sonatype/multienv_ghteams
Browse files Browse the repository at this point in the history
Multienv and ghteams
  • Loading branch information
jamengual authored Sep 22, 2020
2 parents 6e0f265 + 48fbde5 commit 09dd404
Show file tree
Hide file tree
Showing 18 changed files with 200 additions and 4 deletions.
17 changes: 17 additions & 0 deletions cmd/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ const (
DisableApplyAllFlag = "disable-apply-all"
DisableMarkdownFoldingFlag = "disable-markdown-folding"
GHHostnameFlag = "gh-hostname"
GHTeamWhitelistFlag = "gh-team-whitelist"
GHTokenFlag = "gh-token"
GHUserFlag = "gh-user"
GHWebhookSecretFlag = "gh-webhook-secret" // nolint: gosec
Expand Down Expand Up @@ -90,6 +91,7 @@ const (
DefaultBitbucketBaseURL = bitbucketcloud.BaseURL
DefaultDataDir = "~/.atlantis"
DefaultGHHostname = "github.com"
DefaultGHTeamWhitelist = "*:*"
DefaultGitlabHostname = "gitlab.com"
DefaultLogLevel = "info"
DefaultPort = 4141
Expand Down Expand Up @@ -158,6 +160,18 @@ var stringFlags = map[string]stringFlag{
description: "Hostname of your Github Enterprise installation. If using github.com, no need to set.",
defaultValue: DefaultGHHostname,
},
GHTeamWhitelistFlag: {
description: "Comma separated list of key-value pairs representing the GitHub teams and the operations that " +
"the members of a particular team are allowed to perform. " +
"The format is {team}:{command},{team}:{command}. " +
"Valid values for 'command' are 'plan', 'apply' and '*', e.g. 'dev:plan,ops:apply,devops:*'" +
"This example gives the users from the 'dev' GitHub team the permissions to execute the 'plan' command, " +
"the 'ops' team the permissions to execute the 'apply' command, " +
"and allows the 'devops' team 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 team to perform any operation.",
defaultValue: DefaultGHTeamWhitelist,
},
GHUserFlag: {
description: "GitHub username of API user.",
},
Expand Down Expand Up @@ -502,6 +516,9 @@ func (s *ServerCmd) setDefaults(c *server.UserConfig) {
if c.VCSStatusName == "" {
c.VCSStatusName = DefaultVCSStatusName
}
if c.GithubTeamWhitelist == "" {
c.GithubTeamWhitelist = DefaultGHTeamWhitelist
}
if c.TFEHostname == "" {
c.TFEHostname = DefaultTFEHostname
}
Expand Down
1 change: 0 additions & 1 deletion e2e/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import (

"fmt"

"github.com/google/go-github/v28/github"
multierror "github.com/hashicorp/go-multierror"
)

Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ require (
github.com/go-playground/locales v0.12.1 // indirect
github.com/go-playground/universal-translator v0.16.0 // indirect
github.com/go-test/deep v1.0.3
github.com/google/go-github/v28 v28.1.1
github.com/google/go-github/v31 v31.0.0
github.com/google/uuid v0.0.0-20161128191214-064e2069ce9c // indirect
github.com/gorilla/context v0.0.0-20160226214623-1ea25387ff6f // indirect
Expand Down
3 changes: 3 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,11 @@ github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5a
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY=
github.com/google/go-github/v28 v28.0.0 h1:+UjHI4+1W/vsXR4jJBWt0ZA74XHbvt5yBAvsf1M3bgM=
github.com/google/go-github/v28 v28.0.0/go.mod h1:+5GboIspo7F0NG2qsvfYh7en6F3EK37uyqv+c35AR3s=
github.com/google/go-github/v28 v28.1.1 h1:kORf5ekX5qwXO2mGzXXOjMe/g6ap8ahVe0sBEulhSxo=
github.com/google/go-github/v28 v28.1.1/go.mod h1:bsqJWQX05omyWVmc00nEUql9mhQyv38lDZ8kPZcQVoM=
github.com/google/go-github/v31 v31.0.0/go.mod h1:NQPZol8/1sMoWYGN2yaALIBytu17gAWfhbweiEed3pM=
github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
Expand Down
3 changes: 2 additions & 1 deletion main.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,10 @@ import (
"github.com/spf13/viper"
)

const atlantisVersion = "0.13.0"
const atlantisVersion = "0.12.0-multienv-ght"

func main() {

v := viper.New()

// We're creating commands manually here rather than using init() functions
Expand Down
70 changes: 70 additions & 0 deletions server/events/team_whitelist_checker.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
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

// TeamWhitelistChecker implements checking the teams and the operations that the members
// of a particular team are allowed to perform
type TeamWhitelistChecker struct {
rules []mapOfStrings
}

// NewTeamWhitelistChecker constructs a new checker
func NewTeamWhitelistChecker(whitelist string) (*TeamWhitelistChecker, error) {
var rules []mapOfStrings
pairs := strings.Split(whitelist, ",")
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 &TeamWhitelistChecker{
rules: rules,
}, nil
}

// IsCommandAllowedForTeam returns true if the team is allowed to execute the command
// and false otherwise.
func (checker *TeamWhitelistChecker) IsCommandAllowedForTeam(team string, command string) bool {
t := strings.TrimSpace(team)
c := strings.TrimSpace(command)
for _, rule := range checker.rules {
for key, value := range rule {
if (key == wildcard || strings.EqualFold(key, t)) && (value == wildcard || strings.EqualFold(value, c)) {
return true
}
}
}
return false
}

// IsCommandAllowedForAnyTeam returns true if any of the teams is allowed to execute the command
// and false otherwise.
func (checker *TeamWhitelistChecker) IsCommandAllowedForAnyTeam(teams []string, command string) bool {
c := strings.TrimSpace(command)
if teams == nil || len(teams) == 0 {
for _, rule := range checker.rules {
for key, value := range rule {
if (key == wildcard) && (value == wildcard || strings.EqualFold(value, c)) {
return true
}
}
}
} else {
for _, t := range teams {
if checker.IsCommandAllowedForTeam(t, command) {
return true
}
}
}
return false
}
4 changes: 4 additions & 0 deletions server/events/vcs/azuredevops_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -364,3 +364,7 @@ func SplitAzureDevopsRepoFullName(repoFullName string) (owner string, project st
}
return repoFullName[:lastSlashIdx], "", repoFullName[lastSlashIdx+1:]
}
// 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) {
return nil, nil
}
5 changes: 5 additions & 0 deletions server/events/vcs/bitbucketcloud/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -243,3 +243,8 @@ func (b *Client) makeRequest(method string, path string, reqBody io.Reader) ([]b
}
return respBody, nil
}

// GetTeamNamesForUser returns the names of the teams or groups that the user belongs to (in the organization the repository belongs to).
func (g *Client) GetTeamNamesForUser(repo models.Repo, user models.User) ([]string, error) {
return nil, nil
}
5 changes: 5 additions & 0 deletions server/events/vcs/bitbucketserver/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -311,3 +311,8 @@ func (b *Client) makeRequest(method string, path string, reqBody io.Reader) ([]b
}
return respBody, nil
}

// GetTeamNamesForUser returns the names of the teams or groups that the user belongs to (in the organization the repository belongs to).
func (g *Client) GetTeamNamesForUser(repo models.Repo, user models.User) ([]string, error) {
return nil, nil
}
1 change: 1 addition & 0 deletions server/events/vcs/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,5 @@ type Client interface {
UpdateStatus(repo models.Repo, pull models.PullRequest, state models.CommitStatus, src string, description string, url string) error
MergePull(pull models.PullRequest) error
MarkdownPullLink(pull models.PullRequest) (string, error)
GetTeamNamesForUser(repo models.Repo, user models.User) ([]string, error)
}
27 changes: 27 additions & 0 deletions server/events/vcs/github_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -335,3 +335,30 @@ func (g *GithubClient) MergePull(pull models.PullRequest) error {
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, err
}
for _, t := range teams {
membership, _, err := g.client.Teams.GetTeamMembership(g.ctx, t.GetID(), user.Username)
if err == nil && 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
}
5 changes: 5 additions & 0 deletions server/events/vcs/gitlab_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -263,3 +263,8 @@ 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(repo models.Repo, user models.User) ([]string, error) {
return nil, nil
}
3 changes: 3 additions & 0 deletions server/events/vcs/not_configured_vcs_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,6 @@ func (a *NotConfiguredVCSClient) MarkdownPullLink(pull models.PullRequest) (stri
func (a *NotConfiguredVCSClient) err() error {
return fmt.Errorf("atlantis was not configured to support repos from %s", a.Host.String())
}
func (a *NotConfiguredVCSClient) GetTeamNamesForUser(repo models.Repo, user models.User) ([]string, error) {
return nil, a.err()
}
4 changes: 4 additions & 0 deletions server/events/vcs/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,7 @@ func (d *ClientProxy) MergePull(pull models.PullRequest) error {
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)
}
12 changes: 10 additions & 2 deletions server/events/yaml/parser_validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,16 @@ import (
)

// AtlantisYAMLFilename is the name of the config file for each repo.
const AtlantisYAMLFilename = "atlantis.yaml"
var AtlantisYAMLFilename string

// Simplest hack to allow overriding "atlantis.yaml" to another name
func init() {
AtlantisYAMLFilename = os.Getenv("ATLANTIS_YAML_FILENAME")
if AtlantisYAMLFilename == "" {
AtlantisYAMLFilename = "atlantis.yaml"
}
}


// ParserValidator parses and validates server-side repo config files and
// repo-level atlantis.yaml files.
Expand Down Expand Up @@ -82,7 +91,6 @@ func (p *ParserValidator) ParseRepoCfg(absRepoDir string, globalCfg valid.Global
return validConfig, err
}
}

err = globalCfg.ValidateRepoCfg(validConfig, repoID)
return validConfig, err
}
Expand Down
37 changes: 37 additions & 0 deletions server/events_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ type EventsController struct {
// request validation is done.
GitlabWebhookSecret []byte
RepoWhitelistChecker *events.RepoWhitelistChecker
TeamWhitelistChecker *events.TeamWhitelistChecker
// SilenceWhitelistErrors controls whether we write an error comment on
// pull requests from non-whitelisted repos.
SilenceWhitelistErrors bool
Expand Down Expand Up @@ -435,6 +436,18 @@ func (e *EventsController) handleCommentEvent(w http.ResponseWriter, baseRepo mo
return
}

// Check if the user who commented has the permissions to execute the 'plan' or 'apply' commands
ok, err := e.checkUserPermissions(baseRepo, user, parseResult.Command)
if err != nil {
e.Logger.Err("unable to comment on pull request: %s", err)
return
}
if !ok {
e.commentUserDoesNotHavePermissions(baseRepo, pullNum, user, parseResult.Command)
e.respond(w, logging.Warn, http.StatusForbidden, "User @%s does not have permissions to execute '%s' command", user.Username, parseResult.Command.Name.String())
return
}

e.Logger.Debug("executing command")
fmt.Fprintln(w, "Processing...")
if !e.TestingMode {
Expand Down Expand Up @@ -552,3 +565,27 @@ func (e *EventsController) commentNotWhitelisted(baseRepo models.Repo, pullNum i
e.Logger.Err("unable to comment on pull request: %s", err)
}
}

// commentUserDoesNotHavePermissions comments on the pull request that the user
// is not allowed to execute the command.
func (e *EventsController) commentUserDoesNotHavePermissions(baseRepo models.Repo, pullNum int, user models.User, cmd *events.CommentCommand) {
errMsg := fmt.Sprintf("```\nError: User @%s does not have permissions to execute '%s' command.\n```", user.Username, cmd.Name)
if err := e.VCSClient.CreateComment(baseRepo, pullNum, errMsg); err != nil {
e.Logger.Err("unable to comment on pull request: %s", err)
}
}

// checkUserPermissions checks if the user has permissions to execute the command
func (e *EventsController) checkUserPermissions(repo models.Repo, user models.User, cmd *events.CommentCommand) (bool, error) {
if cmd.Name == models.ApplyCommand || cmd.Name == models.PlanCommand {
teams, err := e.VCSClient.GetTeamNamesForUser(repo, user)
if err != nil {
return false, err
}
ok := e.TeamWhitelistChecker.IsCommandAllowedForAnyTeam(teams, cmd.Name.String())
if !ok {
return false, nil
}
}
return true, nil
}
5 changes: 5 additions & 0 deletions server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,10 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
if err != nil {
return nil, err
}
githubTeamWhitelistChecker, err := events.NewTeamWhitelistChecker(userConfig.GithubTeamWhitelist)
if err != nil {
return nil, err
}
locksController := &LocksController{
AtlantisVersion: config.AtlantisVersion,
AtlantisURL: parsedURL,
Expand All @@ -397,6 +401,7 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
Logger: logger,
GithubWebhookSecret: []byte(userConfig.GithubWebhookSecret),
GithubRequestValidator: &DefaultGithubRequestValidator{},
TeamWhitelistChecker: githubTeamWhitelistChecker,
GitlabRequestParserValidator: &DefaultGitlabRequestParserValidator{},
GitlabWebhookSecret: []byte(userConfig.GitlabWebhookSecret),
RepoWhitelistChecker: repoWhitelist,
Expand Down
1 change: 1 addition & 0 deletions server/user_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ type UserConfig struct {
GithubUser string `mapstructure:"gh-user"`
GithubWebhookSecret string `mapstructure:"gh-webhook-secret"`
GitlabHostname string `mapstructure:"gitlab-hostname"`
GithubTeamWhitelist string `mapstructure:"gh-team-whitelist"`
GitlabToken string `mapstructure:"gitlab-token"`
GitlabUser string `mapstructure:"gitlab-user"`
GitlabWebhookSecret string `mapstructure:"gitlab-webhook-secret"`
Expand Down

0 comments on commit 09dd404

Please sign in to comment.