Skip to content

Commit

Permalink
Merge branch '2.x' into backport/backport-6395-to-2.x
Browse files Browse the repository at this point in the history
  • Loading branch information
BionIT authored Jun 5, 2024
2 parents 55423ff + 6aded08 commit 3aaa87b
Show file tree
Hide file tree
Showing 12 changed files with 183 additions and 146 deletions.
2 changes: 2 additions & 0 deletions changelogs/fragments/6770.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
security:
- [CVE-2024-33883] Bump ejs from `3.1.7` to `3.1.10` ([#6770](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6770))
2 changes: 2 additions & 0 deletions changelogs/fragments/6899.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
feat:
- Remove endpoint validation for create data source saved object API ([#6899](https://github.com/opensearch-project/OpenSearch-Dashboards/pull/6899))
2 changes: 2 additions & 0 deletions changelogs/fragments/6928.yml
Original file line number Diff line number Diff line change
@@ -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))
3 changes: 2 additions & 1 deletion config/opensearch_dashboards.yml
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,7 @@
# savedObjects.permission.enabled: true

# Set the value to true to enable workspace feature
# Please note, workspace will not work with multi-tenancy. To enable workspace feature, you need to disable multi-tenancy first with `opensearch_security.multitenancy.enabled: false`
# workspace.enabled: false

# Optional settings to specify saved object types to be deleted during migration.
Expand All @@ -338,4 +339,4 @@
# Set the backend roles in groups or users, whoever has the backend roles or exactly match the user ids defined in this config will be regard as dashboard admin.
# Dashboard admin will have the access to all the workspaces(workspace.enabled: true) and objects inside OpenSearch Dashboards.
# opensearchDashboards.dashboardAdmin.groups: ["dashboard_admin"]
# opensearchDashboards.dashboardAdmin.users: ["dashboard_admin"]
# opensearchDashboards.dashboardAdmin.users: ["dashboard_admin"]
2 changes: 1 addition & 1 deletion packages/osd-plugin-generator/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"dependencies": {
"@osd/cross-platform": "1.0.0",
"@osd/dev-utils": "1.0.0",
"ejs": "^3.1.7",
"ejs": "^3.1.10",
"execa": "^4.0.2",
"inquirer": "^7.3.3",
"normalize-path": "^3.0.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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<SavedObject<unknown>>((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]),
]);
}
16 changes: 0 additions & 16 deletions src/plugins/data_source/server/data_source_service.mock.ts

This file was deleted.

24 changes: 11 additions & 13 deletions src/plugins/data_source/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,26 +61,16 @@ export class DataSourcePlugin implements Plugin<DataSourcePluginSetup, DataSourc
const cryptographyServiceSetup: CryptographyServiceSetup = this.cryptographyService.setup(
config
);
const dataSourceServiceSetup: DataSourceServiceSetup = await this.dataSourceService.setup(
config
);

const authRegistryPromise = core.getStartServices().then(([, , selfStart]) => {
const dataSourcePluginStart = selfStart as DataSourcePluginStart;
return dataSourcePluginStart.getAuthenticationMethodRegistry();
});
const auditTrailPromise = core.getStartServices().then(([coreStart]) => coreStart.auditTrail);
const customApiSchemaRegistryPromise = core.getStartServices().then(([, , selfStart]) => {
const dataSourcePluginStart = selfStart as DataSourcePluginStart;
return dataSourcePluginStart.getCustomApiSchemaRegistry();
});

const dataSourceSavedObjectsClientWrapper = new DataSourceSavedObjectsClientWrapper(
dataSourceServiceSetup,
cryptographyServiceSetup,
this.logger.get('data-source-saved-objects-client-wrapper-factory'),
authRegistryPromise,
customApiSchemaRegistryPromise,
config.endpointDeniedIPs
);

Expand Down Expand Up @@ -114,12 +104,20 @@ export class DataSourcePlugin implements Plugin<DataSourcePluginSetup, DataSourc
},
};
core.auditTrail.register(auditorFactory);
const auditTrailPromise = core.getStartServices().then(([coreStart]) => coreStart.auditTrail);

const dataSourceService: DataSourceServiceSetup = await this.dataSourceService.setup(config);

const customApiSchemaRegistryPromise = core.getStartServices().then(([, , selfStart]) => {
const dataSourcePluginStart = selfStart as DataSourcePluginStart;
return dataSourcePluginStart.getCustomApiSchemaRegistry();
});

