diff --git a/package.json b/package.json index a2bc0bae3a4c..a210b0a308b7 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": "npm:@opensearch-project/oui@1.0.0", "@elastic/good": "^9.0.1-kibana3", diff --git a/src/dev/jest/config.js b/src/dev/jest/config.js index e0730cc0d916..e44837218b46 100644 --- a/src/dev/jest/config.js +++ b/src/dev/jest/config.js @@ -101,4 +101,7 @@ export default { '/node_modules/enzyme-to-json/serializer', ], reporters: ['default', '/src/dev/jest/junit_reporter.js'], + globals: { + Uint8Array: Uint8Array, + }, }; 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..f3a3ce8b34d7 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 @@ -21,17 +21,18 @@ import { EuiFieldPassword, } from '@elastic/eui'; import { DocLinksStart } from 'src/core/public'; +import { Credential } from '../../../../data_source/common'; + import { getCreateBreadcrumbs } from '../breadcrumbs'; import { CredentialManagmentContextValue } from '../../types'; import { Header } from './components/header'; import { context as contextType } from '../../../../opensearch_dashboards_react/public'; interface CreateCredentialWizardState { - credentialName: string; - authType: string; - credentialMaterialsType: string; - userName: string; - password: string; + credentialName?: string; + credentialMaterialsType?: string; + username?: string; + password?: string; dual: boolean; toasts: EuiGlobalToastListToast[]; docLinks: DocLinksStart; @@ -49,11 +50,10 @@ export class CreateCredentialWizard extends React.Component< context.services.setBreadcrumbs(getCreateBreadcrumbs()); this.state = { - credentialName: '', - authType: 'shared', - credentialMaterialsType: 'username_password_credential', - userName: '', - password: '', + credentialName: undefined, + credentialMaterialsType: undefined, + username: undefined, + password: undefined, dual: true, toasts: [], docLinks: context.services.docLinks, @@ -71,8 +71,11 @@ export class CreateCredentialWizard extends React.Component< const header = this.renderHeader(); const options = [ - { value: 'username_password_credential', text: 'Username and Password Credential' }, - { value: 'no_auth', text: 'No Auth' }, + { value: undefined, text: 'Select Credential Materials Type' }, + { + value: Credential.CredentialMaterialsType.UsernamePasswordType, + text: 'Username and Password Credential', + }, ]; return ( @@ -127,15 +130,15 @@ export class CreateCredentialWizard extends React.Component< this.setState({ userName: e.target.value })} + value={this.state.username || undefined} + onChange={(e) => this.setState({ username: e.target.value })} /> this.setState({ password: e.target.value })} /> @@ -177,11 +180,10 @@ export class CreateCredentialWizard extends React.Component< // TODO: Add rendering spanner https://github.com/opensearch-project/OpenSearch-Dashboards/issues/2050 await savedObjects.client.create('credential', { title: this.state.credentialName, - authType: this.state.authType, credentialMaterials: { credentialMaterialsType: this.state.credentialMaterialsType, credentialMaterialsContent: { - userName: this.state.userName, + username: this.state.username, password: this.state.password, }, }, diff --git a/src/plugins/credential_management/public/components/credential_table/credentials_table.tsx b/src/plugins/credential_management/public/components/credential_table/credentials_table.tsx index 18822eff7595..4383695f3106 100644 --- a/src/plugins/credential_management/public/components/credential_table/credentials_table.tsx +++ b/src/plugins/credential_management/public/components/credential_table/credentials_table.tsx @@ -5,6 +5,7 @@ import React from 'react'; import { withRouter, RouteComponentProps } from 'react-router-dom'; +import { useEffectOnce } from 'react-use'; import { i18n } from '@osd/i18n'; import { FormattedMessage } from '@osd/i18n/react'; @@ -41,7 +42,7 @@ const pagination = { const sorting = { sort: { - field: 'credentialName', + field: 'title', direction: 'asc' as const, }, }; @@ -68,7 +69,11 @@ export const CredentialsTable = ({ canSave, history }: Props) => { const [selectedCredentials, setSelectedCredentials] = React.useState([]); const { setBreadcrumbs } = useOpenSearchDashboards().services; - setBreadcrumbs(getListBreadcrumbs()); + + /* Update breadcrumb*/ + useEffectOnce(() => { + setBreadcrumbs(getListBreadcrumbs()); + }); const { savedObjects, uiSettings } = useOpenSearchDashboards< CredentialManagementContext diff --git a/src/plugins/credential_management/public/components/edit_credential/edit_credential.tsx b/src/plugins/credential_management/public/components/edit_credential/edit_credential.tsx index 0f849725dd82..bee4e43eb135 100644 --- a/src/plugins/credential_management/public/components/edit_credential/edit_credential.tsx +++ b/src/plugins/credential_management/public/components/edit_credential/edit_credential.tsx @@ -25,18 +25,20 @@ import { EuiConfirmModal, } from '@elastic/eui'; import { DocLinksStart } from 'src/core/public'; +import { Credential } from '../../../../data_source/common'; + import { getCreateBreadcrumbs } from '../breadcrumbs'; import { CredentialManagmentContextValue } from '../../types'; // TODO: Add Header https://github.com/opensearch-project/OpenSearch-Dashboards/issues/2051 import { context as contextType } from '../../../../opensearch_dashboards_react/public'; import { CredentialEditPageItem } from '../types'; -import * as localizedContent from '../text_content/text_content'; +import { localizedContent } from '../text_content'; interface EditCredentialState { credentialName: string; credentialMaterialsType: string; - userName: string; - password: string; + username?: string; + password?: string; dual: boolean; toasts: EuiGlobalToastListToast[]; docLinks: DocLinksStart; @@ -61,8 +63,8 @@ export class EditCredentialComponent extends React.Component< this.state = { credentialName: props.credential.title, credentialMaterialsType: props.credential.credentialMaterialsType, - userName: '', - password: '', + username: undefined, + password: undefined, dual: true, toasts: [], docLinks: context.services.docLinks, @@ -140,10 +142,9 @@ export class EditCredentialComponent extends React.Component< renderContent() { const options = [ { - value: 'username_password_credential', + value: Credential.CredentialMaterialsType.UsernamePasswordType, text: 'Username and Password Credential', }, - { value: 'no_auth', text: 'No Auth' }, ]; return ( @@ -198,15 +199,15 @@ export class EditCredentialComponent extends React.Component< this.setState({ userName: e.target.value })} + value={this.state.username || undefined} + onChange={(e) => this.setState({ username: e.target.value })} /> this.setState({ password: e.target.value })} /> @@ -254,7 +255,7 @@ export class EditCredentialComponent 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/credential_management/public/components/text_content/index.ts b/src/plugins/credential_management/public/components/text_content/index.ts new file mode 100644 index 000000000000..4ac019150c2a --- /dev/null +++ b/src/plugins/credential_management/public/components/text_content/index.ts @@ -0,0 +1,6 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +export * as localizedContent from '../text_content/text_content'; diff --git a/src/plugins/credential_management/public/components/utils.ts b/src/plugins/credential_management/public/components/utils.ts index 3fad1ef196be..5b9ae187d0a2 100644 --- a/src/plugins/credential_management/public/components/utils.ts +++ b/src/plugins/credential_management/public/components/utils.ts @@ -18,7 +18,8 @@ export async function getCredentials(savedObjectsClient: SavedObjectsClientContr .map((source) => { const id = source.id; const title = source.get('title'); - const credentialMaterialsType = source.get('credentialMaterials').credentialMaterialsType; + const credentialMaterialsType = source.get('credentialMaterials') + ?.credentialMaterialsType; return { id, title, 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..8888a1a1ba9f --- /dev/null +++ b/src/plugins/data_source/common/credentials/types.ts @@ -0,0 +1,29 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import { SavedObjectAttributes } from 'src/core/types'; + +/** + * Each credential's materials type. For the time being, only username/password pairs are supported. + */ +export enum CredentialMaterialsType { + UsernamePasswordType = 'username_password', +} + +export interface CredentialSavedObjectAttributes extends SavedObjectAttributes { + title: string; + credentialMaterials: CredentialMaterials; + description?: string; +} + +export interface CredentialMaterials extends SavedObjectAttributes { + credentialMaterialsType: CredentialMaterialsType; + 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..5a8a654cd059 --- /dev/null +++ b/src/plugins/data_source/config.ts @@ -0,0 +1,34 @@ +/* + * 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 }), + encryption: schema.object({ + wrappingKeyName: schema.string({ + minLength: KEY_NAME_MIN_LENGTH, + maxLength: KEY_NAME_MAX_LENGTH, + defaultValue: 'changeme', + }), + wrappingKeyNamespace: schema.string({ + minLength: KEY_NAME_MIN_LENGTH, + maxLength: KEY_NAME_MAX_LENGTH, + defaultValue: 'changeme', + }), + wrappingKey: schema.arrayOf(schema.number(), { + minSize: WRAPPING_KEY_SIZE, + maxSize: WRAPPING_KEY_SIZE, + defaultValue: new Array(32).fill(0), + }), + }), +}); + +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..1f8d2596a3c4 --- /dev/null +++ b/src/plugins/data_source/server/cryptography/cryptography_client.test.ts @@ -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); + } + }); + }); +}); 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..f5968ae13adb --- /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 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 { + 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 { + 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..5a4ed403457c 100644 --- a/src/plugins/data_source/server/plugin.ts +++ b/src/plugins/data_source/server/plugin.ts @@ -3,19 +3,24 @@ * 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; - constructor(initializerContext: PluginInitializerContext) { - this.logger = initializerContext.logger.get(); + constructor(private initializerContext: PluginInitializerContext) { + this.logger = this.initializerContext.logger.get(); } - public setup(core: CoreSetup) { + public async setup(core: CoreSetup) { this.logger.debug('data_source: Setup'); // Register credential saved object type @@ -24,6 +29,22 @@ export class DataSourcePlugin implements Plugin { + const createWithCredentialMaterialsEncryption = async ( + type: string, + attributes: T, + options?: SavedObjectsCreateOptions + ) => { + if (this.type !== type) { + return await wrapperOptions.client.create(type, attributes, options); + } + + const encryptedAttributes = await this.validateAndEncryptAttributes(attributes); + + 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; + + if (this.type !== type) { + return object; + } + + return { + ...object, + attributes: await this.validateAndEncryptAttributes(attributes), + }; + }) + ); + return await wrapperOptions.client.bulkCreate(objects, options); + }; + + const updateWithCredentialMaterialsEncryption = async ( + type: string, + id: string, + attributes: Partial, + options: SavedObjectsUpdateOptions = {} + ): Promise> => { + if (this.type !== type) { + return await wrapperOptions.client.update(type, id, attributes, options); + } + + const encryptedAttributes: Partial = await this.validateAndEncryptPartialAttributes( + attributes + ); + + 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; + + if (this.type !== type) { + return object; + } + + const encryptedAttributes: Partial = await this.validateAndEncryptPartialAttributes( + attributes + ); + + return { + ...object, + attributes: encryptedAttributes, + }; + }) + ); + + 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 async validateAndEncryptAttributes(attributes: T) { + this.validateAttributes(attributes); + + return await this.encryptCredentialMaterials(attributes); + } + + private async validateAndEncryptPartialAttributes(attributes: T) { + this.validateCredentialMaterials(attributes.credentialMaterials); + + return await this.encryptCredentialMaterials(attributes); + } + + private validateAttributes(attributes: T) { + const { title, credentialMaterials } = attributes; + + if (title === undefined) { + throw SavedObjectsErrorHelpers.createBadRequestError('attribute "title" required'); + } + + this.validateCredentialMaterials(credentialMaterials); + } + + private validateCredentialMaterials(credentialMaterials: T) { + if (credentialMaterials === undefined) { + throw SavedObjectsErrorHelpers.createBadRequestError( + 'attribute "credentialMaterials" required' + ); + } + + const { credentialMaterialsType, credentialMaterialsContent } = credentialMaterials; + + if (credentialMaterialsType === undefined) { + throw SavedObjectsErrorHelpers.createBadRequestError( + 'attribute "credentialMaterialsType" required for "credentialMaterials"' + ); + } + + if (credentialMaterialsContent === undefined) { + throw SavedObjectsErrorHelpers.createBadRequestError( + 'attribute "credentialMaterialsContent" required for "credentialMaterials"' + ); + } + } + + 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(attributes: T) { + const { credentialMaterials } = attributes; + + const { credentialMaterialsType, credentialMaterialsContent } = credentialMaterials; + + switch (credentialMaterialsType) { + case Credential.CredentialMaterialsType.UsernamePasswordType: + this.validateUsernamePasswordTypedContent(credentialMaterialsContent); + return { + ...attributes, + credentialMaterials: await this.encryptUsernamePasswordTypedCredentialMaterials( + credentialMaterials + ), + }; + default: + throw SavedObjectsErrorHelpers.createBadRequestError( + `Invalid credential materials type: '${credentialMaterialsType}'` + ); + } + } + + private async encryptUsernamePasswordTypedCredentialMaterials( + credentialMaterials: T + ) { + const { credentialMaterialsType, credentialMaterialsContent } = credentialMaterials; + return { + credentialMaterialsType, + credentialMaterialsContent: { + username: credentialMaterialsContent.username, + password: await this.cryptographyClient.encryptAndEncode( + credentialMaterialsContent.password + ), + }, + }; + } +} diff --git a/src/plugins/data_source/server/saved_objects/credential_saved_objects_type.ts b/src/plugins/data_source/server/saved_objects/credential_saved_objects_type.ts index 644592206d95..f98b061f6966 100644 --- a/src/plugins/data_source/server/saved_objects/credential_saved_objects_type.ts +++ b/src/plugins/data_source/server/saved_objects/credential_saved_objects_type.ts @@ -30,7 +30,6 @@ export const credential: SavedObjectsType = { dynamic: false, properties: { title: { type: 'text' }, - authType: { type: 'keyword' }, }, }, migrations: {}, 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';