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

Support creating report for saved objects with custom id #283

Merged
merged 3 commits into from
Jan 6, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 All @@ -57,7 +57,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 @@ -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.core.elasticsearch.legacy.client,
reportDefinition
);
} catch (error) {
logger.error(
`Failed input validation for create report definition ${error}`
Expand Down Expand Up @@ -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.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`);
}
};