Skip to content

Commit

Permalink
[Backport 2.x] MultiDataSource feature merge (opensearch-project#2334) (
Browse files Browse the repository at this point in the history
opensearch-project#2409)

Signed-off-by: mpabba3003 <[email protected]>

Co-authored-by: Kristen Tian <[email protected]>
  • Loading branch information
2 people authored and Peter Fitzgibbons committed Dec 1, 2022
1 parent 355ed30 commit 09d8b50
Show file tree
Hide file tree
Showing 13 changed files with 763 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
* under the License.
*/

import { LegacyAPICaller } from 'opensearch-dashboards/server';
import { LegacyAPICaller, OpenSearchClient } from 'opensearch-dashboards/server';

import { getFieldCapabilities, resolveTimePattern, createNoMatchingIndicesError } from './lib';

Expand All @@ -48,9 +48,9 @@ interface FieldSubType {
}

export class IndexPatternsFetcher {
private _callDataCluster: LegacyAPICaller;
private _callDataCluster: LegacyAPICaller | OpenSearchClient;

constructor(callDataCluster: LegacyAPICaller) {
constructor(callDataCluster: LegacyAPICaller | OpenSearchClient) {
this._callDataCluster = callDataCluster;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@

import { defaults, keyBy, sortBy } from 'lodash';

import { LegacyAPICaller } from 'opensearch-dashboards/server';
import { LegacyAPICaller, OpenSearchClient } from 'opensearch-dashboards/server';
import { callFieldCapsApi } from '../opensearch_api';
import { FieldCapsResponse, readFieldCapsResponse } from './field_caps_response';
import { mergeOverrides } from './overrides';
Expand All @@ -47,7 +47,7 @@ import { FieldDescriptor } from '../../index_patterns_fetcher';
* @return {Promise<Array<FieldDescriptor>>}
*/
export async function getFieldCapabilities(
callCluster: LegacyAPICaller,
callCluster: LegacyAPICaller | OpenSearchClient,
indices: string | string[] = [],
metaFields: string[] = [],
fieldCapsOptions?: { allowNoIndices: boolean }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,23 @@ export interface IndexAliasResponse {
* @return {Promise<IndexAliasResponse>}
*/
export async function callIndexAliasApi(
callCluster: LegacyAPICaller,
callCluster: LegacyAPICaller | OpenSearchClient,
indices: string[] | string
): Promise<IndicesAliasResponse> {
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,
Expand All @@ -84,11 +97,25 @@ export async function callIndexAliasApi(
* @return {Promise<FieldCapsResponse>}
*/
export async function callFieldCapsApi(
callCluster: LegacyAPICaller,
callCluster: LegacyAPICaller | OpenSearchClient,
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: '*',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,10 @@ import { callIndexAliasApi, IndicesAliasResponse } from './opensearch_api';
* and the indices that actually match the time
* pattern (matches);
*/
export async function resolveTimePattern(callCluster: LegacyAPICaller, timePattern: string) {
export async function resolveTimePattern(
callCluster: LegacyAPICaller | OpenSearchClient,
timePattern: string
) {
const aliases = await callIndexAliasApi(callCluster, timePatternToWildcard(timePattern));

const allIndexDetails = chain<IndicesAliasResponse>(aliases)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import { CryptographyClient } from './cryptography_client';
import { randomBytes } from 'crypto';

const dummyWrappingKeyName = 'dummy_wrapping_key_name';
const dummyWrappingKeyNamespace = 'dummy_wrapping_key_namespace';

test('Invalid wrapping key size throws error', () => {
const dummyRandomBytes = [...randomBytes(31)];
const expectedErrorMsg = `Wrapping key size shoule be 32 bytes, as used in envelope encryption. Current wrapping key size: '${dummyRandomBytes.length}' bytes`;
expect(() => {
new CryptographyClient(dummyWrappingKeyName, dummyWrappingKeyNamespace, dummyRandomBytes);
}).toThrowError(new Error(expectedErrorMsg));
});

describe('Test encrpyt and decrypt module', () => {
const dummyPlainText = 'dummy';
const dummyNumArray1 = [...randomBytes(32)];
const dummyNumArray2 = [...randomBytes(32)];

describe('Positive test cases', () => {
test('Encrypt and Decrypt with same in memory keyring', async () => {
const cryptographyClient = new CryptographyClient(
dummyWrappingKeyName,
dummyWrappingKeyNamespace,
dummyNumArray1
);
const encrypted = await cryptographyClient.encryptAndEncode(dummyPlainText);
const outputText = await cryptographyClient.decodeAndDecrypt(encrypted);
expect(outputText).toBe(dummyPlainText);
});
test('Encrypt and Decrypt with two different keyrings with exact same identifiers', async () => {
const cryptographyClient1 = new CryptographyClient(
dummyWrappingKeyName,
dummyWrappingKeyNamespace,
dummyNumArray1
);
const encrypted = await cryptographyClient1.encryptAndEncode(dummyPlainText);

const cryptographyClient2 = new CryptographyClient(
dummyWrappingKeyName,
dummyWrappingKeyNamespace,
dummyNumArray1
);
const outputText = await cryptographyClient2.decodeAndDecrypt(encrypted);
expect(cryptographyClient1 === cryptographyClient2).toBeFalsy();
expect(outputText).toBe(dummyPlainText);
});
});

describe('Negative test cases', () => {
const defaultWrappingKeyName = 'changeme';
const defaultWrappingKeyNamespace = 'changeme';
const expectedErrorMsg = 'unencryptedDataKey has not been set';
test('Encrypt and Decrypt with different key names', async () => {
const cryptographyClient1 = new CryptographyClient(
dummyWrappingKeyName,
dummyWrappingKeyNamespace,
dummyNumArray1
);
const encrypted = await cryptographyClient1.encryptAndEncode(dummyPlainText);

const cryptographyClient2 = new CryptographyClient(
defaultWrappingKeyName,
dummyWrappingKeyNamespace,
dummyNumArray1
);
try {
await cryptographyClient2.decodeAndDecrypt(encrypted);
} catch (error) {
expect(error.message).toMatch(expectedErrorMsg);
}
});
test('Encrypt and Decrypt with different key namespaces', async () => {
const cryptographyClient1 = new CryptographyClient(
dummyWrappingKeyName,
dummyWrappingKeyNamespace,
dummyNumArray1
);
const encrypted = await cryptographyClient1.encryptAndEncode(dummyPlainText);

const cryptographyClient2 = new CryptographyClient(
dummyWrappingKeyName,
defaultWrappingKeyNamespace,
dummyNumArray1
);
try {
await cryptographyClient2.decodeAndDecrypt(encrypted);
} catch (error) {
expect(error.message).toMatch(expectedErrorMsg);
}
});
test('Encrypt and Decrypt with different wrapping keys', async () => {
const cryptographyClient1 = new CryptographyClient(
dummyWrappingKeyName,
dummyWrappingKeyNamespace,
dummyNumArray1
);
const encrypted = await cryptographyClient1.encryptAndEncode(dummyPlainText);

const cryptographyClient2 = new CryptographyClient(
dummyWrappingKeyName,
dummyWrappingKeyNamespace,
dummyNumArray2
);
try {
await cryptographyClient2.decodeAndDecrypt(encrypted);
} catch (error) {
expect(error.message).toMatch(expectedErrorMsg);
}
});
});
});
70 changes: 70 additions & 0 deletions src/plugins/data_source/server/cryptography/cryptography_client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

import {
buildClient,
CommitmentPolicy,
RawAesKeyringNode,
RawAesWrappingSuiteIdentifier,
} from '@aws-crypto/client-node';

export const ENCODING_STRATEGY: BufferEncoding = 'base64';
export const WRAPPING_KEY_SIZE: number = 32;

export class CryptographyClient {
private readonly commitmentPolicy = CommitmentPolicy.REQUIRE_ENCRYPT_REQUIRE_DECRYPT;
private readonly wrappingSuite = RawAesWrappingSuiteIdentifier.AES256_GCM_IV12_TAG16_NO_PADDING;

private keyring: RawAesKeyringNode;

private readonly encrypt: Function;
private readonly decrypt: Function;

/**
* @param {string} wrappingKeyName name value to identify the AES key in a keyring
* @param {string} wrappingKeyNamespace namespace value to identify the AES key in a keyring,
* @param {number[]} wrappingKey 32 Bytes raw wrapping key used to perform envelope encryption
*/
constructor(wrappingKeyName: string, wrappingKeyNamespace: string, wrappingKey: number[]) {
if (wrappingKey.length !== WRAPPING_KEY_SIZE) {
const wrappingKeySizeMismatchMsg = `Wrapping key size shoule be 32 bytes, as used in envelope encryption. Current wrapping key size: '${wrappingKey.length}' bytes`;
throw new Error(wrappingKeySizeMismatchMsg);
}

// Create raw AES keyring
this.keyring = new RawAesKeyringNode({
keyName: wrappingKeyName,
keyNamespace: wrappingKeyNamespace,
unencryptedMasterKey: new Uint8Array(wrappingKey),
wrappingSuite: this.wrappingSuite,
});

// Destructuring encrypt and decrypt functions from client
const { encrypt, decrypt } = buildClient(this.commitmentPolicy);

this.encrypt = encrypt;
this.decrypt = decrypt;
}

/**
* Input text content and output encrypted string encoded with ENCODING_STRATEGY
* @param {string} plainText
* @returns {Promise}
*/
public async encryptAndEncode(plainText: string): Promise<string> {
const result = await this.encrypt(this.keyring, plainText);
return result.result.toString(ENCODING_STRATEGY);
}

/**
* Input encrypted content and output decrypted string
* @param {string} encrypted
* @returns {Promise}
*/
public async decodeAndDecrypt(encrypted: string): Promise<string> {
const result = await this.decrypt(this.keyring, Buffer.from(encrypted, ENCODING_STRATEGY));
return result.plaintext.toString();
}
}
6 changes: 6 additions & 0 deletions src/plugins/data_source/server/cryptography/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

export { CryptographyClient } from './cryptography_client';
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
/*
* Copyright OpenSearch Contributors
* SPDX-License-Identifier: Apache-2.0
*/

export * from './text_content';
Loading

0 comments on commit 09d8b50

Please sign in to comment.