From a558274d3c19a2d0d453b8e9f5bc233f201ebb85 Mon Sep 17 00:00:00 2001 From: Phil Adams Date: Wed, 17 Apr 2024 14:40:10 -0500 Subject: [PATCH] feat: send user-agent header with auth token requests (#272) This commit updates our various request-based authenticators so that the User-Agent header is included with each outbound token request. The value of the User-Agent header will be of the form "ibm-node-sdk-core/- ". Signed-off-by: Phil Adams --- .secrets.baseline | 32 +++--- .../token-managers/container-token-manager.ts | 5 +- auth/token-managers/cp4d-token-manager.ts | 6 +- .../iam-request-based-token-manager.ts | 3 +- auth/token-managers/iam-token-manager.ts | 5 +- auth/token-managers/mcsp-token-manager.ts | 6 +- auth/token-managers/token-manager.ts | 2 + .../vpc-instance-token-manager.ts | 5 + etc/ibm-cloud-sdk-core.api.md | 2 + lib/base-service.ts | 19 +++- lib/build-user-agent.ts | 31 ++++++ test/unit/base-service.test.js | 45 +++++++- test/unit/container-token-manager.test.js | 27 ++++- test/unit/cp4d-token-manager.test.js | 80 +++++++------ .../iam-request-based-token-manager.test.js | 17 +-- test/unit/iam-token-manager.test.js | 36 ++++-- test/unit/mcsp-token-manager.test.js | 52 +++++---- test/unit/utils.js | 27 +++++ test/unit/vpc-instance-token-manager.test.js | 105 +++++++++++------- 19 files changed, 353 insertions(+), 152 deletions(-) create mode 100644 lib/build-user-agent.ts create mode 100644 test/unit/utils.js diff --git a/.secrets.baseline b/.secrets.baseline index e3ebf9f68..80f3b9f03 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -3,7 +3,7 @@ "files": "package-lock.json|^.secrets.baseline$", "lines": null }, - "generated_at": "2024-04-10T16:52:29Z", + "generated_at": "2024-04-16T16:00:48Z", "plugins_used": [ { "name": "AWSKeyDetector" @@ -200,7 +200,7 @@ "hashed_secret": "184ee1f04a018aa3b897e085516a9b657fea0f6b", "is_secret": false, "is_verified": false, - "line_number": 85, + "line_number": 86, "type": "Secret Keyword", "verified_result": null } @@ -210,7 +210,7 @@ "hashed_secret": "d5ff02fa48e492fac0a245ad63d1ae608e705c05", "is_secret": false, "is_verified": false, - "line_number": 97, + "line_number": 98, "type": "Secret Keyword", "verified_result": null }, @@ -218,7 +218,7 @@ "hashed_secret": "8f4bfc22c4fd7cb884f94ec175ff4a3284a174a1", "is_secret": false, "is_verified": false, - "line_number": 98, + "line_number": 99, "type": "Secret Keyword", "verified_result": null }, @@ -226,7 +226,7 @@ "hashed_secret": "45a15668db917c293f16e8add0f5d801889e5923", "is_secret": false, "is_verified": false, - "line_number": 112, + "line_number": 116, "type": "Secret Keyword", "verified_result": null }, @@ -234,7 +234,7 @@ "hashed_secret": "65e622227634e8876cfa733000233fb80c6f0473", "is_secret": false, "is_verified": false, - "line_number": 113, + "line_number": 117, "type": "Secret Keyword", "verified_result": null } @@ -270,7 +270,7 @@ "hashed_secret": "8f4bfc22c4fd7cb884f94ec175ff4a3284a174a1", "is_secret": false, "is_verified": false, - "line_number": 59, + "line_number": 60, "type": "Secret Keyword", "verified_result": null }, @@ -278,7 +278,7 @@ "hashed_secret": "0358c67856fb6a21c4767daf02fcb8fe4dc0a318", "is_secret": false, "is_verified": false, - "line_number": 62, + "line_number": 63, "type": "Secret Keyword", "verified_result": null }, @@ -286,7 +286,7 @@ "hashed_secret": "dbb19b8ae3b78f908e1467721fe4c9f0b0529d9b", "is_secret": false, "is_verified": false, - "line_number": 63, + "line_number": 64, "type": "Secret Keyword", "verified_result": null } @@ -296,7 +296,7 @@ "hashed_secret": "8f4bfc22c4fd7cb884f94ec175ff4a3284a174a1", "is_secret": false, "is_verified": false, - "line_number": 78, + "line_number": 79, "type": "Secret Keyword", "verified_result": null }, @@ -304,7 +304,7 @@ "hashed_secret": "65e622227634e8876cfa733000233fb80c6f0473", "is_secret": false, "is_verified": false, - "line_number": 91, + "line_number": 95, "type": "Secret Keyword", "verified_result": null } @@ -442,7 +442,7 @@ "hashed_secret": "1572bd30ac06678a82df42b5913e5e52e27f9a12", "is_secret": false, "is_verified": false, - "line_number": 31, + "line_number": 27, "type": "Secret Keyword", "verified_result": null }, @@ -450,7 +450,7 @@ "hashed_secret": "16856d955c788df03735a24feb2e3ffefd91f3dc", "is_secret": false, "is_verified": false, - "line_number": 32, + "line_number": 28, "type": "Secret Keyword", "verified_result": null } @@ -512,7 +512,7 @@ "hashed_secret": "43ed4c2d8375dfc89e3dc8c917f404b9481d355b", "is_secret": false, "is_verified": false, - "line_number": 29, + "line_number": 30, "type": "Secret Keyword", "verified_result": null } @@ -522,7 +522,7 @@ "hashed_secret": "a7ef1be18bb8d37af79f3d87761a203378bf26a2", "is_secret": false, "is_verified": false, - "line_number": 151, + "line_number": 169, "type": "Secret Keyword", "verified_result": null } @@ -542,7 +542,7 @@ "hashed_secret": "f2e7745f43b0ef0e2c2faf61d6c6a28be2965750", "is_secret": false, "is_verified": false, - "line_number": 30, + "line_number": 26, "type": "Secret Keyword", "verified_result": null } diff --git a/auth/token-managers/container-token-manager.ts b/auth/token-managers/container-token-manager.ts index 5a5e68a9a..96f903491 100644 --- a/auth/token-managers/container-token-manager.ts +++ b/auth/token-managers/container-token-manager.ts @@ -1,5 +1,5 @@ /** - * Copyright 2021, 2023 IBM Corp. All Rights Reserved. + * (C) Copyright IBM Corp. 2021, 2024. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ import { atLeastOne } from '../utils/helpers'; import { readCrTokenFile } from '../utils/file-reading-helpers'; +import { buildUserAgent } from '../../lib/build-user-agent'; import { IamRequestBasedTokenManager, IamRequestOptions } from './iam-request-based-token-manager'; const DEFAULT_CR_TOKEN_FILEPATH1 = '/var/run/secrets/tokens/vault-token'; @@ -83,6 +84,8 @@ export class ContainerTokenManager extends IamRequestBasedTokenManager { // construct form data for the cr token use case of iam token management this.formData.grant_type = 'urn:ibm:params:oauth:grant-type:cr-token'; + + this.userAgent = buildUserAgent('container-authenticator'); } /** diff --git a/auth/token-managers/cp4d-token-manager.ts b/auth/token-managers/cp4d-token-manager.ts index 14cfbd0c6..381f84e5e 100644 --- a/auth/token-managers/cp4d-token-manager.ts +++ b/auth/token-managers/cp4d-token-manager.ts @@ -1,5 +1,5 @@ /** - * (C) Copyright IBM Corp. 2019, 2023. + * (C) Copyright IBM Corp. 2019, 2024. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ import extend from 'extend'; import { validateInput } from '../utils/helpers'; +import { buildUserAgent } from '../../lib/build-user-agent'; import { JwtTokenManager, JwtTokenManagerOptions } from './jwt-token-manager'; /** Configuration options for CP4D token retrieval. */ @@ -96,12 +97,15 @@ export class Cp4dTokenManager extends JwtTokenManager { this.username = options.username; this.password = options.password; this.apikey = options.apikey; + + this.userAgent = buildUserAgent('cp4d-authenticator'); } protected requestToken(): Promise { // these cannot be overwritten const requiredHeaders = { 'Content-Type': 'application/json', + 'User-Agent': this.userAgent, }; const parameters = { diff --git a/auth/token-managers/iam-request-based-token-manager.ts b/auth/token-managers/iam-request-based-token-manager.ts index cfdf0fddc..405679de7 100644 --- a/auth/token-managers/iam-request-based-token-manager.ts +++ b/auth/token-managers/iam-request-based-token-manager.ts @@ -157,7 +157,8 @@ export class IamRequestBasedTokenManager extends JwtTokenManager { protected requestToken(): Promise { // these cannot be overwritten const requiredHeaders = { - 'Content-type': 'application/x-www-form-urlencoded', + 'Content-Type': 'application/x-www-form-urlencoded', + 'User-Agent': this.userAgent, } as OutgoingHttpHeaders; // If both the clientId and secret were specified by the user, then use them. diff --git a/auth/token-managers/iam-token-manager.ts b/auth/token-managers/iam-token-manager.ts index edbd9ae6a..4710010af 100644 --- a/auth/token-managers/iam-token-manager.ts +++ b/auth/token-managers/iam-token-manager.ts @@ -1,5 +1,5 @@ /** - * (C) Copyright IBM Corp. 2019, 2023. + * (C) Copyright IBM Corp. 2019, 2024. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,6 +15,7 @@ */ import { validateInput } from '../utils/helpers'; +import { buildUserAgent } from '../../lib/build-user-agent'; import { IamRequestBasedTokenManager, IamRequestOptions } from './iam-request-based-token-manager'; /** Configuration options for IAM token retrieval. */ @@ -62,5 +63,7 @@ export class IamTokenManager extends IamRequestBasedTokenManager { this.formData.apikey = this.apikey; this.formData.grant_type = 'urn:ibm:params:oauth:grant-type:apikey'; this.formData.response_type = 'cloud_iam'; + + this.userAgent = buildUserAgent('iam-authenticator'); } } diff --git a/auth/token-managers/mcsp-token-manager.ts b/auth/token-managers/mcsp-token-manager.ts index 13c69924e..a1645918b 100644 --- a/auth/token-managers/mcsp-token-manager.ts +++ b/auth/token-managers/mcsp-token-manager.ts @@ -1,5 +1,5 @@ /** - * (C) Copyright IBM Corp. 2023. + * (C) Copyright IBM Corp. 2023, 2024. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ import extend from 'extend'; import { validateInput } from '../utils/helpers'; +import { buildUserAgent } from '../../lib/build-user-agent'; import { JwtTokenManager, JwtTokenManagerOptions } from './jwt-token-manager'; /** @@ -76,12 +77,15 @@ export class McspTokenManager extends JwtTokenManager { validateInput(options, this.requiredOptions); this.apikey = options.apikey; + + this.userAgent = buildUserAgent('mcsp-authenticator'); } protected requestToken(): Promise { const requiredHeaders = { Accept: 'application/json', 'Content-Type': 'application/json', + 'User-Agent': this.userAgent, }; const parameters = { diff --git a/auth/token-managers/token-manager.ts b/auth/token-managers/token-manager.ts index b5084941f..bf7c15784 100644 --- a/auth/token-managers/token-manager.ts +++ b/auth/token-managers/token-manager.ts @@ -47,6 +47,8 @@ export type TokenManagerOptions = { export class TokenManager { protected url: string; + protected userAgent: string; + protected disableSslVerification: boolean; protected headers: OutgoingHttpHeaders; diff --git a/auth/token-managers/vpc-instance-token-manager.ts b/auth/token-managers/vpc-instance-token-manager.ts index 104c814fe..a3fda8d63 100644 --- a/auth/token-managers/vpc-instance-token-manager.ts +++ b/auth/token-managers/vpc-instance-token-manager.ts @@ -16,6 +16,7 @@ import logger from '../../lib/logger'; import { atMostOne, getCurrentTime } from '../utils/helpers'; +import { buildUserAgent } from '../../lib/build-user-agent'; import { JwtTokenManager, JwtTokenManagerOptions } from './jwt-token-manager'; const DEFAULT_IMS_ENDPOINT = 'http://169.254.169.254'; @@ -87,6 +88,8 @@ export class VpcInstanceTokenManager extends JwtTokenManager { if (options.iamProfileId) { this.iamProfileId = options.iamProfileId; } + + this.userAgent = buildUserAgent('vpc-instance-authenticator'); } /** @@ -130,6 +133,7 @@ export class VpcInstanceTokenManager extends JwtTokenManager { method: 'POST', headers: { 'Content-Type': 'application/json', + 'User-Agent': this.userAgent, Accept: 'application/json', Authorization: `Bearer ${instanceIdentityToken}`, }, @@ -154,6 +158,7 @@ export class VpcInstanceTokenManager extends JwtTokenManager { method: 'PUT', headers: { 'Content-Type': 'application/json', + 'User-Agent': this.userAgent, Accept: 'application/json', 'Metadata-Flavor': 'ibm', }, diff --git a/etc/ibm-cloud-sdk-core.api.md b/etc/ibm-cloud-sdk-core.api.md index fa77e27a2..2f7eebca3 100644 --- a/etc/ibm-cloud-sdk-core.api.md +++ b/etc/ibm-cloud-sdk-core.api.md @@ -426,6 +426,8 @@ export class TokenManager { setHeaders(headers: OutgoingHttpHeaders): void; // (undocumented) protected url: string; + // (undocumented) + protected userAgent: string; } // @public diff --git a/lib/base-service.ts b/lib/base-service.ts index 1608aa535..135276d90 100644 --- a/lib/base-service.ts +++ b/lib/base-service.ts @@ -1,5 +1,5 @@ /** - * (C) Copyright IBM Corp. 2014, 2023. + * (C) Copyright IBM Corp. 2014, 2024. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,12 +14,14 @@ * limitations under the License. */ +import extend from 'extend'; import type { CookieJar } from 'tough-cookie'; import { OutgoingHttpHeaders } from 'http'; import { AuthenticatorInterface, checkCredentials, readExternalSources } from '../auth'; import { stripTrailingSlash } from './helper'; import logger from './logger'; import { RequestWrapper, RetryOptions } from './request-wrapper'; +import { buildUserAgent } from './build-user-agent'; /** * Configuration values for a service. @@ -71,6 +73,8 @@ export class BaseService { private requestWrapperInstance: RequestWrapper; + private defaultUserAgent; + /** * Configuration values for a service. * @@ -131,6 +135,8 @@ export class BaseService { } this.authenticator = options.authenticator; + + this.defaultUserAgent = buildUserAgent(); } /** @@ -271,6 +277,17 @@ export class BaseService { return Promise.reject(new Error('The service URL is required')); } + // make sure the outbound request contains a User-Agent header + const userAgent = { + 'User-Agent': this.defaultUserAgent, + }; + parameters.defaultOptions.headers = extend( + true, + {}, + userAgent, + parameters.defaultOptions.headers + ); + return this.authenticator.authenticate(parameters.defaultOptions).then(() => // resolve() handles rejection as well, so resolving the result of sendRequest should allow for proper handling later this.requestWrapperInstance.sendRequest(parameters) diff --git a/lib/build-user-agent.ts b/lib/build-user-agent.ts new file mode 100644 index 000000000..2c9189f77 --- /dev/null +++ b/lib/build-user-agent.ts @@ -0,0 +1,31 @@ +/** + * (C) Copyright IBM Corp. 2024. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const os = require('os'); +const { version } = require('../package.json'); + +/** + * Returns a string suitable as a value for the User-Agent header + * @param componentName optional name of a component to be included in the returned string + * @returns the user agent header value + */ +export function buildUserAgent(componentName: string = null): string { + const subComponent = componentName ? `/${componentName}` : ''; + const userAgent = `ibm-node-sdk-core${subComponent}-${version} os.name=${os.platform()} os.version=${os.release()} node.version=${ + process.version + }`; + return userAgent; +} diff --git a/test/unit/base-service.test.js b/test/unit/base-service.test.js index 327d641f3..d5ace3f89 100644 --- a/test/unit/base-service.test.js +++ b/test/unit/base-service.test.js @@ -1,5 +1,5 @@ /** - * (C) Copyright IBM Corp. 2019, 2021. + * (C) Copyright IBM Corp. 2019, 2024. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -305,7 +305,10 @@ describe('Base Service', () => { const parameters = { defaultOptions: { serviceUrl: DEFAULT_URL, - Accept: 'application/json', + headers: { + Accept: 'application/json', + 'User-Agent': 'my-user-agent', + }, }, options: { url: '/v2/assistants/{assistant_id}/sessions', @@ -323,7 +326,43 @@ describe('Base Service', () => { const args = sendRequestMock.mock.calls[0]; expect(args[0]).toEqual(parameters); - expect(testService.requestWrapperInstance.sendRequest).toBe(sendRequestMock); // verify it is calling the instance + + // verify it is calling the instance + expect(testService.requestWrapperInstance.sendRequest).toBe(sendRequestMock); + }); + it('createRequest should set a default User-Agent', async () => { + const testService = new TestService({ + authenticator: AUTHENTICATOR, + }); + + const parameters = { + defaultOptions: { + serviceUrl: DEFAULT_URL, + headers: { + Accept: 'application/json', + 'x-custom-header': 'foo', + }, + }, + options: { + url: '/v2/assistants/{assistant_id}/sessions', + method: 'POST', + path: { + id: '123', + }, + }, + }; + + await testService.createRequest(parameters); + + expect(authenticateMock).toHaveBeenCalled(); + expect(sendRequestMock).toHaveBeenCalled(); + + const requestOptions = sendRequestMock.mock.calls[0][0]; + expect(requestOptions.defaultOptions).toBeDefined(); + expect(requestOptions.defaultOptions.headers).toBeDefined(); + expect(requestOptions.defaultOptions.headers['User-Agent']).toMatch(/^ibm-node-sdk-core-.*$/); + expect(requestOptions.defaultOptions.headers.Accept).toBe('application/json'); + expect(requestOptions.defaultOptions.headers['x-custom-header']).toBe('foo'); }); it('createRequest should reject with an error if `serviceUrl` is not set', async () => { diff --git a/test/unit/container-token-manager.test.js b/test/unit/container-token-manager.test.js index de1be7fa8..d8f58db09 100644 --- a/test/unit/container-token-manager.test.js +++ b/test/unit/container-token-manager.test.js @@ -1,7 +1,7 @@ /* eslint-disable no-alert, no-console */ /** - * Copyright 2021, 2023 IBM Corp. All Rights Reserved. + * (C) Copyright IBM Corp. 2021, 2024. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,20 +19,22 @@ const path = require('path'); const { ContainerTokenManager } = require('../../dist/auth'); const { RequestWrapper } = require('../../dist/lib/request-wrapper'); +const { getRequestOptions } = require('./utils'); const logger = require('../../dist/lib/logger').default; // make sure no actual requests are sent jest.mock('../../dist/lib/request-wrapper'); -const sendRequestMock = jest.fn(); -RequestWrapper.mockImplementation(() => ({ - sendRequest: sendRequestMock, -})); const CR_TOKEN_FILENAME = '/path/to/file'; const IAM_PROFILE_NAME = 'some-name'; const IAM_PROFILE_ID = 'some-id'; describe('Container Token Manager', () => { + const sendRequestMock = jest.fn(); + RequestWrapper.mockImplementation(() => ({ + sendRequest: sendRequestMock, + })); + afterAll(() => { sendRequestMock.mockRestore(); }); @@ -133,5 +135,20 @@ describe('Container Token Manager', () => { expect(instance.formData.profile_id).toBe(IAM_PROFILE_ID); expect(instance.formData.profile_id).not.toBe(firstValue); }); + + it('should set User-Agent header', async () => { + const instance = new ContainerTokenManager({ + crTokenFilename: pathToTestToken, + iamProfileName: IAM_PROFILE_NAME, + }); + + await instance.requestToken(); + + const requestOptions = getRequestOptions(sendRequestMock); + expect(requestOptions.headers).toBeDefined(); + expect(requestOptions.headers['User-Agent']).toMatch( + /^ibm-node-sdk-core\/container-authenticator.*$/ + ); + }); }); }); diff --git a/test/unit/cp4d-token-manager.test.js b/test/unit/cp4d-token-manager.test.js index 9626e5210..05cf94a54 100644 --- a/test/unit/cp4d-token-manager.test.js +++ b/test/unit/cp4d-token-manager.test.js @@ -1,7 +1,7 @@ /* eslint-disable no-alert, no-console */ /** - * (C) Copyright IBM Corp. 2019, 2021. + * (C) Copyright IBM Corp. 2019, 2024. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,11 +21,7 @@ const { Cp4dTokenManager } = require('../../dist/auth'); // mock sendRequest jest.mock('../../dist/lib/request-wrapper'); const { RequestWrapper } = require('../../dist/lib/request-wrapper'); - -const mockSendRequest = jest.fn(); -RequestWrapper.mockImplementation(() => ({ - sendRequest: mockSendRequest, -})); +const { getRequestOptions } = require('./utils'); const USERNAME = 'sherlock'; const PASSWORD = 'holmes'; @@ -34,6 +30,11 @@ const URL = 'tokenservice.com'; const FULL_URL = 'tokenservice.com/v1/authorize'; describe('CP4D Token Manager', () => { + const sendRequestMock = jest.fn(); + RequestWrapper.mockImplementation(() => ({ + sendRequest: sendRequestMock, + })); + describe('constructor', () => { it('should initialize base variables - password edition', () => { const instance = new Cp4dTokenManager({ @@ -130,7 +131,7 @@ describe('CP4D Token Manager', () => { describe('requestToken', () => { afterEach(() => { - mockSendRequest.mockClear(); + sendRequestMock.mockClear(); }); it('should call sendRequest with all request options - password edition', () => { @@ -142,20 +143,16 @@ describe('CP4D Token Manager', () => { instance.requestToken(); - // extract arguments sendRequest was called with - const params = mockSendRequest.mock.calls[0][0]; - - expect(mockSendRequest).toHaveBeenCalled(); - expect(params.options).toBeDefined(); - expect(params.options.url).toBe(FULL_URL); - expect(params.options.method).toBe('POST'); - expect(params.options.rejectUnauthorized).toBe(true); - expect(params.options.headers).toBeDefined(); - expect(params.options.headers['Content-Type']).toBe('application/json'); - expect(params.options.body).toBeDefined(); - expect(params.options.body.username).toBe(USERNAME); - expect(params.options.body.password).toBe(PASSWORD); - expect(params.options.body.api_key).toBeUndefined(); + const requestOptions = getRequestOptions(sendRequestMock); + expect(requestOptions.url).toBe(FULL_URL); + expect(requestOptions.method).toBe('POST'); + expect(requestOptions.rejectUnauthorized).toBe(true); + expect(requestOptions.headers).toBeDefined(); + expect(requestOptions.headers['Content-Type']).toBe('application/json'); + expect(requestOptions.body).toBeDefined(); + expect(requestOptions.body.username).toBe(USERNAME); + expect(requestOptions.body.password).toBe(PASSWORD); + expect(requestOptions.body.api_key).toBeUndefined(); }); it('should call sendRequest with all request options - API key edition', () => { @@ -167,20 +164,33 @@ describe('CP4D Token Manager', () => { instance.requestToken(); - // extract arguments sendRequest was called with - const params = mockSendRequest.mock.calls[0][0]; - - expect(mockSendRequest).toHaveBeenCalled(); - expect(params.options).toBeDefined(); - expect(params.options.url).toBe(FULL_URL); - expect(params.options.method).toBe('POST'); - expect(params.options.rejectUnauthorized).toBe(true); - expect(params.options.headers).toBeDefined(); - expect(params.options.headers['Content-Type']).toBe('application/json'); - expect(params.options.body).toBeDefined(); - expect(params.options.body.username).toBe(USERNAME); - // expect(params.options.body.api_key).toBe(APIKEY); - expect(params.options.body.password).toBeUndefined(); + const requestOptions = getRequestOptions(sendRequestMock); + expect(sendRequestMock).toHaveBeenCalled(); + expect(requestOptions).toBeDefined(); + expect(requestOptions.url).toBe(FULL_URL); + expect(requestOptions.method).toBe('POST'); + expect(requestOptions.rejectUnauthorized).toBe(true); + expect(requestOptions.headers).toBeDefined(); + expect(requestOptions.headers['Content-Type']).toBe('application/json'); + expect(requestOptions.body).toBeDefined(); + expect(requestOptions.body.username).toBe(USERNAME); + expect(requestOptions.body.password).toBeUndefined(); + }); + + it('should set User-Agent header', async () => { + const instance = new Cp4dTokenManager({ + url: URL, + username: USERNAME, + password: PASSWORD, + }); + + await instance.requestToken(); + + const requestOptions = getRequestOptions(sendRequestMock); + expect(requestOptions.headers).toBeDefined(); + expect(requestOptions.headers['User-Agent']).toMatch( + /^ibm-node-sdk-core\/cp4d-authenticator.*$/ + ); }); }); }); diff --git a/test/unit/iam-request-based-token-manager.test.js b/test/unit/iam-request-based-token-manager.test.js index 50a38bf3c..b44c81a26 100644 --- a/test/unit/iam-request-based-token-manager.test.js +++ b/test/unit/iam-request-based-token-manager.test.js @@ -1,7 +1,7 @@ /* eslint-disable no-alert, no-console */ /** - * Copyright 2021 IBM Corp. All Rights Reserved. + * (C) Copyright IBM Corp. 2021, 2024. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ jest.mock('../../dist/lib/request-wrapper'); const { RequestWrapper } = require('../../dist/lib/request-wrapper'); +const { getRequestOptions } = require('./utils'); const logger = require('../../dist/lib/logger').default; const { IamRequestBasedTokenManager } = require('../../dist/auth'); @@ -30,16 +31,6 @@ const CLIENT_SECRET = 'some-secret'; const CLIENT_ID_SECRET_WARNING = 'Warning: Client ID and Secret must BOTH be given, or the header will not be included.'; -// a function to pull the arguments out of the `sendRequest` mock -// and verify the structure looks like it is supposed to -function getRequestOptions(sendRequestMock) { - const sendRequestArgs = sendRequestMock.mock.calls[0][0]; - expect(sendRequestArgs).toBeDefined(); - expect(sendRequestArgs.options).toBeDefined(); - - return sendRequestArgs.options; -} - describe('IAM Request Based Token Manager', () => { // set up mocks jest.spyOn(logger, 'warn').mockImplementation(() => {}); @@ -186,7 +177,7 @@ describe('IAM Request Based Token Manager', () => { const requestOptions = getRequestOptions(sendRequestMock); expect(requestOptions.headers).toBeDefined(); - expect(requestOptions.headers['Content-type']).toBe('application/x-www-form-urlencoded'); + expect(requestOptions.headers['Content-Type']).toBe('application/x-www-form-urlencoded'); }); // add custom headers if set @@ -201,7 +192,7 @@ describe('IAM Request Based Token Manager', () => { const requestOptions = getRequestOptions(sendRequestMock); expect(requestOptions.headers).toBeDefined(); - expect(requestOptions.headers['Content-type']).toBeDefined(); // verify default headers aren't overwritten + expect(requestOptions.headers['Content-Type']).toBeDefined(); // verify default headers aren't overwritten expect(requestOptions.headers['My-Header']).toBe('some-value'); }); diff --git a/test/unit/iam-token-manager.test.js b/test/unit/iam-token-manager.test.js index 8e58e022e..27e9b0f02 100644 --- a/test/unit/iam-token-manager.test.js +++ b/test/unit/iam-token-manager.test.js @@ -25,12 +25,7 @@ jest.mock('../../dist/lib/request-wrapper'); const { RequestWrapper } = require('../../dist/lib/request-wrapper'); const { IamTokenManager } = require('../../dist/auth'); const { getCurrentTime } = require('../../dist/auth/utils/helpers'); - -const mockSendRequest = jest.fn(); - -RequestWrapper.mockImplementation(() => ({ - sendRequest: mockSendRequest, -})); +const { getRequestOptions } = require('./utils'); const EXPIRATION_WINDOW = 10; const ACCESS_TOKEN = '9012'; @@ -47,12 +42,17 @@ const IAM_RESPONSE = { }; describe('IAM Token Manager', () => { + const sendRequestMock = jest.fn(); + RequestWrapper.mockImplementation(() => ({ + sendRequest: sendRequestMock, + })); + beforeEach(() => { - mockSendRequest.mockReset(); + sendRequestMock.mockReset(); }); afterAll(() => { - mockSendRequest.mockRestore(); + sendRequestMock.mockRestore(); }); it('should throw an error if apikey is not provided', () => { @@ -77,7 +77,7 @@ describe('IAM Token Manager', () => { apikey: 'abcd-1234', }); - mockSendRequest.mockImplementation((parameters) => Promise.resolve(IAM_RESPONSE)); + sendRequestMock.mockImplementation((parameters) => Promise.resolve(IAM_RESPONSE)); await instance.getToken(); @@ -91,7 +91,7 @@ describe('IAM Token Manager', () => { it('should turn an iam apikey into an access token', async () => { const instance = new IamTokenManager({ apikey: 'abcd-1234' }); - mockSendRequest.mockImplementation((parameters) => Promise.resolve(IAM_RESPONSE)); + sendRequestMock.mockImplementation((parameters) => Promise.resolve(IAM_RESPONSE)); const token = await instance.getToken(); @@ -114,7 +114,7 @@ describe('IAM Token Manager', () => { instance.expireTime = getCurrentTime() + EXPIRATION_WINDOW; // Set up our second mock response, then call getToken() and make sure we got the second access token. - mockSendRequest.mockImplementation((parameters) => Promise.resolve(IAM_RESPONSE)); + sendRequestMock.mockImplementation((parameters) => Promise.resolve(IAM_RESPONSE)); token = await instance.getToken(); expect(token).toBe(ACCESS_TOKEN); expect(requestMock).toHaveBeenCalled(); @@ -137,7 +137,7 @@ describe('IAM Token Manager', () => { .spyOn(instance, 'requestToken') .mockImplementation(() => Promise.resolve({ result: { access_token: ACCESS_TOKEN } })); - mockSendRequest.mockImplementation((parameters) => Promise.resolve(IAM_RESPONSE)); + sendRequestMock.mockImplementation((parameters) => Promise.resolve(IAM_RESPONSE)); const token = await instance.getToken(); expect(token).toBe(CURRENT_ACCESS_TOKEN); @@ -164,4 +164,16 @@ describe('IAM Token Manager', () => { expect(token).toBe(ACCESS_TOKEN); expect(requestMock).not.toHaveBeenCalled(); }); + + it('should set User-Agent header', async () => { + const instance = new IamTokenManager({ apikey: 'abcd-1234' }); + + await instance.requestToken(); + + const requestOptions = getRequestOptions(sendRequestMock); + expect(requestOptions.headers).toBeDefined(); + expect(requestOptions.headers['User-Agent']).toMatch( + /^ibm-node-sdk-core\/iam-authenticator.*$/ + ); + }); }); diff --git a/test/unit/mcsp-token-manager.test.js b/test/unit/mcsp-token-manager.test.js index ab036864b..5b41b08e1 100644 --- a/test/unit/mcsp-token-manager.test.js +++ b/test/unit/mcsp-token-manager.test.js @@ -1,7 +1,7 @@ /* eslint-disable no-alert, no-console */ /** - * (C) Copyright IBM Corp. 2023. + * (C) Copyright IBM Corp. 2023, 2024. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,11 +21,7 @@ const { McspTokenManager } = require('../../dist/auth'); // mock sendRequest jest.mock('../../dist/lib/request-wrapper'); const { RequestWrapper } = require('../../dist/lib/request-wrapper'); - -const mockSendRequest = jest.fn(); -RequestWrapper.mockImplementation(() => ({ - sendRequest: mockSendRequest, -})); +const { getRequestOptions } = require('./utils'); const APIKEY = 'my-api-key'; const URL = 'https://mcsp.ibm.com'; @@ -74,8 +70,13 @@ describe('MCSP Token Manager', () => { }); describe('requestToken', () => { + const sendRequestMock = jest.fn(); + RequestWrapper.mockImplementation(() => ({ + sendRequest: sendRequestMock, + })); + afterEach(() => { - mockSendRequest.mockClear(); + sendRequestMock.mockClear(); }); it('should call sendRequest with all request options', () => { @@ -86,19 +87,30 @@ describe('MCSP Token Manager', () => { instance.requestToken(); - // extract arguments sendRequest was called with - const params = mockSendRequest.mock.calls[0][0]; - - expect(mockSendRequest).toHaveBeenCalled(); - expect(params.options).toBeDefined(); - expect(params.options.url).toBe(FULL_URL); - expect(params.options.method).toBe('POST'); - expect(params.options.rejectUnauthorized).toBe(true); - expect(params.options.headers).toBeDefined(); - expect(params.options.headers['Content-Type']).toBe('application/json'); - expect(params.options.headers.Accept).toBe('application/json'); - expect(params.options.body).toBeDefined(); - expect(params.options.body.apikey).toBe(APIKEY); + const requestOptions = getRequestOptions(sendRequestMock); + expect(requestOptions.url).toBe(FULL_URL); + expect(requestOptions.method).toBe('POST'); + expect(requestOptions.rejectUnauthorized).toBe(true); + expect(requestOptions.headers).toBeDefined(); + expect(requestOptions.headers['Content-Type']).toBe('application/json'); + expect(requestOptions.headers.Accept).toBe('application/json'); + expect(requestOptions.body).toBeDefined(); + expect(requestOptions.body.apikey).toBe(APIKEY); + }); + + it('should set User-Agent header', async () => { + const instance = new McspTokenManager({ + url: URL, + apikey: APIKEY, + }); + + await instance.requestToken(); + + const requestOptions = getRequestOptions(sendRequestMock); + expect(requestOptions.headers).toBeDefined(); + expect(requestOptions.headers['User-Agent']).toMatch( + /^ibm-node-sdk-core\/mcsp-authenticator.*$/ + ); }); }); }); diff --git a/test/unit/utils.js b/test/unit/utils.js new file mode 100644 index 000000000..1fe9cf515 --- /dev/null +++ b/test/unit/utils.js @@ -0,0 +1,27 @@ +/** + * (C) Copyright IBM Corp. 2024. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// a function to pull the arguments out of the `sendRequest` mock +// and verify the structure looks like it is supposed to +function getRequestOptions(sendRequestMock, requestIndex = 0) { + const sendRequestArgs = sendRequestMock.mock.calls[requestIndex][0]; + expect(sendRequestArgs).toBeDefined(); + expect(sendRequestArgs.options).toBeDefined(); + + return sendRequestArgs.options; +} + +module.exports = { getRequestOptions }; diff --git a/test/unit/vpc-instance-token-manager.test.js b/test/unit/vpc-instance-token-manager.test.js index 7d5d45da8..9ebcd179e 100644 --- a/test/unit/vpc-instance-token-manager.test.js +++ b/test/unit/vpc-instance-token-manager.test.js @@ -1,7 +1,7 @@ /* eslint-disable no-alert, no-console */ /** - * Copyright 2021, 2024 IBM Corp. All Rights Reserved. + * (C) Copyright IBM Corp. 2021, 2024. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,15 +25,12 @@ const path = require('path'); const { VpcInstanceTokenManager } = require('../../dist/auth'); const { RequestWrapper } = require('../../dist/lib/request-wrapper'); const { getCurrentTime } = require('../../dist/auth/utils/helpers'); +const { getRequestOptions } = require('./utils'); const logger = require('../../dist/lib/logger').default; // make sure no actual requests are sent jest.mock('../../dist/lib/request-wrapper'); const TOKEN = 'abc123'; -const sendRequestMock = jest.fn().mockImplementation(() => ({ result: { access_token: TOKEN } })); -RequestWrapper.mockImplementation(() => ({ - sendRequest: sendRequestMock, -})); const debugLogSpy = jest.spyOn(logger, 'debug').mockImplementation(() => {}); @@ -42,6 +39,11 @@ const IAM_PROFILE_ID = 'some-id'; const EXPIRATION_WINDOW = 10; describe('VPC Instance Token Manager', () => { + const sendRequestMock = jest.fn().mockImplementation(() => ({ result: { access_token: TOKEN } })); + RequestWrapper.mockImplementation(() => ({ + sendRequest: sendRequestMock, + })); + afterAll(() => { sendRequestMock.mockRestore(); }); @@ -108,22 +110,20 @@ describe('VPC Instance Token Manager', () => { const instance = new VpcInstanceTokenManager({ url: '123.345.567' }); await instance.getInstanceIdentityToken(); - const parameters = sendRequestMock.mock.calls[0][0]; - expect(parameters).toBeDefined(); - expect(parameters.options).toBeDefined(); - expect(parameters.options.url).toBe('123.345.567/instance_identity/v1/token'); - expect(parameters.options.method).toBe('PUT'); + const requestOptions = getRequestOptions(sendRequestMock); + expect(requestOptions.url).toBe('123.345.567/instance_identity/v1/token'); + expect(requestOptions.method).toBe('PUT'); - expect(parameters.options.qs).toBeDefined(); - expect(parameters.options.qs.version).toBe('2022-03-01'); + expect(requestOptions.qs).toBeDefined(); + expect(requestOptions.qs.version).toBe('2022-03-01'); - expect(parameters.options.body).toBeDefined(); - expect(parameters.options.body.expires_in).toBe(300); + expect(requestOptions.body).toBeDefined(); + expect(requestOptions.body.expires_in).toBe(300); - expect(parameters.options.headers).toBeDefined(); - expect(parameters.options.headers['Content-Type']).toBe('application/json'); - expect(parameters.options.headers.Accept).toBe('application/json'); - expect(parameters.options.headers['Metadata-Flavor']).toBe('ibm'); + expect(requestOptions.headers).toBeDefined(); + expect(requestOptions.headers['Content-Type']).toBe('application/json'); + expect(requestOptions.headers.Accept).toBe('application/json'); + expect(requestOptions.headers['Metadata-Flavor']).toBe('ibm'); }); it('should make the request then extract and return the token', async () => { @@ -152,6 +152,18 @@ describe('VPC Instance Token Manager', () => { "Caught exception from VPC 'create_access_token' operation: This is an error." ); }); + + it('should set User-Agent header', async () => { + const instance = new VpcInstanceTokenManager({ iamProfileId: 'some-id' }); + + await instance.requestToken(); + + const requestOptions = getRequestOptions(sendRequestMock); + expect(requestOptions.headers).toBeDefined(); + expect(requestOptions.headers['User-Agent']).toMatch( + /^ibm-node-sdk-core\/vpc-instance-authenticator.*$/ + ); + }); }); describe('requestToken', () => { @@ -159,22 +171,21 @@ describe('VPC Instance Token Manager', () => { const instance = new VpcInstanceTokenManager({ url: '123.345.567' }); await instance.requestToken(); - const parameters = sendRequestMock.mock.calls[1][0]; - expect(parameters).toBeDefined(); - expect(parameters.options).toBeDefined(); - expect(parameters.options.url).toBe('123.345.567/instance_identity/v1/iam_token'); - expect(parameters.options.method).toBe('POST'); + const requestOptions = getRequestOptions(sendRequestMock, 1); + expect(requestOptions).toBeDefined(); + expect(requestOptions.url).toBe('123.345.567/instance_identity/v1/iam_token'); + expect(requestOptions.method).toBe('POST'); - expect(parameters.options.qs).toBeDefined(); - expect(parameters.options.qs.version).toBe('2022-03-01'); + expect(requestOptions.qs).toBeDefined(); + expect(requestOptions.qs.version).toBe('2022-03-01'); // if neither the profile id or crn is set, then the body should be undefined - expect(parameters.options.body).toBeUndefined(); + expect(requestOptions.body).toBeUndefined(); - expect(parameters.options.headers).toBeDefined(); - expect(parameters.options.headers['Content-Type']).toBe('application/json'); - expect(parameters.options.headers.Accept).toBe('application/json'); - expect(parameters.options.headers.Authorization).toBe(`Bearer ${TOKEN}`); + expect(requestOptions.headers).toBeDefined(); + expect(requestOptions.headers['Content-Type']).toBe('application/json'); + expect(requestOptions.headers.Accept).toBe('application/json'); + expect(requestOptions.headers.Authorization).toBe(`Bearer ${TOKEN}`); // check logs expect(debugLogSpy.mock.calls[2][0]).toBe( @@ -186,24 +197,34 @@ describe('VPC Instance Token Manager', () => { const instance = new VpcInstanceTokenManager({ iamProfileCrn: 'some-crn' }); await instance.requestToken(); - const parameters = sendRequestMock.mock.calls[1][0]; - expect(parameters).toBeDefined(); - expect(parameters.options).toBeDefined(); - expect(parameters.options.body).toBeDefined(); - expect(parameters.options.body.trusted_profile).toBeDefined(); - expect(parameters.options.body.trusted_profile.crn).toBe('some-crn'); + const requestOptions = getRequestOptions(sendRequestMock, 1); + expect(requestOptions).toBeDefined(); + expect(requestOptions.body).toBeDefined(); + expect(requestOptions.body.trusted_profile).toBeDefined(); + expect(requestOptions.body.trusted_profile.crn).toBe('some-crn'); }); it('should set trusted profile to iam profile id, if set', async () => { const instance = new VpcInstanceTokenManager({ iamProfileId: 'some-id' }); await instance.requestToken(); - const parameters = sendRequestMock.mock.calls[1][0]; - expect(parameters).toBeDefined(); - expect(parameters.options).toBeDefined(); - expect(parameters.options.body).toBeDefined(); - expect(parameters.options.body.trusted_profile).toBeDefined(); - expect(parameters.options.body.trusted_profile.id).toBe('some-id'); + const requestOptions = getRequestOptions(sendRequestMock, 1); + expect(requestOptions).toBeDefined(); + expect(requestOptions.body).toBeDefined(); + expect(requestOptions.body.trusted_profile).toBeDefined(); + expect(requestOptions.body.trusted_profile.id).toBe('some-id'); + }); + + it('should set User-Agent header', async () => { + const instance = new VpcInstanceTokenManager({ iamProfileId: 'some-id' }); + + await instance.requestToken(); + + const requestOptions = getRequestOptions(sendRequestMock, 1); + expect(requestOptions.headers).toBeDefined(); + expect(requestOptions.headers['User-Agent']).toMatch( + /^ibm-node-sdk-core\/vpc-instance-authenticator.*$/ + ); }); }); describe('getToken', () => {