From e1408234080dcf9f6e9f96399ce5ee2708b0efe6 Mon Sep 17 00:00:00 2001 From: Andrew Jeffree Date: Thu, 18 Jul 2019 13:20:11 +1000 Subject: [PATCH 1/2] Add flag for configuring git-credentials-store --write-git-creds will create a .git-credentials file and configure git to use it. To allow authentication with your git remotes over https. To access private repos. --- cmd/server.go | 6 ++ cmd/server_test.go | 14 +++ runatlantis.io/docs/server-configuration.md | 10 +++ server/events/git_cred_writer.go | 51 +++++++++++ server/events/git_cred_writer_test.go | 98 +++++++++++++++++++++ server/server.go | 23 +++++ server/user_config.go | 1 + 7 files changed, 203 insertions(+) create mode 100644 server/events/git_cred_writer.go create mode 100644 server/events/git_cred_writer_test.go diff --git a/cmd/server.go b/cmd/server.go index 497b48e8a7..5f2ccd14a7 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -70,6 +70,7 @@ const ( SSLKeyFileFlag = "ssl-key-file" TFEHostnameFlag = "tfe-hostname" TFETokenFlag = "tfe-token" + WriteGitCredsFlag = "write-git-creds" // Flag defaults. // NOTE: Must manually set these as defaults in the setDefaults function. @@ -227,6 +228,11 @@ var boolFlags = map[string]boolFlag{ description: "Silences the posting of whitelist error comments.", defaultValue: false, }, + WriteGitCredsFlag: { + description: "Write out a .git-credentials file with the provider user and token to allow authentication with git over HTTPS." + + " This does write secrets to disk and should only be enabled in a secure environment.", + defaultValue: false, + }, } var intFlags = map[string]intFlag{ PortFlag: { diff --git a/cmd/server_test.go b/cmd/server_test.go index 0f06c55a06..e80af58e10 100644 --- a/cmd/server_test.go +++ b/cmd/server_test.go @@ -364,6 +364,7 @@ func TestExecute_Defaults(t *testing.T) { Equals(t, "", passedConfig.SSLKeyFile) Equals(t, "app.terraform.io", passedConfig.TFEHostname) Equals(t, "", passedConfig.TFEToken) + Equals(t, false, passedConfig.WriteGitCreds) } func TestExecute_ExpandHomeInDataDir(t *testing.T) { @@ -469,6 +470,7 @@ func TestExecute_Flags(t *testing.T) { cmd.SSLKeyFileFlag: "key-file", cmd.TFEHostnameFlag: "my-hostname", cmd.TFETokenFlag: "my-token", + cmd.WriteGitCredsFlag: true, }) err := c.Execute() Ok(t, err) @@ -503,6 +505,7 @@ func TestExecute_Flags(t *testing.T) { Equals(t, "key-file", passedConfig.SSLKeyFile) Equals(t, "my-hostname", passedConfig.TFEHostname) Equals(t, "my-token", passedConfig.TFEToken) + Equals(t, true, passedConfig.WriteGitCreds) } func TestExecute_ConfigFile(t *testing.T) { @@ -538,6 +541,7 @@ ssl-cert-file: cert-file ssl-key-file: key-file tfe-hostname: my-hostname tfe-token: my-token +write-git-creds: true `) defer os.Remove(tmpFile) // nolint: errcheck c := setup(map[string]interface{}{ @@ -576,6 +580,7 @@ tfe-token: my-token Equals(t, "key-file", passedConfig.SSLKeyFile) Equals(t, "my-hostname", passedConfig.TFEHostname) Equals(t, "my-token", passedConfig.TFEToken) + Equals(t, true, passedConfig.WriteGitCreds) } func TestExecute_EnvironmentOverride(t *testing.T) { @@ -610,6 +615,7 @@ ssl-cert-file: cert-file ssl-key-file: key-file tfe-hostname: my-hostname tfe-token: my-token +write-git-creds: true `) defer os.Remove(tmpFile) // nolint: errcheck @@ -645,6 +651,7 @@ tfe-token: my-token "SSL_KEY_FILE": "override-key-file", "TFE_HOSTNAME": "override-my-hostname", "TFE_TOKEN": "override-my-token", + "WRITE_GIT_CREDS": "false", } { os.Setenv("ATLANTIS_"+name, value) // nolint: errcheck } @@ -683,6 +690,7 @@ tfe-token: my-token Equals(t, "override-key-file", passedConfig.SSLKeyFile) Equals(t, "override-my-hostname", passedConfig.TFEHostname) Equals(t, "override-my-token", passedConfig.TFEToken) + Equals(t, false, passedConfig.WriteGitCreds) } func TestExecute_FlagConfigOverride(t *testing.T) { @@ -718,6 +726,7 @@ ssl-cert-file: cert-file ssl-key-file: key-file tfe-hostname: my-hostname tfe-token: my-token +write-git-creds: true `) defer os.Remove(tmpFile) // nolint: errcheck @@ -752,6 +761,7 @@ tfe-token: my-token cmd.SSLKeyFileFlag: "override-key-file", cmd.TFEHostnameFlag: "override-my-hostname", cmd.TFETokenFlag: "override-my-token", + cmd.WriteGitCredsFlag: false, }) err := c.Execute() Ok(t, err) @@ -784,6 +794,7 @@ tfe-token: my-token Equals(t, "override-key-file", passedConfig.SSLKeyFile) Equals(t, "override-my-hostname", passedConfig.TFEHostname) Equals(t, "override-my-token", passedConfig.TFEToken) + Equals(t, false, passedConfig.WriteGitCreds) } @@ -821,6 +832,7 @@ func TestExecute_FlagEnvVarOverride(t *testing.T) { "SSL_KEY_FILE": "key-file", "TFE_HOSTNAME": "my-hostname", "TFE_TOKEN": "my-token", + "WRITE_GIT_CREDS": "true", } for name, value := range envVars { os.Setenv("ATLANTIS_"+name, value) // nolint: errcheck @@ -863,6 +875,7 @@ func TestExecute_FlagEnvVarOverride(t *testing.T) { cmd.SSLKeyFileFlag: "override-key-file", cmd.TFEHostnameFlag: "override-my-hostname", cmd.TFETokenFlag: "override-my-token", + cmd.WriteGitCredsFlag: false, }) err := c.Execute() Ok(t, err) @@ -897,6 +910,7 @@ func TestExecute_FlagEnvVarOverride(t *testing.T) { Equals(t, "override-key-file", passedConfig.SSLKeyFile) Equals(t, "override-my-hostname", passedConfig.TFEHostname) Equals(t, "override-my-token", passedConfig.TFEToken) + Equals(t, false, passedConfig.WriteGitCreds) } // If using bitbucket cloud, webhook secrets are not supported. diff --git a/runatlantis.io/docs/server-configuration.md b/runatlantis.io/docs/server-configuration.md index 1a5131d2f5..32d48e562c 100644 --- a/runatlantis.io/docs/server-configuration.md +++ b/runatlantis.io/docs/server-configuration.md @@ -402,3 +402,13 @@ Values are chosen in this order: ATLANTIS_TFE_TOKEN='xxx.atlasv1.yyy' atlantis server ``` A token for Terraform Cloud/Terraform Enteprise integration. See [Terraform Cloud](terraform-cloud.html) for more details. + +* ### `--write-git-creds` + ```bash + atlantis server --write-git-creds + ``` + Write out a .git-credentials file and configure git-credentials-store. To allow authentication with your git remotes over https. See [here](https://git-scm.com/docs/git-credential-store) for more information. + + ::: warning SECURITY WARNING + Potentially dangerous to enable + This writes your credentials to disk and a malicious pull request could then access these details diff --git a/server/events/git_cred_writer.go b/server/events/git_cred_writer.go new file mode 100644 index 0000000000..c093f92c49 --- /dev/null +++ b/server/events/git_cred_writer.go @@ -0,0 +1,51 @@ +package events + +import ( + "fmt" + "github.com/pkg/errors" + "github.com/runatlantis/atlantis/server/logging" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "strings" +) + +// WriteGitCreds generates a .git-credentials file containing the username and token +// used for authenticating with git over HTTPS +// It will create the file in home/.git-credentials +func WriteGitCreds(gitUser string, gitToken string, gitHostname string, home string, logger *logging.SimpleLogger) error { + const credsFilename = ".git-credentials" + credsFile := filepath.Join(home, credsFilename) + credsFileContents := `https://%s:%s@%s` + config := fmt.Sprintf(credsFileContents, gitUser, gitToken, gitHostname) + + // If there is already a .git-credentials file and its contents aren't exactly + // what we would have written to it, then we error out because we don't + // want to overwrite anything + if _, err := os.Stat(credsFile); err == nil { + currContents, err := ioutil.ReadFile(credsFile) // nolint: gosec + if err != nil { + return errors.Wrapf(err, "trying to read %s to ensure we're not overwriting it", credsFile) + } + if config != string(currContents) { + return fmt.Errorf("can't write git-credentials to %s because that file has contents that would be overwritten", credsFile) + } + // Otherwise we don't need to write the file because it already has + // what we need. + return nil + } + + if err := ioutil.WriteFile(credsFile, []byte(config), 0600); err != nil { + return errors.Wrapf(err, "writing generated %s file with user, token and hostname to %s", credsFilename, credsFile) + } + + logger.Info("wrote git credentials to %s", credsFile) + + cmd := exec.Command("git", "config", "--global", "credential.helper", "store") + if out, err := cmd.CombinedOutput(); err != nil { + return errors.Wrapf(err, "There was an error running %s: %s", strings.Join(cmd.Args, " "), string(out)) + } + logger.Info("successfully ran %s", strings.Join(cmd.Args, " ")) + return nil +} diff --git a/server/events/git_cred_writer_test.go b/server/events/git_cred_writer_test.go new file mode 100644 index 0000000000..d4c0033597 --- /dev/null +++ b/server/events/git_cred_writer_test.go @@ -0,0 +1,98 @@ +package events_test + +import ( + "fmt" + "io/ioutil" + "os/exec" + "path/filepath" + "testing" + + "github.com/runatlantis/atlantis/server/events" + "github.com/runatlantis/atlantis/server/logging" + . "github.com/runatlantis/atlantis/testing" +) + +var logger *logging.SimpleLogger + +// Test that we write the file as expected +func TestWriteGitCreds_WriteFile(t *testing.T) { + tmp, cleanup := TempDir(t) + defer cleanup() + + err := events.WriteGitCreds("user", "token", "hostname", tmp, logger) + Ok(t, err) + + expContents := `https://user:token@hostname` + + actContents, err := ioutil.ReadFile(filepath.Join(tmp, ".git-credentials")) + Ok(t, err) + Equals(t, expContents, string(actContents)) +} + +// Test that if the file already exists and its contents will be modified if +// we write our config that we error out +func TestWriteGitCreds_WillNotOverwrite(t *testing.T) { + tmp, cleanup := TempDir(t) + defer cleanup() + + credsFile := filepath.Join(tmp, ".git-credentials") + err := ioutil.WriteFile(credsFile, []byte("contents"), 0600) + Ok(t, err) + + actErr := events.WriteGitCreds("user", "token", "hostname", tmp, logger) + expErr := fmt.Sprintf("can't write git-credentials to %s because that file has contents that would be overwritten", tmp+"/.git-credentials") + ErrEquals(t, expErr, actErr) +} + +// Test that if the file already exists and its contents will NOT be modified if +// we write our config that we don't error. +func TestWriteGitCreds_NoErrIfContentsSame(t *testing.T) { + tmp, cleanup := TempDir(t) + defer cleanup() + + credsFile := filepath.Join(tmp, ".git-credentials") + contents := `https://user:token@hostname` + + err := ioutil.WriteFile(credsFile, []byte(contents), 0600) + Ok(t, err) + + err = events.WriteGitCreds("user", "token", "hostname", tmp, logger) + Ok(t, err) +} + +// Test that if we can't read the existing file to see if the contents will be +// the same that we just error out. +func TestWriteGitCreds_ErrIfCannotRead(t *testing.T) { + tmp, cleanup := TempDir(t) + defer cleanup() + + credsFile := filepath.Join(tmp, ".git-credentials") + err := ioutil.WriteFile(credsFile, []byte("can't see me!"), 0000) + Ok(t, err) + + expErr := fmt.Sprintf("trying to read %s to ensure we're not overwriting it: open %s: permission denied", credsFile, credsFile) + actErr := events.WriteGitCreds("user", "token", "hostname", tmp, logger) + ErrEquals(t, expErr, actErr) +} + +// Test that if we can't write, we error out. +func TestWriteGitCreds_ErrIfCannotWrite(t *testing.T) { + credsFile := "/this/dir/does/not/exist/.git-credentials" + expErr := fmt.Sprintf("writing generated .git-credentials file with user, token and hostname to %s: open %s: no such file or directory", credsFile, credsFile) + actErr := events.WriteGitCreds("user", "token", "hostname", "/this/dir/does/not/exist", logger) + ErrEquals(t, expErr, actErr) +} + +// Test that git is actually configured to use the credentials +func TestWriteGitCreds_ConfigureGit(t *testing.T) { + tmp, cleanup := TempDir(t) + defer cleanup() + + err := events.WriteGitCreds("user", "token", "hostname", tmp, logger) + Ok(t, err) + + expOutput := `store` + actOutput, err := exec.Command("git", "config", "--global", "credential.helper").Output() + Ok(t, err) + Equals(t, expOutput+"\n", string(actOutput)) +} diff --git a/server/server.go b/server/server.go index 1cbbfae725..744fac704f 100644 --- a/server/server.go +++ b/server/server.go @@ -29,6 +29,7 @@ import ( "syscall" "time" + "github.com/mitchellh/go-homedir" "github.com/runatlantis/atlantis/server/events/db" "github.com/runatlantis/atlantis/server/events/yaml/valid" @@ -152,6 +153,28 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { } } + if userConfig.WriteGitCreds { + home, err := homedir.Dir() + if err != nil { + return nil, errors.Wrap(err, "getting home dir to write ~/.git-credentials file") + } + if userConfig.GithubUser != "" { + if err := events.WriteGitCreds(userConfig.GithubUser, userConfig.GithubToken, userConfig.GithubHostname, home, logger); err != nil { + return nil, err + } + } + if userConfig.GitlabUser != "" { + if err := events.WriteGitCreds(userConfig.GitlabUser, userConfig.GitlabToken, userConfig.GitlabHostname, home, logger); err != nil { + return nil, err + } + } + if userConfig.BitbucketUser != "" { + if err := events.WriteGitCreds(userConfig.BitbucketUser, userConfig.BitbucketToken, userConfig.BitbucketBaseURL, home, logger); err != nil { + return nil, err + } + } + } + var webhooksConfig []webhooks.Config for _, c := range userConfig.Webhooks { config := webhooks.Config{ diff --git a/server/user_config.go b/server/user_config.go index 9f7f59d386..c6c194d428 100644 --- a/server/user_config.go +++ b/server/user_config.go @@ -44,6 +44,7 @@ type UserConfig struct { TFEToken string `mapstructure:"tfe-token"` DefaultTFVersion string `mapstructure:"default-tf-version"` Webhooks []WebhookConfig `mapstructure:"webhooks"` + WriteGitCreds bool `mapstructure:"write-git-creds"` } // ToLogLevel returns the LogLevel object corresponding to the user-passed From 6120974991e416605520dc8635fd5af0e86cc69a Mon Sep 17 00:00:00 2001 From: Luke Kysow <1034429+lkysow@users.noreply.github.com> Date: Tue, 6 Aug 2019 12:17:12 +0100 Subject: [PATCH 2/2] Update server-configuration.md --- runatlantis.io/docs/server-configuration.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/runatlantis.io/docs/server-configuration.md b/runatlantis.io/docs/server-configuration.md index 32d48e562c..612fcc56ae 100644 --- a/runatlantis.io/docs/server-configuration.md +++ b/runatlantis.io/docs/server-configuration.md @@ -410,5 +410,5 @@ Values are chosen in this order: Write out a .git-credentials file and configure git-credentials-store. To allow authentication with your git remotes over https. See [here](https://git-scm.com/docs/git-credential-store) for more information. ::: warning SECURITY WARNING - Potentially dangerous to enable - This writes your credentials to disk and a malicious pull request could then access these details + Potentially dangerous to enable as this writes your credentials to disk. + :::