diff --git a/x-pack/plugins/cases/common/constants/application.ts b/x-pack/plugins/cases/common/constants/application.ts new file mode 100644 index 0000000000000..4b43a17708ab6 --- /dev/null +++ b/x-pack/plugins/cases/common/constants/application.ts @@ -0,0 +1,31 @@ +/* + * 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 { CASE_VIEW_PAGE_TABS } from '../types'; + +/** + * Application + */ + +export const APP_ID = 'cases' as const; +export const FEATURE_ID = 'generalCases' as const; +export const APP_OWNER = 'cases' as const; +export const APP_PATH = '/app/management/insightsAndAlerting/cases' as const; +export const CASES_CREATE_PATH = '/create' as const; +export const CASES_CONFIGURE_PATH = '/configure' as const; +export const CASE_VIEW_PATH = '/:detailName' as const; +export const CASE_VIEW_COMMENT_PATH = `${CASE_VIEW_PATH}/:commentId` as const; +export const CASE_VIEW_ALERT_TABLE_PATH = + `${CASE_VIEW_PATH}/?tabId=${CASE_VIEW_PAGE_TABS.ALERTS}` as const; +export const CASE_VIEW_TAB_PATH = `${CASE_VIEW_PATH}/?tabId=:tabId` as const; + +/** + * The main Cases application is in the stack management under the + * Alerts and Insights section. To do that, Cases registers to the management + * application. This constant holds the application ID of the management plugin + */ +export const STACK_APP_ID = 'management' as const; diff --git a/x-pack/plugins/cases/common/constants/files.ts b/x-pack/plugins/cases/common/constants/files.ts new file mode 100644 index 0000000000000..7cd7109137976 --- /dev/null +++ b/x-pack/plugins/cases/common/constants/files.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 { HttpApiTagOperation, Owner } from './types'; + +export const MAX_FILE_SIZE = 100 * 1024 * 1024; // 100 MiB + +export const constructFilesHttpOperationTag = (owner: Owner, operation: HttpApiTagOperation) => { + return `${owner}FilesCases${operation}`; +}; diff --git a/x-pack/plugins/cases/common/constants.ts b/x-pack/plugins/cases/common/constants/index.ts similarity index 73% rename from x-pack/plugins/cases/common/constants.ts rename to x-pack/plugins/cases/common/constants/index.ts index 601599104641b..f837223e6a07c 100644 --- a/x-pack/plugins/cases/common/constants.ts +++ b/x-pack/plugins/cases/common/constants/index.ts @@ -4,34 +4,15 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { CASE_VIEW_PAGE_TABS } from './types'; -import type { CasesFeaturesAllRequired } from './ui/types'; -export const DEFAULT_DATE_FORMAT = 'dateFormat' as const; -export const DEFAULT_DATE_FORMAT_TZ = 'dateFormat:tz' as const; - -/** - * Application - */ +import type { CasesFeaturesAllRequired } from '../ui/types'; -export const APP_ID = 'cases' as const; -export const FEATURE_ID = 'generalCases' as const; -export const APP_OWNER = 'cases' as const; -export const APP_PATH = '/app/management/insightsAndAlerting/cases' as const; -export const CASES_CREATE_PATH = '/create' as const; -export const CASES_CONFIGURE_PATH = '/configure' as const; -export const CASE_VIEW_PATH = '/:detailName' as const; -export const CASE_VIEW_COMMENT_PATH = `${CASE_VIEW_PATH}/:commentId` as const; -export const CASE_VIEW_ALERT_TABLE_PATH = - `${CASE_VIEW_PATH}/?tabId=${CASE_VIEW_PAGE_TABS.ALERTS}` as const; -export const CASE_VIEW_TAB_PATH = `${CASE_VIEW_PATH}/?tabId=:tabId` as const; +export * from './owners'; +export * from './files'; +export * from './application'; -/** - * The main Cases application is in the stack management under the - * Alerts and Insights section. To do that, Cases registers to the management - * application. This constant holds the application ID of the management plugin - */ -export const STACK_APP_ID = 'management' as const; +export const DEFAULT_DATE_FORMAT = 'dateFormat' as const; +export const DEFAULT_DATE_FORMAT_TZ = 'dateFormat:tz' as const; /** * Saved objects @@ -111,37 +92,6 @@ export const CONNECTORS_URL = `${ACTION_URL}/connectors` as const; */ export const MAX_ALERTS_PER_CASE = 1000 as const; -/** - * Owner - */ -export const SECURITY_SOLUTION_OWNER = 'securitySolution' as const; -export const OBSERVABILITY_OWNER = 'observability' as const; -export const GENERAL_CASES_OWNER = APP_ID; - -export const OWNER_INFO = { - [SECURITY_SOLUTION_OWNER]: { - id: SECURITY_SOLUTION_OWNER, - appId: 'securitySolutionUI', - label: 'Security', - iconType: 'logoSecurity', - appRoute: '/app/security', - }, - [OBSERVABILITY_OWNER]: { - id: OBSERVABILITY_OWNER, - appId: 'observability-overview', - label: 'Observability', - iconType: 'logoObservability', - appRoute: '/app/observability', - }, - [GENERAL_CASES_OWNER]: { - id: GENERAL_CASES_OWNER, - appId: 'management', - label: 'Stack', - iconType: 'casesApp', - appRoute: '/app/management/insightsAndAlerting', - }, -} as const; - /** * Searching */ @@ -186,6 +136,20 @@ export const UPDATE_CASES_CAPABILITY = 'update_cases' as const; export const DELETE_CASES_CAPABILITY = 'delete_cases' as const; export const PUSH_CASES_CAPABILITY = 'push_cases' as const; +/** + * Cases API Tags + */ + +/** + * This tag registered for the cases suggest user profiles API + */ +export const SUGGEST_USER_PROFILES_API_TAG = 'casesSuggestUserProfiles'; + +/** + * This tag is registered for the security bulk get API + */ +export const BULK_GET_USER_PROFILES_API_TAG = 'bulkGetUserProfiles'; + /** * User profiles */ diff --git a/x-pack/plugins/cases/common/constants/mime_types.ts b/x-pack/plugins/cases/common/constants/mime_types.ts new file mode 100644 index 0000000000000..9f1f455513dab --- /dev/null +++ b/x-pack/plugins/cases/common/constants/mime_types.ts @@ -0,0 +1,110 @@ +/* + * 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. + */ + +/** + * These were retrieved from https://www.iana.org/assignments/media-types/media-types.xhtml#image + */ +const imageMimeTypes = [ + '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', +]; + +const textMimeTypes = ['text/plain', 'text/csv', 'text/json', 'application/json']; + +const compressionMimeTypes = [ + 'application/zip', + 'application/gzip', + 'application/x-bzip', + 'application/x-bzip2', + 'application/x-7z-compressed', + 'application/x-tar', +]; + +const pdfMimeTypes = ['application/pdf']; + +export const ALLOWED_MIME_TYPES = [ + ...imageMimeTypes, + ...textMimeTypes, + ...compressionMimeTypes, + ...pdfMimeTypes, +]; + +export const IMAGE_MIME_TYPES = new Set(imageMimeTypes); diff --git a/x-pack/plugins/cases/common/constants/owners.ts b/x-pack/plugins/cases/common/constants/owners.ts new file mode 100644 index 0000000000000..60463fa57a976 --- /dev/null +++ b/x-pack/plugins/cases/common/constants/owners.ts @@ -0,0 +1,50 @@ +/* + * 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 { APP_ID } from './application'; +import type { Owner } from './types'; + +/** + * Owner + */ +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; + +interface RouteInfo { + id: Owner; + appId: string; + label: string; + iconType: string; + appRoute: string; +} + +export const OWNER_INFO: Record = { + [SECURITY_SOLUTION_OWNER]: { + id: SECURITY_SOLUTION_OWNER, + appId: 'securitySolutionUI', + label: 'Security', + iconType: 'logoSecurity', + appRoute: '/app/security', + }, + [OBSERVABILITY_OWNER]: { + id: OBSERVABILITY_OWNER, + appId: 'observability-overview', + label: 'Observability', + iconType: 'logoObservability', + appRoute: '/app/observability', + }, + [GENERAL_CASES_OWNER]: { + id: GENERAL_CASES_OWNER, + appId: 'management', + label: 'Stack', + iconType: 'casesApp', + appRoute: '/app/management/insightsAndAlerting', + }, +} as const; diff --git a/x-pack/plugins/cases/common/constants/types.ts b/x-pack/plugins/cases/common/constants/types.ts new file mode 100644 index 0000000000000..27ee99fa95c9a --- /dev/null +++ b/x-pack/plugins/cases/common/constants/types.ts @@ -0,0 +1,16 @@ +/* + * 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 { OWNERS } from './owners'; + +export enum HttpApiTagOperation { + Read = 'Read', + Create = 'Create', + Delete = 'Delete', +} + +export type Owner = typeof OWNERS[number]; diff --git a/x-pack/plugins/cases/common/index.ts b/x-pack/plugins/cases/common/index.ts index 8ca2c23c1eb8d..d41de5543654d 100644 --- a/x-pack/plugins/cases/common/index.ts +++ b/x-pack/plugins/cases/common/index.ts @@ -55,3 +55,4 @@ export { StatusAll } from './ui/types'; export { getCreateConnectorUrl, getAllConnectorsUrl } from './utils/connectors_api'; export { createUICapabilities } from './utils/capabilities'; +export { getApiTags } from './utils/api_tags'; diff --git a/x-pack/plugins/cases/common/utils/__snapshots__/api_tags.test.ts.snap b/x-pack/plugins/cases/common/utils/__snapshots__/api_tags.test.ts.snap new file mode 100644 index 0000000000000..ea1ef29e71c59 --- /dev/null +++ b/x-pack/plugins/cases/common/utils/__snapshots__/api_tags.test.ts.snap @@ -0,0 +1,58 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`api_tags getApiTags constructs the https tags for the owner: cases 1`] = ` +Object { + "all": Array [ + "casesSuggestUserProfiles", + "bulkGetUserProfiles", + "casesFilesCasesCreate", + "casesFilesCasesRead", + ], + "delete": Array [ + "casesFilesCasesDelete", + ], + "read": Array [ + "casesSuggestUserProfiles", + "bulkGetUserProfiles", + "casesFilesCasesRead", + ], +} +`; + +exports[`api_tags getApiTags constructs the https tags for the owner: observability 1`] = ` +Object { + "all": Array [ + "casesSuggestUserProfiles", + "bulkGetUserProfiles", + "observabilityFilesCasesCreate", + "observabilityFilesCasesRead", + ], + "delete": Array [ + "observabilityFilesCasesDelete", + ], + "read": Array [ + "casesSuggestUserProfiles", + "bulkGetUserProfiles", + "observabilityFilesCasesRead", + ], +} +`; + +exports[`api_tags getApiTags constructs the https tags for the owner: securitySolution 1`] = ` +Object { + "all": Array [ + "casesSuggestUserProfiles", + "bulkGetUserProfiles", + "securitySolutionFilesCasesCreate", + "securitySolutionFilesCasesRead", + ], + "delete": Array [ + "securitySolutionFilesCasesDelete", + ], + "read": Array [ + "casesSuggestUserProfiles", + "bulkGetUserProfiles", + "securitySolutionFilesCasesRead", + ], +} +`; diff --git a/x-pack/plugins/cases/common/utils/api_tags.test.ts b/x-pack/plugins/cases/common/utils/api_tags.test.ts new file mode 100644 index 0000000000000..61e8e335be2cf --- /dev/null +++ b/x-pack/plugins/cases/common/utils/api_tags.test.ts @@ -0,0 +1,17 @@ +/* + * 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 { OWNERS } from '../constants'; +import { getApiTags } from './api_tags'; + +describe('api_tags', () => { + describe('getApiTags', () => { + it.each(OWNERS)('constructs the https tags for the owner: %s', (owner) => { + expect(getApiTags(owner)).toMatchSnapshot(); + }); + }); +}); diff --git a/x-pack/plugins/cases/common/utils/api_tags.ts b/x-pack/plugins/cases/common/utils/api_tags.ts new file mode 100644 index 0000000000000..707188a0fba33 --- /dev/null +++ b/x-pack/plugins/cases/common/utils/api_tags.ts @@ -0,0 +1,26 @@ +/* + * 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 { + BULK_GET_USER_PROFILES_API_TAG, + constructFilesHttpOperationTag, + SUGGEST_USER_PROFILES_API_TAG, +} from '../constants'; +import { HttpApiTagOperation } from '../constants/types'; +import type { Owner } from '../constants/types'; + +export const getApiTags = (owner: Owner) => { + const create = constructFilesHttpOperationTag(owner, HttpApiTagOperation.Create); + const deleteTag = constructFilesHttpOperationTag(owner, HttpApiTagOperation.Delete); + const read = constructFilesHttpOperationTag(owner, HttpApiTagOperation.Read); + + return { + all: [SUGGEST_USER_PROFILES_API_TAG, BULK_GET_USER_PROFILES_API_TAG, create, read] as const, + read: [SUGGEST_USER_PROFILES_API_TAG, BULK_GET_USER_PROFILES_API_TAG, read] as const, + delete: [deleteTag] as const, + }; +}; diff --git a/x-pack/plugins/cases/kibana.jsonc b/x-pack/plugins/cases/kibana.jsonc index 94263c383745f..afed1cd7631f8 100644 --- a/x-pack/plugins/cases/kibana.jsonc +++ b/x-pack/plugins/cases/kibana.jsonc @@ -25,7 +25,8 @@ "management", "security", "notifications", - "ruleRegistry" + "ruleRegistry", + "files", ], "optionalPlugins": [ "home", diff --git a/x-pack/plugins/cases/public/files/index.ts b/x-pack/plugins/cases/public/files/index.ts new file mode 100644 index 0000000000000..e06c8eda615a0 --- /dev/null +++ b/x-pack/plugins/cases/public/files/index.ts @@ -0,0 +1,36 @@ +/* + * 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 { 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 type { Owner } from '../../common/constants/types'; +import { APP_ID, OBSERVABILITY_OWNER, SECURITY_SOLUTION_OWNER } from '../../common'; + +const buildFileKind = (owner: Owner): FileKindBrowser => { + return { + id: owner, + allowedMimeTypes: ALLOWED_MIME_TYPES, + maxSizeBytes: 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), +}; + +export const registerCaseFileKinds = (filesSetupPlugin: FilesSetup) => { + for (const fileKind of Object.values(CASES_FILE_KINDS)) { + filesSetupPlugin.registerFileKind(fileKind); + } +}; diff --git a/x-pack/plugins/cases/public/plugin.ts b/x-pack/plugins/cases/public/plugin.ts index 51f2ae92e3094..83b0f2fb0f009 100644 --- a/x-pack/plugins/cases/public/plugin.ts +++ b/x-pack/plugins/cases/public/plugin.ts @@ -27,6 +27,7 @@ import { groupAlertsByRule } from './client/helpers/group_alerts_by_rule'; import { getUICapabilities } from './client/helpers/capabilities'; import { ExternalReferenceAttachmentTypeRegistry } from './client/attachment_framework/external_reference_registry'; import { PersistableStateAttachmentTypeRegistry } from './client/attachment_framework/persistable_state_registry'; +import { registerCaseFileKinds } from './files'; /** * @public @@ -52,6 +53,8 @@ export class CasesUiPlugin const externalReferenceAttachmentTypeRegistry = this.externalReferenceAttachmentTypeRegistry; const persistableStateAttachmentTypeRegistry = this.persistableStateAttachmentTypeRegistry; + registerCaseFileKinds(plugins.files); + if (plugins.home) { plugins.home.featureCatalogue.register({ id: APP_ID, diff --git a/x-pack/plugins/cases/public/types.ts b/x-pack/plugins/cases/public/types.ts index 732fcfee5f0d6..f430b99ae17f2 100644 --- a/x-pack/plugins/cases/public/types.ts +++ b/x-pack/plugins/cases/public/types.ts @@ -22,6 +22,7 @@ import type { TriggersAndActionsUIPublicPluginStart as TriggersActionsStart } fr import type { DistributiveOmit } from '@elastic/eui'; import type { ApmBase } from '@elastic/apm-rum'; import type { LicensingPluginStart } from '@kbn/licensing-plugin/public'; +import type { FilesSetup, FilesStart } from '@kbn/files-plugin/public'; import type { CasesByAlertId, CasesByAlertIDRequest, @@ -50,6 +51,7 @@ import type { ExternalReferenceAttachmentTypeRegistry } from './client/attachmen import type { PersistableStateAttachmentTypeRegistry } from './client/attachment_framework/persistable_state_registry'; export interface CasesPluginSetup { + files: FilesSetup; security: SecurityPluginSetup; management: ManagementSetup; home?: HomePublicPluginSetup; @@ -58,6 +60,7 @@ export interface CasesPluginSetup { export interface CasesPluginStart { data: DataPublicPluginStart; embeddable: EmbeddableStart; + files: FilesStart; licensing?: LicensingPluginStart; lens: LensPublicStart; storage: Storage; diff --git a/x-pack/plugins/cases/server/features.ts b/x-pack/plugins/cases/server/features.ts index 2573e5f58b3f3..05ee00cb1b037 100644 --- a/x-pack/plugins/cases/server/features.ts +++ b/x-pack/plugins/cases/server/features.ts @@ -8,10 +8,11 @@ import { i18n } from '@kbn/i18n'; import type { KibanaFeatureConfig } from '@kbn/features-plugin/common'; +import { hiddenTypes as filesSavedObjectTypes } from '@kbn/files-plugin/server/saved_objects'; import { DEFAULT_APP_CATEGORIES } from '@kbn/core/server'; import { APP_ID, FEATURE_ID } from '../common/constants'; -import { createUICapabilities } from '../common'; +import { createUICapabilities, getApiTags } from '../common'; /** * The order of appearance in the feature privilege page @@ -23,6 +24,7 @@ const FEATURE_ORDER = 3100; export const getCasesKibanaFeature = (): KibanaFeatureConfig => { const capabilities = createUICapabilities(); + const apiTags = getApiTags(APP_ID); return { id: FEATURE_ID, @@ -38,7 +40,7 @@ export const getCasesKibanaFeature = (): KibanaFeatureConfig => { cases: [APP_ID], privileges: { all: { - api: ['casesSuggestUserProfiles', 'bulkGetUserProfiles'], + api: apiTags.all, cases: { create: [APP_ID], read: [APP_ID], @@ -49,13 +51,13 @@ export const getCasesKibanaFeature = (): KibanaFeatureConfig => { insightsAndAlerting: [APP_ID], }, savedObject: { - all: [], - read: [], + all: [...filesSavedObjectTypes], + read: [...filesSavedObjectTypes], }, ui: capabilities.all, }, read: { - api: ['casesSuggestUserProfiles', 'bulkGetUserProfiles'], + api: apiTags.read, cases: { read: [APP_ID], }, @@ -64,7 +66,7 @@ export const getCasesKibanaFeature = (): KibanaFeatureConfig => { }, savedObject: { all: [], - read: [], + read: [...filesSavedObjectTypes], }, ui: capabilities.read, }, @@ -79,7 +81,7 @@ export const getCasesKibanaFeature = (): KibanaFeatureConfig => { groupType: 'independent', privileges: [ { - api: [], + api: apiTags.delete, id: 'cases_delete', name: i18n.translate('xpack.cases.features.deleteSubFeatureDetails', { defaultMessage: 'Delete cases and comments', diff --git a/x-pack/plugins/cases/server/files/index.ts b/x-pack/plugins/cases/server/files/index.ts new file mode 100644 index 0000000000000..fb17fd50fa870 --- /dev/null +++ b/x-pack/plugins/cases/server/files/index.ts @@ -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 type { FileJSON, FileKind } from '@kbn/files-plugin/common'; +import type { FilesSetup } from '@kbn/files-plugin/server'; +import { + APP_ID, + constructFilesHttpOperationTag, + MAX_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'; + +const buildFileKind = (owner: Owner): FileKind => { + return { + id: owner, + http: fileKindHttpTags(owner), + maxSizeBytes, + allowedMimeTypes: ALLOWED_MIME_TYPES, + }; +}; + +const fileKindHttpTags = (owner: Owner): FileKind['http'] => { + return { + create: buildTag(owner, HttpApiTagOperation.Create), + delete: buildTag(owner, HttpApiTagOperation.Delete), + download: buildTag(owner, HttpApiTagOperation.Read), + getById: buildTag(owner, HttpApiTagOperation.Read), + list: buildTag(owner, HttpApiTagOperation.Read), + }; +}; + +const access = 'access:'; + +const buildTag = (owner: Owner, operation: HttpApiTagOperation) => { + return { + tags: [`${access}${constructFilesHttpOperationTag(owner, operation)}`], + }; +}; + +const MAX_IMAGE_FILE_SIZE = 10 * 1024 * 1024; // 10 MiB + +const maxSizeBytes = (file: FileJSON): number => { + if (file.mimeType != null && 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), +}; + +export const registerCaseFileKinds = (filesSetupPlugin: FilesSetup) => { + for (const fileKind of Object.values(CASES_FILE_KINDS)) { + filesSetupPlugin.registerFileKind(fileKind); + } +}; diff --git a/x-pack/plugins/cases/server/plugin.ts b/x-pack/plugins/cases/server/plugin.ts index a5a1d728311d4..8d2036921d84b 100644 --- a/x-pack/plugins/cases/server/plugin.ts +++ b/x-pack/plugins/cases/server/plugin.ts @@ -14,6 +14,7 @@ import type { CoreStart, } from '@kbn/core/server'; +import type { FilesSetup } from '@kbn/files-plugin/server'; import type { SecurityPluginSetup, SecurityPluginStart } from '@kbn/security-plugin/server'; import type { PluginSetupContract as ActionsPluginSetup, @@ -56,11 +57,13 @@ import { PersistableStateAttachmentTypeRegistry } from './attachment_framework/p import { ExternalReferenceAttachmentTypeRegistry } from './attachment_framework/external_reference_registry'; import { UserProfileService } from './services'; import { LICENSING_CASE_ASSIGNMENT_FEATURE } from './common/constants'; +import { registerCaseFileKinds } from './files'; export interface PluginsSetup { actions: ActionsPluginSetup; lens: LensServerPluginSetup; features: FeaturesPluginSetup; + files: FilesSetup; security: SecurityPluginSetup; licensing: LicensingPluginSetup; taskManager?: TaskManagerSetupContract; @@ -104,6 +107,8 @@ export class CasePlugin { )}] and plugins [${Object.keys(plugins)}]` ); + registerCaseFileKinds(plugins.files); + this.securityPluginSetup = plugins.security; this.lensEmbeddableFactory = plugins.lens.lensEmbeddableFactory; diff --git a/x-pack/plugins/cases/tsconfig.json b/x-pack/plugins/cases/tsconfig.json index a1737094134f2..5ba7e85918975 100644 --- a/x-pack/plugins/cases/tsconfig.json +++ b/x-pack/plugins/cases/tsconfig.json @@ -56,6 +56,8 @@ "@kbn/core-saved-objects-base-server-mocks", "@kbn/core-saved-objects-utils-server", "@kbn/shared-ux-router", + "@kbn/files-plugin", + "@kbn/shared-ux-file-types", ], "exclude": [ "target/**/*", diff --git a/x-pack/plugins/observability/kibana.jsonc b/x-pack/plugins/observability/kibana.jsonc index d49e3ea870a62..4026d9f41088d 100644 --- a/x-pack/plugins/observability/kibana.jsonc +++ b/x-pack/plugins/observability/kibana.jsonc @@ -17,6 +17,7 @@ "data", "dataViews", "features", + "files", "inspector", "ruleRegistry", "triggersActionsUi", @@ -24,7 +25,7 @@ "unifiedSearch", "security", "guidedOnboarding", - "share" + "share", ], "optionalPlugins": [ "discover", diff --git a/x-pack/plugins/observability/server/plugin.ts b/x-pack/plugins/observability/server/plugin.ts index e570fd7887952..4b2e2c722230b 100644 --- a/x-pack/plugins/observability/server/plugin.ts +++ b/x-pack/plugins/observability/server/plugin.ts @@ -13,10 +13,14 @@ import { DEFAULT_APP_CATEGORIES, Logger, } from '@kbn/core/server'; +import { hiddenTypes as filesSavedObjectTypes } from '@kbn/files-plugin/server/saved_objects'; import { PluginSetupContract } from '@kbn/alerting-plugin/server'; import { Dataset, RuleRegistryPluginSetupContract } from '@kbn/rule-registry-plugin/server'; import { PluginSetupContract as FeaturesSetup } from '@kbn/features-plugin/server'; -import { createUICapabilities } from '@kbn/cases-plugin/common'; +import { + createUICapabilities as createCasesUICapabilities, + getApiTags as getCasesApiTags, +} from '@kbn/cases-plugin/common'; import { SpacesPluginSetup } from '@kbn/spaces-plugin/server'; import { experimentalRuleFieldMap } from '@kbn/rule-registry-plugin/common/assets/field_maps/experimental_rule_field_map'; import { ECS_COMPONENT_TEMPLATE_NAME } from '@kbn/alerting-plugin/server'; @@ -64,7 +68,9 @@ export class ObservabilityPlugin implements Plugin { } public setup(core: CoreSetup, plugins: PluginSetup) { - const casesCapabilities = createUICapabilities(); + const casesCapabilities = createCasesUICapabilities(); + const casesApiTags = getCasesApiTags(observabilityFeatureId); + const config = this.initContext.config.get(); plugins.features.registerKibanaFeature({ @@ -79,7 +85,7 @@ export class ObservabilityPlugin implements Plugin { cases: [observabilityFeatureId], privileges: { all: { - api: ['casesSuggestUserProfiles', 'bulkGetUserProfiles'], + api: casesApiTags.all, app: [casesFeatureId, 'kibana'], catalogue: [observabilityFeatureId], cases: { @@ -89,13 +95,13 @@ export class ObservabilityPlugin implements Plugin { push: [observabilityFeatureId], }, savedObject: { - all: [], - read: [], + all: [...filesSavedObjectTypes], + read: [...filesSavedObjectTypes], }, ui: casesCapabilities.all, }, read: { - api: ['casesSuggestUserProfiles', 'bulkGetUserProfiles'], + api: casesApiTags.read, app: [casesFeatureId, 'kibana'], catalogue: [observabilityFeatureId], cases: { @@ -103,7 +109,7 @@ export class ObservabilityPlugin implements Plugin { }, savedObject: { all: [], - read: [], + read: [...filesSavedObjectTypes], }, ui: casesCapabilities.read, }, @@ -118,7 +124,7 @@ export class ObservabilityPlugin implements Plugin { groupType: 'independent', privileges: [ { - api: [], + api: casesApiTags.delete, id: 'cases_delete', name: i18n.translate( 'xpack.observability.featureRegistry.deleteSubFeatureDetails', diff --git a/x-pack/plugins/observability/tsconfig.json b/x-pack/plugins/observability/tsconfig.json index eb85b5fb25b26..eadf340054f52 100644 --- a/x-pack/plugins/observability/tsconfig.json +++ b/x-pack/plugins/observability/tsconfig.json @@ -71,6 +71,7 @@ "@kbn/shared-ux-router", "@kbn/alerts-ui-shared", "@kbn/core-application-browser", + "@kbn/files-plugin", ], "exclude": [ "target/**/*", diff --git a/x-pack/plugins/security_solution/server/features.ts b/x-pack/plugins/security_solution/server/features.ts index 197b9fc926cc7..5f838f76d3bf5 100644 --- a/x-pack/plugins/security_solution/server/features.ts +++ b/x-pack/plugins/security_solution/server/features.ts @@ -7,10 +7,14 @@ import { i18n } from '@kbn/i18n'; +import { hiddenTypes as filesSavedObjectTypes } from '@kbn/files-plugin/server/saved_objects'; import type { KibanaFeatureConfig, SubFeatureConfig } from '@kbn/features-plugin/common'; import { DEFAULT_APP_CATEGORIES } from '@kbn/core/server'; import { DATA_VIEW_SAVED_OBJECT_TYPE } from '@kbn/data-views-plugin/common'; -import { createUICapabilities } from '@kbn/cases-plugin/common'; +import { + createUICapabilities as createCasesUICapabilities, + getApiTags as getCasesApiTags, +} from '@kbn/cases-plugin/common'; import { EXCEPTION_LIST_NAMESPACE_AGNOSTIC } from '@kbn/securitysolution-list-constants'; import { APP_ID, CASES_FEATURE_ID, SERVER_APP_ID } from '../common/constants'; @@ -18,7 +22,8 @@ import { savedObjectTypes } from './saved_objects'; import type { ConfigType } from './config'; export const getCasesKibanaFeature = (): KibanaFeatureConfig => { - const casesCapabilities = createUICapabilities(); + const casesCapabilities = createCasesUICapabilities(); + const casesApiTags = getCasesApiTags(APP_ID); return { id: CASES_FEATURE_ID, @@ -32,7 +37,7 @@ export const getCasesKibanaFeature = (): KibanaFeatureConfig => { cases: [APP_ID], privileges: { all: { - api: ['casesSuggestUserProfiles', 'bulkGetUserProfiles'], + api: casesApiTags.all, app: [CASES_FEATURE_ID, 'kibana'], catalogue: [APP_ID], cases: { @@ -42,13 +47,13 @@ export const getCasesKibanaFeature = (): KibanaFeatureConfig => { push: [APP_ID], }, savedObject: { - all: [], - read: [], + all: [...filesSavedObjectTypes], + read: [...filesSavedObjectTypes], }, ui: casesCapabilities.all, }, read: { - api: ['casesSuggestUserProfiles', 'bulkGetUserProfiles'], + api: casesApiTags.read, app: [CASES_FEATURE_ID, 'kibana'], catalogue: [APP_ID], cases: { @@ -56,7 +61,7 @@ export const getCasesKibanaFeature = (): KibanaFeatureConfig => { }, savedObject: { all: [], - read: [], + read: [...filesSavedObjectTypes], }, ui: casesCapabilities.read, }, @@ -71,7 +76,7 @@ export const getCasesKibanaFeature = (): KibanaFeatureConfig => { groupType: 'independent', privileges: [ { - api: [], + api: casesApiTags.delete, id: 'cases_delete', name: i18n.translate( 'xpack.securitySolution.featureRegistry.deleteSubFeatureDetails', diff --git a/x-pack/test/api_integration/apis/cases/common/users.ts b/x-pack/test/api_integration/apis/cases/common/users.ts index 29c14c35a5b76..f37b13b98f31a 100644 --- a/x-pack/test/api_integration/apis/cases/common/users.ts +++ b/x-pack/test/api_integration/apis/cases/common/users.ts @@ -4,12 +4,6 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -/* - * 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 { User } from '../../../../cases_api_integration/common/lib/authentication/types'; import { diff --git a/x-pack/test/api_integration/apis/cases/files.ts b/x-pack/test/api_integration/apis/cases/files.ts new file mode 100644 index 0000000000000..95ad400459f02 --- /dev/null +++ b/x-pack/test/api_integration/apis/cases/files.ts @@ -0,0 +1,365 @@ +/* + * 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 expect from '@kbn/expect'; + +import { APP_ID as CASES_APP_ID } from '@kbn/cases-plugin/common/constants'; +import { APP_ID as SECURITY_SOLUTION_APP_ID } from '@kbn/security-solution-plugin/common/constants'; +import { observabilityFeatureId as OBSERVABILITY_APP_ID } from '@kbn/observability-plugin/common'; +import { Owner } from '@kbn/cases-plugin/common/constants/types'; +import { BaseFilesClient } from '@kbn/shared-ux-file-types'; +import { User } from '../../../cases_api_integration/common/lib/authentication/types'; +import { + createFile, + deleteFiles, + uploadFile, + downloadFile, + createAndUploadFile, + listFiles, + getFileById, + deleteAllFiles, +} from '../../../cases_api_integration/common/lib/api'; +import { FtrProviderContext } from '../../ftr_provider_context'; +import { + casesAllUser, + casesNoDeleteUser, + casesReadUser, + obsCasesAllUser, + obsCasesNoDeleteUser, + obsCasesReadUser, + secAllCasesNoDeleteUser, + secAllUser, + secReadCasesReadUser, +} from './common/users'; + +interface TestScenario { + user: User; + owner: Owner; +} + +export default ({ getService }: FtrProviderContext): void => { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const supertest = getService('supertest'); + + describe('files', () => { + describe('failure requests', () => { + const createFileFailure = async (scenario: TestScenario) => { + await createFile({ + supertest: supertestWithoutAuth, + auth: { user: scenario.user, space: null }, + params: { + kind: scenario.owner, + name: 'testFile', + mimeType: 'image/png', + }, + expectedHttpCode: 403, + }); + }; + + const deleteFileFailure = async (scenario: TestScenario) => { + await deleteFiles({ + supertest: supertestWithoutAuth, + auth: { user: scenario.user, space: null }, + files: [{ kind: scenario.owner, id: 'abc' }], + expectedHttpCode: 403, + }); + }; + + const uploadFileFailure = async (scenario: TestScenario) => { + await uploadFile({ + supertest: supertestWithoutAuth, + auth: { user: scenario.user, space: null }, + data: 'abc', + kind: scenario.owner, + mimeType: 'image/png', + fileId: '123', + expectedHttpCode: 403, + }); + }; + + const listFilesFailure = async (scenario: TestScenario) => { + await listFiles({ + supertest: supertestWithoutAuth, + auth: { user: scenario.user, space: null }, + params: { + kind: scenario.owner, + }, + expectedHttpCode: 403, + }); + }; + + const downloadFileFailure = async (scenario: TestScenario) => { + await downloadFile({ + supertest: supertestWithoutAuth, + auth: { user: scenario.user, space: null }, + fileId: 'abc', + fileName: '123', + mimeType: 'image/png', + kind: scenario.owner, + expectedHttpCode: 401, + }); + }; + + const getFileByIdFailure = async (scenario: TestScenario) => { + await getFileById({ + supertest: supertestWithoutAuth, + auth: { user: scenario.user, space: null }, + id: 'abc', + kind: scenario.owner, + expectedHttpCode: 403, + }); + }; + + describe('user not authorized for a delete operation', () => { + const testScenarios: TestScenario[] = [ + { user: secAllCasesNoDeleteUser, owner: SECURITY_SOLUTION_APP_ID }, + { user: casesNoDeleteUser, owner: CASES_APP_ID }, + { user: obsCasesNoDeleteUser, owner: OBSERVABILITY_APP_ID }, + ]; + + for (const scenario of testScenarios) { + it('should fail to delete a file', async () => { + await deleteFileFailure(scenario); + }); + } + }); + + describe('user not authorized for write operations', () => { + const testScenarios: TestScenario[] = [ + { user: secReadCasesReadUser, owner: SECURITY_SOLUTION_APP_ID }, + { user: casesReadUser, owner: CASES_APP_ID }, + { user: obsCasesReadUser, owner: OBSERVABILITY_APP_ID }, + ]; + + for (const scenario of testScenarios) { + it('should fail to create a file', async () => { + await createFileFailure(scenario); + }); + + it('should fail to upload a file', async () => { + await uploadFileFailure(scenario); + }); + + it('should fail to delete a file', async () => { + await deleteFileFailure(scenario); + }); + } + }); + + describe('user not authorized for file kind', () => { + const testScenarios: TestScenario[] = [ + { user: secAllUser, owner: CASES_APP_ID }, + { + user: casesAllUser, + owner: SECURITY_SOLUTION_APP_ID, + }, + { + user: obsCasesAllUser, + owner: CASES_APP_ID, + }, + ]; + + for (const scenario of testScenarios) { + describe(`scenario user: ${scenario.user.username} owner: ${scenario.owner}`, () => { + it('should fail to create a file', async () => { + await createFileFailure(scenario); + }); + + it('should fail to upload a file', async () => { + await uploadFileFailure(scenario); + }); + + it('should fail to delete a file', async () => { + await deleteFileFailure(scenario); + }); + + it('should fail to list files', async () => { + await listFilesFailure(scenario); + }); + + it('should fail to download a file', async () => { + await downloadFileFailure(scenario); + }); + + it('should fail to get a file by its id', async () => { + await getFileByIdFailure(scenario); + }); + }); + } + }); + }); + + describe('successful requests', () => { + describe('users with read privileges', () => { + const testScenarios: TestScenario[] = [ + { user: secReadCasesReadUser, owner: SECURITY_SOLUTION_APP_ID }, + { user: casesReadUser, owner: CASES_APP_ID }, + { user: obsCasesReadUser, owner: OBSERVABILITY_APP_ID }, + ]; + + for (const scenario of testScenarios) { + describe(`scenario user: ${scenario.user.username} owner: ${scenario.owner}`, () => { + let createdFile: Awaited>; + + beforeEach(async () => { + const { create } = await createAndUploadFile({ + supertest, + data: 'abc', + createFileParams: { + name: 'testFile', + mimeType: 'image/png', + kind: scenario.owner, + }, + }); + createdFile = create; + }); + + afterEach(async () => { + await deleteAllFiles({ + supertest, + kind: scenario.owner, + }); + }); + + it('should list files', async () => { + const files = await listFiles({ + supertest: supertestWithoutAuth, + params: { kind: scenario.owner }, + auth: { user: scenario.user, space: null }, + }); + + expect(files.total).to.be(1); + expect(files.files[0].name).to.be(createdFile.file.name); + }); + + it('should get a file by its id', async () => { + const file = await getFileById({ + supertest: supertestWithoutAuth, + id: createdFile.file.id, + kind: scenario.owner, + auth: { user: scenario.user, space: null }, + }); + + expect(file.file.name).to.be(createdFile.file.name); + }); + }); + } + }); + + describe('users with all privileges', () => { + const testScenarios: TestScenario[] = [ + { user: secAllUser, owner: SECURITY_SOLUTION_APP_ID }, + { user: casesAllUser, owner: CASES_APP_ID }, + { user: obsCasesAllUser, owner: OBSERVABILITY_APP_ID }, + ]; + + for (const scenario of testScenarios) { + describe(`scenario user: ${scenario.user.username} owner: ${scenario.owner}`, () => { + it('should create and delete a file', async () => { + const createResult = await createFile({ + supertest: supertestWithoutAuth, + auth: { user: scenario.user, space: null }, + params: { + kind: scenario.owner, + name: 'testFile', + mimeType: 'image/png', + }, + }); + + await deleteFiles({ + supertest: supertestWithoutAuth, + auth: { user: scenario.user, space: null }, + files: [{ kind: scenario.owner, id: createResult.file.id }], + }); + }); + + describe('delete created file after test', () => { + afterEach(async () => { + await deleteAllFiles({ + supertest, + kind: scenario.owner, + }); + }); + + it('should list files', async () => { + const { create } = await createAndUploadFile({ + supertest: supertestWithoutAuth, + data: 'abc', + createFileParams: { + name: 'testFile', + mimeType: 'image/png', + kind: scenario.owner, + }, + auth: { user: scenario.user, space: null }, + }); + + const files = await listFiles({ + supertest: supertestWithoutAuth, + params: { kind: scenario.owner }, + auth: { user: scenario.user, space: null }, + }); + + expect(files.total).to.be(1); + expect(files.files[0].name).to.be(create.file.name); + }); + + it('should download a file', async () => { + const { create } = await createAndUploadFile({ + supertest: supertestWithoutAuth, + data: 'abc', + createFileParams: { + name: 'testFile', + mimeType: 'image/png', + kind: scenario.owner, + }, + auth: { user: scenario.user, space: null }, + }); + + const { body: buffer, header } = await downloadFile({ + supertest, + auth: { user: scenario.user, space: null }, + fileId: create.file.id, + kind: scenario.owner, + mimeType: 'image/png', + fileName: 'test.png', + }); + + expect(header['content-type']).to.eql('image/png'); + expect(header['content-disposition']).to.eql('attachment; filename="test.png"'); + expect(buffer.toString('utf8')).to.eql('abc'); + }); + + it('should upload a file', async () => { + const createResult = await createFile({ + supertest: supertestWithoutAuth, + auth: { user: scenario.user, space: null }, + params: { + kind: scenario.owner, + name: 'testFile', + mimeType: 'image/png', + }, + }); + + const uploadResult = await uploadFile({ + supertest: supertestWithoutAuth, + auth: { user: scenario.user, space: null }, + data: 'abc', + kind: scenario.owner, + mimeType: 'image/png', + fileId: createResult.file.id, + }); + + expect(uploadResult.ok).to.be(true); + expect(uploadResult.size).to.be(3); + }); + }); + }); + } + }); + }); + }); +}; diff --git a/x-pack/test/api_integration/apis/cases/index.ts b/x-pack/test/api_integration/apis/cases/index.ts index f12e43b34d784..5bce534873f10 100644 --- a/x-pack/test/api_integration/apis/cases/index.ts +++ b/x-pack/test/api_integration/apis/cases/index.ts @@ -34,5 +34,6 @@ export default function ({ loadTestFile, getService }: FtrProviderContext) { loadTestFile(require.resolve('./privileges')); loadTestFile(require.resolve('./suggest_user_profiles')); loadTestFile(require.resolve('./bulk_get_user_profiles')); + loadTestFile(require.resolve('./files')); }); } diff --git a/x-pack/test/cases_api_integration/common/lib/api/files.ts b/x-pack/test/cases_api_integration/common/lib/api/files.ts new file mode 100644 index 0000000000000..05450e9da2cc9 --- /dev/null +++ b/x-pack/test/cases_api_integration/common/lib/api/files.ts @@ -0,0 +1,226 @@ +/* + * 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 SuperTest from 'supertest'; +import { apiRoutes as fileApiRoutes } from '@kbn/files-plugin/public/files_client/files_client'; +import { BaseFilesClient } from '@kbn/shared-ux-file-types'; +import { superUser } from '../authentication/users'; +import { User } from '../authentication/types'; +import { getSpaceUrlPrefix } from './helpers'; + +export const downloadFile = async ({ + supertest, + fileId, + kind, + mimeType, + fileName, + expectedHttpCode = 200, + auth = { user: superUser, space: null }, +}: { + supertest: SuperTest.SuperTest; + fileId: string; + kind: string; + mimeType: string; + fileName: string; + expectedHttpCode?: number; + auth?: { user: User; space: string | null }; +}): ReturnType => { + const result = await supertest + .get( + `${getSpaceUrlPrefix(auth.space)}${fileApiRoutes.getDownloadRoute(kind, fileId, fileName)}` + ) + .set('accept', mimeType) + .buffer() + .expect(expectedHttpCode); + + return result; +}; + +export interface FileDescriptor { + kind: string; + id: string; +} + +export const deleteFiles = async ({ + supertest, + files, + expectedHttpCode = 200, + auth = { user: superUser, space: null }, +}: { + supertest: SuperTest.SuperTest; + files: FileDescriptor[]; + expectedHttpCode?: number; + auth?: { user: User; space: string | null }; +}) => { + await Promise.all( + files.map(async (fileInfo) => { + return await supertest + .delete( + `${getSpaceUrlPrefix(auth.space)}${fileApiRoutes.getDeleteRoute( + fileInfo.kind, + fileInfo.id + )}` + ) + .set('kbn-xsrf', 'true') + .auth(auth.user.username, auth.user.password) + .send() + .expect(expectedHttpCode); + }) + ); +}; + +export const deleteAllFiles = async ({ + supertest, + kind, + auth = { user: superUser, space: null }, + expectedHttpCode = 200, +}: { + supertest: SuperTest.SuperTest; + kind: string; + expectedHttpCode?: number; + auth?: { user: User; space: string | null }; +}) => { + const files = await listFiles({ supertest, params: { kind }, auth, expectedHttpCode }); + + await deleteFiles({ + supertest, + files: files.files.map((fileInfo) => ({ kind, id: fileInfo.id })), + auth, + expectedHttpCode, + }); +}; + +export const getFileById = async ({ + supertest, + id, + kind, + expectedHttpCode = 200, + auth = { user: superUser, space: null }, +}: { + supertest: SuperTest.SuperTest; + id: string; + kind: string; + expectedHttpCode?: number; + auth?: { user: User; space: string | null }; +}): ReturnType => { + const { body } = await supertest + .get(`${getSpaceUrlPrefix(auth.space)}${fileApiRoutes.getByIdRoute(kind, id)}`) + .auth(auth.user.username, auth.user.password) + .expect(expectedHttpCode); + + return body; +}; + +export const listFiles = async ({ + supertest, + params, + expectedHttpCode = 200, + auth = { user: superUser, space: null }, +}: { + supertest: SuperTest.SuperTest; + params: Parameters[0]; + expectedHttpCode?: number; + auth?: { user: User; space: string | null }; +}): ReturnType => { + const { page, perPage, kind, ...rest } = params; + + const { body } = await supertest + .post(`${getSpaceUrlPrefix(auth.space)}${fileApiRoutes.getListRoute(kind)}`) + .auth(auth.user.username, auth.user.password) + .set('kbn-xsrf', 'true') + .query({ page, perPage }) + .send(rest) + .expect(expectedHttpCode); + + return body; +}; + +type CreateFileSchema = Omit[0], 'mimeType'> & { + mimeType: string; +}; + +export const createFile = async ({ + supertest, + params, + expectedHttpCode = 200, + auth = { user: superUser, space: null }, +}: { + supertest: SuperTest.SuperTest; + params: CreateFileSchema; + expectedHttpCode?: number; + auth?: { user: User; space: string | null }; +}): ReturnType => { + const { kind, ...rest } = params; + const { body } = await supertest + .post(`${getSpaceUrlPrefix(auth.space)}${fileApiRoutes.getCreateFileRoute(kind)}`) + .auth(auth.user.username, auth.user.password) + .set('kbn-xsrf', 'true') + .send(rest) + .expect(expectedHttpCode); + + return body; +}; + +export const uploadFile = async ({ + supertest, + data, + kind, + fileId, + mimeType, + expectedHttpCode = 200, + auth = { user: superUser, space: null }, +}: { + supertest: SuperTest.SuperTest; + data: string | object; + kind: string; + fileId: string; + mimeType: string; + expectedHttpCode?: number; + auth?: { user: User; space: string | null }; +}): ReturnType => { + const { body } = await supertest + .put(`${getSpaceUrlPrefix(auth.space)}${fileApiRoutes.getUploadRoute(kind, fileId)}`) + .auth(auth.user.username, auth.user.password) + .set('kbn-xsrf', 'true') + .set('Content-Type', mimeType) + .send(data) + .expect(expectedHttpCode); + + return body; +}; + +export const createAndUploadFile = async ({ + supertest, + data, + createFileParams, + expectedHttpCode = 200, + auth = { user: superUser, space: null }, +}: { + supertest: SuperTest.SuperTest; + data: string | object; + createFileParams: CreateFileSchema; + expectedHttpCode?: number; + auth?: { user: User; space: string | null }; +}) => { + const createFileResult = await createFile({ + supertest, + params: createFileParams, + expectedHttpCode, + auth, + }); + + const uploadFileResult = await uploadFile({ + supertest, + data, + fileId: createFileResult.file.id, + mimeType: createFileParams.mimeType, + kind: createFileParams.kind, + auth, + }); + + return { create: createFileResult, upload: uploadFileResult }; +}; diff --git a/x-pack/test/cases_api_integration/common/lib/api/index.ts b/x-pack/test/cases_api_integration/common/lib/api/index.ts index c15c19232edf6..3bf0c470b9ba2 100644 --- a/x-pack/test/cases_api_integration/common/lib/api/index.ts +++ b/x-pack/test/cases_api_integration/common/lib/api/index.ts @@ -54,6 +54,7 @@ export * from './user_actions'; export * from './user_profiles'; export * from './omit'; export * from './configuration'; +export * from './files'; export { getSpaceUrlPrefix } from './helpers'; function toArray(input: T | T[]): T[] { diff --git a/x-pack/test/cases_api_integration/common/lib/authentication/index.ts b/x-pack/test/cases_api_integration/common/lib/authentication/index.ts index d425eae5a373c..195549561571b 100644 --- a/x-pack/test/cases_api_integration/common/lib/authentication/index.ts +++ b/x-pack/test/cases_api_integration/common/lib/authentication/index.ts @@ -20,9 +20,8 @@ export const getUserInfo = (user: User): UserInfo => ({ export const createSpaces = async (getService: CommonFtrProviderContext['getService']) => { const spacesService = getService('spaces'); - for (const space of spaces) { - await spacesService.create(space); - } + + await Promise.all(spaces.map((space) => spacesService.create(space))); }; /** @@ -51,23 +50,16 @@ export const createUsersAndRoles = async ( }); }; - for (const role of rolesToCreate) { - await createRole(role); - } - - for (const user of usersToCreate) { - await createUser(user); - } + await Promise.all(rolesToCreate.map((role) => createRole(role))); + await Promise.all(usersToCreate.map((user) => createUser(user))); }; export const deleteSpaces = async (getService: CommonFtrProviderContext['getService']) => { const spacesService = getService('spaces'); - for (const space of spaces) { - try { - await spacesService.delete(space.id); - } catch (error) { - // ignore errors because if a migration is run it will delete the .kibana index which remove the spaces and users - } + try { + await Promise.allSettled(spaces.map((space) => spacesService.delete(space.id))); + } catch (error) { + // ignore errors because if a migration is run it will delete the .kibana index which remove the spaces and users } }; @@ -78,20 +70,16 @@ export const deleteUsersAndRoles = async ( ) => { const security = getService('security'); - for (const user of usersToDelete) { - try { - await security.user.delete(user.username); - } catch (error) { - // ignore errors because if a migration is run it will delete the .kibana index which remove the spaces and users - } + try { + await Promise.allSettled(usersToDelete.map((user) => security.user.delete(user.username))); + } catch (error) { + // ignore errors because if a migration is run it will delete the .kibana index which remove the spaces and users } - for (const role of rolesToDelete) { - try { - await security.role.delete(role.name); - } catch (error) { - // ignore errors because if a migration is run it will delete the .kibana index which remove the spaces and users - } + try { + await Promise.allSettled(rolesToDelete.map((role) => security.role.delete(role.name))); + } catch (error) { + // ignore errors because if a migration is run it will delete the .kibana index which remove the spaces and users } }; diff --git a/x-pack/test/tsconfig.json b/x-pack/test/tsconfig.json index 58a8f285bc9ef..bf4fe75140b6f 100644 --- a/x-pack/test/tsconfig.json +++ b/x-pack/test/tsconfig.json @@ -117,6 +117,8 @@ "@kbn/security-api-integration-helpers", "@kbn/alerts-as-data-utils", "@kbn/discover-plugin", + "@kbn/files-plugin", + "@kbn/shared-ux-file-types", "@kbn/securitysolution-io-ts-alerting-types", ] }