Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use credentials store instead of extraheader #1754

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ Please refer to the [release page](https://github.com/actions/checkout/releases/
# Default: true
persist-credentials: ''

# Custom git credential helper
custom-credential-helper: ''

# Relative path under $GITHUB_WORKSPACE to place the repository
path: ''

Expand Down
2 changes: 2 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ inputs:
persist-credentials:
description: 'Whether to configure the token or SSH key with the local git config'
default: true
custom-credential-helper:
description: 'Custom git credential helper'
path:
description: 'Relative path under $GITHUB_WORKSPACE to place the repository'
clean:
Expand Down
90 changes: 49 additions & 41 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -166,13 +166,16 @@ class GitAuthHelper {
this.temporaryHomePath = '';
this.git = gitCommandManager;
this.settings = gitSourceSettings || {};
// Token auth header
this.credentialConfigKey = `credential.helper`;
const runnerTemp = process.env['RUNNER_TEMP'] || '';
assert.ok(runnerTemp, 'RUNNER_TEMP is not defined');
const uniqueId = (0, uuid_1.v4)();
this.credentialStorePath = path.join(runnerTemp, `${uniqueId}_credential_store`);
this.credentialConfigValue = `store --file ${this.credentialStorePath}`;
const serverUrl = urlHelper.getServerUrl(this.settings.githubServerUrl);
this.tokenConfigKey = `http.${serverUrl.origin}/.extraheader`; // "origin" is SCHEME://HOSTNAME[:PORT]
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}`;
serverUrl.username = `x-access-token`;
serverUrl.password = this.settings.authToken;
this.tokenCredential = serverUrl.href;
// Instead of SSH URL
this.insteadOfKey = `url.${serverUrl.origin}/.insteadOf`; // "origin" is SCHEME://HOSTNAME[:PORT]
this.insteadOfValues.push(`git@${serverUrl.hostname}:`);
Expand Down Expand Up @@ -246,7 +249,7 @@ class GitAuthHelper {
catch (err) {
// Unset in case somehow written to the real global config
core.info('Encountered an error when attempting to configure token. Attempting unconfigure.');
yield this.git.tryConfigUnset(this.tokenConfigKey, true);
yield this.git.tryConfigUnset(this.credentialConfigKey, true);
throw err;
}
});
Expand All @@ -256,18 +259,12 @@ class GitAuthHelper {
// Remove possible previous HTTPS instead of SSH
yield this.removeGitConfig(this.insteadOfKey, true);
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 = yield this.git.submoduleForeach(
// 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"`, this.settings.nestedSubmodules);
// Replace the placeholder
const configPaths = output.match(/(?<=(^|\n)file:)[^\t]+(?=\tremote\.origin\.url)/g) || [];
for (const configPath of configPaths) {
core.debug(`Replacing token placeholder in '${configPath}'`);
yield this.replaceTokenPlaceholder(configPath);
if (this.settings.customCredentialHelper) {
yield this.git.submoduleForeach(`sh -c "git config --local --add '${this.credentialConfigKey}' '${this.settings.customCredentialHelper}' && git config --local 'credential.useHttpPath' 'true'"`, this.settings.nestedSubmodules);
}
yield this.git.submoduleForeach(
// 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 --add '${this.credentialConfigKey}' '${this.credentialConfigValue}' && git config --local --show-origin --name-only --get-regexp remote.origin.url"`, this.settings.nestedSubmodules);
if (this.settings.sshKey) {
// Configure core.sshCommand
yield this.git.submoduleForeach(`git config --local '${SSH_COMMAND_KEY}' '${this.sshCommand}'`, this.settings.nestedSubmodules);
Expand Down Expand Up @@ -306,7 +303,7 @@ class GitAuthHelper {
const runnerTemp = process.env['RUNNER_TEMP'] || '';
assert.ok(runnerTemp, 'RUNNER_TEMP is not defined');
const uniqueId = (0, uuid_1.v4)();
this.sshKeyPath = path.join(runnerTemp, uniqueId);
this.sshKeyPath = path.join(runnerTemp, `${uniqueId}_ssh_key`);
stateHelper.setSshKeyPath(this.sshKeyPath);
yield fs.promises.mkdir(runnerTemp, { recursive: true });
yield fs.promises.writeFile(this.sshKeyPath, this.settings.sshKey.trim() + '\n', { mode: 0o600 });
Expand Down Expand Up @@ -357,30 +354,17 @@ class GitAuthHelper {
return __awaiter(this, void 0, void 0, function* () {
// Validate args
assert.ok((configPath && globalConfig) || (!configPath && !globalConfig), 'Unexpected configureToken parameter combinations');
stateHelper.setCredentialStorePath(this.credentialStorePath);
yield fs.promises.writeFile(this.credentialStorePath, this.tokenCredential);
// 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
yield this.git.config(this.tokenConfigKey, this.tokenPlaceholderConfigValue, globalConfig);
// Replace the placeholder
yield this.replaceTokenPlaceholder(configPath || '');
});
}
replaceTokenPlaceholder(configPath) {
return __awaiter(this, void 0, void 0, function* () {
assert.ok(configPath, 'configPath is not defined');
let content = (yield fs.promises.readFile(configPath)).toString();
const placeholderIndex = content.indexOf(this.tokenPlaceholderConfigValue);
if (placeholderIndex < 0 ||
placeholderIndex != content.lastIndexOf(this.tokenPlaceholderConfigValue)) {
throw new Error(`Unable to replace auth placeholder in ${configPath}`);
if (this.settings.customCredentialHelper) {
yield this.git.config(this.credentialConfigKey, this.settings.customCredentialHelper, globalConfig, true);
yield this.git.config('credential.useHttpPath', 'true', globalConfig);
}
assert.ok(this.tokenConfigValue, 'tokenConfigValue is not defined');
content = content.replace(this.tokenPlaceholderConfigValue, this.tokenConfigValue);
yield fs.promises.writeFile(configPath, content);
yield this.git.config(this.credentialConfigKey, this.credentialConfigValue, globalConfig, true);
});
}
removeSsh() {
Expand Down Expand Up @@ -413,8 +397,19 @@ class GitAuthHelper {
}
removeToken() {
return __awaiter(this, void 0, void 0, function* () {
// HTTP extra header
yield this.removeGitConfig(this.tokenConfigKey);
var _a;
// Credential Helper
const credentialStorePath = this.credentialStorePath || stateHelper.CredentialStorePath;
if (credentialStorePath) {
try {
yield io.rmRF(credentialStorePath);
}
catch (err) {
core.debug(`${(_a = err === null || err === void 0 ? void 0 : err.message) !== null && _a !== void 0 ? _a : err}`);
core.warning(`Failed to remove credential store '${credentialStorePath}'`);
}
}
yield this.removeGitConfig(this.credentialConfigKey);
});
}
removeGitConfig(configKey_1) {
Expand Down Expand Up @@ -1826,6 +1821,8 @@ function getInputs() {
// Persist credentials
result.persistCredentials =
(core.getInput('persist-credentials') || 'false').toUpperCase() === 'TRUE';
// Custom credential helper
result.customCredentialHelper = core.getInput('custom-credential-helper');
// Workflow organization ID
result.workflowOrganizationId =
yield workflowContextHelper.getOrganizationId();
Expand Down Expand Up @@ -2347,7 +2344,7 @@ var __importStar = (this && this.__importStar) || function (mod) {
return result;
};
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.setSafeDirectory = exports.setSshKnownHostsPath = exports.setSshKeyPath = exports.setRepositoryPath = exports.SshKnownHostsPath = exports.SshKeyPath = exports.PostSetSafeDirectory = exports.RepositoryPath = exports.IsPost = void 0;
exports.setCredentialStorePath = exports.setSafeDirectory = exports.setSshKnownHostsPath = exports.setSshKeyPath = exports.setRepositoryPath = exports.CredentialStorePath = exports.SshKnownHostsPath = exports.SshKeyPath = exports.PostSetSafeDirectory = exports.RepositoryPath = exports.IsPost = void 0;
const core = __importStar(__nccwpck_require__(2186));
/**
* Indicates whether the POST action is running
Expand All @@ -2369,6 +2366,10 @@ exports.SshKeyPath = core.getState('sshKeyPath');
* The SSH known hosts path for the POST action. The value is empty during the MAIN action.
*/
exports.SshKnownHostsPath = core.getState('sshKnownHostsPath');
/**
* The credential store path for git-credential-store
*/
exports.CredentialStorePath = core.getState('credentialStorePath');
/**
* Save the repository path so the POST action can retrieve the value.
*/
Expand Down Expand Up @@ -2402,6 +2403,13 @@ exports.setSafeDirectory = setSafeDirectory;
if (!exports.IsPost) {
core.saveState('isPost', 'true');
}
/**
* Save the credential store path so the POST action can retrieve the value.
*/
function setCredentialStorePath(credentialStorePath) {
core.saveState('credentialStorePath', credentialStorePath);
}
exports.setCredentialStorePath = setCredentialStorePath;


/***/ }),
Expand Down
114 changes: 59 additions & 55 deletions src/git-auth-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,10 @@ export function createAuthHelper(
class GitAuthHelper {
private readonly git: IGitCommandManager
private readonly settings: IGitSourceSettings
private readonly tokenConfigKey: string
private readonly tokenConfigValue: string
private readonly tokenPlaceholderConfigValue: string
private readonly credentialConfigKey: string
private readonly credentialConfigValue: string
private readonly tokenCredential: string
private readonly credentialStorePath: string
private readonly insteadOfKey: string
private readonly insteadOfValues: string[] = []
private sshCommand = ''
Expand All @@ -51,16 +52,20 @@ class GitAuthHelper {
this.git = gitCommandManager
this.settings = gitSourceSettings || ({} as unknown as IGitSourceSettings)

// Token auth header
this.credentialConfigKey = `credential.helper`
const runnerTemp = process.env['RUNNER_TEMP'] || ''
assert.ok(runnerTemp, 'RUNNER_TEMP is not defined')
const uniqueId = uuid()
this.credentialStorePath = path.join(
runnerTemp,
`${uniqueId}_credential_store`
)
this.credentialConfigValue = `store --file ${this.credentialStorePath}`

const serverUrl = urlHelper.getServerUrl(this.settings.githubServerUrl)
this.tokenConfigKey = `http.${serverUrl.origin}/.extraheader` // "origin" is SCHEME://HOSTNAME[:PORT]
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}`
serverUrl.username = `x-access-token`
serverUrl.password = this.settings.authToken
this.tokenCredential = serverUrl.href

// Instead of SSH URL
this.insteadOfKey = `url.${serverUrl.origin}/.insteadOf` // "origin" is SCHEME://HOSTNAME[:PORT]
Expand Down Expand Up @@ -143,7 +148,7 @@ class GitAuthHelper {
core.info(
'Encountered an error when attempting to configure token. Attempting unconfigure.'
)
await this.git.tryConfigUnset(this.tokenConfigKey, true)
await this.git.tryConfigUnset(this.credentialConfigKey, true)
throw err
}
}
Expand All @@ -153,23 +158,19 @@ class GitAuthHelper {
await this.removeGitConfig(this.insteadOfKey, true)

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(
if (this.settings.customCredentialHelper) {
await this.git.submoduleForeach(
`sh -c "git config --local --add '${this.credentialConfigKey}' '${this.settings.customCredentialHelper}' && git config --local 'credential.useHttpPath' 'true'"`,
this.settings.nestedSubmodules
)
}

await this.git.submoduleForeach(
// 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"`,
`sh -c "git config --local --add '${this.credentialConfigKey}' '${this.credentialConfigValue}' && 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}'`)
await this.replaceTokenPlaceholder(configPath)
}

if (this.settings.sshKey) {
// Configure core.sshCommand
await this.git.submoduleForeach(
Expand Down Expand Up @@ -210,7 +211,7 @@ class GitAuthHelper {
const runnerTemp = process.env['RUNNER_TEMP'] || ''
assert.ok(runnerTemp, 'RUNNER_TEMP is not defined')
const uniqueId = uuid()
this.sshKeyPath = path.join(runnerTemp, uniqueId)
this.sshKeyPath = path.join(runnerTemp, `${uniqueId}_ssh_key`)
stateHelper.setSshKeyPath(this.sshKeyPath)
await fs.promises.mkdir(runnerTemp, {recursive: true})
await fs.promises.writeFile(
Expand Down Expand Up @@ -282,40 +283,31 @@ class GitAuthHelper {
'Unexpected configureToken parameter combinations'
)

stateHelper.setCredentialStorePath(this.credentialStorePath)
await fs.promises.writeFile(this.credentialStorePath, this.tokenCredential)

// 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
await this.git.config(
this.tokenConfigKey,
this.tokenPlaceholderConfigValue,
globalConfig
)

// Replace the placeholder
await this.replaceTokenPlaceholder(configPath || '')
}
if (this.settings.customCredentialHelper) {
await this.git.config(
this.credentialConfigKey,
this.settings.customCredentialHelper,
globalConfig,
true
)

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(this.tokenPlaceholderConfigValue)
if (
placeholderIndex < 0 ||
placeholderIndex != content.lastIndexOf(this.tokenPlaceholderConfigValue)
) {
throw new Error(`Unable to replace auth placeholder in ${configPath}`)
await this.git.config('credential.useHttpPath', 'true', globalConfig)
}
assert.ok(this.tokenConfigValue, 'tokenConfigValue is not defined')
content = content.replace(
this.tokenPlaceholderConfigValue,
this.tokenConfigValue

await this.git.config(
this.credentialConfigKey,
this.credentialConfigValue,
globalConfig,
true
)
await fs.promises.writeFile(configPath, content)
}

private async removeSsh(): Promise<void> {
Expand Down Expand Up @@ -346,8 +338,20 @@ class GitAuthHelper {
}

private async removeToken(): Promise<void> {
// HTTP extra header
await this.removeGitConfig(this.tokenConfigKey)
// Credential Helper
const credentialStorePath =
this.credentialStorePath || stateHelper.CredentialStorePath
if (credentialStorePath) {
try {
await io.rmRF(credentialStorePath)
} catch (err) {
core.debug(`${(err as any)?.message ?? err}`)
core.warning(
`Failed to remove credential store '${credentialStorePath}'`
)
}
}
await this.removeGitConfig(this.credentialConfigKey)
}

private async removeGitConfig(
Expand Down
5 changes: 5 additions & 0 deletions src/git-source-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,11 @@ export interface IGitSourceSettings {
*/
persistCredentials: boolean

/**
* Use following command/script as value for "credential.<URL>.helper"
*/
customCredentialHelper: string | undefined

/**
* Organization ID for the currently running workflow (used for auth settings)
*/
Expand Down
3 changes: 3 additions & 0 deletions src/input-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,9 @@ export async function getInputs(): Promise<IGitSourceSettings> {
result.persistCredentials =
(core.getInput('persist-credentials') || 'false').toUpperCase() === 'TRUE'

// Custom credential helper
result.customCredentialHelper = core.getInput('custom-credential-helper')

// Workflow organization ID
result.workflowOrganizationId =
await workflowContextHelper.getOrganizationId()
Expand Down
Loading