diff --git a/changelogs/fragments/6928.yml b/changelogs/fragments/6928.yml new file mode 100644 index 000000000000..66ba2e4d9e86 --- /dev/null +++ b/changelogs/fragments/6928.yml @@ -0,0 +1,2 @@ +feat: +- [MD]Use placeholder for data source credentials fields when export saved object ([#6928](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6928)) \ No newline at end of file diff --git a/src/core/server/saved_objects/export/get_sorted_objects_for_export.test.ts b/src/core/server/saved_objects/export/get_sorted_objects_for_export.test.ts index da477604c029..bfca6f88c5c2 100644 --- a/src/core/server/saved_objects/export/get_sorted_objects_for_export.test.ts +++ b/src/core/server/saved_objects/export/get_sorted_objects_for_export.test.ts @@ -28,7 +28,10 @@ * under the License. */ -import { exportSavedObjectsToStream } from './get_sorted_objects_for_export'; +import { + DATA_SOURCE_CREDENTIALS_PLACEHOLDER, + exportSavedObjectsToStream, +} from './get_sorted_objects_for_export'; import { savedObjectsClientMock } from '../service/saved_objects_client.mock'; import { Readable } from 'stream'; import { createPromiseFromStreams, createConcatStream } from '../../utils/streams'; @@ -706,6 +709,50 @@ describe('getSortedObjectsForExport()', () => { ]); }); + test('modifies return results to update `credentials` of data-source to use placeholder', async () => { + const createDataSourceSavedObject = (id: string, auth: any) => ({ + id, + type: 'data-source', + attributes: { auth }, + references: [], + }); + + const dataSourceNoAuthInfo = { type: 'no_auth' }; + const dataSourceBasicAuthInfo = { + type: 'username_password', + credentials: { username: 'foo', password: 'bar' }, + }; + + const redactedDataSourceBasicAuthInfo = { + type: 'username_password', + credentials: { + username: DATA_SOURCE_CREDENTIALS_PLACEHOLDER, + password: DATA_SOURCE_CREDENTIALS_PLACEHOLDER, + }, + }; + + savedObjectsClient.bulkGet.mockResolvedValueOnce({ + saved_objects: [ + createDataSourceSavedObject('1', dataSourceNoAuthInfo), + createDataSourceSavedObject('2', dataSourceBasicAuthInfo), + ], + }); + const exportStream = await exportSavedObjectsToStream({ + exportSizeLimit: 10000, + savedObjectsClient, + objects: [ + { type: 'data-source', id: '1' }, + { type: 'data-source', id: '2' }, + ], + }); + const response = await readStreamToCompletion(exportStream); + expect(response).toEqual([ + createDataSourceSavedObject('1', dataSourceNoAuthInfo), + createDataSourceSavedObject('2', redactedDataSourceBasicAuthInfo), + expect.objectContaining({ exportedCount: 2 }), + ]); + }); + test('includes nested dependencies when passed in', async () => { savedObjectsClient.bulkGet.mockResolvedValueOnce({ saved_objects: [ diff --git a/src/core/server/saved_objects/export/get_sorted_objects_for_export.ts b/src/core/server/saved_objects/export/get_sorted_objects_for_export.ts index 17e9af3c3e1b..229de09de440 100644 --- a/src/core/server/saved_objects/export/get_sorted_objects_for_export.ts +++ b/src/core/server/saved_objects/export/get_sorted_objects_for_export.ts @@ -34,6 +34,8 @@ import { SavedObjectsClientContract, SavedObject, SavedObjectsBaseOptions } from import { fetchNestedDependencies } from './inject_nested_depdendencies'; import { sortObjects } from './sort_objects'; +export const DATA_SOURCE_CREDENTIALS_PLACEHOLDER = 'pleaseUpdateCredentials'; + /** * Options controlling the export operation. * @public @@ -185,10 +187,40 @@ export async function exportSavedObjectsToStream({ ({ namespaces, ...object }) => object ); + // update the credential fields from "data-source" saved object to use placeholder to avoid exporting sensitive information + const redactedObjectsWithoutCredentials = redactedObjects.map>((object) => { + if (object.type === 'data-source') { + const { auth, ...rest } = object.attributes as { + auth: { type: string; credentials?: any }; + }; + const hasCredentials = auth && auth.credentials; + const updatedCredentials = hasCredentials + ? Object.keys(auth.credentials).reduce((acc, key) => { + acc[key] = DATA_SOURCE_CREDENTIALS_PLACEHOLDER; + return acc; + }, {} as { [key: string]: any }) + : undefined; + return { + ...object, + attributes: { + ...rest, + auth: { + type: auth.type, + ...(hasCredentials && { credentials: updatedCredentials }), + }, + }, + }; + } + return object; + }); + const exportDetails: SavedObjectsExportResultDetails = { exportedCount: exportedObjects.length, missingRefCount: missingReferences.length, missingReferences, }; - return createListStream([...redactedObjects, ...(excludeExportDetails ? [] : [exportDetails])]); + return createListStream([ + ...redactedObjectsWithoutCredentials, + ...(excludeExportDetails ? [] : [exportDetails]), + ]); }