diff --git a/src/plugins/files/public/mocks.ts b/src/plugins/files/public/mocks.ts index c22d9bda24608..447f8c2b85d54 100644 --- a/src/plugins/files/public/mocks.ts +++ b/src/plugins/files/public/mocks.ts @@ -8,10 +8,25 @@ import { createMockFilesClient as createBaseMocksFilesClient } from '@kbn/shared-ux-file-mocks'; import type { DeeplyMockedKeys } from '@kbn/utility-types-jest'; -import type { FilesClient } from './types'; +import { FilesSetup } from '.'; +import type { FilesClient, FilesClientFactory } from './types'; export const createMockFilesClient = (): DeeplyMockedKeys => ({ ...createBaseMocksFilesClient(), getMetrics: jest.fn(), publicDownload: jest.fn(), }); + +export const createMockFilesSetup = (): DeeplyMockedKeys => { + return { + filesClientFactory: createMockFilesClientFactory(), + registerFileKind: jest.fn(), + }; +}; + +export const createMockFilesClientFactory = (): DeeplyMockedKeys => { + return { + asScoped: jest.fn(), + asUnscoped: jest.fn(), + }; +}; diff --git a/src/plugins/files/server/mocks.ts b/src/plugins/files/server/mocks.ts index 60f0b5c38ee8d..8472717b544a1 100644 --- a/src/plugins/files/server/mocks.ts +++ b/src/plugins/files/server/mocks.ts @@ -10,7 +10,7 @@ import { KibanaRequest } from '@kbn/core/server'; import { DeeplyMockedKeys } from '@kbn/utility-types-jest'; import * as stream from 'stream'; import { File } from '../common'; -import { FileClient, FileServiceFactory, FileServiceStart } from '.'; +import { FileClient, FileServiceFactory, FileServiceStart, FilesSetup } from '.'; export const createFileServiceMock = (): DeeplyMockedKeys => ({ create: jest.fn(), @@ -78,3 +78,9 @@ export const createFileClientMock = (): DeeplyMockedKeys => { listShares: jest.fn().mockResolvedValue({ shares: [] }), }; }; + +export const createFilesSetupMock = (): DeeplyMockedKeys => { + return { + registerFileKind: jest.fn(), + }; +}; diff --git a/test/plugin_functional/test_suites/core_plugins/rendering.ts b/test/plugin_functional/test_suites/core_plugins/rendering.ts index 0ce5819e3f973..2e462dd66bb1b 100644 --- a/test/plugin_functional/test_suites/core_plugins/rendering.ts +++ b/test/plugin_functional/test_suites/core_plugins/rendering.ts @@ -165,6 +165,8 @@ export default function ({ getService }: PluginFunctionalProviderContext) { 'xpack.apm.serviceMapEnabled (boolean)', 'xpack.apm.ui.enabled (boolean)', 'xpack.apm.ui.maxTraceItems (number)', + 'xpack.cases.files.allowedMimeTypes (array)', + 'xpack.cases.files.maxSize (number)', 'xpack.cases.markdownPlugins.lens (boolean)', 'xpack.ccr.ui.enabled (boolean)', 'xpack.cloud.base_url (string)', diff --git a/x-pack/plugins/cases/common/constants/files.ts b/x-pack/plugins/cases/common/constants/files.ts index 572b612e2efc2..3c7a8d80d9ae6 100644 --- a/x-pack/plugins/cases/common/constants/files.ts +++ b/x-pack/plugins/cases/common/constants/files.ts @@ -6,5 +6,6 @@ */ export const MAX_FILE_SIZE = 100 * 1024 * 1024; // 100 MiB +export const MAX_IMAGE_FILE_SIZE = 10 * 1024 * 1024; // 10 MiB export const MAX_FILES_PER_CASE = 100; export const MAX_DELETE_FILES = 50; diff --git a/x-pack/plugins/cases/common/constants/owners.ts b/x-pack/plugins/cases/common/constants/owners.ts index 60463fa57a976..3e799030c7d5b 100644 --- a/x-pack/plugins/cases/common/constants/owners.ts +++ b/x-pack/plugins/cases/common/constants/owners.ts @@ -15,7 +15,7 @@ export const SECURITY_SOLUTION_OWNER = 'securitySolution' as const; export const OBSERVABILITY_OWNER = 'observability' as const; export const GENERAL_CASES_OWNER = APP_ID; -export const OWNERS = [SECURITY_SOLUTION_OWNER, OBSERVABILITY_OWNER, GENERAL_CASES_OWNER] as const; +export const OWNERS = [GENERAL_CASES_OWNER, OBSERVABILITY_OWNER, SECURITY_SOLUTION_OWNER] as const; interface RouteInfo { id: Owner; diff --git a/x-pack/plugins/cases/common/ui/types.ts b/x-pack/plugins/cases/common/ui/types.ts index 06765eb17ca4f..e9bfcd0682f5b 100644 --- a/x-pack/plugins/cases/common/ui/types.ts +++ b/x-pack/plugins/cases/common/ui/types.ts @@ -52,6 +52,10 @@ export interface CasesUiConfigType { markdownPlugins: { lens: boolean; }; + files: { + maxSize?: number; + allowedMimeTypes: string[]; + }; } export const StatusAll = 'all' as const; diff --git a/x-pack/plugins/cases/public/common/lib/kibana/services.ts b/x-pack/plugins/cases/public/common/lib/kibana/services.ts index f4c546a553881..ab06c1be0bf02 100644 --- a/x-pack/plugins/cases/public/common/lib/kibana/services.ts +++ b/x-pack/plugins/cases/public/common/lib/kibana/services.ts @@ -19,7 +19,10 @@ export class KibanaServices { http, kibanaVersion, config, - }: GlobalServices & { kibanaVersion: string; config: CasesUiConfigType }) { + }: GlobalServices & { + kibanaVersion: string; + config: CasesUiConfigType; + }) { this.services = { http }; this.kibanaVersion = kibanaVersion; this.config = config; diff --git a/x-pack/plugins/cases/public/files/index.test.ts b/x-pack/plugins/cases/public/files/index.test.ts new file mode 100644 index 0000000000000..3db225cf35d6f --- /dev/null +++ b/x-pack/plugins/cases/public/files/index.test.ts @@ -0,0 +1,98 @@ +/* + * 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 { MAX_FILE_SIZE } from '../../common/constants'; +import { createMockFilesSetup } from '@kbn/files-plugin/public/mocks'; +import { registerCaseFileKinds } from '.'; +import type { FilesConfig } from './types'; + +describe('ui files index', () => { + describe('registerCaseFileKinds', () => { + const mockFilesSetup = createMockFilesSetup(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('allowedMimeTypes', () => { + const config: FilesConfig = { + allowedMimeTypes: ['abc'], + maxSize: undefined, + }; + + beforeEach(() => { + registerCaseFileKinds(config, mockFilesSetup); + }); + + it('sets cases allowed mime types to abc', () => { + expect(mockFilesSetup.registerFileKind.mock.calls[0][0].allowedMimeTypes).toEqual(['abc']); + }); + + it('sets observability allowed mime types to abc', () => { + expect(mockFilesSetup.registerFileKind.mock.calls[1][0].allowedMimeTypes).toEqual(['abc']); + }); + + it('sets securitySolution allowed mime types to 100 mb', () => { + expect(mockFilesSetup.registerFileKind.mock.calls[2][0].allowedMimeTypes).toEqual(['abc']); + }); + }); + + describe('max file size', () => { + describe('default max file size', () => { + const config: FilesConfig = { + allowedMimeTypes: [], + maxSize: undefined, + }; + + beforeEach(() => { + registerCaseFileKinds(config, mockFilesSetup); + }); + + it('sets cases max file size to 100 mb', () => { + expect(mockFilesSetup.registerFileKind.mock.calls[0][0].maxSizeBytes).toEqual( + MAX_FILE_SIZE + ); + }); + + it('sets observability max file size to 100 mb', () => { + expect(mockFilesSetup.registerFileKind.mock.calls[1][0].maxSizeBytes).toEqual( + MAX_FILE_SIZE + ); + }); + + it('sets securitySolution max file size to 100 mb', () => { + expect(mockFilesSetup.registerFileKind.mock.calls[2][0].maxSizeBytes).toEqual( + MAX_FILE_SIZE + ); + }); + }); + + describe('custom file size', () => { + const config: FilesConfig = { + allowedMimeTypes: [], + maxSize: 5, + }; + + beforeEach(() => { + registerCaseFileKinds(config, mockFilesSetup); + }); + + it('sets cases max file size to 5', () => { + expect(mockFilesSetup.registerFileKind.mock.calls[0][0].maxSizeBytes).toEqual(5); + }); + + it('sets observability max file size to 5', () => { + expect(mockFilesSetup.registerFileKind.mock.calls[1][0].maxSizeBytes).toEqual(5); + }); + + it('sets securitySolution max file size to 5', () => { + expect(mockFilesSetup.registerFileKind.mock.calls[2][0].maxSizeBytes).toEqual(5); + }); + }); + }); + }); +}); diff --git a/x-pack/plugins/cases/public/files/index.ts b/x-pack/plugins/cases/public/files/index.ts index f9490323085af..30a406c788f63 100644 --- a/x-pack/plugins/cases/public/files/index.ts +++ b/x-pack/plugins/cases/public/files/index.ts @@ -7,31 +7,36 @@ import type { FilesSetup } from '@kbn/files-plugin/public'; import type { FileKindBrowser } from '@kbn/shared-ux-file-types'; -import { ALLOWED_MIME_TYPES } from '../../common/constants/mime_types'; -import { MAX_FILE_SIZE } from '../../common/constants'; +import { MAX_FILE_SIZE, OWNERS } from '../../common/constants'; import type { Owner } from '../../common/constants/types'; -import { APP_ID, OBSERVABILITY_OWNER, SECURITY_SOLUTION_OWNER } from '../../common'; import { constructFileKindIdByOwner } from '../../common/files'; +import type { CaseFileKinds, FilesConfig } from './types'; -const buildFileKind = (owner: Owner): FileKindBrowser => { +const buildFileKind = (config: FilesConfig, owner: Owner): FileKindBrowser => { return { id: constructFileKindIdByOwner(owner), - allowedMimeTypes: ALLOWED_MIME_TYPES, - maxSizeBytes: MAX_FILE_SIZE, + allowedMimeTypes: config.allowedMimeTypes, + maxSizeBytes: config.maxSize ?? MAX_FILE_SIZE, }; }; /** * The file kind definition for interacting with the file service for the UI */ -const CASES_FILE_KINDS: Record = { - [APP_ID]: buildFileKind(APP_ID), - [SECURITY_SOLUTION_OWNER]: buildFileKind(SECURITY_SOLUTION_OWNER), - [OBSERVABILITY_OWNER]: buildFileKind(OBSERVABILITY_OWNER), +const createFileKinds = (config: FilesConfig): CaseFileKinds => { + const caseFileKinds = new Map(); + + for (const owner of OWNERS) { + caseFileKinds.set(owner, buildFileKind(config, owner)); + } + + return caseFileKinds; }; -export const registerCaseFileKinds = (filesSetupPlugin: FilesSetup) => { - for (const fileKind of Object.values(CASES_FILE_KINDS)) { +export const registerCaseFileKinds = (config: FilesConfig, filesSetupPlugin: FilesSetup) => { + const fileKinds = createFileKinds(config); + + for (const fileKind of fileKinds.values()) { filesSetupPlugin.registerFileKind(fileKind); } }; diff --git a/x-pack/plugins/cases/public/files/types.ts b/x-pack/plugins/cases/public/files/types.ts new file mode 100644 index 0000000000000..d7c4563eb1c06 --- /dev/null +++ b/x-pack/plugins/cases/public/files/types.ts @@ -0,0 +1,14 @@ +/* + * 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 type { FileKindBrowser } from '@kbn/shared-ux-file-types'; +import type { Owner } from '../../common/constants/types'; +import type { CasesUiConfigType } from '../containers/types'; + +export type FilesConfig = CasesUiConfigType['files']; + +export type CaseFileKinds = Map; diff --git a/x-pack/plugins/cases/public/plugin.ts b/x-pack/plugins/cases/public/plugin.ts index 4053e35aa6cfc..b1ab9f32700dd 100644 --- a/x-pack/plugins/cases/public/plugin.ts +++ b/x-pack/plugins/cases/public/plugin.ts @@ -53,7 +53,8 @@ export class CasesUiPlugin const externalReferenceAttachmentTypeRegistry = this.externalReferenceAttachmentTypeRegistry; const persistableStateAttachmentTypeRegistry = this.persistableStateAttachmentTypeRegistry; - registerCaseFileKinds(plugins.files); + const config = this.initializerContext.config.get(); + registerCaseFileKinds(config.files, plugins.files); if (plugins.home) { plugins.home.featureCatalogue.register({ @@ -106,7 +107,13 @@ export class CasesUiPlugin public start(core: CoreStart, plugins: CasesPluginStart): CasesUiStart { const config = this.initializerContext.config.get(); - KibanaServices.init({ ...core, ...plugins, kibanaVersion: this.kibanaVersion, config }); + + KibanaServices.init({ + ...core, + ...plugins, + kibanaVersion: this.kibanaVersion, + config, + }); /** * getCasesContextLazy returns a new component each time is being called. To avoid re-renders diff --git a/x-pack/plugins/cases/server/config.test.ts b/x-pack/plugins/cases/server/config.test.ts new file mode 100644 index 0000000000000..54fc42f694bc2 --- /dev/null +++ b/x-pack/plugins/cases/server/config.test.ts @@ -0,0 +1,113 @@ +/* + * 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 { ConfigSchema } from './config'; + +describe('config validation', () => { + describe('defaults', () => { + it('sets the defaults correctly', () => { + expect(ConfigSchema.validate({})).toMatchInlineSnapshot(` + Object { + "files": Object { + "allowedMimeTypes": Array [ + "image/aces", + "image/apng", + "image/avci", + "image/avcs", + "image/avif", + "image/bmp", + "image/cgm", + "image/dicom-rle", + "image/dpx", + "image/emf", + "image/example", + "image/fits", + "image/g3fax", + "image/heic", + "image/heic-sequence", + "image/heif", + "image/heif-sequence", + "image/hej2k", + "image/hsj2", + "image/jls", + "image/jp2", + "image/jpeg", + "image/jph", + "image/jphc", + "image/jpm", + "image/jpx", + "image/jxr", + "image/jxrA", + "image/jxrS", + "image/jxs", + "image/jxsc", + "image/jxsi", + "image/jxss", + "image/ktx", + "image/ktx2", + "image/naplps", + "image/png", + "image/prs.btif", + "image/prs.pti", + "image/pwg-raster", + "image/svg+xml", + "image/t38", + "image/tiff", + "image/tiff-fx", + "image/vnd.adobe.photoshop", + "image/vnd.airzip.accelerator.azv", + "image/vnd.cns.inf2", + "image/vnd.dece.graphic", + "image/vnd.djvu", + "image/vnd.dwg", + "image/vnd.dxf", + "image/vnd.dvb.subtitle", + "image/vnd.fastbidsheet", + "image/vnd.fpx", + "image/vnd.fst", + "image/vnd.fujixerox.edmics-mmr", + "image/vnd.fujixerox.edmics-rlc", + "image/vnd.globalgraphics.pgb", + "image/vnd.microsoft.icon", + "image/vnd.mix", + "image/vnd.ms-modi", + "image/vnd.mozilla.apng", + "image/vnd.net-fpx", + "image/vnd.pco.b16", + "image/vnd.radiance", + "image/vnd.sealed.png", + "image/vnd.sealedmedia.softseal.gif", + "image/vnd.sealedmedia.softseal.jpg", + "image/vnd.svf", + "image/vnd.tencent.tap", + "image/vnd.valve.source.texture", + "image/vnd.wap.wbmp", + "image/vnd.xiff", + "image/vnd.zbrush.pcx", + "image/webp", + "image/wmf", + "text/plain", + "text/csv", + "text/json", + "application/json", + "application/zip", + "application/gzip", + "application/x-bzip", + "application/x-bzip2", + "application/x-7z-compressed", + "application/x-tar", + "application/pdf", + ], + }, + "markdownPlugins": Object { + "lens": true, + }, + } + `); + }); + }); +}); diff --git a/x-pack/plugins/cases/server/config.ts b/x-pack/plugins/cases/server/config.ts index dc110412d1733..c2daeb73b03de 100644 --- a/x-pack/plugins/cases/server/config.ts +++ b/x-pack/plugins/cases/server/config.ts @@ -7,11 +7,19 @@ import type { TypeOf } from '@kbn/config-schema'; import { schema } from '@kbn/config-schema'; +import { ALLOWED_MIME_TYPES } from '../common/constants/mime_types'; export const ConfigSchema = schema.object({ markdownPlugins: schema.object({ lens: schema.boolean({ defaultValue: true }), }), + files: schema.object({ + allowedMimeTypes: schema.arrayOf(schema.string({ minLength: 1 }), { + defaultValue: ALLOWED_MIME_TYPES, + }), + // intentionally not setting a default here so that we can determine if the user set it + maxSize: schema.maybe(schema.number({ min: 0 })), + }), }); export type ConfigType = TypeOf; diff --git a/x-pack/plugins/cases/server/files/index.test.ts b/x-pack/plugins/cases/server/files/index.test.ts new file mode 100644 index 0000000000000..29c58c8b1d685 --- /dev/null +++ b/x-pack/plugins/cases/server/files/index.test.ts @@ -0,0 +1,720 @@ +/* + * 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 { MAX_FILE_SIZE, MAX_IMAGE_FILE_SIZE } from '../../common/constants'; +import { createFilesSetupMock } from '@kbn/files-plugin/server/mocks'; +import type { FileJSON } from '@kbn/shared-ux-file-types'; +import { createMaxCallback, registerCaseFileKinds } from '.'; +import { ConfigSchema } from '../config'; + +describe('server files', () => { + describe('registerCaseFileKinds', () => { + const mockFilesSetup = createFilesSetupMock(); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('file sizes', () => { + it('sets the max image file size to 10 mb', () => { + const schema = ConfigSchema.validate({}); + + const maxFileSizeFn = createMaxCallback(schema.files); + + expect( + maxFileSizeFn({ + mimeType: 'image/png', + } as unknown as FileJSON) + ).toEqual(MAX_IMAGE_FILE_SIZE); + }); + + it('sets the max file size to 1 when an image is passed but the configuration was specified', () => { + const schema = ConfigSchema.validate({ + files: { + maxSize: 1, + }, + }); + + const maxFileSizeFn = createMaxCallback(schema.files); + + expect( + maxFileSizeFn({ + mimeType: 'image/png', + } as unknown as FileJSON) + ).toEqual(1); + }); + + it('sets the max non-image file size to default 100 mb', () => { + const schema = ConfigSchema.validate({}); + + const maxFileSizeFn = createMaxCallback(schema.files); + + expect( + maxFileSizeFn({ + mimeType: 'text/plain', + } as unknown as FileJSON) + ).toEqual(MAX_FILE_SIZE); + }); + + it('returns 100 mb when images are not allowed in the mime type and an image is passed', () => { + const schemaNoImages = ConfigSchema.validate({ + files: { + allowedMimeTypes: ['abc/123'], + }, + }); + + const maxFn = createMaxCallback(schemaNoImages.files); + + expect( + maxFn({ + mimeType: 'image/png', + } as unknown as FileJSON) + ).toEqual(MAX_FILE_SIZE); + }); + + it('returns 100 mb when the mime type is not recognized', () => { + const schemaNoImages = ConfigSchema.validate({ + files: { + allowedMimeTypes: ['abc/123'], + }, + }); + + const maxFn = createMaxCallback(schemaNoImages.files); + + expect( + maxFn({ + mimeType: 'hi/bye', + } as unknown as FileJSON) + ).toEqual(MAX_FILE_SIZE); + }); + + it('returns 100 mb when the mime type is undefined', () => { + const schemaNoImages = ConfigSchema.validate({ + files: { + allowedMimeTypes: ['abc/123'], + }, + }); + + const maxFn = createMaxCallback(schemaNoImages.files); + + expect( + maxFn({ + mimeType: undefined, + } as unknown as FileJSON) + ).toEqual(MAX_FILE_SIZE); + }); + }); + + describe('allowed mime types', () => { + describe('image png', () => { + const schema = ConfigSchema.validate({ files: { allowedMimeTypes: ['image/png'] } }); + + it('sets the cases file kind allowed mime types to only image png', () => { + registerCaseFileKinds(schema.files, mockFilesSetup); + + expect(mockFilesSetup.registerFileKind.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "allowedMimeTypes": Array [ + "image/png", + ], + "http": Object { + "create": Object { + "tags": Array [ + "access:casesFilesCasesCreate", + ], + }, + "download": Object { + "tags": Array [ + "access:casesFilesCasesRead", + ], + }, + "getById": Object { + "tags": Array [ + "access:casesFilesCasesRead", + ], + }, + "list": Object { + "tags": Array [ + "access:casesFilesCasesRead", + ], + }, + }, + "id": "casesFilesCases", + "maxSizeBytes": [Function], + }, + ] + `); + }); + + it('sets the observability file kind allowed mime types to only image png', () => { + registerCaseFileKinds(schema.files, mockFilesSetup); + + expect(mockFilesSetup.registerFileKind.mock.calls[1]).toMatchInlineSnapshot(` + Array [ + Object { + "allowedMimeTypes": Array [ + "image/png", + ], + "http": Object { + "create": Object { + "tags": Array [ + "access:observabilityFilesCasesCreate", + ], + }, + "download": Object { + "tags": Array [ + "access:observabilityFilesCasesRead", + ], + }, + "getById": Object { + "tags": Array [ + "access:observabilityFilesCasesRead", + ], + }, + "list": Object { + "tags": Array [ + "access:observabilityFilesCasesRead", + ], + }, + }, + "id": "observabilityFilesCases", + "maxSizeBytes": [Function], + }, + ] + `); + }); + + it('sets the security solution file kind allowed mime types to only image png', () => { + registerCaseFileKinds(schema.files, mockFilesSetup); + + expect(mockFilesSetup.registerFileKind.mock.calls[2]).toMatchInlineSnapshot(` + Array [ + Object { + "allowedMimeTypes": Array [ + "image/png", + ], + "http": Object { + "create": Object { + "tags": Array [ + "access:securitySolutionFilesCasesCreate", + ], + }, + "download": Object { + "tags": Array [ + "access:securitySolutionFilesCasesRead", + ], + }, + "getById": Object { + "tags": Array [ + "access:securitySolutionFilesCasesRead", + ], + }, + "list": Object { + "tags": Array [ + "access:securitySolutionFilesCasesRead", + ], + }, + }, + "id": "securitySolutionFilesCases", + "maxSizeBytes": [Function], + }, + ] + `); + }); + }); + + describe('no mime types', () => { + const schema = ConfigSchema.validate({ files: { allowedMimeTypes: [] } }); + + it('sets the cases file kind allowed mime types to an empty array', () => { + registerCaseFileKinds(schema.files, mockFilesSetup); + + expect(mockFilesSetup.registerFileKind.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "allowedMimeTypes": Array [], + "http": Object { + "create": Object { + "tags": Array [ + "access:casesFilesCasesCreate", + ], + }, + "download": Object { + "tags": Array [ + "access:casesFilesCasesRead", + ], + }, + "getById": Object { + "tags": Array [ + "access:casesFilesCasesRead", + ], + }, + "list": Object { + "tags": Array [ + "access:casesFilesCasesRead", + ], + }, + }, + "id": "casesFilesCases", + "maxSizeBytes": [Function], + }, + ] + `); + }); + + it('sets the observability file kind allowed mime types to an empty array', () => { + registerCaseFileKinds(schema.files, mockFilesSetup); + + expect(mockFilesSetup.registerFileKind.mock.calls[1]).toMatchInlineSnapshot(` + Array [ + Object { + "allowedMimeTypes": Array [], + "http": Object { + "create": Object { + "tags": Array [ + "access:observabilityFilesCasesCreate", + ], + }, + "download": Object { + "tags": Array [ + "access:observabilityFilesCasesRead", + ], + }, + "getById": Object { + "tags": Array [ + "access:observabilityFilesCasesRead", + ], + }, + "list": Object { + "tags": Array [ + "access:observabilityFilesCasesRead", + ], + }, + }, + "id": "observabilityFilesCases", + "maxSizeBytes": [Function], + }, + ] + `); + }); + + it('sets the security solution file kind allowed mime types to an empty array', () => { + registerCaseFileKinds(schema.files, mockFilesSetup); + + expect(mockFilesSetup.registerFileKind.mock.calls[2]).toMatchInlineSnapshot(` + Array [ + Object { + "allowedMimeTypes": Array [], + "http": Object { + "create": Object { + "tags": Array [ + "access:securitySolutionFilesCasesCreate", + ], + }, + "download": Object { + "tags": Array [ + "access:securitySolutionFilesCasesRead", + ], + }, + "getById": Object { + "tags": Array [ + "access:securitySolutionFilesCasesRead", + ], + }, + "list": Object { + "tags": Array [ + "access:securitySolutionFilesCasesRead", + ], + }, + }, + "id": "securitySolutionFilesCases", + "maxSizeBytes": [Function], + }, + ] + `); + }); + }); + }); + + describe('defaults', () => { + const defaultSchema = ConfigSchema.validate({}); + + it('sets the cases file kind with defaults correctly', () => { + registerCaseFileKinds(defaultSchema.files, mockFilesSetup); + + expect(mockFilesSetup.registerFileKind.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "allowedMimeTypes": Array [ + "image/aces", + "image/apng", + "image/avci", + "image/avcs", + "image/avif", + "image/bmp", + "image/cgm", + "image/dicom-rle", + "image/dpx", + "image/emf", + "image/example", + "image/fits", + "image/g3fax", + "image/heic", + "image/heic-sequence", + "image/heif", + "image/heif-sequence", + "image/hej2k", + "image/hsj2", + "image/jls", + "image/jp2", + "image/jpeg", + "image/jph", + "image/jphc", + "image/jpm", + "image/jpx", + "image/jxr", + "image/jxrA", + "image/jxrS", + "image/jxs", + "image/jxsc", + "image/jxsi", + "image/jxss", + "image/ktx", + "image/ktx2", + "image/naplps", + "image/png", + "image/prs.btif", + "image/prs.pti", + "image/pwg-raster", + "image/svg+xml", + "image/t38", + "image/tiff", + "image/tiff-fx", + "image/vnd.adobe.photoshop", + "image/vnd.airzip.accelerator.azv", + "image/vnd.cns.inf2", + "image/vnd.dece.graphic", + "image/vnd.djvu", + "image/vnd.dwg", + "image/vnd.dxf", + "image/vnd.dvb.subtitle", + "image/vnd.fastbidsheet", + "image/vnd.fpx", + "image/vnd.fst", + "image/vnd.fujixerox.edmics-mmr", + "image/vnd.fujixerox.edmics-rlc", + "image/vnd.globalgraphics.pgb", + "image/vnd.microsoft.icon", + "image/vnd.mix", + "image/vnd.ms-modi", + "image/vnd.mozilla.apng", + "image/vnd.net-fpx", + "image/vnd.pco.b16", + "image/vnd.radiance", + "image/vnd.sealed.png", + "image/vnd.sealedmedia.softseal.gif", + "image/vnd.sealedmedia.softseal.jpg", + "image/vnd.svf", + "image/vnd.tencent.tap", + "image/vnd.valve.source.texture", + "image/vnd.wap.wbmp", + "image/vnd.xiff", + "image/vnd.zbrush.pcx", + "image/webp", + "image/wmf", + "text/plain", + "text/csv", + "text/json", + "application/json", + "application/zip", + "application/gzip", + "application/x-bzip", + "application/x-bzip2", + "application/x-7z-compressed", + "application/x-tar", + "application/pdf", + ], + "http": Object { + "create": Object { + "tags": Array [ + "access:casesFilesCasesCreate", + ], + }, + "download": Object { + "tags": Array [ + "access:casesFilesCasesRead", + ], + }, + "getById": Object { + "tags": Array [ + "access:casesFilesCasesRead", + ], + }, + "list": Object { + "tags": Array [ + "access:casesFilesCasesRead", + ], + }, + }, + "id": "casesFilesCases", + "maxSizeBytes": [Function], + }, + ] + `); + }); + + it('sets the observability file kind with defaults correctly', () => { + registerCaseFileKinds(defaultSchema.files, mockFilesSetup); + + expect(mockFilesSetup.registerFileKind.mock.calls[1]).toMatchInlineSnapshot(` + Array [ + Object { + "allowedMimeTypes": Array [ + "image/aces", + "image/apng", + "image/avci", + "image/avcs", + "image/avif", + "image/bmp", + "image/cgm", + "image/dicom-rle", + "image/dpx", + "image/emf", + "image/example", + "image/fits", + "image/g3fax", + "image/heic", + "image/heic-sequence", + "image/heif", + "image/heif-sequence", + "image/hej2k", + "image/hsj2", + "image/jls", + "image/jp2", + "image/jpeg", + "image/jph", + "image/jphc", + "image/jpm", + "image/jpx", + "image/jxr", + "image/jxrA", + "image/jxrS", + "image/jxs", + "image/jxsc", + "image/jxsi", + "image/jxss", + "image/ktx", + "image/ktx2", + "image/naplps", + "image/png", + "image/prs.btif", + "image/prs.pti", + "image/pwg-raster", + "image/svg+xml", + "image/t38", + "image/tiff", + "image/tiff-fx", + "image/vnd.adobe.photoshop", + "image/vnd.airzip.accelerator.azv", + "image/vnd.cns.inf2", + "image/vnd.dece.graphic", + "image/vnd.djvu", + "image/vnd.dwg", + "image/vnd.dxf", + "image/vnd.dvb.subtitle", + "image/vnd.fastbidsheet", + "image/vnd.fpx", + "image/vnd.fst", + "image/vnd.fujixerox.edmics-mmr", + "image/vnd.fujixerox.edmics-rlc", + "image/vnd.globalgraphics.pgb", + "image/vnd.microsoft.icon", + "image/vnd.mix", + "image/vnd.ms-modi", + "image/vnd.mozilla.apng", + "image/vnd.net-fpx", + "image/vnd.pco.b16", + "image/vnd.radiance", + "image/vnd.sealed.png", + "image/vnd.sealedmedia.softseal.gif", + "image/vnd.sealedmedia.softseal.jpg", + "image/vnd.svf", + "image/vnd.tencent.tap", + "image/vnd.valve.source.texture", + "image/vnd.wap.wbmp", + "image/vnd.xiff", + "image/vnd.zbrush.pcx", + "image/webp", + "image/wmf", + "text/plain", + "text/csv", + "text/json", + "application/json", + "application/zip", + "application/gzip", + "application/x-bzip", + "application/x-bzip2", + "application/x-7z-compressed", + "application/x-tar", + "application/pdf", + ], + "http": Object { + "create": Object { + "tags": Array [ + "access:observabilityFilesCasesCreate", + ], + }, + "download": Object { + "tags": Array [ + "access:observabilityFilesCasesRead", + ], + }, + "getById": Object { + "tags": Array [ + "access:observabilityFilesCasesRead", + ], + }, + "list": Object { + "tags": Array [ + "access:observabilityFilesCasesRead", + ], + }, + }, + "id": "observabilityFilesCases", + "maxSizeBytes": [Function], + }, + ] + `); + }); + + it('sets the securitySolution file kind with defaults correctly', () => { + registerCaseFileKinds(defaultSchema.files, mockFilesSetup); + + expect(mockFilesSetup.registerFileKind.mock.calls[2]).toMatchInlineSnapshot(` + Array [ + Object { + "allowedMimeTypes": Array [ + "image/aces", + "image/apng", + "image/avci", + "image/avcs", + "image/avif", + "image/bmp", + "image/cgm", + "image/dicom-rle", + "image/dpx", + "image/emf", + "image/example", + "image/fits", + "image/g3fax", + "image/heic", + "image/heic-sequence", + "image/heif", + "image/heif-sequence", + "image/hej2k", + "image/hsj2", + "image/jls", + "image/jp2", + "image/jpeg", + "image/jph", + "image/jphc", + "image/jpm", + "image/jpx", + "image/jxr", + "image/jxrA", + "image/jxrS", + "image/jxs", + "image/jxsc", + "image/jxsi", + "image/jxss", + "image/ktx", + "image/ktx2", + "image/naplps", + "image/png", + "image/prs.btif", + "image/prs.pti", + "image/pwg-raster", + "image/svg+xml", + "image/t38", + "image/tiff", + "image/tiff-fx", + "image/vnd.adobe.photoshop", + "image/vnd.airzip.accelerator.azv", + "image/vnd.cns.inf2", + "image/vnd.dece.graphic", + "image/vnd.djvu", + "image/vnd.dwg", + "image/vnd.dxf", + "image/vnd.dvb.subtitle", + "image/vnd.fastbidsheet", + "image/vnd.fpx", + "image/vnd.fst", + "image/vnd.fujixerox.edmics-mmr", + "image/vnd.fujixerox.edmics-rlc", + "image/vnd.globalgraphics.pgb", + "image/vnd.microsoft.icon", + "image/vnd.mix", + "image/vnd.ms-modi", + "image/vnd.mozilla.apng", + "image/vnd.net-fpx", + "image/vnd.pco.b16", + "image/vnd.radiance", + "image/vnd.sealed.png", + "image/vnd.sealedmedia.softseal.gif", + "image/vnd.sealedmedia.softseal.jpg", + "image/vnd.svf", + "image/vnd.tencent.tap", + "image/vnd.valve.source.texture", + "image/vnd.wap.wbmp", + "image/vnd.xiff", + "image/vnd.zbrush.pcx", + "image/webp", + "image/wmf", + "text/plain", + "text/csv", + "text/json", + "application/json", + "application/zip", + "application/gzip", + "application/x-bzip", + "application/x-bzip2", + "application/x-7z-compressed", + "application/x-tar", + "application/pdf", + ], + "http": Object { + "create": Object { + "tags": Array [ + "access:securitySolutionFilesCasesCreate", + ], + }, + "download": Object { + "tags": Array [ + "access:securitySolutionFilesCasesRead", + ], + }, + "getById": Object { + "tags": Array [ + "access:securitySolutionFilesCasesRead", + ], + }, + "list": Object { + "tags": Array [ + "access:securitySolutionFilesCasesRead", + ], + }, + }, + "id": "securitySolutionFilesCases", + "maxSizeBytes": [Function], + }, + ] + `); + }); + }); + }); +}); diff --git a/x-pack/plugins/cases/server/files/index.ts b/x-pack/plugins/cases/server/files/index.ts index 3c0b35c193f25..af8f0c1440277 100644 --- a/x-pack/plugins/cases/server/files/index.ts +++ b/x-pack/plugins/cases/server/files/index.ts @@ -10,20 +10,22 @@ import type { FilesSetup } from '@kbn/files-plugin/server'; import { APP_ID, MAX_FILE_SIZE, + MAX_IMAGE_FILE_SIZE, OBSERVABILITY_OWNER, SECURITY_SOLUTION_OWNER, } from '../../common/constants'; import type { Owner } from '../../common/constants/types'; import { HttpApiTagOperation } from '../../common/constants/types'; -import { ALLOWED_MIME_TYPES, IMAGE_MIME_TYPES } from '../../common/constants/mime_types'; +import { IMAGE_MIME_TYPES } from '../../common/constants/mime_types'; +import type { FilesConfig } from './types'; import { constructFileKindIdByOwner, constructFilesHttpOperationTag } from '../../common/files'; -const buildFileKind = (owner: Owner): FileKind => { +const buildFileKind = (config: FilesConfig, owner: Owner): FileKind => { return { id: constructFileKindIdByOwner(owner), http: fileKindHttpTags(owner), - maxSizeBytes, - allowedMimeTypes: ALLOWED_MIME_TYPES, + maxSizeBytes: createMaxCallback(config), + allowedMimeTypes: config.allowedMimeTypes, }; }; @@ -44,27 +46,44 @@ const buildTag = (owner: Owner, operation: HttpApiTagOperation) => { }; }; -const MAX_IMAGE_FILE_SIZE = 10 * 1024 * 1024; // 10 MiB +export const createMaxCallback = + (config: FilesConfig) => + (file: FileJSON): number => { + // if the user set a max size, always return that + if (config.maxSize != null) { + return config.maxSize; + } -const maxSizeBytes = (file: FileJSON): number => { - if (file.mimeType != null && IMAGE_MIME_TYPES.has(file.mimeType)) { - return MAX_IMAGE_FILE_SIZE; - } + const allowedMimeTypesSet = new Set(config.allowedMimeTypes); - return MAX_FILE_SIZE; -}; + // if we have the mime type for the file and it exists within the allowed types and it is an image then return the + // image size + if ( + file.mimeType != null && + allowedMimeTypesSet.has(file.mimeType) && + IMAGE_MIME_TYPES.has(file.mimeType) + ) { + return MAX_IMAGE_FILE_SIZE; + } + + return MAX_FILE_SIZE; + }; /** * The file kind definition for interacting with the file service for the backend */ -const CASES_FILE_KINDS: Record = { - [APP_ID]: buildFileKind(APP_ID), - [SECURITY_SOLUTION_OWNER]: buildFileKind(SECURITY_SOLUTION_OWNER), - [OBSERVABILITY_OWNER]: buildFileKind(OBSERVABILITY_OWNER), +const createFileKinds = (config: FilesConfig): Record => { + return { + [APP_ID]: buildFileKind(config, APP_ID), + [OBSERVABILITY_OWNER]: buildFileKind(config, OBSERVABILITY_OWNER), + [SECURITY_SOLUTION_OWNER]: buildFileKind(config, SECURITY_SOLUTION_OWNER), + }; }; -export const registerCaseFileKinds = (filesSetupPlugin: FilesSetup) => { - for (const fileKind of Object.values(CASES_FILE_KINDS)) { +export const registerCaseFileKinds = (config: FilesConfig, filesSetupPlugin: FilesSetup) => { + const fileKinds = createFileKinds(config); + + for (const fileKind of Object.values(fileKinds)) { filesSetupPlugin.registerFileKind(fileKind); } }; diff --git a/x-pack/plugins/cases/server/files/types.ts b/x-pack/plugins/cases/server/files/types.ts new file mode 100644 index 0000000000000..5c7c1693e5a04 --- /dev/null +++ b/x-pack/plugins/cases/server/files/types.ts @@ -0,0 +1,10 @@ +/* + * 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 type { ConfigType } from '../config'; + +export type FilesConfig = ConfigType['files']; diff --git a/x-pack/plugins/cases/server/index.ts b/x-pack/plugins/cases/server/index.ts index d039d7ebd41a1..62748905f6b82 100644 --- a/x-pack/plugins/cases/server/index.ts +++ b/x-pack/plugins/cases/server/index.ts @@ -15,6 +15,7 @@ export const config: PluginConfigDescriptor = { schema: ConfigSchema, exposeToBrowser: { markdownPlugins: true, + files: { maxSize: true, allowedMimeTypes: true }, }, deprecations: ({ renameFromRoot }) => [ renameFromRoot('xpack.case.enabled', 'xpack.cases.enabled', { level: 'critical' }), diff --git a/x-pack/plugins/cases/server/plugin.ts b/x-pack/plugins/cases/server/plugin.ts index 21eb63bde3ff9..6fcf27e6410e6 100644 --- a/x-pack/plugins/cases/server/plugin.ts +++ b/x-pack/plugins/cases/server/plugin.ts @@ -59,6 +59,7 @@ import { UserProfileService } from './services'; import { LICENSING_CASE_ASSIGNMENT_FEATURE } from './common/constants'; import { registerInternalAttachments } from './internal_attachments'; import { registerCaseFileKinds } from './files'; +import type { ConfigType } from './config'; export interface PluginsSetup { actions: ActionsPluginSetup; @@ -84,6 +85,7 @@ export interface PluginsStart { } export class CasePlugin { + private readonly caseConfig: ConfigType; private readonly logger: Logger; private readonly kibanaVersion: PluginInitializerContext['env']['packageInfo']['version']; private clientFactory: CasesClientFactory; @@ -94,6 +96,7 @@ export class CasePlugin { private userProfileService: UserProfileService; constructor(private readonly initializerContext: PluginInitializerContext) { + this.caseConfig = initializerContext.config.get(); this.kibanaVersion = initializerContext.env.packageInfo.version; this.logger = this.initializerContext.logger.get(); this.clientFactory = new CasesClientFactory(this.logger); @@ -110,7 +113,7 @@ export class CasePlugin { ); registerInternalAttachments(this.externalReferenceAttachmentTypeRegistry); - registerCaseFileKinds(plugins.files); + registerCaseFileKinds(this.caseConfig.files, plugins.files); this.securityPluginSetup = plugins.security; this.lensEmbeddableFactory = plugins.lens.lensEmbeddableFactory;