diff --git a/CHANGELOG.md b/CHANGELOG.md index a174a4027dd4..a86899cb5dc2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) ### 📈 Features/Enhancements +* [MD] Support legacy client for data source ([#2204](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2204)) + ### 🐛 Bug Fixes * [Vis Builder] Fixes auto bounds for timeseries bar chart visualization ([2401](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2401)) * [Vis Builder] Fixes visualization shift when editing agg ([2401](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/2401)) diff --git a/src/core/server/http/router/router.ts b/src/core/server/http/router/router.ts index 752706879d0e..047639372454 100644 --- a/src/core/server/http/router/router.ts +++ b/src/core/server/http/router/router.ts @@ -306,7 +306,6 @@ export class Router implements IRouter { opensearchDashboardsResponseFactory.badRequest({ body: e.message }) ); } - // TODO: add legacy data source client config error handling return hapiResponseAdapter.toInternalError(); } diff --git a/src/plugins/data/server/index_patterns/fetcher/index_patterns_fetcher.ts b/src/plugins/data/server/index_patterns/fetcher/index_patterns_fetcher.ts index a53e06cdea0a..9144e505133c 100644 --- a/src/plugins/data/server/index_patterns/fetcher/index_patterns_fetcher.ts +++ b/src/plugins/data/server/index_patterns/fetcher/index_patterns_fetcher.ts @@ -28,7 +28,7 @@ * under the License. */ -import { LegacyAPICaller, OpenSearchClient } from 'opensearch-dashboards/server'; +import { LegacyAPICaller } from 'opensearch-dashboards/server'; import { getFieldCapabilities, resolveTimePattern, createNoMatchingIndicesError } from './lib'; @@ -48,9 +48,9 @@ interface FieldSubType { } export class IndexPatternsFetcher { - private _callDataCluster: LegacyAPICaller | OpenSearchClient; + private _callDataCluster: LegacyAPICaller; - constructor(callDataCluster: LegacyAPICaller | OpenSearchClient) { + constructor(callDataCluster: LegacyAPICaller) { this._callDataCluster = callDataCluster; } diff --git a/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_capabilities.ts b/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_capabilities.ts index 57d59ce5a486..0aee1222a361 100644 --- a/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_capabilities.ts +++ b/src/plugins/data/server/index_patterns/fetcher/lib/field_capabilities/field_capabilities.ts @@ -30,7 +30,7 @@ import { defaults, keyBy, sortBy } from 'lodash'; -import { LegacyAPICaller, OpenSearchClient } from 'opensearch-dashboards/server'; +import { LegacyAPICaller } from 'opensearch-dashboards/server'; import { callFieldCapsApi } from '../opensearch_api'; import { FieldCapsResponse, readFieldCapsResponse } from './field_caps_response'; import { mergeOverrides } from './overrides'; @@ -47,7 +47,7 @@ import { FieldDescriptor } from '../../index_patterns_fetcher'; * @return {Promise>} */ export async function getFieldCapabilities( - callCluster: LegacyAPICaller | OpenSearchClient, + callCluster: LegacyAPICaller, indices: string | string[] = [], metaFields: string[] = [], fieldCapsOptions?: { allowNoIndices: boolean } diff --git a/src/plugins/data/server/index_patterns/fetcher/lib/opensearch_api.ts b/src/plugins/data/server/index_patterns/fetcher/lib/opensearch_api.ts index 5fcbb01ce07a..0a33e51b1018 100644 --- a/src/plugins/data/server/index_patterns/fetcher/lib/opensearch_api.ts +++ b/src/plugins/data/server/index_patterns/fetcher/lib/opensearch_api.ts @@ -57,23 +57,10 @@ export interface IndexAliasResponse { * @return {Promise} */ export async function callIndexAliasApi( - callCluster: LegacyAPICaller | OpenSearchClient, + callCluster: LegacyAPICaller, indices: string[] | string ): Promise { try { - // This approach of identify between OpenSearchClient vs LegacyAPICaller - // will be deprecated after support data client with legacy client - // https://github.com/opensearch-project/OpenSearch-Dashboards/issues/2133 - if ('transport' in callCluster) { - return ( - await callCluster.indices.getAlias({ - index: indices, - ignore_unavailable: true, - allow_no_indices: true, - }) - ).body as IndicesAliasResponse; - } - return (await callCluster('indices.getAlias', { index: indices, ignoreUnavailable: true, @@ -97,25 +84,11 @@ export async function callIndexAliasApi( * @return {Promise} */ export async function callFieldCapsApi( - callCluster: LegacyAPICaller | OpenSearchClient, + callCluster: LegacyAPICaller, indices: string[] | string, fieldCapsOptions: { allowNoIndices: boolean } = { allowNoIndices: false } ) { try { - // This approach of identify between OpenSearchClient vs LegacyAPICaller - // will be deprecated after support data client with legacy client - // https://github.com/opensearch-project/OpenSearch-Dashboards/issues/2133 - if ('transport' in callCluster) { - return ( - await callCluster.fieldCaps({ - index: indices, - fields: '*', - ignore_unavailable: true, - allow_no_indices: fieldCapsOptions.allowNoIndices, - }) - ).body as FieldCapsResponse; - } - return (await callCluster('fieldCaps', { index: indices, fields: '*', diff --git a/src/plugins/data/server/index_patterns/fetcher/lib/resolve_time_pattern.ts b/src/plugins/data/server/index_patterns/fetcher/lib/resolve_time_pattern.ts index 7b19ff78646f..c1ef0074a1d6 100644 --- a/src/plugins/data/server/index_patterns/fetcher/lib/resolve_time_pattern.ts +++ b/src/plugins/data/server/index_patterns/fetcher/lib/resolve_time_pattern.ts @@ -47,10 +47,7 @@ import { callIndexAliasApi, IndicesAliasResponse } from './opensearch_api'; * and the indices that actually match the time * pattern (matches); */ -export async function resolveTimePattern( - callCluster: LegacyAPICaller | OpenSearchClient, - timePattern: string -) { +export async function resolveTimePattern(callCluster: LegacyAPICaller, timePattern: string) { const aliases = await callIndexAliasApi(callCluster, timePatternToWildcard(timePattern)); const allIndexDetails = chain(aliases) diff --git a/src/plugins/data/server/index_patterns/routes.ts b/src/plugins/data/server/index_patterns/routes.ts index f41c15ccb381..3adc1970dd81 100644 --- a/src/plugins/data/server/index_patterns/routes.ts +++ b/src/plugins/data/server/index_patterns/routes.ts @@ -29,7 +29,11 @@ */ import { schema } from '@osd/config-schema'; -import { HttpServiceSetup, RequestHandlerContext } from 'opensearch-dashboards/server'; +import { + HttpServiceSetup, + LegacyAPICaller, + RequestHandlerContext, +} from 'opensearch-dashboards/server'; import { IndexPatternsFetcher } from './fetcher'; export function registerRoutes(http: HttpServiceSetup) { @@ -151,11 +155,12 @@ export function registerRoutes(http: HttpServiceSetup) { ); } -const decideClient = async (context: RequestHandlerContext, request: any) => { +const decideClient = async ( + context: RequestHandlerContext, + request: any +): Promise => { const dataSourceId = request.query.data_source; - if (dataSourceId) { - return await context.dataSource.opensearch.getClient(dataSourceId); - } - - return context.core.opensearch.legacy.client.callAsCurrentUser; + return dataSourceId + ? (context.dataSource.opensearch.legacy.getClient(dataSourceId).callAPI as LegacyAPICaller) + : context.core.opensearch.legacy.client.callAsCurrentUser; }; diff --git a/src/plugins/data_source/server/client/client_pool.ts b/src/plugins/data_source/server/client/client_pool.ts index be1957bc769e..fe5458d8f6ca 100644 --- a/src/plugins/data_source/server/client/client_pool.ts +++ b/src/plugins/data_source/server/client/client_pool.ts @@ -4,31 +4,32 @@ */ import { Client } from '@opensearch-project/opensearch'; +import { Client as LegacyClient } from 'elasticsearch'; import LRUCache from 'lru-cache'; import { Logger } from 'src/core/server'; import { DataSourcePluginConfigType } from '../../config'; export interface OpenSearchClientPoolSetup { - getClientFromPool: (id: string) => Client | undefined; - addClientToPool: (endpoint: string, client: Client) => void; + getClientFromPool: (id: string) => Client | LegacyClient | undefined; + addClientToPool: (endpoint: string, client: Client | LegacyClient) => void; } /** - * OpenSearch client pool. + * OpenSearch client pool for data source. * * This client pool uses an LRU cache to manage OpenSearch Js client objects. * It reuse TPC connections for each OpenSearch endpoint. */ export class OpenSearchClientPool { // LRU cache - // key: data source endpoint url - // value: OpenSearch client object - private cache?: LRUCache; + // key: data source endpoint + // value: OpenSearch client object | Legacy client object + private cache?: LRUCache; private isClosed = false; constructor(private logger: Logger) {} - public async setup(config: DataSourcePluginConfigType) { + public async setup(config: DataSourcePluginConfigType): Promise { const logger = this.logger; const { size } = config.clientPool; @@ -53,7 +54,7 @@ export class OpenSearchClientPool { return this.cache!.get(endpoint); }; - const addClientToPool = (endpoint: string, client: Client) => { + const addClientToPool = (endpoint: string, client: Client | LegacyClient) => { this.cache!.set(endpoint, client); }; diff --git a/src/plugins/data_source/server/client/configure_client.test.ts b/src/plugins/data_source/server/client/configure_client.test.ts index f8f8f2cb5802..696c7ce2daf6 100644 --- a/src/plugins/data_source/server/client/configure_client.test.ts +++ b/src/plugins/data_source/server/client/configure_client.test.ts @@ -15,6 +15,7 @@ import { ClientOptions } from '@opensearch-project/opensearch'; // eslint-disable-next-line @osd/eslint/no-restricted-paths import { opensearchClientMock } from '../../../../core/server/opensearch/client/mocks'; import { CryptographyClient } from '../cryptography'; +import { DataSourceClientParams } from '../types'; const DATA_SOURCE_ID = 'a54b76ec86771ee865a0f74a305dfff8'; const cryptoClient = new CryptographyClient('test', 'test', new Array(32).fill(0)); @@ -28,6 +29,7 @@ describe('configureClient', () => { let clientOptions: ClientOptions; let dataSourceAttr: DataSourceAttributes; let dsClient: ReturnType; + let dataSourceClientParams: DataSourceClientParams; beforeEach(() => { dsClient = opensearchClientMock.createInternalClient(); @@ -70,9 +72,13 @@ describe('configureClient', () => { references: [], }); - ClientMock.mockImplementation(() => { - return dsClient; - }); + dataSourceClientParams = { + dataSourceId: DATA_SOURCE_ID, + savedObjects: savedObjectsMock, + cryptographyClient: cryptoClient, + }; + + ClientMock.mockImplementation(() => dsClient); }); afterEach(() => { @@ -94,14 +100,7 @@ describe('configureClient', () => { parseClientOptionsMock.mockReturnValue(clientOptions); - const client = await configureClient( - DATA_SOURCE_ID, - savedObjectsMock, - cryptoClient, - clientPoolSetup, - config, - logger - ); + const client = await configureClient(dataSourceClientParams, clientPoolSetup, config, logger); expect(parseClientOptionsMock).toHaveBeenCalled(); expect(ClientMock).toHaveBeenCalledTimes(1); @@ -113,14 +112,7 @@ describe('configureClient', () => { test('configure client with auth.type == username_password, will first call decrypt()', async () => { const spy = jest.spyOn(cryptoClient, 'decodeAndDecrypt').mockResolvedValue('password'); - const client = await configureClient( - DATA_SOURCE_ID, - savedObjectsMock, - cryptoClient, - clientPoolSetup, - config, - logger - ); + const client = await configureClient(dataSourceClientParams, clientPoolSetup, config, logger); expect(ClientMock).toHaveBeenCalledTimes(1); expect(savedObjectsMock.get).toHaveBeenCalledTimes(1); diff --git a/src/plugins/data_source/server/client/configure_client.ts b/src/plugins/data_source/server/client/configure_client.ts index 8cfa9769de7a..febac85070ea 100644 --- a/src/plugins/data_source/server/client/configure_client.ts +++ b/src/plugins/data_source/server/client/configure_client.ts @@ -14,13 +14,12 @@ import { import { DataSourcePluginConfigType } from '../../config'; import { CryptographyClient } from '../cryptography'; import { DataSourceConfigError } from '../lib/error'; +import { DataSourceClientParams } from '../types'; import { parseClientOptions } from './client_config'; import { OpenSearchClientPoolSetup } from './client_pool'; export const configureClient = async ( - dataSourceId: string, - savedObjects: SavedObjectsClientContract, - cryptographyClient: CryptographyClient, + { dataSourceId, savedObjects, cryptographyClient }: DataSourceClientParams, openSearchClientPoolSetup: OpenSearchClientPoolSetup, config: DataSourcePluginConfigType, logger: Logger @@ -68,7 +67,7 @@ export const getCredential = async ( * * @param rootClient root client for the connection with given data source endpoint. * @param dataSource data source saved object - * @param cryptographyClient cryptography client for password encryption / decrpytion + * @param cryptographyClient cryptography client for password encryption / decryption * @returns child client. */ const getQueryClient = async ( @@ -76,12 +75,18 @@ const getQueryClient = async ( dataSource: SavedObject, cryptographyClient: CryptographyClient ): Promise => { - if (AuthType.NoAuth === dataSource.attributes.auth.type) { - return rootClient.child(); - } else { - const credential = await getCredential(dataSource, cryptographyClient); + const authType = dataSource.attributes.auth.type; + + switch (authType) { + case AuthType.NoAuth: + return rootClient.child(); + + case AuthType.UsernamePasswordType: + const credential = await getCredential(dataSource, cryptographyClient); + return getBasicAuthClient(rootClient, credential); - return getBasicAuthClient(rootClient, credential); + default: + throw Error(`${authType} is not a supported auth type for data source`); } }; @@ -101,7 +106,7 @@ const getRootClient = ( const endpoint = dataSourceAttr.endpoint; const cachedClient = getClientFromPool(endpoint); if (cachedClient) { - return cachedClient; + return cachedClient as Client; } else { const clientOptions = parseClientOptions(config, endpoint); diff --git a/src/plugins/data_source/server/client/index.ts b/src/plugins/data_source/server/client/index.ts index 8adc96115b91..f27848965077 100644 --- a/src/plugins/data_source/server/client/index.ts +++ b/src/plugins/data_source/server/client/index.ts @@ -3,5 +3,5 @@ * SPDX-License-Identifier: Apache-2.0 */ -export { OpenSearchClientPool } from './client_pool'; -export { configureClient } from './configure_client'; +export { OpenSearchClientPool, OpenSearchClientPoolSetup } from './client_pool'; +export { configureClient, getDataSource, getCredential } from './configure_client'; diff --git a/src/plugins/data_source/server/data_source_service.test.ts b/src/plugins/data_source/server/data_source_service.test.ts index 53dfb6f273eb..690188562360 100644 --- a/src/plugins/data_source/server/data_source_service.test.ts +++ b/src/plugins/data_source/server/data_source_service.test.ts @@ -33,6 +33,7 @@ describe('Data Source Service', () => { test('exposes proper contract', async () => { const setup = await service.setup(config); expect(setup).toHaveProperty('getDataSourceClient'); + expect(setup).toHaveProperty('getDataSourceLegacyClient'); }); }); }); diff --git a/src/plugins/data_source/server/data_source_service.ts b/src/plugins/data_source/server/data_source_service.ts index 73f61b87cb38..8466bb7e914b 100644 --- a/src/plugins/data_source/server/data_source_service.ts +++ b/src/plugins/data_source/server/data_source_service.ts @@ -5,47 +5,66 @@ import { Auditor, + LegacyCallAPIOptions, Logger, OpenSearchClient, - SavedObjectsClientContract, } from '../../../../src/core/server'; import { DataSourcePluginConfigType } from '../config'; import { configureClient, OpenSearchClientPool } from './client'; -import { CryptographyClient } from './cryptography'; +import { configureLegacyClient } from './legacy'; +import { DataSourceClientParams } from './types'; export interface DataSourceServiceSetup { - getDataSourceClient: ( - dataSourceId: string, - // this saved objects client is used to fetch data source on behalf of users, caller should pass scoped saved objects client - savedObjects: SavedObjectsClientContract, - cryptographyClient: CryptographyClient - ) => Promise; + getDataSourceClient: (params: DataSourceClientParams) => Promise; + + getDataSourceLegacyClient: ( + params: DataSourceClientParams + ) => { + callAPI: ( + endpoint: string, + clientParams?: Record, + options?: LegacyCallAPIOptions + ) => Promise; + }; } export class DataSourceService { private readonly openSearchClientPool: OpenSearchClientPool; + private readonly legacyClientPool: OpenSearchClientPool; + private readonly legacyLogger: Logger; constructor(private logger: Logger) { + this.legacyLogger = logger.get('legacy'); this.openSearchClientPool = new OpenSearchClientPool(logger); + this.legacyClientPool = new OpenSearchClientPool(this.legacyLogger); } - async setup(config: DataSourcePluginConfigType) { - const openSearchClientPoolSetup = await this.openSearchClientPool.setup(config); + async setup(config: DataSourcePluginConfigType): Promise { + const opensearchClientPoolSetup = await this.openSearchClientPool.setup(config); + const legacyClientPoolSetup = await this.legacyClientPool.setup(config); - const getDataSourceClient = ( - dataSourceId: string, - savedObjects: SavedObjectsClientContract, - cryptographyClient: CryptographyClient + const getDataSourceClient = async ( + params: DataSourceClientParams ): Promise => { - return configureClient( - dataSourceId, - savedObjects, - cryptographyClient, - openSearchClientPoolSetup, - config, - this.logger - ); + return configureClient(params, opensearchClientPoolSetup, config, this.logger); + }; + + const getDataSourceLegacyClient = (params: DataSourceClientParams) => { + return { + callAPI: ( + endpoint: string, + clientParams?: Record, + options?: LegacyCallAPIOptions + ) => + configureLegacyClient( + params, + { endpoint, clientParams, options }, + legacyClientPoolSetup, + config, + this.legacyLogger + ), + }; }; - return { getDataSourceClient }; + return { getDataSourceClient, getDataSourceLegacyClient }; } start() {} diff --git a/src/plugins/data_source/server/legacy/client_config.test.ts b/src/plugins/data_source/server/legacy/client_config.test.ts new file mode 100644 index 000000000000..1b21eede35bc --- /dev/null +++ b/src/plugins/data_source/server/legacy/client_config.test.ts @@ -0,0 +1,28 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ +import { DataSourcePluginConfigType } from '../../config'; +import { parseClientOptions } from './client_config'; + +const TEST_DATA_SOURCE_ENDPOINT = 'http://datasource.com'; + +const config = { + enabled: true, + clientPool: { + size: 5, + }, +} as DataSourcePluginConfigType; + +describe('parseClientOptions', () => { + test('include the ssl client configs as defaults', () => { + expect(parseClientOptions(config, TEST_DATA_SOURCE_ENDPOINT)).toEqual( + expect.objectContaining({ + host: TEST_DATA_SOURCE_ENDPOINT, + ssl: { + rejectUnauthorized: true, + }, + }) + ); + }); +}); diff --git a/src/plugins/data_source/server/legacy/client_config.ts b/src/plugins/data_source/server/legacy/client_config.ts new file mode 100644 index 000000000000..d9b1cc704e3a --- /dev/null +++ b/src/plugins/data_source/server/legacy/client_config.ts @@ -0,0 +1,28 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { ConfigOptions } from 'elasticsearch'; +import { DataSourcePluginConfigType } from '../../config'; + +/** + * Parse the client options from given data source config and endpoint + * + * @param config The config to generate the client options from. + * @param endpoint endpoint url of data source + */ +export function parseClientOptions( + // TODO: will use client configs, that comes from a merge result of user config and default legacy client config, + config: DataSourcePluginConfigType, + endpoint: string +): ConfigOptions { + const configOptions: ConfigOptions = { + host: endpoint, + ssl: { + rejectUnauthorized: true, + }, + }; + + return configOptions; +} diff --git a/src/plugins/data_source/server/legacy/configure_legacy_client.test.mocks.ts b/src/plugins/data_source/server/legacy/configure_legacy_client.test.mocks.ts new file mode 100644 index 000000000000..e6c1b3363896 --- /dev/null +++ b/src/plugins/data_source/server/legacy/configure_legacy_client.test.mocks.ts @@ -0,0 +1,18 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export const ClientMock = jest.fn(); +jest.doMock('elasticsearch', () => { + const actual = jest.requireActual('elasticsearch'); + return { + ...actual, + Client: ClientMock, + }; +}); + +export const parseClientOptionsMock = jest.fn(); +jest.doMock('./client_config', () => ({ + parseClientOptions: parseClientOptionsMock, +})); diff --git a/src/plugins/data_source/server/legacy/configure_legacy_client.test.ts b/src/plugins/data_source/server/legacy/configure_legacy_client.test.ts new file mode 100644 index 000000000000..752487741f10 --- /dev/null +++ b/src/plugins/data_source/server/legacy/configure_legacy_client.test.ts @@ -0,0 +1,173 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { SavedObjectsClientContract } from '../../../../core/server'; +import { loggingSystemMock, savedObjectsClientMock } from '../../../../core/server/mocks'; +import { DATA_SOURCE_SAVED_OBJECT_TYPE } from '../../common'; +import { AuthType, DataSourceAttributes } from '../../common/data_sources'; +import { DataSourcePluginConfigType } from '../../config'; +import { CryptographyClient } from '../cryptography'; +import { DataSourceClientParams, LegacyClientCallAPIParams } from '../types'; +import { OpenSearchClientPoolSetup } from '../client'; +import { ConfigOptions } from 'elasticsearch'; +import { ClientMock, parseClientOptionsMock } from './configure_legacy_client.test.mocks'; +import { configureLegacyClient } from './configure_legacy_client'; + +const DATA_SOURCE_ID = 'a54b76ec86771ee865a0f74a305dfff8'; +const cryptographyClient = new CryptographyClient('test', 'test', new Array(32).fill(0)); + +// TODO: improve UT +describe('configureLegacyClient', () => { + let logger: ReturnType; + let config: DataSourcePluginConfigType; + let savedObjectsMock: jest.Mocked; + let clientPoolSetup: OpenSearchClientPoolSetup; + let configOptions: ConfigOptions; + let dataSourceAttr: DataSourceAttributes; + + let mockOpenSearchClientInstance: { + close: jest.Mock; + ping: jest.Mock; + }; + let dataSourceClientParams: DataSourceClientParams; + let callApiParams: LegacyClientCallAPIParams; + let decodeAndDecryptSpy: jest.SpyInstance, [encrypted: string]>; + + const mockResponse = { data: 'ping' }; + + beforeEach(() => { + mockOpenSearchClientInstance = { + close: jest.fn(), + ping: jest.fn(), + }; + logger = loggingSystemMock.createLogger(); + savedObjectsMock = savedObjectsClientMock.create(); + config = { + enabled: true, + clientPool: { + size: 5, + }, + } as DataSourcePluginConfigType; + + configOptions = { + host: 'http://localhost', + ssl: { + rejectUnauthorized: true, + }, + } as ConfigOptions; + + dataSourceAttr = { + title: 'title', + endpoint: 'http://localhost', + auth: { + type: AuthType.UsernamePasswordType, + credentials: { + username: 'username', + password: 'password', + }, + }, + } as DataSourceAttributes; + + clientPoolSetup = { + getClientFromPool: jest.fn(), + addClientToPool: jest.fn(), + }; + + callApiParams = { + endpoint: 'ping', + }; + + savedObjectsMock.get.mockResolvedValueOnce({ + id: DATA_SOURCE_ID, + type: DATA_SOURCE_SAVED_OBJECT_TYPE, + attributes: dataSourceAttr, + references: [], + }); + + dataSourceClientParams = { + dataSourceId: DATA_SOURCE_ID, + savedObjects: savedObjectsMock, + cryptographyClient, + }; + + ClientMock.mockImplementation(() => mockOpenSearchClientInstance); + + mockOpenSearchClientInstance.ping.mockImplementation(function mockCall(this: any) { + return Promise.resolve({ + context: this, + response: mockResponse, + }); + }); + + decodeAndDecryptSpy = jest + .spyOn(cryptographyClient, 'decodeAndDecrypt') + .mockResolvedValue('password'); + }); + + afterEach(() => { + ClientMock.mockReset(); + jest.resetAllMocks(); + }); + + test('configure client with auth.type == no_auth, will call new Client() to create client', async () => { + savedObjectsMock.get.mockReset().mockResolvedValueOnce({ + id: DATA_SOURCE_ID, + type: DATA_SOURCE_SAVED_OBJECT_TYPE, + attributes: { + ...dataSourceAttr, + auth: { + type: AuthType.NoAuth, + }, + }, + references: [], + }); + + parseClientOptionsMock.mockReturnValue(configOptions); + + await configureLegacyClient( + dataSourceClientParams, + callApiParams, + clientPoolSetup, + config, + logger + ); + + expect(parseClientOptionsMock).toHaveBeenCalled(); + expect(ClientMock).toHaveBeenCalledTimes(1); + expect(ClientMock).toHaveBeenCalledWith(configOptions); + expect(savedObjectsMock.get).toHaveBeenCalledTimes(1); + }); + + test('configure client with auth.type == no_auth, will first call decrypt()', async () => { + const mockResult = await configureLegacyClient( + dataSourceClientParams, + callApiParams, + clientPoolSetup, + config, + logger + ); + + expect(ClientMock).toHaveBeenCalledTimes(1); + expect(savedObjectsMock.get).toHaveBeenCalledTimes(1); + expect(decodeAndDecryptSpy).toHaveBeenCalledTimes(1); + expect(mockResult).toBeDefined(); + }); + + test('correctly called with endpoint and params', async () => { + const mockParams = { param: 'ping' }; + const mockResult = await configureLegacyClient( + dataSourceClientParams, + { ...callApiParams, clientParams: mockParams }, + clientPoolSetup, + config, + logger + ); + + expect(mockResult.response).toBe(mockResponse); + expect(mockResult.context).toBe(mockOpenSearchClientInstance); + expect(mockOpenSearchClientInstance.ping).toHaveBeenCalledTimes(1); + expect(mockOpenSearchClientInstance.ping).toHaveBeenLastCalledWith(mockParams); + }); +}); diff --git a/src/plugins/data_source/server/legacy/configure_legacy_client.ts b/src/plugins/data_source/server/legacy/configure_legacy_client.ts new file mode 100644 index 000000000000..0f2a63287e14 --- /dev/null +++ b/src/plugins/data_source/server/legacy/configure_legacy_client.ts @@ -0,0 +1,167 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { Client } from 'elasticsearch'; +import { get } from 'lodash'; +import { + Headers, + LegacyAPICaller, + LegacyCallAPIOptions, + LegacyOpenSearchErrorHelpers, + Logger, + SavedObject, +} from '../../../../../src/core/server'; +import { + AuthType, + DataSourceAttributes, + UsernamePasswordTypedContent, +} from '../../common/data_sources'; +import { DataSourcePluginConfigType } from '../../config'; +import { CryptographyClient } from '../cryptography'; +import { DataSourceClientParams, LegacyClientCallAPIParams } from '../types'; +import { OpenSearchClientPoolSetup, getCredential, getDataSource } from '../client'; +import { parseClientOptions } from './client_config'; +import { DataSourceConfigError } from '../lib/error'; + +export const configureLegacyClient = async ( + { dataSourceId, savedObjects, cryptographyClient }: DataSourceClientParams, + callApiParams: LegacyClientCallAPIParams, + openSearchClientPoolSetup: OpenSearchClientPoolSetup, + config: DataSourcePluginConfigType, + logger: Logger +) => { + try { + const dataSource = await getDataSource(dataSourceId, savedObjects); + const rootClient = getRootClient(dataSource.attributes, config, openSearchClientPoolSetup); + + return await getQueryClient(rootClient, dataSource, cryptographyClient, callApiParams); + } catch (error: any) { + logger.error(`Fail to get data source client for dataSourceId: [${dataSourceId}]`); + logger.error(error); + // Re-throw as DataSourceConfigError + throw new DataSourceConfigError('Fail to get data source client: ', error); + } +}; + +/** + * With given auth info, wrap the rootClient and return + * + * @param rootClient root client for the connection with given data source endpoint. + * @param dataSource data source saved object + * @param cryptographyClient cryptography client for password encryption / decryption + * @returns child client. + */ +const getQueryClient = async ( + rootClient: Client, + dataSource: SavedObject, + cryptographyClient: CryptographyClient, + { endpoint, clientParams, options }: LegacyClientCallAPIParams +) => { + const authType = dataSource.attributes.auth.type; + + switch (authType) { + case AuthType.NoAuth: + return await (callAPI.bind(null, rootClient) as LegacyAPICaller)( + endpoint, + clientParams, + options + ); + case AuthType.UsernamePasswordType: + const credential = await getCredential(dataSource, cryptographyClient); + return getBasicAuthClient(rootClient, { endpoint, clientParams, options }, credential); + + default: + throw Error(`${authType} is not a supported auth type for data source`); + } +}; + +/** + * Gets a root client object of the OpenSearch endpoint. + * Will attempt to get from cache, if cache miss, create a new one and load into cache. + * + * @param dataSourceAttr data source saved objects attributes. + * @param config data source config + * @returns Legacy client for the given data source endpoint. + */ +const getRootClient = ( + dataSourceAttr: DataSourceAttributes, + config: DataSourcePluginConfigType, + { getClientFromPool, addClientToPool }: OpenSearchClientPoolSetup +): Client => { + const endpoint = dataSourceAttr.endpoint; + const cachedClient = getClientFromPool(endpoint); + if (cachedClient) { + return cachedClient as Client; + } else { + const configOptions = parseClientOptions(config, endpoint); + const client = new Client(configOptions); + addClientToPool(endpoint, client); + + return client; + } +}; + +/** + * Calls the OpenSearch API endpoint with the specified parameters. + * @param client Raw legacy JS client instance to use. + * @param endpoint Name of the API endpoint to call. + * @param clientParams Parameters that will be directly passed to the + * legacy JS client. + * @param options Options that affect the way we call the API and process the result. + * make wrap401Errors default to false, because we don't want login pop-up from browser + */ +const callAPI = async ( + client: Client, + endpoint: string, + clientParams: Record = {}, + options: LegacyCallAPIOptions = { wrap401Errors: false } +) => { + const clientPath = endpoint.split('.'); + const api: any = get(client, clientPath); + if (!api) { + throw new Error(`called with an invalid endpoint: ${endpoint}`); + } + + const apiContext = clientPath.length === 1 ? client : get(client, clientPath.slice(0, -1)); + try { + return await new Promise((resolve, reject) => { + const request = api.call(apiContext, clientParams); + if (options.signal) { + options.signal.addEventListener('abort', () => { + request.abort(); + reject(new Error('Request was aborted')); + }); + } + return request.then(resolve, reject); + }); + } catch (err) { + if (!options.wrap401Errors || err.statusCode !== 401) { + throw err; + } + + throw LegacyOpenSearchErrorHelpers.decorateNotAuthorizedError(err); + } +}; + +/** + * Get a legacy client that configured with basic auth + * + * @param rootClient Raw legacy client instance to use. + * @param endpoint - String descriptor of the endpoint e.g. `cluster.getSettings` or `ping`. + * @param clientParams - A dictionary of parameters that will be passed directly to the legacy JS client. + * @param options - Options that affect the way we call the API and process the result. + */ +const getBasicAuthClient = async ( + rootClient: Client, + { endpoint, clientParams = {}, options }: LegacyClientCallAPIParams, + { username, password }: UsernamePasswordTypedContent +) => { + const headers: Headers = { + authorization: 'Basic ' + Buffer.from(`${username}:${password}`).toString('base64'), + }; + clientParams.headers = Object.assign({}, clientParams.headers, headers); + + return await (callAPI.bind(null, rootClient) as LegacyAPICaller)(endpoint, clientParams, options); +}; diff --git a/src/plugins/data_source/server/legacy/index.ts b/src/plugins/data_source/server/legacy/index.ts new file mode 100644 index 000000000000..b7c8d7a26ad7 --- /dev/null +++ b/src/plugins/data_source/server/legacy/index.ts @@ -0,0 +1,6 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export { configureLegacyClient } from './configure_legacy_client'; diff --git a/src/plugins/data_source/server/lib/error.ts b/src/plugins/data_source/server/lib/error.ts index e921a8d8043f..6667b41992f3 100644 --- a/src/plugins/data_source/server/lib/error.ts +++ b/src/plugins/data_source/server/lib/error.ts @@ -14,8 +14,8 @@ export class DataSourceConfigError extends OsdError { ? error.output.payload.message : error.message; super(messagePrefix + messageContent); - // Cast all 5xx error returned by saveObjectClient to 500, 400 for both savedObject client - // 4xx errors, and other errors + // Cast all 5xx error returned by saveObjectClient to 500. + // Cast both savedObject client 4xx errors, and other errors to 400 this.statusCode = SavedObjectsErrorHelpers.isOpenSearchUnavailableError(error) ? 500 : 400; } } diff --git a/src/plugins/data_source/server/plugin.ts b/src/plugins/data_source/server/plugin.ts index b0b6718ae11b..c166fb736768 100644 --- a/src/plugins/data_source/server/plugin.ts +++ b/src/plugins/data_source/server/plugin.ts @@ -131,11 +131,20 @@ export class DataSourcePlugin implements Plugin auditTrail.asScoped(req)); this.logAuditMessage(auditor, dataSourceId, req); - return dataSourceService.getDataSourceClient( + return dataSourceService.getDataSourceClient({ dataSourceId, - context.core.savedObjects.client, - cryptographyClient - ); + savedObjects: context.core.savedObjects.client, + cryptographyClient, + }); + }, + legacy: { + getClient: (dataSourceId: string) => { + return dataSourceService.getDataSourceLegacyClient({ + dataSourceId, + savedObjects: context.core.savedObjects.client, + cryptographyClient, + }); + }, }, }, }; diff --git a/src/plugins/data_source/server/types.ts b/src/plugins/data_source/server/types.ts index bad309b4b871..2f20363b4b2d 100644 --- a/src/plugins/data_source/server/types.ts +++ b/src/plugins/data_source/server/types.ts @@ -3,11 +3,40 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { OpenSearchClient } from 'src/core/server'; +import { + LegacyCallAPIOptions, + OpenSearchClient, + SavedObjectsClientContract, +} from 'src/core/server'; +import { CryptographyClient } from './cryptography'; + +export interface LegacyClientCallAPIParams { + endpoint: string; + clientParams?: Record; + options?: LegacyCallAPIOptions; +} + +export interface DataSourceClientParams { + dataSourceId: string; + // this saved objects client is used to fetch data source on behalf of users, caller should pass scoped saved objects client + savedObjects: SavedObjectsClientContract; + cryptographyClient: CryptographyClient; +} export interface DataSourcePluginRequestContext { opensearch: { getClient: (dataSourceId: string) => Promise; + legacy: { + getClient: ( + dataSourceId: string + ) => { + callAPI: ( + endpoint: string, + clientParams: Record, + options?: LegacyCallAPIOptions + ) => Promise; + }; + }; }; } declare module 'src/core/server' { diff --git a/src/plugins/index_pattern_management/server/routes/resolve_index.ts b/src/plugins/index_pattern_management/server/routes/resolve_index.ts index 510eeb367da8..baf19ca5b7d0 100644 --- a/src/plugins/index_pattern_management/server/routes/resolve_index.ts +++ b/src/plugins/index_pattern_management/server/routes/resolve_index.ts @@ -29,7 +29,7 @@ */ import { schema } from '@osd/config-schema'; -import { IRouter } from 'src/core/server'; +import { IRouter, LegacyAPICaller } from 'src/core/server'; export function registerResolveIndexRoute(router: IRouter): void { router.get( @@ -59,25 +59,16 @@ export function registerResolveIndexRoute(router: IRouter): void { : null; const dataSourceId = req.query.data_source; - if (dataSourceId) { - const result = await ( - await context.dataSource.opensearch.getClient(dataSourceId) - ).indices.resolveIndex({ - name: encodeURIComponent(req.params.query), - expand_wildcards: req.query.expand_wildcards, - }); - return res.ok({ body: result.body }); - } + const caller = dataSourceId + ? context.dataSource.opensearch.legacy.getClient(dataSourceId).callAPI + : context.core.opensearch.legacy.client.callAsCurrentUser; - const result = await context.core.opensearch.legacy.client.callAsCurrentUser( - 'transport.request', - { - method: 'GET', - path: `/_resolve/index/${encodeURIComponent(req.params.query)}${ - queryString ? '?' + new URLSearchParams(queryString).toString() : '' - }`, - } - ); + const result = await caller('transport.request', { + method: 'GET', + path: `/_resolve/index/${encodeURIComponent(req.params.query)}${ + queryString ? '?' + new URLSearchParams(queryString).toString() : '' + }`, + }); return res.ok({ body: result }); } );