[Cases] Adding files configuration fields (#154013)

Fixes: https://github.com/elastic/kibana/issues/151935

This PR allows the mime types and max file size for the files
functionality within cases to be configured through the kibana.yml. We
set the defaults maxSize to be 100 mb and if it is not set by the user
we also restrict images to be 10 mb. If the `maxSize` is set by the user
we use it for all mime types including images (or whatever the user has
specified in `allowedMimeTypes`).

The file service changes are just mocks to help with testing some of the
configuration options.

New fields

```
{
  files: {
    allowedMimeTypes: string[]
    maxSize: positive number (minimum 0) <-- exposed to the browser
  }
}
```

## Release Notes
Cases added two configuration options to allow users to control which
files mime types are allowed to be attached to cases and the approved
max size of a file being upload.

`xpack.cases.files.allowedMimeTypes` - An array of strings representing
the allowed mime types to be attached to a case.
`xpack.cases.files.maxSize` - A number representing the file size limit
for files being attached to a case (in bytes).
This commit is contained in:
Jonathan Buttner 2023-04-03 15:01:01 -04:00 committed by GitHub
parent 210a7eb335
commit 234d48d9bc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 1065 additions and 36 deletions

View file

@ -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<FilesClient> => ({
...createBaseMocksFilesClient(),
getMetrics: jest.fn(),
publicDownload: jest.fn(),
});
export const createMockFilesSetup = (): DeeplyMockedKeys<FilesSetup> => {
return {
filesClientFactory: createMockFilesClientFactory(),
registerFileKind: jest.fn(),
};
};
export const createMockFilesClientFactory = (): DeeplyMockedKeys<FilesClientFactory> => {
return {
asScoped: jest.fn(),
asUnscoped: jest.fn(),
};
};

View file

@ -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<FileServiceStart> => ({
create: jest.fn(),
@ -78,3 +78,9 @@ export const createFileClientMock = (): DeeplyMockedKeys<FileClient> => {
listShares: jest.fn().mockResolvedValue({ shares: [] }),
};
};
export const createFilesSetupMock = (): DeeplyMockedKeys<FilesSetup> => {
return {
registerFileKind: jest.fn(),
};
};

View file

@ -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)',

View file

@ -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;

View file

@ -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;

View file

@ -52,6 +52,10 @@ export interface CasesUiConfigType {
markdownPlugins: {
lens: boolean;
};
files: {
maxSize?: number;
allowedMimeTypes: string[];
};
}
export const StatusAll = 'all' as const;

View file

@ -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;

View file

@ -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);
});
});
});
});
});

View file

@ -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<Owner, FileKindBrowser> = {
[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<Owner, FileKindBrowser>();
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);
}
};

View file

@ -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<Owner, FileKindBrowser>;

View file

@ -53,7 +53,8 @@ export class CasesUiPlugin
const externalReferenceAttachmentTypeRegistry = this.externalReferenceAttachmentTypeRegistry;
const persistableStateAttachmentTypeRegistry = this.persistableStateAttachmentTypeRegistry;
registerCaseFileKinds(plugins.files);
const config = this.initializerContext.config.get<CasesUiConfigType>();
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<CasesUiConfigType>();
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

View file

@ -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,
},
}
`);
});
});
});

View file

@ -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<typeof ConfigSchema>;

View file

@ -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],
},
]
`);
});
});
});
});

View file

@ -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<Owner, FileKind> = {
[APP_ID]: buildFileKind(APP_ID),
[SECURITY_SOLUTION_OWNER]: buildFileKind(SECURITY_SOLUTION_OWNER),
[OBSERVABILITY_OWNER]: buildFileKind(OBSERVABILITY_OWNER),
const createFileKinds = (config: FilesConfig): Record<Owner, FileKind> => {
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);
}
};

View file

@ -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'];

View file

@ -15,6 +15,7 @@ export const config: PluginConfigDescriptor<ConfigType> = {
schema: ConfigSchema,
exposeToBrowser: {
markdownPlugins: true,
files: { maxSize: true, allowedMimeTypes: true },
},
deprecations: ({ renameFromRoot }) => [
renameFromRoot('xpack.case.enabled', 'xpack.cases.enabled', { level: 'critical' }),

View file

@ -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<ConfigType>();
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;