2020-03-02 17:33:30 +01:00
|
|
|
import * as assert from 'assert'
|
|
|
|
import * as core from '@actions/core'
|
|
|
|
import * as exec from '@actions/exec'
|
|
|
|
import * as fs from 'fs'
|
|
|
|
import * as io from '@actions/io'
|
|
|
|
import * as os from 'os'
|
|
|
|
import * as path from 'path'
|
2020-03-05 20:21:59 +01:00
|
|
|
import * as regexpHelper from './regexp-helper'
|
2020-03-02 17:33:30 +01:00
|
|
|
import * as stateHelper from './state-helper'
|
|
|
|
import {default as uuid} from 'uuid/v4'
|
|
|
|
import {IGitCommandManager} from './git-command-manager'
|
|
|
|
import {IGitSourceSettings} from './git-source-settings'
|
|
|
|
|
|
|
|
const IS_WINDOWS = process.platform === 'win32'
|
|
|
|
const HOSTNAME = 'github.com'
|
|
|
|
|
|
|
|
export interface IGitAuthHelper {
|
|
|
|
configureAuth(): Promise<void>
|
2020-03-05 20:21:59 +01:00
|
|
|
configureGlobalAuth(): Promise<void>
|
|
|
|
configureSubmoduleAuth(): Promise<void>
|
2020-03-02 17:33:30 +01:00
|
|
|
removeAuth(): Promise<void>
|
2020-03-05 20:21:59 +01:00
|
|
|
removeGlobalAuth(): Promise<void>
|
2020-03-02 17:33:30 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
export function createAuthHelper(
|
|
|
|
git: IGitCommandManager,
|
|
|
|
settings?: IGitSourceSettings
|
|
|
|
): IGitAuthHelper {
|
|
|
|
return new GitAuthHelper(git, settings)
|
|
|
|
}
|
|
|
|
|
|
|
|
class GitAuthHelper {
|
2020-03-05 20:21:59 +01:00
|
|
|
private readonly git: IGitCommandManager
|
|
|
|
private readonly settings: IGitSourceSettings
|
|
|
|
private readonly tokenConfigKey: string = `http.https://${HOSTNAME}/.extraheader`
|
|
|
|
private readonly tokenPlaceholderConfigValue: string
|
2020-03-10 15:45:50 +01:00
|
|
|
private readonly insteadOfKey: string = `url.https://${HOSTNAME}/.insteadOf`
|
|
|
|
private readonly insteadOfValue: string = `git@${HOSTNAME}:`
|
2020-03-05 20:21:59 +01:00
|
|
|
private temporaryHomePath = ''
|
|
|
|
private tokenConfigValue: string
|
2020-03-02 17:33:30 +01:00
|
|
|
|
|
|
|
constructor(
|
|
|
|
gitCommandManager: IGitCommandManager,
|
|
|
|
gitSourceSettings?: IGitSourceSettings
|
|
|
|
) {
|
|
|
|
this.git = gitCommandManager
|
|
|
|
this.settings = gitSourceSettings || (({} as unknown) as IGitSourceSettings)
|
2020-03-05 20:21:59 +01:00
|
|
|
|
|
|
|
// 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}`
|
2020-03-02 17:33:30 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
async configureAuth(): Promise<void> {
|
|
|
|
// Remove possible previous values
|
|
|
|
await this.removeAuth()
|
|
|
|
|
|
|
|
// Configure new values
|
|
|
|
await this.configureToken()
|
|
|
|
}
|
|
|
|
|
2020-03-05 20:21:59 +01:00
|
|
|
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, '')
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
2020-03-10 15:45:50 +01:00
|
|
|
// Override HOME
|
2020-03-05 20:21:59 +01:00
|
|
|
core.info(
|
|
|
|
`Temporarily overriding HOME='${this.temporaryHomePath}' before making global git config changes`
|
|
|
|
)
|
|
|
|
this.git.setEnvironmentVariable('HOME', this.temporaryHomePath)
|
2020-03-10 15:45:50 +01:00
|
|
|
|
|
|
|
// Configure the token
|
2020-03-05 20:21:59 +01:00
|
|
|
await this.configureToken(newGitConfigPath, true)
|
2020-03-10 15:45:50 +01:00
|
|
|
|
|
|
|
// Configure HTTPS instead of SSH
|
|
|
|
await this.git.tryConfigUnset(this.insteadOfKey, true)
|
|
|
|
await this.git.config(this.insteadOfKey, this.insteadOfValue, true)
|
2020-03-05 20:21:59 +01:00
|
|
|
} 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
|
2020-03-10 15:45:50 +01:00
|
|
|
const commands = [
|
|
|
|
`git config --local "${this.tokenConfigKey}" "${this.tokenPlaceholderConfigValue}"`,
|
|
|
|
`git config --local "${this.insteadOfKey}" "${this.insteadOfValue}"`,
|
|
|
|
`git config --local --show-origin --name-only --get-regexp remote.origin.url`
|
|
|
|
]
|
2020-03-05 20:21:59 +01:00
|
|
|
const output = await this.git.submoduleForeach(
|
2020-03-10 15:45:50 +01:00
|
|
|
commands.join(' && '),
|
2020-03-05 20:21:59 +01:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-03-02 17:33:30 +01:00
|
|
|
async removeAuth(): Promise<void> {
|
|
|
|
await this.removeToken()
|
|
|
|
}
|
|
|
|
|
2020-03-05 20:21:59 +01:00
|
|
|
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')
|
|
|
|
}
|
|
|
|
|
2020-03-02 17:33:30 +01:00
|
|
|
// 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
|
2020-03-05 20:21:59 +01:00
|
|
|
await this.git.config(
|
|
|
|
this.tokenConfigKey,
|
|
|
|
this.tokenPlaceholderConfigValue,
|
|
|
|
globalConfig
|
|
|
|
)
|
2020-03-02 17:33:30 +01:00
|
|
|
|
2020-03-05 20:21:59 +01:00
|
|
|
// Replace the placeholder
|
|
|
|
await this.replaceTokenPlaceholder(configPath || '')
|
|
|
|
}
|
2020-03-02 17:33:30 +01:00
|
|
|
|
2020-03-05 20:21:59 +01:00
|
|
|
private async replaceTokenPlaceholder(configPath: string): Promise<void> {
|
|
|
|
assert.ok(configPath, 'configPath is not defined')
|
2020-03-02 17:33:30 +01:00
|
|
|
let content = (await fs.promises.readFile(configPath)).toString()
|
2020-03-05 20:21:59 +01:00
|
|
|
const placeholderIndex = content.indexOf(this.tokenPlaceholderConfigValue)
|
2020-03-02 17:33:30 +01:00
|
|
|
if (
|
|
|
|
placeholderIndex < 0 ||
|
2020-03-05 20:21:59 +01:00
|
|
|
placeholderIndex != content.lastIndexOf(this.tokenPlaceholderConfigValue)
|
2020-03-02 17:33:30 +01:00
|
|
|
) {
|
2020-03-05 20:21:59 +01:00
|
|
|
throw new Error(`Unable to replace auth placeholder in ${configPath}`)
|
2020-03-02 17:33:30 +01:00
|
|
|
}
|
2020-03-05 20:21:59 +01:00
|
|
|
assert.ok(this.tokenConfigValue, 'tokenConfigValue is not defined')
|
2020-03-02 17:33:30 +01:00
|
|
|
content = content.replace(
|
2020-03-05 20:21:59 +01:00
|
|
|
this.tokenPlaceholderConfigValue,
|
|
|
|
this.tokenConfigValue
|
2020-03-02 17:33:30 +01:00
|
|
|
)
|
|
|
|
await fs.promises.writeFile(configPath, content)
|
|
|
|
}
|
|
|
|
|
|
|
|
private async removeToken(): Promise<void> {
|
|
|
|
// HTTP extra header
|
2020-03-05 20:21:59 +01:00
|
|
|
await this.removeGitConfig(this.tokenConfigKey)
|
2020-03-02 17:33:30 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
private async removeGitConfig(configKey: string): Promise<void> {
|
|
|
|
if (
|
|
|
|
(await this.git.configExists(configKey)) &&
|
|
|
|
!(await this.git.tryConfigUnset(configKey))
|
|
|
|
) {
|
|
|
|
// Load the config contents
|
|
|
|
core.warning(`Failed to remove '${configKey}' from the git config`)
|
|
|
|
}
|
2020-03-05 20:21:59 +01:00
|
|
|
|
|
|
|
const pattern = regexpHelper.escape(configKey)
|
|
|
|
await this.git.submoduleForeach(
|
|
|
|
`git config --local --name-only --get-regexp ${pattern} && git config --local --unset-all ${configKey} || :`,
|
|
|
|
true
|
|
|
|
)
|
2020-03-02 17:33:30 +01:00
|
|
|
}
|
|
|
|
}
|