fix: use valid git credentials when multiple are provided (#1669)
This commit is contained in:
		
							parent
							
								
									77a75f072b
								
							
						
					
					
						commit
						2bf377194e
					
				| @ -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 | ||||
| @ -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, | ||||
| @ -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); | ||||
| 
 | ||||
| @ -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])); | ||||
| 
 | ||||
|     const envVar = Object.keys(GIT_TOKENS).find((envVar) => !isNil(env[envVar])); | ||||
|     const gitCredentials = `${GIT_TOKENS[envVar] || ''}${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); | ||||
|     } | ||||
| 
 | ||||
|     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 | ||||
|       ); | ||||
|     if (envVars.length > 1) { | ||||
|       debug(`Found ${envVars.length} credentials in environment, trying all of them`); | ||||
| 
 | ||||
|       return format({ | ||||
|         ...parsed, | ||||
|         auth: gitCredentials, | ||||
|         host: `${hostname}${protocol === 'ssh:' ? '' : port ? `:${port}` : ''}`, | ||||
|         protocol: protocol && /http[^s]/.test(protocol) ? 'http' : 'https', | ||||
|       }); | ||||
|       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)); | ||||
|       } | ||||
| 
 | ||||
|       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]; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|  | ||||
| @ -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, | ||||
| @ -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 | ||||
|   ); | ||||
| }); | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user