From 707d35db30a73bc79d139900c1c79669e3677f88 Mon Sep 17 00:00:00 2001 From: Michael Kriese <michael.kriese@visualon.de> Date: Fri, 27 Mar 2020 11:28:20 +0100 Subject: [PATCH] feat(npm): try auth recursive (#5698) --- .../npm/__snapshots__/index.spec.ts.snap | 24 ++-- lib/datasource/npm/get.spec.ts | 128 ++++++++++++++++++ lib/datasource/npm/get.ts | 22 ++- lib/datasource/npm/index.spec.ts | 6 +- lib/datasource/npm/npmrc.ts | 14 +- package.json | 1 - yarn.lock | 5 - 7 files changed, 156 insertions(+), 44 deletions(-) create mode 100644 lib/datasource/npm/get.spec.ts diff --git a/lib/datasource/npm/__snapshots__/index.spec.ts.snap b/lib/datasource/npm/__snapshots__/index.spec.ts.snap index 99fc4368c2..4a87ed7192 100644 --- a/lib/datasource/npm/__snapshots__/index.spec.ts.snap +++ b/lib/datasource/npm/__snapshots__/index.spec.ts.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`api/npm should fetch package info from custom registry 1`] = ` +exports[`datasource/npm/index should fetch package info from custom registry 1`] = ` Object { "homepage": "https://github.com/renovateapp/dummy", "latestVersion": "0.0.1", @@ -28,7 +28,7 @@ Object { } `; -exports[`api/npm should fetch package info from npm 1`] = ` +exports[`datasource/npm/index should fetch package info from npm 1`] = ` Object { "homepage": "https://github.com/renovateapp/dummy", "latestVersion": "0.0.1", @@ -56,7 +56,7 @@ Object { } `; -exports[`api/npm should handle foobar 1`] = ` +exports[`datasource/npm/index should handle foobar 1`] = ` Object { "homepage": "https://github.com/renovateapp/dummy", "latestVersion": "0.0.1", @@ -84,7 +84,7 @@ Object { } `; -exports[`api/npm should handle no time 1`] = ` +exports[`datasource/npm/index should handle no time 1`] = ` Object { "homepage": "https://github.com/renovateapp/dummy", "latestVersion": "0.0.1", @@ -110,7 +110,7 @@ Object { } `; -exports[`api/npm should parse repo url (string) 1`] = ` +exports[`datasource/npm/index should parse repo url (string) 1`] = ` Object { "homepage": undefined, "latestVersion": "0.0.1", @@ -131,7 +131,7 @@ Object { } `; -exports[`api/npm should parse repo url 1`] = ` +exports[`datasource/npm/index should parse repo url 1`] = ` Object { "homepage": undefined, "latestVersion": "0.0.1", @@ -152,7 +152,7 @@ Object { } `; -exports[`api/npm should replace any environment variable in npmrc 1`] = ` +exports[`datasource/npm/index should replace any environment variable in npmrc 1`] = ` Object { "homepage": "https://github.com/renovateapp/dummy", "latestVersion": "0.0.1", @@ -180,7 +180,7 @@ Object { } `; -exports[`api/npm should return deprecated 1`] = ` +exports[`datasource/npm/index should return deprecated 1`] = ` Object { "deprecationMessage": "On registry \`https://registry.npmjs.org/\`, the \\"latest\\" version (v0.0.2) of dependency \`foobar\` has the following deprecation notice: @@ -214,7 +214,7 @@ Marking the latest version of an npm package as deprecated results in the entire } `; -exports[`api/npm should return deprecated 2`] = ` +exports[`datasource/npm/index should return deprecated 2`] = ` "On registry \`https://registry.npmjs.org/\`, the \\"latest\\" version (v0.0.2) of dependency \`foobar\` has the following deprecation notice: \`This is deprecated\` @@ -222,7 +222,7 @@ exports[`api/npm should return deprecated 2`] = ` Marking the latest version of an npm package as deprecated results in the entire package being considered deprecated, so contact the package author you think this is a mistake." `; -exports[`api/npm should send an authorization header if provided 1`] = ` +exports[`datasource/npm/index should send an authorization header if provided 1`] = ` Object { "homepage": "https://github.com/renovateapp/dummy", "latestVersion": "0.0.1", @@ -250,7 +250,7 @@ Object { } `; -exports[`api/npm should use NPM_TOKEN if provided 1`] = ` +exports[`datasource/npm/index should use NPM_TOKEN if provided 1`] = ` Object { "homepage": "https://github.com/renovateapp/dummy", "latestVersion": "0.0.1", @@ -278,7 +278,7 @@ Object { } `; -exports[`api/npm should use default registry if missing from npmrc 1`] = ` +exports[`datasource/npm/index should use default registry if missing from npmrc 1`] = ` Object { "homepage": "https://github.com/renovateapp/dummy", "latestVersion": "0.0.1", diff --git a/lib/datasource/npm/get.spec.ts b/lib/datasource/npm/get.spec.ts new file mode 100644 index 0000000000..d83b3f906f --- /dev/null +++ b/lib/datasource/npm/get.spec.ts @@ -0,0 +1,128 @@ +import got from 'got'; +import { getName, partial } from '../../../test/util'; +import { getDependency, resetMemCache } from './get'; +import { setNpmrc } from './npmrc'; +import * as _got from '../../util/got'; +import { DatasourceError } from '../common'; + +jest.mock('../../util/got'); + +const api: jest.Mock<got.GotPromise<object>> = _got.api as never; + +describe(getName(__filename), () => { + function mock(body: object): void { + api.mockResolvedValueOnce( + partial<got.Response<object>>({ body }) + ); + } + + beforeEach(() => { + jest.clearAllMocks(); + resetMemCache(); + mock({ body: { name: '@myco/test' } }); + }); + + describe('has bearer auth', () => { + const configs = [ + `registry=https://test.org\n//test.org/:_authToken=XXX`, + `registry=https://test.org/sub\n//test.org/:_authToken=XXX`, + `registry=https://test.org/sub\n//test.org/sub/:_authToken=XXX`, + `registry=https://test.org/sub\n_authToken=XXX`, + `registry=https://test.org\n_authToken=XXX`, + `registry=https://test.org\n_authToken=XXX`, + `@myco:registry=https://test.org\n//test.org/:_authToken=XXX`, + ]; + + it.each(configs)('%p', async npmrc => { + expect.assertions(1); + setNpmrc(npmrc); + await getDependency('@myco/test', 0); + + expect(api.mock.calls[0][1].headers.authorization).toEqual('Bearer XXX'); + }); + }); + + describe('has basic auth', () => { + const configs = [ + `registry=https://test.org\n//test.org/:_auth=dGVzdDp0ZXN0`, + `registry=https://test.org\n//test.org/:username=test\n//test.org/:_password=dGVzdA==`, + `registry=https://test.org/sub\n//test.org/:_auth=dGVzdDp0ZXN0`, + `registry=https://test.org/sub\n//test.org/sub/:_auth=dGVzdDp0ZXN0`, + `registry=https://test.org/sub\n_auth=dGVzdDp0ZXN0`, + `registry=https://test.org\n_auth=dGVzdDp0ZXN0`, + `registry=https://test.org\n_auth=dGVzdDp0ZXN0`, + `@myco:registry=https://test.org\n//test.org/:_auth=dGVzdDp0ZXN0`, + `@myco:registry=https://test.org\n_auth=dGVzdDp0ZXN0`, + ]; + + it.each(configs)('%p', async npmrc => { + expect.assertions(1); + setNpmrc(npmrc); + await getDependency('@myco/test', 0); + + expect(api.mock.calls[0][1].headers.authorization).toEqual( + 'Basic dGVzdDp0ZXN0' + ); + }); + }); + + describe('no auth', () => { + const configs = [ + `@myco:registry=https://test.org\n_authToken=XXX`, + `@myco:registry=https://test.org\n//test.org/sub/:_authToken=XXX`, + `@myco:registry=https://test.org\n//test.org/sub/:_auth=dGVzdDp0ZXN0`, + `@myco:registry=https://test.org`, + `registry=https://test.org`, + ]; + + it.each(configs)('%p', async npmrc => { + expect.assertions(1); + setNpmrc(npmrc); + await getDependency('@myco/test', 0); + + expect(api.mock.calls[0][1].headers.authorization).toBeUndefined(); + }); + }); + + it('cover all paths', async () => { + expect.assertions(9); + + setNpmrc('registry=https://test.org\n_authToken=XXX'); + + expect(await getDependency('none', 0)).toBeNull(); + + mock({ + name: '@myco/test', + repository: {}, + versions: { '1.0.0': {} }, + 'dist-tags': { latest: '1.0.0' }, + }); + expect(await getDependency('@myco/test', 0)).toBeDefined(); + + mock({ + name: '@myco/test2', + versions: { '1.0.0': {} }, + 'dist-tags': { latest: '1.0.0' }, + }); + expect(await getDependency('@myco/test2', 0)).toBeDefined(); + + api.mockRejectedValueOnce({ statusCode: 401 }); + expect(await getDependency('error-401', 0)).toBeNull(); + api.mockRejectedValueOnce({ statusCode: 402 }); + expect(await getDependency('error-402', 0)).toBeNull(); + api.mockRejectedValueOnce({ statusCode: 404 }); + expect(await getDependency('error-404', 0)).toBeNull(); + + api.mockRejectedValueOnce({}); + expect(await getDependency('error4', 0)).toBeNull(); + + setNpmrc(); + api.mockRejectedValueOnce({ name: 'ParseError', body: 'parse-error' }); + await expect(getDependency('npm-parse-error', 0)).rejects.toThrow( + DatasourceError + ); + + api.mockRejectedValueOnce({ statusCode: 402 }); + expect(await getDependency('npm-error-402', 0)).toBeNull(); + }); +}); diff --git a/lib/datasource/npm/get.ts b/lib/datasource/npm/get.ts index ade5f9d2cd..b23c0b7e81 100644 --- a/lib/datasource/npm/get.ts +++ b/lib/datasource/npm/get.ts @@ -3,7 +3,6 @@ import moment from 'moment'; import url from 'url'; import getRegistryUrl from 'registry-auth-token/registry-url'; import registryAuthToken from 'registry-auth-token'; -import isBase64 from 'validator/lib/isBase64'; import { OutgoingHttpHeaders } from 'http'; import is from '@sindresorhus/is'; import { logger } from '../../logger'; @@ -75,15 +74,19 @@ export async function getDependency( if (cachedResult) { return cachedResult; } - const authInfo = registryAuthToken(regUrl, { npmrc }); const headers: OutgoingHttpHeaders = {}; + let authInfo = registryAuthToken(regUrl, { npmrc, recursive: true }); + + if ( + !authInfo && + npmrc && + npmrc._authToken && + regUrl.replace(/\/?$/, '/') === npmrc.registry?.replace(/\/?$/, '/') + ) { + authInfo = { type: 'Bearer', token: npmrc._authToken }; + } if (authInfo && authInfo.type && authInfo.token) { - // istanbul ignore if - if (npmrc && npmrc.massagedAuth && isBase64(authInfo.token)) { - logger.debug('Massaging authorization type to Basic'); - authInfo.type = 'Basic'; - } headers.authorization = `${authInfo.type} ${authInfo.token}`; logger.trace( { token: maskToken(authInfo.token), npmName: packageName }, @@ -115,7 +118,6 @@ export async function getDependency( useCache, }; const raw = await got(pkgUrl, opts); - // istanbul ignore if if (retries < 3) { logger.debug({ pkgUrl, retries }, 'Recovered from npm error'); } @@ -207,7 +209,6 @@ export async function getDependency( ); return null; } - // istanbul ignore if if (err.statusCode === 402) { logger.debug( { @@ -231,7 +232,6 @@ export async function getDependency( return null; } if (uri.host === 'registry.npmjs.org') { - // istanbul ignore if if ( (err.name === 'ParseError' || err.code === 'ECONNRESET' || @@ -242,13 +242,11 @@ export async function getDependency( await delay(5000); return getDependency(packageName, retries - 1); } - // istanbul ignore if if (err.name === 'ParseError' && err.body) { err.body = 'err.body deleted by Renovate'; } throw new DatasourceError(err); } - // istanbul ignore next return null; } } diff --git a/lib/datasource/npm/index.spec.ts b/lib/datasource/npm/index.spec.ts index 6b146d9ea5..6d900f5fec 100644 --- a/lib/datasource/npm/index.spec.ts +++ b/lib/datasource/npm/index.spec.ts @@ -3,11 +3,12 @@ import nock from 'nock'; import moment from 'moment'; import * as npm from '.'; import { DATASOURCE_FAILURE } from '../../constants/error-messages'; +import { getName } from '../../../test/util'; jest.mock('registry-auth-token'); jest.mock('delay'); -const registryAuthToken: any = _registryAuthToken; +const registryAuthToken: jest.Mock<_registryAuthToken.NpmCredentials> = _registryAuthToken as never; let npmResponse: any; function getRelease( @@ -19,13 +20,14 @@ function getRelease( ); } -describe('api/npm', () => { +describe(getName(__filename), () => { delete process.env.NPM_TOKEN; beforeEach(() => { jest.resetAllMocks(); global.repoCache = {}; global.trustLevel = 'low'; npm.resetCache(); + npm.setNpmrc(); npmResponse = { name: 'foobar', versions: { diff --git a/lib/datasource/npm/npmrc.ts b/lib/datasource/npm/npmrc.ts index 2ebfac5ad9..2c78b868bf 100644 --- a/lib/datasource/npm/npmrc.ts +++ b/lib/datasource/npm/npmrc.ts @@ -1,6 +1,5 @@ import is from '@sindresorhus/is'; import ini from 'ini'; -import isBase64 from 'validator/lib/isBase64'; import { logger } from '../../logger'; let npmrc: Record<string, any> | null = null; @@ -36,7 +35,6 @@ export function setNpmrc(input?: string): void { npmrcRaw = input; logger.debug('Setting npmrc'); npmrc = ini.parse(input.replace(/\\n/g, '\n')); - // massage _auth to _authToken for (const [key, val] of Object.entries(npmrc)) { // istanbul ignore if if ( @@ -52,20 +50,12 @@ export function setNpmrc(input?: string): void { npmrc = existingNpmrc; return; } - if (key !== '_auth' && key.endsWith('_auth') && isBase64(val)) { - logger.debug('Massaging _auth to _authToken'); - npmrc[key + 'Token'] = val; - npmrc.massagedAuth = true; - delete npmrc[key]; - } } if (global.trustLevel !== 'high') { return; } - for (const key in npmrc) { - if (Object.prototype.hasOwnProperty.call(npmrc, key)) { - npmrc[key] = envReplace(npmrc[key]); - } + for (const key of Object.keys(npmrc)) { + npmrc[key] = envReplace(npmrc[key]); } } else if (npmrc) { logger.debug('Resetting npmrc'); diff --git a/package.json b/package.json index 12fca5b689..02b356fbca 100644 --- a/package.json +++ b/package.json @@ -165,7 +165,6 @@ "traverse": "0.6.6", "upath": "1.2.0", "validate-npm-package-name": "3.0.0", - "validator": "12.2.0", "www-authenticate": "0.6.2", "xmldoc": "1.1.2", "yarn": "1.22.4", diff --git a/yarn.lock b/yarn.lock index 52a18c72a7..80ce97acbf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9867,11 +9867,6 @@ validate-npm-package-name@3.0.0, validate-npm-package-name@^3.0.0, validate-npm- dependencies: builtins "^1.0.3" -validator@12.2.0: - version "12.2.0" - resolved "https://registry.yarnpkg.com/validator/-/validator-12.2.0.tgz#660d47e96267033fd070096c3b1a6f2db4380a0a" - integrity sha512-jJfE/DW6tIK1Ek8nCfNFqt8Wb3nzMoAbocBF6/Icgg1ZFSBpObdnwVY2jQj6qUqzhx5jc71fpvBWyLGO7Xl+nQ== - verror@1.10.0: version "1.10.0" resolved "https://registry.yarnpkg.com/verror/-/verror-1.10.0.tgz#3a105ca17053af55d6e270c1f8288682e18da400" -- GitLab