From 607b151f0e38be83eeb84dea5ee9b0dcb97e83ce Mon Sep 17 00:00:00 2001
From: Michael Kriese <michael.kriese@visualon.de>
Date: Tue, 9 Mar 2021 19:25:18 +0100
Subject: [PATCH] feat(http): support custom auth types (#9053)

---
 docs/usage/configuration-options.md           | 20 ++++++++++++++
 lib/config/definitions.ts                     | 11 ++++++++
 .../npm/__snapshots__/get.spec.ts.snap        | 17 ++++++++++++
 lib/datasource/npm/get.spec.ts                | 22 ++++++++++++++++
 lib/types/host-rules.ts                       |  1 +
 lib/util/http/auth.spec.ts                    | 26 +++++++++++++++++++
 lib/util/http/auth.ts                         |  4 ++-
 lib/util/http/host-rules.spec.ts              | 21 +++++++++++++++
 lib/util/http/host-rules.ts                   |  3 ++-
 lib/util/http/types.ts                        |  5 ++++
 10 files changed, 128 insertions(+), 2 deletions(-)

diff --git a/docs/usage/configuration-options.md b/docs/usage/configuration-options.md
index 36e2638495..05eb7fe579 100644
--- a/docs/usage/configuration-options.md
+++ b/docs/usage/configuration-options.md
@@ -815,6 +815,26 @@ To abort Renovate for errors for a specific `docker` host:
 
 When this field is enabled, Renovate will abort its run if it encounters either (a) any low-level http error (e.g. `ETIMEDOUT`) or (b) receives a response _not_ matching any of the configured `abortIgnoreStatusCodes` (e.g. `500 Internal Error`);
 
+### authType
+
+This can be used with `token` to create a custom http `authorization` header.
+
+An example for npm basic auth with token:
+
+```json
+{
+  "hostRules": [
+    {
+      "domainName": "npm.custom.org",
+      "token": "<some-token>",
+      "authType": "Basic"
+    }
+  ]
+}
+```
+
+This will generate the following header: `authorization: Basic <some-token>`.
+
 ### baseUrl
 
 Use this instead of `domainName` or `hostName` if you need a rule to apply to a specific path on a host.
diff --git a/lib/config/definitions.ts b/lib/config/definitions.ts
index 2526f7e367..26deb9cc61 100644
--- a/lib/config/definitions.ts
+++ b/lib/config/definitions.ts
@@ -1695,6 +1695,17 @@ const options: RenovateOptions[] = [
     cli: false,
     env: false,
   },
+  {
+    name: 'authType',
+    description:
+      'Authentication type for http header. e.g. "Bearer" or "Basic".',
+    type: 'string',
+    stage: 'repository',
+    parent: 'hostRules',
+    default: 'Bearer',
+    cli: false,
+    env: false,
+  },
   {
     name: 'prBodyDefinitions',
     description: 'Table column definitions for use in PR tables.',
diff --git a/lib/datasource/npm/__snapshots__/get.spec.ts.snap b/lib/datasource/npm/__snapshots__/get.spec.ts.snap
index 1698be307c..32ea080110 100644
--- a/lib/datasource/npm/__snapshots__/get.spec.ts.snap
+++ b/lib/datasource/npm/__snapshots__/get.spec.ts.snap
@@ -500,6 +500,23 @@ Array [
 ]
 `;
 
+exports[`datasource/npm/get uses hostRules basic token auth 1`] = `
+Array [
+  Object {
+    "headers": Object {
+      "accept": "application/json",
+      "accept-encoding": "gzip, deflate",
+      "authorization": "Basic XXX",
+      "cache-control": "no-cache",
+      "host": "registry.npmjs.org",
+      "user-agent": "https://github.com/renovatebot/renovate",
+    },
+    "method": "GET",
+    "url": "https://registry.npmjs.org/renovate",
+  },
+]
+`;
+
 exports[`datasource/npm/get uses hostRules token auth 1`] = `
 Array [
   Object {
diff --git a/lib/datasource/npm/get.spec.ts b/lib/datasource/npm/get.spec.ts
index c3b11a31b5..b44e95dd86 100644
--- a/lib/datasource/npm/get.spec.ts
+++ b/lib/datasource/npm/get.spec.ts
@@ -153,6 +153,28 @@ describe(getName(__filename), () => {
     expect(httpMock.getTrace()).toMatchSnapshot();
   });
 
+  it('uses hostRules basic token auth', async () => {
+    expect.assertions(1);
+    const npmrc = ``;
+    hostRules.add({
+      baseUrl: 'https://registry.npmjs.org',
+      token: 'XXX',
+      authType: 'Basic',
+    });
+
+    httpMock
+      .scope('https://registry.npmjs.org', {
+        reqheaders: {
+          authorization: 'Basic XXX',
+        },
+      })
+      .get('/renovate')
+      .reply(200, { name: 'renovate' });
+    setNpmrc(npmrc);
+    await getDependency('renovate', 0);
+    expect(httpMock.getTrace()).toMatchSnapshot();
+  });
+
   it('cover all paths', async () => {
     expect.assertions(10);
 
diff --git a/lib/types/host-rules.ts b/lib/types/host-rules.ts
index a9a1081fad..72c7aef6ff 100644
--- a/lib/types/host-rules.ts
+++ b/lib/types/host-rules.ts
@@ -1,4 +1,5 @@
 export interface HostRule {
+  authType?: string;
   endpoint?: string;
   host?: string;
   hostType?: string;
diff --git a/lib/util/http/auth.spec.ts b/lib/util/http/auth.spec.ts
index 3252caa8e1..e612b1958b 100644
--- a/lib/util/http/auth.spec.ts
+++ b/lib/util/http/auth.spec.ts
@@ -112,6 +112,32 @@ describe(getName(__filename), () => {
     });
   });
 
+  it(`npm basic token`, () => {
+    const opts: GotOptions = {
+      headers: {},
+      token: 'a40bdd925a0c0b9c4cdd19d101c0df3b2bcd063ab7ad6706f03bcffcec01e863',
+      hostType: 'npm',
+      context: {
+        authType: 'Basic',
+      },
+    };
+
+    applyAuthorization(opts);
+
+    expect(opts).toMatchInlineSnapshot(`
+      Object {
+        "context": Object {
+          "authType": "Basic",
+        },
+        "headers": Object {
+          "authorization": "Basic a40bdd925a0c0b9c4cdd19d101c0df3b2bcd063ab7ad6706f03bcffcec01e863",
+        },
+        "hostType": "npm",
+        "token": "a40bdd925a0c0b9c4cdd19d101c0df3b2bcd063ab7ad6706f03bcffcec01e863",
+      }
+    `);
+  });
+
   describe('removeAuthorization', () => {
     it('no authorization', () => {
       const opts = partial<NormalizedOptions>({
diff --git a/lib/util/http/auth.ts b/lib/util/http/auth.ts
index 6e98d5f582..f0ac28da8b 100644
--- a/lib/util/http/auth.ts
+++ b/lib/util/http/auth.ts
@@ -36,7 +36,9 @@ export function applyAuthorization(inOptions: GotOptions): GotOptions {
         options.headers.authorization = `Bearer ${options.token}`;
       }
     } else {
-      options.headers.authorization = `Bearer ${options.token}`;
+      // Custom Auth type, eg `Basic XXXX_TOKEN`
+      const type = options.context?.authType ?? 'Bearer';
+      options.headers.authorization = `${type} ${options.token}`;
     }
     delete options.token;
   } else if (options.password !== undefined) {
diff --git a/lib/util/http/host-rules.spec.ts b/lib/util/http/host-rules.spec.ts
index fc4aad4f8c..9810d6febe 100644
--- a/lib/util/http/host-rules.spec.ts
+++ b/lib/util/http/host-rules.spec.ts
@@ -33,6 +33,12 @@ describe(getName(__filename), () => {
       password: 'password',
     });
 
+    hostRules.add({
+      hostType: 'npm',
+      authType: 'Basic',
+      token: 'XXX',
+    });
+
     httpMock.reset();
     httpMock.setup();
   });
@@ -45,6 +51,9 @@ describe(getName(__filename), () => {
   it('adds token', () => {
     expect(applyHostRules(url, { ...options })).toMatchInlineSnapshot(`
       Object {
+        "context": Object {
+          "authType": undefined,
+        },
         "hostType": "github",
         "token": "token",
       }
@@ -62,6 +71,18 @@ describe(getName(__filename), () => {
     `);
   });
 
+  it('adds custom auth', () => {
+    expect(applyHostRules(url, { hostType: 'npm' })).toMatchInlineSnapshot(`
+      Object {
+        "context": Object {
+          "authType": "Basic",
+        },
+        "hostType": "npm",
+        "token": "XXX",
+      }
+    `);
+  });
+
   it('skips', () => {
     expect(applyHostRules(url, { ...options, token: 'xxx' }))
       .toMatchInlineSnapshot(`
diff --git a/lib/util/http/host-rules.ts b/lib/util/http/host-rules.ts
index f83f599b1c..557d44db75 100644
--- a/lib/util/http/host-rules.ts
+++ b/lib/util/http/host-rules.ts
@@ -12,7 +12,7 @@ export function applyHostRules(url: string, inOptions: GotOptions): GotOptions {
       hostType: options.hostType,
       url,
     }) || /* istanbul ignore next: can only happen in tests */ {};
-  const { username, password, token, enabled } = foundRules;
+  const { username, password, token, enabled, authType } = foundRules;
   if (options.headers?.authorization || options.password || options.token) {
     logger.trace({ url }, `Authorization already set`);
   } else if (password !== undefined) {
@@ -22,6 +22,7 @@ export function applyHostRules(url: string, inOptions: GotOptions): GotOptions {
   } else if (token) {
     logger.trace({ url }, `Applying Bearer authentication`);
     options.token = token;
+    options.context = { ...options.context, authType };
   } else if (enabled === false) {
     options.enabled = false;
   }
diff --git a/lib/util/http/types.ts b/lib/util/http/types.ts
index 86dc0b7258..63a53e4e2a 100644
--- a/lib/util/http/types.ts
+++ b/lib/util/http/types.ts
@@ -1,5 +1,9 @@
 import { OptionsOfJSONResponseBody, RequestError as RequestError_ } from 'got';
 
+export type GotContextOptions = {
+  authType?: string;
+} & Record<string, unknown>;
+
 // TODO: Move options to context
 export type GotOptions = OptionsOfJSONResponseBody & {
   abortOnError?: boolean;
@@ -8,6 +12,7 @@ export type GotOptions = OptionsOfJSONResponseBody & {
   hostType?: string;
   enabled?: boolean;
   useCache?: boolean;
+  context?: GotContextOptions;
 };
 
 export { RequestError_ as HttpError };
-- 
GitLab