[Cases] Delete alerts when deleting all comments (#154202)

## Summary

This PR remove the case id from the alerts when deleting all cases
comments

### Checklist

Delete any items that are not applicable to this PR.

- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios

### For maintainers

- [x] This was checked for breaking API changes and was [labeled
appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)
This commit is contained in:
Christos Nasikas 2023-04-04 15:24:55 +03:00 committed by GitHub
parent 4cd0460f5a
commit 66ad9e0ba5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 699 additions and 185 deletions

View file

@ -7,40 +7,93 @@
import { mockCaseComments } from '../../mocks';
import { createCasesClientMockArgs } from '../mocks';
import { deleteComment } from './delete';
import { deleteComment, deleteAll } from './delete';
describe('deleteComment', () => {
const clientArgs = createCasesClientMockArgs();
describe('delete', () => {
describe('deleteComment', () => {
const clientArgs = createCasesClientMockArgs();
beforeEach(() => {
jest.clearAllMocks();
});
describe('Alerts', () => {
const commentSO = mockCaseComments[0];
const alertsSO = mockCaseComments[3];
clientArgs.services.attachmentService.getter.get.mockResolvedValue(alertsSO);
it('delete alerts correctly', async () => {
await deleteComment({ caseID: 'mock-id-4', attachmentID: 'mock-comment-4' }, clientArgs);
expect(clientArgs.services.alertsService.ensureAlertsAuthorized).toHaveBeenCalledWith({
alerts: [{ id: 'test-id', index: 'test-index' }],
});
expect(clientArgs.services.alertsService.removeCaseIdFromAlerts).toHaveBeenCalledWith({
alerts: [{ id: 'test-id', index: 'test-index' }],
caseId: 'mock-id-4',
});
beforeEach(() => {
jest.clearAllMocks();
});
it('does not call the alert service when the attachment is not an alert', async () => {
clientArgs.services.attachmentService.getter.get.mockResolvedValue(commentSO);
await deleteComment({ caseID: 'mock-id-1', attachmentID: 'mock-comment-1' }, clientArgs);
describe('Alerts', () => {
const commentSO = mockCaseComments[0];
const alertsSO = mockCaseComments[3];
clientArgs.services.attachmentService.getter.get.mockResolvedValue(alertsSO);
expect(clientArgs.services.alertsService.ensureAlertsAuthorized).not.toHaveBeenCalledWith();
it('delete alerts correctly', async () => {
await deleteComment({ caseID: 'mock-id-4', attachmentID: 'mock-comment-4' }, clientArgs);
expect(clientArgs.services.alertsService.removeCaseIdFromAlerts).not.toHaveBeenCalledWith();
expect(clientArgs.services.alertsService.ensureAlertsAuthorized).toHaveBeenCalledWith({
alerts: [{ id: 'test-id', index: 'test-index' }],
});
expect(clientArgs.services.alertsService.removeCaseIdFromAlerts).toHaveBeenCalledWith({
alerts: [{ id: 'test-id', index: 'test-index' }],
caseId: 'mock-id-4',
});
});
it('does not call the alert service when the attachment is not an alert', async () => {
clientArgs.services.attachmentService.getter.get.mockResolvedValue(commentSO);
await deleteComment({ caseID: 'mock-id-1', attachmentID: 'mock-comment-1' }, clientArgs);
expect(clientArgs.services.alertsService.ensureAlertsAuthorized).not.toHaveBeenCalledWith();
expect(clientArgs.services.alertsService.removeCaseIdFromAlerts).not.toHaveBeenCalledWith();
});
});
});
describe('deleteAll', () => {
const clientArgs = createCasesClientMockArgs();
const getAllCaseCommentsResponse = {
saved_objects: mockCaseComments.map((so) => ({ ...so, score: 0 })),
total: mockCaseComments.length,
page: 1,
per_page: mockCaseComments.length,
};
beforeEach(() => {
jest.clearAllMocks();
});
describe('Alerts', () => {
clientArgs.services.caseService.getAllCaseComments.mockResolvedValue(
getAllCaseCommentsResponse
);
it('delete alerts correctly', async () => {
await deleteAll({ caseID: 'mock-id-4' }, clientArgs);
expect(clientArgs.services.alertsService.ensureAlertsAuthorized).toHaveBeenCalledWith({
alerts: [
{ id: 'test-id', index: 'test-index' },
{ id: 'test-id-2', index: 'test-index-2' },
{ id: 'test-id-3', index: 'test-index-3' },
],
});
expect(clientArgs.services.alertsService.removeCaseIdFromAlerts).toHaveBeenCalledWith({
alerts: [
{ id: 'test-id', index: 'test-index' },
{ id: 'test-id-2', index: 'test-index-2' },
{ id: 'test-id-3', index: 'test-index-3' },
],
caseId: 'mock-id-4',
});
});
it('does not call the alert service when the attachment is not an alert', async () => {
clientArgs.services.caseService.getAllCaseComments.mockResolvedValue({
...getAllCaseCommentsResponse,
saved_objects: [{ ...mockCaseComments[0], score: 0 }],
});
await deleteAll({ caseID: 'mock-id-1' }, clientArgs);
expect(clientArgs.services.alertsService.ensureAlertsAuthorized).not.toHaveBeenCalledWith();
expect(clientArgs.services.alertsService.removeCaseIdFromAlerts).not.toHaveBeenCalledWith();
});
});
});
});

View file

@ -7,7 +7,7 @@
import Boom from '@hapi/boom';
import type { CommentAttributes } from '../../../common/api';
import type { CommentRequest, CommentRequestAlertType } from '../../../common/api';
import { Actions, ActionTypes } from '../../../common/api';
import { CASE_SAVED_OBJECT } from '../../../common/constants';
import { getAlertInfoFromComments, isCommentRequestTypeAlert } from '../../common/utils';
@ -25,7 +25,7 @@ export async function deleteAll(
): Promise<void> {
const {
user,
services: { caseService, attachmentService, userActionService },
services: { caseService, attachmentService, userActionService, alertsService },
logger,
authorization,
} = clientArgs;
@ -61,6 +61,10 @@ export async function deleteAll(
})),
user,
});
const attachments = comments.saved_objects.map((comment) => comment.attributes);
await handleAlerts({ alertsService, attachments, caseId: caseID });
} catch (error) {
throw createCaseError({
message: `Failed to delete all comments case id: ${caseID}: ${error}`,
@ -121,7 +125,7 @@ export async function deleteComment(
owner: attachment.attributes.owner,
});
await handleAlerts({ alertsService, attachment: attachment.attributes, caseId: id });
await handleAlerts({ alertsService, attachments: [attachment.attributes], caseId: id });
} catch (error) {
throw createCaseError({
message: `Failed to delete comment: ${caseID} comment id: ${attachmentID}: ${error}`,
@ -133,16 +137,20 @@ export async function deleteComment(
interface HandleAlertsArgs {
alertsService: CasesClientArgs['services']['alertsService'];
attachment: CommentAttributes;
attachments: CommentRequest[];
caseId: string;
}
const handleAlerts = async ({ alertsService, attachment, caseId }: HandleAlertsArgs) => {
if (!isCommentRequestTypeAlert(attachment)) {
const handleAlerts = async ({ alertsService, attachments, caseId }: HandleAlertsArgs) => {
const alertAttachments = attachments.filter((attachment): attachment is CommentRequestAlertType =>
isCommentRequestTypeAlert(attachment)
);
if (alertAttachments.length === 0) {
return;
}
const alerts = getAlertInfoFromComments([attachment]);
const alerts = getAlertInfoFromComments(alertAttachments);
await alertsService.ensureAlertsAuthorized({ alerts });
await alertsService.removeCaseIdFromAlerts({ alerts, caseId });
};

View file

@ -370,8 +370,8 @@ export const mockCaseComments: Array<SavedObject<CommentAttributes>> = [
id: 'mock-comment-6',
attributes: {
type: CommentType.alert,
index: 'test-index',
alertId: 'test-id',
index: 'test-index-3',
alertId: 'test-id-3',
created_at: '2019-11-25T22:32:30.608Z',
created_by: {
full_name: 'elastic',

View file

@ -5,12 +5,15 @@
* 2.0.
*/
import expect from '@kbn/expect';
import type SuperTest from 'supertest';
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { ToolingLog } from '@kbn/tooling-log';
import { DETECTION_ENGINE_QUERY_SIGNALS_URL } from '@kbn/security-solution-plugin/common/constants';
import { DetectionAlert } from '@kbn/security-solution-plugin/common/detection_engine/schemas/alerts';
import { RiskEnrichmentFields } from '@kbn/security-solution-plugin/server/lib/detection_engine/rule_types/utils/enrichments/types';
import { CommentType } from '@kbn/cases-plugin/common';
import { ALERT_CASE_IDS } from '@kbn/rule-data-utils';
import {
getRuleForSignalTesting,
createRule,
@ -22,6 +25,9 @@ import {
import { superUser } from './authentication/users';
import { User } from './authentication/types';
import { getSpaceUrlPrefix } from './api/helpers';
import { createCase } from './api/case';
import { createComment, deleteAllComments } from './api';
import { postCaseReq } from './mock';
export const createSecuritySolutionAlerts = async (
supertest: SuperTest.SuperTest<SuperTest.Test>,
@ -74,3 +80,92 @@ export const getAlertById = async ({
return alert;
};
export type Alerts = Array<{ _id: string; _index: string }>;
export const createCaseAttachAlertAndDeleteAlert = async ({
supertest,
totalCases,
indexOfCaseToDelete,
owner,
expectedHttpCode = 204,
deleteCommentAuth = { user: superUser, space: 'space1' },
alerts,
getAlerts,
}: {
supertest: SuperTest.SuperTest<SuperTest.Test>;
totalCases: number;
indexOfCaseToDelete: number;
owner: string;
expectedHttpCode?: number;
deleteCommentAuth?: { user: User; space: string | null };
alerts: Alerts;
getAlerts: (alerts: Alerts) => Promise<Array<Record<string, unknown>>>;
}) => {
const cases = await Promise.all(
[...Array(totalCases).keys()].map((index) =>
createCase(
supertest,
{
...postCaseReq,
owner,
settings: { syncAlerts: false },
},
200,
{ user: superUser, space: 'space1' }
)
)
);
const updatedCases = [];
for (const theCase of cases) {
const updatedCase = await createComment({
supertest,
caseId: theCase.id,
params: {
alertId: alerts.map((alert) => alert._id),
index: alerts.map((alert) => alert._index),
rule: {
id: 'id',
name: 'name',
},
owner,
type: CommentType.alert,
},
auth: { user: superUser, space: 'space1' },
});
updatedCases.push(updatedCase);
}
const caseIds = updatedCases.map((theCase) => theCase.id);
const updatedAlerts = await getAlerts(alerts);
for (const alert of updatedAlerts) {
expect(alert[ALERT_CASE_IDS]).eql(caseIds);
}
const caseToDelete = updatedCases[indexOfCaseToDelete];
await deleteAllComments({
supertest,
caseId: caseToDelete.id,
expectedHttpCode,
auth: deleteCommentAuth,
});
const alertAfterDeletion = await getAlerts(alerts);
const caseIdsWithoutRemovedCase =
expectedHttpCode === 204
? updatedCases
.filter((theCase) => theCase.id !== caseToDelete.id)
.map((theCase) => theCase.id)
: updatedCases.map((theCase) => theCase.id);
for (const alert of alertAfterDeletion) {
expect(alert[ALERT_CASE_IDS]).eql(caseIdsWithoutRemovedCase);
}
};

View file

@ -6,9 +6,8 @@
*/
import expect from '@kbn/expect';
import { CommentType } from '@kbn/cases-plugin/common';
import { ALERT_CASE_IDS } from '@kbn/rule-data-utils';
import {
createCaseAttachAlertAndDeleteAlert,
createSecuritySolutionAlerts,
getAlertById,
getSecuritySolutionAlerts,
@ -19,7 +18,6 @@ import {
deleteAllRules,
} from '../../../../../detection_engine_api_integration/utils';
import { FtrProviderContext } from '../../../../common/ftr_provider_context';
import { User } from '../../../../common/lib/authentication/types';
import { getPostCaseRequest, postCaseReq, postCommentUserReq } from '../../../../common/lib/mock';
import {
@ -30,7 +28,6 @@ import {
createCase,
createComment,
deleteComment,
deleteAllComments,
superUserSpace1Auth,
} from '../../../../common/lib/api';
import {
@ -114,93 +111,6 @@ export default ({ getService }: FtrProviderContext): void => {
describe('alerts', () => {
type Alerts = Array<{ _id: string; _index: string }>;
const createCaseAttachAlertAndDeleteAlert = async ({
totalCases,
indexOfCaseToDelete,
owner,
expectedHttpCode = 204,
deleteCommentAuth = { user: superUser, space: 'space1' },
alerts,
getAlerts,
}: {
totalCases: number;
indexOfCaseToDelete: number;
owner: string;
expectedHttpCode?: number;
deleteCommentAuth?: { user: User; space: string | null };
alerts: Alerts;
getAlerts: (alerts: Alerts) => Promise<Array<Record<string, unknown>>>;
}) => {
const cases = await Promise.all(
[...Array(totalCases).keys()].map((index) =>
createCase(
supertestWithoutAuth,
{
...postCaseReq,
owner,
settings: { syncAlerts: false },
},
200,
{ user: superUser, space: 'space1' }
)
)
);
const updatedCases = [];
for (const theCase of cases) {
const updatedCase = await createComment({
supertest: supertestWithoutAuth,
caseId: theCase.id,
params: {
alertId: alerts.map((alert) => alert._id),
index: alerts.map((alert) => alert._index),
rule: {
id: 'id',
name: 'name',
},
owner,
type: CommentType.alert,
},
auth: { user: superUser, space: 'space1' },
});
updatedCases.push(updatedCase);
}
const caseIds = updatedCases.map((theCase) => theCase.id);
const updatedAlerts = await getAlerts(alerts);
for (const alert of updatedAlerts) {
expect(alert[ALERT_CASE_IDS]).eql(caseIds);
}
const caseToDelete = updatedCases[indexOfCaseToDelete];
const commentId = caseToDelete.comments![0].id;
await deleteComment({
supertest: supertestWithoutAuth,
caseId: caseToDelete.id,
commentId,
expectedHttpCode,
auth: deleteCommentAuth,
});
const alertAfterDeletion = await getAlerts(alerts);
const caseIdsWithoutRemovedCase =
expectedHttpCode === 204
? updatedCases
.filter((theCase) => theCase.id !== caseToDelete.id)
.map((theCase) => theCase.id)
: updatedCases.map((theCase) => theCase.id);
for (const alert of alertAfterDeletion) {
expect(alert[ALERT_CASE_IDS]).eql(caseIdsWithoutRemovedCase);
}
};
describe('security_solution', () => {
let alerts: Alerts = [];
@ -229,6 +139,7 @@ export default ({ getService }: FtrProviderContext): void => {
it('removes a case from the alert schema when deleting an alert attachment', async () => {
await createCaseAttachAlertAndDeleteAlert({
supertest: supertestWithoutAuth,
totalCases: 1,
indexOfCaseToDelete: 0,
owner: 'securitySolutionFixture',
@ -239,6 +150,7 @@ export default ({ getService }: FtrProviderContext): void => {
it('should remove only one case', async () => {
await createCaseAttachAlertAndDeleteAlert({
supertest: supertestWithoutAuth,
totalCases: 3,
indexOfCaseToDelete: 1,
owner: 'securitySolutionFixture',
@ -249,6 +161,7 @@ export default ({ getService }: FtrProviderContext): void => {
it('should delete case ID from the alert schema when the user has write access to the indices and only read access to the siem solution', async () => {
await createCaseAttachAlertAndDeleteAlert({
supertest: supertestWithoutAuth,
totalCases: 1,
indexOfCaseToDelete: 0,
owner: 'securitySolutionFixture',
@ -261,6 +174,7 @@ export default ({ getService }: FtrProviderContext): void => {
it('should NOT delete case ID from the alert schema when the user does NOT have access to the alert', async () => {
await createCaseAttachAlertAndDeleteAlert({
supertest: supertestWithoutAuth,
totalCases: 1,
indexOfCaseToDelete: 0,
owner: 'securitySolutionFixture',
@ -273,6 +187,7 @@ export default ({ getService }: FtrProviderContext): void => {
it('should delete the case ID from the alert schema when the user has read access to the kibana feature but no read access to the ES index', async () => {
await createCaseAttachAlertAndDeleteAlert({
supertest: supertestWithoutAuth,
totalCases: 1,
indexOfCaseToDelete: 0,
owner: 'securitySolutionFixture',
@ -315,6 +230,7 @@ export default ({ getService }: FtrProviderContext): void => {
it('removes a case from the alert schema when deleting an alert attachment', async () => {
await createCaseAttachAlertAndDeleteAlert({
supertest: supertestWithoutAuth,
totalCases: 1,
indexOfCaseToDelete: 0,
owner: 'observabilityFixture',
@ -325,6 +241,7 @@ export default ({ getService }: FtrProviderContext): void => {
it('should remove only one case', async () => {
await createCaseAttachAlertAndDeleteAlert({
supertest: supertestWithoutAuth,
totalCases: 3,
indexOfCaseToDelete: 1,
owner: 'observabilityFixture',
@ -335,6 +252,7 @@ export default ({ getService }: FtrProviderContext): void => {
it('should delete case ID from the alert schema when the user has read access only', async () => {
await createCaseAttachAlertAndDeleteAlert({
supertest: supertestWithoutAuth,
totalCases: 1,
indexOfCaseToDelete: 0,
expectedHttpCode: 204,
@ -347,6 +265,7 @@ export default ({ getService }: FtrProviderContext): void => {
it('should NOT delete case ID from the alert schema when the user does NOT have access to the alert', async () => {
await createCaseAttachAlertAndDeleteAlert({
supertest: supertestWithoutAuth,
totalCases: 1,
indexOfCaseToDelete: 0,
expectedHttpCode: 403,
@ -387,35 +306,6 @@ export default ({ getService }: FtrProviderContext): void => {
});
});
it('should delete multiple comments from the appropriate owner', async () => {
const secCase = await createCase(
supertestWithoutAuth,
getPostCaseRequest({ owner: 'securitySolutionFixture' }),
200,
{ user: secOnly, space: 'space1' }
);
await createComment({
supertest: supertestWithoutAuth,
caseId: secCase.id,
params: postCommentUserReq,
auth: { user: secOnly, space: 'space1' },
});
await createComment({
supertest: supertestWithoutAuth,
caseId: secCase.id,
params: postCommentUserReq,
auth: { user: secOnly, space: 'space1' },
});
await deleteAllComments({
supertest: supertestWithoutAuth,
caseId: secCase.id,
auth: { user: secOnly, space: 'space1' },
});
});
it('should not delete a comment from a different owner', async () => {
const secCase = await createCase(
supertestWithoutAuth,
@ -438,13 +328,6 @@ export default ({ getService }: FtrProviderContext): void => {
auth: { user: obsOnly, space: 'space1' },
expectedHttpCode: 403,
});
await deleteAllComments({
supertest: supertestWithoutAuth,
caseId: secCase.id,
auth: { user: obsOnly, space: 'space1' },
expectedHttpCode: 403,
});
});
for (const user of [globalRead, secOnlyRead, obsOnlyRead, obsSecRead, noKibanaPrivileges]) {
@ -472,13 +355,6 @@ export default ({ getService }: FtrProviderContext): void => {
auth: { user, space: 'space1' },
expectedHttpCode: 403,
});
await deleteAllComments({
supertest: supertestWithoutAuth,
caseId: postedCase.id,
auth: { user, space: 'space1' },
expectedHttpCode: 403,
});
});
}
@ -504,13 +380,6 @@ export default ({ getService }: FtrProviderContext): void => {
auth: { user: secOnly, space: 'space2' },
expectedHttpCode: 403,
});
await deleteAllComments({
supertest: supertestWithoutAuth,
caseId: postedCase.id,
auth: { user: secOnly, space: 'space2' },
expectedHttpCode: 403,
});
});
it('should NOT delete a comment created in space2 by making a request to space1', async () => {
@ -535,13 +404,6 @@ export default ({ getService }: FtrProviderContext): void => {
auth: { user: secOnly, space: 'space1' },
expectedHttpCode: 404,
});
await deleteAllComments({
supertest: supertestWithoutAuth,
caseId: postedCase.id,
auth: { user: secOnly, space: 'space1' },
expectedHttpCode: 404,
});
});
});
});

View file

@ -0,0 +1,495 @@
/*
* 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 {
Alerts,
createCaseAttachAlertAndDeleteAlert,
createSecuritySolutionAlerts,
getAlertById,
getSecuritySolutionAlerts,
} from '../../../../common/lib/alerts';
import {
createSignalsIndex,
deleteSignalsIndex,
deleteAllRules,
} from '../../../../../detection_engine_api_integration/utils';
import { FtrProviderContext } from '../../../../common/ftr_provider_context';
import {
getPostCaseRequest,
persistableStateAttachment,
postCaseReq,
postCommentActionsReleaseReq,
postCommentActionsReq,
postCommentAlertReq,
postCommentUserReq,
postExternalReferenceESReq,
postExternalReferenceSOReq,
} from '../../../../common/lib/mock';
import {
deleteAllCaseItems,
deleteCasesByESQuery,
deleteCasesUserActions,
deleteComments,
createCase,
createComment,
deleteAllComments,
superUserSpace1Auth,
bulkCreateAttachments,
getAllComments,
} from '../../../../common/lib/api';
import {
globalRead,
noKibanaPrivileges,
obsOnly,
obsOnlyRead,
obsOnlyReadAlerts,
obsSec,
obsSecRead,
secOnly,
secOnlyRead,
secOnlyReadAlerts,
secSolutionOnlyReadNoIndexAlerts,
superUser,
} from '../../../../common/lib/authentication/users';
// eslint-disable-next-line import/no-default-export
export default ({ getService }: FtrProviderContext): void => {
const supertest = getService('supertest');
const es = getService('es');
const esArchiver = getService('esArchiver');
const log = getService('log');
const supertestWithoutAuth = getService('supertestWithoutAuth');
describe('delete_comments', () => {
afterEach(async () => {
await deleteCasesByESQuery(es);
await deleteComments(es);
await deleteCasesUserActions(es);
});
describe('happy path', () => {
it('should delete all comments', async () => {
const postedCase = await createCase(supertest, postCaseReq);
await createComment({
supertest,
caseId: postedCase.id,
params: postCommentUserReq,
});
const comment = await deleteAllComments({
supertest,
caseId: postedCase.id,
});
expect(comment).to.eql({});
});
});
describe('unhappy path', () => {
it('404s when comment belongs to different case', async () => {
const postedCase = await createCase(supertest, postCaseReq);
await createComment({
supertest,
caseId: postedCase.id,
params: postCommentUserReq,
});
const error = (await deleteAllComments({
supertest,
caseId: 'fake-id',
expectedHttpCode: 404,
})) as Error;
expect(error.message).to.be('No comments found for fake-id.');
});
});
describe('alerts', () => {
describe('security_solution', () => {
let alerts: Alerts = [];
const getAlerts = async (_alerts: Alerts) => {
await es.indices.refresh({ index: _alerts.map((alert) => alert._index) });
const updatedAlerts = await getSecuritySolutionAlerts(
supertest,
alerts.map((alert) => alert._id)
);
return updatedAlerts.hits.hits.map((alert) => ({ ...alert._source }));
};
beforeEach(async () => {
await esArchiver.load('x-pack/test/functional/es_archives/auditbeat/hosts');
await createSignalsIndex(supertest, log);
const signals = await createSecuritySolutionAlerts(supertest, log);
alerts = [signals.hits.hits[0], signals.hits.hits[1]];
});
afterEach(async () => {
await deleteSignalsIndex(supertest, log);
await deleteAllRules(supertest, log);
await esArchiver.unload('x-pack/test/functional/es_archives/auditbeat/hosts');
});
it('deletes alerts and comments', async () => {
const postedCase = await createCase(supertest, postCaseReq);
await createComment({
supertest,
caseId: postedCase.id,
params: postCommentUserReq,
});
await bulkCreateAttachments({
supertest,
caseId: postedCase.id,
params: [
{
...postCommentAlertReq,
alertId: alerts[0]._id,
index: alerts[0]._index,
},
{
...postCommentAlertReq,
alertId: alerts[1]._id,
index: alerts[1]._index,
},
postCommentUserReq,
postCommentActionsReq,
postCommentActionsReleaseReq,
postExternalReferenceESReq,
postExternalReferenceSOReq,
persistableStateAttachment,
],
});
await deleteAllComments({
supertest,
caseId: postedCase.id,
});
const comments = await getAllComments({ supertest, caseId: postedCase.id });
expect(comments.length).to.eql(0);
});
it('removes a case from the alert schema when deleting all alert attachments', async () => {
await createCaseAttachAlertAndDeleteAlert({
supertest: supertestWithoutAuth,
totalCases: 1,
indexOfCaseToDelete: 0,
owner: 'securitySolutionFixture',
alerts,
getAlerts,
});
});
it('should remove only one case', async () => {
await createCaseAttachAlertAndDeleteAlert({
supertest: supertestWithoutAuth,
totalCases: 3,
indexOfCaseToDelete: 1,
owner: 'securitySolutionFixture',
alerts,
getAlerts,
});
});
it('should delete case ID from the alert schema when the user has write access to the indices and only read access to the siem solution', async () => {
await createCaseAttachAlertAndDeleteAlert({
supertest: supertestWithoutAuth,
totalCases: 1,
indexOfCaseToDelete: 0,
owner: 'securitySolutionFixture',
alerts,
getAlerts,
expectedHttpCode: 204,
deleteCommentAuth: { user: secOnlyReadAlerts, space: 'space1' },
});
});
it('should NOT delete case ID from the alert schema when the user does NOT have access to the alert', async () => {
await createCaseAttachAlertAndDeleteAlert({
supertest: supertestWithoutAuth,
totalCases: 1,
indexOfCaseToDelete: 0,
owner: 'securitySolutionFixture',
alerts,
getAlerts,
expectedHttpCode: 403,
deleteCommentAuth: { user: obsSec, space: 'space1' },
});
});
it('should delete the case ID from the alert schema when the user has read access to the kibana feature but no read access to the ES index', async () => {
await createCaseAttachAlertAndDeleteAlert({
supertest: supertestWithoutAuth,
totalCases: 1,
indexOfCaseToDelete: 0,
owner: 'securitySolutionFixture',
alerts,
getAlerts,
expectedHttpCode: 204,
deleteCommentAuth: { user: secSolutionOnlyReadNoIndexAlerts, space: 'space1' },
});
});
});
describe('observability', () => {
const alerts = [
{ _id: 'NoxgpHkBqbdrfX07MqXV', _index: '.alerts-observability.apm.alerts' },
{ _id: 'space1alert', _index: '.alerts-observability.apm.alerts' },
];
const getAlerts = async (_alerts: Alerts) => {
const updatedAlerts = await Promise.all(
_alerts.map((alert) =>
getAlertById({
supertest: supertestWithoutAuth,
id: alert._id,
index: alert._index,
auth: { user: superUser, space: 'space1' },
})
)
);
return updatedAlerts as Array<Record<string, unknown>>;
};
beforeEach(async () => {
await esArchiver.load('x-pack/test/functional/es_archives/rule_registry/alerts');
});
afterEach(async () => {
await esArchiver.unload('x-pack/test/functional/es_archives/rule_registry/alerts');
});
it('deletes alerts and comments', async () => {
const postedCase = await createCase(supertest, postCaseReq);
await createComment({
supertest,
caseId: postedCase.id,
params: postCommentUserReq,
});
await bulkCreateAttachments({
supertest,
caseId: postedCase.id,
params: [
{
...postCommentAlertReq,
alertId: alerts[0]._id,
index: alerts[0]._index,
},
{
...postCommentAlertReq,
alertId: alerts[1]._id,
index: alerts[1]._index,
},
postCommentUserReq,
postCommentActionsReq,
postCommentActionsReleaseReq,
postExternalReferenceESReq,
postExternalReferenceSOReq,
persistableStateAttachment,
],
});
await deleteAllComments({
supertest,
caseId: postedCase.id,
});
const comments = await getAllComments({ supertest, caseId: postedCase.id });
expect(comments.length).to.eql(0);
});
it('removes a case from the alert schema when deleting all alert attachments', async () => {
await createCaseAttachAlertAndDeleteAlert({
supertest: supertestWithoutAuth,
totalCases: 1,
indexOfCaseToDelete: 0,
owner: 'observabilityFixture',
alerts,
getAlerts,
});
});
it('should remove only one case', async () => {
await createCaseAttachAlertAndDeleteAlert({
supertest: supertestWithoutAuth,
totalCases: 3,
indexOfCaseToDelete: 1,
owner: 'observabilityFixture',
alerts,
getAlerts,
});
});
it('should delete case ID from the alert schema when the user has read access only', async () => {
await createCaseAttachAlertAndDeleteAlert({
supertest: supertestWithoutAuth,
totalCases: 1,
indexOfCaseToDelete: 0,
expectedHttpCode: 204,
owner: 'observabilityFixture',
alerts,
getAlerts,
deleteCommentAuth: { user: obsOnlyReadAlerts, space: 'space1' },
});
});
it('should NOT delete case ID from the alert schema when the user does NOT have access to the alert', async () => {
await createCaseAttachAlertAndDeleteAlert({
supertest: supertestWithoutAuth,
totalCases: 1,
indexOfCaseToDelete: 0,
expectedHttpCode: 403,
owner: 'observabilityFixture',
alerts,
getAlerts,
deleteCommentAuth: { user: obsSec, space: 'space1' },
});
});
});
});
describe('rbac', () => {
afterEach(async () => {
await deleteAllCaseItems(es);
});
it('should delete multiple comments from the appropriate owner', async () => {
const secCase = await createCase(
supertestWithoutAuth,
getPostCaseRequest({ owner: 'securitySolutionFixture' }),
200,
{ user: secOnly, space: 'space1' }
);
await createComment({
supertest: supertestWithoutAuth,
caseId: secCase.id,
params: postCommentUserReq,
auth: { user: secOnly, space: 'space1' },
});
await createComment({
supertest: supertestWithoutAuth,
caseId: secCase.id,
params: postCommentUserReq,
auth: { user: secOnly, space: 'space1' },
});
await deleteAllComments({
supertest: supertestWithoutAuth,
caseId: secCase.id,
auth: { user: secOnly, space: 'space1' },
});
});
it('should not delete a comment from a different owner', async () => {
const secCase = await createCase(
supertestWithoutAuth,
getPostCaseRequest({ owner: 'securitySolutionFixture' }),
200,
{ user: secOnly, space: 'space1' }
);
await createComment({
supertest: supertestWithoutAuth,
caseId: secCase.id,
params: postCommentUserReq,
auth: { user: secOnly, space: 'space1' },
});
await deleteAllComments({
supertest: supertestWithoutAuth,
caseId: secCase.id,
auth: { user: obsOnly, space: 'space1' },
expectedHttpCode: 403,
});
});
for (const user of [globalRead, secOnlyRead, obsOnlyRead, obsSecRead, noKibanaPrivileges]) {
it(`User ${
user.username
} with role(s) ${user.roles.join()} - should NOT delete all comments`, async () => {
const postedCase = await createCase(
supertestWithoutAuth,
getPostCaseRequest({ owner: 'securitySolutionFixture' }),
200,
superUserSpace1Auth
);
await createComment({
supertest: supertestWithoutAuth,
caseId: postedCase.id,
params: postCommentUserReq,
auth: superUserSpace1Auth,
});
await deleteAllComments({
supertest: supertestWithoutAuth,
caseId: postedCase.id,
auth: { user, space: 'space1' },
expectedHttpCode: 403,
});
});
}
it('should NOT delete a comment in a space with where the user does not have permissions', async () => {
const postedCase = await createCase(
supertestWithoutAuth,
getPostCaseRequest({ owner: 'securitySolutionFixture' }),
200,
{ user: superUser, space: 'space2' }
);
await createComment({
supertest: supertestWithoutAuth,
caseId: postedCase.id,
params: postCommentUserReq,
auth: { user: superUser, space: 'space2' },
});
await deleteAllComments({
supertest: supertestWithoutAuth,
caseId: postedCase.id,
auth: { user: secOnly, space: 'space2' },
expectedHttpCode: 403,
});
});
it('should NOT delete a comment created in space2 by making a request to space1', async () => {
const postedCase = await createCase(
supertestWithoutAuth,
getPostCaseRequest({ owner: 'securitySolutionFixture' }),
200,
{ user: superUser, space: 'space2' }
);
await createComment({
supertest: supertestWithoutAuth,
caseId: postedCase.id,
params: postCommentUserReq,
auth: { user: superUser, space: 'space2' },
});
await deleteAllComments({
supertest: supertestWithoutAuth,
caseId: postedCase.id,
auth: { user: secOnly, space: 'space1' },
expectedHttpCode: 404,
});
});
});
});
};

View file

@ -12,6 +12,7 @@ export default ({ loadTestFile }: FtrProviderContext): void => {
describe('Common', function () {
loadTestFile(require.resolve('./client/update_alert_status'));
loadTestFile(require.resolve('./comments/delete_comment'));
loadTestFile(require.resolve('./comments/delete_comments'));
loadTestFile(require.resolve('./comments/find_comments'));
loadTestFile(require.resolve('./comments/get_comment'));
loadTestFile(require.resolve('./comments/get_all_comments'));