Cases delete files case deletion (#153979)

This PR extends the case deletion functionality to delete any files that
are attached to the case when a case is deleted.

To avoid attempting to delete too many files at once I chunk case ids
into 50 at a time such that we're at most only deleting 5000 (50 case *
100 files per case) at once. That way we don't exceed the 10k find api
limit.

## Testing
Run the python script from here:
https://github.com/elastic/cases-files-generator to generate some files
for a case

Deleting the case should remove all files
This commit is contained in:
Jonathan Buttner 2023-04-10 12:20:58 -04:00 committed by GitHub
parent c98fa8214e
commit b6a113ccfa
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 568 additions and 46 deletions

View file

@ -105,5 +105,5 @@ export interface FindFileArgs extends Pagination {
/**
* File metadata values. These values are governed by the consumer.
*/
meta?: Record<string, string>;
meta?: Record<string, string | string[]>;
}

View file

@ -21,6 +21,7 @@ import {
INTERNAL_CONNECTORS_URL,
INTERNAL_CASE_USERS_URL,
INTERNAL_DELETE_FILE_ATTACHMENTS_URL,
CASE_FIND_ATTACHMENTS_URL,
} from '../constants';
export const getCaseDetailsUrl = (id: string): string => {
@ -39,6 +40,10 @@ export const getCaseCommentDetailsUrl = (caseId: string, commentId: string): str
return CASE_COMMENT_DETAILS_URL.replace('{case_id}', caseId).replace('{comment_id}', commentId);
};
export const getCaseFindAttachmentsUrl = (caseId: string): string => {
return CASE_FIND_ATTACHMENTS_URL.replace('{case_id}', caseId);
};
export const getCaseCommentDeleteUrl = (caseId: string, commentId: string): string => {
return CASE_COMMENT_DELETE_URL.replace('{case_id}', caseId).replace('{comment_id}', commentId);
};

View file

