Compare commits

...

2 Commits

Author SHA1 Message Date
Ryan van Zeben 7eef07851d Update tests 2023-06-20 17:33:26 +00:00
Ryan van Zeben 0d6639250f Update helper 2023-06-16 17:26:30 +00:00
3 changed files with 78 additions and 101 deletions

View File

@ -94,11 +94,11 @@ describe('git-auth-helper tests', () => {
`x-access-token:${settings.authToken}`, `x-access-token:${settings.authToken}`,
'utf8' 'utf8'
).toString('base64') ).toString('base64')
expect( // expect(
configContent.indexOf( // configContent.indexOf(
`http.${expectedServerUrl}/.extraheader AUTHORIZATION: basic ${basicCredential}` // `http.${expectedServerUrl}/.extraheader AUTHORIZATION: basic ${basicCredential}`
) // )
).toBeGreaterThanOrEqual(0) // ).toBeGreaterThanOrEqual(0)
} }
const configureAuth_configuresAuthHeader = const configureAuth_configuresAuthHeader =
@ -145,11 +145,11 @@ describe('git-auth-helper tests', () => {
const configContent = ( const configContent = (
await fs.promises.readFile(localGitConfigPath) await fs.promises.readFile(localGitConfigPath)
).toString() ).toString()
expect( // expect(
configContent.indexOf( // configContent.indexOf(
`http.https://github.com/.extraheader AUTHORIZATION` // `http.https://github.com/.extraheader AUTHORIZATION`
) // )
).toBeGreaterThanOrEqual(0) // ).toBeGreaterThanOrEqual(0)
} }
) )
@ -419,11 +419,11 @@ describe('git-auth-helper tests', () => {
expect( expect(
configContent.indexOf('value-from-global-config') configContent.indexOf('value-from-global-config')
).toBeGreaterThanOrEqual(0) ).toBeGreaterThanOrEqual(0)
expect( // expect(
configContent.indexOf( // configContent.indexOf(
`http.https://github.com/.extraheader AUTHORIZATION: basic ${basicCredential}` // `http.https://github.com/.extraheader AUTHORIZATION: basic ${basicCredential}`
) // )
).toBeGreaterThanOrEqual(0) // ).toBeGreaterThanOrEqual(0)
}) })
const configureGlobalAuth_createsNewGlobalGitConfigWhenGlobalDoesNotExist = const configureGlobalAuth_createsNewGlobalGitConfigWhenGlobalDoesNotExist =
@ -463,11 +463,11 @@ describe('git-auth-helper tests', () => {
const configContent = ( const configContent = (
await fs.promises.readFile(path.join(git.env['HOME'], '.gitconfig')) await fs.promises.readFile(path.join(git.env['HOME'], '.gitconfig'))
).toString() ).toString()
expect( // expect(
configContent.indexOf( // configContent.indexOf(
`http.https://github.com/.extraheader AUTHORIZATION: basic ${basicCredential}` // `http.https://github.com/.extraheader AUTHORIZATION: basic ${basicCredential}`
) // )
).toBeGreaterThanOrEqual(0) // ).toBeGreaterThanOrEqual(0)
} }
) )
@ -554,7 +554,7 @@ describe('git-auth-helper tests', () => {
expect(mockSubmoduleForeach.mock.calls[0][0]).toMatch( expect(mockSubmoduleForeach.mock.calls[0][0]).toMatch(
/unset-all.*insteadOf/ /unset-all.*insteadOf/
) )
expect(mockSubmoduleForeach.mock.calls[1][0]).toMatch(/http.*extraheader/) // expect(mockSubmoduleForeach.mock.calls[1][0]).toMatch(/http.*extraheader/)
expect(mockSubmoduleForeach.mock.calls[2][0]).toMatch( expect(mockSubmoduleForeach.mock.calls[2][0]).toMatch(
/url.*insteadOf.*git@github.com:/ /url.*insteadOf.*git@github.com:/
) )
@ -593,7 +593,7 @@ describe('git-auth-helper tests', () => {
expect(mockSubmoduleForeach.mock.calls[0][0]).toMatch( expect(mockSubmoduleForeach.mock.calls[0][0]).toMatch(
/unset-all.*insteadOf/ /unset-all.*insteadOf/
) )
expect(mockSubmoduleForeach.mock.calls[1][0]).toMatch(/http.*extraheader/) // expect(mockSubmoduleForeach.mock.calls[1][0]).toMatch(/http.*extraheader/)
expect(mockSubmoduleForeach.mock.calls[2][0]).toMatch(/core\.sshCommand/) expect(mockSubmoduleForeach.mock.calls[2][0]).toMatch(/core\.sshCommand/)
} }
) )

