2020-03-02 19:33:30 +03: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 22:21:59 +03:00
import * as regexpHelper from './regexp-helper'
2020-03-02 19:33:30 +03: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 22:21:59 +03:00
configureGlobalAuth ( ) : Promise < void >
configureSubmoduleAuth ( ) : Promise < void >
2020-03-02 19:33:30 +03:00
removeAuth ( ) : Promise < void >
2020-03-05 22:21:59 +03:00
removeGlobalAuth ( ) : Promise < void >
2020-03-02 19:33:30 +03:00
}
export function createAuthHelper (
git : IGitCommandManager ,
settings? : IGitSourceSettings
) : IGitAuthHelper {
return new GitAuthHelper ( git , settings )
}
class GitAuthHelper {
2020-03-05 22:21:59 +03:00
private readonly git : IGitCommandManager
private readonly settings : IGitSourceSettings
private readonly tokenConfigKey : string = ` http.https:// ${ HOSTNAME } /.extraheader `
private readonly tokenPlaceholderConfigValue : string
2020-03-10 17:45:50 +03:00
private readonly insteadOfKey : string = ` url.https:// ${ HOSTNAME } /.insteadOf `
private readonly insteadOfValue : string = ` git@ ${ HOSTNAME } : `
2020-03-05 22:21:59 +03:00
private temporaryHomePath = ''
private tokenConfigValue : string
2020-03-02 19:33:30 +03:00
constructor (
gitCommandManager : IGitCommandManager ,
gitSourceSettings? : IGitSourceSettings
) {
this . git = gitCommandManager
this . settings = gitSourceSettings || ( ( { } as unknown ) as IGitSourceSettings )
2020-03-05 22:21:59 +03: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 19:33:30 +03:00
}
async configureAuth ( ) : Promise < void > {
// Remove possible previous values
await this . removeAuth ( )
// Configure new values
await this . configureToken ( )
}
2020-03-05 22:21:59 +03: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 17:45:50 +03:00
// Override HOME
2020-03-05 22:21:59 +03:00
core . info (
` Temporarily overriding HOME=' ${ this . temporaryHomePath } ' before making global git config changes `
)
this . git . setEnvironmentVariable ( 'HOME' , this . temporaryHomePath )
2020-03-10 17:45:50 +03:00
// Configure the token
2020-03-05 22:21:59 +03:00
await this . configureToken ( newGitConfigPath , true )
2020-03-10 17:45:50 +03: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 22:21:59 +03: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 17:45:50 +03: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 22:21:59 +03:00
const output = await this . git . submoduleForeach (
2020-03-10 17:45:50 +03:00
commands . join ( ' && ' ) ,
2020-03-05 22:21:59 +03: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 19:33:30 +03:00
async removeAuth ( ) : Promise < void > {
await this . removeToken ( )
}
2020-03-05 22:21:59 +03: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 19:33:30 +03: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 22:21:59 +03:00
await this . git . config (
this . tokenConfigKey ,
this . tokenPlaceholderConfigValue ,
globalConfig
)
2020-03-02 19:33:30 +03:00
2020-03-05 22:21:59 +03:00
// Replace the placeholder
await this . replaceTokenPlaceholder ( configPath || '' )
}
2020-03-02 19:33:30 +03:00
2020-03-05 22:21:59 +03:00
private async replaceTokenPlaceholder ( configPath : string ) : Promise < void > {
assert . ok ( configPath , 'configPath is not defined' )
2020-03-02 19:33:30 +03:00
let content = ( await fs . promises . readFile ( configPath ) ) . toString ( )
2020-03-05 22:21:59 +03:00
const placeholderIndex = content . indexOf ( this . tokenPlaceholderConfigValue )
2020-03-02 19:33:30 +03:00
if (
placeholderIndex < 0 ||
2020-03-05 22:21:59 +03:00
placeholderIndex != content . lastIndexOf ( this . tokenPlaceholderConfigValue )
2020-03-02 19:33:30 +03:00
) {
2020-03-05 22:21:59 +03:00
throw new Error ( ` Unable to replace auth placeholder in ${ configPath } ` )
2020-03-02 19:33:30 +03:00
}
2020-03-05 22:21:59 +03:00
assert . ok ( this . tokenConfigValue , 'tokenConfigValue is not defined' )
2020-03-02 19:33:30 +03:00
content = content . replace (
2020-03-05 22:21:59 +03:00
this . tokenPlaceholderConfigValue ,
this . tokenConfigValue
2020-03-02 19:33:30 +03:00
)
await fs . promises . writeFile ( configPath , content )
}
private async removeToken ( ) : Promise < void > {
// HTTP extra header
2020-03-05 22:21:59 +03:00
await this . removeGitConfig ( this . tokenConfigKey )
2020-03-02 19:33:30 +03: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 22:21:59 +03: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 19:33:30 +03:00
}
}