diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c342806c41a..d942cd4713b0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,6 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - [CVE-2022-24999] Resolve qs from 6.5.3 to 6.11.0 ([#3450](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/3450)) ### 📈 Features/Enhancements -- [Multiple DataSource] Add support for SigV4 authentication ([#3058](https://github.com/opensearch-project/OpenSearch-Dashboards/issues/3058)) ### 🐛 Bug Fixes diff --git a/package.json b/package.json index 714496e25943..7a64fe3cdbea 100644 --- a/package.json +++ b/package.json @@ -133,7 +133,7 @@ "@hapi/podium": "^4.1.3", "@hapi/vision": "^6.1.0", "@hapi/wreck": "^17.1.0", - "@opensearch-project/opensearch": "^2.1.0", + "@opensearch-project/opensearch": "^1.1.0", "@osd/ace": "1.0.0", "@osd/analytics": "1.0.0", "@osd/apm-config-loader": "1.0.0", @@ -166,7 +166,6 @@ "dns-sync": "^0.2.1", "elastic-apm-node": "^3.7.0", "elasticsearch": "^16.7.0", - "http-aws-es": "6.0.0", "execa": "^4.0.2", "expiry-js": "0.1.7", "fast-deep-equal": "^3.1.1", @@ -335,7 +334,6 @@ "@types/zen-observable": "^0.8.0", "@typescript-eslint/eslint-plugin": "^3.10.0", "@typescript-eslint/parser": "^3.10.0", - "@types/http-aws-es": "6.0.2", "angular-aria": "^1.8.0", "angular-mocks": "^1.8.2", "angular-recursion": "^1.0.5", diff --git a/packages/osd-opensearch/package.json b/packages/osd-opensearch/package.json index 4a9aafa875a5..88a6733757a6 100644 --- a/packages/osd-opensearch/package.json +++ b/packages/osd-opensearch/package.json @@ -12,7 +12,7 @@ "osd:watch": "node scripts/build --watch" }, "dependencies": { - "@opensearch-project/opensearch": "^2.1.0", + "@opensearch-project/opensearch": "^1.1.0", "@osd/dev-utils": "1.0.0", "abort-controller": "^3.0.0", "chalk": "^4.1.0", diff --git a/src/dev/jest/config.js b/src/dev/jest/config.js index c7e31c37f7e2..8dd5240f69de 100644 --- a/src/dev/jest/config.js +++ b/src/dev/jest/config.js @@ -52,8 +52,6 @@ export default { moduleNameMapper: { '@elastic/eui$': '/node_modules/@elastic/eui/test-env', '@elastic/eui/lib/(.*)?': '/node_modules/@elastic/eui/test-env/$1', - '@opensearch-project/opensearch/aws': - '/node_modules/@opensearch-project/opensearch/lib/aws', '^src/plugins/(.*)': '/src/plugins/$1', '^test_utils/(.*)': '/src/test_utils/public/$1', '^fixtures/(.*)': '/src/fixtures/$1', diff --git a/src/plugins/data_source/common/data_sources/types.ts b/src/plugins/data_source/common/data_sources/types.ts index 366e5a0f3f55..afcf3d662fed 100644 --- a/src/plugins/data_source/common/data_sources/types.ts +++ b/src/plugins/data_source/common/data_sources/types.ts @@ -11,20 +11,8 @@ export interface DataSourceAttributes extends SavedObjectAttributes { endpoint: string; auth: { type: AuthType; - credentials: UsernamePasswordTypedContent | SigV4Content | undefined; + credentials: UsernamePasswordTypedContent | undefined; }; - lastUpdatedTime?: string; -} - -/** - * Multiple datasource supports authenticating as IAM user, it doesn't support IAM role. - * Because IAM role session requires temporary security credentials through assuming role, - * which makes no sense to store the credentials. - */ -export interface SigV4Content extends SavedObjectAttributes { - accessKey: string; - secretKey: string; - region: string; } export interface UsernamePasswordTypedContent extends SavedObjectAttributes { @@ -35,5 +23,4 @@ export interface UsernamePasswordTypedContent extends SavedObjectAttributes { export enum AuthType { NoAuth = 'no_auth', UsernamePasswordType = 'username_password', - SigV4 = 'sigv4', } diff --git a/src/plugins/data_source/server/client/client_pool.ts b/src/plugins/data_source/server/client/client_pool.ts index 288682ef2538..f492d6bc2898 100644 --- a/src/plugins/data_source/server/client/client_pool.ts +++ b/src/plugins/data_source/server/client/client_pool.ts @@ -7,12 +7,11 @@ import { Client } from '@opensearch-project/opensearch'; import { Client as LegacyClient } from 'elasticsearch'; import LRUCache from 'lru-cache'; import { Logger } from 'src/core/server'; -import { AuthType } from '../../common/data_sources'; import { DataSourcePluginConfigType } from '../../config'; export interface OpenSearchClientPoolSetup { - getClientFromPool: (endpoint: string, authType: AuthType) => Client | LegacyClient | undefined; - addClientToPool: (endpoint: string, authType: AuthType, client: Client | LegacyClient) => void; + getClientFromPool: (id: string) => Client | LegacyClient | undefined; + addClientToPool: (endpoint: string, client: Client | LegacyClient) => void; } /** @@ -22,14 +21,10 @@ export interface OpenSearchClientPoolSetup { * It reuse TPC connections for each OpenSearch endpoint. */ export class OpenSearchClientPool { - // LRU cache of client + // LRU cache // key: data source endpoint - // value: OpenSearch client | Legacy client - private clientCache?: LRUCache; - // LRU cache of aws clients - // key: endpoint + dataSourceId + lastUpdatedTime together to support update case. - // value: OpenSearch client | Legacy client - private awsClientCache?: LRUCache; + // value: OpenSearch client object | Legacy client object + private cache?: LRUCache; private isClosed = false; constructor(private logger: Logger) {} @@ -37,13 +32,12 @@ export class OpenSearchClientPool { public setup(config: DataSourcePluginConfigType): OpenSearchClientPoolSetup { const logger = this.logger; const { size } = config.clientPool; - const MAX_AGE = 15 * 60 * 1000; // by default, TCP connection times out in 15 minutes - this.clientCache = new LRUCache({ + this.cache = new LRUCache({ max: size, - maxAge: MAX_AGE, + maxAge: 15 * 60 * 1000, // by default, TCP connection times out in 15 minutes - async dispose(key, client) { + async dispose(endpoint, client) { try { await client.close(); } catch (error: any) { @@ -56,34 +50,12 @@ export class OpenSearchClientPool { }); this.logger.info(`Created data source client pool of size ${size}`); - // aws client specific pool - this.awsClientCache = new LRUCache({ - max: size, - maxAge: MAX_AGE, - - async dispose(key, client) { - try { - await client.close(); - } catch (error: any) { - logger.warn( - `Error closing OpenSearch client when removing from aws client pool: ${error.message}` - ); - } - }, - }); - this.logger.info(`Created data source aws client pool of size ${size}`); - - const getClientFromPool = (key: string, authType: AuthType) => { - const selectedCache = authType === AuthType.SigV4 ? this.awsClientCache : this.clientCache; - - return selectedCache!.get(key); + const getClientFromPool = (endpoint: string) => { + return this.cache!.get(endpoint); }; - const addClientToPool = (key: string, authType: string, client: Client | LegacyClient) => { - const selectedCache = authType === AuthType.SigV4 ? this.awsClientCache : this.clientCache; - if (!selectedCache?.has(key)) { - return selectedCache!.set(key, client); - } + const addClientToPool = (endpoint: string, client: Client | LegacyClient) => { + this.cache!.set(endpoint, client); }; return { @@ -99,15 +71,7 @@ export class OpenSearchClientPool { if (this.isClosed) { return; } - - try { - await Promise.all([ - ...this.clientCache!.values().map((client) => client.close()), - ...this.awsClientCache!.values().map((client) => client.close()), - ]); - this.isClosed = true; - } catch (error) { - this.logger.error(`Error closing clients in pool. ${error}`); - } + await Promise.all(this.cache!.values().map((client) => client.close())); + this.isClosed = true; } } 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 e00b62f04882..fa4044163610 100644 --- a/src/plugins/data_source/server/client/configure_client.test.ts +++ b/src/plugins/data_source/server/client/configure_client.test.ts @@ -137,7 +137,7 @@ describe('configureClient', () => { configureClient(dataSourceClientParams, clientPoolSetup, config, logger) ).rejects.toThrowError(); - expect(ClientMock).not.toHaveBeenCalled(); + expect(ClientMock).toHaveBeenCalledTimes(1); expect(savedObjectsMock.get).toHaveBeenCalledTimes(1); expect(decodeAndDecryptSpy).toHaveBeenCalledTimes(1); }); @@ -152,7 +152,7 @@ describe('configureClient', () => { configureClient(dataSourceClientParams, clientPoolSetup, config, logger) ).rejects.toThrowError(); - expect(ClientMock).not.toHaveBeenCalled(); + expect(ClientMock).toHaveBeenCalledTimes(1); expect(savedObjectsMock.get).toHaveBeenCalledTimes(1); expect(decodeAndDecryptSpy).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 03d5d3f21d0b..cc9bcfd9b361 100644 --- a/src/plugins/data_source/server/client/configure_client.ts +++ b/src/plugins/data_source/server/client/configure_client.ts @@ -3,15 +3,12 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Client, ClientOptions } from '@opensearch-project/opensearch'; -import { Client as LegacyClient } from 'elasticsearch'; -import { Credentials } from 'aws-sdk'; -import { AwsSigv4Signer } from '@opensearch-project/opensearch/aws'; -import { Logger } from '../../../../../src/core/server'; +import { Client } from '@opensearch-project/opensearch'; +import { Logger, SavedObject, SavedObjectsClientContract } from '../../../../../src/core/server'; +import { DATA_SOURCE_SAVED_OBJECT_TYPE } from '../../common'; import { AuthType, DataSourceAttributes, - SigV4Content, UsernamePasswordTypedContent, } from '../../common/data_sources'; import { DataSourcePluginConfigType } from '../../config'; @@ -20,13 +17,6 @@ import { createDataSourceError } from '../lib/error'; import { DataSourceClientParams } from '../types'; import { parseClientOptions } from './client_config'; import { OpenSearchClientPoolSetup } from './client_pool'; -import { - getRootClient, - getAWSCredential, - getCredential, - getDataSource, - generateCacheKey, -} from './configure_client_utils'; export const configureClient = async ( { dataSourceId, savedObjects, cryptography }: DataSourceClientParams, @@ -35,25 +25,13 @@ export const configureClient = async ( logger: Logger ): Promise => { try { - const dataSource = await getDataSource(dataSourceId!, savedObjects); - const rootClient = getRootClient( - dataSource, - openSearchClientPoolSetup.getClientFromPool, - dataSourceId - ) as Client; - - return await getQueryClient( - dataSource, - openSearchClientPoolSetup.addClientToPool, - config, - cryptography, - rootClient, - dataSourceId - ); + const { attributes: dataSource } = await getDataSource(dataSourceId!, savedObjects); + const rootClient = getRootClient(dataSource, config, openSearchClientPoolSetup); + + return await getQueryClient(rootClient, dataSource, cryptography); } catch (error: any) { - logger.error( - `Failed to get data source client for dataSourceId: [${dataSourceId}]. ${error}: ${error.stack}` - ); + logger.error(`Failed to get data source client for dataSourceId: [${dataSourceId}]`); + logger.error(error); // Re-throw as DataSourceError throw createDataSourceError(error); } @@ -61,7 +39,7 @@ export const configureClient = async ( export const configureTestClient = async ( { savedObjects, cryptography, dataSourceId }: DataSourceClientParams, - dataSourceAttr: DataSourceAttributes, + dataSource: DataSourceAttributes, openSearchClientPoolSetup: OpenSearchClientPoolSetup, config: DataSourcePluginConfigType, logger: Logger @@ -69,93 +47,122 @@ export const configureTestClient = async ( try { const { auth: { type, credentials }, - } = dataSourceAttr; + } = dataSource; let requireDecryption = false; - const rootClient = getRootClient( - dataSourceAttr, - openSearchClientPoolSetup.getClientFromPool, - dataSourceId - ) as Client; + const rootClient = getRootClient(dataSource, config, openSearchClientPoolSetup); if (type === AuthType.UsernamePasswordType && !credentials?.password && dataSourceId) { - dataSourceAttr = await getDataSource(dataSourceId, savedObjects); + const dataSourceSavedObject = await getDataSource(dataSourceId, savedObjects); + dataSource = dataSourceSavedObject.attributes; requireDecryption = true; } - return getQueryClient( - dataSourceAttr, - openSearchClientPoolSetup.addClientToPool, - config, - cryptography, - rootClient, - dataSourceId, - requireDecryption - ); + return getQueryClient(rootClient, dataSource, cryptography, requireDecryption); } catch (error: any) { - logger.error(`Failed to get test client. ${error}: ${error.stack}`); + logger.error(`Failed to get test client for dataSource: ${dataSource}`); + logger.error(error); // Re-throw as DataSourceError throw createDataSourceError(error); } }; +export const getDataSource = async ( + dataSourceId: string, + savedObjects: SavedObjectsClientContract +): Promise> => { + const dataSource = await savedObjects.get( + DATA_SOURCE_SAVED_OBJECT_TYPE, + dataSourceId + ); + + return dataSource; +}; + +export const getCredential = async ( + dataSource: DataSourceAttributes, + cryptography: CryptographyServiceSetup +): Promise => { + const { endpoint } = dataSource; + + const { username, password } = dataSource.auth.credentials!; + + const { decryptedText, encryptionContext } = await cryptography + .decodeAndDecrypt(password) + .catch((err: any) => { + // Re-throw as DataSourceError + throw createDataSourceError(err); + }); + + if (encryptionContext!.endpoint !== endpoint) { + throw new Error( + 'Data source "endpoint" contaminated. Please delete and create another data source.' + ); + } + + const credential = { + username, + password: decryptedText, + }; + + return credential; +}; + /** * Create a child client object with given auth info. * - * @param rootClient root client for the given data source. - * @param dataSourceAttr data source saved object attributes + * @param rootClient root client for the connection with given data source endpoint. + * @param dataSource data source saved object * @param cryptography cryptography service for password encryption / decryption - * @param config data source config - * @param addClientToPool function to add client to client pool - * @param dataSourceId id of data source saved Object - * @param requireDecryption boolean - * @returns Promise of query client + * @returns child client. */ const getQueryClient = async ( - dataSourceAttr: DataSourceAttributes, - addClientToPool: (endpoint: string, authType: AuthType, client: Client | LegacyClient) => void, - config: DataSourcePluginConfigType, + rootClient: Client, + dataSource: DataSourceAttributes, cryptography?: CryptographyServiceSetup, - rootClient?: Client, - dataSourceId?: string, requireDecryption: boolean = true ): Promise => { - const { - auth: { type }, - endpoint, - } = dataSourceAttr; - const clientOptions = parseClientOptions(config, endpoint); - const cacheKey = generateCacheKey(dataSourceAttr, dataSourceId); - - switch (type) { - case AuthType.NoAuth: - if (!rootClient) rootClient = new Client(clientOptions); - addClientToPool(cacheKey, type, rootClient); + const authType = dataSource.auth.type; + switch (authType) { + case AuthType.NoAuth: return rootClient.child(); case AuthType.UsernamePasswordType: const credential = requireDecryption - ? await getCredential(dataSourceAttr, cryptography!) - : (dataSourceAttr.auth.credentials as UsernamePasswordTypedContent); - - if (!rootClient) rootClient = new Client(clientOptions); - addClientToPool(cacheKey, type, rootClient); - + ? await getCredential(dataSource, cryptography!) + : (dataSource.auth.credentials as UsernamePasswordTypedContent); return getBasicAuthClient(rootClient, credential); - case AuthType.SigV4: - const awsCredential = requireDecryption - ? await getAWSCredential(dataSourceAttr, cryptography!) - : (dataSourceAttr.auth.credentials as SigV4Content); + default: + throw Error(`${authType} is not a supported auth type for data source`); + } +}; - const awsClient = rootClient ? rootClient : getAWSClient(awsCredential, clientOptions); - addClientToPool(cacheKey, type, awsClient); +/** + * 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 OpenSearch 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 clientOptions = parseClientOptions(config, endpoint); - return awsClient; + const client = new Client(clientOptions); + addClientToPool(endpoint, client); - default: - throw Error(`${type} is not a supported auth type for data source`); + return client; } }; @@ -175,21 +182,3 @@ const getBasicAuthClient = ( headers: { authorization: null }, }); }; - -const getAWSClient = (credential: SigV4Content, clientOptions: ClientOptions): Client => { - const { accessKey, secretKey, region } = credential; - - const credentialProvider = (): Promise => { - return new Promise((resolve) => { - resolve(new Credentials({ accessKeyId: accessKey, secretAccessKey: secretKey })); - }); - }; - - return new Client({ - ...AwsSigv4Signer({ - region, - getCredentials: credentialProvider, - }), - ...clientOptions, - }); -}; diff --git a/src/plugins/data_source/server/client/configure_client_utils.ts b/src/plugins/data_source/server/client/configure_client_utils.ts deleted file mode 100644 index 293f52ff43a5..000000000000 --- a/src/plugins/data_source/server/client/configure_client_utils.ts +++ /dev/null @@ -1,138 +0,0 @@ -/* - * Copyright OpenSearch Contributors - * SPDX-License-Identifier: Apache-2.0 - */ - -import { Client } from '@opensearch-project/opensearch'; -import { Client as LegacyClient } from 'elasticsearch'; -import { SavedObjectsClientContract } from '../../../../../src/core/server'; -import { DATA_SOURCE_SAVED_OBJECT_TYPE } from '../../common'; -import { - DataSourceAttributes, - UsernamePasswordTypedContent, - SigV4Content, - AuthType, -} from '../../common/data_sources'; -import { CryptographyServiceSetup } from '../cryptography_service'; -import { createDataSourceError } from '../lib/error'; - -/** - * Get the root client of datasource from - * client cache. If there's a cache miss, return undefined. - * - * @param dataSourceAttr data source saved objects attributes - * @param dataSourceId id of data source saved Object - * @param addClientToPool function to get client from client pool - * @returns cached OpenSearch client, or undefined if cache miss - */ -export const getRootClient = ( - dataSourceAttr: DataSourceAttributes, - getClientFromPool: (endpoint: string, authType: AuthType) => Client | LegacyClient | undefined, - dataSourceId?: string -): Client | LegacyClient | undefined => { - const { - auth: { type }, - } = dataSourceAttr; - const cacheKey = generateCacheKey(dataSourceAttr, dataSourceId); - const cachedClient = getClientFromPool(cacheKey, type); - - return cachedClient; -}; - -export const getDataSource = async ( - dataSourceId: string, - savedObjects: SavedObjectsClientContract -): Promise => { - const dataSourceSavedObject = await savedObjects.get( - DATA_SOURCE_SAVED_OBJECT_TYPE, - dataSourceId - ); - - const dataSourceAttr = { - ...dataSourceSavedObject.attributes, - lastUpdatedTime: dataSourceSavedObject.updated_at, - }; - - return dataSourceAttr; -}; - -export const getCredential = async ( - dataSource: DataSourceAttributes, - cryptography: CryptographyServiceSetup -): Promise => { - const { endpoint } = dataSource; - const { username, password } = dataSource.auth.credentials as UsernamePasswordTypedContent; - const { decryptedText, encryptionContext } = await cryptography.decodeAndDecrypt(password); - - if (encryptionContext!.endpoint !== endpoint) { - throw new Error( - 'Data source "endpoint" contaminated. Please delete and create another data source.' - ); - } - - const credential = { - username, - password: decryptedText, - }; - - return credential; -}; - -export const getAWSCredential = async ( - dataSource: DataSourceAttributes, - cryptography: CryptographyServiceSetup -): Promise => { - const { endpoint } = dataSource; - const { accessKey, secretKey, region } = dataSource.auth.credentials! as SigV4Content; - - const { - decryptedText: accessKeyText, - encryptionContext: accessKeyEncryptionContext, - } = await cryptography.decodeAndDecrypt(accessKey).catch((err: any) => { - // Re-throw as DataSourceError - throw createDataSourceError(err); - }); - - const { - decryptedText: secretKeyText, - encryptionContext: secretKeyEncryptionContext, - } = await cryptography.decodeAndDecrypt(secretKey).catch((err: any) => { - // Re-throw as DataSourceError - throw createDataSourceError(err); - }); - - if ( - accessKeyEncryptionContext.endpoint !== endpoint || - secretKeyEncryptionContext.endpoint !== endpoint - ) { - throw new Error( - 'Data source "endpoint" contaminated. Please delete and create another data source.' - ); - } - - const credential = { - region, - accessKey: accessKeyText, - secretKey: secretKeyText, - }; - - return credential; -}; - -export const generateCacheKey = (dataSourceAttr: DataSourceAttributes, dataSourceId?: string) => { - const CACHE_KEY_DELIMITER = ','; - const { - auth: { type }, - endpoint, - lastUpdatedTime, - } = dataSourceAttr; - // opensearch-js client doesn't support spawning child with aws sigv4 connection class, - // we are storing/getting the actual client instead of rootClient in/from aws client pool, - // by a key of ",," - const key = - type === AuthType.SigV4 - ? endpoint + CACHE_KEY_DELIMITER + dataSourceId + CACHE_KEY_DELIMITER + lastUpdatedTime - : endpoint; - - return key; -}; diff --git a/src/plugins/data_source/server/client/index.ts b/src/plugins/data_source/server/client/index.ts index 9b6824dfa1d5..faf5dabe4417 100644 --- a/src/plugins/data_source/server/client/index.ts +++ b/src/plugins/data_source/server/client/index.ts @@ -4,3 +4,10 @@ */ export { OpenSearchClientPool, OpenSearchClientPoolSetup } from './client_pool'; +export { + configureClient, + getDataSource, + getCredential, + getRootClient, + getValidationClient, +} from './configure_client'; diff --git a/src/plugins/data_source/server/data_source_service.ts b/src/plugins/data_source/server/data_source_service.ts index e816a25a729f..798fce739216 100644 --- a/src/plugins/data_source/server/data_source_service.ts +++ b/src/plugins/data_source/server/data_source_service.ts @@ -5,11 +5,11 @@ import { LegacyCallAPIOptions, Logger, OpenSearchClient } from '../../../../src/core/server'; import { DataSourcePluginConfigType } from '../config'; -import { OpenSearchClientPool } from './client'; +import { configureClient, OpenSearchClientPool } from './client'; import { configureLegacyClient } from './legacy'; import { DataSourceClientParams } from './types'; import { DataSourceAttributes } from '../common/data_sources'; -import { configureTestClient, configureClient } from './client/configure_client'; +import { configureTestClient } from './client/configure_client'; export interface DataSourceServiceSetup { getDataSourceClient: (params: DataSourceClientParams) => Promise; @@ -49,7 +49,7 @@ export class DataSourceService { return configureClient(params, opensearchClientPoolSetup, config, this.logger); }; - const getTestingClient = async ( + const getTestingClient = ( params: DataSourceClientParams, dataSource: DataSourceAttributes ): Promise => { 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 index c047da70b285..bfdf0ce585f0 100644 --- a/src/plugins/data_source/server/legacy/configure_legacy_client.test.ts +++ b/src/plugins/data_source/server/legacy/configure_legacy_client.test.ts @@ -168,7 +168,7 @@ describe('configureLegacyClient', () => { configureLegacyClient(dataSourceClientParams, callApiParams, clientPoolSetup, config, logger) ).rejects.toThrowError(); - expect(ClientMock).not.toHaveBeenCalled(); + expect(ClientMock).toHaveBeenCalledTimes(1); expect(savedObjectsMock.get).toHaveBeenCalledTimes(1); expect(decodeAndDecryptSpy).toHaveBeenCalledTimes(1); }); @@ -183,7 +183,7 @@ describe('configureLegacyClient', () => { configureLegacyClient(dataSourceClientParams, callApiParams, clientPoolSetup, config, logger) ).rejects.toThrowError(); - expect(ClientMock).not.toHaveBeenCalled(); + expect(ClientMock).toHaveBeenCalledTimes(1); expect(savedObjectsMock.get).toHaveBeenCalledTimes(1); expect(decodeAndDecryptSpy).toHaveBeenCalledTimes(1); }); diff --git a/src/plugins/data_source/server/legacy/configure_legacy_client.ts b/src/plugins/data_source/server/legacy/configure_legacy_client.ts index 3a9b65634a28..137d5b506fb3 100644 --- a/src/plugins/data_source/server/legacy/configure_legacy_client.ts +++ b/src/plugins/data_source/server/legacy/configure_legacy_client.ts @@ -3,38 +3,27 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { Client } from '@opensearch-project/opensearch'; -import { Client as LegacyClient, ConfigOptions } from 'elasticsearch'; -import { Credentials } from 'aws-sdk'; +import { Client } from 'elasticsearch'; import { get } from 'lodash'; -import HttpAmazonESConnector from 'http-aws-es'; -import { Config } from 'aws-sdk'; import { Headers, LegacyAPICaller, LegacyCallAPIOptions, LegacyOpenSearchErrorHelpers, Logger, + SavedObject, } from '../../../../../src/core/server'; import { AuthType, DataSourceAttributes, - SigV4Content, UsernamePasswordTypedContent, } from '../../common/data_sources'; import { DataSourcePluginConfigType } from '../../config'; import { CryptographyServiceSetup } from '../cryptography_service'; import { DataSourceClientParams, LegacyClientCallAPIParams } from '../types'; -import { OpenSearchClientPoolSetup } from '../client'; +import { OpenSearchClientPoolSetup, getCredential, getDataSource } from '../client'; import { parseClientOptions } from './client_config'; import { createDataSourceError, DataSourceError } from '../lib/error'; -import { - getRootClient, - getAWSCredential, - getCredential, - getDataSource, - generateCacheKey, -} from '../client/configure_client_utils'; export const configureLegacyClient = async ( { dataSourceId, savedObjects, cryptography }: DataSourceClientParams, @@ -44,26 +33,13 @@ export const configureLegacyClient = async ( logger: Logger ) => { try { - const dataSourceAttr = await getDataSource(dataSourceId!, savedObjects); - const rootClient = getRootClient( - dataSourceAttr, - openSearchClientPoolSetup.getClientFromPool, - dataSourceId - ) as LegacyClient; + const dataSource = await getDataSource(dataSourceId!, savedObjects); + const rootClient = getRootClient(dataSource.attributes, config, openSearchClientPoolSetup); - return await getQueryClient( - dataSourceAttr, - cryptography, - callApiParams, - openSearchClientPoolSetup.addClientToPool, - config, - rootClient, - dataSourceId - ); + return await getQueryClient(rootClient, dataSource.attributes, cryptography, callApiParams); } catch (error: any) { - logger.error( - `Failed to get data source client for dataSourceId: [${dataSourceId}]. ${error}: ${error.stack}` - ); + logger.error(`Failed to get data source client for dataSourceId: [${dataSourceId}]`); + logger.error(error); // Re-throw as DataSourceError throw createDataSourceError(error); } @@ -73,62 +49,57 @@ export const configureLegacyClient = async ( * With given auth info, wrap the rootClient and return * * @param rootClient root client for the connection with given data source endpoint. - * @param dataSourceAttr data source saved object attributes + * @param dataSource data source saved object * @param cryptography cryptography service for password encryption / decryption - * @param config data source config - * @param addClientToPool function to add client to client pool - * @param dataSourceId id of data source saved Object * @returns child client. */ const getQueryClient = async ( - dataSourceAttr: DataSourceAttributes, + rootClient: Client, + dataSource: DataSourceAttributes, cryptography: CryptographyServiceSetup, - { endpoint, clientParams, options }: LegacyClientCallAPIParams, - addClientToPool: (endpoint: string, authType: AuthType, client: Client | LegacyClient) => void, - config: DataSourcePluginConfigType, - rootClient?: LegacyClient, - dataSourceId?: string + { endpoint, clientParams, options }: LegacyClientCallAPIParams ) => { - const { - auth: { type }, - endpoint: nodeUrl, - } = dataSourceAttr; - const clientOptions = parseClientOptions(config, nodeUrl); - const cacheKey = generateCacheKey(dataSourceAttr, dataSourceId); + const authType = dataSource.auth.type; - switch (type) { + switch (authType) { case AuthType.NoAuth: - if (!rootClient) rootClient = new LegacyClient(clientOptions); - addClientToPool(cacheKey, type, rootClient); - return await (callAPI.bind(null, rootClient) as LegacyAPICaller)( endpoint, clientParams, options ); - case AuthType.UsernamePasswordType: - const credential = await getCredential(dataSourceAttr, cryptography); - - if (!rootClient) rootClient = new LegacyClient(clientOptions); - addClientToPool(cacheKey, type, rootClient); - + const credential = await getCredential(dataSource, cryptography); return getBasicAuthClient(rootClient, { endpoint, clientParams, options }, credential); - case AuthType.SigV4: - const awsCredential = await getAWSCredential(dataSourceAttr, cryptography); - - const awsClient = rootClient ? rootClient : getAWSClient(awsCredential, clientOptions); - addClientToPool(cacheKey, type, awsClient); - - return await (callAPI.bind(null, awsClient) as LegacyAPICaller)( - endpoint, - clientParams, - options - ); - default: - throw Error(`${type} is not a supported auth type for data source`); + 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; } }; @@ -142,7 +113,7 @@ const getQueryClient = async ( * make wrap401Errors default to false, because we don't want login pop-up from browser */ const callAPI = async ( - client: LegacyClient, + client: Client, endpoint: string, clientParams: Record = {}, options: LegacyCallAPIOptions = { wrap401Errors: false } @@ -182,7 +153,7 @@ const callAPI = async ( * @param options - Options that affect the way we call the API and process the result. */ const getBasicAuthClient = async ( - rootClient: LegacyClient, + rootClient: Client, { endpoint, clientParams = {}, options }: LegacyClientCallAPIParams, { username, password }: UsernamePasswordTypedContent ) => { @@ -193,16 +164,3 @@ const getBasicAuthClient = async ( return await (callAPI.bind(null, rootClient) as LegacyAPICaller)(endpoint, clientParams, options); }; - -const getAWSClient = (credential: SigV4Content, clientOptions: ConfigOptions): LegacyClient => { - const { accessKey, secretKey, region } = credential; - const client = new LegacyClient({ - connectionClass: HttpAmazonESConnector, - awsConfig: new Config({ - region, - credentials: new Credentials({ accessKeyId: accessKey, secretAccessKey: secretKey }), - }), - ...clientOptions, - }); - return client; -}; diff --git a/src/plugins/data_source/server/saved_objects/data_source_saved_objects_client_wrapper.ts b/src/plugins/data_source/server/saved_objects/data_source_saved_objects_client_wrapper.ts index 6b79248d1a94..525923f4c577 100644 --- a/src/plugins/data_source/server/saved_objects/data_source_saved_objects_client_wrapper.ts +++ b/src/plugins/data_source/server/saved_objects/data_source_saved_objects_client_wrapper.ts @@ -17,12 +17,7 @@ import { } from 'opensearch-dashboards/server'; import { Logger, SavedObjectsErrorHelpers } from '../../../../../src/core/server'; import { DATA_SOURCE_SAVED_OBJECT_TYPE } from '../../common'; -import { - AuthType, - DataSourceAttributes, - SigV4Content, - UsernamePasswordTypedContent, -} from '../../common/data_sources'; +import { AuthType } from '../../common/data_sources'; import { EncryptionContext, CryptographyServiceSetup } from '../cryptography_service'; /** @@ -34,7 +29,7 @@ export class DataSourceSavedObjectsClientWrapper { /** * Describes the factory used to create instances of Saved Objects Client Wrappers - * for data source specific operations such as credentials encryption + * for data source spcific operations such as credntials encryption */ public wrapperFactory: SavedObjectsClientWrapperFactory = (wrapperOptions) => { const createWithCredentialsEncryption = async ( @@ -164,14 +159,13 @@ export class DataSourceSavedObjectsClientWrapper { }; case AuthType.UsernamePasswordType: // Signing the data source with endpoint - return { - ...attributes, - auth: await this.encryptBasicAuthCredential(auth, { endpoint }), + const encryptionContext = { + endpoint, }; - case AuthType.SigV4: + return { ...attributes, - auth: await this.encryptSigV4Credential(auth, { endpoint }), + auth: await this.encryptCredentials(auth, encryptionContext), }; default: throw SavedObjectsErrorHelpers.createBadRequestError(`Invalid auth type: '${auth.type}'`); @@ -197,8 +191,6 @@ export class DataSourceSavedObjectsClientWrapper { } const { type, credentials } = auth; - const existingDataSourceAttr = await this.getDataSourceAttributes(wrapperOptions, id, options); - const encryptionContext = await this.getEncryptionContext(existingDataSourceAttr); switch (type) { case AuthType.NoAuth: @@ -212,33 +204,18 @@ export class DataSourceSavedObjectsClientWrapper { }; case AuthType.UsernamePasswordType: if (credentials?.password) { - this.validateEncryptionContext(encryptionContext, existingDataSourceAttr); - return { - ...attributes, - auth: await this.encryptBasicAuthCredential(auth, encryptionContext), - }; - } else { - return attributes; - } - case AuthType.SigV4: - this.validateEncryptionContext(encryptionContext, existingDataSourceAttr); - if (credentials?.accessKey && credentials?.secretKey) { + // Fetch and validate existing signature + const encryptionContext = await this.validateEncryptionContext( + wrapperOptions, + id, + options + ); + return { ...attributes, - auth: await this.encryptSigV4Credential(auth, encryptionContext), + auth: await this.encryptCredentials(auth, encryptionContext), }; } else { - if (credentials?.accessKey) { - throw SavedObjectsErrorHelpers.createBadRequestError( - `Failed to update existing data source with auth type ${type}: "credentials.secretKey" missing.` - ); - } - - if (credentials?.secretKey) { - throw SavedObjectsErrorHelpers.createBadRequestError( - `Failed to update existing data source with auth type ${type}: "credentials.accessKey" missing.` - ); - } return attributes; } default: @@ -282,7 +259,7 @@ export class DataSourceSavedObjectsClientWrapper { ); } - const { username, password } = credentials as UsernamePasswordTypedContent; + const { username, password } = credentials; if (!username) { throw SavedObjectsErrorHelpers.createBadRequestError( @@ -295,45 +272,36 @@ export class DataSourceSavedObjectsClientWrapper { '"auth.credentials.password" attribute is required' ); } - break; - case AuthType.SigV4: - if (!credentials) { - throw SavedObjectsErrorHelpers.createBadRequestError( - '"auth.credentials" attribute is required' - ); - } - const { accessKey, secretKey, region } = credentials as SigV4Content; - - if (!accessKey) { - throw SavedObjectsErrorHelpers.createBadRequestError( - '"auth.credentials.accessKey" attribute is required' - ); - } - - if (!secretKey) { - throw SavedObjectsErrorHelpers.createBadRequestError( - '"auth.credentials.secretKey" attribute is required' - ); - } - - if (!region) { - throw SavedObjectsErrorHelpers.createBadRequestError( - '"auth.credentials.region" attribute is required' - ); - } break; default: throw SavedObjectsErrorHelpers.createBadRequestError(`Invalid auth type: '${type}'`); } } - private async getEncryptionContext(attributes: DataSourceAttributes) { - let encryptionContext: EncryptionContext; + private async validateEncryptionContext( + wrapperOptions: SavedObjectsClientWrapperOptions, + id: string, + options: SavedObjectsUpdateOptions = {} + ) { + let attributes; + + try { + // Fetch existing data source by id + const savedObject = await wrapperOptions.client.get(DATA_SOURCE_SAVED_OBJECT_TYPE, id, { + namespace: options.namespace, + }); + attributes = savedObject.attributes; + } catch (err: any) { + const errMsg = `Failed to fetch existing data source for dataSourceId [${id}]`; + this.logger.error(errMsg); + this.logger.error(err); + throw SavedObjectsErrorHelpers.decorateBadRequestError(err, errMsg); + } if (!attributes) { throw SavedObjectsErrorHelpers.createBadRequestError( - 'Failed to update existing data source: "attributes" missing. Please delete and create another data source.' + 'Update failed due to deprecated data source: "attributes" missing. Please delete and create another data source.' ); } @@ -341,109 +309,65 @@ export class DataSourceSavedObjectsClientWrapper { if (!endpoint) { throw SavedObjectsErrorHelpers.createBadRequestError( - 'Failed to update existing data source: "endpoint" missing. Please delete and create another data source.' + 'Update failed due to deprecated data source: "endpoint" missing. Please delete and create another data source.' ); } if (!auth) { throw SavedObjectsErrorHelpers.createBadRequestError( - 'Failed to update existing data source: "auth" missing. Please delete and create another data source.' + 'Update failed due to deprecated data source: "auth" missing. Please delete and create another data source.' ); } switch (auth.type) { case AuthType.NoAuth: - // Signing the data source with existing endpoint - encryptionContext = { endpoint }; - break; + // Signing the data source with exsiting endpoint + return { + endpoint, + }; case AuthType.UsernamePasswordType: const { credentials } = auth; if (!credentials) { throw SavedObjectsErrorHelpers.createBadRequestError( - 'Failed to update existing data source: "credentials" missing. Please delete and create another data source.' + 'Update failed due to deprecated data source: "credentials" missing. Please delete and create another data source.' ); } - const { username, password } = credentials as UsernamePasswordTypedContent; + const { username, password } = credentials; if (!username) { throw SavedObjectsErrorHelpers.createBadRequestError( - 'Failed to update existing data source: "auth.credentials.username" missing. Please delete and create another data source.' + 'Update failed due to deprecated data source: "auth.credentials.username" missing. Please delete and create another data source.' ); } if (!password) { throw SavedObjectsErrorHelpers.createBadRequestError( - 'Failed to update existing data source: "auth.credentials.password" missing. Please delete and create another data source.' + 'Update failed due to deprecated data source: "auth.credentials.username" missing. Please delete and create another data source.' ); } - encryptionContext = await this.getEncryptionContextFromCipher(password); - break; - case AuthType.SigV4: - const { accessKey, secretKey } = auth.credentials as SigV4Content; - const accessKeyEncryptionContext = await this.getEncryptionContextFromCipher(accessKey); - const secretKeyEncryptionContext = await this.getEncryptionContextFromCipher(secretKey); - if (accessKeyEncryptionContext.endpoint !== secretKeyEncryptionContext.endpoint) { + const { encryptionContext } = await this.cryptography + .decodeAndDecrypt(password) + .catch((err: any) => { + const errMsg = `Failed to update existing data source for dataSourceId [${id}]: unable to decrypt "auth.credentials.password"`; + this.logger.error(errMsg); + this.logger.error(err); + throw SavedObjectsErrorHelpers.decorateBadRequestError(err, errMsg); + }); + + if (encryptionContext.endpoint !== endpoint) { throw SavedObjectsErrorHelpers.createBadRequestError( - 'Failed to update existing data source: encryption contexts for "auth.credentials.accessKey" and "auth.credentials.secretKey" must be same. Please delete and create another data source.' + 'Update failed due to deprecated data source: "endpoint" contaminated. Please delete and create another data source.' ); } - encryptionContext = accessKeyEncryptionContext; - break; + return encryptionContext; default: - throw SavedObjectsErrorHelpers.createBadRequestError(`Invalid auth type: '${auth.type}'`); - } - - return encryptionContext; - } - - private async getDataSourceAttributes( - wrapperOptions: SavedObjectsClientWrapperOptions, - id: string, - options: SavedObjectsUpdateOptions = {} - ): Promise { - try { - // Fetch existing data source by id - const savedObject = await wrapperOptions.client.get(DATA_SOURCE_SAVED_OBJECT_TYPE, id, { - namespace: options.namespace, - }); - return savedObject.attributes as DataSourceAttributes; - } catch (err: any) { - const errMsg = `Failed to fetch existing data source for dataSourceId [${id}]`; - this.logger.error(`${errMsg}: ${err} ${err.stack}`); - throw SavedObjectsErrorHelpers.decorateBadRequestError(err, errMsg); - } - } - - private validateEncryptionContext = ( - encryptionContext: EncryptionContext, - dataSource: DataSourceAttributes - ) => { - // validate encryption context - if (encryptionContext.endpoint !== dataSource.endpoint) { - throw SavedObjectsErrorHelpers.createBadRequestError( - 'Failed to update existing data source: "endpoint" contaminated. Please delete and create another data source.' - ); + throw SavedObjectsErrorHelpers.createBadRequestError(`Invalid auth type: '${type}'`); } - }; - - private async getEncryptionContextFromCipher(cipher: string) { - const { encryptionContext } = await this.cryptography - .decodeAndDecrypt(cipher) - .catch((err: any) => { - const errMsg = `Failed to update existing data source: unable to decrypt auth content`; - this.logger.error(`${errMsg}: ${err} ${err.stack}`); - throw SavedObjectsErrorHelpers.decorateBadRequestError(err, errMsg); - }); - - return encryptionContext; } - private async encryptBasicAuthCredential( - auth: T, - encryptionContext: EncryptionContext - ) { + private async encryptCredentials(auth: T, encryptionContext: EncryptionContext) { const { credentials: { username, password }, } = auth; @@ -456,19 +380,4 @@ export class DataSourceSavedObjectsClientWrapper { }, }; } - - private async encryptSigV4Credential(auth: T, encryptionContext: EncryptionContext) { - const { - credentials: { accessKey, secretKey, region }, - } = auth; - - return { - ...auth, - credentials: { - region, - accessKey: await this.cryptography.encryptAndEncode(accessKey, encryptionContext), - secretKey: await this.cryptography.encryptAndEncode(secretKey, encryptionContext), - }, - }; - } } diff --git a/src/plugins/data_source_management/public/components/create_data_source_wizard/components/create_form/create_data_source_form.tsx b/src/plugins/data_source_management/public/components/create_data_source_wizard/components/create_form/create_data_source_form.tsx index 6c4cb6a97588..2de6fa497315 100644 --- a/src/plugins/data_source_management/public/components/create_data_source_wizard/components/create_form/create_data_source_form.tsx +++ b/src/plugins/data_source_management/public/components/create_data_source_wizard/components/create_form/create_data_source_form.tsx @@ -25,7 +25,6 @@ import { DataSourceAttributes, DataSourceManagementContextValue, UsernamePasswordTypedContent, - SigV4Content, } from '../../../../types'; import { Header } from '../header'; import { context as contextType } from '../../../../../../opensearch_dashboards_react/public'; @@ -51,7 +50,7 @@ export interface CreateDataSourceState { endpoint: string; auth: { type: AuthType; - credentials: UsernamePasswordTypedContent | SigV4Content; + credentials: UsernamePasswordTypedContent; }; } @@ -121,7 +120,20 @@ export class CreateDataSourceForm extends React.Component< }; onChangeAuthType = (value: string) => { - this.setState({ auth: { ...this.state.auth, type: value as AuthType } }); + const valueToSave = + value === AuthType.UsernamePasswordType ? AuthType.UsernamePasswordType : AuthType.NoAuth; + + const formErrorsByField = { + ...this.state.formErrorsByField, + createCredential: { ...this.state.formErrorsByField.createCredential }, + }; + if (valueToSave === AuthType.NoAuth) { + formErrorsByField.createCredential = { + username: [], + password: [], + }; + } + this.setState({ auth: { ...this.state.auth, type: valueToSave }, formErrorsByField }); }; onChangeUsername = (e: { target: { value: any } }) => { @@ -168,72 +180,6 @@ export class CreateDataSourceForm extends React.Component< }); }; - onChangeRegion = (e: { target: { value: any } }) => { - this.setState({ - auth: { - ...this.state.auth, - credentials: { ...this.state.auth.credentials, region: e.target.value }, - }, - }); - }; - - validateRegion = () => { - const isValid = !!this.state.auth.credentials.region?.trim().length; - this.setState({ - formErrorsByField: { - ...this.state.formErrorsByField, - awsCredential: { - ...this.state.formErrorsByField.awsCredential, - region: isValid ? [] : [''], - }, - }, - }); - }; - - onChangeAccessKey = (e: { target: { value: any } }) => { - this.setState({ - auth: { - ...this.state.auth, - credentials: { ...this.state.auth.credentials, accessKey: e.target.value }, - }, - }); - }; - - validateAccessKey = () => { - const isValid = !!this.state.auth.credentials.accessKey; - this.setState({ - formErrorsByField: { - ...this.state.formErrorsByField, - awsCredential: { - ...this.state.formErrorsByField.awsCredential, - accessKey: isValid ? [] : [''], - }, - }, - }); - }; - - onChangeSecretKey = (e: { target: { value: any } }) => { - this.setState({ - auth: { - ...this.state.auth, - credentials: { ...this.state.auth.credentials, secretKey: e.target.value }, - }, - }); - }; - - validateSecretKey = () => { - const isValid = !!this.state.auth.credentials.secretKey; - this.setState({ - formErrorsByField: { - ...this.state.formErrorsByField, - awsCredential: { - ...this.state.formErrorsByField.awsCredential, - secretKey: isValid ? [] : [''], - }, - }, - }); - }; - onClickCreateNewDataSource = () => { if (this.isFormValid()) { const formValues: DataSourceAttributes = this.getFormValues(); @@ -255,26 +201,11 @@ export class CreateDataSourceForm extends React.Component< }; getFormValues = (): DataSourceAttributes => { - let credentials = this.state.auth.credentials; - if (this.state.auth.type === AuthType.UsernamePasswordType) { - credentials = { - username: this.state.auth.credentials.username, - password: this.state.auth.credentials.password, - } as UsernamePasswordTypedContent; - } - if (this.state.auth.type === AuthType.SigV4) { - credentials = { - region: this.state.auth.credentials.region, - accessKey: this.state.auth.credentials.accessKey, - secretKey: this.state.auth.credentials.secretKey, - } as SigV4Content; - } - return { title: this.state.title, description: this.state.description, endpoint: this.state.endpoint, - auth: { ...this.state.auth, credentials }, + auth: { ...this.state.auth, credentials: { ...this.state.auth.credentials } }, }; }; @@ -316,133 +247,55 @@ export class CreateDataSourceForm extends React.Component< }; /* Render create new credentials*/ - renderCreateNewCredentialsForm = (type: AuthType) => { - switch (type) { - case AuthType.UsernamePasswordType: - return ( - <> - - - - - - - - ); - case AuthType.SigV4: - return ( - <> - - - - - - - - - - - ); - - default: - break; - } + renderCreateNewCredentialsForm = () => { + return ( + <> + + + + + + + + ); }; renderContent = () => { @@ -566,11 +419,7 @@ export class CreateDataSourceForm extends React.Component< {/* Create New credentials */} {this.state.auth.type === AuthType.UsernamePasswordType - ? this.renderCreateNewCredentialsForm(this.state.auth.type) - : null} - - {this.state.auth.type === AuthType.SigV4 - ? this.renderCreateNewCredentialsForm(this.state.auth.type) + ? this.renderCreateNewCredentialsForm() : null} @@ -581,8 +430,7 @@ export class CreateDataSourceForm extends React.Component< diff --git a/src/plugins/data_source_management/public/components/edit_data_source/components/edit_form/edit_data_source_form.tsx b/src/plugins/data_source_management/public/components/edit_data_source/components/edit_form/edit_data_source_form.tsx index fafca5b724b4..561a651edee2 100644 --- a/src/plugins/data_source_management/public/components/edit_data_source/components/edit_form/edit_data_source_form.tsx +++ b/src/plugins/data_source_management/public/components/edit_data_source/components/edit_form/edit_data_source_form.tsx @@ -29,7 +29,6 @@ import { credentialSourceOptions, DataSourceAttributes, DataSourceManagementContextValue, - SigV4Content, ToastMessageItem, UsernamePasswordTypedContent, } from '../../../../types'; @@ -41,7 +40,6 @@ import { performDataSourceFormValidation, } from '../../../validation'; import { UpdatePasswordModal } from '../update_password_modal'; -import { UpdateAwsCredentialModal } from '../update_aws_credential_modal'; export interface EditDataSourceProps { existingDataSource: DataSourceAttributes; @@ -58,10 +56,9 @@ export interface EditDataSourceState { endpoint: string; auth: { type: AuthType; - credentials: UsernamePasswordTypedContent | SigV4Content | undefined; + credentials: UsernamePasswordTypedContent; }; showUpdatePasswordModal: boolean; - showUpdateAwsCredentialModal: boolean; showUpdateOptions: boolean; isLoading: boolean; } @@ -84,13 +81,9 @@ export class EditDataSourceForm extends React.Component { - this.setState({ auth: { ...this.state.auth, type: value as AuthType } }, () => { + const valueToSave = + value === AuthType.UsernamePasswordType ? AuthType.UsernamePasswordType : AuthType.NoAuth; + + const formErrorsByField = { + ...this.state.formErrorsByField, + createCredential: { ...this.state.formErrorsByField.createCredential }, + }; + if (valueToSave === AuthType.NoAuth) { + formErrorsByField.createCredential = { + username: [], + password: [], + }; + } + this.setState({ auth: { ...this.state.auth, type: valueToSave }, formErrorsByField }, () => { this.onChangeFormValues(); }); }; @@ -176,10 +174,7 @@ export class EditDataSourceForm extends React.Component { - this.setState({ - auth: { - ...this.state.auth, - credentials: { ...this.state.auth.credentials, region: e.target.value } as SigV4Content, - }, - }); - }; - - validateRegion = () => { - const isValid = !!this.state.auth.credentials.region?.trim().length; - this.setState({ - formErrorsByField: { - ...this.state.formErrorsByField, - awsCredential: { - ...this.state.formErrorsByField.awsCredential, - region: isValid ? [] : [''], - }, - }, - }); - }; - - onChangeAccessKey = (e: { target: { value: any } }) => { - this.setState({ - auth: { - ...this.state.auth, - credentials: { ...this.state.auth.credentials, accessKey: e.target.value } as SigV4Content, - }, - }); - }; - - validateAccessKey = () => { - const isValid = !!this.state.auth.credentials.accessKey; - this.setState({ - formErrorsByField: { - ...this.state.formErrorsByField, - awsCredential: { - ...this.state.formErrorsByField.awsCredential, - accessKey: isValid ? [] : [''], - }, - }, - }); - }; - - onChangeSecretKey = (e: { target: { value: any } }) => { - this.setState({ - auth: { - ...this.state.auth, - credentials: { ...this.state.auth.credentials, secretKey: e.target.value } as SigV4Content, - }, - }); - }; - - validateSecretKey = () => { - const isValid = !!this.state.auth.credentials.secretKey; - this.setState({ - formErrorsByField: { - ...this.state.formErrorsByField, - awsCredential: { - ...this.state.formErrorsByField.awsCredential, - secretKey: isValid ? [] : [''], - }, + credentials: { ...this.state.auth.credentials, password: e.target.value }, }, }); }; @@ -295,30 +221,12 @@ export class EditDataSourceForm extends React.Component { - this.setState({ showUpdateAwsCredentialModal: true }); - }; - /* Update password */ updatePassword = async (password: string) => { const { title, description, auth } = this.props.existingDataSource; @@ -411,7 +315,7 @@ export class EditDataSourceForm extends React.Component { - const { title, description, auth } = this.props.existingDataSource; - const updateAttributes: DataSourceAttributes = { - title, - description, - endpoint: undefined, - auth: { - type: auth.type, - credentials: { - region: auth.credentials ? auth.credentials.region : '', - accessKey, - secretKey, - } as SigV4Content, - }, - }; - this.closeAwsCredentialModal(); - - try { - await this.props.handleSubmit(updateAttributes); - this.props.displayToastMessage({ - id: 'dataSourcesManagement.editDataSource.updatePasswordSuccessMsg', - defaultMessage: 'Password updated successfully.', - success: true, - }); - } catch (e) { - this.props.displayToastMessage({ - id: 'dataSourcesManagement.editDataSource.updatePasswordFailMsg', - defaultMessage: 'Updating the stored password failed with some errors.', - }); - } - }; - /* Render methods */ - /* Render modal for new credential */ + /* Render Modal for new credential */ closePasswordModal = () => { this.setState({ showUpdatePasswordModal: false }); }; - closeAwsCredentialModal = () => { - this.setState({ showUpdateAwsCredentialModal: false }); - }; - renderUpdatePasswordModal = () => { return ( <> @@ -500,33 +367,6 @@ export class EditDataSourceForm extends React.Component ); }; - - renderUpdateAwsCredentialModal = () => { - return ( - <> - - { - - } - - - {this.state.showUpdateAwsCredentialModal ? ( - - ) : null} - - ); - }; - /* Render header*/ renderHeader = () => { return ( @@ -735,106 +575,8 @@ export class EditDataSourceForm extends React.Component - {this.renderSelectedAuthType(this.state.auth.type)} - - ); - }; - renderSelectedAuthType = (type: AuthType) => { - switch (type) { - case AuthType.UsernamePasswordType: - return this.renderUsernamePasswordFields(); - case AuthType.SigV4: - return this.renderSigV4ContentFields(); - default: - return null; - } - }; - - renderSigV4ContentFields = () => { - return ( - <> - - - - - - - - - - - {this.props.existingDataSource.auth.type === AuthType.SigV4 - ? this.renderUpdateAwsCredentialModal() - : null} + {this.state.auth.type !== AuthType.NoAuth ? this.renderUsernamePasswordFields() : null} ); }; @@ -858,7 +600,7 @@ export class EditDataSourceForm extends React.Component - {this.props.existingDataSource.auth.type === AuthType.UsernamePasswordType ? ( + {this.props.existingDataSource.auth.type !== AuthType.NoAuth ? ( {this.renderUpdatePasswordModal()} ) : null} @@ -917,17 +659,12 @@ export class EditDataSourceForm extends React.Component void; - closeUpdateAwsCredentialModal: () => void; -} - -export const UpdateAwsCredentialModal = ({ - region, - handleUpdateAwsCredential, - closeUpdateAwsCredentialModal, -}: UpdateAwsCredentialModalProps) => { - /* State Variables */ - const [newAccessKey, setNewAccessKey] = useState(''); - const [isNewAccessKeyValid, setIsNewAccessKeyValid] = useState(true); - - const [newSecretKey, setNewSecretKey] = useState(''); - const [isNewSecretKeyValid, setIsNewSecretKeyValid] = useState(true); - - const onClickUpdateAwsCredential = () => { - if (isFormValid()) { - handleUpdateAwsCredential(newAccessKey, newSecretKey); - } - }; - - const isFormValid = () => { - return !!(newAccessKey && newSecretKey); - }; - - const validateNewAccessKey = () => { - setIsNewAccessKeyValid(!!newAccessKey); - }; - - const validateNewSecretKey = () => { - setIsNewSecretKeyValid(!!newSecretKey); - }; - - const renderUpdateAwsCredentialModal = () => { - return ( - - - -

- { - - } -

-
-
- - - - - { - - } - - - - - - {/* Region */} - - - {region} - - - - {/* updated access key */} - - setNewAccessKey(e.target.value)} - onBlur={validateNewAccessKey} - /> - - - {/* updated secret key */} - - setNewSecretKey(e.target.value)} - onBlur={validateNewSecretKey} - /> - - - - - - - { - - } - - - {i18n.translate('dataSourcesManagement.editDataSource.updateStoredAwsCredential', { - defaultMessage: 'Update stored aws credential', - })} - - -
- ); - }; - - /* Return the modal */ - return
{renderUpdateAwsCredentialModal()}
; -}; diff --git a/src/plugins/data_source_management/public/components/validation/datasource_form_validation.ts b/src/plugins/data_source_management/public/components/validation/datasource_form_validation.ts index 1abde2d54edb..0e861f1184f3 100644 --- a/src/plugins/data_source_management/public/components/validation/datasource_form_validation.ts +++ b/src/plugins/data_source_management/public/components/validation/datasource_form_validation.ts @@ -16,11 +16,6 @@ export interface CreateEditDataSourceValidation { username: string[]; password: string[]; }; - awsCredential: { - region: string[]; - accessKey: string[]; - secretKey: string[]; - }; } export const defaultValidation: CreateEditDataSourceValidation = { @@ -30,11 +25,6 @@ export const defaultValidation: CreateEditDataSourceValidation = { username: [], password: [], }, - awsCredential: { - region: [], - accessKey: [], - secretKey: [], - }, }; export const isTitleValid = ( @@ -94,23 +84,6 @@ export const performDataSourceFormValidation = ( return false; } } - /* AWS SigV4 Content */ - if (formValues?.auth?.type === AuthType.SigV4) { - /* Access key */ - if (!formValues.auth.credentials?.accessKey) { - return false; - } - - /* Secret key */ - if (!formValues.auth.credentials?.secretKey) { - return false; - } - - /* Region */ - if (!formValues.auth.credentials?.region) { - return false; - } - } return true; }; diff --git a/src/plugins/data_source_management/public/types.ts b/src/plugins/data_source_management/public/types.ts index db8b6f1d9a1c..fe52466df1e5 100644 --- a/src/plugins/data_source_management/public/types.ts +++ b/src/plugins/data_source_management/public/types.ts @@ -53,13 +53,11 @@ export type DataSourceManagementContextValue = OpenSearchDashboardsReactContextV export enum AuthType { NoAuth = 'no_auth', UsernamePasswordType = 'username_password', - SigV4 = 'sigv4', } export const credentialSourceOptions = [ { id: AuthType.NoAuth, label: 'No authentication' }, { id: AuthType.UsernamePasswordType, label: 'Username & Password' }, - { id: AuthType.SigV4, label: 'AWS SigV4' }, ]; export interface DataSourceAttributes extends SavedObjectAttributes { @@ -68,7 +66,7 @@ export interface DataSourceAttributes extends SavedObjectAttributes { endpoint?: string; auth: { type: AuthType; - credentials: UsernamePasswordTypedContent | SigV4Content | undefined; + credentials: UsernamePasswordTypedContent | undefined; }; } @@ -76,9 +74,3 @@ export interface UsernamePasswordTypedContent extends SavedObjectAttributes { username: string; password?: string; } - -export interface SigV4Content extends SavedObjectAttributes { - accessKey: string; - secretKey: string; - region: string; -} diff --git a/yarn.lock b/yarn.lock index d39c0ac6f850..b1cdf6319ace 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2338,12 +2338,11 @@ mkdirp "^1.0.4" rimraf "^3.0.2" -"@opensearch-project/opensearch@^2.1.0": - version "2.1.0" - resolved "https://registry.yarnpkg.com/@opensearch-project/opensearch/-/opensearch-2.1.0.tgz#d79ab4ae643493512099673e117faffe40b4fe56" - integrity sha512-iM2u63j2IlUOuMSbcw1TZFpRqjK6qMwVhb3jLLa/x4aATxdKOiO1i17mgzfkeepqj85efNzXBZzN+jkq1/EXhQ== +"@opensearch-project/opensearch@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@opensearch-project/opensearch/-/opensearch-1.1.0.tgz#8b3c8b4cbcea01755ba092d2997bf0b4ca7f22f7" + integrity sha512-1TDw92JL8rD1b2QGluqBsIBLIiD5SGciIpz4qkrGAe9tcdfQ1ptub5e677rhWl35UULSjr6hP8M6HmISZ/M5HQ== dependencies: - aws4 "^1.11.0" debug "^4.3.1" hpagent "^0.1.1" ms "^2.1.3" @@ -2851,7 +2850,7 @@ resolved "https://registry.yarnpkg.com/@types/ejs/-/ejs-3.1.0.tgz#ab8109208106b5e764e5a6c92b2ba1c625b73020" integrity sha512-DCg+Ka+uDQ31lJ/UtEXVlaeV3d6t81gifaVWKJy4MYVVgvJttyX/viREy+If7fz+tK/gVxTGMtyrFPnm4gjrVA== -"@types/elasticsearch@*", "@types/elasticsearch@^5.0.33": +"@types/elasticsearch@^5.0.33": version "5.0.40" resolved "https://registry.yarnpkg.com/@types/elasticsearch/-/elasticsearch-5.0.40.tgz#811f6954088c264173e0a9876b97933250a4da10" integrity sha512-lhnbkC0XorAD7Dt7X+94cXUSHEdDNnEVk/DgFLHgIZQNhixV631Lj4+KpXunTT5rCHyj9RqK3TfO7QrOiwEeUQ== @@ -3062,15 +3061,6 @@ "@types/react" "*" hoist-non-react-statics "^3.3.0" -"@types/http-aws-es@6.0.2": - version "6.0.2" - resolved "https://registry.yarnpkg.com/@types/http-aws-es/-/http-aws-es-6.0.2.tgz#3c608f7da83382bb5a1a35c4f9704296b979ca26" - integrity sha512-VfQ/h+xxdeWP2Sf3BDf2feyzC8duBH5rFPJw2RW5m800fJLkZof/oojn1Atw1jCh4XerjiXRTIyqd5gUQ2iWNw== - dependencies: - "@types/elasticsearch" "*" - "@types/node" "*" - aws-sdk "^2.814.0" - "@types/http-cache-semantics@*": version "4.0.1" resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz#0ea7b61496902b95890dc4c3a116b60cb8dae812" @@ -4756,28 +4746,12 @@ aws-sdk@^2.650.0: uuid "8.0.0" xml2js "0.4.19" -aws-sdk@^2.814.0: - version "2.1318.0" - resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.1318.0.tgz#a633c82a51d0572805f744b51ca04a3babf6c909" - integrity sha512-xRCKqx4XWXUIpjDCVHmdOSINEVCIC5+yhmgUGR9A6VfxfPs59HbxKyd5LB+CmXhVbwVUM4SRWG5O+agQj+w7Eg== - dependencies: - buffer "4.9.2" - events "1.1.1" - ieee754 "1.1.13" - jmespath "0.16.0" - querystring "0.2.0" - sax "1.2.1" - url "0.10.3" - util "^0.12.4" - uuid "8.0.0" - xml2js "0.4.19" - aws-sign2@~0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" integrity sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA== -aws4@^1.11.0, aws4@^1.8.0: +aws4@^1.8.0: version "1.12.0" resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.12.0.tgz#ce1c9d143389679e253b314241ea9aa5cec980d3" integrity sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg== @@ -9877,11 +9851,6 @@ htmlparser2@^7.0: domutils "^2.8.0" entities "^3.0.1" -http-aws-es@6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/http-aws-es/-/http-aws-es-6.0.0.tgz#1528978d2bee718b8732dcdced0856efa747aeff" - integrity sha512-g+qp7J110/m4aHrR3iit4akAlnW0UljZ6oTq/rCcbsI8KP9x+95vqUtx49M2XQ2JMpwJio3B6gDYx+E8WDxqiA== - http-cache-semantics@^4.0.0, http-cache-semantics@^4.1.0: version "4.1.1" resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz#abe02fcb2985460bf0323be664436ec3476a6d5a"