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 valid git credentials when multiple are provided #1669

Merged
merged 3 commits into from Oct 29, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
82 changes: 66 additions & 16 deletions lib/get-git-auth-url.js
Expand Up @@ -4,6 +4,48 @@ const hostedGitInfo = require('hosted-git-info');
const {verifyAuth} = require('./git');
const debug = require('debug')('semantic-release:get-git-auth-url');

/**
* Machinery to format a repository URL with the given credentials
*
* @param {String} protocol URL protocol (which should not be present in repositoryUrl)
* @param {String} repositoryUrl User-given repository URL
* @param {String} gitCredentials The basic auth part of the URL
*
* @return {String} The formatted Git repository URL.
*/
function formatAuthUrl(protocol, repositoryUrl, gitCredentials) {
const [match, auth, host, basePort, path] =
/^(?!.+:\/\/)(?:(?<auth>.*)@)?(?<host>.*?):(?<port>\d+)?:?\/?(?<path>.*)$/.exec(repositoryUrl) || [];
const {port, hostname, ...parsed} = parse(
match ? `ssh://${auth ? `${auth}@` : ''}${host}${basePort ? `:${basePort}` : ''}/${path}` : repositoryUrl
);

return format({
...parsed,
auth: gitCredentials,
host: `${hostname}${protocol === 'ssh:' ? '' : port ? `:${port}` : ''}`,
protocol: protocol && /http[^s]/.test(protocol) ? 'http' : 'https',
});
}

/**
* Verify authUrl by calling git.verifyAuth, but don't throw on failure
*
* @param {Object} context semantic-release context.
* @param {String} authUrl Repository URL to verify
*
* @return {String} The authUrl as is if the connection was successfull, null otherwise
*/
async function ensureValidAuthUrl({cwd, env, branch}, authUrl) {
try {
await verifyAuth(authUrl, branch.name, {cwd, env});
return authUrl;
} catch (error) {
debug(error);
return null;
}
}

/**
* Determine the the git repository URL to use to push, either:
* - The `repositoryUrl` as is if allowed to push
Expand All @@ -15,7 +57,8 @@ const debug = require('debug')('semantic-release:get-git-auth-url');
*
* @return {String} The formatted Git repository URL.
*/
module.exports = async ({cwd, env, branch, options: {repositoryUrl}}) => {
module.exports = async (context) => {
const {cwd, env, branch} = context;
const GIT_TOKENS = {
GIT_CREDENTIALS: undefined,
GH_TOKEN: undefined,
Expand All @@ -30,6 +73,7 @@ module.exports = async ({cwd, env, branch, options: {repositoryUrl}}) => {
BITBUCKET_TOKEN_BASIC_AUTH: '',
};

let {repositoryUrl} = context.options;
const info = hostedGitInfo.fromUrl(repositoryUrl, {noGitPlus: true});
const {protocol, ...parsed} = parse(repositoryUrl);

Expand All @@ -47,24 +91,30 @@ module.exports = async ({cwd, env, branch, options: {repositoryUrl}}) => {
await verifyAuth(repositoryUrl, branch.name, {cwd, env});
} catch (_) {
debug('SSH key auth failed, falling back to https.');
const envVars = Object.keys(GIT_TOKENS).filter((envVar) => !isNil(env[envVar]));

// Skip verification if there is no ambiguity on which env var to use for authentication
if (envVars.length === 1) {
const gitCredentials = `${GIT_TOKENS[envVars[0]] || ''}${env[envVars[0]]}`;
return formatAuthUrl(protocol, repositoryUrl, gitCredentials);
}

const envVar = Object.keys(GIT_TOKENS).find((envVar) => !isNil(env[envVar]));
const gitCredentials = `${GIT_TOKENS[envVar] || ''}${env[envVar] || ''}`;
if (envVars.length > 1) {
debug(`Found ${envVars.length} credentials in environment, trying all of them`);

if (gitCredentials) {
// If credentials are set via environment variables, convert the URL to http/https and add basic auth, otherwise return `repositoryUrl` as is
const [match, auth, host, basePort, path] =
/^(?!.+:\/\/)(?:(?<auth>.*)@)?(?<host>.*?):(?<port>\d+)?:?\/?(?<path>.*)$/.exec(repositoryUrl) || [];
const {port, hostname, ...parsed} = parse(
match ? `ssh://${auth ? `${auth}@` : ''}${host}${basePort ? `:${basePort}` : ''}/${path}` : repositoryUrl
);
const candidateRepositoryUrls = [];
for (const envVar of envVars) {
const gitCredentials = `${GIT_TOKENS[envVar] || ''}${env[envVar]}`;
const authUrl = formatAuthUrl(protocol, repositoryUrl, gitCredentials);
candidateRepositoryUrls.push(ensureValidAuthUrl(context, authUrl));
}

return format({
...parsed,
auth: gitCredentials,
host: `${hostname}${protocol === 'ssh:' ? '' : port ? `:${port}` : ''}`,
protocol: protocol && /http[^s]/.test(protocol) ? 'http' : 'https',
});
const validRepositoryUrls = await Promise.all(candidateRepositoryUrls);
const chosenAuthUrlIndex = validRepositoryUrls.findIndex((url) => url !== null);
if (chosenAuthUrlIndex > -1) {
debug(`Using "${envVars[chosenAuthUrlIndex]}" to authenticate`);
return validRepositoryUrls[chosenAuthUrlIndex];
}
}
}

Expand Down
41 changes: 41 additions & 0 deletions test/integration.test.js
Expand Up @@ -6,6 +6,7 @@ const {writeJson, readJson} = require('fs-extra');
const execa = require('execa');
const {WritableStreamBuffer} = require('stream-buffers');
const delay = require('delay');
const getAuthUrl = require('../lib/get-git-auth-url');
const {SECRET_REPLACEMENT} = require('../lib/definitions/constants');
const {
gitHead,
Expand Down Expand Up @@ -656,3 +657,43 @@ test('Hide sensitive environment variable values from the logs', async (t) => {
t.regex(stderr, new RegExp(`Error: Console token ${escapeRegExp(SECRET_REPLACEMENT)}`));
t.regex(stderr, new RegExp(`Throw error: Exposing ${escapeRegExp(SECRET_REPLACEMENT)}`));
});

test('Use the valid git credentials when multiple are provided', async (t) => {
const {cwd, authUrl} = await gitbox.createRepo('test-auth');

t.is(
await getAuthUrl({
cwd,
env: {
GITHUB_TOKEN: 'dummy',
GITLAB_TOKEN: 'trash',
BB_TOKEN_BASIC_AUTH: gitbox.gitCredential,
GIT_ASKPASS: 'echo',
GIT_TERMINAL_PROMPT: 0,
},
branch: {name: 'master'},
options: {repositoryUrl: 'http://toto@localhost:2080/git/test-auth.git'},
}),
authUrl
);
});

test('Use the repository URL as is if none of the given git credentials are valid', async (t) => {
const {cwd} = await gitbox.createRepo('test-invalid-auth');
const dummyUrl = 'http://toto@localhost:2080/git/test-auth.git';

t.is(
await getAuthUrl({
cwd,
env: {
GITHUB_TOKEN: 'dummy',
GITLAB_TOKEN: 'trash',
GIT_ASKPASS: 'echo',
GIT_TERMINAL_PROMPT: 0,
},
branch: {name: 'master'},
options: {repositoryUrl: dummyUrl},
}),
dummyUrl
);
});