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 {verifyAuth} = require('./git'); | ||||||
| const debug = require('debug')('semantic-release:get-git-auth-url'); | 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: |  * Determine the the git repository URL to use to push, either: | ||||||
|  * - The `repositoryUrl` as is if allowed to push |  * - 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. |  * @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 = { |   const GIT_TOKENS = { | ||||||
|     GIT_CREDENTIALS: undefined, |     GIT_CREDENTIALS: undefined, | ||||||
|     GH_TOKEN: undefined, |     GH_TOKEN: undefined, | ||||||
| @ -30,6 +73,7 @@ module.exports = async ({cwd, env, branch, options: {repositoryUrl}}) => { | |||||||
|     BITBUCKET_TOKEN_BASIC_AUTH: '', |     BITBUCKET_TOKEN_BASIC_AUTH: '', | ||||||
|   }; |   }; | ||||||
| 
 | 
 | ||||||
|  |   let {repositoryUrl} = context.options; | ||||||
|   const info = hostedGitInfo.fromUrl(repositoryUrl, {noGitPlus: true}); |   const info = hostedGitInfo.fromUrl(repositoryUrl, {noGitPlus: true}); | ||||||
|   const {protocol, ...parsed} = parse(repositoryUrl); |   const {protocol, ...parsed} = parse(repositoryUrl); | ||||||
| 
 | 
 | ||||||
| @ -47,24 +91,30 @@ module.exports = async ({cwd, env, branch, options: {repositoryUrl}}) => { | |||||||
|     await verifyAuth(repositoryUrl, branch.name, {cwd, env}); |     await verifyAuth(repositoryUrl, branch.name, {cwd, env}); | ||||||
|   } catch (_) { |   } catch (_) { | ||||||
|     debug('SSH key auth failed, falling back to https.'); |     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])); |     // Skip verification if there is no ambiguity on which env var to use for authentication
 | ||||||
|     const gitCredentials = `${GIT_TOKENS[envVar] || ''}${env[envVar] || ''}`; |     if (envVars.length === 1) { | ||||||
|  |       const gitCredentials = `${GIT_TOKENS[envVars[0]] || ''}${env[envVars[0]]}`; | ||||||
|  |       return formatAuthUrl(protocol, repositoryUrl, gitCredentials); | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|     if (gitCredentials) { |     if (envVars.length > 1) { | ||||||
|       // If credentials are set via environment variables, convert the URL to http/https and add basic auth, otherwise return `repositoryUrl` as is
 |       debug(`Found ${envVars.length} credentials in environment, trying all of them`); | ||||||
|       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({ |       const candidateRepositoryUrls = []; | ||||||
|         ...parsed, |       for (const envVar of envVars) { | ||||||
|         auth: gitCredentials, |         const gitCredentials = `${GIT_TOKENS[envVar] || ''}${env[envVar]}`; | ||||||
|         host: `${hostname}${protocol === 'ssh:' ? '' : port ? `:${port}` : ''}`, |         const authUrl = formatAuthUrl(protocol, repositoryUrl, gitCredentials); | ||||||
|         protocol: protocol && /http[^s]/.test(protocol) ? 'http' : 'https', |         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 execa = require('execa'); | ||||||
| const {WritableStreamBuffer} = require('stream-buffers'); | const {WritableStreamBuffer} = require('stream-buffers'); | ||||||
| const delay = require('delay'); | const delay = require('delay'); | ||||||
|  | const getAuthUrl = require('../lib/get-git-auth-url'); | ||||||
| const {SECRET_REPLACEMENT} = require('../lib/definitions/constants'); | const {SECRET_REPLACEMENT} = require('../lib/definitions/constants'); | ||||||
| const { | const { | ||||||
|   gitHead, |   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(`Error: Console token ${escapeRegExp(SECRET_REPLACEMENT)}`)); | ||||||
|   t.regex(stderr, new RegExp(`Throw error: Exposing ${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