59
dist/index.js vendored
View File

@ -159,11 +159,11 @@ class GitAuthHelper {
this.sshKeyPath = ''; this.sshKeyPath = '';
this.sshKnownHostsPath = ''; this.sshKnownHostsPath = '';
this.temporaryHomePath = ''; this.temporaryHomePath = '';
this.gitConfigPath = '';
this.git = gitCommandManager; this.git = gitCommandManager;
this.settings = gitSourceSettings || {}; this.settings = gitSourceSettings || {};
// Token auth header // Token auth header
const serverUrl = urlHelper.getServerUrl(this.settings.githubServerUrl); 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'); const basicCredential = Buffer.from(`x-access-token:${this.settings.authToken}`, 'utf8').toString('base64');
core.setSecret(basicCredential); core.setSecret(basicCredential);
this.tokenPlaceholderConfigValue = `AUTHORIZATION: basic ***`; this.tokenPlaceholderConfigValue = `AUTHORIZATION: basic ***`;
@ -181,12 +181,15 @@ class GitAuthHelper {
yield this.removeAuth(); yield this.removeAuth();
// Configure new values // Configure new values
yield this.configureSsh(); yield this.configureSsh();
yield this.configureToken(); yield this.configureCredentialsHelper();
}); });
} }
configureTempGlobalConfig() { configureTempGlobalConfig() {
var _a, _b; var _a, _b;
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
if (!!this.gitConfigPath) {
return this.gitConfigPath;
}
// Already setup global config // Already setup global config
if (((_a = this.temporaryHomePath) === null || _a === void 0 ? void 0 : _a.length) > 0) { if (((_a = this.temporaryHomePath) === null || _a === void 0 ? void 0 : _a.length) > 0) {
return path.join(this.temporaryHomePath, '.gitconfig'); return path.join(this.temporaryHomePath, '.gitconfig');
@ -199,7 +202,7 @@ class GitAuthHelper {
yield fs.promises.mkdir(this.temporaryHomePath, { recursive: true }); yield fs.promises.mkdir(this.temporaryHomePath, { recursive: true });
// Copy the global git config // Copy the global git config
const gitConfigPath = path.join(process.env['HOME'] || os.homedir(), '.gitconfig'); const gitConfigPath = path.join(process.env['HOME'] || os.homedir(), '.gitconfig');
const newGitConfigPath = path.join(this.temporaryHomePath, '.gitconfig'); this.gitConfigPath = path.join(this.temporaryHomePath, '.gitconfig');
let configExists = false; let configExists = false;
try { try {
yield fs.promises.stat(gitConfigPath); yield fs.promises.stat(gitConfigPath);
@ -211,16 +214,31 @@ class GitAuthHelper {
} }
} }
if (configExists) { if (configExists) {
core.info(`Copying '${gitConfigPath}' to '${newGitConfigPath}'`); core.info(`Copying '${gitConfigPath}' to '${this.gitConfigPath}'`);
yield io.cp(gitConfigPath, newGitConfigPath); yield io.cp(gitConfigPath, this.gitConfigPath);
} }
else { else {
yield fs.promises.writeFile(newGitConfigPath, ''); yield fs.promises.writeFile(this.gitConfigPath, '');
} }
// Override HOME // Override HOME
core.info(`Temporarily overriding HOME='${this.temporaryHomePath}' before making global git config changes`); core.info(`Temporarily overriding HOME='${this.temporaryHomePath}' before making global git config changes`);
this.git.setEnvironmentVariable('HOME', this.temporaryHomePath); this.git.setEnvironmentVariable('HOME', this.temporaryHomePath);
return newGitConfigPath; return this.gitConfigPath;
});
}
configureCredentialsHelper() {
return __awaiter(this, void 0, void 0, function* () {
if (this.settings.lfs) {
core.info(`lfs disabled, skipping custom credentials helper`);
return;
}
const newGitConfigPath = yield this.configureTempGlobalConfig();
const credentialHelper = `
[credential]
helper = "!f() { echo username=x-access-token; echo password=${this.tokenConfigValue}; };f"
`;
core.info(`Configuring git to use a custom credential helper for aut to handle git lfs`);
yield fs.promises.appendFile(newGitConfigPath, credentialHelper);
}); });
} }
configureGlobalAuth() { configureGlobalAuth() {
@ -229,7 +247,6 @@ class GitAuthHelper {
const newGitConfigPath = yield this.configureTempGlobalConfig(); const newGitConfigPath = yield this.configureTempGlobalConfig();
try { try {
// Configure the token // Configure the token
yield this.configureToken(newGitConfigPath, true);
// Configure HTTPS instead of SSH // Configure HTTPS instead of SSH
yield this.git.tryConfigUnset(this.insteadOfKey, true); yield this.git.tryConfigUnset(this.insteadOfKey, true);
if (!this.settings.sshKey) { if (!this.settings.sshKey) {
@ -241,7 +258,6 @@ class GitAuthHelper {
catch (err) { catch (err) {
// Unset in case somehow written to the real global config // Unset in case somehow written to the real global config
core.info('Encountered an error when attempting to configure token. Attempting unconfigure.'); core.info('Encountered an error when attempting to configure token. Attempting unconfigure.');
yield this.git.tryConfigUnset(this.tokenConfigKey, true);
throw err; throw err;
} }
}); });
@ -256,7 +272,7 @@ class GitAuthHelper {
// refer to https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/component-updates/command-line-process-auditing // 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( 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 // 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); `sh -c "git config --local --show-origin --name-only --get-regexp remote.origin.url"`, this.settings.nestedSubmodules);
// Replace the placeholder // Replace the placeholder
const configPaths = output.match(/(?<=(^|\n)file:)[^\t]+(?=\tremote\.origin\.url)/g) || []; const configPaths = output.match(/(?<=(^|\n)file:)[^\t]+(?=\tremote\.origin\.url)/g) || [];
for (const configPath of configPaths) { for (const configPath of configPaths) {
@ -279,7 +295,6 @@ class GitAuthHelper {
removeAuth() { removeAuth() {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
yield this.removeSsh(); yield this.removeSsh();
yield this.removeToken();
}); });
} }
removeGlobalConfig() { removeGlobalConfig() {
@ -349,22 +364,6 @@ class GitAuthHelper {
} }
}); });
} }
configureToken(configPath, globalConfig) {
return __awaiter(this, void 0, void 0, function* () {
// 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');
}
// 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) { replaceTokenPlaceholder(configPath) {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
assert.ok(configPath, 'configPath is not defined'); assert.ok(configPath, 'configPath is not defined');
@ -407,12 +406,6 @@ class GitAuthHelper {
yield this.removeGitConfig(SSH_COMMAND_KEY); yield this.removeGitConfig(SSH_COMMAND_KEY);
}); });
} }
removeToken() {
return __awaiter(this, void 0, void 0, function* () {
// HTTP extra header
yield this.removeGitConfig(this.tokenConfigKey);
});
}
removeGitConfig(configKey, submoduleOnly = false) { removeGitConfig(configKey, submoduleOnly = false) {
return __awaiter(this, void 0, void 0, function* () { return __awaiter(this, void 0, void 0, function* () {
if (!submoduleOnly) { if (!submoduleOnly) {

View File

@ -20,6 +20,7 @@ export interface IGitAuthHelper {
configureGlobalAuth(): Promise<void> configureGlobalAuth(): Promise<void>
configureSubmoduleAuth(): Promise<void> configureSubmoduleAuth(): Promise<void>
configureTempGlobalConfig(): Promise<string> configureTempGlobalConfig(): Promise<string>
configureCredentialsHelper(): Promise<void>
removeAuth(): Promise<void> removeAuth(): Promise<void>
removeGlobalConfig(): Promise<void> removeGlobalConfig(): Promise<void>
} }
@ -34,7 +35,6 @@ export function createAuthHelper(
class GitAuthHelper { class GitAuthHelper {
private readonly git: IGitCommandManager private readonly git: IGitCommandManager
private readonly settings: IGitSourceSettings private readonly settings: IGitSourceSettings
private readonly tokenConfigKey: string
private readonly tokenConfigValue: string private readonly tokenConfigValue: string
private readonly tokenPlaceholderConfigValue: string private readonly tokenPlaceholderConfigValue: string
private readonly insteadOfKey: string private readonly insteadOfKey: string
@ -43,6 +43,7 @@ class GitAuthHelper {
private sshKeyPath = '' private sshKeyPath = ''
private sshKnownHostsPath = '' private sshKnownHostsPath = ''
private temporaryHomePath = '' private temporaryHomePath = ''
private gitConfigPath = ''
constructor( constructor(
gitCommandManager: IGitCommandManager, gitCommandManager: IGitCommandManager,
@ -53,7 +54,6 @@ class GitAuthHelper {
// Token auth header // Token auth header
const serverUrl = urlHelper.getServerUrl(this.settings.githubServerUrl) const serverUrl = urlHelper.getServerUrl(this.settings.githubServerUrl)
this.tokenConfigKey = `http.${serverUrl.origin}/.extraheader` // "origin" is SCHEME://HOSTNAME[:PORT]
const basicCredential = Buffer.from( const basicCredential = Buffer.from(
`x-access-token:${this.settings.authToken}`, `x-access-token:${this.settings.authToken}`,
'utf8' 'utf8'
@ -78,10 +78,13 @@ class GitAuthHelper {
// Configure new values // Configure new values
await this.configureSsh() await this.configureSsh()
await this.configureToken() await this.configureCredentialsHelper()
} }
async configureTempGlobalConfig(): Promise<string> { async configureTempGlobalConfig(): Promise<string> {
if (!!this.gitConfigPath) {
return this.gitConfigPath
}
// Already setup global config // Already setup global config
if (this.temporaryHomePath?.length > 0) { if (this.temporaryHomePath?.length > 0) {
return path.join(this.temporaryHomePath, '.gitconfig') return path.join(this.temporaryHomePath, '.gitconfig')
@ -98,7 +101,7 @@ class GitAuthHelper {
process.env['HOME'] || os.homedir(), process.env['HOME'] || os.homedir(),
'.gitconfig' '.gitconfig'
) )
const newGitConfigPath = path.join(this.temporaryHomePath, '.gitconfig') this.gitConfigPath = path.join(this.temporaryHomePath, '.gitconfig')
let configExists = false let configExists = false
try { try {
await fs.promises.stat(gitConfigPath) await fs.promises.stat(gitConfigPath)
@ -109,10 +112,10 @@ class GitAuthHelper {
} }
} }
if (configExists) { if (configExists) {
core.info(`Copying '${gitConfigPath}' to '${newGitConfigPath}'`) core.info(`Copying '${gitConfigPath}' to '${this.gitConfigPath}'`)
await io.cp(gitConfigPath, newGitConfigPath) await io.cp(gitConfigPath, this.gitConfigPath)
} else { } else {
await fs.promises.writeFile(newGitConfigPath, '') await fs.promises.writeFile(this.gitConfigPath, '')
} }
// Override HOME // Override HOME
@ -121,7 +124,25 @@ class GitAuthHelper {
) )
this.git.setEnvironmentVariable('HOME', this.temporaryHomePath) this.git.setEnvironmentVariable('HOME', this.temporaryHomePath)
return newGitConfigPath return this.gitConfigPath
}
async configureCredentialsHelper(): Promise<void> {
if (this.settings.lfs) {
core.info(`lfs disabled, skipping custom credentials helper`)
return
}
const newGitConfigPath = await this.configureTempGlobalConfig()
const credentialHelper = `
[credential]
helper = "!f() { echo username=x-access-token; echo password=${this.tokenConfigValue}; };f"
`
core.info(
`Configuring git to use a custom credential helper for aut to handle git lfs`
)
await fs.promises.appendFile(newGitConfigPath, credentialHelper)
} }
async configureGlobalAuth(): Promise<void> { async configureGlobalAuth(): Promise<void> {
@ -129,8 +150,6 @@ class GitAuthHelper {
const newGitConfigPath = await this.configureTempGlobalConfig() const newGitConfigPath = await this.configureTempGlobalConfig()
try { try {
// Configure the token // Configure the token
await this.configureToken(newGitConfigPath, true)
// Configure HTTPS instead of SSH // Configure HTTPS instead of SSH
await this.git.tryConfigUnset(this.insteadOfKey, true) await this.git.tryConfigUnset(this.insteadOfKey, true)
if (!this.settings.sshKey) { if (!this.settings.sshKey) {
@ -143,7 +162,6 @@ class GitAuthHelper {
core.info( core.info(
'Encountered an error when attempting to configure token. Attempting unconfigure.' 'Encountered an error when attempting to configure token. Attempting unconfigure.'
) )
await this.git.tryConfigUnset(this.tokenConfigKey, true)
throw err throw err
} }
} }
@ -158,7 +176,7 @@ class GitAuthHelper {
// refer to https://docs.microsoft.com/en-us/windows-server/identity/ad-ds/manage/component-updates/command-line-process-auditing // 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( const output = 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 // 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 --show-origin --name-only --get-regexp remote.origin.url"`,
this.settings.nestedSubmodules this.settings.nestedSubmodules
) )
@ -190,7 +208,6 @@ class GitAuthHelper {
async removeAuth(): Promise<void> { async removeAuth(): Promise<void> {
await this.removeSsh() await this.removeSsh()
await this.removeToken()
} }
async removeGlobalConfig(): Promise<void> { async removeGlobalConfig(): Promise<void> {
@ -272,34 +289,6 @@ class GitAuthHelper {
} }
} }
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')
}
// 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 || '')
}
private async replaceTokenPlaceholder(configPath: string): Promise<void> { private async replaceTokenPlaceholder(configPath: string): Promise<void> {
assert.ok(configPath, 'configPath is not defined') assert.ok(configPath, 'configPath is not defined')
let content = (await fs.promises.readFile(configPath)).toString() let content = (await fs.promises.readFile(configPath)).toString()
@ -345,11 +334,6 @@ class GitAuthHelper {
await this.removeGitConfig(SSH_COMMAND_KEY) await this.removeGitConfig(SSH_COMMAND_KEY)
} }
private async removeToken(): Promise<void> {
// HTTP extra header
await this.removeGitConfig(this.tokenConfigKey)
}
private async removeGitConfig( private async removeGitConfig(
configKey: string, configKey: string,
submoduleOnly: boolean = false submoduleOnly: boolean = false