Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create .git-credentials to allow secure auth when cloning private repos #711

Merged
merged 2 commits into from
Aug 6, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions cmd/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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: {
Expand Down
14 changes: 14 additions & 0 deletions cmd/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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{}{
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)

}

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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.
Expand Down
10 changes: 10 additions & 0 deletions runatlantis.io/docs/server-configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 as this writes your credentials to disk.
:::
51 changes: 51 additions & 0 deletions server/events/git_cred_writer.go
Original file line number Diff line number Diff line change
@@ -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
}
98 changes: 98 additions & 0 deletions server/events/git_cred_writer_test.go
Original file line number Diff line number Diff line change
@@ -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))
}
23 changes: 23 additions & 0 deletions server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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{
Expand Down
1 change: 1 addition & 0 deletions server/user_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down