diff --git a/__test__/git-auth-helper.test.ts b/__test__/git-auth-helper.test.ts index 51449bd..fec6573 100644 --- a/__test__/git-auth-helper.test.ts +++ b/__test__/git-auth-helper.test.ts @@ -94,11 +94,11 @@ describe('git-auth-helper tests', () => { `x-access-token:${settings.authToken}`, 'utf8' ).toString('base64') - // expect( - // configContent.indexOf( - // `http.${expectedServerUrl}/.extraheader AUTHORIZATION: basic ${basicCredential}` - // ) - // ).toBeGreaterThanOrEqual(0) + expect( + configContent.indexOf( + `http.${expectedServerUrl}/.extraheader AUTHORIZATION: basic ${basicCredential}` + ) + ).toBeGreaterThanOrEqual(0) } const configureAuth_configuresAuthHeader = @@ -145,11 +145,11 @@ describe('git-auth-helper tests', () => { const configContent = ( await fs.promises.readFile(localGitConfigPath) ).toString() - // expect( - // configContent.indexOf( - // `http.https://github.com/.extraheader AUTHORIZATION` - // ) - // ).toBeGreaterThanOrEqual(0) + expect( + configContent.indexOf( + `http.https://github.com/.extraheader AUTHORIZATION` + ) + ).toBeGreaterThanOrEqual(0) } ) @@ -419,11 +419,11 @@ describe('git-auth-helper tests', () => { expect( configContent.indexOf('value-from-global-config') ).toBeGreaterThanOrEqual(0) - // expect( - // configContent.indexOf( - // `http.https://github.com/.extraheader AUTHORIZATION: basic ${basicCredential}` - // ) - // ).toBeGreaterThanOrEqual(0) + expect( + configContent.indexOf( + `http.https://github.com/.extraheader AUTHORIZATION: basic ${basicCredential}` + ) + ).toBeGreaterThanOrEqual(0) }) const configureGlobalAuth_createsNewGlobalGitConfigWhenGlobalDoesNotExist = @@ -463,11 +463,11 @@ describe('git-auth-helper tests', () => { const configContent = ( await fs.promises.readFile(path.join(git.env['HOME'], '.gitconfig')) ).toString() - // expect( - // configContent.indexOf( - // `http.https://github.com/.extraheader AUTHORIZATION: basic ${basicCredential}` - // ) - // ).toBeGreaterThanOrEqual(0) + expect( + configContent.indexOf( + `http.https://github.com/.extraheader AUTHORIZATION: basic ${basicCredential}` + ) + ).toBeGreaterThanOrEqual(0) } ) @@ -554,7 +554,7 @@ describe('git-auth-helper tests', () => { expect(mockSubmoduleForeach.mock.calls[0][0]).toMatch( /unset-all.*insteadOf/ ) - // expect(mockSubmoduleForeach.mock.calls[1][0]).toMatch(/http.*extraheader/) + expect(mockSubmoduleForeach.mock.calls[1][0]).toMatch(/http.*extraheader/) expect(mockSubmoduleForeach.mock.calls[2][0]).toMatch( /url.*insteadOf.*git@github.com:/ ) @@ -593,7 +593,7 @@ describe('git-auth-helper tests', () => { expect(mockSubmoduleForeach.mock.calls[0][0]).toMatch( /unset-all.*insteadOf/ ) - // expect(mockSubmoduleForeach.mock.calls[1][0]).toMatch(/http.*extraheader/) + expect(mockSubmoduleForeach.mock.calls[1][0]).toMatch(/http.*extraheader/) expect(mockSubmoduleForeach.mock.calls[2][0]).toMatch(/core\.sshCommand/) } ) diff --git a/dist/index.js b/dist/index.js index 03e28d5..4556295 100644 --- a/dist/index.js +++ b/dist/index.js @@ -159,11 +159,11 @@ class GitAuthHelper { this.sshKeyPath = ''; this.sshKnownHostsPath = ''; this.temporaryHomePath = ''; - this.gitConfigPath = ''; this.git = gitCommandManager; this.settings = gitSourceSettings || {}; // Token auth header const serverUrl = urlHelper.getServerUrl(this.settings.githubServerUrl); + this.tokenConfigKey = `http.${serverUrl.origin}/.extraheader`; // "origin" is SCHEME://HOSTNAME[:PORT] const basicCredential = Buffer.from(`x-access-token:${this.settings.authToken}`, 'utf8').toString('base64'); core.setSecret(basicCredential); this.tokenPlaceholderConfigValue = `AUTHORIZATION: basic ***`; @@ -181,15 +181,12 @@ class GitAuthHelper { yield this.removeAuth(); // Configure new values yield this.configureSsh(); - yield this.configureCredentialsHelper(); + yield this.configureToken(); }); } configureTempGlobalConfig() { var _a, _b; return __awaiter(this, void 0, void 0, function* () { - if (!!this.gitConfigPath) { - return this.gitConfigPath; - } // Already setup global config if (((_a = this.temporaryHomePath) === null || _a === void 0 ? void 0 : _a.length) > 0) { return path.join(this.temporaryHomePath, '.gitconfig'); @@ -202,7 +199,7 @@ class GitAuthHelper { yield fs.promises.mkdir(this.temporaryHomePath, { recursive: true }); // Copy the global git config const gitConfigPath = path.join(process.env['HOME'] || os.homedir(), '.gitconfig'); - this.gitConfigPath = path.join(this.temporaryHomePath, '.gitconfig'); + const newGitConfigPath = path.join(this.temporaryHomePath, '.gitconfig'); let configExists = false; try { yield fs.promises.stat(gitConfigPath); @@ -214,31 +211,16 @@ class GitAuthHelper { } } if (configExists) { - core.info(`Copying '${gitConfigPath}' to '${this.gitConfigPath}'`); - yield io.cp(gitConfigPath, this.gitConfigPath); + core.info(`Copying '${gitConfigPath}' to '${newGitConfigPath}'`); + yield io.cp(gitConfigPath, newGitConfigPath); } else { - yield fs.promises.writeFile(this.gitConfigPath, ''); + yield fs.promises.writeFile(newGitConfigPath, ''); } // Override HOME core.info(`Temporarily overriding HOME='${this.temporaryHomePath}' before making global git config changes`); this.git.setEnvironmentVariable('HOME', this.temporaryHomePath); - return this.gitConfigPath; - }); - } - configureCredentialsHelper() { - return __awaiter(this, void 0, void 0, function* () { - if (this.settings.lfs) { - core.info(`lfs disabled, skipping custom credentials helper`); - return; - } - const newGitConfigPath = yield this.configureTempGlobalConfig(); - const credentialHelper = ` - [credential] - helper = "!f() { echo username=x-access-token; echo password=${this.tokenConfigValue}; };f" - `; - core.info(`Configuring git to use a custom credential helper for aut to handle git lfs`); - yield fs.promises.appendFile(newGitConfigPath, credentialHelper); + return newGitConfigPath; }); } configureGlobalAuth() { @@ -247,6 +229,7 @@ class GitAuthHelper { const newGitConfigPath = yield this.configureTempGlobalConfig(); try { // Configure the token + yield this.configureToken(newGitConfigPath, true); // Configure HTTPS instead of SSH yield this.git.tryConfigUnset(this.insteadOfKey, true); if (!this.settings.sshKey) { @@ -258,6 +241,7 @@ class GitAuthHelper { catch (err) { // Unset in case somehow written to the real global config core.info('Encountered an error when attempting to configure token. Attempting unconfigure.'); + yield this.git.tryConfigUnset(this.tokenConfigKey, true); throw err; } }); @@ -272,7 +256,7 @@ class GitAuthHelper { // refer to https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/component-updates/command-line-process-auditing const output = yield this.git.submoduleForeach( // wrap the pipeline in quotes to make sure it's handled properly by submoduleForeach, rather than just the first part of the pipeline - `sh -c "git config --local --show-origin --name-only --get-regexp remote.origin.url"`, this.settings.nestedSubmodules); + `sh -c "git config --local '${this.tokenConfigKey}' '${this.tokenPlaceholderConfigValue}' && git config --local --show-origin --name-only --get-regexp remote.origin.url"`, this.settings.nestedSubmodules); // Replace the placeholder const configPaths = output.match(/(?<=(^|\n)file:)[^\t]+(?=\tremote\.origin\.url)/g) || []; for (const configPath of configPaths) { @@ -295,6 +279,7 @@ class GitAuthHelper { removeAuth() { return __awaiter(this, void 0, void 0, function* () { yield this.removeSsh(); + yield this.removeToken(); }); } removeGlobalConfig() { @@ -364,6 +349,22 @@ class GitAuthHelper { } }); } + configureToken(configPath, globalConfig) { + return __awaiter(this, void 0, void 0, function* () { + // Validate args + assert.ok((configPath && globalConfig) || (!configPath && !globalConfig), 'Unexpected configureToken parameter combinations'); + // Default config path + if (!configPath && !globalConfig) { + configPath = path.join(this.git.getWorkingDirectory(), '.git', 'config'); + } + // Configure a placeholder value. This approach avoids the credential being captured + // by process creation audit events, which are commonly logged. For more information, + // refer to https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/component-updates/command-line-process-auditing + yield this.git.config(this.tokenConfigKey, this.tokenPlaceholderConfigValue, globalConfig); + // Replace the placeholder + yield this.replaceTokenPlaceholder(configPath || ''); + }); + } replaceTokenPlaceholder(configPath) { return __awaiter(this, void 0, void 0, function* () { assert.ok(configPath, 'configPath is not defined'); @@ -406,6 +407,12 @@ class GitAuthHelper { yield this.removeGitConfig(SSH_COMMAND_KEY); }); } + removeToken() { + return __awaiter(this, void 0, void 0, function* () { + // HTTP extra header + yield this.removeGitConfig(this.tokenConfigKey); + }); + } removeGitConfig(configKey, submoduleOnly = false) { return __awaiter(this, void 0, void 0, function* () { if (!submoduleOnly) { diff --git a/src/git-auth-helper.ts b/src/git-auth-helper.ts index 0866aae..364a04e 100644 --- a/src/git-auth-helper.ts +++ b/src/git-auth-helper.ts @@ -20,7 +20,6 @@ export interface IGitAuthHelper { configureGlobalAuth(): Promise configureSubmoduleAuth(): Promise configureTempGlobalConfig(): Promise - configureCredentialsHelper(): Promise removeAuth(): Promise removeGlobalConfig(): Promise } @@ -35,6 +34,7 @@ export function createAuthHelper( class GitAuthHelper { private readonly git: IGitCommandManager private readonly settings: IGitSourceSettings + private readonly tokenConfigKey: string private readonly tokenConfigValue: string private readonly tokenPlaceholderConfigValue: string private readonly insteadOfKey: string @@ -43,7 +43,6 @@ class GitAuthHelper { private sshKeyPath = '' private sshKnownHostsPath = '' private temporaryHomePath = '' - private gitConfigPath = '' constructor( gitCommandManager: IGitCommandManager, @@ -54,6 +53,7 @@ class GitAuthHelper { // Token auth header const serverUrl = urlHelper.getServerUrl(this.settings.githubServerUrl) + this.tokenConfigKey = `http.${serverUrl.origin}/.extraheader` // "origin" is SCHEME://HOSTNAME[:PORT] const basicCredential = Buffer.from( `x-access-token:${this.settings.authToken}`, 'utf8' @@ -78,13 +78,10 @@ class GitAuthHelper { // Configure new values await this.configureSsh() - await this.configureCredentialsHelper() + await this.configureToken() } async configureTempGlobalConfig(): Promise { - if (!!this.gitConfigPath) { - return this.gitConfigPath - } // Already setup global config if (this.temporaryHomePath?.length > 0) { return path.join(this.temporaryHomePath, '.gitconfig') @@ -101,7 +98,7 @@ class GitAuthHelper { process.env['HOME'] || os.homedir(), '.gitconfig' ) - this.gitConfigPath = path.join(this.temporaryHomePath, '.gitconfig') + const newGitConfigPath = path.join(this.temporaryHomePath, '.gitconfig') let configExists = false try { await fs.promises.stat(gitConfigPath) @@ -112,10 +109,10 @@ class GitAuthHelper { } } if (configExists) { - core.info(`Copying '${gitConfigPath}' to '${this.gitConfigPath}'`) - await io.cp(gitConfigPath, this.gitConfigPath) + core.info(`Copying '${gitConfigPath}' to '${newGitConfigPath}'`) + await io.cp(gitConfigPath, newGitConfigPath) } else { - await fs.promises.writeFile(this.gitConfigPath, '') + await fs.promises.writeFile(newGitConfigPath, '') } // Override HOME @@ -124,25 +121,7 @@ class GitAuthHelper { ) this.git.setEnvironmentVariable('HOME', this.temporaryHomePath) - return this.gitConfigPath - } - - async configureCredentialsHelper(): Promise { - if (this.settings.lfs) { - core.info(`lfs disabled, skipping custom credentials helper`) - return - } - const newGitConfigPath = await this.configureTempGlobalConfig() - - const credentialHelper = ` - [credential] - helper = "!f() { echo username=x-access-token; echo password=${this.tokenConfigValue}; };f" - ` - - core.info( - `Configuring git to use a custom credential helper for aut to handle git lfs` - ) - await fs.promises.appendFile(newGitConfigPath, credentialHelper) + return newGitConfigPath } async configureGlobalAuth(): Promise { @@ -150,6 +129,8 @@ class GitAuthHelper { const newGitConfigPath = await this.configureTempGlobalConfig() try { // Configure the token + await this.configureToken(newGitConfigPath, true) + // Configure HTTPS instead of SSH await this.git.tryConfigUnset(this.insteadOfKey, true) if (!this.settings.sshKey) { @@ -162,6 +143,7 @@ class GitAuthHelper { core.info( 'Encountered an error when attempting to configure token. Attempting unconfigure.' ) + await this.git.tryConfigUnset(this.tokenConfigKey, true) throw err } } @@ -176,7 +158,7 @@ class GitAuthHelper { // refer to https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/component-updates/command-line-process-auditing const output = await this.git.submoduleForeach( // wrap the pipeline in quotes to make sure it's handled properly by submoduleForeach, rather than just the first part of the pipeline - `sh -c "git config --local --show-origin --name-only --get-regexp remote.origin.url"`, + `sh -c "git config --local '${this.tokenConfigKey}' '${this.tokenPlaceholderConfigValue}' && git config --local --show-origin --name-only --get-regexp remote.origin.url"`, this.settings.nestedSubmodules ) @@ -208,6 +190,7 @@ class GitAuthHelper { async removeAuth(): Promise { await this.removeSsh() + await this.removeToken() } async removeGlobalConfig(): Promise { @@ -289,6 +272,34 @@ class GitAuthHelper { } } + private async configureToken( + configPath?: string, + globalConfig?: boolean + ): Promise { + // Validate args + assert.ok( + (configPath && globalConfig) || (!configPath && !globalConfig), + 'Unexpected configureToken parameter combinations' + ) + + // Default config path + if (!configPath && !globalConfig) { + configPath = path.join(this.git.getWorkingDirectory(), '.git', 'config') + } + + // Configure a placeholder value. This approach avoids the credential being captured + // by process creation audit events, which are commonly logged. For more information, + // refer to https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/component-updates/command-line-process-auditing + await this.git.config( + this.tokenConfigKey, + this.tokenPlaceholderConfigValue, + globalConfig + ) + + // Replace the placeholder + await this.replaceTokenPlaceholder(configPath || '') + } + private async replaceTokenPlaceholder(configPath: string): Promise { assert.ok(configPath, 'configPath is not defined') let content = (await fs.promises.readFile(configPath)).toString() @@ -334,6 +345,11 @@ class GitAuthHelper { await this.removeGitConfig(SSH_COMMAND_KEY) } + private async removeToken(): Promise { + // HTTP extra header + await this.removeGitConfig(this.tokenConfigKey) + } + private async removeGitConfig( configKey: string, submoduleOnly: boolean = false