Skip to content

Commit

Permalink
Merge branch 'main' into speed-up-github-comments-hide3
Browse files Browse the repository at this point in the history
  • Loading branch information
X-Guardian authored Jan 26, 2025
2 parents eb824c3 + 05659b8 commit 6dff324
Show file tree
Hide file tree
Showing 41 changed files with 1,084 additions and 178 deletions.
21 changes: 13 additions & 8 deletions cmd/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ const (
GiteaUserFlag = "gitea-user"
GiteaWebhookSecretFlag = "gitea-webhook-secret" // nolint: gosec
GiteaPageSizeFlag = "gitea-page-size"
GitlabGroupAllowlistFlag = "gitlab-group-allowlist"
GitlabHostnameFlag = "gitlab-hostname"
GitlabTokenFlag = "gitlab-token"
GitlabUserFlag = "gitlab-user"
Expand Down Expand Up @@ -261,7 +262,7 @@ var stringFlags = map[string]stringFlag{
defaultValue: DefaultBitbucketBaseURL,
},
BitbucketWebhookSecretFlag: {
description: "Secret used to validate Bitbucket webhooks. Only Bitbucket Server supports webhook secrets." +
description: "Secret used to validate Bitbucket webhooks." +
" SECURITY WARNING: If not specified, Atlantis won't be able to validate that the incoming webhook call came from Bitbucket. " +
"This means that an attacker could spoof calls to Atlantis and cause it to perform malicious actions. " +
"Should be specified via the ATLANTIS_BITBUCKET_WEBHOOK_SECRET environment variable.",
Expand Down Expand Up @@ -360,6 +361,17 @@ var stringFlags = map[string]stringFlag{
"This means that an attacker could spoof calls to Atlantis and cause it to perform malicious actions. " +
"Should be specified via the ATLANTIS_GITEA_WEBHOOK_SECRET environment variable.",
},
GitlabGroupAllowlistFlag: {
description: "Comma separated list of key-value pairs representing the GitLab groups and the operations that " +
"the members of a particular group are allowed to perform. " +
"The format is {group}:{command},{group}:{command}. " +
"Valid values for 'command' are 'plan', 'apply' and '*', e.g. 'myorg/dev:plan,myorg/ops:apply,myorg/devops:*'" +
"This example gives the users from the 'myorg/dev' GitLab group the permissions to execute the 'plan' command, " +
"the 'myorg/ops' group the permissions to execute the 'apply' command, " +
"and allows the 'myorg/devops' group 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 group to perform any operation.",
},
GitlabHostnameFlag: {
description: "Hostname of your GitLab Enterprise installation. If using gitlab.com, no need to set.",
defaultValue: DefaultGitlabHostname,
Expand Down Expand Up @@ -1022,10 +1034,6 @@ func (s *ServerCmd) validate(userConfig server.UserConfig) error {
return fmt.Errorf("--%s cannot contain ://, should be hostnames only", RepoAllowlistFlag)
}

if userConfig.BitbucketBaseURL == DefaultBitbucketBaseURL && userConfig.BitbucketWebhookSecret != "" {
return fmt.Errorf("--%s cannot be specified for Bitbucket Cloud because it is not supported by Bitbucket", BitbucketWebhookSecretFlag)
}

parsed, err := url.Parse(userConfig.BitbucketBaseURL)
if err != nil {
return fmt.Errorf("error parsing --%s flag value %q: %s", BitbucketWebhookSecretFlag, userConfig.BitbucketBaseURL, err)
Expand Down Expand Up @@ -1173,9 +1181,6 @@ func (s *ServerCmd) securityWarnings(userConfig *server.UserConfig) {
if userConfig.BitbucketUser != "" && userConfig.BitbucketBaseURL != DefaultBitbucketBaseURL && userConfig.BitbucketWebhookSecret == "" && !s.SilenceOutput {
s.Logger.Warn("no Bitbucket webhook secret set. This could allow attackers to spoof requests from Bitbucket")
}
if userConfig.BitbucketUser != "" && userConfig.BitbucketBaseURL == DefaultBitbucketBaseURL && !s.SilenceOutput {
s.Logger.Warn("Bitbucket Cloud does not support webhook secrets. This could allow attackers to spoof requests from Bitbucket. Ensure you are allowing only Bitbucket IPs")
}
if userConfig.AzureDevopsWebhookUser != "" && userConfig.AzureDevopsWebhookPassword == "" && !s.SilenceOutput {
s.Logger.Warn("no Azure DevOps webhook user and password set. This could allow attackers to spoof requests from Azure DevOps.")
}
Expand Down
13 changes: 1 addition & 12 deletions cmd/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ var testFlags = map[string]interface{}{
GiteaUserFlag: "gitea-user",
GiteaWebhookSecretFlag: "gitea-secret",
GiteaPageSizeFlag: 30,
GitlabGroupAllowlistFlag: "",
GitlabHostnameFlag: "gitlab-hostname",
GitlabTokenFlag: "gitlab-token",
GitlabUserFlag: "gitlab-user",
Expand Down Expand Up @@ -939,18 +940,6 @@ func TestExecute_ADUser(t *testing.T) {
Equals(t, "user", passedConfig.AzureDevopsUser)
}

// If using bitbucket cloud, webhook secrets are not supported.
func TestExecute_BitbucketCloudWithWebhookSecret(t *testing.T) {
c := setup(map[string]interface{}{
BitbucketUserFlag: "user",
BitbucketTokenFlag: "token",
RepoAllowlistFlag: "*",
BitbucketWebhookSecretFlag: "my secret",
}, t)
err := c.Execute()
ErrEquals(t, "--bitbucket-webhook-secret cannot be specified for Bitbucket Cloud because it is not supported by Bitbucket", err)
}

// Base URL must have a scheme.
func TestExecute_BitbucketServerBaseURLScheme(t *testing.T) {
c := setup(map[string]interface{}{
Expand Down
2 changes: 1 addition & 1 deletion runatlantis.io/docs/access-credentials.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ A new permission for `Actions` has been added, which is required for checking if

* Create an App Password by following [BitBucket Cloud: Create an app password](https://support.atlassian.com/bitbucket-cloud/docs/create-an-app-password/)
* Label the password "atlantis"
* Select **Pull requests**: **Read** and **Write** so that Atlantis can read your pull requests and write comments to them
* Select **Pull requests**: **Read** and **Write** so that Atlantis can read your pull requests and write comments to them. If you want to enable the [hide-prev-plan-comments](./server-configuration#hide-prev-plan-comments) feature and thus delete old comments, please add **Account**: **Read** as well.
* Record the access token

### Bitbucket Server (aka Stash)
Expand Down
10 changes: 6 additions & 4 deletions runatlantis.io/docs/deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,10 +117,6 @@ echo -n "yoursecret" > webhook-secret
kubectl create secret generic atlantis-vcs --from-file=token --from-file=webhook-secret
```
::: tip Note
If you're using Bitbucket Cloud then there is no webhook secret since it's not supported.
:::
Next, edit the manifests below as follows:
1. Replace `<VERSION>` in `image: ghcr.io/runatlantis/atlantis:<VERSION>` with the most recent version from [GitHub: Atlantis latest release](https://github.com/runatlantis/atlantis/releases/latest).
Expand Down Expand Up @@ -231,6 +227,11 @@ spec:
secretKeyRef:
name: atlantis-vcs
key: token
- name: ATLANTIS_BITBUCKET_WEBHOOK_SECRET
valueFrom:
secretKeyRef:
name: atlantis-vcs
key: webhook-secret
### End Bitbucket Config ###
### Azure DevOps Config ###
Expand Down Expand Up @@ -742,6 +743,7 @@ atlantis server \
--atlantis-url="$URL" \
--bitbucket-user="$USERNAME" \
--bitbucket-token="$TOKEN" \
--bitbucket-webhook-secret="$SECRET" \
--repo-allowlist="$REPO_ALLOWLIST"
```
Expand Down
15 changes: 0 additions & 15 deletions runatlantis.io/docs/security.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,21 +22,6 @@ Atlantis could be exploited by
* Running malicious custom build commands specified in an `atlantis.yaml` file. Atlantis uses the `atlantis.yaml` file from the pull request branch, **not** `main`.
* Someone adding `atlantis plan/apply` comments on your valid pull requests causing terraform to run when you don't want it to.
## Bitbucket Cloud (bitbucket.org)
::: danger
Bitbucket Cloud does not support webhook secrets. This could allow attackers to spoof requests from Bitbucket. Ensure you are allowing only Bitbucket IPs.
:::
Bitbucket Cloud doesn't support webhook secrets. This means that an attacker could
make fake requests to Atlantis that look like they're coming from Bitbucket.
If you are specifying `--repo-allowlist` then they could only fake requests pertaining
to those repos so the most damage they could do would be to plan/apply on your
own repos.
To prevent this, allowlist [Bitbucket's IP addresses](https://confluence.atlassian.com/bitbucket/what-are-the-bitbucket-cloud-ip-addresses-i-should-use-to-configure-my-corporate-firewall-343343385.html)
(see Outbound IPv4 addresses).
## Mitigations
### Don't Use On Public Repos
Expand Down
29 changes: 24 additions & 5 deletions runatlantis.io/docs/server-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -330,8 +330,7 @@ and set `--autoplan-modules` to `false`.
ATLANTIS_BITBUCKET_WEBHOOK_SECRET="secret"
```

Secret used to validate Bitbucket webhooks. Only Bitbucket Server supports webhook secrets.
For Bitbucket.org, see [Security](security.md#bitbucket-cloud-bitbucket-org) for mitigations.
Secret used to validate Bitbucket webhooks.

::: warning SECURITY WARNING
If not specified, Atlantis won't be able to validate that the incoming webhook call came from Bitbucket.
Expand Down Expand Up @@ -800,6 +799,21 @@ based on the organization or user that triggered the webhook.
This means that an attacker could spoof calls to Atlantis and cause it to perform malicious actions.
:::

### `--gitlab-group-allowlist`

```bash
atlantis server --gitlab-group-allowlist="myorg/mygroup:plan, myorg/secteam:apply, myorg/devops:apply, myorg/devops:import"
# or
ATLANTIS_GITLAB_GROUP_ALLOWLIST="myorg/mygroup:plan, myorg/secteam:apply, myorg/devops:apply, myorg/devops:import"
```

Comma-separated list of GitLab groups and permission pairs.

By default, any group can plan and apply.

::: warning NOTE
Atlantis needs to be able to view the listed group members, inaccessible or non-existent groups are silently ignored.

### `--gitlab-hostname`

```bash
Expand Down Expand Up @@ -863,9 +877,14 @@ based on the organization or user that triggered the webhook.
```

Hide previous plan comments to declutter PRs. This is only supported in
GitHub and GitLab currently. This is not enabled by default. When using Github App, you need to set `--gh-app-slug` to enable this feature.
For github, ensure the `--gh-user` is set appropriately or comments will not be hidden.

GitHub and GitLab and Bitbucket currently and is not enabled by default.

For Bitbucket, the comments are deleted rather than hidden as Bitbucket does not support hiding comments.

For GitHub, ensure the `--gh-user` is set appropriately or comments will not be hidden.

When using the GitHub App, you need to set `--gh-app-slug` to enable this feature.

### `--hide-unchanged-plan-comments`

```bash
Expand Down
5 changes: 0 additions & 5 deletions runatlantis.io/docs/webhook-secrets.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,6 @@ Azure DevOps uses Basic authentication for webhooks rather than webhook secrets.
An app-wide token is generated during [GitHub App setup](access-credentials.md#github-app). You can recover it by navigating to the [GitHub app settings page](https://github.com/settings/apps) and selecting "Edit" next to your Atlantis app's name. Token appears after clicking "Edit" under the Webhook header.
:::

::: warning
Bitbucket.org **does not** support webhook secrets.
To mitigate, use repo allowlists and IP allowlists. See [Security](security.md#bitbucket-cloud-bitbucket-org) for more information.
:::

## Generating A Webhook Secret

You can use any random string generator to create your Webhook secret. It should be > 24 characters.
Expand Down
6 changes: 1 addition & 5 deletions runatlantis.io/guide/testing-locally.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,7 @@ URL="https://{YOUR_HOSTNAME}.ngrok.io"

GitHub and GitLab use webhook secrets so clients can verify that the webhooks came
from them.
::: warning
Bitbucket Cloud (bitbucket.org) doesn't use webhook secrets so if you're using Bitbucket Cloud you can skip this step.
When you're ready to do a production deploy of Atlantis you should allowlist [Bitbucket IPs](https://confluence.atlassian.com/bitbucket/what-are-the-bitbucket-cloud-ip-addresses-i-should-use-to-configure-my-corporate-firewall-343343385.html)
to ensure the webhooks are coming from them.
:::

Create a random string of any length (you can use [random.org](https://www.random.org/strings/))
and set an environment variable:

Expand Down
11 changes: 9 additions & 2 deletions server/controllers/events/events_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ const giteaRequestIDHeader = "X-Gitea-Delivery"
const bitbucketEventTypeHeader = "X-Event-Key"
const bitbucketCloudRequestIDHeader = "X-Request-UUID"
const bitbucketServerRequestIDHeader = "X-Request-ID"
const bitbucketServerSignatureHeader = "X-Hub-Signature"
const bitbucketSignatureHeader = "X-Hub-Signature"

// The URL used for Azure DevOps test webhooks
const azuredevopsTestURL = "https://fabrikam.visualstudio.com/DefaultCollection/_apis/git/repositories/4bc14d40-c903-45e2-872e-0462c7748079"
Expand Down Expand Up @@ -223,12 +223,19 @@ func (e *VCSEventsController) handleGithubPost(w http.ResponseWriter, r *http.Re
func (e *VCSEventsController) handleBitbucketCloudPost(w http.ResponseWriter, r *http.Request) {
eventType := r.Header.Get(bitbucketEventTypeHeader)
reqID := r.Header.Get(bitbucketCloudRequestIDHeader)
sig := r.Header.Get(bitbucketSignatureHeader)
defer r.Body.Close() // nolint: errcheck
body, err := io.ReadAll(r.Body)
if err != nil {
e.respond(w, logging.Error, http.StatusBadRequest, "Unable to read body: %s %s=%s", err, bitbucketCloudRequestIDHeader, reqID)
return
}
if len(e.BitbucketWebhookSecret) > 0 {
if err := bitbucketcloud.ValidateSignature(body, sig, e.BitbucketWebhookSecret); err != nil {
e.respond(w, logging.Warn, http.StatusBadRequest, "%s", errors.Wrap(err, "request did not pass validation").Error())
return
}
}
switch eventType {
case bitbucketcloud.PullCreatedHeader, bitbucketcloud.PullUpdatedHeader, bitbucketcloud.PullFulfilledHeader, bitbucketcloud.PullRejectedHeader:
e.Logger.Debug("handling as pull request state changed event")
Expand All @@ -246,7 +253,7 @@ func (e *VCSEventsController) handleBitbucketCloudPost(w http.ResponseWriter, r
func (e *VCSEventsController) handleBitbucketServerPost(w http.ResponseWriter, r *http.Request) {
eventType := r.Header.Get(bitbucketEventTypeHeader)
reqID := r.Header.Get(bitbucketServerRequestIDHeader)
sig := r.Header.Get(bitbucketServerSignatureHeader)
sig := r.Header.Get(bitbucketSignatureHeader)
defer r.Body.Close() // nolint: errcheck
body, err := io.ReadAll(r.Body)
if err != nil {
Expand Down
14 changes: 14 additions & 0 deletions server/core/config/valid/policies.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package valid

import (
"slices"
"strings"

version "github.com/hashicorp/go-version"
Expand Down Expand Up @@ -67,3 +68,16 @@ func (o *PolicyOwners) IsOwner(username string, userTeams []string) bool {

return false
}

// Return all owner teams from all policy sets
func (p *PolicySets) AllTeams() []string {
teams := p.Owners.Teams
for _, policySet := range p.PolicySets {
for _, team := range policySet.Owners.Teams {
if !slices.Contains(teams, team) {
teams = append(teams, team)
}
}
}
return teams
}
63 changes: 63 additions & 0 deletions server/core/config/valid/policies_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,3 +120,66 @@ func TestPoliciesConfig_IsOwners(t *testing.T) {
})
}
}

func TestPoliciesConfig_AllTeams(t *testing.T) {
cases := []struct {
description string
input valid.PolicySets
expResult []string
}{
{
description: "has only top-level team owner",
input: valid.PolicySets{
Owners: valid.PolicyOwners{
Teams: []string{
"team1",
},
},
},
expResult: []string{"team1"},
},
{
description: "has only policy-level team owner",
input: valid.PolicySets{
PolicySets: []valid.PolicySet{
{
Name: "policy1",
Owners: valid.PolicyOwners{
Teams: []string{
"team2",
},
},
},
},
},
expResult: []string{"team2"},
},
{
description: "has both top-level and policy-level team owners",
input: valid.PolicySets{
Owners: valid.PolicyOwners{
Teams: []string{
"team1",
},
},
PolicySets: []valid.PolicySet{
{
Name: "policy1",
Owners: valid.PolicyOwners{
Teams: []string{
"team2",
},
},
},
},
},
expResult: []string{"team1", "team2"},
},
}
for _, c := range cases {
t.Run(c.description, func(t *testing.T) {
result := c.input.AllTeams()
Equals(t, c.expResult, result)
})
}
}
5 changes: 3 additions & 2 deletions server/core/runtime/run_step_runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,10 @@ func (r *RunStepRunner) Run(
err = fmt.Errorf("%s: running %q in %q: \n%s", err, command, path, output)
if !ctx.CustomPolicyCheck {
ctx.Log.Debug("error: %s", err)
return "", err
} else {
ctx.Log.Debug("Treating custom policy tool error exit code as a policy failure. Error output: %s", err)
}
ctx.Log.Debug("Treating custom policy tool error exit code as a policy failure. Error output: %s", err)
return "", err
}

switch postProcessOutput {
Expand Down
Loading

0 comments on commit 6dff324

Please sign in to comment.