Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[7.x] [Reporting/UI Settings] Validation for the Reporting UI Setting Custom Logo (#94746) #94850

Merged
merged 1 commit into from
Mar 17, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion x-pack/plugins/reporting/server/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@
* 2.0.
*/

import { get } from 'lodash';
import { PluginConfigDescriptor } from 'kibana/server';
import { get } from 'lodash';

import { ConfigSchema, ReportingConfigType } from './schema';
export { buildConfig } from './config';
export { registerUiSettings } from './ui_settings';
export { ConfigSchema, ReportingConfigType };

export const config: PluginConfigDescriptor<ReportingConfigType> = {
Expand Down
71 changes: 71 additions & 0 deletions x-pack/plugins/reporting/server/config/ui_settings.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { range } from 'lodash';
import { PdfLogoSchema } from './ui_settings';

test('validates when provided with image data', () => {
const jpgString =
`data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAoHCBcUFBUUExUYGRUaGRsZGxsZHB8bIh0iGhgbGxkbGx8dIy0kGx0rIiIbJTcoKi8xNDU0ISY6Pzo2` +
`+8snFz9eWgvYKS4ZsvS05zRQsDveIzH4Er4iDtr6iICIiAiIgIiICIiD//2Q==`;
expect(PdfLogoSchema.validate(jpgString)).toBe(jpgString);

const pngString =
`data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAO4AAADUCAMAAACs0e/bAAAAjVBMVEX////8/Pz4+Pj5+fnb29vz8/Px8fFeXl7r6+u/v79nZ` +
`tcAAAAASUVORK5CYII=`;
expect(PdfLogoSchema.validate(pngString)).toBe(pngString);

const gifString =
`data:image/gif;base64,R0lGODlhoADIAPYAAO/w7wgFBwsLCxMTExsbGyMjI5SUlLS0tLu7u9vb2+Hh4e/v7/Ds7////0NDQ2RkZCkXJO/w8PLy8g8QD` +
`53IIefTH3WR4N8lXzvKWu/zlMI+5zGdO85rb/OY4z7nOd87znvv850APutCHTvSiG/3oSE+60pfO9KY7/elQj7rU5xIIADs=`;
expect(PdfLogoSchema.validate(gifString)).toBe(gifString);

const svgString =
`data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+CjwhLS0gQ3JlYXRlZCB3aXR` +
`AgPC9nPgogIDwvZz4KPC9zdmc+Cg==`;
expect(PdfLogoSchema.validate(svgString)).toBe(svgString);
});

test('validates if provided with null / undefined value', () => {
expect(() => PdfLogoSchema.validate(undefined)).not.toThrow();
expect(() => PdfLogoSchema.validate(null)).not.toThrow();
});

test('throws validation error if provided with data over max size', () => {
const largeJpgMock =
`data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wCEAAoHCBcUFBUUExUYGRUaGRsZGxsZHB8bIh0iGhgbGxkbGx8dIy0kGx0rIiIbJTcoKi8xNDU0ISY6Pzo2` +
range(0, 2050)
.map(
() =>
`Pi0zNDMBCwsLBgYGEAYGEDEcFRwxMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMTExMf/AABEIAOgA2gMBIgACEQEDEQH/xAAcAAEAAgMBAQE`
)
.join('') +
`+8snFz9eWgvYKS4ZsvS05zRQsDveIzH4Er4iDtr6iICIiAiIgIiICIiD//2Q==`;
expect(() => PdfLogoSchema.validate(largeJpgMock)).toThrowError(/too large/);
});

test('throws validation error if provided with non-image data', () => {
const invalidErrorMatcher = /try a different image/;

expect(() => PdfLogoSchema.validate('')).toThrowError(invalidErrorMatcher);
expect(() => PdfLogoSchema.validate(true)).toThrow(invalidErrorMatcher);
expect(() => PdfLogoSchema.validate(false)).toThrow(invalidErrorMatcher);
expect(() => PdfLogoSchema.validate({})).toThrow(invalidErrorMatcher);
expect(() => PdfLogoSchema.validate([])).toThrow(invalidErrorMatcher);
expect(() => PdfLogoSchema.validate(0)).toThrow(invalidErrorMatcher);
expect(() => PdfLogoSchema.validate(0x00f)).toThrow(invalidErrorMatcher);

const csvString =
`data:text/csv;base64,Il9pZCIsIl9pbmRleCIsIl9zY29yZSIsIl90eXBlIiwiZm9vLmJhciIsImZvby5iYXIua2V5d29yZCIKZjY1QU9IZ0J5bFZmWW04W` +
`TRvb1EsYmVlLDEsIi0iLGJheixiYXoKbks1QU9IZ0J5bFZmWW04WTdZcUcsYmVlLDEsIi0iLGJvbyxib28K`;
expect(() => PdfLogoSchema.validate(csvString)).toThrow(invalidErrorMatcher);

const scriptString =
`data:application/octet-stream;base64,QEVDSE8gT0ZGCldFRUtPRllSLkNPTSB8IEZJTkQgIlRoaXMgaXMiID4gVEVNUC5CQV` +
`QKRUNITz5USElTLkJBVCBTRVQgV0VFSz0lJTMKQ0FMTCBURU1QLkJBVApERUwgIFRFTVAuQkFUCkRFTCAgVEhJUy5CQVQKRUNITyBXZWVrICVXRUVLJQo=`;
expect(() => PdfLogoSchema.validate(scriptString)).toThrow(invalidErrorMatcher);
});
81 changes: 81 additions & 0 deletions x-pack/plugins/reporting/server/config/ui_settings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

import { schema } from '@kbn/config-schema';
import { i18n } from '@kbn/i18n';
import { CoreSetup, UiSettingsParams } from 'kibana/server';
import { PLUGIN_ID, UI_SETTINGS_CUSTOM_PDF_LOGO } from '../../common/constants';

const kbToBase64Length = (kb: number) => Math.floor((kb * 1024 * 8) / 6);
const maxLogoSizeInKilobytes = kbToBase64Length(200);

// inspired by x-pack/plugins/canvas/common/lib/dataurl.ts
const dataurlRegex = /^data:([a-z]+\/[a-z0-9-+.]+)(;[a-z-]+=[a-z0-9-]+)?(;([a-z0-9]+))?,/;
const imageTypes = ['image/svg+xml', 'image/jpeg', 'image/png', 'image/gif'];

const isImageData = (str: any): boolean => {
const matches = str.match(dataurlRegex);

if (!matches) {
return false;
}

const [, mimetype, , , encoding] = matches;
const imageTypeIndex = imageTypes.indexOf(mimetype);
if (imageTypeIndex < 0 || encoding !== 'base64') {
return false;
}

return true;
};

const isLessThanMaxSize = (str: any) => {
if (str.length > maxLogoSizeInKilobytes) {
return false;
}

return true;
};

const validatePdfLogoBase64String = (str: any) => {
if (typeof str !== 'string' || !isImageData(str)) {
return i18n.translate('xpack.reporting.uiSettings.validate.customLogo.badFile', {
defaultMessage: `Sorry, that file will not work. Please try a different image file.`,
});
}
if (!isLessThanMaxSize(str)) {
return i18n.translate('xpack.reporting.uiSettings.validate.customLogo.tooLarge', {
defaultMessage: `Sorry, that file is too large. The image file must be less than 200 kilobytes.`,
});
}
};

export const PdfLogoSchema = schema.nullable(schema.any({ validate: validatePdfLogoBase64String }));

export function registerUiSettings(core: CoreSetup<object, unknown>) {
core.uiSettings.register({
[UI_SETTINGS_CUSTOM_PDF_LOGO]: {
name: i18n.translate('xpack.reporting.pdfFooterImageLabel', {
defaultMessage: 'PDF footer image',
}),
value: null,
description: i18n.translate('xpack.reporting.pdfFooterImageDescription', {
defaultMessage: `Custom image to use in the PDF's footer`,
}),
sensitive: true,
type: 'image',
schema: PdfLogoSchema,
category: [PLUGIN_ID],
validation: {
maxSize: {
length: maxLogoSizeInKilobytes,
description: '200 kB',
},
},
},
} as Record<string, UiSettingsParams<null>>);
}
42 changes: 11 additions & 31 deletions x-pack/plugins/reporting/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,22 @@
* 2.0.
*/

import { schema } from '@kbn/config-schema';
import { i18n } from '@kbn/i18n';
import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'src/core/server';
import { PLUGIN_ID, UI_SETTINGS_CUSTOM_PDF_LOGO } from '../common/constants';
import type { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'src/core/server';
import { PLUGIN_ID } from '../common/constants';
import { ReportingCore } from './';
import { initializeBrowserDriverFactory } from './browsers';
import { buildConfig, ReportingConfigType } from './config';
import { buildConfig, registerUiSettings, ReportingConfigType } from './config';
import { createQueueFactory, LevelLogger, ReportingStore } from './lib';
import { registerRoutes } from './routes';
import { setFieldFormats } from './services';
import { ReportingSetup, ReportingSetupDeps, ReportingStart, ReportingStartDeps } from './types';
import type {
ReportingRequestHandlerContext,
ReportingSetup,
ReportingSetupDeps,
ReportingStart,
ReportingStartDeps,
} from './types';
import { registerReportingUsageCollector } from './usage';
import type { ReportingRequestHandlerContext } from './types';

const kbToBase64Length = (kb: number) => Math.floor((kb * 1024 * 8) / 6);

export class ReportingPlugin
implements Plugin<ReportingSetup, ReportingStart, ReportingSetupDeps, ReportingStartDeps> {
Expand All @@ -44,28 +45,7 @@ export class ReportingPlugin
}
});

core.uiSettings.register({
[UI_SETTINGS_CUSTOM_PDF_LOGO]: {
name: i18n.translate('xpack.reporting.pdfFooterImageLabel', {
defaultMessage: 'PDF footer image',
}),
value: null,
description: i18n.translate('xpack.reporting.pdfFooterImageDescription', {
defaultMessage: `Custom image to use in the PDF's footer`,
}),
sensitive: true,
type: 'image',
schema: schema.nullable(schema.byteSize({ max: '200kb' })),
category: [PLUGIN_ID],
// Used client-side for size validation
validation: {
maxSize: {
length: kbToBase64Length(200),
description: '200 kB',
},
},
},
});
registerUiSettings(core);

const { elasticsearch, http } = core;
const { features, licensing, security, spaces } = plugins;
Expand Down