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