diff --git a/package.json b/package.json
index b18c6dc863a5..7e8c61c59557 100644
--- a/package.json
+++ b/package.json
@@ -110,6 +110,7 @@
]
},
"dependencies": {
+ "@aws-crypto/client-node": "^3.1.1",
"@elastic/datemath": "5.0.3",
"@elastic/eui": "34.6.0",
"@elastic/good": "^9.0.1-kibana3",
diff --git a/src/plugins/credential_management/public/components/create_credential_wizard/create_credential_wizard.tsx b/src/plugins/credential_management/public/components/create_credential_wizard/create_credential_wizard.tsx
index e60a470ff0f2..19770188dd6a 100644
--- a/src/plugins/credential_management/public/components/create_credential_wizard/create_credential_wizard.tsx
+++ b/src/plugins/credential_management/public/components/create_credential_wizard/create_credential_wizard.tsx
@@ -30,7 +30,7 @@ interface CreateCredentialWizardState {
credentialName: string;
authType: string;
credentialMaterialsType: string;
- userName: string;
+ username: string;
password: string;
dual: boolean;
toasts: EuiGlobalToastListToast[];
@@ -51,8 +51,8 @@ export class CreateCredentialWizard extends React.Component<
this.state = {
credentialName: '',
authType: 'shared',
- credentialMaterialsType: 'username_password_credential',
- userName: '',
+ credentialMaterialsType: 'username_password',
+ username: '',
password: '',
dual: true,
toasts: [],
@@ -71,7 +71,7 @@ export class CreateCredentialWizard extends React.Component<
const header = this.renderHeader();
const options = [
- { value: 'username_password_credential', text: 'Username and Password Credential' },
+ { value: 'username_password', text: 'Username and Password Credential' },
{ value: 'no_auth', text: 'No Auth' },
];
@@ -127,8 +127,8 @@ export class CreateCredentialWizard extends React.Component<
this.setState({ userName: e.target.value })}
+ value={this.state.username || ''}
+ onChange={(e) => this.setState({ username: e.target.value })}
/>
@@ -181,7 +181,7 @@ export class CreateCredentialWizard extends React.Component<
credentialMaterials: {
credentialMaterialsType: this.state.credentialMaterialsType,
credentialMaterialsContent: {
- userName: this.state.userName,
+ username: this.state.username,
password: this.state.password,
},
},
diff --git a/src/plugins/data_source/common/credentials/index.ts b/src/plugins/data_source/common/credentials/index.ts
new file mode 100644
index 000000000000..70bfde226f9f
--- /dev/null
+++ b/src/plugins/data_source/common/credentials/index.ts
@@ -0,0 +1,6 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+export * as Credential from './types';
diff --git a/src/plugins/data_source/common/credentials/types.ts b/src/plugins/data_source/common/credentials/types.ts
new file mode 100644
index 000000000000..a774f515a03a
--- /dev/null
+++ b/src/plugins/data_source/common/credentials/types.ts
@@ -0,0 +1,27 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { SavedObjectAttributes } from 'src/core/types';
+
+export type SharedAuthType = 'shared';
+export type UsernamePasswordType = 'username_password';
+export type NoAuthType = 'no_auth';
+
+export interface CredentialSavedObjectAttributes extends SavedObjectAttributes {
+ title: string;
+ authType: SharedAuthType;
+ credentialMaterials: CredentialMaterials;
+ description?: string;
+}
+
+export interface CredentialMaterials extends SavedObjectAttributes {
+ credentialMaterialsType: UsernamePasswordType | NoAuthType;
+ credentialMaterialsContent?: UsernamePasswordTypedContent;
+}
+
+export interface UsernamePasswordTypedContent extends SavedObjectAttributes {
+ username: string;
+ password: string;
+}
diff --git a/src/plugins/data_source/common/index.ts b/src/plugins/data_source/common/index.ts
index 10fde727a706..bf5c6b1b0197 100644
--- a/src/plugins/data_source/common/index.ts
+++ b/src/plugins/data_source/common/index.ts
@@ -5,3 +5,5 @@
export const PLUGIN_ID = 'dataSource';
export const PLUGIN_NAME = 'data_source';
+
+export { Credential } from './credentials';
diff --git a/src/plugins/data_source/config.ts b/src/plugins/data_source/config.ts
new file mode 100644
index 000000000000..032f2c5a915a
--- /dev/null
+++ b/src/plugins/data_source/config.ts
@@ -0,0 +1,31 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { schema, TypeOf } from '@osd/config-schema';
+
+const KEY_NAME_MIN_LENGTH: number = 1;
+const KEY_NAME_MAX_LENGTH: number = 100;
+// Wrapping key size shoule be 32 bytes, as used in envelope encryption algorithms.
+const WRAPPING_KEY_SIZE: number = 32;
+
+export const configSchema = schema.object({
+ enabled: schema.boolean({ defaultValue: false }),
+ wrappingKeyName: schema.string({
+ minLength: KEY_NAME_MIN_LENGTH,
+ maxLength: KEY_NAME_MAX_LENGTH,
+ defaultValue: 'wrappingKeyName',
+ }),
+ wrappingKeyNamespace: schema.string({
+ minLength: KEY_NAME_MIN_LENGTH,
+ maxLength: KEY_NAME_MAX_LENGTH,
+ defaultValue: 'wrappingKeyNamespace',
+ }),
+ wrappingKey: schema.arrayOf(schema.number(), {
+ minSize: WRAPPING_KEY_SIZE,
+ maxSize: WRAPPING_KEY_SIZE,
+ }),
+});
+
+export type DataSourcePluginConfigType = TypeOf;
diff --git a/src/plugins/data_source/server/cryptography/cryptography_client.test.ts b/src/plugins/data_source/server/cryptography/cryptography_client.test.ts
new file mode 100644
index 000000000000..c1674f1af085
--- /dev/null
+++ b/src/plugins/data_source/server/cryptography/cryptography_client.test.ts
@@ -0,0 +1,20 @@
+/*
+ * 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));
+});
+
+// TODO: Add more test cases for encrypt / decrypt https://github.com/opensearch-project/OpenSearch-Dashboards/issues/2068
diff --git a/src/plugins/data_source/server/cryptography/cryptography_client.ts b/src/plugins/data_source/server/cryptography/cryptography_client.ts
new file mode 100644
index 000000000000..1d2ce378d771
--- /dev/null
+++ b/src/plugins/data_source/server/cryptography/cryptography_client.ts
@@ -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 readonly _keyring: RawAesKeyringNode;
+
+ private readonly _encrypt;
+ private readonly _decrypt;
+
+ /**
+ * @param {string} wrappingKeyName
+ * @param {string} wrappingKeyNamespace
+ * @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 encrypt(plainText: string | Buffer): Promise {
+ 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 decrypt(encrypted: string): Promise {
+ const result = await this._decrypt(this._keyring, Buffer.from(encrypted, ENCODING_STRATEGY));
+ return result.plaintext.toString();
+ }
+}
diff --git a/src/plugins/data_source/server/cryptography/index.ts b/src/plugins/data_source/server/cryptography/index.ts
new file mode 100644
index 000000000000..857fa691bddf
--- /dev/null
+++ b/src/plugins/data_source/server/cryptography/index.ts
@@ -0,0 +1,6 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+export { CryptographyClient } from './cryptography_client';
diff --git a/src/plugins/data_source/server/index.ts b/src/plugins/data_source/server/index.ts
index 41df94090986..f05b833817d6 100644
--- a/src/plugins/data_source/server/index.ts
+++ b/src/plugins/data_source/server/index.ts
@@ -3,15 +3,9 @@
* SPDX-License-Identifier: Apache-2.0
*/
-import { schema, TypeOf } from '@osd/config-schema';
import { PluginConfigDescriptor, PluginInitializerContext } from 'src/core/server';
import { DataSourcePlugin } from './plugin';
-
-export const configSchema = schema.object({
- enabled: schema.boolean({ defaultValue: false }),
-});
-
-export type DataSourcePluginConfigType = TypeOf;
+import { configSchema, DataSourcePluginConfigType } from '../config';
export const config: PluginConfigDescriptor = {
schema: configSchema,
diff --git a/src/plugins/data_source/server/plugin.ts b/src/plugins/data_source/server/plugin.ts
index b92d6dffd9d7..f1eef0c8506e 100644
--- a/src/plugins/data_source/server/plugin.ts
+++ b/src/plugins/data_source/server/plugin.ts
@@ -3,20 +3,27 @@
* SPDX-License-Identifier: Apache-2.0
*/
+import { first } from 'rxjs/operators';
+
import { PluginInitializerContext, CoreSetup, CoreStart, Plugin, Logger } from 'src/core/server';
-import { dataSource, credential } from './saved_objects';
+import { dataSource, credential, CredentialSavedObjectsClientWrapper } from './saved_objects';
+import { DataSourcePluginConfigType } from '../config';
import { DataSourcePluginSetup, DataSourcePluginStart } from './types';
+import { CryptographyClient } from './cryptography';
+
export class DataSourcePlugin implements Plugin {
- private readonly logger: Logger;
+ private readonly _logger: Logger;
+ private _initializerContext: PluginInitializerContext;
constructor(initializerContext: PluginInitializerContext) {
- this.logger = initializerContext.logger.get();
+ this._initializerContext = initializerContext;
+ this._logger = initializerContext.logger.get();
}
- public setup(core: CoreSetup) {
- this.logger.debug('data_source: Setup');
+ public async setup(core: CoreSetup) {
+ this._logger.debug('data_source: Setup');
// Register credential saved object type
core.savedObjects.registerType(credential);
@@ -24,13 +31,37 @@ export class DataSourcePlugin implements Plugin {
+ // Create credential saved objects client wrapper
+ const credentialSavedObjectsClientWrapper = new CredentialSavedObjectsClientWrapper(
+ cryptographyClient
+ );
+
+ // Add credential saved objects client wrapper factory
+ core.savedObjects.addClientWrapper(
+ 1,
+ 'credential',
+ credentialSavedObjectsClientWrapper.wrapperFactory
+ );
+ });
+
return {};
}
public start(core: CoreStart) {
- this.logger.debug('data_source: Started');
+ this._logger.debug('data_source: Started');
return {};
}
public stop() {}
+
+ private async createCryptographyClient() {
+ const {
+ wrappingKeyName,
+ wrappingKeyNamespace,
+ wrappingKey,
+ } = await this._initializerContext.config.create().pipe(first()).toPromise();
+
+ return new CryptographyClient(wrappingKeyName, wrappingKeyNamespace, wrappingKey);
+ }
}
diff --git a/src/plugins/data_source/server/saved_objects/credential_saved_objects_client_wrapper.ts b/src/plugins/data_source/server/saved_objects/credential_saved_objects_client_wrapper.ts
new file mode 100644
index 000000000000..26b2bae84610
--- /dev/null
+++ b/src/plugins/data_source/server/saved_objects/credential_saved_objects_client_wrapper.ts
@@ -0,0 +1,278 @@
+/*
+ * Copyright OpenSearch Contributors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {
+ HttpServiceStart,
+ SavedObjectsBulkCreateObject,
+ SavedObjectsBulkResponse,
+ SavedObjectsBulkUpdateObject,
+ SavedObjectsBulkUpdateOptions,
+ SavedObjectsBulkUpdateResponse,
+ SavedObjectsClientWrapperFactory,
+ SavedObjectsCreateOptions,
+ SavedObjectsUpdateOptions,
+ SavedObjectsUpdateResponse,
+} from 'opensearch-dashboards/server';
+
+import { SavedObjectsErrorHelpers } from '../../../../core/server';
+
+import { CryptographyClient } from '../cryptography';
+
+import { Credential } from '../../common';
+
+/**
+ * Describes the Credential Saved Objects Client Wrapper class,
+ * which contains the factory used to create Saved Objects Client Wrapper instances
+ */
+export class CredentialSavedObjectsClientWrapper {
+ public httpStart?: HttpServiceStart;
+
+ private _type: string = 'credential';
+ private _authType: Credential.SharedAuthType = 'shared';
+ private _noAuthType: Credential.NoAuthType = 'no_auth';
+ private _usernamePasswordType: Credential.UsernamePasswordType = 'username_password';
+
+ private _cryptographyClient: CryptographyClient;
+
+ constructor(cryptographyClient: CryptographyClient) {
+ this._cryptographyClient = cryptographyClient;
+ }
+
+ /**
+ * Describes the factory used to create instances of Saved Objects Client Wrappers
+ * for credential spcific operations such as encryption
+ * Check {@link Credential.CredentialSavedObjectAttributes} for attributes type details
+ * Check {@link Credential.CredentialMaterials} for credential materials type and
+ * credential materials content details
+ */
+ public wrapperFactory: SavedObjectsClientWrapperFactory = (wrapperOptions) => {
+ const createWithCredentialMaterialsEncryption = async (
+ type: string,
+ attributes: T,
+ options?: SavedObjectsCreateOptions
+ ) => {
+ this.validateType(type);
+ this.validateAttributes(attributes);
+
+ const { title, authType, credentialMaterials, description } = attributes;
+
+ if (this._noAuthType === credentialMaterials.credentialMaterialsType) {
+ return await wrapperOptions.client.create(type, attributes, options);
+ }
+
+ const encryptCredentialMaterials = await this.validateAndEncryptCredentialMaterials(
+ credentialMaterials
+ );
+
+ const encryptedAttributes = {
+ title,
+ authType,
+ credentialMaterials: encryptCredentialMaterials,
+ description,
+ };
+
+ return await wrapperOptions.client.create(type, encryptedAttributes, options);
+ };
+
+ const bulkCreateWithCredentialMaterialsEncryption = async (
+ objects: Array>,
+ options?: SavedObjectsCreateOptions
+ ): Promise> => {
+ objects = await Promise.all(
+ objects.map(async (object) => {
+ const { type, attributes } = object;
+
+ this.validateType(type);
+ this.validateAttributes(attributes);
+
+ const { title, authType, credentialMaterials, description } = attributes;
+
+ if (this._noAuthType === credentialMaterials.credentialMaterialsType) {
+ return object;
+ }
+
+ const encryptedAttributes = {
+ title,
+ authType,
+ credentialMaterials: await this.validateAndEncryptCredentialMaterials(
+ credentialMaterials
+ ),
+ description,
+ };
+
+ return ({
+ ...object,
+ attributes: encryptedAttributes,
+ // Unfortunately this throws a typescript error without the casting. I think it's due to the
+ // convoluted way SavedObjects are created.
+ } as unknown) as SavedObjectsBulkCreateObject;
+ })
+ );
+ return await wrapperOptions.client.bulkCreate(objects, options);
+ };
+
+ const updateWithCredentialMaterialsEncryption = async (
+ type: string,
+ id: string,
+ attributes: Partial,
+ options: SavedObjectsUpdateOptions = {}
+ ): Promise> => {
+ this.validateType(type);
+ this.validateAttributes(attributes);
+
+ const { title, authType, credentialMaterials, description } = attributes;
+
+ if (this._noAuthType === credentialMaterials.credentialMaterialsType) {
+ return await wrapperOptions.client.create(type, attributes, options);
+ }
+
+ const encryptedAttributes: Partial = {
+ title,
+ authType,
+ credentialMaterials: await this.validateAndEncryptCredentialMaterials(credentialMaterials),
+ description,
+ };
+
+ return await wrapperOptions.client.update(type, id, encryptedAttributes, options);
+ };
+
+ const bulkUpdateWithCredentialMaterialsEncryption = async (
+ objects: Array>,
+ options?: SavedObjectsBulkUpdateOptions
+ ): Promise> => {
+ objects = await Promise.all(
+ objects.map(async (object) => {
+ const { type, attributes } = object;
+
+ this.validateType(type);
+ this.validateAttributes(attributes);
+
+ const { title, authType, credentialMaterials, description } = attributes;
+
+ if (this._noAuthType === credentialMaterials.credentialMaterialsType) {
+ return object;
+ }
+
+ const encryptedAttributes = {
+ title,
+ authType,
+ credentialMaterials: await this.validateAndEncryptCredentialMaterials(
+ credentialMaterials
+ ),
+ description,
+ };
+
+ return ({
+ ...object,
+ attributes: encryptedAttributes,
+ // Unfortunately this throws a typescript error without the casting. I think it's due to the
+ // convoluted way SavedObjects are created.
+ } as unknown) as SavedObjectsBulkUpdateObject;
+ })
+ );
+
+ return await wrapperOptions.client.bulkUpdate(objects, options);
+ };
+
+ return {
+ ...wrapperOptions.client,
+ create: createWithCredentialMaterialsEncryption,
+ bulkCreate: bulkCreateWithCredentialMaterialsEncryption,
+ checkConflicts: wrapperOptions.client.checkConflicts,
+ delete: wrapperOptions.client.delete,
+ find: wrapperOptions.client.find,
+ bulkGet: wrapperOptions.client.bulkGet,
+ get: wrapperOptions.client.get,
+ update: updateWithCredentialMaterialsEncryption,
+ bulkUpdate: bulkUpdateWithCredentialMaterialsEncryption,
+ errors: wrapperOptions.client.errors,
+ addToNamespaces: wrapperOptions.client.addToNamespaces,
+ deleteFromNamespaces: wrapperOptions.client.deleteFromNamespaces,
+ };
+ };
+
+ private validateType(type: string) {
+ if (this._type !== type) {
+ throw SavedObjectsErrorHelpers.createUnsupportedTypeError(type);
+ }
+ return;
+ }
+
+ private validateAttributes(attributes: T) {
+ const { title, authType, credentialMaterials } = attributes;
+
+ if (title === undefined) {
+ throw SavedObjectsErrorHelpers.createBadRequestError('attribute "title" required');
+ }
+
+ if (authType === undefined) {
+ throw SavedObjectsErrorHelpers.createBadRequestError('attribute "authType" required');
+ }
+
+ if (credentialMaterials === undefined) {
+ throw SavedObjectsErrorHelpers.createBadRequestError(
+ 'attribute "credentialMaterials" required'
+ );
+ }
+
+ // TODO: Support other auth types https://github.com/opensearch-project/OpenSearch-Dashboards/issues/2110
+ if (this._authType !== authType) {
+ throw SavedObjectsErrorHelpers.createBadRequestError(
+ `Unsupported authType type: '${attributes.authType}'`
+ );
+ }
+ return;
+ }
+
+ private async validateAndEncryptCredentialMaterials(credentialMaterials: T) {
+ this.validateUsernamePasswordTypedCredentialMaterials(credentialMaterials);
+ return await this.encryptCredentialMaterials(credentialMaterials);
+ }
+
+ private validateUsernamePasswordTypedCredentialMaterials(credentialMaterials: T) {
+ const { credentialMaterialsType, credentialMaterialsContent } = credentialMaterials;
+
+ if (this._usernamePasswordType !== credentialMaterialsType) {
+ throw SavedObjectsErrorHelpers.createBadRequestError(
+ `Invalid credential materials type: '${credentialMaterialsType}'`
+ );
+ }
+
+ if (credentialMaterialsContent === undefined) {
+ throw SavedObjectsErrorHelpers.createBadRequestError(
+ `attribute "credentialMaterialsContent" required for: '${credentialMaterialsType}'`
+ );
+ }
+
+ this.validateUsernamePasswordTypedContent(credentialMaterialsContent);
+
+ return;
+ }
+
+ private validateUsernamePasswordTypedContent(credentialMaterialsContent: T) {
+ const { username, password } = credentialMaterialsContent;
+
+ if (username === undefined) {
+ throw SavedObjectsErrorHelpers.createBadRequestError('attribute "username" required');
+ }
+
+ if (password === undefined) {
+ throw SavedObjectsErrorHelpers.createBadRequestError('attribute "password" required');
+ }
+
+ return;
+ }
+
+ private async encryptCredentialMaterials(credentialMaterials: T) {
+ const { credentialMaterialsType, credentialMaterialsContent } = credentialMaterials;
+ return {
+ credentialMaterialsType,
+ credentialMaterialsContent: {
+ username: credentialMaterialsContent.username,
+ password: await this._cryptographyClient.encrypt(credentialMaterialsContent.password),
+ },
+ };
+ }
+}
diff --git a/src/plugins/data_source/server/saved_objects/index.ts b/src/plugins/data_source/server/saved_objects/index.ts
index aa3411811e88..bd1c8a6613a3 100644
--- a/src/plugins/data_source/server/saved_objects/index.ts
+++ b/src/plugins/data_source/server/saved_objects/index.ts
@@ -5,3 +5,4 @@
export { credential } from './credential_saved_objects_type';
export { dataSource } from './data_source';
+export { CredentialSavedObjectsClientWrapper } from './credential_saved_objects_client_wrapper';