From 316d73a4c95ff24d08085f761f7a94162b8a02d5 Mon Sep 17 00:00:00 2001 From: Zhongnan Su Date: Mon, 4 Jan 2021 17:41:32 -0800 Subject: [PATCH 1/3] validate if saved_object id exists --- kibana-reports/server/routes/report.ts | 4 +- .../server/routes/reportDefinition.ts | 12 ++- .../server/utils/validationHelper.ts | 80 ++++++++++++++++++- 3 files changed, 90 insertions(+), 6 deletions(-) diff --git a/kibana-reports/server/routes/report.ts b/kibana-reports/server/routes/report.ts index 9244d712..a62d12d8 100644 --- a/kibana-reports/server/routes/report.ts +++ b/kibana-reports/server/routes/report.ts @@ -23,7 +23,6 @@ import { } from '../../../../src/core/server'; import { API_PREFIX } from '../../common'; import { createReport } from './lib/createReport'; -import { reportSchema } from '../model'; import { checkErrorType, errorResponse } from './utils/helpers'; import { DEFAULT_MAX_SIZE, DELIVERY_TYPE } from './utils/constants'; import { @@ -31,6 +30,7 @@ import { backendToUiReportsList, } from './utils/converters/backendToUi'; import { addToMetric } from './utils/metricHelper'; +import { validateReport } from '../../server/utils/validationHelper'; export default function (router: IRouter) { // generate report (with provided metadata) @@ -57,7 +57,7 @@ export default function (router: IRouter) { try { report.report_definition.report_params.core_params.origin = request.headers.origin; - report = reportSchema.validate(report); + report = await validateReport(context, report); } catch (error) { logger.error(`Failed input validation for create report ${error}`); addToMetric('report', 'create', 'user_error'); diff --git a/kibana-reports/server/routes/reportDefinition.ts b/kibana-reports/server/routes/reportDefinition.ts index 7ae3c801..24a11179 100644 --- a/kibana-reports/server/routes/reportDefinition.ts +++ b/kibana-reports/server/routes/reportDefinition.ts @@ -21,7 +21,6 @@ import { ILegacyScopedClusterClient, } from '../../../../src/core/server'; import { API_PREFIX } from '../../common'; -import { reportDefinitionSchema } from '../model'; import { checkErrorType, errorResponse } from './utils/helpers'; import { createReportDefinition } from './lib/createReportDefinition'; import { @@ -31,6 +30,7 @@ import { import { updateReportDefinition } from './lib/updateReportDefinition'; import { DEFAULT_MAX_SIZE } from './utils/constants'; import { addToMetric } from './utils/metricHelper'; +import { validateReportDefinition } from '../../server/utils/validationHelper'; export default function (router: IRouter) { // Create report Definition @@ -53,7 +53,10 @@ export default function (router: IRouter) { try { reportDefinition.report_params.core_params.origin = request.headers.origin; - reportDefinition = reportDefinitionSchema.validate(reportDefinition); + reportDefinition = await validateReportDefinition( + context, + reportDefinition + ); } catch (error) { logger.error( `Failed input validation for create report definition ${error}` @@ -108,7 +111,10 @@ export default function (router: IRouter) { try { reportDefinition.report_params.core_params.origin = request.headers.origin; - reportDefinition = reportDefinitionSchema.validate(reportDefinition); + reportDefinition = await validateReportDefinition( + context, + reportDefinition + ); } catch (error) { logger.error( `Failed input validation for update report definition ${error}` diff --git a/kibana-reports/server/utils/validationHelper.ts b/kibana-reports/server/utils/validationHelper.ts index 34cf1f63..fca393f4 100644 --- a/kibana-reports/server/utils/validationHelper.ts +++ b/kibana-reports/server/utils/validationHelper.ts @@ -13,7 +13,15 @@ * permissions and limitations under the License. */ +import { RequestParams } from '@elastic/elasticsearch'; import path from 'path'; +import { + reportDefinitionSchema, + ReportDefinitionSchemaType, + reportSchema, + ReportSchemaType, +} from '../../server/model'; +import { REPORT_TYPE } from '../../server/routes/utils/constants'; export const isValidRelativeUrl = (relativeUrl: string) => { const normalizedRelativeUrl = path.posix.normalize(relativeUrl); @@ -32,4 +40,74 @@ export const isValidRelativeUrl = (relativeUrl: string) => { export const regexDuration = /^(-?)P(?=\d|T\d)(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)([DW]))?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+(?:\.\d+)?)S)?)?$/; export const regexEmailAddress = /\S+@\S+\.\S+/; export const regexReportName = /^[\w\-\s\(\)\[\]\,\_\-+]+$/; -export const regexRelativeUrl = /^\/(_plugin\/kibana\/app|app)\/(dashboards|visualize|discover)#\/(view|edit)\/([a-f0-9-]+)($|\?\S+$)/; +export const regexRelativeUrl = /^\/(_plugin\/kibana\/app|app)\/(dashboards|visualize|discover)#\/(view|edit)\/[^\/]+$/; + +export const validateReport = async ( + context: any, + report: ReportSchemaType +) => { + // validate basic schema + report = reportSchema.validate(report); + // parse to retrieve data + const { + query_url: queryUrl, + report_definition: { + report_params: { report_source: reportSource }, + }, + } = report; + // Check if saved object actually exists + await validateSavedObject(context, queryUrl, reportSource); + return report; +}; + +export const validateReportDefinition = async ( + context: any, + reportDefinition: ReportDefinitionSchemaType +) => { + // validate basic schema + reportDefinition = reportDefinitionSchema.validate(reportDefinition); + // parse to retrieve data + const { + report_params: { + report_source: reportSource, + core_params: { base_url: baseUrl }, + }, + } = reportDefinition; + // Check if saved object actually exists + await validateSavedObject(context, baseUrl, reportSource); + return reportDefinition; +}; + +const validateSavedObject = async ( + context: any, + url: string, + source: REPORT_TYPE +) => { + const getId = (url: string) => { + return url.split('/')[4]?.replace(/\?\S+$/, ''); + }; + const getType = (source: REPORT_TYPE) => { + switch (source) { + case REPORT_TYPE.dashboard: + return 'dashboard'; + case REPORT_TYPE.savedSearch: + return 'search'; + case REPORT_TYPE.visualization: + return 'visualization'; + } + }; + + const savedObjectId = `${getType(source)}:${getId(url)}`; + const params: RequestParams.Exists = { + index: '.kibana', + id: savedObjectId, + }; + + const exist = await context.core.elasticsearch.legacy.client.callAsCurrentUser( + 'exists', + params + ); + if (!exist) { + throw Error(`saved object with id ${savedObjectId} does not exist`); + } +}; From 5eed556757f0d84003071b92315efb7d86734e36 Mon Sep 17 00:00:00 2001 From: Zhongnan Su Date: Tue, 5 Jan 2021 11:07:42 -0800 Subject: [PATCH 2/3] typo --- kibana-reports/server/utils/validationHelper.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/kibana-reports/server/utils/validationHelper.ts b/kibana-reports/server/utils/validationHelper.ts index fca393f4..22eb1f7f 100644 --- a/kibana-reports/server/utils/validationHelper.ts +++ b/kibana-reports/server/utils/validationHelper.ts @@ -84,7 +84,10 @@ const validateSavedObject = async ( source: REPORT_TYPE ) => { const getId = (url: string) => { - return url.split('/')[4]?.replace(/\?\S+$/, ''); + return url + .split('/') + .pop() + ?.replace(/\?\S+$/, ''); }; const getType = (source: REPORT_TYPE) => { switch (source) { From f803b9e4a8c96f378b2663d71db9c9b0241eb649 Mon Sep 17 00:00:00 2001 From: Zhongnan Su Date: Tue, 5 Jan 2021 15:21:10 -0800 Subject: [PATCH 3/3] add unit test --- kibana-reports/server/routes/report.ts | 5 +- .../server/routes/reportDefinition.ts | 4 +- .../utils/__tests__/validationHelper.test.ts | 111 ++++++++++++++++++ .../server/utils/validationHelper.ts | 16 ++- 4 files changed, 124 insertions(+), 12 deletions(-) create mode 100644 kibana-reports/server/utils/__tests__/validationHelper.test.ts diff --git a/kibana-reports/server/routes/report.ts b/kibana-reports/server/routes/report.ts index a62d12d8..fb17aeda 100644 --- a/kibana-reports/server/routes/report.ts +++ b/kibana-reports/server/routes/report.ts @@ -57,7 +57,10 @@ export default function (router: IRouter) { try { report.report_definition.report_params.core_params.origin = request.headers.origin; - report = await validateReport(context, report); + report = await validateReport( + context.core.elasticsearch.legacy.client, + report + ); } catch (error) { logger.error(`Failed input validation for create report ${error}`); addToMetric('report', 'create', 'user_error'); diff --git a/kibana-reports/server/routes/reportDefinition.ts b/kibana-reports/server/routes/reportDefinition.ts index 24a11179..367bae82 100644 --- a/kibana-reports/server/routes/reportDefinition.ts +++ b/kibana-reports/server/routes/reportDefinition.ts @@ -54,7 +54,7 @@ export default function (router: IRouter) { reportDefinition.report_params.core_params.origin = request.headers.origin; reportDefinition = await validateReportDefinition( - context, + context.core.elasticsearch.legacy.client, reportDefinition ); } catch (error) { @@ -112,7 +112,7 @@ export default function (router: IRouter) { reportDefinition.report_params.core_params.origin = request.headers.origin; reportDefinition = await validateReportDefinition( - context, + context.core.elasticsearch.legacy.client, reportDefinition ); } catch (error) { diff --git a/kibana-reports/server/utils/__tests__/validationHelper.test.ts b/kibana-reports/server/utils/__tests__/validationHelper.test.ts new file mode 100644 index 00000000..f01c4cf4 --- /dev/null +++ b/kibana-reports/server/utils/__tests__/validationHelper.test.ts @@ -0,0 +1,111 @@ +/* + * Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +import { ReportDefinitionSchemaType, ReportSchemaType } from '../../model'; +import { + DELIVERY_TYPE, + FORMAT, + REPORT_TYPE, + TRIGGER_TYPE, +} from '../../routes/utils/constants'; +import { validateReport, validateReportDefinition } from '../validationHelper'; + +const SAMPLE_SAVED_OBJECT_ID = '3ba638e0-b894-11e8-a6d9-e546fe2bba5f'; +const createReportDefinitionInput: ReportDefinitionSchemaType = { + report_params: { + report_name: 'test visual report', + report_source: REPORT_TYPE.dashboard, + description: 'Hi this is your Dashboard on demand', + core_params: { + base_url: `/app/dashboards#/view/${SAMPLE_SAVED_OBJECT_ID}`, + window_width: 1300, + window_height: 900, + report_format: FORMAT.pdf, + time_duration: 'PT5M', + origin: 'http://localhost:5601', + }, + }, + delivery: { + delivery_type: DELIVERY_TYPE.kibanaUser, + delivery_params: { + kibana_recipients: [], + }, + }, + trigger: { + trigger_type: TRIGGER_TYPE.onDemand, + }, +}; +const createReportInput: ReportSchemaType = { + query_url: `/app/dashboards#/view/${SAMPLE_SAVED_OBJECT_ID}`, + time_from: 1343576635300, + time_to: 1596037435301, + report_definition: createReportDefinitionInput, +}; + +describe('test input validation', () => { + test('create report with correct saved object id', async () => { + const savedObjectIds = [`dashboard:${SAMPLE_SAVED_OBJECT_ID}`]; + const client = mockEsClient(savedObjectIds); + const report = await validateReport(client, createReportInput); + expect(report).toBeDefined(); + }); + + test('create report with non-exist saved object id', async () => { + const savedObjectIds = ['dashboard:fake-id']; + const client = mockEsClient(savedObjectIds); + await expect( + validateReport(client, createReportInput) + ).rejects.toThrowError( + `saved object with id dashboard:${SAMPLE_SAVED_OBJECT_ID} does not exist` + ); + }); + + test('create report definition with correct saved object id', async () => { + const savedObjectIds = [`dashboard:${SAMPLE_SAVED_OBJECT_ID}`]; + const client = mockEsClient(savedObjectIds); + const report = await validateReportDefinition( + client, + createReportDefinitionInput + ); + expect(report).toBeDefined(); + }); + + test('create report definition with non-exist saved object id', async () => { + const savedObjectIds = ['dashboard:fake-id']; + const client = mockEsClient(savedObjectIds); + await expect( + validateReportDefinition(client, createReportDefinitionInput) + ).rejects.toThrowError( + `saved object with id dashboard:${SAMPLE_SAVED_OBJECT_ID} does not exist` + ); + }); +}); +// TODO: merge this with other mock clients used in testing, to create some mock helpers file +const mockEsClient = (mockSavedObjectIds: string[]) => { + const client = { + callAsCurrentUser: jest + .fn() + .mockImplementation((endpoint: string, params: any) => { + switch (endpoint) { + case 'exists': + return mockSavedObjectIds.includes(params.id); + default: + fail('Fail due to unexpected function call on client'); + } + }), + }; + + return client; +}; diff --git a/kibana-reports/server/utils/validationHelper.ts b/kibana-reports/server/utils/validationHelper.ts index 22eb1f7f..77334d13 100644 --- a/kibana-reports/server/utils/validationHelper.ts +++ b/kibana-reports/server/utils/validationHelper.ts @@ -15,6 +15,7 @@ import { RequestParams } from '@elastic/elasticsearch'; import path from 'path'; +import { ILegacyScopedClusterClient } from '../../../../src/core/server'; import { reportDefinitionSchema, ReportDefinitionSchemaType, @@ -43,7 +44,7 @@ export const regexReportName = /^[\w\-\s\(\)\[\]\,\_\-+]+$/; export const regexRelativeUrl = /^\/(_plugin\/kibana\/app|app)\/(dashboards|visualize|discover)#\/(view|edit)\/[^\/]+$/; export const validateReport = async ( - context: any, + client: ILegacyScopedClusterClient, report: ReportSchemaType ) => { // validate basic schema @@ -56,12 +57,12 @@ export const validateReport = async ( }, } = report; // Check if saved object actually exists - await validateSavedObject(context, queryUrl, reportSource); + await validateSavedObject(client, queryUrl, reportSource); return report; }; export const validateReportDefinition = async ( - context: any, + client: ILegacyScopedClusterClient, reportDefinition: ReportDefinitionSchemaType ) => { // validate basic schema @@ -74,12 +75,12 @@ export const validateReportDefinition = async ( }, } = reportDefinition; // Check if saved object actually exists - await validateSavedObject(context, baseUrl, reportSource); + await validateSavedObject(client, baseUrl, reportSource); return reportDefinition; }; const validateSavedObject = async ( - context: any, + client: ILegacyScopedClusterClient, url: string, source: REPORT_TYPE ) => { @@ -106,10 +107,7 @@ const validateSavedObject = async ( id: savedObjectId, }; - const exist = await context.core.elasticsearch.legacy.client.callAsCurrentUser( - 'exists', - params - ); + const exist = await client.callAsCurrentUser('exists', params); if (!exist) { throw Error(`saved object with id ${savedObjectId} does not exist`); }