@ -47,6 +47,7 @@ export const CASE_CONFIGURE_DETAILS_URL = `${CASES_URL}/configure/{configuration
export const CASE_CONFIGURE_CONNECTORS_URL = `${CASE_CONFIGURE_URL}/connectors` as const;
export const CASE_COMMENTS_URL = `${CASE_DETAILS_URL}/comments` as const;
export const CASE_FIND_ATTACHMENTS_URL = `${CASE_COMMENTS_URL}/_find` as const;
export const CASE_COMMENT_DETAILS_URL = `${CASE_DETAILS_URL}/comments/{comment_id}` as const;
export const CASE_COMMENT_DELETE_URL = `${CASE_DETAILS_URL}/comments/{comment_id}` as const;
export const CASE_PUSH_URL = `${CASE_DETAILS_URL}/connector/{connector_id}/_push` as const;

View file

@ -18,7 +18,7 @@ export const CaseFileMetadataForDeletionRt = rt.type({
caseIds: rt.array(rt.string),
});
export type CaseFileMetadata = rt.TypeOf<typeof CaseFileMetadataForDeletionRt>;
export type CaseFileMetadataForDeletion = rt.TypeOf<typeof CaseFileMetadataForDeletionRt>;
const FILE_KIND_DELIMITER = 'FilesCases';

View file

@ -12,18 +12,18 @@ import { identity } from 'fp-ts/lib/function';
import pMap from 'p-map';
import { partition } from 'lodash';
import type { File } from '@kbn/files-plugin/common';
import type { File, FileJSON } from '@kbn/files-plugin/common';
import type { FileServiceStart } from '@kbn/files-plugin/server';
import { FileNotFoundError } from '@kbn/files-plugin/server/file_service/errors';
import { BulkDeleteFileAttachmentsRequestRt, excess, throwErrors } from '../../../common/api';
import { MAX_CONCURRENT_SEARCHES } from '../../../common/constants';
import type { CasesClientArgs } from '../types';
import { createCaseError } from '../../common/error';
import type { OwnerEntity } from '../../authorization';
import { Operations } from '../../authorization';
import type { BulkDeleteFileArgs } from './types';
import { constructOwnerFromFileKind, CaseFileMetadataForDeletionRt } from '../../../common/files';
import { CaseFileMetadataForDeletionRt } from '../../../common/files';
import type { CasesClient } from '../client';
import { createFileEntities, deleteFiles } from '../files';
export const bulkDeleteFileAttachments = async (
{ caseId, fileIds }: BulkDeleteFileArgs,
@ -67,9 +67,7 @@ export const bulkDeleteFileAttachments = async (
});
await Promise.all([
pMap(request.ids, async (fileId: string) => fileService.delete({ id: fileId }), {
concurrency: MAX_CONCURRENT_SEARCHES,
}),
deleteFiles(request.ids, fileService),
attachmentService.bulkDelete({
attachmentIds: fileAttachments.map((so) => so.id),
refresh: false,
@ -117,7 +115,7 @@ const getFiles = async (
caseId: BulkDeleteFileArgs['caseId'],
fileIds: BulkDeleteFileArgs['fileIds'],
fileService: FileServiceStart
) => {
): Promise<FileJSON[]> => {
// it's possible that we're trying to delete a file when an attachment wasn't created (for example if the create
// attachment request failed)
const files = await pMap(fileIds, async (fileId: string) => fileService.getById({ id: fileId }), {
@ -143,25 +141,5 @@ const getFiles = async (
throw Boom.badRequest('Failed to find files to delete');
}
return validFiles;
};
const createFileEntities = (files: File[]) => {
const fileEntities: OwnerEntity[] = [];
// It's possible that the owner array could have invalid information in it so we'll use the file kind for determining if the user
// has the correct authorization for deleting these files
for (const fileInfo of files) {
const ownerFromFileKind = constructOwnerFromFileKind(fileInfo.data.fileKind);
if (ownerFromFileKind == null) {
throw Boom.badRequest(
`File id ${fileInfo.id} has invalid file kind ${fileInfo.data.fileKind}`
);
}
fileEntities.push({ id: fileInfo.id, owner: ownerFromFileKind });
}
return fileEntities;
return validFiles.map((fileInfo) => fileInfo.data);
};

View file

@ -0,0 +1,79 @@
/*
* 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_FILES_PER_CASE } from '../../../common/constants';
import type { FindFileArgs } from '@kbn/files-plugin/server';
import { createFileServiceMock } from '@kbn/files-plugin/server/mocks';
import type { FileJSON } from '@kbn/shared-ux-file-types';
import type { CaseFileMetadataForDeletion } from '../../../common/files';
import { constructFileKindIdByOwner } from '../../../common/files';
import { getFileEntities } from './delete';
const getCaseIds = (numIds: number) => {
return Array.from(Array(numIds).keys()).map((key) => key.toString());
};
describe('delete', () => {
describe('getFileEntities', () => {
const numCaseIds = 1000;
const caseIds = getCaseIds(numCaseIds);
const mockFileService = createFileServiceMock();
mockFileService.find.mockImplementation(async (args: FindFileArgs) => {
const caseMeta = args.meta as unknown as CaseFileMetadataForDeletion;
const numFilesToGen = caseMeta.caseIds.length * MAX_FILES_PER_CASE;
const files = Array.from(Array(numFilesToGen).keys()).map(() => createMockFileJSON());
return {
files,
total: files.length,
};
});
beforeEach(() => {
jest.clearAllMocks();
});
it('only provides 50 case ids in a single call to the find api', async () => {
await getFileEntities(caseIds, mockFileService);
for (const call of mockFileService.find.mock.calls) {
const callMeta = call[0].meta as unknown as CaseFileMetadataForDeletion;
expect(callMeta.caseIds.length).toEqual(50);
}
});
it('calls the find function the number of case ids divided by the chunk size', async () => {
await getFileEntities(caseIds, mockFileService);
const chunkSize = 50;
expect(mockFileService.find).toHaveBeenCalledTimes(numCaseIds / chunkSize);
});
it('returns the number of entities equal to the case ids times the max files per case limit', async () => {
const expectedEntities = Array.from(Array(numCaseIds * MAX_FILES_PER_CASE).keys()).map(
() => ({
id: '123',
owner: 'securitySolution',
})
);
const entities = await getFileEntities(caseIds, mockFileService);
expect(entities.length).toEqual(numCaseIds * MAX_FILES_PER_CASE);
expect(entities).toEqual(expectedEntities);
});
});
});
const createMockFileJSON = (): FileJSON => {
return {
id: '123',
fileKind: constructFileKindIdByOwner('securitySolution'),
meta: {
owner: ['securitySolution'],
},
} as unknown as FileJSON;
};

View file

@ -6,27 +6,32 @@
*/
import { Boom } from '@hapi/boom';
import pMap from 'p-map';
import { chunk } from 'lodash';
import type { SavedObjectsBulkDeleteObject } from '@kbn/core/server';
import type { FileServiceStart } from '@kbn/files-plugin/server';
import {
CASE_COMMENT_SAVED_OBJECT,
CASE_SAVED_OBJECT,
CASE_USER_ACTION_SAVED_OBJECT,
MAX_FILES_PER_CASE,
MAX_DOCS_PER_PAGE,
} from '../../../common/constants';
import type { CasesClientArgs } from '..';
import { createCaseError } from '../../common/error';
import type { OwnerEntity } from '../../authorization';
import { Operations } from '../../authorization';
import { createFileEntities, deleteFiles } from '../files';
/**
* Deletes the specified cases and their attachments.
*
* @ignore
*/
export async function deleteCases(ids: string[], clientArgs: CasesClientArgs): Promise<void> {
const {
services: { caseService, attachmentService, userActionService },
logger,
authorization,
fileService,
} = clientArgs;
try {
const cases = await caseService.getCases({ caseIds: ids });
@ -44,9 +49,11 @@ export async function deleteCases(ids: string[], clientArgs: CasesClientArgs): P
entities.set(theCase.id, { id: theCase.id, owner: theCase.attributes.owner });
}
const fileEntities = await getFileEntities(ids, fileService);
await authorization.ensureAuthorized({
operation: Operations.deleteCase,
entities: Array.from(entities.values()),
entities: [...Array.from(entities.values()), ...fileEntities],
});
const attachmentIds = await attachmentService.getter.getAttachmentIdsForCases({
@ -61,10 +68,14 @@ export async function deleteCases(ids: string[], clientArgs: CasesClientArgs): P
...userActionIds.map((id) => ({ id, type: CASE_USER_ACTION_SAVED_OBJECT })),
];
await caseService.bulkDeleteCaseEntities({
entities: bulkDeleteEntities,
options: { refresh: 'wait_for' },
});
const fileIds = fileEntities.map((entity) => entity.id);
await Promise.all([
deleteFiles(fileIds, fileService),
caseService.bulkDeleteCaseEntities({
entities: bulkDeleteEntities,
options: { refresh: 'wait_for' },
}),
]);
await userActionService.creator.bulkAuditLogCaseDeletion(
cases.saved_objects.map((caseInfo) => caseInfo.id)
@ -77,3 +88,29 @@ export async function deleteCases(ids: string[], clientArgs: CasesClientArgs): P
});
}
}
export const getFileEntities = async (
caseIds: string[],
fileService: FileServiceStart
): Promise<OwnerEntity[]> => {
// using 50 just to be safe, each case can have 100 files = 50 * 100 = 5000 which is half the max number of docs that
// the client can request
const chunkSize = MAX_FILES_PER_CASE / 2;
const chunkedIds = chunk(caseIds, chunkSize);
const entityResults = await pMap(chunkedIds, async (ids: string[]) => {
const findRes = await fileService.find({
perPage: MAX_DOCS_PER_PAGE,
meta: {
caseIds: ids,
},
});
const fileEntities = createFileEntities(findRes.files);
return fileEntities;
});
const entities = entityResults.flatMap((res) => res);
return entities;
};

View file

@ -0,0 +1,77 @@
/*
* 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 { createFileServiceMock } from '@kbn/files-plugin/server/mocks';
import { OBSERVABILITY_OWNER, SECURITY_SOLUTION_OWNER } from '../../../common';
import { constructFileKindIdByOwner } from '../../../common/files';
import { createFileEntities, deleteFiles } from '.';
describe('server files', () => {
describe('createFileEntities', () => {
it('returns an empty array when passed no files', () => {
expect(createFileEntities([])).toEqual([]);
});
it('throws an error when the file kind is not valid', () => {
expect.assertions(1);
expect(() =>
createFileEntities([{ fileKind: 'abc', id: '1' }])
).toThrowErrorMatchingInlineSnapshot(`"File id 1 has invalid file kind abc"`);
});
it('throws an error when one of the file entities does not have a valid file kind', () => {
expect.assertions(1);
expect(() =>
createFileEntities([
{ fileKind: constructFileKindIdByOwner(SECURITY_SOLUTION_OWNER), id: '1' },
{ fileKind: 'abc', id: '2' },
])
).toThrowErrorMatchingInlineSnapshot(`"File id 2 has invalid file kind abc"`);
});
it('returns an array of entities when the file kind is valid', () => {
expect.assertions(1);
expect(
createFileEntities([
{ fileKind: constructFileKindIdByOwner(SECURITY_SOLUTION_OWNER), id: '1' },
{ fileKind: constructFileKindIdByOwner(OBSERVABILITY_OWNER), id: '2' },
])
).toEqual([
{ id: '1', owner: 'securitySolution' },
{ id: '2', owner: 'observability' },
]);
});
});
describe('deleteFiles', () => {
it('calls delete twice with the ids passed in', async () => {
const fileServiceMock = createFileServiceMock();
expect.assertions(2);
await deleteFiles(['1', '2'], fileServiceMock);
expect(fileServiceMock.delete).toBeCalledTimes(2);
expect(fileServiceMock.delete.mock.calls).toMatchInlineSnapshot(`
Array [
Array [
Object {
"id": "1",
},
],
Array [
Object {
"id": "2",
},
],
]
`);
});
});
});

View file

@ -0,0 +1,39 @@
/*
* 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 Boom from '@hapi/boom';
import type { FileJSON } from '@kbn/files-plugin/common';
import type { FileServiceStart } from '@kbn/files-plugin/server';
import pMap from 'p-map';
import { constructOwnerFromFileKind } from '../../../common/files';
import { MAX_CONCURRENT_SEARCHES } from '../../../common/constants';
import type { OwnerEntity } from '../../authorization';
type FileEntityInfo = Pick<FileJSON, 'fileKind' | 'id'>;
export const createFileEntities = (files: FileEntityInfo[]): OwnerEntity[] => {
const fileEntities: OwnerEntity[] = [];
// It's possible that the owner array could have invalid information in it so we'll use the file kind for determining if the user
// has the correct authorization for deleting these files
for (const fileInfo of files) {
const ownerFromFileKind = constructOwnerFromFileKind(fileInfo.fileKind);
if (ownerFromFileKind == null) {
throw Boom.badRequest(`File id ${fileInfo.id} has invalid file kind ${fileInfo.fileKind}`);
}
fileEntities.push({ id: fileInfo.id, owner: ownerFromFileKind });
}
return fileEntities;
};
export const deleteFiles = async (fileIds: string[], fileService: FileServiceStart) =>
pMap(fileIds, async (fileId: string) => fileService.delete({ id: fileId }), {
concurrency: MAX_CONCURRENT_SEARCHES,
});

View file

@ -13,13 +13,13 @@ import { fold } from 'fp-ts/lib/Either';
import { identity } from 'fp-ts/lib/function';
import { FindQueryParamsRt, throwErrors, excess } from '../../../../common/api';
import { CASE_COMMENTS_URL } from '../../../../common/constants';
import { CASE_FIND_ATTACHMENTS_URL } from '../../../../common/constants';
import { createCasesRoute } from '../create_cases_route';
import { createCaseError } from '../../../common/error';
export const findCommentsRoute = createCasesRoute({
method: 'get',
path: `${CASE_COMMENTS_URL}/_find`,
path: CASE_FIND_ATTACHMENTS_URL,
params: {
params: schema.object({
case_id: schema.string(),

View file

@ -15,7 +15,9 @@ import {
CommentPatchRequest,
CommentRequest,
CommentResponse,
CommentsResponse,
CommentType,
getCaseFindAttachmentsUrl,
getCasesDeleteFileAttachmentsUrl,
} from '@kbn/cases-plugin/common/api';
import { User } from '../authentication/types';
@ -280,3 +282,26 @@ export const bulkDeleteFileAttachments = async ({
.auth(auth.user.username, auth.user.password)
.expect(expectedHttpCode);
};
export const findAttachments = async ({
supertest,
caseId,
query = {},
expectedHttpCode = 200,
auth = { user: superUser, space: null },
}: {
supertest: SuperTest.SuperTest<SuperTest.Test>;
caseId: string;
query?: Record<string, unknown>;
expectedHttpCode?: number;
auth?: { user: User; space: string | null };
}): Promise<CommentsResponse> => {
const { body } = await supertest
.get(`${getSpaceUrlPrefix(auth.space)}${getCaseFindAttachmentsUrl(caseId)}`)
.set('kbn-xsrf', 'true')
.query(query)
.auth(auth.user.username, auth.user.password)
.expect(expectedHttpCode);
return body;
};

View file

@ -6,13 +6,16 @@
*/
import expect from '@kbn/expect';
import type SuperTest from 'supertest';
import { MAX_DOCS_PER_PAGE } from '@kbn/cases-plugin/common/constants';
import { FtrProviderContext } from '../../../../common/ftr_provider_context';
import { getPostCaseRequest, postCommentUserReq } from '../../../../common/lib/mock';
import {
deleteCasesByESQuery,
deleteCasesUserActions,
deleteComments,
getFilesAttachmentReq,
getPostCaseRequest,
postCommentUserReq,
} from '../../../../common/lib/mock';
import {
createCase,
deleteCases,
createComment,
@ -20,6 +23,12 @@ import {
getCase,
superUserSpace1Auth,
getCaseUserActions,
deleteAllCaseItems,
createAndUploadFile,
deleteAllFiles,
listFiles,
findAttachments,
bulkCreateAttachments,
} from '../../../../common/lib/api';
import {
secOnly,
@ -31,6 +40,17 @@ import {
obsOnly,
superUser,
} from '../../../../common/lib/authentication/users';
import {
secAllUser,
users as api_int_users,
} from '../../../../../api_integration/apis/cases/common/users';
import { roles as api_int_roles } from '../../../../../api_integration/apis/cases/common/roles';
import { createUsersAndRoles, deleteUsersAndRoles } from '../../../../common/lib/authentication';
import {
OBSERVABILITY_FILE_KIND,
SECURITY_SOLUTION_FILE_KIND,
} from '../../../../common/lib/constants';
import { User } from '../../../../common/lib/authentication/types';
// eslint-disable-next-line import/no-default-export
export default ({ getService }: FtrProviderContext): void => {
@ -40,9 +60,7 @@ export default ({ getService }: FtrProviderContext): void => {
describe('delete_cases', () => {
afterEach(async () => {
await deleteCasesByESQuery(es);
await deleteComments(es);
await deleteCasesUserActions(es);
await deleteAllCaseItems(es);
});
it('should delete a case', async () => {
@ -106,7 +124,204 @@ export default ({ getService }: FtrProviderContext): void => {
await deleteCases({ supertest, caseIDs: ['fake-id'], expectedHttpCode: 404 });
});
describe('files', () => {
afterEach(async () => {
await deleteAllFiles({
supertest,
});
});
it('should delete all files associated with a case', async () => {
const { caseInfo: postedCase } = await createCaseWithFiles({
supertest: supertestWithoutAuth,
fileKind: SECURITY_SOLUTION_FILE_KIND,
owner: 'securitySolution',
});
await deleteCases({ supertest: supertestWithoutAuth, caseIDs: [postedCase.id] });
const [filesAfterDelete, attachmentsAfterDelete] = await Promise.all([
listFiles({
supertest: supertestWithoutAuth,
params: {
kind: SECURITY_SOLUTION_FILE_KIND,
},
}),
findAttachments({
supertest: supertestWithoutAuth,
caseId: postedCase.id,
query: {
perPage: MAX_DOCS_PER_PAGE,
},
}),
]);
expect(filesAfterDelete.total).to.be(0);
expect(attachmentsAfterDelete.comments.length).to.be(0);
});
it('should delete all files associated with multiple cases', async () => {
const [{ caseInfo: postedCase1 }, { caseInfo: postedCase2 }] = await Promise.all([
createCaseWithFiles({
supertest: supertestWithoutAuth,
fileKind: SECURITY_SOLUTION_FILE_KIND,
owner: 'securitySolution',
}),
createCaseWithFiles({
supertest: supertestWithoutAuth,
fileKind: SECURITY_SOLUTION_FILE_KIND,
owner: 'securitySolution',
}),
]);
await deleteCases({
supertest: supertestWithoutAuth,
caseIDs: [postedCase1.id, postedCase2.id],
});
const [filesAfterDelete, attachmentsAfterDelete, attachmentsAfterDelete2] =
await Promise.all([
listFiles({
supertest: supertestWithoutAuth,
params: {
kind: SECURITY_SOLUTION_FILE_KIND,
},
}),
findAttachments({
supertest: supertestWithoutAuth,
caseId: postedCase1.id,
query: {
perPage: MAX_DOCS_PER_PAGE,
},
}),
findAttachments({
supertest: supertestWithoutAuth,
caseId: postedCase2.id,
query: {
perPage: MAX_DOCS_PER_PAGE,
},
}),
]);
expect(filesAfterDelete.total).to.be(0);
expect(attachmentsAfterDelete.comments.length).to.be(0);
expect(attachmentsAfterDelete2.comments.length).to.be(0);
});
});
describe('rbac', () => {
describe('files', () => {
// we need api_int_users and roles because they have authorization for the actual plugins (not the fixtures). This
// is needed because the fixture plugins are not registered as file kinds
before(async () => {
await createUsersAndRoles(getService, api_int_users, api_int_roles);
});
after(async () => {
await deleteUsersAndRoles(getService, api_int_users, api_int_roles);
});
it('should delete a case when the user has access to delete the case and files', async () => {
const { caseInfo: postedCase } = await createCaseWithFiles({
supertest: supertestWithoutAuth,
fileKind: SECURITY_SOLUTION_FILE_KIND,
owner: 'securitySolution',
auth: { user: secAllUser, space: 'space1' },
});
await deleteCases({
supertest: supertestWithoutAuth,
caseIDs: [postedCase.id],
auth: { user: secAllUser, space: 'space1' },
});
const [filesAfterDelete, attachmentsAfterDelete] = await Promise.all([
listFiles({
supertest: supertestWithoutAuth,
params: {
kind: SECURITY_SOLUTION_FILE_KIND,
},
auth: { user: secAllUser, space: 'space1' },
}),
findAttachments({
supertest: supertestWithoutAuth,
caseId: postedCase.id,
query: {
perPage: MAX_DOCS_PER_PAGE,
},
auth: { user: secAllUser, space: 'space1' },
}),
]);
expect(filesAfterDelete.total).to.be(0);
expect(attachmentsAfterDelete.comments.length).to.be(0);
});
it('should not delete a case when the user does not have access to the file kind of the files', async () => {
const postedCase = await createCase(
supertestWithoutAuth,
getPostCaseRequest({ owner: 'securitySolution' }),
200,
{ user: secAllUser, space: 'space1' }
);
const { create: createdFile } = await createAndUploadFile({
supertest: supertestWithoutAuth,
createFileParams: {
name: 'testfile',
// use observability for the file kind which the security user should not have access to
kind: OBSERVABILITY_FILE_KIND,
mimeType: 'text/plain',
meta: {
caseIds: [postedCase.id],
owner: [postedCase.owner],
},
},
data: 'abc',
auth: { user: superUser, space: 'space1' },
});
await bulkCreateAttachments({
supertest: supertestWithoutAuth,
caseId: postedCase.id,
params: [
getFilesAttachmentReq({
externalReferenceId: createdFile.file.id,
owner: 'securitySolution',
}),
],
auth: { user: secAllUser, space: 'space1' },
});
await deleteCases({
supertest: supertestWithoutAuth,
caseIDs: [postedCase.id],
auth: { user: secAllUser, space: 'space1' },
expectedHttpCode: 403,
});
const [filesAfterDelete, attachmentsAfterDelete] = await Promise.all([
listFiles({
supertest: supertestWithoutAuth,
params: {
kind: OBSERVABILITY_FILE_KIND,
},
auth: { user: superUser, space: 'space1' },
}),
findAttachments({
supertest: supertestWithoutAuth,
caseId: postedCase.id,
query: {
perPage: MAX_DOCS_PER_PAGE,
},
auth: { user: secAllUser, space: 'space1' },
}),
]);
expect(filesAfterDelete.total).to.be(1);
expect(attachmentsAfterDelete.comments.length).to.be(1);
});
});
it('User: security solution only - should delete a case', async () => {
const postedCase = await createCase(
supertestWithoutAuth,
@ -255,3 +470,69 @@ export default ({ getService }: FtrProviderContext): void => {
});
});
};
const createCaseWithFiles = async ({
supertest,
fileKind,
owner,
auth = { user: superUser, space: null },
}: {
supertest: SuperTest.SuperTest<SuperTest.Test>;
fileKind: string;
owner: string;
auth?: { user: User; space: string | null };
}) => {
const postedCase = await createCase(supertest, getPostCaseRequest({ owner }), 200, auth);
const files = await Promise.all([
createAndUploadFile({
supertest,
createFileParams: {
name: 'testfile',
kind: fileKind,
mimeType: 'text/plain',
meta: {
caseIds: [postedCase.id],
owner: [postedCase.owner],
},
},
data: 'abc',
auth,
}),
createAndUploadFile({
supertest,
createFileParams: {
name: 'testfile',
kind: fileKind,
mimeType: 'text/plain',
meta: {
caseIds: [postedCase.id],
owner: [postedCase.owner],
},
},
data: 'abc',
auth,
}),
]);
const caseWithAttachments = await bulkCreateAttachments({
supertest,
caseId: postedCase.id,
params: [
getFilesAttachmentReq({
externalReferenceId: files[0].create.file.id,
owner,
}),
getFilesAttachmentReq({
externalReferenceId: files[1].create.file.id,
owner,
}),
],
auth,
});
return {
caseInfo: caseWithAttachments,
attachments: files,
};
};