diff --git a/.secrets.baseline b/.secrets.baseline index 4222988c7..1f229f736 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -3,7 +3,7 @@ "files": "package-lock.json|^.secrets.baseline$", "lines": null }, - "generated_at": "2023-06-22T15:14:17Z", + "generated_at": "2023-11-09T19:29:53Z", "plugins_used": [ { "name": "AWSKeyDetector" @@ -70,7 +70,7 @@ "hashed_secret": "91dfd9ddb4198affc5c194cd8ce6d338fde470e2", "is_secret": false, "is_verified": false, - "line_number": 73, + "line_number": 74, "type": "Secret Keyword", "verified_result": null }, @@ -78,7 +78,7 @@ "hashed_secret": "98635b2eaa2379f28cd6d72a38299f286b81b459", "is_secret": false, "is_verified": false, - "line_number": 432, + "line_number": 433, "type": "Secret Keyword", "verified_result": null }, @@ -86,7 +86,7 @@ "hashed_secret": "47fcf185ee7e15fe05cae31fbe9e4ebe4a06a40d", "is_secret": false, "is_verified": false, - "line_number": 470, + "line_number": 543, "type": "Secret Keyword", "verified_result": null } @@ -96,7 +96,7 @@ "hashed_secret": "bc2f74c22f98f7b6ffbc2f67453dbfa99bce9a32", "is_secret": false, "is_verified": false, - "line_number": 55, + "line_number": 97, "type": "Secret Keyword", "verified_result": null } @@ -106,7 +106,7 @@ "hashed_secret": "32e8612d8ca77c7ea8374aa7918db8e5df9252ed", "is_secret": false, "is_verified": false, - "line_number": 62, + "line_number": 63, "type": "Secret Keyword", "verified_result": null } @@ -185,6 +185,16 @@ "verified_result": null } ], + "auth/authenticators/mcsp-authenticator.ts": [ + { + "hashed_secret": "8f4bfc22c4fd7cb884f94ec175ff4a3284a174a1", + "is_secret": false, + "is_verified": false, + "line_number": 60, + "type": "Secret Keyword", + "verified_result": null + } + ], "auth/token-managers/container-token-manager.ts": [ { "hashed_secret": "184ee1f04a018aa3b897e085516a9b657fea0f6b", @@ -281,12 +291,30 @@ "verified_result": null } ], + "auth/token-managers/mcsp-token-manager.ts": [ + { + "hashed_secret": "8f4bfc22c4fd7cb884f94ec175ff4a3284a174a1", + "is_secret": false, + "is_verified": false, + "line_number": 78, + "type": "Secret Keyword", + "verified_result": null + }, + { + "hashed_secret": "65e622227634e8876cfa733000233fb80c6f0473", + "is_secret": false, + "is_verified": false, + "line_number": 91, + "type": "Secret Keyword", + "verified_result": null + } + ], "auth/utils/get-authenticator-from-environment.ts": [ { "hashed_secret": "6947818ac409551f11fbaa78f0ea6391960aa5b8", "is_secret": false, "is_verified": false, - "line_number": 49, + "line_number": 50, "type": "Secret Keyword", "verified_result": null } @@ -306,7 +334,7 @@ "hashed_secret": "45c43fe97e3a06ab078b0eeff6fbe622cc417a25", "is_secret": false, "is_verified": false, - "line_number": 279, + "line_number": 283, "type": "Secret Keyword", "verified_result": null } @@ -499,6 +527,26 @@ "verified_result": null } ], + "test/unit/mcsp-authenticator.test.js": [ + { + "hashed_secret": "0c910ad3070d996b37a1c65f542b17adc3f962bc", + "is_secret": false, + "is_verified": false, + "line_number": 20, + "type": "Secret Keyword", + "verified_result": null + } + ], + "test/unit/mcsp-token-manager.test.js": [ + { + "hashed_secret": "f2e7745f43b0ef0e2c2faf61d6c6a28be2965750", + "is_secret": false, + "is_verified": false, + "line_number": 30, + "type": "Secret Keyword", + "verified_result": null + } + ], "test/unit/read-external-sources.test.js": [ { "hashed_secret": "4c65cd3f160d60f7ca28ca04fa60b9035132781c", @@ -534,7 +582,7 @@ } ] }, - "version": "0.13.1+ibm.60.dss", + "version": "0.13.1+ibm.61.dss", "word_list": { "file": null, "hash": null diff --git a/Authentication.md b/Authentication.md index 5ef5d6f60..4514d0c8d 100644 --- a/Authentication.md +++ b/Authentication.md @@ -6,6 +6,7 @@ The node-sdk-core project supports the following types of authentication: - Container Authentication - VPC Instance Authentication - Cloud Pak for Data Authentication +- Multi-Cloud Saas Platform (MCSP) Authentication - No Authentication (for testing) The SDK user configures the appropriate type of authentication for use with service instances. @@ -484,6 +485,78 @@ const service = ExampleServiceV1.newInstance(options); ``` +## Multi-Cloud Saas Platform (MCSP) Authentication +The `McspAuthenticator` can be used in scenarios where an application needs to +interact with an IBM Cloud service that has been deployed to a non-IBM Cloud environment (e.g. AWS). +It accepts a user-supplied apikey and performs the necessary interactions with the +Multi-Cloud Saas Platform token service to obtain a suitable MCSP access token (a bearer token) +for the specified apikey. +The authenticator will also obtain a new bearer token when the current token expires. +The bearer token is then added to each outbound request in the `Authorization` header in the +form: +``` + Authorization: Bearer +``` + +### Properties + +- apikey: (required) the apikey to be used to obtain an MCSP access token. + +- url: (required) The URL representing the MCSP token service endpoint's base URL string. Do not include the +operation path (e.g. `/siusermgr/api/1.0/apikeys/token`) as part of this property's value. + +- disableSSLVerification: (optional) A flag that indicates whether verificaton of the server's SSL +certificate should be disabled or not. The default value is `false`. + +- headers: (optional) A set of key/value pairs that will be sent as HTTP headers in requests +made to the MCSP token service. + +### Usage Notes +- When constructing an McspAuthenticator instance, you must specify the apikey and url properties. + +- The authenticator will use the token server's `POST /siusermgr/api/1.0/apikeys/token` operation to +exchange the user-supplied apikey for an MCSP access token (the bearer token). + +### Programming example +```js +const { McspAuthenticator } = require('ibm-cloud-sdk-core'); +const ExampleServiceV1 = require('/example-service/v1'); + +const authenticator = new McspAuthenticator({ + apikey: 'myapikey', + url: 'https://example.mcsp.token-exchange.com', +}); + +const options = { + authenticator, +}; + +const service = new ExampleServiceV1(options); + +// 'service' can now be used to invoke operations. +``` + +### Configuration example +External configuration: +``` +export EXAMPLE_SERVICE_AUTH_TYPE=mcsp +export EXAMPLE_SERVICE_APIKEY=myapikey +export EXAMPLE_SERVICE_AUTH_URL=https://example.mcsp.token-exchange.com +``` +Application code: +```js +const ExampleServiceV1 = require('/example-service/v1'); + +const options = { + serviceName: 'example_service', +}; + +const service = ExampleServiceV1.newInstance(options); + +// 'service' can now be used to invoke operations. +``` + + ## No Auth Authentication The `NoAuthAuthenticator` is a placeholder authenticator which performs no actual authentication function. It can be used in situations where authentication needs to be bypassed, perhaps while developing diff --git a/auth/authenticators/authenticator.ts b/auth/authenticators/authenticator.ts index 45f596f11..94becdd96 100644 --- a/auth/authenticators/authenticator.ts +++ b/auth/authenticators/authenticator.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-unused-vars, class-methods-use-this */ /** - * (C) Copyright IBM Corp. 2019, 2022. + * (C) Copyright IBM Corp. 2019, 2023. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -40,6 +40,8 @@ export class Authenticator implements AuthenticatorInterface { static AUTHTYPE_VPC = 'vpc'; + static AUTHTYPE_MCSP = 'mcsp'; + static AUTHTYPE_UNKNOWN = 'unknown'; /** diff --git a/auth/authenticators/index.ts b/auth/authenticators/index.ts index b18ccebd4..c25b6e92a 100644 --- a/auth/authenticators/index.ts +++ b/auth/authenticators/index.ts @@ -38,6 +38,7 @@ * IAMAuthenticator: Authenticator for passing IAM authentication information to service endpoint. * ContainerAuthenticator: Authenticator for passing IAM authentication to a service, based on a token living on the container. * VpcInstanceAuthenticator: Authenticator that uses the VPC Instance Metadata Service API to retrieve an IAM token. + * McspAuthenticator: Authenticator for passing MCSP authentication to a service endpoint. * NoAuthAuthenticator: Performs no authentication. Useful for testing purposes. */ @@ -52,3 +53,4 @@ export { NoAuthAuthenticator } from './no-auth-authenticator'; export { IamRequestBasedAuthenticator } from './iam-request-based-authenticator'; export { TokenRequestBasedAuthenticator } from './token-request-based-authenticator'; export { VpcInstanceAuthenticator } from './vpc-instance-authenticator'; +export { McspAuthenticator } from './mcsp-authenticator'; diff --git a/auth/authenticators/mcsp-authenticator.ts b/auth/authenticators/mcsp-authenticator.ts new file mode 100644 index 000000000..ff1ab6032 --- /dev/null +++ b/auth/authenticators/mcsp-authenticator.ts @@ -0,0 +1,78 @@ +/** + * (C) Copyright IBM Corp. 2023. + * + * 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. + */ + +import { Authenticator } from './authenticator'; +import { McspTokenManager } from '../token-managers/mcsp-token-manager'; +import { BaseOptions, TokenRequestBasedAuthenticator } from './token-request-based-authenticator'; + +/** Configuration options for Multi-Cloud Saas Platform (MCSP) authentication. */ +export interface Options extends BaseOptions { + /** The API key used to obtain an MCSP access token. */ + apikey: string; + /** The URL representing the MCSP token service endpoint. */ + url: string; +} + +/** + * The McspAuthenticator uses an apikey to obtain an access token from the MCSP token server. + * When the access token expires, a new access token is obtained from the token server. + * The access token will be added to outbound requests via the Authorization header + * of the form: "Authorization: Bearer " + */ +export class McspAuthenticator extends TokenRequestBasedAuthenticator { + protected requiredOptions = ['apikey', 'url']; + + protected tokenManager: McspTokenManager; + + private apikey: string; + + /** + * Create a new McspAuthenticator instance. + * + * @param options - Configuration options for CloudPakForData authentication. + * This should be an object containing these fields: + * - url: (required) the endpoint URL for the CloudPakForData token service + * - username: (required) the username used to obtain a bearer token + * - password: (optional) the password used to obtain a bearer token (required if apikey is not specified) + * - apikey: (optional) the API key used to obtain a bearer token (required if password is not specified) + * - disableSslVerification: (optional) a flag that indicates whether verification of the token server's SSL certificate + * should be disabled or not + * - headers: (optional) a set of HTTP headers to be sent with each request to the token service + * + * @throws Error: the username, password, and/or url are not valid, or unspecified, for Cloud Pak For Data token requests. + */ + constructor(options: Options) { + super(options); + + this.apikey = options.apikey; + this.url = options.url; + + // the param names are shared between the authenticator and the token + // manager so we can just pass along the options object. + // also, the token manager will handle input validation + this.tokenManager = new McspTokenManager(options); + } + + /** + * Returns the authenticator's type ('cp4d'). + * + * @returns a string that indicates the authenticator's type + */ + // eslint-disable-next-line class-methods-use-this + public authenticationType(): string { + return Authenticator.AUTHTYPE_MCSP; + } +} diff --git a/auth/token-managers/index.ts b/auth/token-managers/index.ts index 8f81c52d5..3836a0ec3 100644 --- a/auth/token-managers/index.ts +++ b/auth/token-managers/index.ts @@ -1,5 +1,5 @@ /** - * (C) Copyright IBM Corp. 2019, 2021. + * (C) Copyright IBM Corp. 2019, 2023. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,15 +22,17 @@ * Cloud Pak for Data * Container (IKS, etc) * VPC Instance + * Multi-Cloud Saas Platform (MCSP) * * The token managers sit inside of an authenticator and do the work to retrieve - * tokens where as the authenticators add these tokens to the actual request. + * tokens, whereas the authenticators add these tokens to the actual request. * * classes: * IamTokenManager: Token Manager of IAM via apikey. * Cp4dTokenManager: Token Manager of CloudPak for data. * ContainerTokenManager: Token manager of IAM via compute resource token. * VpcInstanceTokenManager: Token manager of VPC Instance Metadata Service API tokens. + * McspTokenManager: Token Manager of MCSP via apikey. * JwtTokenManager: A class for shared functionality for parsing, storing, and requesting JWT tokens. */ @@ -41,3 +43,4 @@ export { IamRequestBasedTokenManager, IamRequestOptions } from './iam-request-ba export { JwtTokenManager, JwtTokenManagerOptions } from './jwt-token-manager'; export { TokenManager, TokenManagerOptions } from './token-manager'; export { VpcInstanceTokenManager } from './vpc-instance-token-manager'; +export { McspTokenManager } from './mcsp-token-manager'; diff --git a/auth/token-managers/mcsp-token-manager.ts b/auth/token-managers/mcsp-token-manager.ts new file mode 100644 index 000000000..13c69924e --- /dev/null +++ b/auth/token-managers/mcsp-token-manager.ts @@ -0,0 +1,100 @@ +/** + * (C) Copyright IBM Corp. 2023. + * + * 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. + */ + +import extend from 'extend'; +import { validateInput } from '../utils/helpers'; +import { JwtTokenManager, JwtTokenManagerOptions } from './jwt-token-manager'; + +/** + * Configuration options for MCSP token retrieval. + */ +interface Options extends JwtTokenManagerOptions { + /** The API key used to obtain an access token. */ + apikey: string; + + /** The base endpoint URL for MCSP token requests. */ + url: string; +} + +/** + * This interface models the response object received from the MCSP token service. + */ +export interface McspTokenData { + token: string; + token_type: string; + expires_in: number; +} + +/** + * This is the path associated with the operation used to obtain + * an access token from the MCSP token service. + */ +const OPERATION_PATH = '/siusermgr/api/1.0/apikeys/token'; + +/** + * Token Manager for Multi-Cloud Saas Platform (MCSP) authenticator. + * + * The Token Manager will invoke the MCSP token service's 'POST /siusermgr/api/1.0/apikeys/token' + * operation to obtain an MCSP access token for a user-supplied apikey. + */ +export class McspTokenManager extends JwtTokenManager { + protected requiredOptions = ['apikey', 'url']; + + private apikey: string; + + /** + * Create a new McspTokenManager instance. + * + * @param options - Configuration options + * This should be an object containing these fields: + * - url: (required) the base endpoint URL for the MCSP token service + * - apikey: (required) the API key used to obtain the MCSP access token. + * - disableSslVerification: (optional) a flag that indicates whether verification of the token server's SSL certificate + * should be disabled or not + * - headers: (optional) a set of HTTP headers to be sent with each request to the token service + * + * @throws Error: the configuration options were invalid. + */ + constructor(options: Options) { + super(options); + + this.tokenName = 'token'; + + validateInput(options, this.requiredOptions); + + this.apikey = options.apikey; + } + + protected requestToken(): Promise { + const requiredHeaders = { + Accept: 'application/json', + 'Content-Type': 'application/json', + }; + + const parameters = { + options: { + url: this.url + OPERATION_PATH, + body: { + apikey: this.apikey, + }, + method: 'POST', + headers: extend(true, {}, this.headers, requiredHeaders), + rejectUnauthorized: !this.disableSslVerification, + }, + }; + return this.requestWrapperInstance.sendRequest(parameters); + } +} diff --git a/auth/utils/get-authenticator-from-environment.ts b/auth/utils/get-authenticator-from-environment.ts index 6538160a5..5601917c2 100644 --- a/auth/utils/get-authenticator-from-environment.ts +++ b/auth/utils/get-authenticator-from-environment.ts @@ -23,6 +23,7 @@ import { ContainerAuthenticator, NoAuthAuthenticator, VpcInstanceAuthenticator, + McspAuthenticator, } from '../authenticators'; import { readExternalSources } from './read-external-sources'; @@ -98,6 +99,8 @@ export function getAuthenticatorFromEnvironment(serviceName: string): Authentica authenticator = new ContainerAuthenticator(credentials); } else if (authType === Authenticator.AUTHTYPE_VPC.toLowerCase()) { authenticator = new VpcInstanceAuthenticator(credentials); + } else if (authType === Authenticator.AUTHTYPE_MCSP.toLowerCase()) { + authenticator = new McspAuthenticator(credentials); } else { throw new Error(`Invalid value for AUTH_TYPE: ${authType}`); } diff --git a/etc/ibm-cloud-sdk-core.api.md b/etc/ibm-cloud-sdk-core.api.md index dcd9936ff..a030dba6a 100644 --- a/etc/ibm-cloud-sdk-core.api.md +++ b/etc/ibm-cloud-sdk-core.api.md @@ -34,6 +34,8 @@ export class Authenticator implements AuthenticatorInterface { // (undocumented) static AUTHTYPE_IAM: string; // (undocumented) + static AUTHTYPE_MCSP: string; + // (undocumented) static AUTHTYPE_NOAUTH: string; // (undocumented) static AUTHTYPE_UNKNOWN: string; @@ -329,6 +331,27 @@ export class JwtTokenManager extends TokenManager { // @public export type JwtTokenManagerOptions = TokenManagerOptions; +// @public +export class McspAuthenticator extends TokenRequestBasedAuthenticator { + // Warning: (ae-forgotten-export) The symbol "Options_12" needs to be exported by the entry point index.d.ts + constructor(options: Options_12); + authenticationType(): string; + // (undocumented) + protected requiredOptions: string[]; + // (undocumented) + protected tokenManager: McspTokenManager; +} + +// @public +export class McspTokenManager extends JwtTokenManager { + // Warning: (ae-forgotten-export) The symbol "Options_11" needs to be exported by the entry point index.d.ts + constructor(options: Options_11); + // (undocumented) + protected requestToken(): Promise; + // (undocumented) + protected requiredOptions: string[]; +} + // @public export class NoAuthAuthenticator extends Authenticator { // (undocumented) diff --git a/test/integration/iam-authenticator.test.js b/test/integration/iam-authenticator.test.js index a13903c57..de9a91c6f 100644 --- a/test/integration/iam-authenticator.test.js +++ b/test/integration/iam-authenticator.test.js @@ -26,12 +26,12 @@ const { getAuthenticatorFromEnvironment } = require('../../dist'); // IAMTEST_APIKEY= // // Then run this command from the project root: -// jest test/integration/iam-authenticator.test.js +// npm run jest test/integration/iam-authenticator.test.js describe('IAM Authenticator - Integration Test', () => { process.env.IBM_CREDENTIALS_FILE = `${__dirname}/../../iamtest.env`; - it('should retrieve an IAM access successfully', async () => { + it('should retrieve an IAM access token successfully', async () => { // set up environment const authenticator = getAuthenticatorFromEnvironment('iamtest'); diff --git a/test/integration/mcsp-authenticator.test.js b/test/integration/mcsp-authenticator.test.js new file mode 100644 index 000000000..dd181a720 --- /dev/null +++ b/test/integration/mcsp-authenticator.test.js @@ -0,0 +1,48 @@ +/** + * Copyright 2023 IBM Corp. All Rights Reserved. + * + * 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 { getAuthenticatorFromEnvironment } = require('../../dist'); + +// Note: Only the unit tests are run by default. +// +// In order to test with a live MCSP server, create file "mcsptest.env" in the project root. +// It should look like this: +// +// MCSPTEST_AUTH_URL= e.g. https://iam.platform.test.saas.ibm.com +// MCSPTEST_AUTH_TYPE=mcsp +// MCSPTEST_APIKEY= +// +// Then run this command from the project root: +// npm run jest test/integration/mcsp-authenticator.test.js + +describe('MCSP Authenticator - Integration Test', () => { + process.env.IBM_CREDENTIALS_FILE = `${__dirname}/../../mcsptest.env`; + + it('should retrieve an MCSP access token successfully', async () => { + // set up environment + const authenticator = getAuthenticatorFromEnvironment('mcsptest1'); + + // build a mock request + const requestOptions = {}; + + // authenticate the request + await authenticator.authenticate(requestOptions); + + // check for proper authentication + expect(requestOptions.headers.Authorization).toBeDefined(); + expect(requestOptions.headers.Authorization.startsWith('Bearer')).toBe(true); + }); +}); diff --git a/test/unit/get-authenticator-from-environment.test.js b/test/unit/get-authenticator-from-environment.test.js index 5f07e0eb1..a3e9e5fda 100644 --- a/test/unit/get-authenticator-from-environment.test.js +++ b/test/unit/get-authenticator-from-environment.test.js @@ -23,6 +23,7 @@ const { IamAuthenticator, ContainerAuthenticator, VpcInstanceAuthenticator, + McspAuthenticator, NoAuthAuthenticator, } = require('../../dist/auth'); @@ -80,6 +81,13 @@ describe('Get Authenticator From Environment Module', () => { expect(authenticator.authenticationType()).toEqual(Authenticator.AUTHTYPE_IAM); }); + it('should get mcsp authenticator', () => { + setUpMcspPayload(); + const authenticator = getAuthenticatorFromEnvironment(SERVICE_NAME); + expect(authenticator).toBeInstanceOf(McspAuthenticator); + expect(authenticator.authenticationType()).toEqual(Authenticator.AUTHTYPE_MCSP); + }); + it('should get cp4d authenticator', () => { setUpCp4dPayload(); const authenticator = getAuthenticatorFromEnvironment(SERVICE_NAME); @@ -180,6 +188,14 @@ function setUpIamPayloadWithScope() { })); } +function setUpMcspPayload() { + readExternalSourcesMock.mockImplementation(() => ({ + authType: 'mcsp', + apikey: APIKEY, + authUrl: TOKEN_URL, + })); +} + function setUpCp4dPayload() { readExternalSourcesMock.mockImplementation(() => ({ authtype: 'cP4d', diff --git a/test/unit/mcsp-authenticator.test.js b/test/unit/mcsp-authenticator.test.js new file mode 100644 index 000000000..cbcd73d1a --- /dev/null +++ b/test/unit/mcsp-authenticator.test.js @@ -0,0 +1,117 @@ +/** + * (C) Copyright IBM Corp. 2023. + * + * 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 { Authenticator, McspAuthenticator } = require('../../dist/auth'); +const { McspTokenManager } = require('../../dist/auth'); + +const APIKEY = '32611'; +const URL = 'https://mcsp.ibm.com'; +const CONFIG = { + apikey: APIKEY, + url: URL, + disableSslVerification: true, + headers: { + 'X-My-Header': 'some-value', + }, +}; + +// mock the `getToken` method in the token manager - dont make any rest calls +const fakeToken = 'mcsp-acess-token'; +const mockedTokenManager = new McspTokenManager({ + url: URL, + apikey: APIKEY, +}); + +const getTokenSpy = jest + .spyOn(mockedTokenManager, 'getToken') + .mockImplementation(() => Promise.resolve(fakeToken)); + +describe('MCSP Authenticator', () => { + it('should store all CONFIG options on the class', () => { + const authenticator = new McspAuthenticator(CONFIG); + expect(authenticator.authenticationType()).toEqual(Authenticator.AUTHTYPE_MCSP); + expect(authenticator.apikey).toBe(CONFIG.apikey); + expect(authenticator.url).toBe(CONFIG.url); + expect(authenticator.disableSslVerification).toBe(CONFIG.disableSslVerification); + expect(authenticator.headers).toEqual(CONFIG.headers); + + // should also create a token manager + expect(authenticator.tokenManager).toBeInstanceOf(McspTokenManager); + }); + + it('should store apikey and url on the class if provided', () => { + const authenticator = new McspAuthenticator({ + url: URL, + apikey: APIKEY, + }); + + expect(authenticator.apikey).toBe(APIKEY); + expect(authenticator.url).toBe(URL); + }); + + it('should throw an error when apikey is not provided', () => { + expect(() => { + const unused = new McspAuthenticator({ url: URL }); + }).toThrow(/Missing required parameter/); + }); + + it('should throw an error when url is not provided', () => { + expect(() => { + const unused = new McspAuthenticator({ apikey: APIKEY }); + }).toThrow(/Missing required parameter/); + }); + + it('should update the options and resolve with `null`', async () => { + const authenticator = new McspAuthenticator(CONFIG); + + // override the created token manager with the mocked one + authenticator.tokenManager = mockedTokenManager; + + const options = { headers: { 'X-Some-Header': 'user-supplied header' } }; + const result = await authenticator.authenticate(options); + + expect(result).toBeUndefined(); + expect(options.headers.Authorization).toBe(`Bearer ${fakeToken}`); + expect(getTokenSpy).toHaveBeenCalled(); + + // verify that the original options are kept intact + expect(options.headers['X-Some-Header']).toBe('user-supplied header'); + }); + + it('should re-set disableSslVerification using the setter', () => { + const authenticator = new McspAuthenticator(CONFIG); + expect(authenticator.disableSslVerification).toBe(CONFIG.disableSslVerification); + + const newValue = false; + authenticator.setDisableSslVerification(newValue); + expect(authenticator.disableSslVerification).toBe(newValue); + + // also, verify that the underlying token manager has been updated + expect(authenticator.tokenManager.disableSslVerification).toBe(newValue); + }); + + it('should re-set the headers using the setter', () => { + const authenticator = new McspAuthenticator(CONFIG); + expect(authenticator.headers).toEqual(CONFIG.headers); + + const newHeader = { 'X-New-Header': 'updated-header' }; + authenticator.setHeaders(newHeader); + expect(authenticator.headers).toEqual(newHeader); + + // also, verify that the underlying token manager has been updated + expect(authenticator.tokenManager.headers).toEqual(newHeader); + }); +}); diff --git a/test/unit/mcsp-token-manager.test.js b/test/unit/mcsp-token-manager.test.js new file mode 100644 index 000000000..ab036864b --- /dev/null +++ b/test/unit/mcsp-token-manager.test.js @@ -0,0 +1,104 @@ +/* eslint-disable no-alert, no-console */ + +/** + * (C) Copyright IBM Corp. 2023. + * + * 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 { 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 APIKEY = 'my-api-key'; +const URL = 'https://mcsp.ibm.com'; +const FULL_URL = `${URL}/siusermgr/api/1.0/apikeys/token`; + +describe('MCSP Token Manager', () => { + describe('constructor', () => { + it('should initialize base variables', () => { + const instance = new McspTokenManager({ + url: URL, + apikey: APIKEY, + }); + + expect(instance.tokenName).toBe('token'); + expect(instance.apikey).toBe(APIKEY); + expect(instance.disableSslVerification).toBe(false); + }); + + it('should set disableSslVerification', () => { + const instance = new McspTokenManager({ + apikey: APIKEY, + url: URL, + disableSslVerification: true, + }); + + expect(instance.disableSslVerification).toBe(true); + }); + + it('should throw an error if `url` is not given', () => { + expect( + () => + new McspTokenManager({ + apikey: APIKEY, + }) + ).toThrow(/Missing required parameter/); + }); + + it('should throw an error if `apikey` is not given', () => { + expect( + () => + new McspTokenManager({ + url: URL, + }) + ).toThrow(/Missing required parameter/); + }); + }); + + describe('requestToken', () => { + afterEach(() => { + mockSendRequest.mockClear(); + }); + + it('should call sendRequest with all request options', () => { + const instance = new McspTokenManager({ + url: URL, + apikey: APIKEY, + }); + + 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); + }); + }); +});