From cbb86c248e7773ca423c295189aa61dd0956335d Mon Sep 17 00:00:00 2001 From: Luke Massa Date: Mon, 31 Jul 2023 09:46:11 -0400 Subject: [PATCH] feat: Allow for negations in repo allowlist (#3414) * feat: Omit repos from allowlist * Add quote in comment * Better comment * Remove test --- runatlantis.io/docs/security.md | 1 + runatlantis.io/docs/server-configuration.md | 3 +++ server/events/repo_allowlist_checker.go | 24 ++++++++++++++++---- server/events/repo_allowlist_checker_test.go | 14 ++++++++++++ 4 files changed, 37 insertions(+), 5 deletions(-) diff --git a/runatlantis.io/docs/security.md b/runatlantis.io/docs/security.md index 3c4bed54a1..a6bafda5a8 100644 --- a/runatlantis.io/docs/security.md +++ b/runatlantis.io/docs/security.md @@ -46,6 +46,7 @@ For example: * Specific repositories: `--repo-allowlist=github.com/runatlantis/atlantis,github.com/runatlantis/atlantis-tests` * Your whole organization: `--repo-allowlist=github.com/runatlantis/*` * Every repository in your GitHub Enterprise install: `--repo-allowlist=github.yourcompany.com/*` +* You can also omit specific repos: `--repo-allowlist='github.com/runatlantis/*,!github.com/runatlantis/untrusted-repo'` * All repositories: `--repo-allowlist=*`. Useful for when you're in a protected network but dangerous without also setting a webhook secret. This flag ensures your Atlantis install isn't being used with repositories you don't control. See `atlantis server --help` for more details. diff --git a/runatlantis.io/docs/server-configuration.md b/runatlantis.io/docs/server-configuration.md index 14c5710cda..bca4e04da4 100644 --- a/runatlantis.io/docs/server-configuration.md +++ b/runatlantis.io/docs/server-configuration.md @@ -810,6 +810,7 @@ This is useful when you have many projects and want to keep the pull request cle * Accepts a comma separated list, ex. `definition1,definition2` * Format is `{hostname}/{owner}/{repo}`, ex. `github.com/runatlantis/atlantis` * `*` matches any characters, ex. `github.com/runatlantis/*` will match all repos in the runatlantis organization + * An entry beginning with `!` negates it, ex. `github.com/foo/*,!github.com/foo/bar` will match all github repos in the `foo` owner *except* `bar`. * For Bitbucket Server: `{hostname}` is the domain without scheme and port, `{owner}` is the name of the project (not the key), and `{repo}` is the repo name * User (not project) repositories take on the format: `{hostname}/{full name}/{repo}` (e.g., `bitbucket.example.com/Jane Doe/myatlantis` for username `jdoe` and full name `Jane Doe`, which is not very intuitive) * For Azure DevOps the allowlist takes one of two forms: `{owner}.visualstudio.com/{project}/{repo}` or `dev.azure.com/{owner}/{project}/{repo}` @@ -820,6 +821,8 @@ This is useful when you have many projects and want to keep the pull request cle * `--repo-allowlist=github.com/myorg/repo1,github.com/myorg/repo2` * Allowlist all repos under `myorg` on `github.com` * `--repo-allowlist='github.com/myorg/*'` + * Allowlist all repos under `myorg` on `github.com`, excluding `myorg/untrusted-repo` + * `--repo-allowlist='github.com/myorg/*,!github.com/myorg/untrusted-repo'` * Allowlist all repos in my GitHub Enterprise installation * `--repo-allowlist='github.yourcompany.com/*'` * Allowlist all repos under `myorg` project `myproject` on Azure DevOps diff --git a/server/events/repo_allowlist_checker.go b/server/events/repo_allowlist_checker.go index 5acc4e7050..4950b24056 100644 --- a/server/events/repo_allowlist_checker.go +++ b/server/events/repo_allowlist_checker.go @@ -24,20 +24,28 @@ const Wildcard = "*" // RepoAllowlistChecker implements checking if repos are allowlisted to be used with // this Atlantis. type RepoAllowlistChecker struct { - rules []string + includeRules []string + omitRules []string } // NewRepoAllowlistChecker constructs a new checker and validates that the // allowlist isn't malformed. func NewRepoAllowlistChecker(allowlist string) (*RepoAllowlistChecker, error) { - rules := strings.Split(allowlist, ",") - for _, rule := range rules { + includeRules := make([]string, 0) + omitRules := make([]string, 0) + for _, rule := range strings.Split(allowlist, ",") { if strings.Contains(rule, "://") { return nil, fmt.Errorf("allowlist %q contained ://", rule) } + if len(rule) > 1 && rule[0] == '!' { + omitRules = append(omitRules, rule[1:]) + } else { + includeRules = append(includeRules, rule) + } } return &RepoAllowlistChecker{ - rules: rules, + includeRules: includeRules, + omitRules: omitRules, }, nil } @@ -45,7 +53,13 @@ func NewRepoAllowlistChecker(allowlist string) (*RepoAllowlistChecker, error) { // otherwise. func (r *RepoAllowlistChecker) IsAllowlisted(repoFullName string, vcsHostname string) bool { candidate := fmt.Sprintf("%s/%s", vcsHostname, repoFullName) - for _, rule := range r.rules { + shouldInclude := r.matchesAtLeastOneRule(r.includeRules, candidate) + shouldOmit := r.matchesAtLeastOneRule(r.omitRules, candidate) + return shouldInclude && !shouldOmit +} + +func (r *RepoAllowlistChecker) matchesAtLeastOneRule(rules []string, candidate string) bool { + for _, rule := range rules { if r.matchesRule(rule, candidate) { return true } diff --git a/server/events/repo_allowlist_checker_test.go b/server/events/repo_allowlist_checker_test.go index 79ff3d4615..c03e714244 100644 --- a/server/events/repo_allowlist_checker_test.go +++ b/server/events/repo_allowlist_checker_test.go @@ -175,6 +175,20 @@ func TestRepoAllowlistChecker_IsAllowlisted(t *testing.T) { "github.com", true, }, + { + "should exclude with negative match", + "github.com/owner/*,!github.com/owner/badrepo", + "owner/badrepo", + "github.com", + false, + }, + { + "should match if with negative rule doesn't match", + "github.com/owner/*,!github.com/owner/badrepo", + "owner/otherrepo", + "github.com", + true, + }, } for _, c := range cases {