diff options
| author | eric sciple <ericsciple@users.noreply.github.com> | 2020-03-05 14:21:59 -0500 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2020-03-05 14:21:59 -0500 |
| commit | 7ca5766bfacc6c307d7e69645aa94ac44318af46 (patch) | |
| tree | 6cf0884441af3df11112f769de7e2bd6b2db727e /src/git-auth-helper.ts | |
| parent | 05e00fbe0759a9339f189796ba5ceb3c4c78a556 (diff) | |
add support for submodules (#173)
Diffstat (limited to 'src/git-auth-helper.ts')
| -rw-r--r-- | src/git-auth-helper.ts | 154 |
1 files changed, 130 insertions, 24 deletions
diff --git a/src/git-auth-helper.ts b/src/git-auth-helper.ts index 7f7f11f..dd76fe9 100644 --- a/src/git-auth-helper.ts +++ b/src/git-auth-helper.ts @@ -5,6 +5,7 @@ import * as fs from 'fs' import * as io from '@actions/io' import * as os from 'os' import * as path from 'path' +import * as regexpHelper from './regexp-helper' import * as stateHelper from './state-helper' import {default as uuid} from 'uuid/v4' import {IGitCommandManager} from './git-command-manager' @@ -12,11 +13,13 @@ import {IGitSourceSettings} from './git-source-settings' const IS_WINDOWS = process.platform === 'win32' const HOSTNAME = 'github.com' -const EXTRA_HEADER_KEY = `http.https://${HOSTNAME}/.extraheader` export interface IGitAuthHelper { configureAuth(): Promise<void> + configureGlobalAuth(): Promise<void> + configureSubmoduleAuth(): Promise<void> removeAuth(): Promise<void> + removeGlobalAuth(): Promise<void> } export function createAuthHelper( @@ -27,8 +30,12 @@ export function createAuthHelper( } class GitAuthHelper { - private git: IGitCommandManager - private settings: IGitSourceSettings + private readonly git: IGitCommandManager + private readonly settings: IGitSourceSettings + private readonly tokenConfigKey: string = `http.https://${HOSTNAME}/.extraheader` + private readonly tokenPlaceholderConfigValue: string + private temporaryHomePath = '' + private tokenConfigValue: string constructor( gitCommandManager: IGitCommandManager, @@ -36,6 +43,15 @@ class GitAuthHelper { ) { this.git = gitCommandManager this.settings = gitSourceSettings || (({} as unknown) as IGitSourceSettings) + + // Token auth header + const basicCredential = Buffer.from( + `x-access-token:${this.settings.authToken}`, + 'utf8' + ).toString('base64') + core.setSecret(basicCredential) + this.tokenPlaceholderConfigValue = `AUTHORIZATION: basic ***` + this.tokenConfigValue = `AUTHORIZATION: basic ${basicCredential}` } async configureAuth(): Promise<void> { @@ -46,48 +62,132 @@ class GitAuthHelper { await this.configureToken() } + async configureGlobalAuth(): Promise<void> { + // Create a temp home directory + const runnerTemp = process.env['RUNNER_TEMP'] || '' + assert.ok(runnerTemp, 'RUNNER_TEMP is not defined') + const uniqueId = uuid() + this.temporaryHomePath = path.join(runnerTemp, uniqueId) + await fs.promises.mkdir(this.temporaryHomePath, {recursive: true}) + + // Copy the global git config + const gitConfigPath = path.join( + process.env['HOME'] || os.homedir(), + '.gitconfig' + ) + const newGitConfigPath = path.join(this.temporaryHomePath, '.gitconfig') + let configExists = false + try { + await fs.promises.stat(gitConfigPath) + configExists = true + } catch (err) { + if (err.code !== 'ENOENT') { + throw err + } + } + if (configExists) { + core.info(`Copying '${gitConfigPath}' to '${newGitConfigPath}'`) + await io.cp(gitConfigPath, newGitConfigPath) + } else { + await fs.promises.writeFile(newGitConfigPath, '') + } + + // Configure the token + try { + core.info( + `Temporarily overriding HOME='${this.temporaryHomePath}' before making global git config changes` + ) + this.git.setEnvironmentVariable('HOME', this.temporaryHomePath) + await this.configureToken(newGitConfigPath, true) + } catch (err) { + // Unset in case somehow written to the real global config + core.info( + 'Encountered an error when attempting to configure token. Attempting unconfigure.' + ) + await this.git.tryConfigUnset(this.tokenConfigKey, true) + throw err + } + } + + async configureSubmoduleAuth(): Promise<void> { + if (this.settings.persistCredentials) { + // 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 + const output = await this.git.submoduleForeach( + `git config "${this.tokenConfigKey}" "${this.tokenPlaceholderConfigValue}" && git config --local --show-origin --name-only --get-regexp remote.origin.url`, + this.settings.nestedSubmodules + ) + + // Replace the placeholder + const configPaths: string[] = + output.match(/(?<=(^|\n)file:)[^\t]+(?=\tremote\.origin\.url)/g) || [] + for (const configPath of configPaths) { + core.debug(`Replacing token placeholder in '${configPath}'`) + this.replaceTokenPlaceholder(configPath) + } + } + } + async removeAuth(): Promise<void> { await this.removeToken() } - private async configureToken(): Promise<void> { + async removeGlobalAuth(): Promise<void> { + core.info(`Unsetting HOME override`) + this.git.removeEnvironmentVariable('HOME') + await io.rmRF(this.temporaryHomePath) + } + + private async configureToken( + configPath?: string, + globalConfig?: boolean + ): Promise<void> { + // 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 - const placeholder = `AUTHORIZATION: basic ***` - await this.git.config(EXTRA_HEADER_KEY, placeholder) + await this.git.config( + this.tokenConfigKey, + this.tokenPlaceholderConfigValue, + globalConfig + ) - // Determine the basic credential value - const basicCredential = Buffer.from( - `x-access-token:${this.settings.authToken}`, - 'utf8' - ).toString('base64') - core.setSecret(basicCredential) + // Replace the placeholder + await this.replaceTokenPlaceholder(configPath || '') + } - // Replace the value in the config file - const configPath = path.join( - this.git.getWorkingDirectory(), - '.git', - 'config' - ) + private async replaceTokenPlaceholder(configPath: string): Promise<void> { + assert.ok(configPath, 'configPath is not defined') let content = (await fs.promises.readFile(configPath)).toString() - const placeholderIndex = content.indexOf(placeholder) + const placeholderIndex = content.indexOf(this.tokenPlaceholderConfigValue) if ( placeholderIndex < 0 || - placeholderIndex != content.lastIndexOf(placeholder) + placeholderIndex != content.lastIndexOf(this.tokenPlaceholderConfigValue) ) { - throw new Error('Unable to replace auth placeholder in .git/config') + throw new Error(`Unable to replace auth placeholder in ${configPath}`) } + assert.ok(this.tokenConfigValue, 'tokenConfigValue is not defined') content = content.replace( - placeholder, - `AUTHORIZATION: basic ${basicCredential}` + this.tokenPlaceholderConfigValue, + this.tokenConfigValue ) await fs.promises.writeFile(configPath, content) } private async removeToken(): Promise<void> { // HTTP extra header - await this.removeGitConfig(EXTRA_HEADER_KEY) + await this.removeGitConfig(this.tokenConfigKey) } private async removeGitConfig(configKey: string): Promise<void> { @@ -98,5 +198,11 @@ class GitAuthHelper { // Load the config contents core.warning(`Failed to remove '${configKey}' from the git config`) } + + const pattern = regexpHelper.escape(configKey) + await this.git.submoduleForeach( + `git config --local --name-only --get-regexp ${pattern} && git config --local --unset-all ${configKey} || :`, + true + ) } } |
