From 171b8895155a0f7b935bdf8301c6c08dcd9a9289 Mon Sep 17 00:00:00 2001 From: Henry Muru Paenga Date: Thu, 12 Sep 2024 11:49:57 +1200 Subject: [PATCH] Support loading git token from disk Signed-off-by: Henry Muru Paenga --- cmd/server.go | 24 ++++++--- cmd/server_test.go | 20 +++++++- runatlantis.io/docs/server-configuration.md | 10 ++++ server/events/vcs/git_cred_writer.go | 2 +- .../events/vcs/github_client_internal_test.go | 4 +- server/events/vcs/github_client_test.go | 36 ++++++------- server/events/vcs/github_credentials.go | 50 ++++++++++++++++--- ...eds_rotator.go => github_token_rotator.go} | 27 +++++----- ...r_test.go => github_token_rotator_test.go} | 6 +-- server/server.go | 16 ++++-- server/user_config.go | 1 + 11 files changed, 142 insertions(+), 54 deletions(-) rename server/events/vcs/{gh_app_creds_rotator.go => github_token_rotator.go} (62%) rename server/events/vcs/{gh_app_creds_rotator_test.go => github_token_rotator_test.go} (89%) diff --git a/cmd/server.go b/cmd/server.go index e53ca20418..f967e5a5c4 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -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" @@ -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: "", @@ -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. @@ -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, diff --git a/cmd/server_test.go b/cmd/server_test.go index cccf9cc055..e4ccb96b15 100644 --- a/cmd/server_test.go +++ b/cmd/server_test.go @@ -86,6 +86,7 @@ var testFlags = map[string]interface{}{ GHHostnameFlag: "ghhostname", GHTeamAllowlistFlag: "", GHTokenFlag: "token", + GHTokenFileFlag: "", GHUserFlag: "user", GHAppIDFlag: int64(0), GHAppKeyFlag: "", @@ -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{} @@ -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{}{ diff --git a/runatlantis.io/docs/server-configuration.md b/runatlantis.io/docs/server-configuration.md index 610838f262..92de6bac71 100644 --- a/runatlantis.io/docs/server-configuration.md +++ b/runatlantis.io/docs/server-configuration.md @@ -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 diff --git a/server/events/vcs/git_cred_writer.go b/server/events/vcs/git_cred_writer.go index eca5dc00d7..f877abcfdf 100644 --- a/server/events/vcs/git_cred_writer.go +++ b/server/events/vcs/git_cred_writer.go @@ -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 diff --git a/server/events/vcs/github_client_internal_test.go b/server/events/vcs/github_client_internal_test.go index 798264d269..63f7a73e6b 100644 --- a/server/events/vcs/github_client_internal_test.go +++ b/server/events/vcs/github_client_internal_test.go @@ -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 diff --git a/server/events/vcs/github_client_test.go b/server/events/vcs/github_client_test.go index 632fe6bca6..cf9f3789f1 100644 --- a/server/events/vcs/github_client_test.go +++ b/server/events/vcs/github_client_test.go @@ -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()() @@ -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()() @@ -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()() @@ -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()() @@ -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()() @@ -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()() @@ -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()() @@ -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()() @@ -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()() @@ -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()() @@ -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) @@ -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} @@ -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{ @@ -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{ @@ -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()() @@ -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 { @@ -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()() @@ -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()() diff --git a/server/events/vcs/github_credentials.go b/server/events/vcs/github_credentials.go index b00ec146a5..4b322fa6cb 100644 --- a/server/events/vcs/github_credentials.go +++ b/server/events/vcs/github_credentials.go @@ -5,6 +5,7 @@ import ( "fmt" "net/http" "net/url" + "os" "strings" "github.com/bradleyfalzon/ghinstallation/v2" @@ -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. @@ -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 } diff --git a/server/events/vcs/gh_app_creds_rotator.go b/server/events/vcs/github_token_rotator.go similarity index 62% rename from server/events/vcs/gh_app_creds_rotator.go rename to server/events/vcs/github_token_rotator.go index d5b059f972..2b184bd6b8 100644 --- a/server/events/vcs/gh_app_creds_rotator.go +++ b/server/events/vcs/github_token_rotator.go @@ -8,37 +8,40 @@ import ( "github.com/runatlantis/atlantis/server/scheduled" ) -// GitCredsTokenRotator continuously tries to rotate the github app access token every 30 seconds and writes the ~/.git-credentials file -type GitCredsTokenRotator interface { +// GithubTokenRotator continuously tries to rotate the github app access token every 30 seconds and writes the ~/.git-credentials file +type GithubTokenRotator interface { Run() GenerateJob() (scheduled.JobDefinition, error) } -type githubAppTokenRotator struct { +type githubTokenRotator struct { log logging.SimpleLogging githubCredentials GithubCredentials githubHostname string + gitUser string homeDirPath string } -func NewGithubAppTokenRotator( +func NewGithubTokenRotator( log logging.SimpleLogging, githubCredentials GithubCredentials, githubHostname string, - homeDirPath string) GitCredsTokenRotator { + gitUser string, + homeDirPath string) GithubTokenRotator { - return &githubAppTokenRotator{ + return &githubTokenRotator{ log: log, githubCredentials: githubCredentials, githubHostname: githubHostname, + gitUser: gitUser, homeDirPath: homeDirPath, } } // make sure interface is implemented correctly -var _ GitCredsTokenRotator = (*githubAppTokenRotator)(nil) +var _ GithubTokenRotator = (*githubTokenRotator)(nil) -func (r *githubAppTokenRotator) GenerateJob() (scheduled.JobDefinition, error) { +func (r *githubTokenRotator) GenerateJob() (scheduled.JobDefinition, error) { return scheduled.JobDefinition{ Job: r, @@ -46,7 +49,7 @@ func (r *githubAppTokenRotator) GenerateJob() (scheduled.JobDefinition, error) { }, r.rotate() } -func (r *githubAppTokenRotator) Run() { +func (r *githubTokenRotator) Run() { err := r.rotate() if err != nil { // at least log the error message here, as we want to notify the that user that the key rotation wasn't successful @@ -54,8 +57,8 @@ func (r *githubAppTokenRotator) Run() { } } -func (r *githubAppTokenRotator) rotate() error { - r.log.Debug("Refreshing git tokens for Github App") +func (r *githubTokenRotator) rotate() error { + r.log.Debug("Refreshing Github tokens for .git-credentials") token, err := r.githubCredentials.GetToken() if err != nil { @@ -64,7 +67,7 @@ func (r *githubAppTokenRotator) rotate() error { r.log.Debug("Token successfully refreshed") // https://developer.github.com/apps/building-github-apps/authenticating-with-github-apps/#http-based-git-access-by-an-installation - if err := WriteGitCreds("x-access-token", token, r.githubHostname, r.homeDirPath, r.log, true); err != nil { + if err := WriteGitCreds(r.gitUser, token, r.githubHostname, r.homeDirPath, r.log, true); err != nil { return errors.Wrap(err, "Writing ~/.git-credentials file") } return nil diff --git a/server/events/vcs/gh_app_creds_rotator_test.go b/server/events/vcs/github_token_rotator_test.go similarity index 89% rename from server/events/vcs/gh_app_creds_rotator_test.go rename to server/events/vcs/github_token_rotator_test.go index 8aa65c707a..19e30b95f1 100644 --- a/server/events/vcs/gh_app_creds_rotator_test.go +++ b/server/events/vcs/github_token_rotator_test.go @@ -13,7 +13,7 @@ import ( . "github.com/runatlantis/atlantis/testing" ) -func Test_githubAppTokenRotator_GenerateJob(t *testing.T) { +func Test_githubTokenRotator_GenerateJob(t *testing.T) { logger := logging.NewNoopLogger(t) defer disableSSLVerification()() testServer, err := testdata.GithubAppTestServer(t) @@ -68,10 +68,10 @@ func Test_githubAppTokenRotator_GenerateJob(t *testing.T) { t.Run(tt.name, func(t *testing.T) { tmpDir := t.TempDir() t.Setenv("HOME", tmpDir) - r := vcs.NewGithubAppTokenRotator(logging.NewNoopLogger(t), tt.fields.githubCredentials, testServer, tmpDir) + r := vcs.NewGithubTokenRotator(logging.NewNoopLogger(t), tt.fields.githubCredentials, testServer, "x-access-token", tmpDir) got, err := r.GenerateJob() if (err != nil) != tt.wantErr { - t.Errorf("githubAppTokenRotator.GenerateJob() error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("githubTokenRotator.GenerateJob() error = %v, wantErr %v", err, tt.wantErr) return } if tt.credsFileWritten { diff --git a/server/server.go b/server/server.go index af940f3787..3eb0262512 100644 --- a/server/server.go +++ b/server/server.go @@ -230,8 +230,9 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { supportedVCSHosts = append(supportedVCSHosts, models.Github) if userConfig.GithubUser != "" { githubCredentials = &vcs.GithubUserCredentials{ - User: userConfig.GithubUser, - Token: userConfig.GithubToken, + User: userConfig.GithubUser, + Token: userConfig.GithubToken, + TokenFile: userConfig.GithubTokenFile, } } else if userConfig.GithubAppID != 0 && userConfig.GithubAppKeyFile != "" { privateKey, err := os.ReadFile(userConfig.GithubAppKeyFile) @@ -514,7 +515,7 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { GithubHostname: userConfig.GithubHostname, } - githubAppTokenRotator := vcs.NewGithubAppTokenRotator(logger, githubCredentials, userConfig.GithubHostname, home) + githubAppTokenRotator := vcs.NewGithubTokenRotator(logger, githubCredentials, userConfig.GithubHostname, "x-access-token", home) tokenJd, err := githubAppTokenRotator.GenerateJob() if err != nil { return nil, errors.Wrap(err, "could not write credentials") @@ -522,6 +523,15 @@ func NewServer(userConfig UserConfig, config Config) (*Server, error) { scheduledExecutorService.AddJob(tokenJd) } + if userConfig.GithubUser != "" && userConfig.GithubTokenFile != "" && userConfig.WriteGitCreds { + githubTokenRotator := vcs.NewGithubTokenRotator(logger, githubCredentials, userConfig.GithubHostname, userConfig.GithubUser, home) + tokenJd, err := githubTokenRotator.GenerateJob() + if err != nil { + return nil, errors.Wrap(err, "could not write credentials") + } + scheduledExecutorService.AddJob(tokenJd) + } + projectLocker := &events.DefaultProjectLocker{ Locker: lockingClient, NoOpLocker: noOpLocker, diff --git a/server/user_config.go b/server/user_config.go index 9a6247ae09..741e551951 100644 --- a/server/user_config.go +++ b/server/user_config.go @@ -50,6 +50,7 @@ type UserConfig struct { GithubAllowMergeableBypassApply bool `mapstructure:"gh-allow-mergeable-bypass-apply"` GithubHostname string `mapstructure:"gh-hostname"` GithubToken string `mapstructure:"gh-token"` + GithubTokenFile string `mapstructure:"gh-token-file"` GithubUser string `mapstructure:"gh-user"` GithubWebhookSecret string `mapstructure:"gh-webhook-secret"` GithubOrg string `mapstructure:"gh-org"`