mirror of
https://git.vectorsigma.ru/public/atlantis.git
synced 2026-07-29 00:58:17 +00:00
Add webhook secret checking.
This commit is contained in:
100
cmd/server.go
100
cmd/server.go
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
// APIs.
|
||||
package bitbucketcloud
|
||||
|
||||
const Hostname = "bitbucket.org"
|
||||
const BaseURL = "https://api.bitbucket.org"
|
||||
|
||||
71
server/events/vcs/bitbucketserver/request_validation.go
Normal file
71
server/events/vcs/bitbucketserver/request_validation.go
Normal 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
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user