Refactor extraheader auth handling

This commit is contained in:
Peter Evans 2020-07-17 20:54:39 +09:00
parent a6a1a418bf
commit 24012f5c84
9 changed files with 460 additions and 419 deletions

View file

@ -2,13 +2,10 @@ import * as core from '@actions/core'
import {createOrUpdateBranch} from './create-or-update-branch'
import {GitHubHelper} from './github-helper'
import {GitCommandManager} from './git-command-manager'
import {ConfigOption, GitConfigHelper} from './git-config-helper'
import {GitAuthHelper} from './git-auth-helper'
import {GitIdentityHelper} from './git-identity-helper'
import * as utils from './utils'
const EXTRAHEADER_OPTION = 'http.https://github.com/.extraheader'
const EXTRAHEADER_VALUE_REGEX = '^AUTHORIZATION:'
const DEFAULT_COMMIT_MESSAGE = '[create-pull-request] automated change'
const DEFAULT_TITLE = 'Changes by create-pull-request action'
const DEFAULT_BODY =
@ -36,21 +33,17 @@ export interface Inputs {
}
export async function createPullRequest(inputs: Inputs): Promise<void> {
let gitConfigHelper
let extraHeaderOption = new ConfigOption()
let gitAuthHelper
try {
// Get the repository path
const repoPath = utils.getRepoPath(inputs.path)
// Create a git command manager
const git = await GitCommandManager.create(repoPath)
// Unset and save the extraheader config option if it exists
// Save and unset the extraheader auth config if it exists
core.startGroup('Save persisted git credentials')
gitConfigHelper = new GitConfigHelper(git)
extraHeaderOption = await gitConfigHelper.getAndUnsetConfigOption(
EXTRAHEADER_OPTION,
EXTRAHEADER_VALUE_REGEX
)
gitAuthHelper = new GitAuthHelper(git)
await gitAuthHelper.savePersistedAuth()
core.endGroup()
// Set defaults
@ -64,10 +57,8 @@ export async function createPullRequest(inputs: Inputs): Promise<void> {
// Determine the GitHub repository from git config
// This will be the target repository for the pull request branch
core.startGroup('Determining the checked out repository')
const remoteOriginUrlConfig = await gitConfigHelper.getConfigOption(
'remote.origin.url'
)
const remote = utils.getRemoteDetail(remoteOriginUrlConfig.value)
const remoteUrl = await git.tryGetRemoteUrl()
const remote = utils.getRemoteDetail(remoteUrl)
core.endGroup()
core.info(
`Pull request branch target repository set to ${remote.repository}`
@ -75,16 +66,7 @@ export async function createPullRequest(inputs: Inputs): Promise<void> {
if (remote.protocol == 'HTTPS') {
core.startGroup('Configuring credential for HTTPS authentication')
// Encode and configure the basic credential for HTTPS access
const basicCredential = Buffer.from(
`x-access-token:${inputs.token}`,
'utf8'
).toString('base64')
core.setSecret(basicCredential)
git.setAuthGitOptions([
'-c',
`http.https://github.com/.extraheader=AUTHORIZATION: basic ${basicCredential}`
])
await gitAuthHelper.configureAuth(inputs.token)
core.endGroup()
}
@ -216,17 +198,10 @@ export async function createPullRequest(inputs: Inputs): Promise<void> {
} catch (error) {
core.setFailed(error.message)
} finally {
// Restore the extraheader config option
// Remove auth and restore persisted auth config if it existed
core.startGroup('Restore persisted git credentials')
if (extraHeaderOption.value != '') {
if (
await gitConfigHelper.addConfigOption(
EXTRAHEADER_OPTION,
extraHeaderOption.value
)
)
core.debug(`Restored config option '${EXTRAHEADER_OPTION}'`)
}
await gitAuthHelper.removeAuth()
await gitAuthHelper.restorePersistedAuth()
core.endGroup()
}
}

126
src/git-auth-helper.ts Normal file
View file

@ -0,0 +1,126 @@
import * as core from '@actions/core'
import * as fs from 'fs'
import {GitCommandManager} from './git-command-manager'
import * as path from 'path'
import {URL} from 'url'
export class GitAuthHelper {
private git: GitCommandManager
private gitConfigPath: string
private extraheaderConfigKey: string
private extraheaderConfigPlaceholderValue = 'AUTHORIZATION: basic ***'
private extraheaderConfigValueRegex = '^AUTHORIZATION:'
private persistedExtraheaderConfigValue = ''
constructor(git: GitCommandManager) {
this.git = git
this.gitConfigPath = path.join(
this.git.getWorkingDirectory(),
'.git',
'config'
)
const serverUrl = this.getServerUrl()
this.extraheaderConfigKey = `http.${serverUrl.origin}/.extraheader`
}
async savePersistedAuth(): Promise<void> {
// Save and unset persisted extraheader credential in git config if it exists
this.persistedExtraheaderConfigValue = await this.getAndUnset()
}
async restorePersistedAuth(): Promise<void> {
if (this.persistedExtraheaderConfigValue) {
try {
await this.setExtraheaderConfig(this.persistedExtraheaderConfigValue)
core.info('Persisted git credentials restored')
} catch (e) {
core.warning(e)
}
}
}
async configureToken(token: string): Promise<void> {
// Encode and configure the basic credential for HTTPS access
const basicCredential = Buffer.from(
`x-access-token:${token}`,
'utf8'
).toString('base64')
core.setSecret(basicCredential)
const extraheaderConfigValue = `AUTHORIZATION: basic ${basicCredential}`
await this.setExtraheaderConfig(extraheaderConfigValue)
}
async removeAuth(): Promise<void> {
await this.getAndUnset()
}
private async setExtraheaderConfig(
extraheaderConfigValue: string
): Promise<void> {
// 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
// See https://github.com/actions/checkout/blob/main/src/git-auth-helper.ts#L267-L274
await this.git.config(
this.extraheaderConfigKey,
this.extraheaderConfigPlaceholderValue
)
// Replace the placeholder
await this.gitConfigStringReplace(
this.extraheaderConfigPlaceholderValue,
extraheaderConfigValue
)
}
private async getAndUnset(): Promise<string> {
let configValue = ''
// Save and unset persisted extraheader credential in git config if it exists
if (
await this.git.configExists(
this.extraheaderConfigKey,
this.extraheaderConfigValueRegex
)
) {
configValue = await this.git.getConfigValue(
this.extraheaderConfigKey,
this.extraheaderConfigValueRegex
)
if (
await this.git.tryConfigUnset(
this.extraheaderConfigKey,
this.extraheaderConfigValueRegex
)
) {
core.info(`Unset config key '${this.extraheaderConfigKey}'`)
} else {
core.warning(
`Failed to unset config key '${this.extraheaderConfigKey}'`
)
}
}
return configValue
}
private async gitConfigStringReplace(
find: string,
replace: string
): Promise<void> {
let content = (await fs.promises.readFile(this.gitConfigPath)).toString()
const index = content.indexOf(find)
if (index < 0 || index != content.lastIndexOf(find)) {
throw new Error(`Unable to replace '${find}' in ${this.gitConfigPath}`)
}
content = content.replace(find, replace)
await fs.promises.writeFile(this.gitConfigPath, content)
}
private getServerUrl(): URL {
// todo: remove GITHUB_URL after support for GHES Alpha is no longer needed
// See https://github.com/actions/checkout/blob/main/src/url-helper.ts#L22-L29
return new URL(
process.env['GITHUB_SERVER_URL'] ||
process.env['GITHUB_URL'] ||
'https://github.com'
)
}
}

View file

@ -6,8 +6,6 @@ const tagsRefSpec = '+refs/tags/*:refs/tags/*'
export class GitCommandManager {
private gitPath: string
private workingDirectory: string
// Git options used when commands require auth
private authGitOptions?: string[]
// Git options used when commands require an identity
private identityGitOptions?: string[]
@ -21,10 +19,6 @@ export class GitCommandManager {
return new GitCommandManager(workingDirectory, gitPath)
}
setAuthGitOptions(authGitOptions: string[]): void {
this.authGitOptions = authGitOptions
}
setIdentityGitOptions(identityGitOptions: string[]): void {
this.identityGitOptions = identityGitOptions
}
@ -68,6 +62,38 @@ export class GitCommandManager {
await this.exec(args)
}
async config(
configKey: string,
configValue: string,
globalConfig?: boolean
): Promise<void> {
await this.exec([
'config',
globalConfig ? '--global' : '--local',
configKey,
configValue
])
}
async configExists(
configKey: string,
configValue = '.',
globalConfig?: boolean
): Promise<boolean> {
const output = await this.exec(
[
'config',
globalConfig ? '--global' : '--local',
'--name-only',
'--get-regexp',
configKey,
configValue
],
true
)
return output.exitCode === 0
}
async diff(options?: string[]): Promise<string> {
const args = ['-c', 'core.pager=cat', 'diff']
if (options) {
@ -82,12 +108,7 @@ export class GitCommandManager {
remoteName?: string,
options?: string[]
): Promise<void> {
const args = ['-c', 'protocol.version=2']
if (this.authGitOptions) {
args.push(...this.authGitOptions)
}
args.push('fetch')
const args = ['-c', 'protocol.version=2', 'fetch']
if (!refSpec.some(x => x === tagsRefSpec)) {
args.push('--no-tags')
}
@ -110,6 +131,17 @@ export class GitCommandManager {
await this.exec(args)
}
async getConfigValue(configKey: string, configValue = '.'): Promise<string> {
const output = await this.exec([
'config',
'--local',
'--get-regexp',
configKey,
configValue
])
return output.stdout.trim().split(`${configKey} `)[1]
}
getWorkingDirectory(): string {
return this.workingDirectory
}
@ -133,14 +165,9 @@ export class GitCommandManager {
async push(options?: string[]): Promise<void> {
const args = ['push']
if (this.authGitOptions) {
args.unshift(...this.authGitOptions)
}
if (options) {
args.push(...options)
}
await this.exec(args)
}
@ -187,21 +214,23 @@ export class GitCommandManager {
async tryConfigUnset(
configKey: string,
configValue = '.',
globalConfig?: boolean
): Promise<boolean> {
const output = await this.exec(
[
'config',
globalConfig ? '--global' : '--local',
'--unset-all',
configKey
'--unset',
configKey,
configValue
],
true
)
return output.exitCode === 0
}
async tryGetFetchUrl(): Promise<string> {
async tryGetRemoteUrl(): Promise<string> {
const output = await this.exec(
['config', '--local', '--get', 'remote.origin.url'],
true

View file

@ -1,64 +0,0 @@
import * as core from '@actions/core'
import {GitCommandManager} from './git-command-manager'
export class ConfigOption {
name = ''
value = ''
}
export class GitConfigHelper {
private git: GitCommandManager
constructor(git: GitCommandManager) {
this.git = git
}
async addConfigOption(name: string, value: string): Promise<boolean> {
const result = await this.git.exec(
['config', '--local', '--add', name, value],
true
)
return result.exitCode === 0
}
async unsetConfigOption(name: string, valueRegex = '.'): Promise<boolean> {
const result = await this.git.exec(
['config', '--local', '--unset', name, valueRegex],
true
)
return result.exitCode === 0
}
async configOptionExists(name: string, valueRegex = '.'): Promise<boolean> {
const result = await this.git.exec(
['config', '--local', '--name-only', '--get-regexp', name, valueRegex],
true
)
return result.exitCode === 0
}
async getConfigOption(name: string, valueRegex = '.'): Promise<ConfigOption> {
const option = new ConfigOption()
const result = await this.git.exec(
['config', '--local', '--get-regexp', name, valueRegex],
true
)
option.name = name
option.value = result.stdout.trim().split(`${name} `)[1]
return option
}
async getAndUnsetConfigOption(
name: string,
valueRegex = '.'
): Promise<ConfigOption> {
if (await this.configOptionExists(name, valueRegex)) {
const option = await this.getConfigOption(name, valueRegex)
if (await this.unsetConfigOption(name, valueRegex)) {
core.debug(`Unset config option '${name}'`)
return option
}
}
return new ConfigOption()
}
}

View file

@ -1,6 +1,5 @@
import * as core from '@actions/core'
import {GitCommandManager} from './git-command-manager'
import {GitConfigHelper} from './git-config-helper'
import * as utils from './utils'
// Default the committer and author to the GitHub Actions bot
@ -23,41 +22,35 @@ export class GitIdentityHelper {
}
private async getGitIdentityFromConfig(): Promise<GitIdentity | undefined> {
const gitConfigHelper = new GitConfigHelper(this.git)
if (
(await gitConfigHelper.configOptionExists('user.name')) &&
(await gitConfigHelper.configOptionExists('user.email'))
(await this.git.configExists('user.name')) &&
(await this.git.configExists('user.email'))
) {
const userName = await gitConfigHelper.getConfigOption('user.name')
const userEmail = await gitConfigHelper.getConfigOption('user.email')
const userName = await this.git.getConfigValue('user.name')
const userEmail = await this.git.getConfigValue('user.email')
return {
authorName: userName.value,
authorEmail: userEmail.value,
committerName: userName.value,
committerEmail: userEmail.value
authorName: userName,
authorEmail: userEmail,
committerName: userName,
committerEmail: userEmail
}
}
if (
(await gitConfigHelper.configOptionExists('committer.name')) &&
(await gitConfigHelper.configOptionExists('committer.email')) &&
(await gitConfigHelper.configOptionExists('author.name')) &&
(await gitConfigHelper.configOptionExists('author.email'))
(await this.git.configExists('committer.name')) &&
(await this.git.configExists('committer.email')) &&
(await this.git.configExists('author.name')) &&
(await this.git.configExists('author.email'))
) {
const committerName = await gitConfigHelper.getConfigOption(
'committer.name'
)
const committerEmail = await gitConfigHelper.getConfigOption(
'committer.email'
)
const authorName = await gitConfigHelper.getConfigOption('author.name')
const authorEmail = await gitConfigHelper.getConfigOption('author.email')
const committerName = await this.git.getConfigValue('committer.name')
const committerEmail = await this.git.getConfigValue('committer.email')
const authorName = await this.git.getConfigValue('author.name')
const authorEmail = await this.git.getConfigValue('author.email')
return {
authorName: authorName.value,
authorEmail: authorEmail.value,
committerName: committerName.value,
committerEmail: committerEmail.value
authorName: authorName,
authorEmail: authorEmail,
committerName: committerName,
committerEmail: committerEmail
}
}