Add webhook secret checking.

This commit is contained in:
Luke Kysow
2018-07-24 16:13:55 +02:00
parent 33926a01f2
commit e8c0f7c5bb
6 changed files with 351 additions and 163 deletions

View File

@@ -15,6 +15,7 @@ package cmd
import (
"fmt"
"net/url"
"os"
"path/filepath"
"strings"
@@ -33,36 +34,37 @@ import (
// 3. Add your flag's description etc. to the stringFlags, intFlags, or boolFlags slices.
const (
// Flag names.
AllowForkPRsFlag = "allow-fork-prs"
AllowRepoConfigFlag = "allow-repo-config"
AtlantisURLFlag = "atlantis-url"
BitbucketHostnameFlag = "bitbucket-hostname"
BitbucketTokenFlag = "bitbucket-token"
BitbucketUserFlag = "bitbucket-user"
ConfigFlag = "config"
DataDirFlag = "data-dir"
GHHostnameFlag = "gh-hostname"
GHTokenFlag = "gh-token"
GHUserFlag = "gh-user"
GHWebhookSecret = "gh-webhook-secret" // nolint: gosec
GitlabHostnameFlag = "gitlab-hostname"
GitlabTokenFlag = "gitlab-token"
GitlabUserFlag = "gitlab-user"
GitlabWebhookSecret = "gitlab-webhook-secret" // nolint: gosec
LogLevelFlag = "log-level"
PortFlag = "port"
RepoWhitelistFlag = "repo-whitelist"
RequireApprovalFlag = "require-approval"
SSLCertFileFlag = "ssl-cert-file"
SSLKeyFileFlag = "ssl-key-file"
AllowForkPRsFlag = "allow-fork-prs"
AllowRepoConfigFlag = "allow-repo-config"
AtlantisURLFlag = "atlantis-url"
BitbucketBaseURLFlag = "bitbucket-base-url"
BitbucketTokenFlag = "bitbucket-token"
BitbucketUserFlag = "bitbucket-user"
BitbucketWebhookSecretFlag = "bitbucket-webhook-secret" // nolint: gosec
ConfigFlag = "config"
DataDirFlag = "data-dir"
GHHostnameFlag = "gh-hostname"
GHTokenFlag = "gh-token"
GHUserFlag = "gh-user"
GHWebhookSecretFlag = "gh-webhook-secret" // nolint: gosec
GitlabHostnameFlag = "gitlab-hostname"
GitlabTokenFlag = "gitlab-token"
GitlabUserFlag = "gitlab-user"
GitlabWebhookSecretFlag = "gitlab-webhook-secret" // nolint: gosec
LogLevelFlag = "log-level"
PortFlag = "port"
RepoWhitelistFlag = "repo-whitelist"
RequireApprovalFlag = "require-approval"
SSLCertFileFlag = "ssl-cert-file"
SSLKeyFileFlag = "ssl-key-file"
// Flag defaults.
DefaultBitbucketHostname = bitbucketcloud.Hostname
DefaultDataDir = "~/.atlantis"
DefaultGHHostname = "github.com"
DefaultGitlabHostname = "gitlab.com"
DefaultLogLevel = "info"
DefaultPort = 4141
DefaultBitbucketBaseURL = bitbucketcloud.BaseURL
DefaultDataDir = "~/.atlantis"
DefaultGHHostname = "github.com"
DefaultGitlabHostname = "gitlab.com"
DefaultLogLevel = "info"
DefaultPort = 4141
)
const RedTermStart = "\033[31m"
@@ -82,9 +84,18 @@ var stringFlags = []stringFlag{
description: "Bitbucket app password of API user. Can also be specified via the ATLANTIS_BITBUCKET_TOKEN environment variable.",
},
{
name: BitbucketHostnameFlag,
description: "Hostname and port of your Bitbucket Server (aka Stash) installation. If using Bitbucket Cloud (bitbucket.org), no need to set.",
defaultValue: DefaultBitbucketHostname,
name: BitbucketBaseURLFlag,
description: "Base URL of Bitbucket Server (aka Stash) installation." +
" Must include scheme, ex. 'http://bitbucket.corp:7990' or 'https://bitbucket.corp'." +
" If using Bitbucket Cloud (bitbucket.org), do not set.",
defaultValue: DefaultBitbucketBaseURL,
},
{
name: BitbucketWebhookSecretFlag,
description: "Secret used to validate Bitbucket webhooks. Only Bitbucket Server supports webhook secrets." +
" 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.",
},
{
name: ConfigFlag,
@@ -109,7 +120,7 @@ var stringFlags = []stringFlag{
description: "GitHub token of API user. Can also be specified via the ATLANTIS_GH_TOKEN environment variable.",
},
{
name: GHWebhookSecret,
name: GHWebhookSecretFlag,
description: "Secret used to validate GitHub webhooks (see https://developer.github.com/webhooks/securing/)." +
" SECURITY WARNING: If not specified, Atlantis won't be able to validate that the incoming webhook call came from GitHub. " +
"This means that an attacker could spoof calls to Atlantis and cause it to perform malicious actions. " +
@@ -129,7 +140,7 @@ var stringFlags = []stringFlag{
description: "GitLab token of API user. Can also be specified via the ATLANTIS_GITLAB_TOKEN environment variable.",
},
{
name: GitlabWebhookSecret,
name: GitlabWebhookSecretFlag,
description: "Optional secret used to validate GitLab webhooks." +
" SECURITY WARNING: If not specified, Atlantis won't be able to validate that the incoming webhook call came from GitLab. " +
"This means that an attacker could spoof calls to Atlantis and cause it to perform malicious actions. " +
@@ -144,7 +155,8 @@ var stringFlags = []stringFlag{
name: RepoWhitelistFlag,
description: "Comma separated list of repositories that Atlantis will operate on. " +
"The format is {hostname}/{owner}/{repo}, ex. github.com/runatlantis/atlantis. '*' matches any characters until the next comma and can be used for example to whitelist " +
"all repos: '*' (not recommended), an entire hostname: 'internalgithub.com/*' or an organization: 'github.com/runatlantis/*'.",
"all repos: '*' (not recommended), an entire hostname: 'internalgithub.com/*' or an organization: 'github.com/runatlantis/*'." +
" 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.",
},
{
name: SSLCertFileFlag,
@@ -344,8 +356,8 @@ func (s *ServerCmd) setDefaults(c *server.UserConfig) {
if c.GitlabHostname == "" {
c.GitlabHostname = DefaultGitlabHostname
}
if c.BitbucketHostname == "" {
c.BitbucketHostname = DefaultBitbucketHostname
if c.BitbucketBaseURL == "" {
c.BitbucketBaseURL = DefaultBitbucketBaseURL
}
if c.LogLevel == "" {
c.LogLevel = DefaultLogLevel
@@ -387,6 +399,17 @@ func (s *ServerCmd) validate(userConfig server.UserConfig) error {
return fmt.Errorf("--%s cannot contain ://, should be hostnames only", RepoWhitelistFlag)
}
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)
}
if parsed.Scheme != "http" && parsed.Scheme != "https" {
return fmt.Errorf("--%s must have http:// or https://, got %q", BitbucketBaseURLFlag, userConfig.BitbucketBaseURL)
}
return nil
}
@@ -440,7 +463,10 @@ func (s *ServerCmd) securityWarnings(userConfig *server.UserConfig) {
if userConfig.GitlabUser != "" && userConfig.GitlabWebhookSecret == "" && !s.SilenceOutput {
fmt.Fprintf(os.Stderr, "%s[WARN] No GitLab webhook secret set. This could allow attackers to spoof requests from GitLab.%s\n", RedTermStart, RedTermEnd)
}
if userConfig.BitbucketUser != "" && userConfig.BitbucketHostname == DefaultBitbucketHostname && !s.SilenceOutput {
if userConfig.BitbucketUser != "" && userConfig.BitbucketBaseURL != DefaultBitbucketBaseURL && userConfig.BitbucketWebhookSecret == "" && !s.SilenceOutput {
fmt.Fprintf(os.Stderr, "%s[WARN] No Bitbucket webhook secret set. This could allow attackers to spoof requests from Bitbucket.%s\n", RedTermStart, RedTermEnd)
}
if userConfig.BitbucketUser != "" && userConfig.BitbucketBaseURL == DefaultBitbucketBaseURL && !s.SilenceOutput {
fmt.Fprintf(os.Stderr, "%s[WARN] Bitbucket Cloud does not support webhook secrets. This could allow attackers to spoof requests from Bitbucket. Ensure you are whitelisting Bitbucket IPs.%s\n", RedTermStart, RedTermEnd)
}
}

View File

@@ -339,9 +339,10 @@ func TestExecute_Defaults(t *testing.T) {
Equals(t, "gitlab-token", passedConfig.GitlabToken)
Equals(t, "gitlab-user", passedConfig.GitlabUser)
Equals(t, "", passedConfig.GitlabWebhookSecret)
Equals(t, "bitbucket.org", passedConfig.BitbucketHostname)
Equals(t, "https://api.bitbucket.org", passedConfig.BitbucketBaseURL)
Equals(t, "bitbucket-token", passedConfig.BitbucketToken)
Equals(t, "bitbucket-user", passedConfig.BitbucketUser)
Equals(t, "", passedConfig.BitbucketWebhookSecret)
Equals(t, "info", passedConfig.LogLevel)
Equals(t, 4141, passedConfig.Port)
Equals(t, false, passedConfig.RequireApproval)
@@ -422,27 +423,28 @@ func TestExecute_BitbucketUser(t *testing.T) {
func TestExecute_Flags(t *testing.T) {
t.Log("Should use all flags that are set.")
c := setup(map[string]interface{}{
cmd.AtlantisURLFlag: "url",
cmd.AllowForkPRsFlag: true,
cmd.AllowRepoConfigFlag: true,
//cmd.BitbucketHostnameFlag: "ghhostname",
cmd.BitbucketTokenFlag: "bitbucket-token",
cmd.BitbucketUserFlag: "bitbucket-user",
cmd.DataDirFlag: "/path",
cmd.GHHostnameFlag: "ghhostname",
cmd.GHTokenFlag: "token",
cmd.GHUserFlag: "user",
cmd.GHWebhookSecret: "secret",
cmd.GitlabHostnameFlag: "gitlab-hostname",
cmd.GitlabTokenFlag: "gitlab-token",
cmd.GitlabUserFlag: "gitlab-user",
cmd.GitlabWebhookSecret: "gitlab-secret",
cmd.LogLevelFlag: "debug",
cmd.PortFlag: 8181,
cmd.RepoWhitelistFlag: "github.com/runatlantis/atlantis",
cmd.RequireApprovalFlag: true,
cmd.SSLCertFileFlag: "cert-file",
cmd.SSLKeyFileFlag: "key-file",
cmd.AtlantisURLFlag: "url",
cmd.AllowForkPRsFlag: true,
cmd.AllowRepoConfigFlag: true,
cmd.BitbucketBaseURLFlag: "https://bitbucket-base-url.com",
cmd.BitbucketTokenFlag: "bitbucket-token",
cmd.BitbucketUserFlag: "bitbucket-user",
cmd.BitbucketWebhookSecretFlag: "bitbucket-secret",
cmd.DataDirFlag: "/path",
cmd.GHHostnameFlag: "ghhostname",
cmd.GHTokenFlag: "token",
cmd.GHUserFlag: "user",
cmd.GHWebhookSecretFlag: "secret",
cmd.GitlabHostnameFlag: "gitlab-hostname",
cmd.GitlabTokenFlag: "gitlab-token",
cmd.GitlabUserFlag: "gitlab-user",
cmd.GitlabWebhookSecretFlag: "gitlab-secret",
cmd.LogLevelFlag: "debug",
cmd.PortFlag: 8181,
cmd.RepoWhitelistFlag: "github.com/runatlantis/atlantis",
cmd.RequireApprovalFlag: true,
cmd.SSLCertFileFlag: "cert-file",
cmd.SSLKeyFileFlag: "key-file",
})
err := c.Execute()
Ok(t, err)
@@ -450,8 +452,10 @@ func TestExecute_Flags(t *testing.T) {
Equals(t, "url", passedConfig.AtlantisURL)
Equals(t, true, passedConfig.AllowForkPRs)
Equals(t, true, passedConfig.AllowRepoConfig)
Equals(t, "https://bitbucket-base-url.com", passedConfig.BitbucketBaseURL)
Equals(t, "bitbucket-token", passedConfig.BitbucketToken)
Equals(t, "bitbucket-user", passedConfig.BitbucketUser)
Equals(t, "bitbucket-secret", passedConfig.BitbucketWebhookSecret)
Equals(t, "/path", passedConfig.DataDir)
Equals(t, "ghhostname", passedConfig.GithubHostname)
Equals(t, "token", passedConfig.GithubToken)
@@ -475,8 +479,10 @@ func TestExecute_ConfigFile(t *testing.T) {
atlantis-url: "url"
allow-fork-prs: true
allow-repo-config: true
bitbucket-base-url: "https://mydomain.com"
bitbucket-token: "bitbucket-token"
bitbucket-user: "bitbucket-user"
bitbucket-webhook-secret: "bitbucket-secret"
data-dir: "/path"
gh-hostname: "ghhostname"
gh-token: "token"
@@ -503,8 +509,10 @@ ssl-key-file: key-file
Equals(t, "url", passedConfig.AtlantisURL)
Equals(t, true, passedConfig.AllowForkPRs)
Equals(t, true, passedConfig.AllowRepoConfig)
Equals(t, "https://mydomain.com", passedConfig.BitbucketBaseURL)
Equals(t, "bitbucket-token", passedConfig.BitbucketToken)
Equals(t, "bitbucket-user", passedConfig.BitbucketUser)
Equals(t, "bitbucket-secret", passedConfig.BitbucketWebhookSecret)
Equals(t, "/path", passedConfig.DataDir)
Equals(t, "ghhostname", passedConfig.GithubHostname)
Equals(t, "token", passedConfig.GithubToken)
@@ -528,8 +536,10 @@ func TestExecute_EnvironmentOverride(t *testing.T) {
atlantis-url: "url"
allow-fork-prs: true
allow-repo-config: true
bitbucket-base-url: "https://mydomain.com"
bitbucket-token: "bitbucket-token"
bitbucket-user: "bitbucket-user"
bitbucket-webhook-secret: "bitbucket-secret"
data-dir: "/path"
gh-hostname: "ghhostname"
gh-token: "token"
@@ -550,26 +560,28 @@ ssl-key-file: key-file
// NOTE: We add the ATLANTIS_ prefix below.
for name, value := range map[string]string{
"ATLANTIS_URL": "override-url",
"ALLOW_FORK_PRS": "false",
"ALLOW_REPO_CONFIG": "false",
"BITBUCKET_TOKEN": "override-bitbucket-token",
"BITBUCKET_USER": "override-bitbucket-user",
"DATA_DIR": "/override-path",
"GH_HOSTNAME": "override-gh-hostname",
"GH_TOKEN": "override-gh-token",
"GH_USER": "override-gh-user",
"GH_WEBHOOK_SECRET": "override-gh-webhook-secret",
"GITLAB_HOSTNAME": "override-gitlab-hostname",
"GITLAB_TOKEN": "override-gitlab-token",
"GITLAB_USER": "override-gitlab-user",
"GITLAB_WEBHOOK_SECRET": "override-gitlab-webhook-secret",
"LOG_LEVEL": "info",
"PORT": "8282",
"REPO_WHITELIST": "override,override",
"REQUIRE_APPROVAL": "false",
"SSL_CERT_FILE": "override-cert-file",
"SSL_KEY_FILE": "override-key-file",
"ATLANTIS_URL": "override-url",
"ALLOW_FORK_PRS": "false",
"ALLOW_REPO_CONFIG": "false",
"BITBUCKET_BASE_URL": "https://override-bitbucket-base-url",
"BITBUCKET_TOKEN": "override-bitbucket-token",
"BITBUCKET_USER": "override-bitbucket-user",
"BITBUCKET_WEBHOOK_SECRET": "override-bitbucket-secret",
"DATA_DIR": "/override-path",
"GH_HOSTNAME": "override-gh-hostname",
"GH_TOKEN": "override-gh-token",
"GH_USER": "override-gh-user",
"GH_WEBHOOK_SECRET": "override-gh-webhook-secret",
"GITLAB_HOSTNAME": "override-gitlab-hostname",
"GITLAB_TOKEN": "override-gitlab-token",
"GITLAB_USER": "override-gitlab-user",
"GITLAB_WEBHOOK_SECRET": "override-gitlab-webhook-secret",
"LOG_LEVEL": "info",
"PORT": "8282",
"REPO_WHITELIST": "override,override",
"REQUIRE_APPROVAL": "false",
"SSL_CERT_FILE": "override-cert-file",
"SSL_KEY_FILE": "override-key-file",
} {
os.Setenv("ATLANTIS_"+name, value) // nolint: errcheck
}
@@ -581,8 +593,10 @@ ssl-key-file: key-file
Equals(t, "override-url", passedConfig.AtlantisURL)
Equals(t, false, passedConfig.AllowForkPRs)
Equals(t, false, passedConfig.AllowRepoConfig)
Equals(t, "https://override-bitbucket-base-url", passedConfig.BitbucketBaseURL)
Equals(t, "override-bitbucket-token", passedConfig.BitbucketToken)
Equals(t, "override-bitbucket-user", passedConfig.BitbucketUser)
Equals(t, "override-bitbucket-secret", passedConfig.BitbucketWebhookSecret)
Equals(t, "/override-path", passedConfig.DataDir)
Equals(t, "override-gh-hostname", passedConfig.GithubHostname)
Equals(t, "override-gh-token", passedConfig.GithubToken)
@@ -606,8 +620,10 @@ func TestExecute_FlagConfigOverride(t *testing.T) {
atlantis-url: "url"
allow-fork-prs: true
allow-repo-config: true
bitbucket-base-url: "https://bitbucket-base-url"
bitbucket-token: "bitbucket-token"
bitbucket-user: "bitbucket-user"
bitbucket-webhook-secret: "bitbucket-secret"
data-dir: "/path"
gh-hostname: "ghhostname"
gh-token: "token"
@@ -627,33 +643,37 @@ ssl-key-file: key-file
defer os.Remove(tmpFile) // nolint: errcheck
c := setup(map[string]interface{}{
cmd.AtlantisURLFlag: "override-url",
cmd.AllowForkPRsFlag: false,
cmd.AllowRepoConfigFlag: false,
cmd.BitbucketTokenFlag: "override-bitbucket-token",
cmd.BitbucketUserFlag: "override-bitbucket-user",
cmd.DataDirFlag: "/override-path",
cmd.GHHostnameFlag: "override-gh-hostname",
cmd.GHTokenFlag: "override-gh-token",
cmd.GHUserFlag: "override-gh-user",
cmd.GHWebhookSecret: "override-gh-webhook-secret",
cmd.GitlabHostnameFlag: "override-gitlab-hostname",
cmd.GitlabTokenFlag: "override-gitlab-token",
cmd.GitlabUserFlag: "override-gitlab-user",
cmd.GitlabWebhookSecret: "override-gitlab-webhook-secret",
cmd.LogLevelFlag: "info",
cmd.PortFlag: 8282,
cmd.RepoWhitelistFlag: "override,override",
cmd.RequireApprovalFlag: false,
cmd.SSLCertFileFlag: "override-cert-file",
cmd.SSLKeyFileFlag: "override-key-file",
cmd.AtlantisURLFlag: "override-url",
cmd.AllowForkPRsFlag: false,
cmd.AllowRepoConfigFlag: false,
cmd.BitbucketBaseURLFlag: "https://override-bitbucket-base-url",
cmd.BitbucketTokenFlag: "override-bitbucket-token",
cmd.BitbucketUserFlag: "override-bitbucket-user",
cmd.BitbucketWebhookSecretFlag: "override-bitbucket-secret",
cmd.DataDirFlag: "/override-path",
cmd.GHHostnameFlag: "override-gh-hostname",
cmd.GHTokenFlag: "override-gh-token",
cmd.GHUserFlag: "override-gh-user",
cmd.GHWebhookSecretFlag: "override-gh-webhook-secret",
cmd.GitlabHostnameFlag: "override-gitlab-hostname",
cmd.GitlabTokenFlag: "override-gitlab-token",
cmd.GitlabUserFlag: "override-gitlab-user",
cmd.GitlabWebhookSecretFlag: "override-gitlab-webhook-secret",
cmd.LogLevelFlag: "info",
cmd.PortFlag: 8282,
cmd.RepoWhitelistFlag: "override,override",
cmd.RequireApprovalFlag: false,
cmd.SSLCertFileFlag: "override-cert-file",
cmd.SSLKeyFileFlag: "override-key-file",
})
err := c.Execute()
Ok(t, err)
Equals(t, "override-url", passedConfig.AtlantisURL)
Equals(t, false, passedConfig.AllowForkPRs)
Equals(t, "https://override-bitbucket-base-url", passedConfig.BitbucketBaseURL)
Equals(t, "override-bitbucket-token", passedConfig.BitbucketToken)
Equals(t, "override-bitbucket-user", passedConfig.BitbucketUser)
Equals(t, "override-bitbucket-secret", passedConfig.BitbucketWebhookSecret)
Equals(t, "/override-path", passedConfig.DataDir)
Equals(t, "override-gh-hostname", passedConfig.GithubHostname)
Equals(t, "override-gh-token", passedConfig.GithubToken)
@@ -674,52 +694,63 @@ ssl-key-file: key-file
func TestExecute_FlagEnvVarOverride(t *testing.T) {
t.Log("Flags should override environment variables.")
for name, value := range map[string]string{
"ATLANTIS_URL": "url",
"ALLOW_FORK_PRS": "true",
"ALLOW_REPO_CONFIG": "true",
"BITBUCKET_TOKEN": "bitbucket-token",
"BITBUCKET_USER": "bitbucket-user",
"DATA_DIR": "/path",
"GH_HOSTNAME": "gh-hostname",
"GH_TOKEN": "gh-token",
"GH_USER": "gh-user",
"GH_WEBHOOK_SECRET": "gh-webhook-secret",
"GITLAB_HOSTNAME": "gitlab-hostname",
"GITLAB_TOKEN": "gitlab-token",
"GITLAB_USER": "gitlab-user",
"GITLAB_WEBHOOK_SECRET": "gitlab-webhook-secret",
"LOG_LEVEL": "debug",
"PORT": "8181",
"REPO_WHITELIST": "*",
"REQUIRE_APPROVAL": "true",
"SSL_CERT_FILE": "cert-file",
"SSL_KEY_FILE": "key-file",
} {
envVars := map[string]string{
"ATLANTIS_URL": "url",
"ALLOW_FORK_PRS": "true",
"ALLOW_REPO_CONFIG": "true",
"BITBUCKET_BASE_URL": "https://bitbucket-base-url",
"BITBUCKET_TOKEN": "bitbucket-token",
"BITBUCKET_USER": "bitbucket-user",
"BITBUCKET_WEBHOOK_SECRET": "bitbucket-secret",
"DATA_DIR": "/path",
"GH_HOSTNAME": "gh-hostname",
"GH_TOKEN": "gh-token",
"GH_USER": "gh-user",
"GH_WEBHOOK_SECRET": "gh-webhook-secret",
"GITLAB_HOSTNAME": "gitlab-hostname",
"GITLAB_TOKEN": "gitlab-token",
"GITLAB_USER": "gitlab-user",
"GITLAB_WEBHOOK_SECRET": "gitlab-webhook-secret",
"LOG_LEVEL": "debug",
"PORT": "8181",
"REPO_WHITELIST": "*",
"REQUIRE_APPROVAL": "true",
"SSL_CERT_FILE": "cert-file",
"SSL_KEY_FILE": "key-file",
}
for name, value := range envVars {
os.Setenv("ATLANTIS_"+name, value) // nolint: errcheck
}
defer func() {
// Unset after this test finishes.
for name := range envVars {
os.Unsetenv("ATLANTIS_" + name) // nolint: errcheck
}
}()
c := setup(map[string]interface{}{
cmd.AtlantisURLFlag: "override-url",
cmd.AllowForkPRsFlag: false,
cmd.AllowRepoConfigFlag: false,
cmd.BitbucketTokenFlag: "override-bitbucket-token",
cmd.BitbucketUserFlag: "override-bitbucket-user",
cmd.DataDirFlag: "/override-path",
cmd.GHHostnameFlag: "override-gh-hostname",
cmd.GHTokenFlag: "override-gh-token",
cmd.GHUserFlag: "override-gh-user",
cmd.GHWebhookSecret: "override-gh-webhook-secret",
cmd.GitlabHostnameFlag: "override-gitlab-hostname",
cmd.GitlabTokenFlag: "override-gitlab-token",
cmd.GitlabUserFlag: "override-gitlab-user",
cmd.GitlabWebhookSecret: "override-gitlab-webhook-secret",
cmd.LogLevelFlag: "info",
cmd.PortFlag: 8282,
cmd.RepoWhitelistFlag: "override,override",
cmd.RequireApprovalFlag: false,
cmd.SSLCertFileFlag: "override-cert-file",
cmd.SSLKeyFileFlag: "override-key-file",
cmd.AtlantisURLFlag: "override-url",
cmd.AllowForkPRsFlag: false,
cmd.AllowRepoConfigFlag: false,
cmd.BitbucketBaseURLFlag: "https://override-bitbucket-base-url",
cmd.BitbucketTokenFlag: "override-bitbucket-token",
cmd.BitbucketUserFlag: "override-bitbucket-user",
cmd.BitbucketWebhookSecretFlag: "override-bitbucket-secret",
cmd.DataDirFlag: "/override-path",
cmd.GHHostnameFlag: "override-gh-hostname",
cmd.GHTokenFlag: "override-gh-token",
cmd.GHUserFlag: "override-gh-user",
cmd.GHWebhookSecretFlag: "override-gh-webhook-secret",
cmd.GitlabHostnameFlag: "override-gitlab-hostname",
cmd.GitlabTokenFlag: "override-gitlab-token",
cmd.GitlabUserFlag: "override-gitlab-user",
cmd.GitlabWebhookSecretFlag: "override-gitlab-webhook-secret",
cmd.LogLevelFlag: "info",
cmd.PortFlag: 8282,
cmd.RepoWhitelistFlag: "override,override",
cmd.RequireApprovalFlag: false,
cmd.SSLCertFileFlag: "override-cert-file",
cmd.SSLKeyFileFlag: "override-key-file",
})
err := c.Execute()
Ok(t, err)
@@ -727,8 +758,10 @@ func TestExecute_FlagEnvVarOverride(t *testing.T) {
Equals(t, "override-url", passedConfig.AtlantisURL)
Equals(t, false, passedConfig.AllowForkPRs)
Equals(t, false, passedConfig.AllowRepoConfig)
Equals(t, "https://override-bitbucket-base-url", passedConfig.BitbucketBaseURL)
Equals(t, "override-bitbucket-token", passedConfig.BitbucketToken)
Equals(t, "override-bitbucket-user", passedConfig.BitbucketUser)
Equals(t, "override-bitbucket-secret", passedConfig.BitbucketWebhookSecret)
Equals(t, "/override-path", passedConfig.DataDir)
Equals(t, "override-gh-hostname", passedConfig.GithubHostname)
Equals(t, "override-gh-token", passedConfig.GithubToken)
@@ -746,6 +779,49 @@ func TestExecute_FlagEnvVarOverride(t *testing.T) {
Equals(t, "override-key-file", passedConfig.SSLKeyFile)
}
// If using bitbucket cloud, webhook secrets are not supported.
func TestExecute_BitbucketCloudWithWebhookSecret(t *testing.T) {
c := setup(map[string]interface{}{
cmd.BitbucketUserFlag: "user",
cmd.BitbucketTokenFlag: "token",
cmd.RepoWhitelistFlag: "*",
cmd.BitbucketWebhookSecretFlag: "my secret",
})
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{}{
cmd.BitbucketUserFlag: "user",
cmd.BitbucketTokenFlag: "token",
cmd.RepoWhitelistFlag: "*",
cmd.BitbucketBaseURLFlag: "mydomain.com",
})
ErrEquals(t, "--bitbucket-base-url must have http:// or https://, got \"mydomain.com\"", c.Execute())
c = setup(map[string]interface{}{
cmd.BitbucketUserFlag: "user",
cmd.BitbucketTokenFlag: "token",
cmd.RepoWhitelistFlag: "*",
cmd.BitbucketBaseURLFlag: "://mydomain.com",
})
ErrEquals(t, "error parsing --bitbucket-webhook-secret flag value \"://mydomain.com\": parse ://mydomain.com: missing protocol scheme", c.Execute())
}
// Port should be retained on base url.
func TestExecute_BitbucketServerBaseURLPort(t *testing.T) {
c := setup(map[string]interface{}{
cmd.BitbucketUserFlag: "user",
cmd.BitbucketTokenFlag: "token",
cmd.RepoWhitelistFlag: "*",
cmd.BitbucketBaseURLFlag: "http://mydomain.com:7990",
})
Ok(t, c.Execute())
Equals(t, "http://mydomain.com:7990", passedConfig.BitbucketBaseURL)
}
func setup(flags map[string]interface{}) *cobra.Command {
vipr := viper.New()
for k, v := range flags {

View File

@@ -3,4 +3,4 @@
// APIs.
package bitbucketcloud
const Hostname = "bitbucket.org"
const BaseURL = "https://api.bitbucket.org"

View File

@@ -0,0 +1,71 @@
package bitbucketserver
import (
"crypto/hmac"
"crypto/sha1"
"crypto/sha256"
"crypto/sha512"
"encoding/hex"
"fmt"
"hash"
"strings"
"github.com/pkg/errors"
)
// Attribution: This code is taken from https://github.com/google/go-github.
func ValidateSignature(payload []byte, signature string, secretKey []byte) error {
messageMAC, hashFunc, err := messageMAC(signature)
if err != nil {
return err
}
if !checkMAC(payload, messageMAC, secretKey, hashFunc) {
return errors.New("payload signature check failed")
}
return nil
}
// genMAC generates the HMAC signature for a message provided the secret key
// and hashFunc.
func genMAC(message, key []byte, hashFunc func() hash.Hash) []byte {
mac := hmac.New(hashFunc, key)
mac.Write(message)
return mac.Sum(nil)
}
// checkMAC reports whether messageMAC is a valid HMAC tag for message.
func checkMAC(message, messageMAC, key []byte, hashFunc func() hash.Hash) bool {
expectedMAC := genMAC(message, key, hashFunc)
return hmac.Equal(messageMAC, expectedMAC)
}
// messageMAC returns the hex-decoded HMAC tag from the signature and its
// corresponding hash function.
func messageMAC(signature string) ([]byte, func() hash.Hash, error) {
if signature == "" {
return nil, nil, errors.New("missing signature")
}
sigParts := strings.SplitN(signature, "=", 2)
if len(sigParts) != 2 {
return nil, nil, fmt.Errorf("error parsing signature %q", signature)
}
var hashFunc func() hash.Hash
switch sigParts[0] {
case "sha1":
hashFunc = sha1.New
case "sha256":
hashFunc = sha256.New
case "sha512":
hashFunc = sha512.New
default:
return nil, nil, fmt.Errorf("unknown hash type prefix: %q", sigParts[0])
}
buf, err := hex.DecodeString(sigParts[1])
if err != nil {
return nil, nil, fmt.Errorf("error decoding signature %q: %v", signature, err)
}
return buf, hashFunc, nil
}

View File

@@ -20,6 +20,7 @@ import (
"github.com/google/go-github/github"
"github.com/lkysow/go-gitlab"
"github.com/pkg/errors"
"github.com/runatlantis/atlantis/server/events"
"github.com/runatlantis/atlantis/server/events/models"
"github.com/runatlantis/atlantis/server/events/vcs"
@@ -35,6 +36,7 @@ const gitlabHeader = "X-Gitlab-Event"
const bitbucketEventTypeHeader = "X-Event-Key"
const bitbucketCloudRequestIDHeader = "X-Request-UUID"
const bitbucketServerRequestIDHeader = "X-Request-ID"
const bitbucketServerSignatureHeader = "X-Hub-Signature"
// EventsController handles all webhook requests which signify 'events' in the
// VCS host, ex. GitHub.
@@ -60,6 +62,10 @@ type EventsController struct {
SupportedVCSHosts []models.VCSHostType
VCSClient vcs.ClientProxy
TestingMode bool
// BitbucketWebhookSecret is the secret added to this webhook via the Bitbucket
// UI that identifies this call as coming from Bitbucket. If empty, no
// request validation is done.
BitbucketWebhookSecret []byte
}
// Post handles POST webhook requests.
@@ -153,12 +159,19 @@ func (e *EventsController) handleBitbucketCloudPost(w http.ResponseWriter, r *ht
func (e *EventsController) handleBitbucketServerPost(w http.ResponseWriter, r *http.Request) {
eventType := r.Header.Get(bitbucketEventTypeHeader)
reqID := r.Header.Get(bitbucketServerRequestIDHeader)
sig := r.Header.Get(bitbucketServerSignatureHeader)
defer r.Body.Close() // nolint: errcheck
body, err := ioutil.ReadAll(r.Body)
if err != nil {
e.respond(w, logging.Error, http.StatusBadRequest, "Unable to read body: %s %s=%s", err, bitbucketServerRequestIDHeader, reqID)
return
}
if len(e.BitbucketWebhookSecret) > 0 {
if err := bitbucketserver.ValidateSignature(body, sig, e.BitbucketWebhookSecret); err != nil {
e.respond(w, logging.Warn, http.StatusBadRequest, errors.Wrap(err, "request did not pass validation").Error())
return
}
}
switch eventType {
case bitbucketserver.PullCreatedHeader, bitbucketserver.PullFulfilledHeader, bitbucketserver.PullDeclinedHeader:
e.Logger.Debug("handling as pull request state changed event")

View File

@@ -82,24 +82,25 @@ type Server struct {
// The mapstructure tags correspond to flags in cmd/server.go and are used when
// the config is parsed from a YAML file.
type UserConfig struct {
AllowForkPRs bool `mapstructure:"allow-fork-prs"`
AllowRepoConfig bool `mapstructure:"allow-repo-config"`
AtlantisURL string `mapstructure:"atlantis-url"`
BitbucketHostname string `mapstructure:"bitbucket-hostname"`
BitbucketToken string `mapstructure:"bitbucket-token"`
BitbucketUser string `mapstructure:"bitbucket-user"`
DataDir string `mapstructure:"data-dir"`
GithubHostname string `mapstructure:"gh-hostname"`
GithubToken string `mapstructure:"gh-token"`
GithubUser string `mapstructure:"gh-user"`
GithubWebhookSecret string `mapstructure:"gh-webhook-secret"`
GitlabHostname string `mapstructure:"gitlab-hostname"`
GitlabToken string `mapstructure:"gitlab-token"`
GitlabUser string `mapstructure:"gitlab-user"`
GitlabWebhookSecret string `mapstructure:"gitlab-webhook-secret"`
LogLevel string `mapstructure:"log-level"`
Port int `mapstructure:"port"`
RepoWhitelist string `mapstructure:"repo-whitelist"`
AllowForkPRs bool `mapstructure:"allow-fork-prs"`
AllowRepoConfig bool `mapstructure:"allow-repo-config"`
AtlantisURL string `mapstructure:"atlantis-url"`
BitbucketBaseURL string `mapstructure:"bitbucket-base-url"`
BitbucketToken string `mapstructure:"bitbucket-token"`
BitbucketUser string `mapstructure:"bitbucket-user"`
BitbucketWebhookSecret string `mapstructure:"bitbucket-webhook-secret"`
DataDir string `mapstructure:"data-dir"`
GithubHostname string `mapstructure:"gh-hostname"`
GithubToken string `mapstructure:"gh-token"`
GithubUser string `mapstructure:"gh-user"`
GithubWebhookSecret string `mapstructure:"gh-webhook-secret"`
GitlabHostname string `mapstructure:"gitlab-hostname"`
GitlabToken string `mapstructure:"gitlab-token"`
GitlabUser string `mapstructure:"gitlab-user"`
GitlabWebhookSecret string `mapstructure:"gitlab-webhook-secret"`
LogLevel string `mapstructure:"log-level"`
Port int `mapstructure:"port"`
RepoWhitelist string `mapstructure:"repo-whitelist"`
// RequireApproval is whether to require pull request approval before
// allowing terraform apply's to be run.
RequireApproval bool `mapstructure:"require-approval"`
@@ -170,7 +171,7 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
}
}
if userConfig.BitbucketUser != "" {
if userConfig.BitbucketHostname == bitbucketcloud.Hostname {
if userConfig.BitbucketBaseURL == bitbucketcloud.BaseURL {
supportedVCSHosts = append(supportedVCSHosts, models.BitbucketCloud)
bitbucketCloudClient = bitbucketcloud.NewClient(
http.DefaultClient,
@@ -184,7 +185,7 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
http.DefaultClient,
userConfig.BitbucketUser,
userConfig.BitbucketToken,
userConfig.BitbucketHostname,
userConfig.BitbucketBaseURL,
userConfig.AtlantisURL)
if err != nil {
return nil, errors.Wrapf(err, "setting up Bitbucket Server client")
@@ -248,7 +249,7 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
GitlabToken: userConfig.GitlabToken,
BitbucketUser: userConfig.BitbucketUser,
BitbucketToken: userConfig.BitbucketToken,
BitbucketServerURL: userConfig.BitbucketHostname,
BitbucketServerURL: userConfig.BitbucketBaseURL,
}
commentParser := &events.CommentParser{
GithubUser: userConfig.GithubUser,
@@ -326,6 +327,7 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) {
RepoWhitelistChecker: repoWhitelist,
SupportedVCSHosts: supportedVCSHosts,
VCSClient: vcsClient,
BitbucketWebhookSecret: []byte(userConfig.BitbucketWebhookSecret),
}
return &Server{
AtlantisVersion: config.AtlantisVersion,