Skip to content

Commit

Permalink
Merge pull request #711 from ImperialXT/master
Browse files Browse the repository at this point in the history
Create .git-credentials to allow secure auth when cloning private repos
  • Loading branch information
lkysow authored Aug 6, 2019
2 parents 5f9412b + 6120974 commit 3ebebe5
Show file tree
Hide file tree
Showing 7 changed files with 203 additions and 0 deletions.
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

0 comments on commit 3ebebe5

Please sign in to comment.