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