From d204e9c83b07b7b535de06d39a7dd19c5672a314 Mon Sep 17 00:00:00 2001 From: Zhongnan Su Date: Mon, 19 Oct 2020 10:34:24 -0700 Subject: [PATCH 1/8] improve input validation --- .../components/context_menu/context_menu.js | 42 ++-- .../context_menu/context_menu_helpers.js | 85 ++++---- .../public/components/main/main_utils.tsx | 4 +- .../report_definition_details.tsx | 2 +- .../main/report_details/report_details.tsx | 2 +- .../report_settings_helpers.tsx | 6 +- kibana-reports/server/model/index.ts | 75 +++++-- kibana-reports/server/routes/report.ts | 1 + .../utils/__tests__/reportHelper.test.ts | 2 +- kibana-reports/server/routes/utils/helpers.ts | 61 ++++++ .../server/routes/utils/reportHelper.ts | 200 +----------------- kibana-reports/server/routes/utils/types.ts | 15 ++ .../server/routes/utils/visualReportHelper.ts | 149 +++++++++++++ kibana-reports/server/utils/constants.ts | 2 +- kibana-reports/server/utils/executor.ts | 13 +- .../server/utils/validationHelper.ts | 36 ++++ 16 files changed, 399 insertions(+), 296 deletions(-) create mode 100644 kibana-reports/server/routes/utils/visualReportHelper.ts create mode 100644 kibana-reports/server/utils/validationHelper.ts diff --git a/kibana-reports/public/components/context_menu/context_menu.js b/kibana-reports/public/components/context_menu/context_menu.js index 0e980e58..9a4b634e 100644 --- a/kibana-reports/public/components/context_menu/context_menu.js +++ b/kibana-reports/public/components/context_menu/context_menu.js @@ -24,19 +24,20 @@ import { addSuccessOrFailureToast, contextMenuViewReports, } from './context_menu_helpers'; -import { popoverMenu, popoverMenuDiscover, getMenuItem } from './context_menu_ui'; +import { + popoverMenu, + popoverMenuDiscover, + getMenuItem, +} from './context_menu_ui'; const replaceQueryURL = () => { - let url = window.location.href; + let url = location.pathname + location.hash; let timeString = url.substring( url.lastIndexOf('time:'), url.lastIndexOf('))') ); - if (url.includes("visualize") || url.includes("discover")) { - timeString = url.substring( - url.lastIndexOf("time:"), - url.indexOf("))") - ); + if (url.includes('visualize') || url.includes('discover')) { + timeString = url.substring(url.lastIndexOf('time:'), url.indexOf('))')); } let fromDateString = timeString.substring( @@ -68,18 +69,20 @@ const replaceQueryURL = () => { return url; }; -const generateInContextReport = (timeRanges, queryUrl, fileFormat, rest = {}) => { +const generateInContextReport = ( + timeRanges, + queryUrl, + fileFormat, + rest = {} +) => { displayLoadingModal(); - let baseUrl = window.location.href.substr( - 0, - window.location.href.indexOf('?') - ); + const baseUrl = queryUrl.substr(0, queryUrl.indexOf('?')); let reportSource = ''; - if (window.location.href.includes('dashboard')) { + if (baseUrl.includes('dashboard')) { reportSource = 'Dashboard'; - } else if (window.location.href.includes('visualize')) { + } else if (baseUrl.includes('visualize')) { reportSource = 'Visualization'; - } else if (window.location.href.includes('discover')) { + } else if (baseUrl.includes('discover')) { reportSource = 'Saved search'; } @@ -150,7 +153,9 @@ $(function () { if (popoverScreen) { try { const reportPopover = document.createElement('div'); - reportPopover.innerHTML = isDiscover() ? popoverMenuDiscover(getUuidFromUrl()) : popoverMenu(); + reportPopover.innerHTML = isDiscover() + ? popoverMenuDiscover(getUuidFromUrl()) + : popoverMenu(); popoverScreen[0].appendChild(reportPopover.children[0]); $('#reportPopover').show(); } catch (e) { @@ -254,7 +259,10 @@ function locationHashChanged() { } // try to match uuid followed by '?' in URL, which would be the saved search id for discover URL -const getUuidFromUrl = () => window.location.href.match(/(\b[0-9a-f]{8}\b-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-\b[0-9a-f]{12}\b)\?/); +const getUuidFromUrl = () => + window.location.href.match( + /(\b[0-9a-f]{8}\b-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-\b[0-9a-f]{12}\b)\?/ + ); const isDiscover = () => window.location.href.includes('discover'); window.onhashchange = function () { diff --git a/kibana-reports/public/components/context_menu/context_menu_helpers.js b/kibana-reports/public/components/context_menu/context_menu_helpers.js index 963f849d..b7447ac2 100644 --- a/kibana-reports/public/components/context_menu/context_menu_helpers.js +++ b/kibana-reports/public/components/context_menu/context_menu_helpers.js @@ -18,67 +18,63 @@ import moment from 'moment'; import { reportGenerationInProgressModal, reportGenerationSuccess, - reportGenerationFailure + reportGenerationFailure, } from './context_menu_ui'; const getReportSourceURL = (baseURI) => { - let url = baseURI.substr(0, baseURI.indexOf("?")); - const reportSourceId = url.substr(url.lastIndexOf("/") + 1, url.length); + let url = baseURI.substr(0, baseURI.indexOf('?')); + const reportSourceId = url.substr(url.lastIndexOf('/') + 1, url.length); return reportSourceId; -} +}; export const contextMenuCreateReportDefinition = (baseURI) => { const reportSourceId = getReportSourceURL(baseURI); - let reportSource = ""; + let reportSource = ''; let timeRanges = getTimeFieldsFromUrl(); // check report source if (baseURI.includes('dashboard')) { reportSource = 'dashboard:'; - } - else if (baseURI.includes('visualize')) { - reportSource = 'visualize:' - } - else if (baseURI.includes('discover')) { + } else if (baseURI.includes('visualize')) { + reportSource = 'visualize:'; + } else if (baseURI.includes('discover')) { reportSource = 'discover:'; } reportSource += reportSourceId.toString(); window.location.assign( `opendistro_kibana_reports#/create?previous=${reportSource}?timeFrom=${timeRanges.time_from.toISOString()}?timeTo=${timeRanges.time_to.toISOString()}` - ) -} + ); +}; -export const contextMenuViewReports = () => window.location.assign('opendistro_kibana_reports#/'); +export const contextMenuViewReports = () => + window.location.assign('opendistro_kibana_reports#/'); export const getTimeFieldsFromUrl = () => { let url = window.location.href; let timeString = url.substring( - url.lastIndexOf("time:"), - url.lastIndexOf("))") - ); - if (url.includes("visualize") || url.includes("discover")) { - timeString = url.substring( - url.lastIndexOf("time:"), - url.indexOf("))") - ); + url.lastIndexOf('time:'), + url.lastIndexOf('))') + ); + if (url.includes('visualize') || url.includes('discover')) { + timeString = url.substring(url.lastIndexOf('time:'), url.indexOf('))')); } let fromDateString = timeString.substring( - timeString.lastIndexOf("from:") + 5, - timeString.lastIndexOf(",") + timeString.lastIndexOf('from:') + 5, + timeString.lastIndexOf(',') ); - + // remove extra quotes if the 'from' date is absolute time fromDateString = fromDateString.replace(/[']+/g, ''); - + // convert time range to from date format in case time range is relative let fromDateFormat = dateMath.parse(fromDateString); let toDateString = timeString.substring( - timeString.lastIndexOf("to:") + 3, + timeString.lastIndexOf('to:') + 3, timeString.length ); - + toDateString = toDateString.replace(/[']+/g, ''); let toDateFormat = dateMath.parse(toDateString); @@ -89,39 +85,42 @@ export const getTimeFieldsFromUrl = () => { return { time_from: fromDateFormat, time_to: toDateFormat, - time_duration: timeDuration.toISOString() - } -} + time_duration: timeDuration.toISOString(), + }; +}; export const displayLoadingModal = () => { - const kibanaBody = document.getElementById("kibana-body"); + const kibanaBody = document.getElementById('kibana-body'); if (kibanaBody) { try { - const loadingModal = document.createElement("div"); + const loadingModal = document.createElement('div'); loadingModal.innerHTML = reportGenerationInProgressModal(); kibanaBody.appendChild(loadingModal.children[0]); } catch (e) { - console.log("error displaying loading modal:", e); + console.log('error displaying loading modal:', e); } } -} +}; export const addSuccessOrFailureToast = (status) => { - const generateToast = document.querySelectorAll(".euiGlobalToastList"); + const generateToast = document.querySelectorAll('.euiGlobalToastList'); if (generateToast) { try { - const generateInProgressToast = document.createElement("div"); - if (status === "success") { + const generateInProgressToast = document.createElement('div'); + if (status === 'success') { generateInProgressToast.innerHTML = reportGenerationSuccess(); - setTimeout(function () {document.getElementById('reportSuccessToast').style.display='none'}, 6000); // closes toast automatically after 6s - } - else if (status === "failure") { + setTimeout(function () { + document.getElementById('reportSuccessToast').style.display = 'none'; + }, 6000); // closes toast automatically after 6s + } else if (status === 'failure') { generateInProgressToast.innerHTML = reportGenerationFailure(); - setTimeout(function () {document.getElementById('reportFailureToast').style.display='none'}, 6000); + setTimeout(function () { + document.getElementById('reportFailureToast').style.display = 'none'; + }, 6000); } generateToast[0].appendChild(generateInProgressToast.children[0]); } catch (e) { - console.log("error displaying toast", e); + console.log('error displaying toast', e); } } -} \ No newline at end of file +}; diff --git a/kibana-reports/public/components/main/main_utils.tsx b/kibana-reports/public/components/main/main_utils.tsx index f1828031..ed7854be 100644 --- a/kibana-reports/public/components/main/main_utils.tsx +++ b/kibana-reports/public/components/main/main_utils.tsx @@ -63,7 +63,7 @@ export const addReportsTableContent = (data) => { //TODO: wrong name timeCreated: report.time_created, state: report.state, - url: report.query_url, + url: `${location.host}${report.query_url}`, format: reportParams.core_params.report_format, }; reportsTableItems.push(reportsTableEntry); @@ -85,7 +85,7 @@ export const addReportDefinitionsTableContent = (data: any) => { type: trigger.trigger_type, owner: `\u2014`, // Todo: replace source: reportParams.report_source, - baseUrl: reportParams.core_params.base_url, + baseUrl: `${location.host}${reportParams.core_params.base_url}`, lastUpdated: reportDefinition.last_updated, details: trigger.trigger_type === 'On demand' diff --git a/kibana-reports/public/components/main/report_definition_details/report_definition_details.tsx b/kibana-reports/public/components/main/report_definition_details/report_definition_details.tsx index d9398ea4..2262be4e 100644 --- a/kibana-reports/public/components/main/report_definition_details/report_definition_details.tsx +++ b/kibana-reports/public/components/main/report_definition_details/report_definition_details.tsx @@ -283,7 +283,7 @@ export function ReportDefinitionDetails(props) { const sourceURL = (data) => { return ( - + {data['source']} ); diff --git a/kibana-reports/public/components/main/report_details/report_details.tsx b/kibana-reports/public/components/main/report_details/report_details.tsx index 48ba9d63..00f96e09 100644 --- a/kibana-reports/public/components/main/report_details/report_details.tsx +++ b/kibana-reports/public/components/main/report_details/report_details.tsx @@ -189,7 +189,7 @@ export function ReportDetails(props) { const sourceURL = (data) => { return ( - + {data['source']} ); diff --git a/kibana-reports/public/components/report_definitions/report_settings/report_settings_helpers.tsx b/kibana-reports/public/components/report_definitions/report_settings/report_settings_helpers.tsx index 99f47dbf..189320a1 100644 --- a/kibana-reports/public/components/report_definitions/report_settings/report_settings_helpers.tsx +++ b/kibana-reports/public/components/report_definitions/report_settings/report_settings_helpers.tsx @@ -29,7 +29,7 @@ export const getDashboardBaseUrlCreate = ( edit: boolean, editDefinitionId: string ) => { - let baseUrl = window.location.href; + let baseUrl = location.pathname + location.hash; if (edit) { return baseUrl.replace( `opendistro_kibana_reports#/edit/${editDefinitionId}`, @@ -43,7 +43,7 @@ export const getDashboardBaseUrlCreate = ( }; export const getVisualizationBaseUrlCreate = (edit: boolean) => { - let baseUrl = window.location.href; + let baseUrl = location.pathname + location.hash; if (edit) { return baseUrl.replace( 'opendistro_kibana_reports#/edit', @@ -57,7 +57,7 @@ export const getVisualizationBaseUrlCreate = (edit: boolean) => { }; export const getSavedSearchBaseUrlCreate = (edit: boolean) => { - let baseUrl = window.location.href; + let baseUrl = location.pathname + location.hash; if (edit) { return baseUrl.replace( 'opendistro_kibana_reports#/edit', diff --git a/kibana-reports/server/model/index.ts b/kibana-reports/server/model/index.ts index 373982b3..8d91f70a 100644 --- a/kibana-reports/server/model/index.ts +++ b/kibana-reports/server/model/index.ts @@ -14,6 +14,8 @@ */ import { schema, TypeOf } from '@kbn/config-schema'; +import { isValidDuration, isValidRelativeUrl } from '../utils/validationHelper'; +import { isValidCron } from 'cron-validator'; import { REPORT_TYPE, TRIGGER_TYPE, @@ -27,32 +29,56 @@ import { } from '../routes/utils/constants'; export const dataReportSchema = schema.object({ - base_url: schema.uri(), + base_url: schema.string({ + validate(value) { + if (!isValidRelativeUrl(value)) { + return `invalid relative url: ${value}`; + } + }, + }), saved_search_id: schema.string(), //ISO duration format. 'PT10M' means 10 min - time_duration: schema.string(), + time_duration: schema.string({ + validate(value) { + if (!isValidDuration(value)) { + return `invalid time duration: ${value}`; + } + }, + }), //TODO: future support schema.literal('xlsx') report_format: schema.oneOf([schema.literal(FORMAT.csv)]), - limit: schema.number({ defaultValue: DEFAULT_MAX_SIZE }), + limit: schema.number({ defaultValue: DEFAULT_MAX_SIZE, min: 0 }), excel: schema.boolean({ defaultValue: true }), }); const visualReportSchema = schema.object({ - base_url: schema.uri(), - window_width: schema.number({ defaultValue: 1200 }), - window_height: schema.number({ defaultValue: 800 }), + base_url: schema.string({ + validate(value) { + if (!isValidRelativeUrl(value)) { + return `invalid relative url: ${value}`; + } + }, + }), + window_width: schema.number({ defaultValue: 1200, min: 0 }), + window_height: schema.number({ defaultValue: 800, min: 0 }), report_format: schema.oneOf([ schema.literal(FORMAT.pdf), schema.literal(FORMAT.png), ]), header: schema.maybe(schema.string()), footer: schema.maybe(schema.string()), - time_duration: schema.string(), + time_duration: schema.string({ + validate(value) { + if (!isValidDuration(value)) { + return `invalid time duration: ${value}`; + } + }, + }), }); export const intervalSchema = schema.object({ interval: schema.object({ - period: schema.number(), + period: schema.number({ min: 0 }), // Refer to job scheduler SPI https://github.com/opendistro-for-elasticsearch/job-scheduler/blob/b333469c183a15ddbf496a690300cc9e34d937fb/spi/src/main/java/com/amazon/opendistroforelasticsearch/jobscheduler/spi/schedule/IntervalSchedule.java unit: schema.oneOf([ schema.literal('MINUTES'), @@ -66,7 +92,14 @@ export const intervalSchema = schema.object({ export const cronSchema = schema.object({ cron: schema.object({ - expression: schema.string(), + expression: schema.string({ + validate(value) { + if (!isValidCron(value)) { + return `invalid cron expression: ${value}`; + } + }, + }), + //TODO: add more validation once we add full support of timezone timezone: schema.string(), }), }); @@ -74,7 +107,7 @@ export const cronSchema = schema.object({ export const scheduleSchema = schema.object({ schedule_type: schema.oneOf([ /* - TODO: Future Date will be added in the future. + TODO: Future Date option will be added in the future. Currently @kbn/config-schema has no support for more than 2 conditions, keep an eye on library update */ schema.literal(SCHEDULE_TYPE.recurring), @@ -96,9 +129,9 @@ export const kibanaUserSchema = schema.object({ export const channelSchema = schema.object({ recipients: schema.arrayOf(schema.string(), { minSize: 1 }), - title: schema.string(), - textDescription: schema.string(), - htmlDescription: schema.maybe(schema.string()), + title: schema.string({ minLength: 1 }), + textDescription: schema.string({ minLength: 1 }), + htmlDescription: schema.maybe(schema.string({ minLength: 1 })), email_format: schema.oneOf([ schema.literal(EMAIL_FORMAT.attachment), schema.literal(EMAIL_FORMAT.embeddedHtml), @@ -121,13 +154,13 @@ export const deliverySchema = schema.object({ export const reportDefinitionSchema = schema.object({ report_params: schema.object({ - report_name: schema.string(), + report_name: schema.string({ minLength: 1 }), report_source: schema.oneOf([ schema.literal(REPORT_TYPE.dashboard), schema.literal(REPORT_TYPE.visualization), schema.literal(REPORT_TYPE.savedSearch), ]), - description: schema.string(), + description: schema.string({ minLength: 1 }), core_params: schema.conditional( schema.siblingRef('report_source'), REPORT_TYPE.savedSearch, @@ -135,9 +168,7 @@ export const reportDefinitionSchema = schema.object({ visualReportSchema ), }), - delivery: deliverySchema, - trigger: schema.object({ trigger_type: schema.oneOf([ /* @@ -166,8 +197,14 @@ export const reportDefinitionSchema = schema.object({ }); export const reportSchema = schema.object({ - query_url: schema.uri(), - time_from: schema.number(), + query_url: schema.string({ + validate(value) { + if (!isValidRelativeUrl(value)) { + return `this is invalid relative url: ${value}`; + } + }, + }), + time_from: schema.number({ min: 1 }), time_to: schema.number(), report_definition: reportDefinitionSchema, diff --git a/kibana-reports/server/routes/report.ts b/kibana-reports/server/routes/report.ts index 3ddef29f..67a500f8 100644 --- a/kibana-reports/server/routes/report.ts +++ b/kibana-reports/server/routes/report.ts @@ -30,6 +30,7 @@ import { CONFIG_INDEX_NAME, DEFAULT_MAX_SIZE, DELIVERY_TYPE, + LOCAL_HOST, } from './utils/constants'; export default function (router: IRouter) { diff --git a/kibana-reports/server/routes/utils/__tests__/reportHelper.test.ts b/kibana-reports/server/routes/utils/__tests__/reportHelper.test.ts index 08d6384a..73cf1619 100644 --- a/kibana-reports/server/routes/utils/__tests__/reportHelper.test.ts +++ b/kibana-reports/server/routes/utils/__tests__/reportHelper.test.ts @@ -14,7 +14,7 @@ */ import 'regenerator-runtime/runtime'; -import { createVisualReport } from '../reportHelper'; +import { createVisualReport } from '../visualReportHelper'; describe('test create visual report', () => { test('create png report', async () => { diff --git a/kibana-reports/server/routes/utils/helpers.ts b/kibana-reports/server/routes/utils/helpers.ts index 4a21fc6c..6e27821e 100644 --- a/kibana-reports/server/routes/utils/helpers.ts +++ b/kibana-reports/server/routes/utils/helpers.ts @@ -19,6 +19,10 @@ import { ILegacyClusterClient, ILegacyScopedClusterClient, } from '../../../../../src/core/server'; +import { RequestParams } from '@elastic/elasticsearch'; +import { CONFIG_INDEX_NAME, REPORT_STATE } from './constants'; +import { CreateReportResultType } from './types'; +import { ReportSchemaType } from 'server/model'; export function parseEsErrorResponse(error: any) { if (error.response) { @@ -68,3 +72,60 @@ export const callCluster = async ( } return esResp; }; + +export const saveToES = async ( + isScheduledTask: boolean, + report: ReportSchemaType, + esClient: ILegacyClusterClient | ILegacyScopedClusterClient +) => { + const timePending = Date.now(); + const saveParams: RequestParams.Index = { + index: CONFIG_INDEX_NAME.report, + body: { + ...report, + state: REPORT_STATE.pending, + last_updated: timePending, + time_created: timePending, + }, + }; + const esResp = await callCluster( + esClient, + 'index', + saveParams, + isScheduledTask + ); + + return esResp; +}; + +export const updateToES = async ( + isScheduledTask: boolean, + reportId: string, + esClient: ILegacyClusterClient | ILegacyScopedClusterClient, + state: string, + createReportResult?: CreateReportResultType +) => { + const timeStamp = createReportResult + ? createReportResult.timeCreated + : Date.now(); + // update report document with state "created" or "error" + const updateParams: RequestParams.Update = { + id: reportId, + index: CONFIG_INDEX_NAME.report, + body: { + doc: { + state: state, + last_updated: timeStamp, + time_created: timeStamp, + }, + }, + }; + const esResp = await callCluster( + esClient, + 'update', + updateParams, + isScheduledTask + ); + + return esResp; +}; diff --git a/kibana-reports/server/routes/utils/reportHelper.ts b/kibana-reports/server/routes/utils/reportHelper.ts index 7c172150..40ab77be 100644 --- a/kibana-reports/server/routes/utils/reportHelper.ts +++ b/kibana-reports/server/routes/utils/reportHelper.ts @@ -13,22 +13,15 @@ * permissions and limitations under the License. */ -import puppeteer from 'puppeteer'; -import createDOMPurify from 'dompurify'; -import { JSDOM } from 'jsdom'; import { FORMAT, REPORT_TYPE, REPORT_STATE, - CONFIG_INDEX_NAME, LOCAL_HOST, DELIVERY_TYPE, EMAIL_FORMAT, - DEFAULT_REPORT_FOOTER, - DEFAULT_REPORT_HEADER, } from './constants'; -import { RequestParams } from '@elastic/elasticsearch'; -import { getFileName, callCluster } from './helpers'; +import { callCluster, updateToES, saveToES } from './helpers'; import { ILegacyClusterClient, ILegacyScopedClusterClient, @@ -37,123 +30,7 @@ import { import { createSavedSearchReport } from './savedSearchReportHelper'; import { ReportSchemaType } from '../../model'; import { CreateReportResultType } from './types'; - -const window = new JSDOM('').window; -const DOMPurify = createDOMPurify(window); - -export const createVisualReport = async ( - reportParams: any, - queryUrl: string, - logger: Logger -): Promise => { - const coreParams = reportParams.core_params; - // parse params - const reportSource = reportParams.report_source; - const reportName = reportParams.report_name; - const windowWidth = coreParams.window_width; - const windowHeight = coreParams.window_height; - const reportFormat = coreParams.report_format; - - // TODO: polish default header, maybe add a logo, depends on UX design - const header = coreParams.header ? DOMPurify.sanitize(coreParams.header) : DEFAULT_REPORT_HEADER; - const footer = coreParams.footer ? DOMPurify.sanitize(coreParams.footer) : DEFAULT_REPORT_FOOTER; - // set up puppeteer - const browser = await puppeteer.launch({ - headless: true, - /** - * TODO: temp fix to disable sandbox when launching chromium on Linux instance - * https://github.com/puppeteer/puppeteer/blob/main/docs/troubleshooting.md#setting-up-chrome-linux-sandbox - */ - args: ['--no-sandbox', '--disable-setuid-sandbox'], - }); - const page = await browser.newPage(); - page.setDefaultNavigationTimeout(0); - page.setDefaultTimeout(60000); // use 60s timeout instead of default 30s - logger.info(`original queryUrl ${queryUrl}`); - await page.goto(queryUrl, { waitUntil: 'networkidle0' }); - logger.info(`page url ${page.url()}`); - logger.info(`page url includes login? ${page.url().includes('login')}`); - - /** - * TODO: This is a workaround to simulate a login to security enabled domain. - * Need better handle. - */ - if (page.url().includes('login')) { - logger.info( - 'domain enables security, redirecting to login page, start simulating login' - ); - await page.type('[placeholder=Username]', 'admin', { delay: 30 }); - await page.type('[placeholder=Password]', 'admin', { delay: 30 }); - await page.click("[type='submit']"); - await page.waitForNavigation(); - logger.info( - `Done logging in, currently at page: ${page.url()} \nGo to queryUrl again` - ); - await page.goto(queryUrl, { waitUntil: 'networkidle0' }); - logger.info(`wait for network idle, the current page url: ${page.url()}`); - } - - await page.setViewport({ - width: windowWidth, - height: windowHeight, - }); - - let buffer; - let element; - // crop content - if (reportSource === REPORT_TYPE.dashboard) { - await page.waitForSelector('#dashboardViewport'); - element = await page.$('#dashboardViewport'); - } else if (reportSource === REPORT_TYPE.visualization) { - await page.waitForSelector('.visChart'); - element = await page.$('.visChart'); - } - - const screenshot = await element.screenshot({ fullPage: false }); - - /** - * Sets the content of the page to have the header be above the trimmed screenshot - * and the footer be below it - */ - // TODO: need to convert header from markdown to html, either do it on server side, or on client side. - // Email body conversion is done from client side - await page.setContent(` - - -
- ${header} - - ${footer} -
- - `); - - // create pdf or png accordingly - if (reportFormat === FORMAT.pdf) { - const scrollHeight = await page.evaluate( - () => document.documentElement.scrollHeight - ); - - buffer = await page.pdf({ - margin: undefined, - width: windowWidth, - height: scrollHeight + 'px', - printBackground: true, - pageRanges: '1', - }); - } else if (reportFormat === FORMAT.png) { - buffer = await page.screenshot({ - fullPage: true, - }); - } - - const curTime = new Date(); - const timeCreated = curTime.valueOf(); - const fileName = `${getFileName(reportName, curTime)}.${reportFormat}`; - await browser.close(); - - return { timeCreated, dataUrl: buffer.toString('base64'), fileName }; -}; +import { createVisualReport } from './visualReportHelper'; export const createReport = async ( isScheduledTask: boolean, @@ -177,20 +54,8 @@ export const createReport = async ( const reportParams = reportDefinition.report_params; const reportSource = reportParams.report_source; - // validate url - let queryUrl = report.query_url; - const { origin } = new URL(report.query_url); - /** - * Only URL in WHATWG format is accepted. - * e.g http://dasd@test.com/random?size=50 will be considered as invalid and throw error, due to the "@" symbol - * is not part of the url origin according to https://nodejs.org/api/url.html#url_url_strings_and_url_objects - */ - if (report.query_url.search(origin) >= 0) { - queryUrl = report.query_url.replace(origin, LOCAL_HOST); - } else { - throw Error(`query url is not valid: ${queryUrl}`); - } - + // compose url + const queryUrl = `${LOCAL_HOST}${report.query_url}`; try { // generate report if (reportSource === REPORT_TYPE.savedSearch) { @@ -241,63 +106,6 @@ export const createReport = async ( return createReportResult; }; -export const saveToES = async ( - isScheduledTask: boolean, - report: ReportSchemaType, - esClient: ILegacyClusterClient | ILegacyScopedClusterClient -) => { - const timePending = Date.now(); - const saveParams: RequestParams.Index = { - index: CONFIG_INDEX_NAME.report, - body: { - ...report, - state: REPORT_STATE.pending, - last_updated: timePending, - time_created: timePending, - }, - }; - const esResp = await callCluster( - esClient, - 'index', - saveParams, - isScheduledTask - ); - - return esResp; -}; - -export const updateToES = async ( - isScheduledTask: boolean, - reportId: string, - esClient: ILegacyClusterClient | ILegacyScopedClusterClient, - state: string, - createReportResult?: CreateReportResultType -) => { - const timeStamp = createReportResult - ? createReportResult.timeCreated - : Date.now(); - // update report document with state "created" or "error" - const updateParams: RequestParams.Update = { - id: reportId, - index: CONFIG_INDEX_NAME.report, - body: { - doc: { - state: state, - last_updated: timeStamp, - time_created: timeStamp, - }, - }, - }; - const esResp = await callCluster( - esClient, - 'update', - updateParams, - isScheduledTask - ); - - return esResp; -}; - export const deliverReport = async ( report: ReportSchemaType, reportData: CreateReportResultType, diff --git a/kibana-reports/server/routes/utils/types.ts b/kibana-reports/server/routes/utils/types.ts index 88d9617b..1f9226a2 100644 --- a/kibana-reports/server/routes/utils/types.ts +++ b/kibana-reports/server/routes/utils/types.ts @@ -1,3 +1,18 @@ +/* + * 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. + */ + export interface CreateReportResultType { timeCreated: number; dataUrl: string; diff --git a/kibana-reports/server/routes/utils/visualReportHelper.ts b/kibana-reports/server/routes/utils/visualReportHelper.ts new file mode 100644 index 00000000..47c7ddc9 --- /dev/null +++ b/kibana-reports/server/routes/utils/visualReportHelper.ts @@ -0,0 +1,149 @@ +/* + * 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 puppeteer from 'puppeteer'; +import createDOMPurify from 'dompurify'; +import { JSDOM } from 'jsdom'; +import { Logger } from '../../../../../src/core/server'; +import { + DEFAULT_REPORT_HEADER, + DEFAULT_REPORT_FOOTER, + REPORT_TYPE, + FORMAT, +} from './constants'; +import { getFileName } from './helpers'; +import { CreateReportResultType } from './types'; + +export const createVisualReport = async ( + reportParams: any, + queryUrl: string, + logger: Logger +): Promise => { + const coreParams = reportParams.core_params; + // parse params + const reportSource = reportParams.report_source; + const reportName = reportParams.report_name; + const windowWidth = coreParams.window_width; + const windowHeight = coreParams.window_height; + const reportFormat = coreParams.report_format; + + // TODO: polish default header, maybe add a logo, depends on UX design + const window = new JSDOM('').window; + const DOMPurify = createDOMPurify(window); + + const header = coreParams.header + ? DOMPurify.sanitize(coreParams.header) + : DEFAULT_REPORT_HEADER; + const footer = coreParams.footer + ? DOMPurify.sanitize(coreParams.footer) + : DEFAULT_REPORT_FOOTER; + + // set up puppeteer + const browser = await puppeteer.launch({ + headless: true, + /** + * TODO: temp fix to disable sandbox when launching chromium on Linux instance + * https://github.com/puppeteer/puppeteer/blob/main/docs/troubleshooting.md#setting-up-chrome-linux-sandbox + */ + args: ['--no-sandbox', '--disable-setuid-sandbox'], + }); + const page = await browser.newPage(); + page.setDefaultNavigationTimeout(0); + page.setDefaultTimeout(60000); // use 60s timeout instead of default 30s + logger.info(`original queryUrl ${queryUrl}`); + await page.goto(queryUrl, { waitUntil: 'networkidle0' }); + logger.info(`page url ${page.url()}`); + logger.info(`page url includes login? ${page.url().includes('login')}`); + + /** + * TODO: This is a workaround to simulate a login to security enabled domain. + * Need better handle. + */ + if (page.url().includes('login')) { + logger.info( + 'domain enables security, redirecting to login page, start simulating login' + ); + await page.type('[placeholder=Username]', 'admin', { delay: 30 }); + await page.type('[placeholder=Password]', 'admin', { delay: 30 }); + await page.click("[type='submit']"); + await page.waitForNavigation(); + logger.info( + `Done logging in, currently at page: ${page.url()} \nGo to queryUrl again` + ); + await page.goto(queryUrl, { waitUntil: 'networkidle0' }); + logger.info(`wait for network idle, the current page url: ${page.url()}`); + } + + await page.setViewport({ + width: windowWidth, + height: windowHeight, + }); + + let buffer; + let element; + // crop content + if (reportSource === REPORT_TYPE.dashboard) { + await page.waitForSelector('#dashboardViewport'); + element = await page.$('#dashboardViewport'); + } else if (reportSource === REPORT_TYPE.visualization) { + await page.waitForSelector('.visChart'); + element = await page.$('.visChart'); + } + + const screenshot = await element.screenshot({ fullPage: false }); + + /** + * Sets the content of the page to have the header be above the trimmed screenshot + * and the footer be below it + */ + // TODO: need to convert header from markdown to html, either do it on server side, or on client side. + // Email body conversion is done from client side + await page.setContent(` + + +
+ ${header} + + ${footer} +
+ + `); + + // create pdf or png accordingly + if (reportFormat === FORMAT.pdf) { + const scrollHeight = await page.evaluate( + () => document.documentElement.scrollHeight + ); + + buffer = await page.pdf({ + margin: undefined, + width: windowWidth, + height: scrollHeight + 'px', + printBackground: true, + pageRanges: '1', + }); + } else if (reportFormat === FORMAT.png) { + buffer = await page.screenshot({ + fullPage: true, + }); + } + + const curTime = new Date(); + const timeCreated = curTime.valueOf(); + const fileName = `${getFileName(reportName, curTime)}.${reportFormat}`; + await browser.close(); + + return { timeCreated, dataUrl: buffer.toString('base64'), fileName }; +}; diff --git a/kibana-reports/server/utils/constants.ts b/kibana-reports/server/utils/constants.ts index a6858ad3..d81dbdbf 100644 --- a/kibana-reports/server/utils/constants.ts +++ b/kibana-reports/server/utils/constants.ts @@ -13,4 +13,4 @@ * permissions and limitations under the License. */ -export const POLL_INTERVAL = 1000 * 60 * 5; // in milliseconds (5 min) +export const POLL_INTERVAL = 1000 * 60 * 0.5; // in milliseconds (5 min) diff --git a/kibana-reports/server/utils/executor.ts b/kibana-reports/server/utils/executor.ts index 30c1bd18..621a1f92 100644 --- a/kibana-reports/server/utils/executor.ts +++ b/kibana-reports/server/utils/executor.ts @@ -23,6 +23,7 @@ import { } from '../model'; import moment from 'moment'; import { CONFIG_INDEX_NAME } from '../routes/utils/constants'; +import { parseEsErrorResponse } from '../routes/utils/helpers'; async function pollAndExecuteJob( schedulerClient: ILegacyClusterClient, @@ -136,16 +137,4 @@ function createReportMetaData( return report; } -function parseEsErrorResponse(error: any) { - if (error.response) { - try { - const esErrorResponse = JSON.parse(error.response); - return esErrorResponse.reason || error.response; - } catch (parsingError) { - return error.response; - } - } - return error.message; -} - export { pollAndExecuteJob }; diff --git a/kibana-reports/server/utils/validationHelper.ts b/kibana-reports/server/utils/validationHelper.ts new file mode 100644 index 00000000..0f20973b --- /dev/null +++ b/kibana-reports/server/utils/validationHelper.ts @@ -0,0 +1,36 @@ +/* + * 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 { LOCAL_HOST } from '../routes/utils/constants'; + +export const isValidRelativeUrl = (relativeUrl: string) => { + try { + new URL(`${LOCAL_HOST}${relativeUrl}`); + } catch (_) { + return false; + } + return true; +}; + +/** + * moment.js isValid() API fails to validate time duration, so use regex + * https://github.com/moment/moment/issues/1805 + **/ + +export const isValidDuration = (duration: string) => { + return !!duration.match( + /^(-?)P(?=\d|T\d)(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)([DW]))?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+(?:\.\d+)?)S)?)?$/ + ); +}; From 77a4a11d80f0ef82e7394d19f13be39d040a6062 Mon Sep 17 00:00:00 2001 From: Zhongnan Su Date: Mon, 19 Oct 2020 12:19:00 -0700 Subject: [PATCH 2/8] clean up --- kibana-reports/server/routes/report.ts | 1 - kibana-reports/server/utils/constants.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/kibana-reports/server/routes/report.ts b/kibana-reports/server/routes/report.ts index 67a500f8..3ddef29f 100644 --- a/kibana-reports/server/routes/report.ts +++ b/kibana-reports/server/routes/report.ts @@ -30,7 +30,6 @@ import { CONFIG_INDEX_NAME, DEFAULT_MAX_SIZE, DELIVERY_TYPE, - LOCAL_HOST, } from './utils/constants'; export default function (router: IRouter) { diff --git a/kibana-reports/server/utils/constants.ts b/kibana-reports/server/utils/constants.ts index d81dbdbf..a6858ad3 100644 --- a/kibana-reports/server/utils/constants.ts +++ b/kibana-reports/server/utils/constants.ts @@ -13,4 +13,4 @@ * permissions and limitations under the License. */ -export const POLL_INTERVAL = 1000 * 60 * 0.5; // in milliseconds (5 min) +export const POLL_INTERVAL = 1000 * 60 * 5; // in milliseconds (5 min) From 751d61d34b6fcbdb159fe7776cc36393c9a24bc4 Mon Sep 17 00:00:00 2001 From: Zhongnan Su Date: Mon, 19 Oct 2020 16:26:08 -0700 Subject: [PATCH 3/8] validate report name and email address --- kibana-reports/server/model/index.ts | 32 +++++++++++++++---- .../server/utils/validationHelper.ts | 9 ++---- 2 files changed, 29 insertions(+), 12 deletions(-) diff --git a/kibana-reports/server/model/index.ts b/kibana-reports/server/model/index.ts index 8d91f70a..eb4707b1 100644 --- a/kibana-reports/server/model/index.ts +++ b/kibana-reports/server/model/index.ts @@ -14,7 +14,12 @@ */ import { schema, TypeOf } from '@kbn/config-schema'; -import { isValidDuration, isValidRelativeUrl } from '../utils/validationHelper'; +import { + isValidRelativeUrl, + regexDuration, + regexEmailAddress, + regexReportName, +} from '../utils/validationHelper'; import { isValidCron } from 'cron-validator'; import { REPORT_TYPE, @@ -40,7 +45,7 @@ export const dataReportSchema = schema.object({ //ISO duration format. 'PT10M' means 10 min time_duration: schema.string({ validate(value) { - if (!isValidDuration(value)) { + if (!regexDuration.test(value)) { return `invalid time duration: ${value}`; } }, @@ -69,7 +74,7 @@ const visualReportSchema = schema.object({ footer: schema.maybe(schema.string()), time_duration: schema.string({ validate(value) { - if (!isValidDuration(value)) { + if (!regexDuration.test(value)) { return `invalid time duration: ${value}`; } }, @@ -128,7 +133,16 @@ export const kibanaUserSchema = schema.object({ }); export const channelSchema = schema.object({ - recipients: schema.arrayOf(schema.string(), { minSize: 1 }), + recipients: schema.arrayOf( + schema.string({ + validate(value) { + if (!regexEmailAddress.test(value)) { + return `invalid email address ${value}`; + } + }, + }), + { minSize: 1 } + ), title: schema.string({ minLength: 1 }), textDescription: schema.string({ minLength: 1 }), htmlDescription: schema.maybe(schema.string({ minLength: 1 })), @@ -154,7 +168,13 @@ export const deliverySchema = schema.object({ export const reportDefinitionSchema = schema.object({ report_params: schema.object({ - report_name: schema.string({ minLength: 1 }), + report_name: schema.string({ + validate(value) { + if (!regexReportName.test(value)) { + return `invald report name ${value}.\nMust be non-empty, allow a-z, A-Z, 0-9, (), [], ',' - and _ and spaces`; + } + }, + }), report_source: schema.oneOf([ schema.literal(REPORT_TYPE.dashboard), schema.literal(REPORT_TYPE.visualization), @@ -200,7 +220,7 @@ export const reportSchema = schema.object({ query_url: schema.string({ validate(value) { if (!isValidRelativeUrl(value)) { - return `this is invalid relative url: ${value}`; + return `invalid relative url: ${value}`; } }, }), diff --git a/kibana-reports/server/utils/validationHelper.ts b/kibana-reports/server/utils/validationHelper.ts index 0f20973b..28605009 100644 --- a/kibana-reports/server/utils/validationHelper.ts +++ b/kibana-reports/server/utils/validationHelper.ts @@ -28,9 +28,6 @@ export const isValidRelativeUrl = (relativeUrl: string) => { * moment.js isValid() API fails to validate time duration, so use regex * https://github.com/moment/moment/issues/1805 **/ - -export const isValidDuration = (duration: string) => { - return !!duration.match( - /^(-?)P(?=\d|T\d)(?:(\d+)Y)?(?:(\d+)M)?(?:(\d+)([DW]))?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+(?:\.\d+)?)S)?)?$/ - ); -}; +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\(\)\[\]\,\_\-+]+$/; From f5f3389f2a90bd0d7571e63dd0af4b5a92024a59 Mon Sep 17 00:00:00 2001 From: Zhongnan Su Date: Mon, 19 Oct 2020 21:35:59 -0700 Subject: [PATCH 4/8] fix --- kibana-reports/server/model/index.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/kibana-reports/server/model/index.ts b/kibana-reports/server/model/index.ts index eb4707b1..f613362e 100644 --- a/kibana-reports/server/model/index.ts +++ b/kibana-reports/server/model/index.ts @@ -143,9 +143,9 @@ export const channelSchema = schema.object({ }), { minSize: 1 } ), - title: schema.string({ minLength: 1 }), - textDescription: schema.string({ minLength: 1 }), - htmlDescription: schema.maybe(schema.string({ minLength: 1 })), + title: schema.string(), + textDescription: schema.string(), + htmlDescription: schema.maybe(schema.string()), email_format: schema.oneOf([ schema.literal(EMAIL_FORMAT.attachment), schema.literal(EMAIL_FORMAT.embeddedHtml), @@ -180,7 +180,7 @@ export const reportDefinitionSchema = schema.object({ schema.literal(REPORT_TYPE.visualization), schema.literal(REPORT_TYPE.savedSearch), ]), - description: schema.string({ minLength: 1 }), + description: schema.string(), core_params: schema.conditional( schema.siblingRef('report_source'), REPORT_TYPE.savedSearch, From 98bac625cf4069ea1db2217026fa8da57a6c20c0 Mon Sep 17 00:00:00 2001 From: Zhongnan Su Date: Tue, 20 Oct 2020 13:45:53 -0700 Subject: [PATCH 5/8] use session cookie to access security-enabled domain --- .../server/executor/createScheduledReport.ts | 97 +++++++++++++ .../server/{utils => executor}/executor.ts | 13 +- kibana-reports/server/plugin.ts | 2 +- .../server/routes/lib/createReport.ts | 136 ++++++++++++++++++ .../server/routes/lib/createSchedule.ts | 70 +++++++++ .../reportHelper.ts => lib/deliverReport.ts} | 101 ++----------- kibana-reports/server/routes/report.ts | 24 +--- .../server/routes/reportDefinition.ts | 131 +++-------------- .../server/routes/utils/constants.ts | 2 + kibana-reports/server/routes/utils/helpers.ts | 73 +++++++++- .../server/routes/utils/visualReportHelper.ts | 29 +--- 11 files changed, 419 insertions(+), 259 deletions(-) create mode 100644 kibana-reports/server/executor/createScheduledReport.ts rename kibana-reports/server/{utils => executor}/executor.ts (94%) create mode 100644 kibana-reports/server/routes/lib/createReport.ts create mode 100644 kibana-reports/server/routes/lib/createSchedule.ts rename kibana-reports/server/routes/{utils/reportHelper.ts => lib/deliverReport.ts} (53%) diff --git a/kibana-reports/server/executor/createScheduledReport.ts b/kibana-reports/server/executor/createScheduledReport.ts new file mode 100644 index 00000000..e98de7df --- /dev/null +++ b/kibana-reports/server/executor/createScheduledReport.ts @@ -0,0 +1,97 @@ +/* + * 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 { + REPORT_TYPE, + REPORT_STATE, + LOCAL_HOST, +} from '../routes/utils/constants'; +import { updateReportState, saveReport } from '../routes/utils/helpers'; +import { ILegacyClusterClient, Logger } from '../../../../src/core/server'; +import { createSavedSearchReport } from '../routes/utils/savedSearchReportHelper'; +import { ReportSchemaType } from '../model'; +import { CreateReportResultType } from '../routes/utils/types'; +import { createVisualReport } from '../routes/utils/visualReportHelper'; +import { deliverReport } from '../routes/lib/deliverReport'; + +export const createScheduledReport = async ( + report: ReportSchemaType, + esClient: ILegacyClusterClient, + notificationClient: ILegacyClusterClient, + logger: Logger +): Promise => { + const isScheduledTask = true; + let createReportResult: CreateReportResultType; + let reportId; + // create new report instance and set report state to "pending" + + const esResp = await saveReport(isScheduledTask, report, esClient); + reportId = esResp._id; + + const reportDefinition = report.report_definition; + const reportParams = reportDefinition.report_params; + const reportSource = reportParams.report_source; + + // compose url + const queryUrl = `${LOCAL_HOST}${report.query_url}`; + try { + // generate report + if (reportSource === REPORT_TYPE.savedSearch) { + createReportResult = await createSavedSearchReport( + report, + esClient, + isScheduledTask + ); + } else { + // report source can only be one of [saved search, visualization, dashboard] + createReportResult = await createVisualReport( + reportParams, + queryUrl, + logger + ); + } + + await updateReportState( + isScheduledTask, + reportId, + esClient, + REPORT_STATE.created, + createReportResult + ); + + // deliver report + createReportResult = await deliverReport( + report, + createReportResult, + notificationClient, + esClient, + reportId, + isScheduledTask + ); + } catch (error) { + // update report instance with "error" state + //TODO: save error detail and display on UI + + await updateReportState( + isScheduledTask, + reportId, + esClient, + REPORT_STATE.error + ); + throw error; + } + + return createReportResult; +}; diff --git a/kibana-reports/server/utils/executor.ts b/kibana-reports/server/executor/executor.ts similarity index 94% rename from kibana-reports/server/utils/executor.ts rename to kibana-reports/server/executor/executor.ts index 621a1f92..937729a8 100644 --- a/kibana-reports/server/utils/executor.ts +++ b/kibana-reports/server/executor/executor.ts @@ -14,8 +14,8 @@ */ import { ILegacyClusterClient, Logger } from '../../../../src/core/server'; -import { createReport } from '../routes/utils/reportHelper'; -import { POLL_INTERVAL } from './constants'; +import { createScheduledReport } from './createScheduledReport'; +import { POLL_INTERVAL } from '../utils/constants'; import { ReportSchemaType, DataReportSchemaType, @@ -98,15 +98,14 @@ async function executeScheduledJob( reportDefinition, triggeredTime ); - // create report and return report data - const reportData = await createReport( - true, + + const reportData = await createScheduledReport( reportMetaData, esClient, - logger, notificationClient, - undefined + logger ); + logger.info(`new scheduled report created: ${reportData.fileName}`); } catch (error) { logger.error( diff --git a/kibana-reports/server/plugin.ts b/kibana-reports/server/plugin.ts index 7a24250a..79d449bf 100644 --- a/kibana-reports/server/plugin.ts +++ b/kibana-reports/server/plugin.ts @@ -29,7 +29,7 @@ import { OpendistroKibanaReportsPluginStart, } from './types'; import registerRoutes from './routes'; -import { pollAndExecuteJob } from './utils/executor'; +import { pollAndExecuteJob } from './executor/executor'; import { POLL_INTERVAL } from './utils/constants'; export interface ReportsPluginRequestContext { diff --git a/kibana-reports/server/routes/lib/createReport.ts b/kibana-reports/server/routes/lib/createReport.ts new file mode 100644 index 00000000..b3977f43 --- /dev/null +++ b/kibana-reports/server/routes/lib/createReport.ts @@ -0,0 +1,136 @@ +/* + * 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 { + REPORT_TYPE, + REPORT_STATE, + LOCAL_HOST, + SECURITY_AUTH_COOKIE_NAME, +} from '../utils/constants'; +import { updateReportState, saveReport } from '../utils/helpers'; +import { + ILegacyScopedClusterClient, + KibanaRequest, + Logger, + RequestHandlerContext, +} from '../../../../../src/core/server'; +import { createSavedSearchReport } from '../utils/savedSearchReportHelper'; +import { ReportSchemaType } from '../../model'; +import { CreateReportResultType } from '../utils/types'; +import { createVisualReport } from '../utils/visualReportHelper'; +import { SetCookie } from 'puppeteer'; +import { deliverReport } from './deliverReport'; + +export const createReport = async ( + request: KibanaRequest, + context: RequestHandlerContext, + report: ReportSchemaType, + savedReportId?: string +): Promise => { + const isScheduledTask = false; + //@ts-ignore + const logger: Logger = context.reporting_plugin.logger; + // @ts-ignore + const notificationClient: ILegacyScopedClusterClient = context.reporting_plugin.notificationClient.asScoped( + request + ); + const esClient = context.core.elasticsearch.legacy.client; + + let createReportResult: CreateReportResultType; + let reportId; + // create new report instance and set report state to "pending" + if (savedReportId) { + reportId = savedReportId; + } else { + const esResp = await saveReport(isScheduledTask, report, esClient); + reportId = esResp._id; + } + + const reportDefinition = report.report_definition; + const reportParams = reportDefinition.report_params; + const reportSource = reportParams.report_source; + + // compose url + const queryUrl = `${LOCAL_HOST}${report.query_url}`; + try { + // generate report + if (reportSource === REPORT_TYPE.savedSearch) { + createReportResult = await createSavedSearchReport( + report, + esClient, + isScheduledTask + ); + } else { + // report source can only be one of [saved search, visualization, dashboard] + let cookieObject: SetCookie | undefined; + if (request.headers.cookie) { + const cookies = request.headers.cookie.split(';'); + cookies.map((item: string) => { + const cookie = item.trim().split('='); + if (cookie[0] === SECURITY_AUTH_COOKIE_NAME) { + cookieObject = { + name: cookie[0], + value: cookie[1], + url: queryUrl, + }; + } + }); + } + + createReportResult = await createVisualReport( + reportParams, + queryUrl, + logger, + cookieObject + ); + } + // update report state to "created" + if (!savedReportId) { + await updateReportState( + isScheduledTask, + reportId, + esClient, + REPORT_STATE.created, + createReportResult + ); + } + + // deliver report + if (!savedReportId) { + createReportResult = await deliverReport( + report, + createReportResult, + notificationClient, + esClient, + reportId, + isScheduledTask + ); + } + } catch (error) { + // update report instance with "error" state + //TODO: save error detail and display on UI + if (!savedReportId) { + await updateReportState( + isScheduledTask, + reportId, + esClient, + REPORT_STATE.error + ); + } + throw error; + } + + return createReportResult; +}; diff --git a/kibana-reports/server/routes/lib/createSchedule.ts b/kibana-reports/server/routes/lib/createSchedule.ts new file mode 100644 index 00000000..ac6d53d2 --- /dev/null +++ b/kibana-reports/server/routes/lib/createSchedule.ts @@ -0,0 +1,70 @@ +/* + * 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 } from 'server/model'; +import { + KibanaRequest, + RequestHandlerContext, +} from '../../../../../src/core/server'; +import { TRIGGER_TYPE } from '../utils/constants'; + +export const createSchedule = async ( + request: KibanaRequest, + reportDefinitionId: string, + context: RequestHandlerContext +) => { + const reportDefinition: ReportDefinitionSchemaType = request.body; + const trigger = reportDefinition.trigger; + const triggerType = trigger.trigger_type; + const triggerParams = trigger.trigger_params; + + // @ts-ignore + const schedulerClient: ILegacyScopedClusterClient = context.reporting_plugin.schedulerClient.asScoped( + request + ); + + if (triggerType === TRIGGER_TYPE.schedule) { + const schedule = triggerParams.schedule; + + // compose the request body + const scheduledJob = { + schedule: schedule, + name: `${reportDefinition.report_params.report_name}_schedule`, + enabled: triggerParams.enabled, + report_definition_id: reportDefinitionId, + enabled_time: triggerParams.enabled_time, + }; + // send to reports-scheduler to create a scheduled job + const res = await schedulerClient.callAsCurrentUser( + 'reports_scheduler.createSchedule', + { + jobId: reportDefinitionId, + body: scheduledJob, + } + ); + + return res; + } else if (triggerType == TRIGGER_TYPE.onDemand) { + /* + * TODO: return nothing for on Demand report, because currently on-demand report is handled by client side, + * by hitting the create report http endpoint with data to get a report downloaded. Server side only saves + * that on-demand report definition into the index. Need further discussion on what behavior we want + * await createReport(reportDefinition, esClient); + */ + return; + } + // else if (triggerType == TRIGGER_TYPE.alerting) { + //TODO: add alert-based scheduling logic [enhancement feature] +}; diff --git a/kibana-reports/server/routes/utils/reportHelper.ts b/kibana-reports/server/routes/lib/deliverReport.ts similarity index 53% rename from kibana-reports/server/routes/utils/reportHelper.ts rename to kibana-reports/server/routes/lib/deliverReport.ts index 40ab77be..f14643db 100644 --- a/kibana-reports/server/routes/utils/reportHelper.ts +++ b/kibana-reports/server/routes/lib/deliverReport.ts @@ -13,98 +13,19 @@ * permissions and limitations under the License. */ +import { ReportSchemaType } from 'server/model'; import { - FORMAT, - REPORT_TYPE, - REPORT_STATE, - LOCAL_HOST, - DELIVERY_TYPE, - EMAIL_FORMAT, -} from './constants'; -import { callCluster, updateToES, saveToES } from './helpers'; -import { - ILegacyClusterClient, ILegacyScopedClusterClient, - Logger, + ILegacyClusterClient, } from '../../../../../src/core/server'; -import { createSavedSearchReport } from './savedSearchReportHelper'; -import { ReportSchemaType } from '../../model'; -import { CreateReportResultType } from './types'; -import { createVisualReport } from './visualReportHelper'; - -export const createReport = async ( - isScheduledTask: boolean, - report: ReportSchemaType, - esClient: ILegacyClusterClient | ILegacyScopedClusterClient, - logger: Logger, - notificationClient?: ILegacyClusterClient | ILegacyScopedClusterClient, - savedReportId?: string -): Promise => { - let createReportResult: CreateReportResultType; - let reportId; - // create new report instance and set report state to "pending" - if (savedReportId) { - reportId = savedReportId; - } else { - const esResp = await saveToES(isScheduledTask, report, esClient); - reportId = esResp._id; - } - - const reportDefinition = report.report_definition; - const reportParams = reportDefinition.report_params; - const reportSource = reportParams.report_source; - - // compose url - const queryUrl = `${LOCAL_HOST}${report.query_url}`; - try { - // generate report - if (reportSource === REPORT_TYPE.savedSearch) { - createReportResult = await createSavedSearchReport( - report, - esClient, - isScheduledTask - ); - } else { - // report source can only be one of [saved search, visualization, dashboard] - createReportResult = await createVisualReport( - reportParams, - queryUrl, - logger - ); - } - - if (!savedReportId) { - await updateToES( - isScheduledTask, - reportId, - esClient, - REPORT_STATE.created, - createReportResult - ); - } - - // deliver report - if (notificationClient) { - createReportResult = await deliverReport( - report, - createReportResult, - notificationClient, - esClient, - reportId, - isScheduledTask - ); - } - } catch (error) { - // update report instance with "error" state - //TODO: save error detail and display on UI - if (!savedReportId) { - await updateToES(isScheduledTask, reportId, esClient, REPORT_STATE.error); - } - throw error; - } - - return createReportResult; -}; +import { + DELIVERY_TYPE, + EMAIL_FORMAT, + FORMAT, + REPORT_STATE, +} from '../utils/constants'; +import { callCluster, updateReportState } from '../utils/helpers'; +import { CreateReportResultType } from '../utils/types'; export const deliverReport = async ( report: ReportSchemaType, @@ -167,7 +88,7 @@ export const deliverReport = async ( } } // update report document with state "shared" and time_created - await updateToES( + await updateReportState( isScheduledTask, reportId, esClient, diff --git a/kibana-reports/server/routes/report.ts b/kibana-reports/server/routes/report.ts index 3ddef29f..eec494b4 100644 --- a/kibana-reports/server/routes/report.ts +++ b/kibana-reports/server/routes/report.ts @@ -18,12 +18,11 @@ import { IRouter, IKibanaResponse, ResponseError, - ILegacyScopedClusterClient, Logger, } from '../../../../src/core/server'; import { API_PREFIX } from '../../common'; import { RequestParams } from '@elastic/elasticsearch'; -import { createReport } from './utils/reportHelper'; +import { createReport } from './lib/createReport'; import { reportSchema } from '../model'; import { errorResponse } from './utils/helpers'; import { @@ -58,19 +57,7 @@ export default function (router: IRouter) { } try { - // @ts-ignore - const notificationClient: ILegacyScopedClusterClient = context.reporting_plugin.notificationClient.asScoped( - request - ); - const esClient = context.core.elasticsearch.legacy.client; - - const reportData = await createReport( - false, - report, - esClient, - logger, - notificationClient - ); + const reportData = await createReport(request, context, report); // if not deliver to user himself , no need to send actual file data to client const delivery = report.report_definition.delivery; @@ -124,14 +111,11 @@ export default function (router: IRouter) { } ); const report = esResp._source; - const esClient = context.core.elasticsearch.legacy.client; const reportData = await createReport( - false, + request, + context, report, - esClient, - logger, - undefined, savedReportId ); diff --git a/kibana-reports/server/routes/reportDefinition.ts b/kibana-reports/server/routes/reportDefinition.ts index 3350d803..36d18e1e 100644 --- a/kibana-reports/server/routes/reportDefinition.ts +++ b/kibana-reports/server/routes/reportDefinition.ts @@ -18,20 +18,21 @@ import { IRouter, IKibanaResponse, ResponseError, - RequestHandlerContext, - KibanaRequest, - ILegacyScopedClusterClient, } from '../../../../src/core/server'; import { API_PREFIX } from '../../common'; import { RequestParams } from '@elastic/elasticsearch'; import { reportDefinitionSchema, ReportDefinitionSchemaType } from '../model'; -import { errorResponse } from './utils/helpers'; import { - REPORT_DEFINITION_STATUS, + errorResponse, + saveReportDefinition, + updateReportDefinition, +} from './utils/helpers'; +import { TRIGGER_TYPE, CONFIG_INDEX_NAME, DEFAULT_MAX_SIZE, } from './utils/constants'; +import { createSchedule } from './lib/createSchedule'; export default function (router: IRouter) { // Create report Definition @@ -48,6 +49,7 @@ export default function (router: IRouter) { response ): Promise> => { let reportDefinition = request.body; + const esClient = context.core.elasticsearch.legacy.client; //@ts-ignore const logger = context.reporting_plugin.logger; // input validation @@ -63,32 +65,11 @@ export default function (router: IRouter) { // save metadata // TODO: consider create uuid manually and save report after it's scheduled with reports-scheduler try { - const toSave = { - report_definition: { - ...reportDefinition, - time_created: Date.now(), - last_updated: Date.now(), - status: REPORT_DEFINITION_STATUS.active, - }, - }; - - const params: RequestParams.Index = { - index: CONFIG_INDEX_NAME.reportDefinition, - body: toSave, - }; - - const esResp = await context.core.elasticsearch.legacy.client.callAsCurrentUser( - 'index', - params - ); - + // save report definition to ES + const esResp = await saveReportDefinition(reportDefinition, esClient); // create scheduled job by reports-scheduler const reportDefinitionId = esResp._id; - const res = await createScheduledJob( - request, - reportDefinitionId, - context - ); + const res = await createSchedule(request, reportDefinitionId, context); return response.ok({ body: { @@ -121,6 +102,7 @@ export default function (router: IRouter) { ): Promise> => { const reportDefinition: ReportDefinitionSchemaType = request.body; const savedReportDefinitionId = request.params.reportDefinitionId; + const esClient = context.core.elasticsearch.legacy.client; //@ts-ignore const logger = context.reporting_plugin.logger; // input validation @@ -132,48 +114,16 @@ export default function (router: IRouter) { ); return response.badRequest({ body: error }); } - - let newStatus = REPORT_DEFINITION_STATUS.active; - /* - "enabled = false" means de-scheduling a job. - TODO: also need to remove any job in queue and release lock, consider do that - within the createSchedule API exposed from reports-scheduler - */ - if (reportDefinition.trigger.trigger_type == TRIGGER_TYPE.schedule) { - const enabled = reportDefinition.trigger.trigger_params.enabled; - if (enabled) { - newStatus = REPORT_DEFINITION_STATUS.active; - } else { - newStatus = REPORT_DEFINITION_STATUS.disabled; - } - } - // Update report definition metadata try { - const toUpdate = { - report_definition: { - ...reportDefinition, - last_updated: Date.now(), - status: newStatus, - }, - }; - - const params: RequestParams.Index = { - id: savedReportDefinitionId, - index: CONFIG_INDEX_NAME.reportDefinition, - body: toUpdate, - }; - const esResp = await context.core.elasticsearch.legacy.client.callAsCurrentUser( - 'index', - params + const esResp = await updateReportDefinition( + savedReportDefinitionId, + reportDefinition, + esClient ); // update scheduled job by calling reports-scheduler const reportDefinitionId = esResp._id; - const res = await createScheduledJob( - request, - reportDefinitionId, - context - ); + const res = await createSchedule(request, reportDefinitionId, context); return response.ok({ body: { @@ -349,52 +299,3 @@ export default function (router: IRouter) { } ); } - -async function createScheduledJob( - request: KibanaRequest, - reportDefinitionId: string, - context: RequestHandlerContext -) { - const reportDefinition: ReportDefinitionSchemaType = request.body; - const trigger = reportDefinition.trigger; - const triggerType = trigger.trigger_type; - const triggerParams = trigger.trigger_params; - - // @ts-ignore - const schedulerClient: ILegacyScopedClusterClient = context.reporting_plugin.schedulerClient.asScoped( - request - ); - - if (triggerType === TRIGGER_TYPE.schedule) { - const schedule = triggerParams.schedule; - - // compose the request body - const scheduledJob = { - schedule: schedule, - name: `${reportDefinition.report_params.report_name}_schedule`, - enabled: triggerParams.enabled, - report_definition_id: reportDefinitionId, - enabled_time: triggerParams.enabled_time, - }; - // send to reports-scheduler to create a scheduled job - const res = await schedulerClient.callAsCurrentUser( - 'reports_scheduler.createSchedule', - { - jobId: reportDefinitionId, - body: scheduledJob, - } - ); - - return res; - } else if (triggerType == TRIGGER_TYPE.onDemand) { - /* - * TODO: return nothing for on Demand report, because currently on-demand report is handled by client side, - * by hitting the create report http endpoint with data to get a report downloaded. Server side only saves - * that on-demand report definition into the index. Need further discussion on what behavior we want - * await createReport(reportDefinition, esClient); - */ - return; - } - // else if (triggerType == TRIGGER_TYPE.alerting) { - //TODO: add alert-based scheduling logic [enhancement feature] -} diff --git a/kibana-reports/server/routes/utils/constants.ts b/kibana-reports/server/routes/utils/constants.ts index 791fbaff..6060aa68 100644 --- a/kibana-reports/server/routes/utils/constants.ts +++ b/kibana-reports/server/routes/utils/constants.ts @@ -84,3 +84,5 @@ export const LOCAL_HOST = 'http://localhost:5601'; export const DEFAULT_REPORT_HEADER = '

Open Distro Kibana Reports

'; export const DEFAULT_REPORT_FOOTER = '

Open Distro Kibana Reports

'; + +export const SECURITY_AUTH_COOKIE_NAME = 'security_authentication'; diff --git a/kibana-reports/server/routes/utils/helpers.ts b/kibana-reports/server/routes/utils/helpers.ts index 6e27821e..d8c4ac34 100644 --- a/kibana-reports/server/routes/utils/helpers.ts +++ b/kibana-reports/server/routes/utils/helpers.ts @@ -20,9 +20,14 @@ import { ILegacyScopedClusterClient, } from '../../../../../src/core/server'; import { RequestParams } from '@elastic/elasticsearch'; -import { CONFIG_INDEX_NAME, REPORT_STATE } from './constants'; +import { + CONFIG_INDEX_NAME, + REPORT_DEFINITION_STATUS, + REPORT_STATE, + TRIGGER_TYPE, +} from './constants'; import { CreateReportResultType } from './types'; -import { ReportSchemaType } from 'server/model'; +import { ReportDefinitionSchemaType, ReportSchemaType } from 'server/model'; export function parseEsErrorResponse(error: any) { if (error.response) { @@ -73,7 +78,7 @@ export const callCluster = async ( return esResp; }; -export const saveToES = async ( +export const saveReport = async ( isScheduledTask: boolean, report: ReportSchemaType, esClient: ILegacyClusterClient | ILegacyScopedClusterClient @@ -98,7 +103,8 @@ export const saveToES = async ( return esResp; }; -export const updateToES = async ( +// The only thing can be updated of a report instance is its "state" +export const updateReportState = async ( isScheduledTask: boolean, reportId: string, esClient: ILegacyClusterClient | ILegacyScopedClusterClient, @@ -129,3 +135,62 @@ export const updateToES = async ( return esResp; }; + +export const saveReportDefinition = async ( + reportDefinition: ReportDefinitionSchemaType, + esClient: ILegacyScopedClusterClient +) => { + const toSave = { + report_definition: { + ...reportDefinition, + time_created: Date.now(), + last_updated: Date.now(), + status: REPORT_DEFINITION_STATUS.active, + }, + }; + + const params: RequestParams.Index = { + index: CONFIG_INDEX_NAME.reportDefinition, + body: toSave, + }; + + const esResp = await esClient.callAsCurrentUser('index', params); + + return esResp; +}; + +export const updateReportDefinition = async ( + reportDefinitionId: string, + reportDefinition: ReportDefinitionSchemaType, + esClient: ILegacyScopedClusterClient +) => { + let newStatus = REPORT_DEFINITION_STATUS.active; + /** + * "enabled = false" means de-scheduling a job. + * TODO: also need to remove any job in queue and release lock, consider do that + * within the createSchedule API exposed from reports-scheduler + */ + if (reportDefinition.trigger.trigger_type == TRIGGER_TYPE.schedule) { + const enabled = reportDefinition.trigger.trigger_params.enabled; + newStatus = enabled + ? REPORT_DEFINITION_STATUS.active + : REPORT_DEFINITION_STATUS.disabled; + } + + const toUpdate = { + report_definition: { + ...reportDefinition, + last_updated: Date.now(), + status: newStatus, + }, + }; + + const params: RequestParams.Index = { + id: reportDefinitionId, + index: CONFIG_INDEX_NAME.reportDefinition, + body: toUpdate, + }; + const esResp = await esClient.callAsCurrentUser('index', params); + + return esResp; +}; diff --git a/kibana-reports/server/routes/utils/visualReportHelper.ts b/kibana-reports/server/routes/utils/visualReportHelper.ts index 47c7ddc9..55b88a2b 100644 --- a/kibana-reports/server/routes/utils/visualReportHelper.ts +++ b/kibana-reports/server/routes/utils/visualReportHelper.ts @@ -13,7 +13,7 @@ * permissions and limitations under the License. */ -import puppeteer from 'puppeteer'; +import puppeteer, { SetCookie } from 'puppeteer'; import createDOMPurify from 'dompurify'; import { JSDOM } from 'jsdom'; import { Logger } from '../../../../../src/core/server'; @@ -29,7 +29,8 @@ import { CreateReportResultType } from './types'; export const createVisualReport = async ( reportParams: any, queryUrl: string, - logger: Logger + logger: Logger, + cookie?: SetCookie ): Promise => { const coreParams = reportParams.core_params; // parse params @@ -62,29 +63,13 @@ export const createVisualReport = async ( const page = await browser.newPage(); page.setDefaultNavigationTimeout(0); page.setDefaultTimeout(60000); // use 60s timeout instead of default 30s + if (cookie) { + logger.info('domain enables security, use session cookie to access'); + await page.setCookie(cookie); + } logger.info(`original queryUrl ${queryUrl}`); await page.goto(queryUrl, { waitUntil: 'networkidle0' }); logger.info(`page url ${page.url()}`); - logger.info(`page url includes login? ${page.url().includes('login')}`); - - /** - * TODO: This is a workaround to simulate a login to security enabled domain. - * Need better handle. - */ - if (page.url().includes('login')) { - logger.info( - 'domain enables security, redirecting to login page, start simulating login' - ); - await page.type('[placeholder=Username]', 'admin', { delay: 30 }); - await page.type('[placeholder=Password]', 'admin', { delay: 30 }); - await page.click("[type='submit']"); - await page.waitForNavigation(); - logger.info( - `Done logging in, currently at page: ${page.url()} \nGo to queryUrl again` - ); - await page.goto(queryUrl, { waitUntil: 'networkidle0' }); - logger.info(`wait for network idle, the current page url: ${page.url()}`); - } await page.setViewport({ width: windowWidth, From 4eb2a8dbaa74c690eaffd7ce4c982d7138e1bc27 Mon Sep 17 00:00:00 2001 From: Zhongnan Su Date: Thu, 22 Oct 2020 23:20:24 -0700 Subject: [PATCH 6/8] Add email body template --- kibana-reports/package.json | 1 + .../report_definitions/delivery/delivery.tsx | 57 ++- .../delivery/delivery_constants.tsx | 22 +- .../report_definitions/delivery/email.tsx | 135 +---- .../edit/edit_report_definition.tsx | 1 - .../server/executor/createScheduledReport.ts | 3 +- kibana-reports/server/model/index.ts | 8 +- .../server/routes/lib/createReport.ts | 3 +- .../server/routes/lib/createSchedule.ts | 1 - .../server/routes/lib/deliverReport.ts | 108 ++-- .../server/routes/reportDefinition.ts | 4 +- .../server/routes/utils/constants.ts | 5 - .../notification/deliveryContentHelper.ts | 45 ++ .../email_content_template.html | 471 ++++++++++++++++++ .../notification_content_template/logo.png | Bin 0 -> 9995 bytes kibana-reports/yarn.lock | 109 +++- 16 files changed, 759 insertions(+), 214 deletions(-) create mode 100644 kibana-reports/server/routes/utils/notification/deliveryContentHelper.ts create mode 100644 kibana-reports/server/routes/utils/notification/notification_content_template/email_content_template.html create mode 100644 kibana-reports/server/routes/utils/notification/notification_content_template/logo.png diff --git a/kibana-reports/package.json b/kibana-reports/package.json index 7652f19a..87d18883 100644 --- a/kibana-reports/package.json +++ b/kibana-reports/package.json @@ -30,6 +30,7 @@ "@types/dompurify": "^2.0.4", "@types/jsdom": "^16.2.4", "babel-polyfill": "^6.26.0", + "cheerio": "^1.0.0-rc.3", "cron-validator": "^1.1.1", "dompurify": "^2.1.1", "elastic-builder": "^2.7.1", diff --git a/kibana-reports/public/components/report_definitions/delivery/delivery.tsx b/kibana-reports/public/components/report_definitions/delivery/delivery.tsx index dac8d3eb..530296f8 100644 --- a/kibana-reports/public/components/report_definitions/delivery/delivery.tsx +++ b/kibana-reports/public/components/report_definitions/delivery/delivery.tsx @@ -22,9 +22,8 @@ import { EuiPageContentBody, EuiHorizontalRule, EuiSpacer, - EuiRadioGroup, + EuiCheckbox, } from '@elastic/eui'; -import { KibanaUserDelivery } from './kibana_user'; import { DELIVERY_TYPE_OPTIONS } from './delivery_constants'; import 'react-mde/lib/styles/css/react-mde-all.css'; import { reportDefinitionParams } from '../create/create_report_definition'; @@ -44,22 +43,31 @@ export function ReportDelivery(props: ReportDeliveryProps) { editDefinitionId, reportDefinitionRequest, httpClientProps, - showEmailRecipientsError, } = props; - const [deliveryType, setDeliveryType] = useState(DELIVERY_TYPE_OPTIONS[0].id); + const [emailCheckbox, setEmailCheckbox] = useState(false); - const handleDeliveryType = (e: string) => { - setDeliveryType(e); - reportDefinitionRequest.delivery.delivery_type = e; + const handleEmailCheckbox = (e: { + target: { checked: React.SetStateAction }; + }) => { + setEmailCheckbox(e.target.checked); + if (e.target.checked) { + // if checked, set delivery type to email + reportDefinitionRequest.delivery.delivery_type = + DELIVERY_TYPE_OPTIONS[1].id; + } else { + // uncheck email checkbox means to use default setting, which is kibana user + defaultCreateDeliveryParams(); + } }; - const deliverySetting = (props: ReportDeliveryProps) => { - return deliveryType === DELIVERY_TYPE_OPTIONS[0].id ? ( - - ) : ( - - ); + const emailDelivery = emailCheckbox ? : null; + + const defaultCreateDeliveryParams = () => { + reportDefinitionRequest.delivery = { + delivery_type: DELIVERY_TYPE_OPTIONS[0].id, + delivery_params: { kibana_recipients: [] }, + }; }; useEffect(() => { @@ -67,10 +75,14 @@ export function ReportDelivery(props: ReportDeliveryProps) { httpClientProps .get(`../api/reporting/reportDefinitions/${editDefinitionId}`) .then(async (response) => { - handleDeliveryType(response.report_definition.delivery.delivery_type); + const isEmailSelected = + response.report_definition.delivery.delivery_type === + DELIVERY_TYPE_OPTIONS[1].id; + handleEmailCheckbox({ target: { checked: isEmailSelected } }); }); } else { - reportDefinitionRequest.delivery.delivery_type = deliveryType; + // By default it's set to deliver to kibana user + defaultCreateDeliveryParams(); } }, []); @@ -83,15 +95,14 @@ export function ReportDelivery(props: ReportDeliveryProps) { - - - + - {deliverySetting(props)} + {emailDelivery} ); diff --git a/kibana-reports/public/components/report_definitions/delivery/delivery_constants.tsx b/kibana-reports/public/components/report_definitions/delivery/delivery_constants.tsx index 523d13d5..109d5386 100644 --- a/kibana-reports/public/components/report_definitions/delivery/delivery_constants.tsx +++ b/kibana-reports/public/components/report_definitions/delivery/delivery_constants.tsx @@ -13,27 +13,7 @@ * permissions and limitations under the License. */ -export const EMAIL_RECIPIENT_OPTIONS = [ - // TODO: remove once we support actual kibana users - { label: 'admin (self)' }, - { label: 'davidcui' }, - { label: 'szhongna' }, - { label: 'jadhanir' }, - { label: 'kvngar' }, -]; - -export const EMAIL_FORMAT_OPTIONS = [ - { - id: 'Attachment', - label: 'Email with report as attached file', - }, - // TODO: add it back when we have fully support for embedded html - { - id: 'Embedded HTML', - label: 'Embedded HTML report', - disabled: true, - }, -]; +export const EMAIL_RECIPIENT_OPTIONS = []; export const DELIVERY_TYPE_OPTIONS = [ { diff --git a/kibana-reports/public/components/report_definitions/delivery/email.tsx b/kibana-reports/public/components/report_definitions/delivery/email.tsx index a93381df..513034ef 100644 --- a/kibana-reports/public/components/report_definitions/delivery/email.tsx +++ b/kibana-reports/public/components/report_definitions/delivery/email.tsx @@ -13,58 +13,14 @@ * permissions and limitations under the License. */ -import { - EuiComboBox, - EuiFieldText, - EuiFormRow, - EuiLink, - EuiListGroup, - EuiPopover, - EuiRadioGroup, - EuiSpacer, - EuiText, -} from '@elastic/eui'; +import { EuiComboBox, EuiFieldText, EuiFormRow, EuiSpacer } from '@elastic/eui'; import React, { useEffect } from 'react'; import { useState } from 'react'; -import { - EMAIL_FORMAT_OPTIONS, - EMAIL_RECIPIENT_OPTIONS, -} from './delivery_constants'; import ReactMDE from 'react-mde'; import { ReportDeliveryProps } from './delivery'; -import { - ChannelSchemaType, - DeliverySchemaType, - KibanaUserSchemaType, -} from 'server/model'; +import { ChannelSchemaType, DeliverySchemaType } from 'server/model'; import { converter } from '../utils'; - -const INSERT_PLACEHOLDER_OPTIONS = [ - { - label: 'Report details URL', - href: '#', - iconType: 'link', - size: 's', - }, - { - label: 'Report source URL', - href: '#', - iconType: 'link', - size: 's', - }, - { - label: 'File download URL', - href: '#', - iconType: 'link', - size: 's', - }, - { - label: 'Report creation timestamp', - href: '#', - iconType: 'clock', - size: 's', - }, -]; +import { DELIVERY_TYPE_OPTIONS } from './delivery_constants'; export const EmailDelivery = (props: ReportDeliveryProps) => { const { @@ -75,41 +31,13 @@ export const EmailDelivery = (props: ReportDeliveryProps) => { showEmailRecipientsError, } = props; - const [emailRecipients, setEmailRecipients] = useState([]); + const [emailRecipients, setOptions] = useState([]); + const [selectedEmailRecipients, setSelectedEmailRecipients] = useState([]); const [emailSubject, setEmailSubject] = useState(''); const [emailBody, setEmailBody] = useState(''); - const [emailFormat, setEmailFormat] = useState(EMAIL_FORMAT_OPTIONS[0].id); const [selectedTab, setSelectedTab] = React.useState<'write' | 'preview'>( 'write' ); - const [insertPlaceholder, setInsertPlaceholder] = useState(false); - - const handleInsertPlaceholderClick = () => { - setInsertPlaceholder((insertPlaceholder) => !insertPlaceholder); - }; - const closeInsertPlaceholder = () => setInsertPlaceholder(false); - - const placeholderInsert = ( - - - Insert placeholder - - - ); - - const InsertPlaceholderPopover = () => { - return ( -
- - - -
- ); - }; const handleCreateEmailRecipient = ( searchValue: string, @@ -131,15 +59,15 @@ export const EmailDelivery = (props: ReportDeliveryProps) => { (option) => option.label.trim().toLowerCase() === normalizedSearchValue ) === -1 ) { - EMAIL_RECIPIENT_OPTIONS.push(newOption); + setOptions([...emailRecipients, newOption]); } // Select the option. - handleEmailRecipients([...emailRecipients, newOption]); + handleEmailRecipients([...selectedEmailRecipients, newOption]); }; const handleEmailRecipients = (e) => { - setEmailRecipients(e); + setSelectedEmailRecipients(e); reportDefinitionRequest.delivery.delivery_params.recipients = e.map( (option) => option.label ); @@ -150,11 +78,6 @@ export const EmailDelivery = (props: ReportDeliveryProps) => { reportDefinitionRequest.delivery.delivery_params.title = e.target.value; }; - const handleEmailFormat = (e) => { - setEmailFormat(e); - reportDefinitionRequest.delivery.delivery_params.email_format = e; - }; - const handleEmailBody = (e) => { setEmailBody(e); reportDefinitionRequest.delivery.delivery_params.textDescription = e; @@ -164,42 +87,34 @@ export const EmailDelivery = (props: ReportDeliveryProps) => { }; // TODO: need better handling when we add full support for kibana user report delivery - const emailBodyLabel = - emailFormat === 'Embedded HTML' - ? `Add optional message (${selectedTab} mode)` - : `Email body (${selectedTab} mode)`; - - const showPlaceholder = - emailFormat === 'Embedded HTML' ? null : ; + const optionalMessageLabel = `Add optional message (${selectedTab} mode)`; const defaultEditDeliveryParams = (delivery: DeliverySchemaType) => { //TODO: need better handle? - if (delivery.delivery_type === 'Kibana user') { - //@ts-ignore - const kibanaUserParams: KibanaUserSchemaType = delivery.delivery_params; - const { kibana_recipients } = kibanaUserParams; + // if the original notification setting is kibana user + if (delivery.delivery_type === DELIVERY_TYPE_OPTIONS[0].id) { defaultCreateDeliveryParams(); delete reportDefinitionRequest.delivery.delivery_params.kibana_recipients; } else { //@ts-ignore const emailParams: ChannelSchemaType = delivery.delivery_params; - const { recipients, title, textDescription, email_format } = emailParams; + const { recipients, title, textDescription } = emailParams; recipients.map((emailRecipient) => - handleCreateEmailRecipient(emailRecipient, emailRecipients) + handleCreateEmailRecipient(emailRecipient, selectedEmailRecipients) ); setEmailSubject(title); reportDefinitionRequest.delivery.delivery_params.title = title; + reportDefinitionRequest.delivery.delivery_params.origin = location.origin; handleEmailBody(textDescription); - handleEmailFormat(email_format); } }; const defaultCreateDeliveryParams = () => { reportDefinitionRequest.delivery.delivery_params = { - recipients: emailRecipients.map((option) => option.label), + recipients: selectedEmailRecipients.map((option) => option.label), title: emailSubject, - email_format: emailFormat, + origin: location.origin, //TODO: need better render textDescription: emailBody, htmlDescription: converter.makeHtml(emailBody), @@ -227,8 +142,8 @@ export const EmailDelivery = (props: ReportDeliveryProps) => { > { /> - - - - { /> - + { const { httpClient } = props; - httpClient .put(`../api/reporting/reportDefinitions/${reportDefinitionId}`, { body: JSON.stringify(metadata), diff --git a/kibana-reports/server/executor/createScheduledReport.ts b/kibana-reports/server/executor/createScheduledReport.ts index e98de7df..fb1be7d2 100644 --- a/kibana-reports/server/executor/createScheduledReport.ts +++ b/kibana-reports/server/executor/createScheduledReport.ts @@ -78,7 +78,8 @@ export const createScheduledReport = async ( notificationClient, esClient, reportId, - isScheduledTask + isScheduledTask, + logger ); } catch (error) { // update report instance with "error" state diff --git a/kibana-reports/server/model/index.ts b/kibana-reports/server/model/index.ts index f613362e..f6f3153c 100644 --- a/kibana-reports/server/model/index.ts +++ b/kibana-reports/server/model/index.ts @@ -29,7 +29,6 @@ import { REPORT_STATE, REPORT_DEFINITION_STATUS, DELIVERY_TYPE, - EMAIL_FORMAT, DEFAULT_MAX_SIZE, } from '../routes/utils/constants'; @@ -143,13 +142,12 @@ export const channelSchema = schema.object({ }), { minSize: 1 } ), + // TODO: consider add this field next to url-related fields. + // Need this to build the links in email + origin: schema.uri(), //e.g. https://xxxxx.com title: schema.string(), textDescription: schema.string(), htmlDescription: schema.maybe(schema.string()), - email_format: schema.oneOf([ - schema.literal(EMAIL_FORMAT.attachment), - schema.literal(EMAIL_FORMAT.embeddedHtml), - ]), channelIds: schema.maybe(schema.arrayOf(schema.string())), }); diff --git a/kibana-reports/server/routes/lib/createReport.ts b/kibana-reports/server/routes/lib/createReport.ts index b3977f43..6025a69c 100644 --- a/kibana-reports/server/routes/lib/createReport.ts +++ b/kibana-reports/server/routes/lib/createReport.ts @@ -115,7 +115,8 @@ export const createReport = async ( notificationClient, esClient, reportId, - isScheduledTask + isScheduledTask, + logger ); } } catch (error) { diff --git a/kibana-reports/server/routes/lib/createSchedule.ts b/kibana-reports/server/routes/lib/createSchedule.ts index ac6d53d2..01bc8d23 100644 --- a/kibana-reports/server/routes/lib/createSchedule.ts +++ b/kibana-reports/server/routes/lib/createSchedule.ts @@ -65,6 +65,5 @@ export const createSchedule = async ( */ return; } - // else if (triggerType == TRIGGER_TYPE.alerting) { //TODO: add alert-based scheduling logic [enhancement feature] }; diff --git a/kibana-reports/server/routes/lib/deliverReport.ts b/kibana-reports/server/routes/lib/deliverReport.ts index f14643db..e0045389 100644 --- a/kibana-reports/server/routes/lib/deliverReport.ts +++ b/kibana-reports/server/routes/lib/deliverReport.ts @@ -17,13 +17,10 @@ import { ReportSchemaType } from 'server/model'; import { ILegacyScopedClusterClient, ILegacyClusterClient, + Logger, } from '../../../../../src/core/server'; -import { - DELIVERY_TYPE, - EMAIL_FORMAT, - FORMAT, - REPORT_STATE, -} from '../utils/constants'; +import { DELIVERY_TYPE, REPORT_STATE } from '../utils/constants'; +import { composeEmbeddedHtml } from '../utils/notification/deliveryContentHelper'; import { callCluster, updateReportState } from '../utils/helpers'; import { CreateReportResultType } from '../utils/types'; @@ -33,7 +30,8 @@ export const deliverReport = async ( notificationClient: ILegacyScopedClusterClient | ILegacyClusterClient, esClient: ILegacyClusterClient | ILegacyScopedClusterClient, reportId: string, - isScheduledTask: boolean + isScheduledTask: boolean, + logger: Logger ) => { // check delivery type const delivery = report.report_definition.delivery; @@ -49,37 +47,73 @@ export const deliverReport = async ( if (deliveryType === DELIVERY_TYPE.channel) { // deliver through one of [Slack, Chime, Email] - //@ts-ignore - const { email_format: emailFormat, ...rest } = deliveryParams; - // compose request body - if (emailFormat === EMAIL_FORMAT.attachment) { - const reportFormat = - report.report_definition.report_params.core_params.report_format; - const attachment = { - fileName: reportData.fileName, - fileEncoding: reportFormat === FORMAT.csv ? 'text' : 'base64', - //TODO: figure out when this data field is actually needed - // fileContentType: 'application/pdf', - fileData: reportData.dataUrl, - }; - const deliveryBody = { - ...rest, - refTag: reportId, - attachment, - }; - - const res = await callCluster( - notificationClient, - 'notification.send', - { - body: deliveryBody, - }, - isScheduledTask - ); - //TODO: need better error handling or logging - } + const { + query_url: queryUrl, + report_definition: { + report_params: { report_name: reportName }, + }, + } = report; + const { htmlDescription, origin } = deliveryParams; + const originalQueryUrl = origin + queryUrl; + /** + * have to manually compose the url because the Kibana url for AES is.../_plugin/kibana/app/opendistro_kibana_reports#/report_details/${reportId} + * while default Kibana is just .../app/opendistro_kibana_reports#/report_details/${reportId} + */ + const reportDetailUrl = `${originalQueryUrl.replace( + /\/app\/.*$/i, + '' + )}/app/opendistro_kibana_reports#/report_details/${reportId}`; + const template = composeEmbeddedHtml( + htmlDescription, + originalQueryUrl, + reportDetailUrl, + reportName + ); + const deliveryBody = { + ...deliveryParams, + htmlDescription: template, + refTag: reportId, + }; + // send email + const notificationResp = await callCluster( + notificationClient, + 'notification.send', + { + body: deliveryBody, + }, + isScheduledTask + ); + /** + * notification plugin response example: + * { + "refTag": "jeWuU3UBp8p83fn6xwzB", + "recipients": [ + { + "recipient": "odfe@amazon.com", + "statusCode": 200, + "statusText": "Success" + }, + { + "recipient": "wrong.odfe@amazon.com", + "statusCode": 503, + "statusText": "sendEmail Error, SES status:400:Optional[Bad Request]" + } + ] + } + */ + logger.info( + `notification plugin response: ${JSON.stringify(notificationResp)}` + ); + notificationResp.recipients.map((recipient) => { + if (recipient.statusCode !== 200) { + throw new Error( + `Fail to deliver report ${JSON.stringify( + notificationResp.recipients + )}` + ); + } + }); } else { - //TODO: No attachment, use embedded html (not implemented yet) // empty kibana recipients array //TODO: tmp solution // @ts-ignore diff --git a/kibana-reports/server/routes/reportDefinition.ts b/kibana-reports/server/routes/reportDefinition.ts index 36d18e1e..d846b19b 100644 --- a/kibana-reports/server/routes/reportDefinition.ts +++ b/kibana-reports/server/routes/reportDefinition.ts @@ -100,14 +100,14 @@ export default function (router: IRouter) { request, response ): Promise> => { - const reportDefinition: ReportDefinitionSchemaType = request.body; + let reportDefinition = request.body; const savedReportDefinitionId = request.params.reportDefinitionId; const esClient = context.core.elasticsearch.legacy.client; //@ts-ignore const logger = context.reporting_plugin.logger; // input validation try { - reportDefinitionSchema.validate(reportDefinition); + reportDefinition = reportDefinitionSchema.validate(reportDefinition); } catch (error) { logger.error( `Failed input validation for update report definition ${error}` diff --git a/kibana-reports/server/routes/utils/constants.ts b/kibana-reports/server/routes/utils/constants.ts index 6060aa68..912658b7 100644 --- a/kibana-reports/server/routes/utils/constants.ts +++ b/kibana-reports/server/routes/utils/constants.ts @@ -71,11 +71,6 @@ export enum DELIVERY_TYPE { channel = 'Channel', } -export enum EMAIL_FORMAT { - embeddedHtml = 'Embedded HTML', - attachment = 'Attachment', -} - // https://www.elastic.co/guide/en/elasticsearch/reference/6.8/search-request-from-size.html export const DEFAULT_MAX_SIZE = 10000; diff --git a/kibana-reports/server/routes/utils/notification/deliveryContentHelper.ts b/kibana-reports/server/routes/utils/notification/deliveryContentHelper.ts new file mode 100644 index 00000000..3e36bd0e --- /dev/null +++ b/kibana-reports/server/routes/utils/notification/deliveryContentHelper.ts @@ -0,0 +1,45 @@ +/* + * 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 fs from 'fs'; +import cheerio from 'cheerio'; + +export const composeEmbeddedHtml = ( + htmlDescription: string, + originalQueryUrl: string, + reportDetailUrl: string, + reportName: string +) => { + //TODO: need to add AWS styling + const logoAsBase64 = fs.readFileSync( + `${__dirname}/notification_content_template/logo.png`, + 'base64' + ); + + const $ = cheerio.load( + fs.readFileSync( + `${__dirname}/notification_content_template/email_content_template.html` + ) + ); + // set each link and logo + $('.logo').attr('src', `data:image/png;base64,${logoAsBase64}`); + $('.report_name').attr('href', reportDetailUrl).text(reportName); + $('.report_snapshot').attr('href', originalQueryUrl); + $('.optional_message').html(htmlDescription); + //TODO: Add this once we have the actual link to download + // $('.report_download').attr('href', '') + + return $.root().html(); +}; diff --git a/kibana-reports/server/routes/utils/notification/notification_content_template/email_content_template.html b/kibana-reports/server/routes/utils/notification/notification_content_template/email_content_template.html new file mode 100644 index 00000000..d6481718 --- /dev/null +++ b/kibana-reports/server/routes/utils/notification/notification_content_template/email_content_template.html @@ -0,0 +1,471 @@ + + + + + + + Kibana Reports: %REPORT_TITLE% + + + + + A new Kibana report is available                 +                                 +                                 +                                 +                               + + + + + + + + + + + \ No newline at end of file diff --git a/kibana-reports/server/routes/utils/notification/notification_content_template/logo.png b/kibana-reports/server/routes/utils/notification/notification_content_template/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..46aa560ab0d47464ffc94bd4e876ffb07fcc4b19 GIT binary patch literal 9995 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 AOaK4? literal 0 HcmV?d00001 diff --git a/kibana-reports/yarn.lock b/kibana-reports/yarn.lock index 0568186a..bb10f111 100644 --- a/kibana-reports/yarn.lock +++ b/kibana-reports/yarn.lock @@ -2580,6 +2580,11 @@ bn.js@^5.1.1: resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.1.3.tgz#beca005408f642ebebea80b042b4d18d2ac0ee6b" integrity sha512-GkTiFpjFtUzU9CbMeJ5iazkCzGL3jrhzerzZIuqLABjbwRaFt33I9tUdSNryIptM+RxDet6OKm2WnLXzW51KsQ== +boolbase@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" + integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24= + brace-expansion@^1.1.7: version "1.1.11" resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" @@ -2900,6 +2905,18 @@ chardet@^0.7.0: resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== +cheerio@^1.0.0-rc.3: + version "1.0.0-rc.3" + resolved "https://registry.yarnpkg.com/cheerio/-/cheerio-1.0.0-rc.3.tgz#094636d425b2e9c0f4eb91a46c05630c9a1a8bf6" + integrity sha512-0td5ijfUPuubwLUu0OBoe98gZj8C/AA+RW3v67GPlGOrvxWjZmBXiBCRU+I8VEiNyJzjth40POfHiz2RB3gImA== + dependencies: + css-select "~1.2.0" + dom-serializer "~0.1.1" + entities "~1.1.1" + htmlparser2 "^3.9.1" + lodash "^4.15.0" + parse5 "^3.0.1" + chokidar@^2.1.8: version "2.1.8" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917" @@ -3343,6 +3360,21 @@ css-box-model@^1.2.0: dependencies: tiny-invariant "^1.0.6" +css-select@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/css-select/-/css-select-1.2.0.tgz#2b3a110539c5355f1cd8d314623e870b121ec858" + integrity sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg= + dependencies: + boolbase "~1.0.0" + css-what "2.1" + domutils "1.5.1" + nth-check "~1.0.1" + +css-what@2.1: + version "2.1.3" + resolved "https://registry.yarnpkg.com/css-what/-/css-what-2.1.3.tgz#a6d7604573365fe74686c3f311c56513d88285f2" + integrity sha512-a+EPoD+uZiNfh+5fxw2nO9QwFa6nJe2Or35fGY6Ipw1R3R4AGz1d1TEZrCegvw2YTmZ0jXirGYlzxxpYSHwpEg== + css.escape@^1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/css.escape/-/css.escape-1.5.1.tgz#42e27d4fa04ae32f931a4b4d4191fa9cddee97cb" @@ -3647,6 +3679,14 @@ dom-helpers@^5.0.1, dom-helpers@^5.1.3: "@babel/runtime" "^7.8.7" csstype "^3.0.2" +dom-serializer@0: + version "0.2.2" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.2.2.tgz#1afb81f533717175d478655debc5e332d9f9bb51" + integrity sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g== + dependencies: + domelementtype "^2.0.1" + entities "^2.0.0" + dom-serializer@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.0.1.tgz#79695eb49af3cd8abc8d93a73da382deb1ca0795" @@ -3656,6 +3696,14 @@ dom-serializer@^1.0.1: domhandler "^3.0.0" entities "^2.0.0" +dom-serializer@~0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.1.tgz#1ec4059e284babed36eec2941d4a970a189ce7c0" + integrity sha512-l0IU0pPzLWSHBcieZbpOKgkIn3ts3vAh7ZuFyXNwJxJXk/c4Gwj9xaTJwIDVQCXawWD0qb3IzMGH5rglQaO0XA== + dependencies: + domelementtype "^1.3.0" + entities "^1.1.1" + dom4@^2.1.5: version "2.1.5" resolved "https://registry.yarnpkg.com/dom4/-/dom4-2.1.5.tgz#f98a94eb67b340f0fa5b42b0ee9c38cda035428e" @@ -3666,6 +3714,11 @@ domain-browser@^1.1.1: resolved "https://registry.yarnpkg.com/domain-browser/-/domain-browser-1.2.0.tgz#3d31f50191a6749dd1375a7f522e823d42e54eda" integrity sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA== +domelementtype@1, domelementtype@^1.3.0, domelementtype@^1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-1.3.1.tgz#d048c44b37b0d10a7f2a3d5fee3f4333d790481f" + integrity sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w== + domelementtype@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.0.1.tgz#1f8bdfe91f5a78063274e803b4bdcedf6e94f94d" @@ -3678,6 +3731,13 @@ domexception@^2.0.1: dependencies: webidl-conversions "^5.0.0" +domhandler@^2.3.0: + version "2.4.2" + resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-2.4.2.tgz#8805097e933d65e85546f726d60f5eb88b44f803" + integrity sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA== + dependencies: + domelementtype "1" + domhandler@^3.0, domhandler@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-3.0.0.tgz#51cd13efca31da95bbb0c5bee3a48300e333b3e9" @@ -3690,6 +3750,22 @@ dompurify@^2.1.1: resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.1.1.tgz#b5aa988676b093a9c836d8b855680a8598af25fe" integrity sha512-NijiNVkS/OL8mdQL1hUbCD6uty/cgFpmNiuFxrmJ5YPH2cXrPKIewoixoji56rbZ6XBPmtM8GA8/sf9unlSuwg== +domutils@1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf" + integrity sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8= + dependencies: + dom-serializer "0" + domelementtype "1" + +domutils@^1.5.1: + version "1.7.0" + resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.7.0.tgz#56ea341e834e06e6748af7a1cb25da67ea9f8c2a" + integrity sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg== + dependencies: + dom-serializer "0" + domelementtype "1" + domutils@^2.0.0: version "2.2.0" resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.2.0.tgz#f3ce1610af5c30280bde1b71f84b018b958f32cf" @@ -3800,6 +3876,11 @@ enhanced-resolve@~0.9.0: memory-fs "^0.2.0" tapable "^0.1.8" +entities@^1.1.1, entities@~1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/entities/-/entities-1.1.2.tgz#bdfa735299664dfafd34529ed4f8522a275fea56" + integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w== + entities@^2.0.0: version "2.0.3" resolved "https://registry.yarnpkg.com/entities/-/entities-2.0.3.tgz#5c487e5742ab93c15abb5da22759b8590ec03b7f" @@ -4974,6 +5055,18 @@ html@^1.0.0: dependencies: concat-stream "^1.4.7" +htmlparser2@^3.9.1: + version "3.10.1" + resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.1.tgz#bd679dc3f59897b6a34bb10749c855bb53a9392f" + integrity sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ== + dependencies: + domelementtype "^1.3.1" + domhandler "^2.3.0" + domutils "^1.5.1" + entities "^1.1.1" + inherits "^2.0.1" + readable-stream "^3.1.1" + htmlparser2@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-4.1.0.tgz#9a4ef161f2e4625ebf7dfbe6c0a2f52d18a59e78" @@ -6340,7 +6433,7 @@ lodash.unescape@4.0.1: resolved "https://registry.yarnpkg.com/lodash.unescape/-/lodash.unescape-4.0.1.tgz#bf2249886ce514cda112fae9218cdc065211fc9c" integrity sha1-vyJJiGzlFM2hEvrpIYzcBlIR/Jw= -lodash@^4.0.0, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.4, lodash@^4.3.0, lodash@~4.17.10: +lodash@^4.0.0, lodash@^4.15.0, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.4, lodash@^4.3.0, lodash@~4.17.10: version "4.17.20" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== @@ -6889,6 +6982,13 @@ npm-run-path@^4.0.0: gauge "~2.7.3" set-blocking "~2.0.0" +nth-check@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.2.tgz#b2bd295c37e3dd58a3bf0700376663ba4d9cf05c" + integrity sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg== + dependencies: + boolbase "~1.0.0" + number-is-nan@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" @@ -7195,6 +7295,13 @@ parse5@5.1.1: resolved "https://registry.yarnpkg.com/parse5/-/parse5-5.1.1.tgz#f68e4e5ba1852ac2cadc00f4555fff6c2abb6178" integrity sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug== +parse5@^3.0.1: + version "3.0.3" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-3.0.3.tgz#042f792ffdd36851551cf4e9e066b3874ab45b5c" + integrity sha512-rgO9Zg5LLLkfJF9E6CCmXlSE4UVceloys8JrFqCcHloC3usd/kJCyPDwH2SOlzix2j3xaP9sUX3e8+kvkuleAA== + dependencies: + "@types/node" "*" + pascalcase@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" From 155a72ee9bc389dadfc6bd7090212cedb59fda03 Mon Sep 17 00:00:00 2001 From: Zhongnan Su Date: Thu, 22 Oct 2020 23:38:50 -0700 Subject: [PATCH 7/8] update UI --- .../main/report_details/report_details.tsx | 43 ++++++------------- .../report_definitions/delivery/delivery.tsx | 2 +- 2 files changed, 13 insertions(+), 32 deletions(-) diff --git a/kibana-reports/public/components/main/report_details/report_details.tsx b/kibana-reports/public/components/main/report_details/report_details.tsx index f1e361e5..f40d4f5c 100644 --- a/kibana-reports/public/components/main/report_details/report_details.tsx +++ b/kibana-reports/public/components/main/report_details/report_details.tsx @@ -147,20 +147,12 @@ export function ReportDetails(props) { scheduleDetails: `\u2014`, alertDetails: `\u2014`, channel: deliveryType, - kibanaRecipients: deliveryParams.kibana_recipients - ? deliveryParams.kibana_recipients - : `\u2014`, emailRecipients: deliveryType === 'Channel' ? deliveryParams.recipients : `\u2014`, emailSubject: deliveryType === 'Channel' ? deliveryParams.title : `\u2014`, emailBody: deliveryType === 'Channel' ? deliveryParams.textDescription : `\u2014`, - reportAsAttachment: - deliveryType === 'Channel' && - deliveryParams.email_format === 'Attachment' - ? 'True' - : 'False', queryUrl: queryUrl, }; return reportDetails; @@ -194,9 +186,16 @@ export function ReportDetails(props) { let formatUpper = data['defaultFileFormat']; formatUpper = fileFormatsUpper[formatUpper]; return ( - { - generateReportById(reportId, props.httpClient, handleSuccessToast, handleErrorToast); - }}> + { + generateReportById( + reportId, + props.httpClient, + handleSuccessToast, + handleErrorToast + ); + }} + > {formatUpper + ' '} @@ -307,18 +306,10 @@ export function ReportDetails(props) { -

Delivery settings

+

Notification settings

- - - - - - - diff --git a/kibana-reports/public/components/report_definitions/delivery/delivery.tsx b/kibana-reports/public/components/report_definitions/delivery/delivery.tsx index 530296f8..e2286997 100644 --- a/kibana-reports/public/components/report_definitions/delivery/delivery.tsx +++ b/kibana-reports/public/components/report_definitions/delivery/delivery.tsx @@ -90,7 +90,7 @@ export function ReportDelivery(props: ReportDeliveryProps) { -

Delivery settings

+

Notification settings

From e9da8cede1258e1fe7c2efe851296b189cdda923 Mon Sep 17 00:00:00 2001 From: Zhongnan Su Date: Fri, 23 Oct 2020 10:29:44 -0700 Subject: [PATCH 8/8] address comments --- .../report_definitions/edit/edit_report_definition.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/kibana-reports/public/components/report_definitions/edit/edit_report_definition.tsx b/kibana-reports/public/components/report_definitions/edit/edit_report_definition.tsx index f29dc7e9..67f25dc3 100644 --- a/kibana-reports/public/components/report_definitions/edit/edit_report_definition.tsx +++ b/kibana-reports/public/components/report_definitions/edit/edit_report_definition.tsx @@ -97,6 +97,7 @@ export function EditReportDefinition(props) { const callUpdateAPI = async (metadata) => { const { httpClient } = props; + httpClient .put(`../api/reporting/reportDefinitions/${reportDefinitionId}`, { body: JSON.stringify(metadata),