Skip to content
This repository has been archived by the owner on Aug 9, 2022. It is now read-only.

Commit

Permalink
Support creating report for saved objects with custom id (#283)
Browse files Browse the repository at this point in the history
  • Loading branch information
zhongnansu committed Jan 7, 2021
1 parent c147b30 commit 9cca216
Show file tree
Hide file tree
Showing 4 changed files with 205 additions and 6 deletions.
7 changes: 5 additions & 2 deletions kibana-reports/server/routes/report.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,14 @@ 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 {
backendToUiReport,
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)
Expand Down Expand Up @@ -58,7 +58,10 @@ 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.core.elasticsearch.legacy.client,
report
);
} catch (error) {
logger.error(`Failed input validation for create report ${error}`);
addToMetric('report', 'create', 'user_error');
Expand Down
12 changes: 9 additions & 3 deletions kibana-reports/server/routes/reportDefinition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
Expand All @@ -54,7 +54,10 @@ export default function (router: IRouter) {
try {
reportDefinition.report_params.core_params.origin =
request.headers.origin;
reportDefinition = reportDefinitionSchema.validate(reportDefinition);
reportDefinition = await validateReportDefinition(
context.core.elasticsearch.legacy.client,
reportDefinition
);
} catch (error) {
logger.error(
`Failed input validation for create report definition ${error}`
Expand Down Expand Up @@ -109,7 +112,10 @@ export default function (router: IRouter) {
try {
reportDefinition.report_params.core_params.origin =
request.headers.origin;
reportDefinition = reportDefinitionSchema.validate(reportDefinition);
reportDefinition = await validateReportDefinition(
context.core.elasticsearch.legacy.client,
reportDefinition
);
} catch (error) {
logger.error(
`Failed input validation for update report definition ${error}`
Expand Down
111 changes: 111 additions & 0 deletions kibana-reports/server/utils/__tests__/validationHelper.test.ts
Original file line number Diff line number Diff line change
@@ -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;
};
81 changes: 80 additions & 1 deletion kibana-reports/server/utils/validationHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,16 @@
* permissions and limitations under the License.
*/

import { RequestParams } from '@elastic/elasticsearch';
import path from 'path';
import { ILegacyScopedClusterClient } from '../../../../src/core/server';
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);
Expand All @@ -32,4 +41,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 (
client: ILegacyScopedClusterClient,
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(client, queryUrl, reportSource);
return report;
};

export const validateReportDefinition = async (
client: ILegacyScopedClusterClient,
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(client, baseUrl, reportSource);
return reportDefinition;
};

const validateSavedObject = async (
client: ILegacyScopedClusterClient,
url: string,
source: REPORT_TYPE
) => {
const getId = (url: string) => {
return url
.split('/')
.pop()
?.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 client.callAsCurrentUser('exists', params);
if (!exist) {
throw Error(`saved object with id ${savedObjectId} does not exist`);
}
};

0 comments on commit 9cca216

Please sign in to comment.