// Register data source plugin context to route handler context
core.http.registerRouteHandlerContext(
'dataSource',
this.createDataSourceRouteHandlerContext(
dataSourceServiceSetup,
dataSourceService,
cryptographyServiceSetup,
this.logger,
auditTrailPromise,
Expand All @@ -131,14 +129,14 @@ export class DataSourcePlugin implements Plugin<DataSourcePluginSetup, DataSourc
const router = core.http.createRouter();
registerTestConnectionRoute(
router,
dataSourceServiceSetup,
dataSourceService,
cryptographyServiceSetup,
authRegistryPromise,
customApiSchemaRegistryPromise
);
registerFetchDataSourceMetaDataRoute(
router,
dataSourceServiceSetup,
dataSourceService,
cryptographyServiceSetup,
authRegistryPromise,
customApiSchemaRegistryPromise
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,6 @@ import { AuthType } from '../../common/data_sources';
import { cryptographyServiceSetupMock } from '../cryptography_service.mocks';
import { DataSourceSavedObjectsClientWrapper } from './data_source_saved_objects_client_wrapper';
import { SavedObject } from 'opensearch-dashboards/public';
import { dataSourceServiceSetupMock } from '../data_source_service.mock';
import { CustomApiSchemaRegistry } from '../schema_registry';
import { DataSourceConnectionValidator } from '../routes/data_source_connection_validator';
import { DATA_SOURCE_TITLE_LENGTH_LIMIT } from '../util/constants';

describe('DataSourceSavedObjectsClientWrapper', () => {
Expand All @@ -37,23 +34,16 @@ describe('DataSourceSavedObjectsClientWrapper', () => {
const cryptographyMock = cryptographyServiceSetupMock.create();
const logger = loggingSystemMock.createLogger();
const authRegistryPromise = Promise.resolve(authRegistry);
const customApiSchemaRegistry = new CustomApiSchemaRegistry();
const customApiSchemaRegistryPromise = Promise.resolve(customApiSchemaRegistry);
const dataSourceServiceSetup = dataSourceServiceSetupMock.create();
const requestMock = httpServerMock.createOpenSearchDashboardsRequest();
const wrapperInstance = new DataSourceSavedObjectsClientWrapper(
dataSourceServiceSetup,
cryptographyMock,
logger,
authRegistryPromise,
customApiSchemaRegistryPromise
authRegistryPromise
);

const mockedClient = savedObjectsClientMock.create();
const wrapperClient = wrapperInstance.wrapperFactory({
client: mockedClient,
typeRegistry: requestHandlerContext.savedObjects.typeRegistry,
request: requestMock,
request: httpServerMock.createOpenSearchDashboardsRequest(),
});

const getSavedObject = (savedObject: Partial<SavedObject>) => {
Expand All @@ -80,17 +70,13 @@ describe('DataSourceSavedObjectsClientWrapper', () => {
describe('createWithCredentialsEncryption', () => {
beforeEach(() => {
mockedClient.create.mockClear();
jest
.spyOn(DataSourceConnectionValidator.prototype, 'validate')
.mockResolvedValue(Promise.resolve() as Promise<any>);
});
it('should create data source when auth type is NO_AUTH', async () => {
const mockDataSourceAttributesWithNoAuth = attributes({
auth: {
type: AuthType.NoAuth,
},
});

await wrapperClient.create(
DATA_SOURCE_SAVED_OBJECT_TYPE,
mockDataSourceAttributesWithNoAuth,
Expand Down Expand Up @@ -205,23 +191,6 @@ describe('DataSourceSavedObjectsClientWrapper', () => {
).rejects.toThrowError(`Invalid auth type: 'not_in_registry': Bad Request`);
});

it('endpoint validator datasource client should be created with request as param', async () => {
const mockDataSourceAttributesWithNoAuth = attributes({
auth: {
type: AuthType.NoAuth,
},
});

await wrapperClient.create(
DATA_SOURCE_SAVED_OBJECT_TYPE,
mockDataSourceAttributesWithNoAuth,
{}
);
expect(dataSourceServiceSetup.getDataSourceClient).toBeCalledWith(
expect.objectContaining({ request: requestMock })
);
});

describe('createWithCredentialsEncryption: Error handling', () => {
it('should throw error when title is empty', async () => {
const mockDataSourceAttributes = attributes({
Expand Down Expand Up @@ -252,23 +221,6 @@ describe('DataSourceSavedObjectsClientWrapper', () => {
).rejects.toThrowError(`"endpoint" attribute is not valid or allowed`);
});

it('should throw error when endpoint is not valid OpenSearch endpoint', async () => {
const mockDataSourceAttributes = attributes({
auth: {
type: AuthType.NoAuth,
},
});
jest
.spyOn(DataSourceConnectionValidator.prototype, 'validate')
.mockImplementationOnce(() => {
throw new Error();
});

await expect(
wrapperClient.create(DATA_SOURCE_SAVED_OBJECT_TYPE, mockDataSourceAttributes, {})
).rejects.toThrowError(`endpoint is not valid OpenSearch endpoint: Bad Request`);
});

it('should throw error when auth is not present', async () => {
await expect(
wrapperClient.create(DATA_SOURCE_SAVED_OBJECT_TYPE, attributes(), {})
Expand Down
Loading

0 comments on commit 3aaa87b

Please sign in to comment.