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'
2020-03-25 22:12:22 +03:00
import * as urlHelper from './url-helper'
2024-04-24 19:04:10 +03:00
import { v4 as uuid } from 'uuid'
2020-03-02 19:33:30 +03:00
import { IGitCommandManager } from './git-command-manager'
import { IGitSourceSettings } from './git-source-settings'
const IS_WINDOWS = process . platform === 'win32'
2020-03-11 22:55:17 +03:00
const SSH_COMMAND_KEY = 'core.sshCommand'
2020-03-02 19:33:30 +03:00
export interface IGitAuthHelper {
configureAuth ( ) : Promise < void >
2020-03-05 22:21:59 +03:00
configureGlobalAuth ( ) : Promise < void >
configureSubmoduleAuth ( ) : Promise < void >
2022-04-21 04:37:43 +03:00
configureTempGlobalConfig ( ) : Promise < string >
2020-03-02 19:33:30 +03:00
removeAuth ( ) : Promise < void >
2022-04-14 21:13:20 +03:00
removeGlobalConfig ( ) : 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
2020-03-25 22:12:22 +03:00
private readonly tokenConfigKey : string
private readonly tokenConfigValue : string
2020-03-05 22:21:59 +03:00
private readonly tokenPlaceholderConfigValue : string
2020-03-25 22:12:22 +03:00
private readonly insteadOfKey : string
2021-11-01 19:43:18 +03:00
private readonly insteadOfValues : string [ ] = [ ]
2020-03-12 18:42:38 +03:00
private sshCommand = ''
2020-03-11 22:55:17 +03:00
private sshKeyPath = ''
private sshKnownHostsPath = ''
2020-03-05 22:21:59 +03:00
private temporaryHomePath = ''
2020-03-02 19:33:30 +03:00
constructor (
gitCommandManager : IGitCommandManager ,
2021-11-01 19:43:18 +03:00
gitSourceSettings : IGitSourceSettings | undefined
2020-03-02 19:33:30 +03:00
) {
this . git = gitCommandManager
2024-04-24 19:04:10 +03:00
this . settings = gitSourceSettings || ( { } as unknown as IGitSourceSettings )
2020-03-05 22:21:59 +03:00
// Token auth header
2022-09-26 19:34:52 +03:00
const serverUrl = urlHelper . getServerUrl ( this . settings . githubServerUrl )
2020-03-25 22:12:22 +03:00
this . tokenConfigKey = ` http. ${ serverUrl . origin } /.extraheader ` // "origin" is SCHEME://HOSTNAME[:PORT]
2020-03-05 22:21:59 +03:00
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-25 22:12:22 +03:00
// Instead of SSH URL
this . insteadOfKey = ` url. ${ serverUrl . origin } /.insteadOf ` // "origin" is SCHEME://HOSTNAME[:PORT]
2021-11-01 19:43:18 +03:00
this . insteadOfValues . push ( ` git@ ${ serverUrl . hostname } : ` )
if ( this . settings . workflowOrganizationId ) {
this . insteadOfValues . push (
` org- ${ this . settings . workflowOrganizationId } @github.com: `
)
}
2020-03-02 19:33:30 +03:00
}
async configureAuth ( ) : Promise < void > {
// Remove possible previous values
await this . removeAuth ( )
// Configure new values
2020-03-11 22:55:17 +03:00
await this . configureSsh ( )
2020-03-02 19:33:30 +03:00
await this . configureToken ( )
}
2022-04-21 04:37:43 +03:00
async configureTempGlobalConfig ( ) : Promise < string > {
2022-04-14 21:13:20 +03:00
// Already setup global config
if ( this . temporaryHomePath ? . length > 0 ) {
return path . join ( this . temporaryHomePath , '.gitconfig' )
}
2020-03-05 22:21:59 +03:00
// 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 ) {
2021-10-19 17:52:57 +03:00
if ( ( err as any ) ? . code !== 'ENOENT' ) {
2020-03-05 22:21:59 +03:00
throw err
}
}
if ( configExists ) {
core . info ( ` Copying ' ${ gitConfigPath } ' to ' ${ newGitConfigPath } ' ` )
await io . cp ( gitConfigPath , newGitConfigPath )
} else {
await fs . promises . writeFile ( newGitConfigPath , '' )
}
2022-04-14 21:13:20 +03:00
// Override HOME
core . info (
` Temporarily overriding HOME=' ${ this . temporaryHomePath } ' before making global git config changes `
)
this . git . setEnvironmentVariable ( 'HOME' , this . temporaryHomePath )
return newGitConfigPath
}
2020-03-10 17:45:50 +03:00
2022-04-14 21:13:20 +03:00
async configureGlobalAuth ( ) : Promise < void > {
// 'configureTempGlobalConfig' noops if already set, just returns the path
const newGitConfigPath = await this . configureTempGlobalConfig ( )
try {
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 )
2020-03-11 22:55:17 +03:00
if ( ! this . settings . sshKey ) {
2021-11-01 19:43:18 +03:00
for ( const insteadOfValue of this . insteadOfValues ) {
await this . git . config ( this . insteadOfKey , insteadOfValue , true , true )
}
2020-03-11 22:55:17 +03:00
}
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 > {
2020-03-11 22:55:17 +03:00
// Remove possible previous HTTPS instead of SSH
await this . removeGitConfig ( this . insteadOfKey , true )
2020-03-05 22:21:59 +03:00
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 (
2022-11-16 05:51:19 +03:00
// 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 ' ${ this . tokenConfigKey } ' ' ${ this . tokenPlaceholderConfigValue } ' && git config --local --show-origin --name-only --get-regexp remote.origin.url" ` ,
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 } ' ` )
2020-11-03 17:44:09 +03:00
await this . replaceTokenPlaceholder ( configPath )
2020-03-05 22:21:59 +03:00
}
2020-03-11 22:55:17 +03:00
2020-03-12 18:42:38 +03:00
if ( this . settings . sshKey ) {
// Configure core.sshCommand
await this . git . submoduleForeach (
` git config --local ' ${ SSH_COMMAND_KEY } ' ' ${ this . sshCommand } ' ` ,
this . settings . nestedSubmodules
)
} else {
// Configure HTTPS instead of SSH
2021-11-01 19:43:18 +03:00
for ( const insteadOfValue of this . insteadOfValues ) {
await this . git . submoduleForeach (
` git config --local --add ' ${ this . insteadOfKey } ' ' ${ insteadOfValue } ' ` ,
this . settings . nestedSubmodules
)
}
2020-03-11 22:55:17 +03:00
}
2020-03-05 22:21:59 +03:00
}
}
2020-03-02 19:33:30 +03:00
async removeAuth ( ) : Promise < void > {
2020-03-11 22:55:17 +03:00
await this . removeSsh ( )
2020-03-02 19:33:30 +03:00
await this . removeToken ( )
}
2022-04-14 21:13:20 +03:00
async removeGlobalConfig ( ) : Promise < void > {
if ( this . temporaryHomePath ? . length > 0 ) {
core . debug ( ` Unsetting HOME override ` )
this . git . removeEnvironmentVariable ( 'HOME' )
await io . rmRF ( this . temporaryHomePath )
}
2020-03-05 22:21:59 +03:00
}
2020-03-11 22:55:17 +03:00
private async configureSsh ( ) : Promise < void > {
if ( ! this . settings . sshKey ) {
return
}
// Write key
const runnerTemp = process . env [ 'RUNNER_TEMP' ] || ''
assert . ok ( runnerTemp , 'RUNNER_TEMP is not defined' )
const uniqueId = uuid ( )
this . sshKeyPath = path . join ( runnerTemp , uniqueId )
stateHelper . setSshKeyPath ( this . sshKeyPath )
await fs . promises . mkdir ( runnerTemp , { recursive : true } )
await fs . promises . writeFile (
this . sshKeyPath ,
this . settings . sshKey . trim ( ) + '\n' ,
{ mode : 0o600 }
)
// Remove inherited permissions on Windows
if ( IS_WINDOWS ) {
const icacls = await io . which ( 'icacls.exe' )
await exec . exec (
` " ${ icacls } " " ${ this . sshKeyPath } " /grant:r " ${ process . env [ 'USERDOMAIN' ] } \\ ${ process . env [ 'USERNAME' ] } :F" `
)
await exec . exec ( ` " ${ icacls } " " ${ this . sshKeyPath } " /inheritance:r ` )
}
// Write known hosts
const userKnownHostsPath = path . join ( os . homedir ( ) , '.ssh' , 'known_hosts' )
let userKnownHosts = ''
try {
userKnownHosts = (
await fs . promises . readFile ( userKnownHostsPath )
) . toString ( )
} catch ( err ) {
2021-10-19 17:52:57 +03:00
if ( ( err as any ) ? . code !== 'ENOENT' ) {
2020-03-11 22:55:17 +03:00
throw err
}
}
let knownHosts = ''
if ( userKnownHosts ) {
knownHosts += ` # Begin from ${ userKnownHostsPath } \ n ${ userKnownHosts } \ n# End from ${ userKnownHostsPath } \ n `
}
if ( this . settings . sshKnownHosts ) {
knownHosts += ` # Begin from input known hosts \ n ${ this . settings . sshKnownHosts } \ n# end from input known hosts \ n `
}
2023-03-24 08:34:48 +03:00
knownHosts += ` # Begin implicitly added github.com \ ngithub.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCj7ndNxQowgcQnjshcLrqPEiiphnt+VTTvDP6mHBL9j1aNUkY4Ue1gvwnGLVlOhGeYrnZaMgRK6+PKCUXaDbC7qtbW8gIkhL7aGCsOr/C56SJMy/BCZfxd1nWzAOxSDPgVsmerOBYfNqltV9/hWCqBywINIR+5dIg6JTJ72pcEpEjcYgXkE2YEFXV1JHnsKgbLWNlhScqb2UmyRkQyytRLtL+38TGxkxCflmO+5Z8CSSNY7GidjMIZ7Q4zMjA2n1nGrlTDkzwDCsw+wqFPGQA179cnfGWOWRVruj16z6XyvxvjJwbz0wQZ75XK5tKSb7FNyeIEs4TT4jk+S4dhPeAUC5y+bDYirYgM4GC7uEnztnZyaVWQ7B381AK4Qdrwt51ZqExKbQpTUNn+EjqoTwvqNj4kqx5QUCI0ThS/YkOxJCXmPUWZbhjpCg56i+2aB6CmK2JGhn57K5mj0MNdBXA4/WnwH6XoPWJzK5Nyu2zB3nAZp+S5hpQs+p1vN1/wsjk= \ n# End implicitly added github.com \ n `
2020-03-11 22:55:17 +03:00
this . sshKnownHostsPath = path . join ( runnerTemp , ` ${ uniqueId } _known_hosts ` )
stateHelper . setSshKnownHostsPath ( this . sshKnownHostsPath )
await fs . promises . writeFile ( this . sshKnownHostsPath , knownHosts )
// Configure GIT_SSH_COMMAND
const sshPath = await io . which ( 'ssh' , true )
2020-03-12 18:42:38 +03:00
this . sshCommand = ` " ${ sshPath } " -i " $ RUNNER_TEMP/ ${ path . basename (
2020-03-11 22:55:17 +03:00
this . sshKeyPath
) } " `
if ( this . settings . sshStrict ) {
2020-03-12 18:42:38 +03:00
this . sshCommand += ' -o StrictHostKeyChecking=yes -o CheckHostIP=no'
2020-03-11 22:55:17 +03:00
}
2020-03-12 18:42:38 +03:00
this . sshCommand += ` -o "UserKnownHostsFile= $ RUNNER_TEMP/ ${ path . basename (
2020-03-11 22:55:17 +03:00
this . sshKnownHostsPath
) } " `
2020-03-12 18:42:38 +03:00
core . info ( ` Temporarily overriding GIT_SSH_COMMAND= ${ this . sshCommand } ` )
this . git . setEnvironmentVariable ( 'GIT_SSH_COMMAND' , this . sshCommand )
2020-03-11 22:55:17 +03:00
// Configure core.sshCommand
if ( this . settings . persistCredentials ) {
2020-03-12 18:42:38 +03:00
await this . git . config ( SSH_COMMAND_KEY , this . sshCommand )
2020-03-11 22:55:17 +03:00
}
}
2020-03-05 22:21:59 +03:00
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 )
}
2020-03-11 22:55:17 +03:00
private async removeSsh ( ) : Promise < void > {
// SSH key
const keyPath = this . sshKeyPath || stateHelper . SshKeyPath
if ( keyPath ) {
try {
await io . rmRF ( keyPath )
} catch ( err ) {
2021-10-19 17:52:57 +03:00
core . debug ( ` ${ ( err as any ) ? . message ? ? err } ` )
2020-03-11 22:55:17 +03:00
core . warning ( ` Failed to remove SSH key ' ${ keyPath } ' ` )
}
}
// SSH known hosts
const knownHostsPath =
this . sshKnownHostsPath || stateHelper . SshKnownHostsPath
if ( knownHostsPath ) {
try {
await io . rmRF ( knownHostsPath )
} catch {
// Intentionally empty
}
}
// SSH command
await this . removeGitConfig ( SSH_COMMAND_KEY )
}
2020-03-02 19:33:30 +03:00
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
}
2020-03-11 22:55:17 +03:00
private async removeGitConfig (
configKey : string ,
submoduleOnly : boolean = false
) : Promise < void > {
if ( ! submoduleOnly ) {
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-02 19:33:30 +03:00
}
2020-03-05 22:21:59 +03:00
const pattern = regexpHelper . escape ( configKey )
await this . git . submoduleForeach (
2022-11-16 05:51:19 +03:00
// 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 --name-only --get-regexp ' ${ pattern } ' && git config --local --unset-all ' ${ configKey } ' || :" ` ,
2020-03-05 22:21:59 +03:00
true
)
2020-03-02 19:33:30 +03:00
}
}