From 00711ea55e7317840a6617bae872997ca4a14e63 Mon Sep 17 00:00:00 2001 From: "Daniel (dB.) Doubrovkine" Date: Sun, 11 Aug 2024 19:20:56 -0400 Subject: [PATCH] Allow overwriting the Authorization header. (#488) Signed-off-by: dblock --- package-lock.json | 49 +++++++++++++++ package.json | 1 + tools/src/OpenSearchHttpClient.ts | 20 +++--- .../tests/tester/OpenSearchHttpClient.test.ts | 62 +++++++++++-------- 4 files changed, 98 insertions(+), 34 deletions(-) diff --git a/package-lock.json b/package-lock.json index e1156510e..00b73c87a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -28,6 +28,7 @@ "ajv-formats": "^3.0.1", "aws4-axios": "^3.3.7", "axios": "^1.7.1", + "axios-mock-adapter": "^2.0.0", "cbor": "^9.0.2", "commander": "^12.0.0", "eslint": "^8.57.0", @@ -3935,6 +3936,18 @@ "proxy-from-env": "^1.1.0" } }, + "node_modules/axios-mock-adapter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/axios-mock-adapter/-/axios-mock-adapter-2.0.0.tgz", + "integrity": "sha512-D/K0J5Zm6KvaMTnsWrBQZWLzKN9GxUFZEa0mx2qeEHXDeTugCoplWehy8y36dj5vuSjhe1u/Dol8cZ8lzzmDew==", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "is-buffer": "^2.0.5" + }, + "peerDependencies": { + "axios": ">= 0.17.0" + } + }, "node_modules/babel-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", @@ -6423,6 +6436,28 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "engines": { + "node": ">=4" + } + }, "node_modules/is-builtin-module": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz", @@ -12467,6 +12502,15 @@ "proxy-from-env": "^1.1.0" } }, + "axios-mock-adapter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/axios-mock-adapter/-/axios-mock-adapter-2.0.0.tgz", + "integrity": "sha512-D/K0J5Zm6KvaMTnsWrBQZWLzKN9GxUFZEa0mx2qeEHXDeTugCoplWehy8y36dj5vuSjhe1u/Dol8cZ8lzzmDew==", + "requires": { + "fast-deep-equal": "^3.1.3", + "is-buffer": "^2.0.5" + } + }, "babel-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", @@ -14192,6 +14236,11 @@ "has-tostringtag": "^1.0.0" } }, + "is-buffer": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", + "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==" + }, "is-builtin-module": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz", diff --git a/package.json b/package.json index 22d798e13..5ffe41341 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "@types/titlecase": "^1.1.2", "@types/tmp": "^0.2.6", "@typescript-eslint/eslint-plugin": "^6.21.0", + "axios-mock-adapter": "^2.0.0", "ajv": "^8.13.0", "ajv-errors": "^3.0.0", "ajv-formats": "^3.0.1", diff --git a/tools/src/OpenSearchHttpClient.ts b/tools/src/OpenSearchHttpClient.ts index f81656074..afbb7d5fa 100644 --- a/tools/src/OpenSearchHttpClient.ts +++ b/tools/src/OpenSearchHttpClient.ts @@ -133,18 +133,23 @@ export class OpenSearchHttpClient { this._opts = opts this._logger = opts?.logger ?? new Logger() - let auth = undefined - let sigv4_interceptor = undefined + let auth_middleware = undefined if (opts?.basic_auth !== undefined) { this._logger.info(`Authenticating with ${opts.basic_auth.username} ...`) - auth = opts.basic_auth + auth_middleware = ((request: any): any => { + if (request.headers.Authorization === undefined) { + const base64 = Buffer.from(`${opts.basic_auth?.username}:${opts.basic_auth?.password}`, 'utf8').toString('base64'); + request.headers.Authorization = `Basic ${base64}` + } + return request + }) } else if (opts?.aws_auth !== undefined) { this._logger.info(`Authenticating using SigV4 with ${opts.aws_auth.aws_access_key_id} (${opts.aws_auth.aws_region}) ...`) - sigv4_interceptor = aws4Interceptor({ + auth_middleware = aws4Interceptor({ options: { region: opts.aws_auth.aws_region, - service: 'es' + service: opts.aws_auth.aws_service }, credentials: { accessKeyId: opts.aws_auth.aws_access_key_id, @@ -158,13 +163,12 @@ export class OpenSearchHttpClient { this._axios = axios.create({ baseURL: opts?.url ?? DEFAULT_URL, - auth, httpsAgent: new https.Agent({ rejectUnauthorized: !(opts?.insecure ?? DEFAULT_INSECURE) }), responseType: opts?.responseType, }) - if (sigv4_interceptor !== undefined) { - this._axios.interceptors.request.use(sigv4_interceptor) + if (auth_middleware !== undefined) { + this._axios.interceptors.request.use(auth_middleware) } } diff --git a/tools/tests/tester/OpenSearchHttpClient.test.ts b/tools/tests/tester/OpenSearchHttpClient.test.ts index e2687e7fd..85c62af05 100644 --- a/tools/tests/tester/OpenSearchHttpClient.test.ts +++ b/tools/tests/tester/OpenSearchHttpClient.test.ts @@ -9,58 +9,68 @@ import axios from "axios"; import { OpenSearchHttpClient } from "OpenSearchHttpClient" - -jest.mock('axios') +import AxiosMockAdapter from "axios-mock-adapter"; describe('OpenSearchHttpClient', () => { - let mocked_axios: jest.Mocked = axios as jest.Mocked + var mock = new AxiosMockAdapter(axios) - beforeEach(() => { - mocked_axios.create.mockReturnThis() - mocked_axios.interceptors.request.use = jest.fn().mockReturnValue({ headers: {} }) + afterEach(() => { + mock.reset() }) - afterEach(() => { - jest.clearAllMocks() + it('adds a Basic auth header', async () => { + let client = new OpenSearchHttpClient({ + url: 'https://localhost:9200', + basic_auth: { + username: 'u', + password: 'p' + } + }) + + mock.onAny().reply((config) => { + expect(config.headers?.Authorization).toMatch(/^Basic /) + return [200, { called: true }] + }) + + expect((await client.get('/')).data).toEqual({ called: true }) }) - it('uses password authentication', () => { - new OpenSearchHttpClient({ + it('allows to overwrite Authorization', async () => { + let client = new OpenSearchHttpClient({ url: 'https://localhost:9200', basic_auth: { - username: 'admin', - password: 'password' + username: 'u', + password: 'p' } }) - expect(mocked_axios.create.mock.calls[0][0]).toMatchObject({ - auth: { - username: 'admin', - password: 'password' - }, - baseURL: 'https://localhost:9200' + mock.onAny().reply((config) => { + expect(config.headers?.Authorization).toEqual('custom') + return [200, { called: true }] }) - expect(mocked_axios.interceptors.request.use).not.toHaveBeenCalled() + expect((await client.get('/', { headers: { Authorization: 'custom' } })).data).toEqual({ called: true }) }) - it('assigns a request interceptor with SigV4 authentication', () => { - new OpenSearchHttpClient({ + it('adds a Sigv4 header', async () => { + let client = new OpenSearchHttpClient({ url: 'https://localhost:9200', aws_auth: { aws_access_key_id: 'key id', aws_access_secret_key: 'secret key', aws_access_session_token: 'session token', - aws_region: 'us-west-2', + aws_region: 'us-west-42', aws_service: 'aoss' } }) - expect(mocked_axios.create.mock.calls[0][0]).toMatchObject({ - auth: undefined, - baseURL: 'https://localhost:9200' + mock.onAny().reply((config) => { + expect(config.headers?.Authorization).toMatch( + /^AWS4-HMAC-SHA256 Credential=key id\/\d*\/us-west-42\/aoss\/aws4_request/ + ) + return [200, { called: true }] }) - expect(mocked_axios.interceptors.request.use).toHaveBeenCalled() + expect((await client.get('/')).data).toEqual({ called: true }) }) })