Skip to content

Commit

Permalink
Support loading git token from disk
Browse files Browse the repository at this point in the history
Signed-off-by: Henry Muru Paenga <[email protected]>
  • Loading branch information
meringu committed Nov 4, 2024
1 parent f23ab63 commit 3112e2b
Show file tree
Hide file tree
Showing 11 changed files with 142 additions and 54 deletions.
24 changes: 16 additions & 8 deletions cmd/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ const (
GHHostnameFlag = "gh-hostname"
GHTeamAllowlistFlag = "gh-team-allowlist"
GHTokenFlag = "gh-token"
GHTokenFileFlag = "gh-token-file" // nolint: gosec
GHUserFlag = "gh-user"
GHAppIDFlag = "gh-app-id"
GHAppKeyFlag = "gh-app-key"
Expand Down Expand Up @@ -315,6 +316,9 @@ var stringFlags = map[string]stringFlag{
GHTokenFlag: {
description: "GitHub token of API user. Can also be specified via the ATLANTIS_GH_TOKEN environment variable.",
},
GHTokenFileFlag: {
description: "A path to a file containing the GitHub token of API user. Can also be specified via the ATLANTIS_GH_TOKEN_FILE environment variable.",
},
GHAppKeyFlag: {
description: "The GitHub App's private key",
defaultValue: "",
Expand Down Expand Up @@ -954,26 +958,29 @@ func (s *ServerCmd) validate(userConfig server.UserConfig) error {
}

// The following combinations are valid.
// 1. github user and token set
// 1. github user and (token or token file)
// 2. github app ID and (key file set or key set)
// 3. gitea user and token set
// 4. gitlab user and token set
// 5. bitbucket user and token set
// 6. azuredevops user and token set
// 7. any combination of the above
vcsErr := fmt.Errorf("--%s/--%s or --%s/--%s or --%s/--%s or --%s/--%s or --%s/--%s or --%s/--%s or --%s/--%s must be set", GHUserFlag, GHTokenFlag, GHAppIDFlag, GHAppKeyFileFlag, GHAppIDFlag, GHAppKeyFlag, GiteaUserFlag, GiteaTokenFlag, GitlabUserFlag, GitlabTokenFlag, BitbucketUserFlag, BitbucketTokenFlag, ADUserFlag, ADTokenFlag)
if ((userConfig.GithubUser == "") != (userConfig.GithubToken == "")) ||
((userConfig.GiteaUser == "") != (userConfig.GiteaToken == "")) ||
vcsErr := fmt.Errorf("--%s/--%s or --%s/--%s or --%s/--%s or --%s/--%s or --%s/--%s or --%s/--%s or --%s/--%s or --%s/--%s must be set", GHUserFlag, GHTokenFlag, GHUserFlag, GHTokenFileFlag, GHAppIDFlag, GHAppKeyFileFlag, GHAppIDFlag, GHAppKeyFlag, GiteaUserFlag, GiteaTokenFlag, GitlabUserFlag, GitlabTokenFlag, BitbucketUserFlag, BitbucketTokenFlag, ADUserFlag, ADTokenFlag)
if ((userConfig.GiteaUser == "") != (userConfig.GiteaToken == "")) ||
((userConfig.GitlabUser == "") != (userConfig.GitlabToken == "")) ||
((userConfig.BitbucketUser == "") != (userConfig.BitbucketToken == "")) ||
((userConfig.AzureDevopsUser == "") != (userConfig.AzureDevopsToken == "")) {
return vcsErr
}
if (userConfig.GithubAppID != 0) && ((userConfig.GithubAppKey == "") && (userConfig.GithubAppKeyFile == "")) {
return vcsErr
if userConfig.GithubUser != "" {
if (userConfig.GithubToken == "") == (userConfig.GithubTokenFile == "") {
return vcsErr
}
}
if (userConfig.GithubAppID == 0) && ((userConfig.GithubAppKey != "") || (userConfig.GithubAppKeyFile != "")) {
return vcsErr
if userConfig.GithubAppID != 0 {
if (userConfig.GithubAppKey == "") == (userConfig.GithubAppKeyFile == "") {
return vcsErr
}
}
// At this point, we know that there can't be a single user/token without
// its partner, but we haven't checked if any user/token is set at all.
Expand Down Expand Up @@ -1015,6 +1022,7 @@ func (s *ServerCmd) validate(userConfig server.UserConfig) error {
// Warn if any tokens have newlines.
for name, token := range map[string]string{
GHTokenFlag: userConfig.GithubToken,
GHTokenFileFlag: userConfig.GithubTokenFile,
GHWebhookSecretFlag: userConfig.GithubWebhookSecret,
GitlabTokenFlag: userConfig.GitlabToken,
GitlabWebhookSecretFlag: userConfig.GitlabWebhookSecret,
Expand Down
20 changes: 19 additions & 1 deletion cmd/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ var testFlags = map[string]interface{}{
GHHostnameFlag: "ghhostname",
GHTeamAllowlistFlag: "",
GHTokenFlag: "token",
GHTokenFileFlag: "",
GHUserFlag: "user",
GHAppIDFlag: int64(0),
GHAppKeyFlag: "",
Expand Down Expand Up @@ -432,7 +433,7 @@ func TestExecute_ValidateSSLConfig(t *testing.T) {
}

func TestExecute_ValidateVCSConfig(t *testing.T) {
expErr := "--gh-user/--gh-token or --gh-app-id/--gh-app-key-file or --gh-app-id/--gh-app-key or --gitea-user/--gitea-token or --gitlab-user/--gitlab-token or --bitbucket-user/--bitbucket-token or --azuredevops-user/--azuredevops-token must be set"
expErr := "--gh-user/--gh-token or --gh-user/--gh-token-file or --gh-app-id/--gh-app-key-file or --gh-app-id/--gh-app-key or --gitea-user/--gitea-token or --gitlab-user/--gitlab-token or --bitbucket-user/--bitbucket-token or --azuredevops-user/--azuredevops-token must be set"
cases := []struct {
description string
flags map[string]interface{}
Expand Down Expand Up @@ -582,6 +583,23 @@ func TestExecute_ValidateVCSConfig(t *testing.T) {
},
false,
},
{
"github user and github token file and should be successful",
map[string]interface{}{
GHUserFlag: "user",
GHTokenFileFlag: "/path/to/token",
},
false,
},
{
"github user, github token, and github token file and should fail",
map[string]interface{}{
GHUserFlag: "user",
GHTokenFlag: "token",
GHTokenFileFlag: "/path/to/token",
},
true,
},
{
"gitea user and gitea token set and should be successful",
map[string]interface{}{
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 @@ -743,6 +743,16 @@ based on the organization or user that triggered the webhook.

GitHub token of API user.

### `--gh-token-file`

```bash
atlantis server --gh-token-file="/path/to/token"
# or (recommended)
ATLANTIS_GH_TOKEN_FILE="/path/to/token"
```

GitHub token of API user. The token is loaded from disk regularly to allow for rotation of the token without the need to restart the Atlantis server.

### `--gh-user`

```bash
Expand Down
2 changes: 1 addition & 1 deletion server/events/vcs/git_cred_writer.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ func WriteGitCreds(gitUser string, gitToken string, gitHostname string, home str
if err := fileLineReplace(config, gitUser, gitHostname, credsFile); err != nil {
return errors.Wrap(err, "replacing git credentials line for github app")
}
logger.Info("updated git app credentials in %s", credsFile)
logger.Info("updated git credentials in %s", credsFile)
} else {
if err := fileAppend(config, credsFile); err != nil {
return err
Expand Down
4 changes: 2 additions & 2 deletions server/events/vcs/github_client_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,14 @@ import (

// If the hostname is github.com, should use normal BaseURL.
func TestNewGithubClient_GithubCom(t *testing.T) {
client, err := NewGithubClient("github.com", &GithubUserCredentials{"user", "pass"}, GithubConfig{}, 0, logging.NewNoopLogger(t))
client, err := NewGithubClient("github.com", &GithubUserCredentials{"user", "pass", ""}, GithubConfig{}, 0, logging.NewNoopLogger(t))
Ok(t, err)
Equals(t, "https://api.github.com/", client.client.BaseURL.String())
}

// If the hostname is a non-github hostname should use the right BaseURL.
func TestNewGithubClient_NonGithub(t *testing.T) {
client, err := NewGithubClient("example.com", &GithubUserCredentials{"user", "pass"}, GithubConfig{}, 0, logging.NewNoopLogger(t))
client, err := NewGithubClient("example.com", &GithubUserCredentials{"user", "pass", ""}, GithubConfig{}, 0, logging.NewNoopLogger(t))
Ok(t, err)
Equals(t, "https://example.com/api/v3/", client.client.BaseURL.String())
// If possible in the future, test the GraphQL client's URL as well. But at the
Expand Down
36 changes: 18 additions & 18 deletions server/events/vcs/github_client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ func TestGithubClient_GetModifiedFiles(t *testing.T) {

testServerURL, err := url.Parse(testServer.URL)
Ok(t, err)
client, err := vcs.NewGithubClient(testServerURL.Host, &vcs.GithubUserCredentials{"user", "pass"}, vcs.GithubConfig{}, 0, logger)
client, err := vcs.NewGithubClient(testServerURL.Host, &vcs.GithubUserCredentials{"user", "pass", ""}, vcs.GithubConfig{}, 0, logger)
Ok(t, err)
defer disableSSLVerification()()

Expand Down Expand Up @@ -121,7 +121,7 @@ func TestGithubClient_GetModifiedFilesMovedFile(t *testing.T) {

testServerURL, err := url.Parse(testServer.URL)
Ok(t, err)
client, err := vcs.NewGithubClient(testServerURL.Host, &vcs.GithubUserCredentials{"user", "pass"}, vcs.GithubConfig{}, 0, logging.NewNoopLogger(t))
client, err := vcs.NewGithubClient(testServerURL.Host, &vcs.GithubUserCredentials{"user", "pass", ""}, vcs.GithubConfig{}, 0, logging.NewNoopLogger(t))
Ok(t, err)
defer disableSSLVerification()()

Expand Down Expand Up @@ -218,7 +218,7 @@ func TestGithubClient_PaginatesComments(t *testing.T) {
testServerURL, err := url.Parse(testServer.URL)
Ok(t, err)

client, err := vcs.NewGithubClient(testServerURL.Host, &vcs.GithubUserCredentials{"user", "pass"}, vcs.GithubConfig{}, 0, logging.NewNoopLogger(t))
client, err := vcs.NewGithubClient(testServerURL.Host, &vcs.GithubUserCredentials{"user", "pass", ""}, vcs.GithubConfig{}, 0, logging.NewNoopLogger(t))
Ok(t, err)
defer disableSSLVerification()()

Expand Down Expand Up @@ -334,7 +334,7 @@ func TestGithubClient_HideOldComments(t *testing.T) {
testServerURL, err := url.Parse(testServer.URL)
Ok(t, err)

client, err := vcs.NewGithubClient(testServerURL.Host, &vcs.GithubUserCredentials{atlantisUser, "pass"}, vcs.GithubConfig{}, 0,
client, err := vcs.NewGithubClient(testServerURL.Host, &vcs.GithubUserCredentials{atlantisUser, "pass", ""}, vcs.GithubConfig{}, 0,
logging.NewNoopLogger(t))
Ok(t, err)
defer disableSSLVerification()()
Expand Down Expand Up @@ -407,7 +407,7 @@ func TestGithubClient_UpdateStatus(t *testing.T) {

testServerURL, err := url.Parse(testServer.URL)
Ok(t, err)
client, err := vcs.NewGithubClient(testServerURL.Host, &vcs.GithubUserCredentials{"user", "pass"}, vcs.GithubConfig{}, 0, logging.NewNoopLogger(t))
client, err := vcs.NewGithubClient(testServerURL.Host, &vcs.GithubUserCredentials{"user", "pass", ""}, vcs.GithubConfig{}, 0, logging.NewNoopLogger(t))
Ok(t, err)
defer disableSSLVerification()()

Expand Down Expand Up @@ -496,7 +496,7 @@ func TestGithubClient_PullIsApproved(t *testing.T) {

testServerURL, err := url.Parse(testServer.URL)
Ok(t, err)
client, err := vcs.NewGithubClient(testServerURL.Host, &vcs.GithubUserCredentials{"user", "pass"}, vcs.GithubConfig{}, 0, logging.NewNoopLogger(t))
client, err := vcs.NewGithubClient(testServerURL.Host, &vcs.GithubUserCredentials{"user", "pass", ""}, vcs.GithubConfig{}, 0, logging.NewNoopLogger(t))
Ok(t, err)
defer disableSSLVerification()()

Expand Down Expand Up @@ -591,7 +591,7 @@ func TestGithubClient_PullIsMergeable(t *testing.T) {
}))
testServerURL, err := url.Parse(testServer.URL)
Ok(t, err)
client, err := vcs.NewGithubClient(testServerURL.Host, &vcs.GithubUserCredentials{"user", "pass"}, vcs.GithubConfig{}, 0, logging.NewNoopLogger(t))
client, err := vcs.NewGithubClient(testServerURL.Host, &vcs.GithubUserCredentials{"user", "pass", ""}, vcs.GithubConfig{}, 0, logging.NewNoopLogger(t))
Ok(t, err)
defer disableSSLVerification()()

Expand Down Expand Up @@ -855,7 +855,7 @@ func TestGithubClient_PullIsMergeableWithAllowMergeableBypassApply(t *testing.T)
}))
testServerURL, err := url.Parse(testServer.URL)
Ok(t, err)
client, err := vcs.NewGithubClient(testServerURL.Host, &vcs.GithubUserCredentials{"user", "pass"}, vcs.GithubConfig{AllowMergeableBypassApply: true}, 0, logging.NewNoopLogger(t))
client, err := vcs.NewGithubClient(testServerURL.Host, &vcs.GithubUserCredentials{"user", "pass", ""}, vcs.GithubConfig{AllowMergeableBypassApply: true}, 0, logging.NewNoopLogger(t))
Ok(t, err)
defer disableSSLVerification()()

Expand Down Expand Up @@ -940,7 +940,7 @@ func TestGithubClient_MergePullHandlesError(t *testing.T) {

testServerURL, err := url.Parse(testServer.URL)
Ok(t, err)
client, err := vcs.NewGithubClient(testServerURL.Host, &vcs.GithubUserCredentials{"user", "pass"}, vcs.GithubConfig{}, 0, logging.NewNoopLogger(t))
client, err := vcs.NewGithubClient(testServerURL.Host, &vcs.GithubUserCredentials{"user", "pass", ""}, vcs.GithubConfig{}, 0, logging.NewNoopLogger(t))
Ok(t, err)
defer disableSSLVerification()()

Expand Down Expand Up @@ -1120,7 +1120,7 @@ func TestGithubClient_MergePullCorrectMethod(t *testing.T) {

testServerURL, err := url.Parse(testServer.URL)
Ok(t, err)
client, err := vcs.NewGithubClient(testServerURL.Host, &vcs.GithubUserCredentials{"user", "pass"}, vcs.GithubConfig{}, 0, logging.NewNoopLogger(t))
client, err := vcs.NewGithubClient(testServerURL.Host, &vcs.GithubUserCredentials{"user", "pass", ""}, vcs.GithubConfig{}, 0, logging.NewNoopLogger(t))
Ok(t, err)
defer disableSSLVerification()()

Expand Down Expand Up @@ -1154,7 +1154,7 @@ func TestGithubClient_MergePullCorrectMethod(t *testing.T) {
}

func TestGithubClient_MarkdownPullLink(t *testing.T) {
client, err := vcs.NewGithubClient("hostname", &vcs.GithubUserCredentials{"user", "pass"}, vcs.GithubConfig{}, 0, logging.NewNoopLogger(t))
client, err := vcs.NewGithubClient("hostname", &vcs.GithubUserCredentials{"user", "pass", ""}, vcs.GithubConfig{}, 0, logging.NewNoopLogger(t))
Ok(t, err)
pull := models.PullRequest{Num: 1}
s, _ := client.MarkdownPullLink(pull)
Expand Down Expand Up @@ -1210,7 +1210,7 @@ func TestGithubClient_SplitComments(t *testing.T) {

testServerURL, err := url.Parse(testServer.URL)
Ok(t, err)
client, err := vcs.NewGithubClient(testServerURL.Host, &vcs.GithubUserCredentials{"user", "pass"}, vcs.GithubConfig{}, 0, logging.NewNoopLogger(t))
client, err := vcs.NewGithubClient(testServerURL.Host, &vcs.GithubUserCredentials{"user", "pass", ""}, vcs.GithubConfig{}, 0, logging.NewNoopLogger(t))
Ok(t, err)
defer disableSSLVerification()()
pull := models.PullRequest{Num: 1}
Expand Down Expand Up @@ -1269,7 +1269,7 @@ func TestGithubClient_Retry404(t *testing.T) {

testServerURL, err := url.Parse(testServer.URL)
Ok(t, err)
client, err := vcs.NewGithubClient(testServerURL.Host, &vcs.GithubUserCredentials{"user", "pass"}, vcs.GithubConfig{}, 0, logging.NewNoopLogger(t))
client, err := vcs.NewGithubClient(testServerURL.Host, &vcs.GithubUserCredentials{"user", "pass", ""}, vcs.GithubConfig{}, 0, logging.NewNoopLogger(t))
Ok(t, err)
defer disableSSLVerification()()
repo := models.Repo{
Expand Down Expand Up @@ -1315,7 +1315,7 @@ func TestGithubClient_Retry404Files(t *testing.T) {

testServerURL, err := url.Parse(testServer.URL)
Ok(t, err)
client, err := vcs.NewGithubClient(testServerURL.Host, &vcs.GithubUserCredentials{"user", "pass"}, vcs.GithubConfig{}, 0, logging.NewNoopLogger(t))
client, err := vcs.NewGithubClient(testServerURL.Host, &vcs.GithubUserCredentials{"user", "pass", ""}, vcs.GithubConfig{}, 0, logging.NewNoopLogger(t))
Ok(t, err)
defer disableSSLVerification()()
repo := models.Repo{
Expand Down Expand Up @@ -1368,7 +1368,7 @@ func TestGithubClient_GetTeamNamesForUser(t *testing.T) {
}))
testServerURL, err := url.Parse(testServer.URL)
Ok(t, err)
client, err := vcs.NewGithubClient(testServerURL.Host, &vcs.GithubUserCredentials{"user", "pass"}, vcs.GithubConfig{}, 0, logger)
client, err := vcs.NewGithubClient(testServerURL.Host, &vcs.GithubUserCredentials{"user", "pass", ""}, vcs.GithubConfig{}, 0, logger)
Ok(t, err)
defer disableSSLVerification()()

Expand Down Expand Up @@ -1566,7 +1566,7 @@ func TestGithubClient_DiscardReviews(t *testing.T) {
}))
testServerURL, err := url.Parse(testServer.URL)
Ok(t, err)
client, err := vcs.NewGithubClient(testServerURL.Host, &vcs.GithubUserCredentials{"user", "pass"}, vcs.GithubConfig{}, 0, logging.NewNoopLogger(t))
client, err := vcs.NewGithubClient(testServerURL.Host, &vcs.GithubUserCredentials{"user", "pass", ""}, vcs.GithubConfig{}, 0, logging.NewNoopLogger(t))
Ok(t, err)
defer disableSSLVerification()()
if err := client.DiscardReviews(tt.args.repo, tt.args.pull); (err != nil) != tt.wantErr {
Expand Down Expand Up @@ -1635,7 +1635,7 @@ func TestGithubClient_GetPullLabels(t *testing.T) {
}))
testServerURL, err := url.Parse(testServer.URL)
Ok(t, err)
client, err := vcs.NewGithubClient(testServerURL.Host, &vcs.GithubUserCredentials{"user", "pass"}, vcs.GithubConfig{}, 0, logger)
client, err := vcs.NewGithubClient(testServerURL.Host, &vcs.GithubUserCredentials{"user", "pass", ""}, vcs.GithubConfig{}, 0, logger)
Ok(t, err)
defer disableSSLVerification()()

Expand Down Expand Up @@ -1672,7 +1672,7 @@ func TestGithubClient_GetPullLabels_EmptyResponse(t *testing.T) {
}))
testServerURL, err := url.Parse(testServer.URL)
Ok(t, err)
client, err := vcs.NewGithubClient(testServerURL.Host, &vcs.GithubUserCredentials{"user", "pass"}, vcs.GithubConfig{}, 0, logger)
client, err := vcs.NewGithubClient(testServerURL.Host, &vcs.GithubUserCredentials{"user", "pass", ""}, vcs.GithubConfig{}, 0, logger)
Ok(t, err)
defer disableSSLVerification()()

Expand Down
50 changes: 44 additions & 6 deletions server/events/vcs/github_credentials.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"net/http"
"net/url"
"os"
"strings"

"github.com/bradleyfalzon/ghinstallation/v2"
Expand Down Expand Up @@ -42,17 +43,45 @@ func (c *GithubAnonymousCredentials) GetToken() (string, error) {

// GithubUserCredentials implements GithubCredentials for the personal auth token flow.
type GithubUserCredentials struct {
User string
Token string
User string
Token string
TokenFile string
}

type GitHubUserTransport struct {
Credentials *GithubUserCredentials
Transport *github.BasicAuthTransport
}

func (t *GitHubUserTransport) RoundTrip(req *http.Request) (*http.Response, error) {
// update token
token, err := t.Credentials.GetToken()
if err != nil {
return nil, err
}
t.Transport.Password = token

// defer to the underlying transport
return t.Transport.RoundTrip(req)
}

// Client returns a client for basic auth user credentials.
func (c *GithubUserCredentials) Client() (*http.Client, error) {
tr := &github.BasicAuthTransport{
Username: strings.TrimSpace(c.User),
Password: strings.TrimSpace(c.Token),
password, err := c.GetToken()
if err != nil {
return nil, err
}

client := &http.Client{
Transport: &GitHubUserTransport{
Credentials: c,
Transport: &github.BasicAuthTransport{
Username: strings.TrimSpace(c.User),
Password: strings.TrimSpace(password),
},
},
}
return tr.Client(), nil
return client, nil
}

// GetUser returns the username for these credentials.
Expand All @@ -62,6 +91,15 @@ func (c *GithubUserCredentials) GetUser() (string, error) {

// GetToken returns the user token.
func (c *GithubUserCredentials) GetToken() (string, error) {
if c.TokenFile != "" {
content, err := os.ReadFile(c.TokenFile)
if err != nil {
return "", fmt.Errorf("failed reading github token file: %w", err)
}

return string(content), nil
}

return c.Token, nil
}

Expand Down
Loading

0 comments on commit 3112e2b

Please sign in to comment.