From a699e63f8c7f12ec3514b9d72a7d6f7a0fc48ef0 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Tue, 4 May 2021 17:22:14 -0400 Subject: [PATCH 01/12] Starting spaces tests --- .../case_api_integration/common/lib/utils.ts | 10 + .../spaces_only/config.ts | 4 +- .../tests/common/cases/delete_cases.ts | 302 ++++ .../tests/common/cases/find_cases.ts | 815 +++++++++++ .../tests/common/cases/get_case.ts | 207 +++ .../tests/common/cases/migrations.ts | 111 ++ .../tests/common/cases/patch_cases.ts | 1240 +++++++++++++++++ .../tests/common/cases/post_case.ts | 292 ++++ .../common/cases/reporters/get_reporters.ts | 41 + .../tests/common/cases/status/get_status.ts | 75 + .../tests/common/cases/tags/get_tags.ts | 43 + .../tests/common/comments/delete_comment.ts | 372 +++++ .../tests/common/comments/find_comments.ts | 393 ++++++ .../tests/common/comments/get_all_comments.ts | 231 +++ .../tests/common/comments/get_comment.ts | 169 +++ .../tests/common/comments/migrations.ts | 37 + .../tests/common/comments/patch_comment.ts | 641 +++++++++ .../tests/common/comments/post_comment.ts | 605 ++++++++ .../tests/common/configure/get_configure.ts | 215 +++ .../tests/common/configure/get_connectors.ts | 27 + .../tests/common/configure/migrations.ts | 44 + .../tests/common/configure/patch_configure.ts | 243 ++++ .../tests/common/configure/post_configure.ts | 298 ++++ .../tests/common/connectors/case.ts | 1078 ++++++++++++++ .../spaces_only/tests/common/index.ts | 41 + .../spaces_only/tests/common/migrations.ts | 18 + .../common/sub_cases/delete_sub_cases.ts | 112 ++ .../tests/common/sub_cases/find_sub_cases.ts | 480 +++++++ .../tests/common/sub_cases/get_sub_case.ts | 119 ++ .../tests/common/sub_cases/patch_sub_cases.ts | 515 +++++++ .../user_actions/get_all_user_actions.ts | 395 ++++++ .../tests/common/user_actions/migrations.ts | 53 + .../tests/trial/cases/push_case.ts | 360 +++++ .../user_actions/get_all_user_actions.ts | 115 ++ .../tests/trial/configure/get_configure.ts | 98 ++ .../tests/trial/configure/get_connectors.ts | 129 ++ .../tests/trial/configure/index.ts | 18 + .../tests/trial/configure/patch_configure.ts | 168 +++ .../tests/trial/configure/post_configure.ts | 98 ++ 39 files changed, 10210 insertions(+), 2 deletions(-) create mode 100644 x-pack/test/case_api_integration/spaces_only/tests/common/cases/delete_cases.ts create mode 100644 x-pack/test/case_api_integration/spaces_only/tests/common/cases/find_cases.ts create mode 100644 x-pack/test/case_api_integration/spaces_only/tests/common/cases/get_case.ts create mode 100644 x-pack/test/case_api_integration/spaces_only/tests/common/cases/migrations.ts create mode 100644 x-pack/test/case_api_integration/spaces_only/tests/common/cases/patch_cases.ts create mode 100644 x-pack/test/case_api_integration/spaces_only/tests/common/cases/post_case.ts create mode 100644 x-pack/test/case_api_integration/spaces_only/tests/common/cases/reporters/get_reporters.ts create mode 100644 x-pack/test/case_api_integration/spaces_only/tests/common/cases/status/get_status.ts create mode 100644 x-pack/test/case_api_integration/spaces_only/tests/common/cases/tags/get_tags.ts create mode 100644 x-pack/test/case_api_integration/spaces_only/tests/common/comments/delete_comment.ts create mode 100644 x-pack/test/case_api_integration/spaces_only/tests/common/comments/find_comments.ts create mode 100644 x-pack/test/case_api_integration/spaces_only/tests/common/comments/get_all_comments.ts create mode 100644 x-pack/test/case_api_integration/spaces_only/tests/common/comments/get_comment.ts create mode 100644 x-pack/test/case_api_integration/spaces_only/tests/common/comments/migrations.ts create mode 100644 x-pack/test/case_api_integration/spaces_only/tests/common/comments/patch_comment.ts create mode 100644 x-pack/test/case_api_integration/spaces_only/tests/common/comments/post_comment.ts create mode 100644 x-pack/test/case_api_integration/spaces_only/tests/common/configure/get_configure.ts create mode 100644 x-pack/test/case_api_integration/spaces_only/tests/common/configure/get_connectors.ts create mode 100644 x-pack/test/case_api_integration/spaces_only/tests/common/configure/migrations.ts create mode 100644 x-pack/test/case_api_integration/spaces_only/tests/common/configure/patch_configure.ts create mode 100644 x-pack/test/case_api_integration/spaces_only/tests/common/configure/post_configure.ts create mode 100644 x-pack/test/case_api_integration/spaces_only/tests/common/connectors/case.ts create mode 100644 x-pack/test/case_api_integration/spaces_only/tests/common/index.ts create mode 100644 x-pack/test/case_api_integration/spaces_only/tests/common/migrations.ts create mode 100644 x-pack/test/case_api_integration/spaces_only/tests/common/sub_cases/delete_sub_cases.ts create mode 100644 x-pack/test/case_api_integration/spaces_only/tests/common/sub_cases/find_sub_cases.ts create mode 100644 x-pack/test/case_api_integration/spaces_only/tests/common/sub_cases/get_sub_case.ts create mode 100644 x-pack/test/case_api_integration/spaces_only/tests/common/sub_cases/patch_sub_cases.ts create mode 100644 x-pack/test/case_api_integration/spaces_only/tests/common/user_actions/get_all_user_actions.ts create mode 100644 x-pack/test/case_api_integration/spaces_only/tests/common/user_actions/migrations.ts create mode 100644 x-pack/test/case_api_integration/spaces_only/tests/trial/cases/push_case.ts create mode 100644 x-pack/test/case_api_integration/spaces_only/tests/trial/cases/user_actions/get_all_user_actions.ts create mode 100644 x-pack/test/case_api_integration/spaces_only/tests/trial/configure/get_configure.ts create mode 100644 x-pack/test/case_api_integration/spaces_only/tests/trial/configure/get_connectors.ts create mode 100644 x-pack/test/case_api_integration/spaces_only/tests/trial/configure/index.ts create mode 100644 x-pack/test/case_api_integration/spaces_only/tests/trial/configure/patch_configure.ts create mode 100644 x-pack/test/case_api_integration/spaces_only/tests/trial/configure/post_configure.ts diff --git a/x-pack/test/case_api_integration/common/lib/utils.ts b/x-pack/test/case_api_integration/common/lib/utils.ts index 731ddca08a34e3..5800de50bb9c41 100644 --- a/x-pack/test/case_api_integration/common/lib/utils.ts +++ b/x-pack/test/case_api_integration/common/lib/utils.ts @@ -539,6 +539,16 @@ export const deleteMappings = async (es: KibanaClient): Promise => { }); }; +/** + * Returns an auth object with the specified space and user set as super user. The result can be passed to other utility + * functions. + */ +export function getAuthWithSuperUser( + space: string = 'space1' +): { user: User; space: string | null } { + return { user: superUser, space }; +} + export const getSpaceUrlPrefix = (spaceId: string | undefined | null) => { return spaceId && spaceId !== 'default' ? `/s/${spaceId}` : ``; }; diff --git a/x-pack/test/case_api_integration/spaces_only/config.ts b/x-pack/test/case_api_integration/spaces_only/config.ts index 310830a220fb84..c848a08b1f3df9 100644 --- a/x-pack/test/case_api_integration/spaces_only/config.ts +++ b/x-pack/test/case_api_integration/spaces_only/config.ts @@ -10,6 +10,6 @@ import { createTestConfig } from '../common/config'; // eslint-disable-next-line import/no-default-export export default createTestConfig('spaces_only', { disabledPlugins: ['security'], - license: 'basic', - ssl: true, + license: 'trial', + ssl: false, }); diff --git a/x-pack/test/case_api_integration/spaces_only/tests/common/cases/delete_cases.ts b/x-pack/test/case_api_integration/spaces_only/tests/common/cases/delete_cases.ts new file mode 100644 index 00000000000000..17aac2dd7e2859 --- /dev/null +++ b/x-pack/test/case_api_integration/spaces_only/tests/common/cases/delete_cases.ts @@ -0,0 +1,302 @@ +/* + * 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 { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; +import { defaultUser, getPostCaseRequest, postCommentUserReq } from '../../../../common/lib/mock'; +import { + createCaseAction, + createSubCase, + deleteAllCaseItems, + deleteCaseAction, + deleteCasesByESQuery, + deleteCasesUserActions, + deleteComments, + createCase, + deleteCases, + createComment, + getComment, + getAllUserAction, + removeServerGeneratedPropertiesFromUserAction, + getCase, +} from '../../../../common/lib/utils'; +import { getSubCaseDetailsUrl } from '../../../../../../plugins/cases/common/api/helpers'; +import { CaseResponse } from '../../../../../../plugins/cases/common/api'; +import { + secOnly, + secOnlyRead, + globalRead, + obsOnlyRead, + obsSecRead, + noKibanaPrivileges, + obsOnly, + superUser, +} from '../../../../common/lib/authentication/users'; + +import { superUserSpace1Auth } from '../../../../common/lib/authentication'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const supertest = getService('supertest'); + const es = getService('es'); + + describe('delete_cases', () => { + afterEach(async () => { + await deleteCasesByESQuery(es); + await deleteComments(es); + await deleteCasesUserActions(es); + }); + + it('should delete a case', async () => { + const postedCase = await createCase(supertest, getPostCaseRequest()); + const body = await deleteCases({ supertest, caseIDs: [postedCase.id] }); + + expect(body).to.eql({}); + }); + + it(`should delete a case's comments when that case gets deleted`, async () => { + const postedCase = await createCase(supertest, getPostCaseRequest()); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + }); + // ensure that we can get the comment before deleting the case + await getComment({ + supertest, + caseId: postedCase.id, + commentId: patchedCase.comments![0].id, + }); + + await deleteCases({ supertest, caseIDs: [postedCase.id] }); + + // make sure the comment is now gone + await getComment({ + supertest, + caseId: postedCase.id, + commentId: patchedCase.comments![0].id, + expectedHttpCode: 404, + }); + }); + + it('should create a user action when creating a case', async () => { + const postedCase = await createCase(supertest, getPostCaseRequest()); + await deleteCases({ supertest, caseIDs: [postedCase.id] }); + const userActions = await getAllUserAction(supertest, postedCase.id); + const creationUserAction = removeServerGeneratedPropertiesFromUserAction(userActions[1]); + + expect(creationUserAction).to.eql({ + action_field: [ + 'description', + 'status', + 'tags', + 'title', + 'connector', + 'settings', + 'owner', + 'comment', + ], + action: 'delete', + action_by: defaultUser, + old_value: null, + new_value: null, + case_id: `${postedCase.id}`, + comment_id: null, + sub_case_id: '', + owner: 'securitySolutionFixture', + }); + }); + + it('unhappy path - 404s when case is not there', async () => { + await deleteCases({ supertest, caseIDs: ['fake-id'], expectedHttpCode: 404 }); + }); + + // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests + describe.skip('sub cases', () => { + let actionID: string; + before(async () => { + actionID = await createCaseAction(supertest); + }); + after(async () => { + await deleteCaseAction(supertest, actionID); + }); + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + it('should delete the sub cases when deleting a collection', async () => { + const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); + expect(caseInfo.subCases![0].id).to.not.eql(undefined); + + const body = await deleteCases({ supertest, caseIDs: [caseInfo.id] }); + + expect(body).to.eql({}); + await supertest + .get(getSubCaseDetailsUrl(caseInfo.id, caseInfo.subCases![0].id)) + .send() + .expect(404); + }); + + it(`should delete a sub case's comments when that case gets deleted`, async () => { + const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); + expect(caseInfo.subCases![0].id).to.not.eql(undefined); + + // there should be two comments on the sub case now + const { body: patchedCaseWithSubCase }: { body: CaseResponse } = await supertest + .post(`${CASES_URL}/${caseInfo.id}/comments`) + .set('kbn-xsrf', 'true') + .query({ subCaseId: caseInfo.subCases![0].id }) + .send(postCommentUserReq) + .expect(200); + + const subCaseCommentUrl = `${CASES_URL}/${patchedCaseWithSubCase.id}/comments/${ + patchedCaseWithSubCase.comments![1].id + }`; + // make sure we can get the second comment + await supertest.get(subCaseCommentUrl).set('kbn-xsrf', 'true').send().expect(200); + + await deleteCases({ supertest, caseIDs: [caseInfo.id] }); + + await supertest.get(subCaseCommentUrl).set('kbn-xsrf', 'true').send().expect(404); + }); + }); + + describe('rbac', () => { + it('User: security solution only - should delete a case', async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { + user: secOnly, + space: 'space1', + } + ); + + await deleteCases({ + supertest, + caseIDs: [postedCase.id], + expectedHttpCode: 204, + auth: { user: secOnly, space: 'space1' }, + }); + }); + + it('User: security solution only - should NOT delete a case of different owner', async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { + user: secOnly, + space: 'space1', + } + ); + + await deleteCases({ + supertest: supertestWithoutAuth, + caseIDs: [postedCase.id], + expectedHttpCode: 403, + auth: { user: obsOnly, space: 'space1' }, + }); + }); + + it('should get an error if the user has not permissions to all requested cases', async () => { + const caseSec = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { + user: secOnly, + space: 'space1', + } + ); + + const caseObs = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + { + user: obsOnly, + space: 'space1', + } + ); + + await deleteCases({ + supertest: supertestWithoutAuth, + caseIDs: [caseSec.id, caseObs.id], + expectedHttpCode: 403, + auth: { user: obsOnly, space: 'space1' }, + }); + + // Cases should have not been deleted. + await getCase({ + supertest: supertestWithoutAuth, + caseId: caseSec.id, + expectedHttpCode: 200, + auth: superUserSpace1Auth, + }); + + await getCase({ + supertest: supertestWithoutAuth, + caseId: caseObs.id, + expectedHttpCode: 200, + auth: superUserSpace1Auth, + }); + }); + + for (const user of [globalRead, secOnlyRead, obsOnlyRead, obsSecRead, noKibanaPrivileges]) { + it(`User ${ + user.username + } with role(s) ${user.roles.join()} - should NOT delete a case`, async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { + user: superUser, + space: 'space1', + } + ); + + await deleteCases({ + supertest: supertestWithoutAuth, + caseIDs: [postedCase.id], + expectedHttpCode: 403, + auth: { user, space: 'space1' }, + }); + }); + } + + it('should NOT delete a case in a space with no permissions', async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { + user: superUser, + space: 'space2', + } + ); + + /** + * We expect a 404 because the bulkGet inside the delete + * route should return a 404 when requesting a case from + * a different space. + * */ + await deleteCases({ + supertest: supertestWithoutAuth, + caseIDs: [postedCase.id], + expectedHttpCode: 404, + auth: { user: secOnly, space: 'space1' }, + }); + }); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/spaces_only/tests/common/cases/find_cases.ts b/x-pack/test/case_api_integration/spaces_only/tests/common/cases/find_cases.ts new file mode 100644 index 00000000000000..b7838dd9299bcd --- /dev/null +++ b/x-pack/test/case_api_integration/spaces_only/tests/common/cases/find_cases.ts @@ -0,0 +1,815 @@ +/* + * 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 type { ApiResponse, estypes } from '@elastic/elasticsearch'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +import { + CASES_URL, + SUB_CASES_PATCH_DEL_URL, +} from '../../../../../../plugins/cases/common/constants'; +import { + postCaseReq, + postCommentUserReq, + findCasesResp, + getPostCaseRequest, +} from '../../../../common/lib/mock'; +import { + deleteAllCaseItems, + createSubCase, + setStatus, + CreateSubCaseResp, + createCaseAction, + deleteCaseAction, + ensureSavedObjectIsAuthorized, + findCases, + createCase, + updateCase, + createComment, +} from '../../../../common/lib/utils'; +import { CaseResponse, CaseStatuses, CaseType } from '../../../../../../plugins/cases/common/api'; +import { + obsOnly, + secOnly, + obsOnlyRead, + secOnlyRead, + noKibanaPrivileges, + superUser, + globalRead, + obsSecRead, + obsSec, +} from '../../../../common/lib/authentication/users'; + +interface CaseAttributes { + cases: { + title: string; + }; +} + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + describe('find_cases', () => { + describe('basic tests', () => { + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + it('should return empty response', async () => { + const cases = await findCases({ supertest }); + expect(cases).to.eql(findCasesResp); + }); + + it('should return cases', async () => { + const a = await createCase(supertest, postCaseReq); + const b = await createCase(supertest, postCaseReq); + const c = await createCase(supertest, postCaseReq); + + const cases = await findCases({ supertest }); + + expect(cases).to.eql({ + ...findCasesResp, + total: 3, + cases: [a, b, c], + count_open_cases: 3, + }); + }); + + it('filters by tags', async () => { + await createCase(supertest, postCaseReq); + const postedCase = await createCase(supertest, { ...postCaseReq, tags: ['unique'] }); + const cases = await findCases({ supertest, query: { tags: ['unique'] } }); + + expect(cases).to.eql({ + ...findCasesResp, + total: 1, + cases: [postedCase], + count_open_cases: 1, + }); + }); + + it('filters by status', async () => { + await createCase(supertest, postCaseReq); + const toCloseCase = await createCase(supertest, postCaseReq); + const patchedCase = await updateCase({ + supertest, + params: { + cases: [ + { + id: toCloseCase.id, + version: toCloseCase.version, + status: CaseStatuses.closed, + }, + ], + }, + }); + + const cases = await findCases({ supertest, query: { status: CaseStatuses.closed } }); + + expect(cases).to.eql({ + ...findCasesResp, + total: 1, + cases: [patchedCase[0]], + count_open_cases: 1, + count_closed_cases: 1, + count_in_progress_cases: 0, + }); + }); + + it('filters by reporters', async () => { + const postedCase = await createCase(supertest, postCaseReq); + const cases = await findCases({ supertest, query: { reporters: 'elastic' } }); + + expect(cases).to.eql({ + ...findCasesResp, + total: 1, + cases: [postedCase], + count_open_cases: 1, + }); + }); + + it('correctly counts comments', async () => { + const postedCase = await createCase(supertest, postCaseReq); + + // post 2 comments + await createComment({ supertest, caseId: postedCase.id, params: postCommentUserReq }); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + }); + + const cases = await findCases({ supertest }); + expect(cases).to.eql({ + ...findCasesResp, + total: 1, + cases: [ + { + ...patchedCase, + comments: [], + totalComment: 2, + }, + ], + count_open_cases: 1, + }); + }); + + it('correctly counts open/closed/in-progress', async () => { + await createCase(supertest, postCaseReq); + const inProgressCase = await createCase(supertest, postCaseReq); + const postedCase = await createCase(supertest, postCaseReq); + + await updateCase({ + supertest, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + status: CaseStatuses.closed, + }, + ], + }, + }); + + await updateCase({ + supertest, + params: { + cases: [ + { + id: inProgressCase.id, + version: inProgressCase.version, + status: CaseStatuses['in-progress'], + }, + ], + }, + }); + + const cases = await findCases({ supertest }); + expect(cases.count_open_cases).to.eql(1); + expect(cases.count_closed_cases).to.eql(1); + expect(cases.count_in_progress_cases).to.eql(1); + }); + + it('unhappy path - 400s when bad query supplied', async () => { + await findCases({ supertest, query: { perPage: true }, expectedHttpCode: 400 }); + }); + + // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests + describe.skip('stats with sub cases', () => { + let collection: CreateSubCaseResp; + let actionID: string; + before(async () => { + actionID = await createCaseAction(supertest); + }); + after(async () => { + await deleteCaseAction(supertest, actionID); + }); + beforeEach(async () => { + // create a collection with a sub case that is marked as open + collection = await createSubCase({ supertest, actionID }); + + const [, , { body: toCloseCase }] = await Promise.all([ + // set the sub case to in-progress + setStatus({ + supertest, + cases: [ + { + id: collection.newSubCaseInfo.subCases![0].id, + version: collection.newSubCaseInfo.subCases![0].version, + status: CaseStatuses['in-progress'], + }, + ], + type: 'sub_case', + }), + // create two cases that are both open + supertest.post(CASES_URL).set('kbn-xsrf', 'true').send(postCaseReq), + supertest.post(CASES_URL).set('kbn-xsrf', 'true').send(postCaseReq), + ]); + + // set the third case to closed + await setStatus({ + supertest, + cases: [ + { + id: toCloseCase.id, + version: toCloseCase.version, + status: CaseStatuses.closed, + }, + ], + type: 'case', + }); + }); + it('correctly counts stats without using a filter', async () => { + const cases = await findCases({ supertest }); + + expect(cases.total).to.eql(3); + expect(cases.count_closed_cases).to.eql(1); + expect(cases.count_open_cases).to.eql(1); + expect(cases.count_in_progress_cases).to.eql(1); + }); + + it('correctly counts stats with a filter for open cases', async () => { + const cases = await findCases({ supertest, query: { status: CaseStatuses.open } }); + + expect(cases.cases.length).to.eql(1); + + // since we're filtering on status and the collection only has an in-progress case, it should only return the + // individual case that has the open status and no collections + // ENABLE_CASE_CONNECTOR: this value is not correct because it includes a collection + // that does not have an open case. This is a known issue and will need to be resolved + // when this issue is addressed: https://github.com/elastic/kibana/issues/94115 + expect(cases.total).to.eql(2); + expect(cases.count_closed_cases).to.eql(1); + expect(cases.count_open_cases).to.eql(1); + expect(cases.count_in_progress_cases).to.eql(1); + }); + + it('correctly counts stats with a filter for individual cases', async () => { + const cases = await findCases({ supertest, query: { type: CaseType.individual } }); + + expect(cases.total).to.eql(2); + expect(cases.count_closed_cases).to.eql(1); + expect(cases.count_open_cases).to.eql(1); + expect(cases.count_in_progress_cases).to.eql(0); + }); + + it('correctly counts stats with a filter for collection cases with multiple sub cases', async () => { + // this will force the first sub case attached to the collection to be closed + // so we'll have one closed sub case and one open sub case + await createSubCase({ supertest, caseID: collection.newSubCaseInfo.id, actionID }); + const cases = await findCases({ supertest, query: { type: CaseType.collection } }); + + expect(cases.total).to.eql(1); + expect(cases.cases[0].subCases?.length).to.eql(2); + expect(cases.count_closed_cases).to.eql(1); + expect(cases.count_open_cases).to.eql(1); + expect(cases.count_in_progress_cases).to.eql(0); + }); + + it('correctly counts stats with a filter for collection and open cases with multiple sub cases', async () => { + // this will force the first sub case attached to the collection to be closed + // so we'll have one closed sub case and one open sub case + await createSubCase({ supertest, caseID: collection.newSubCaseInfo.id, actionID }); + const cases = await findCases({ + supertest, + query: { + type: CaseType.collection, + status: CaseStatuses.open, + }, + }); + + expect(cases.total).to.eql(1); + expect(cases.cases[0].subCases?.length).to.eql(1); + expect(cases.count_closed_cases).to.eql(1); + expect(cases.count_open_cases).to.eql(1); + expect(cases.count_in_progress_cases).to.eql(0); + }); + + it('correctly counts stats including a collection without sub cases when not filtering on status', async () => { + // delete the sub case on the collection so that it doesn't have any sub cases + await supertest + .delete( + `${SUB_CASES_PATCH_DEL_URL}?ids=["${collection.newSubCaseInfo.subCases![0].id}"]` + ) + .set('kbn-xsrf', 'true') + .send() + .expect(204); + + const cases = await findCases({ supertest, query: { type: CaseType.collection } }); + + // it should include the collection without sub cases because we did not pass in a filter on status + expect(cases.total).to.eql(3); + expect(cases.count_closed_cases).to.eql(1); + expect(cases.count_open_cases).to.eql(1); + expect(cases.count_in_progress_cases).to.eql(0); + }); + + it('correctly counts stats including a collection without sub cases when filtering on tags', async () => { + // delete the sub case on the collection so that it doesn't have any sub cases + await supertest + .delete( + `${SUB_CASES_PATCH_DEL_URL}?ids=["${collection.newSubCaseInfo.subCases![0].id}"]` + ) + .set('kbn-xsrf', 'true') + .send() + .expect(204); + + const cases = await findCases({ supertest, query: { tags: ['defacement'] } }); + + // it should include the collection without sub cases because we did not pass in a filter on status + expect(cases.total).to.eql(3); + expect(cases.count_closed_cases).to.eql(1); + expect(cases.count_open_cases).to.eql(1); + expect(cases.count_in_progress_cases).to.eql(0); + }); + + it('does not return collections without sub cases matching the requested status', async () => { + const cases = await findCases({ supertest, query: { status: CaseStatuses.closed } }); + + expect(cases.cases.length).to.eql(1); + // it should not include the collection that has a sub case as in-progress + // ENABLE_CASE_CONNECTOR: this value is not correct because it includes collections. This short term + // fix for when sub cases are not enabled. When the feature is completed the _find API + // will need to be fixed as explained in this ticket: https://github.com/elastic/kibana/issues/94115 + expect(cases.total).to.eql(2); + expect(cases.count_closed_cases).to.eql(1); + expect(cases.count_open_cases).to.eql(1); + expect(cases.count_in_progress_cases).to.eql(1); + }); + + it('does not return empty collections when filtering on status', async () => { + // delete the sub case on the collection so that it doesn't have any sub cases + await supertest + .delete( + `${SUB_CASES_PATCH_DEL_URL}?ids=["${collection.newSubCaseInfo.subCases![0].id}"]` + ) + .set('kbn-xsrf', 'true') + .send() + .expect(204); + + const cases = await findCases({ supertest, query: { status: CaseStatuses.closed } }); + + expect(cases.cases.length).to.eql(1); + + // ENABLE_CASE_CONNECTOR: this value is not correct because it includes collections. This short term + // fix for when sub cases are not enabled. When the feature is completed the _find API + // will need to be fixed as explained in this ticket: https://github.com/elastic/kibana/issues/94115 + expect(cases.total).to.eql(2); + expect(cases.count_closed_cases).to.eql(1); + expect(cases.count_open_cases).to.eql(1); + expect(cases.count_in_progress_cases).to.eql(0); + }); + }); + }); + + describe('find_cases pagination', () => { + const numCases = 10; + before(async () => { + await createCasesWithTitleAsNumber(numCases); + }); + + after(async () => { + await deleteAllCaseItems(es); + }); + + const createCasesWithTitleAsNumber = async (total: number): Promise => { + const responsePromises = []; + for (let i = 0; i < total; i++) { + // this doesn't guarantee that the cases will be created in order that the for-loop executes, + // for example case with title '2', could be created before the case with title '1' since we're doing a promise all here + // A promise all is just much faster than doing it one by one which would have guaranteed that the cases are + // created in the order that the for-loop executes + responsePromises.push(createCase(supertest, { ...postCaseReq, title: `${i}` })); + } + const responses = await Promise.all(responsePromises); + return responses; + }; + + /** + * This is used to retrieve all the cases in the same sorted order that we're expecting them to come back via the + * _find API so that we have a more true comparison instead of using the _find API to get all the cases which + * could mangle the results if the implementation had a bug. + * + * Ideally we could enforce how the cases are created in reasonable time, waiting for each api call to finish takes + * around 30 seconds which seemed too slow + */ + const getAllCasesSortedByCreatedAtAsc = async () => { + const cases: ApiResponse> = await es.search({ + index: '.kibana', + body: { + size: 10000, + sort: [{ 'cases.created_at': { unmapped_type: 'date', order: 'asc' } }], + query: { + term: { type: 'cases' }, + }, + }, + }); + return cases.body.hits.hits.map((hit) => hit._source); + }; + + it('returns the correct total when perPage is less than the total', async () => { + const cases = await findCases({ + supertest, + query: { + page: 1, + perPage: 5, + }, + }); + + expect(cases.cases.length).to.eql(5); + expect(cases.total).to.eql(10); + expect(cases.page).to.eql(1); + expect(cases.per_page).to.eql(5); + expect(cases.count_open_cases).to.eql(10); + expect(cases.count_closed_cases).to.eql(0); + expect(cases.count_in_progress_cases).to.eql(0); + }); + + it('returns the correct total when perPage is greater than the total', async () => { + const cases = await findCases({ + supertest, + query: { + page: 1, + perPage: 11, + }, + }); + + expect(cases.total).to.eql(10); + expect(cases.page).to.eql(1); + expect(cases.per_page).to.eql(11); + expect(cases.cases.length).to.eql(10); + expect(cases.count_open_cases).to.eql(10); + expect(cases.count_closed_cases).to.eql(0); + expect(cases.count_in_progress_cases).to.eql(0); + }); + + it('returns the correct total when perPage is equal to the total', async () => { + const cases = await findCases({ + supertest, + query: { + page: 1, + perPage: 10, + }, + }); + + expect(cases.total).to.eql(10); + expect(cases.page).to.eql(1); + expect(cases.per_page).to.eql(10); + expect(cases.cases.length).to.eql(10); + expect(cases.count_open_cases).to.eql(10); + expect(cases.count_closed_cases).to.eql(0); + expect(cases.count_in_progress_cases).to.eql(0); + }); + + it('returns the second page of results', async () => { + const perPage = 5; + const cases = await findCases({ + supertest, + query: { + page: 2, + perPage, + }, + }); + + expect(cases.total).to.eql(10); + expect(cases.page).to.eql(2); + expect(cases.per_page).to.eql(5); + expect(cases.cases.length).to.eql(5); + expect(cases.count_open_cases).to.eql(10); + expect(cases.count_closed_cases).to.eql(0); + expect(cases.count_in_progress_cases).to.eql(0); + + const allCases = await getAllCasesSortedByCreatedAtAsc(); + + cases.cases.map((caseInfo, index) => { + // we started on the second page of 10 cases with a perPage of 5, so the first case should 0 + 5 (index + perPage) + expect(caseInfo.title).to.eql(allCases[index + perPage]?.cases.title); + }); + }); + + it('paginates with perPage of 2 through 10 total cases', async () => { + const total = 10; + const perPage = 2; + + // it's less than or equal here because the page starts at 1, so page 5 is a valid page number + // and should have case titles 9, and 10 + for (let currentPage = 1; currentPage <= total / perPage; currentPage++) { + const cases = await findCases({ + supertest, + query: { + page: currentPage, + perPage, + }, + }); + + expect(cases.total).to.eql(total); + expect(cases.page).to.eql(currentPage); + expect(cases.per_page).to.eql(perPage); + expect(cases.cases.length).to.eql(perPage); + expect(cases.count_open_cases).to.eql(total); + expect(cases.count_closed_cases).to.eql(0); + expect(cases.count_in_progress_cases).to.eql(0); + + const allCases = await getAllCasesSortedByCreatedAtAsc(); + + cases.cases.map((caseInfo, index) => { + // for page 1, the cases tiles should be 0,1,2 for page 2: 3,4,5 etc (assuming the titles were sorted + // correctly) + expect(caseInfo.title).to.eql( + allCases[index + perPage * (currentPage - 1)]?.cases.title + ); + }); + } + }); + + it('retrieves the last three cases', async () => { + const cases = await findCases({ + supertest, + query: { + // this should skip the first 7 cases and only return the last 3 + page: 2, + perPage: 7, + }, + }); + + expect(cases.total).to.eql(10); + expect(cases.page).to.eql(2); + expect(cases.per_page).to.eql(7); + expect(cases.cases.length).to.eql(3); + expect(cases.count_open_cases).to.eql(10); + expect(cases.count_closed_cases).to.eql(0); + expect(cases.count_in_progress_cases).to.eql(0); + }); + }); + + describe('rbac', () => { + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + it('should return the correct cases', async () => { + await Promise.all([ + // Create case owned by the security solution user + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { + user: secOnly, + space: 'space1', + } + ), + // Create case owned by the observability user + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + { + user: obsOnly, + space: 'space1', + } + ), + ]); + + for (const scenario of [ + { + user: globalRead, + numberOfExpectedCases: 2, + owners: ['securitySolutionFixture', 'observabilityFixture'], + }, + { + user: superUser, + numberOfExpectedCases: 2, + owners: ['securitySolutionFixture', 'observabilityFixture'], + }, + { user: secOnlyRead, numberOfExpectedCases: 1, owners: ['securitySolutionFixture'] }, + { user: obsOnlyRead, numberOfExpectedCases: 1, owners: ['observabilityFixture'] }, + { + user: obsSecRead, + numberOfExpectedCases: 2, + owners: ['securitySolutionFixture', 'observabilityFixture'], + }, + ]) { + const res = await findCases({ + supertest: supertestWithoutAuth, + auth: { + user: scenario.user, + space: 'space1', + }, + }); + + ensureSavedObjectIsAuthorized(res.cases, scenario.numberOfExpectedCases, scenario.owners); + } + }); + + for (const scenario of [ + { user: noKibanaPrivileges, space: 'space1' }, + { user: secOnly, space: 'space2' }, + ]) { + it(`User ${scenario.user.username} with role(s) ${scenario.user.roles.join()} and space ${ + scenario.space + } - should NOT read a case`, async () => { + // super user creates a case at the appropriate space + await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { + user: superUser, + space: scenario.space, + } + ); + + // user should not be able to read cases at the appropriate space + await findCases({ + supertest: supertestWithoutAuth, + auth: { + user: scenario.user, + space: scenario.space, + }, + expectedHttpCode: 403, + }); + }); + } + + it('should return the correct cases when trying to exploit RBAC through the search query parameter', async () => { + await Promise.all([ + // super user creates a case with owner securitySolutionFixture + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { + user: superUser, + space: 'space1', + } + ), + // super user creates a case with owner observabilityFixture + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + { + user: superUser, + space: 'space1', + } + ), + ]); + + const res = await findCases({ + supertest: supertestWithoutAuth, + query: { + search: 'securitySolutionFixture observabilityFixture', + searchFields: 'owner', + }, + auth: { + user: secOnly, + space: 'space1', + }, + }); + + ensureSavedObjectIsAuthorized(res.cases, 1, ['securitySolutionFixture']); + }); + + // This test is to prevent a future developer to add the filter attribute without taking into consideration + // the authorizationFilter produced by the cases authorization class + it('should NOT allow to pass a filter query parameter', async () => { + await supertest + .get( + `${CASES_URL}/_find?sortOrder=asc&filter=cases.attributes.owner:"observabilityFixture"` + ) + .set('kbn-xsrf', 'true') + .send() + .expect(400); + }); + + // This test ensures that the user is not allowed to define the namespaces query param + // so she cannot search across spaces + it('should NOT allow to pass a namespaces query parameter', async () => { + await supertest + .get(`${CASES_URL}/_find?sortOrder=asc&namespaces[0]=*`) + .set('kbn-xsrf', 'true') + .send() + .expect(400); + + await supertest + .get(`${CASES_URL}/_find?sortOrder=asc&namespaces=*`) + .set('kbn-xsrf', 'true') + .send() + .expect(400); + }); + + it('should NOT allow to pass a non supported query parameter', async () => { + await supertest + .get(`${CASES_URL}/_find?notExists=papa`) + .set('kbn-xsrf', 'true') + .send() + .expect(400); + }); + + it('should respect the owner filter when having permissions', async () => { + await Promise.all([ + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { + user: obsSec, + space: 'space1', + } + ), + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + { + user: obsSec, + space: 'space1', + } + ), + ]); + + const res = await findCases({ + supertest: supertestWithoutAuth, + query: { + owner: 'securitySolutionFixture', + searchFields: 'owner', + }, + auth: { + user: obsSec, + space: 'space1', + }, + }); + + ensureSavedObjectIsAuthorized(res.cases, 1, ['securitySolutionFixture']); + }); + + it('should return the correct cases when trying to exploit RBAC through the owner query parameter', async () => { + await Promise.all([ + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { + user: obsSec, + space: 'space1', + } + ), + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + { + user: obsSec, + space: 'space1', + } + ), + ]); + + // User with permissions only to security solution request cases from observability + const res = await findCases({ + supertest: supertestWithoutAuth, + query: { + owner: ['securitySolutionFixture', 'observabilityFixture'], + }, + auth: { + user: secOnly, + space: 'space1', + }, + }); + + // Only security solution cases are being returned + ensureSavedObjectIsAuthorized(res.cases, 1, ['securitySolutionFixture']); + }); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/spaces_only/tests/common/cases/get_case.ts b/x-pack/test/case_api_integration/spaces_only/tests/common/cases/get_case.ts new file mode 100644 index 00000000000000..222632b41c297c --- /dev/null +++ b/x-pack/test/case_api_integration/spaces_only/tests/common/cases/get_case.ts @@ -0,0 +1,207 @@ +/* + * 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 { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +import { AttributesTypeUser } from '../../../../../../plugins/cases/common/api'; +import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; +import { + defaultUser, + postCaseReq, + postCaseResp, + postCommentUserReq, + getPostCaseRequest, +} from '../../../../common/lib/mock'; +import { + deleteCasesByESQuery, + createCase, + getCase, + createComment, + removeServerGeneratedPropertiesFromCase, + removeServerGeneratedPropertiesFromSavedObject, +} from '../../../../common/lib/utils'; +import { + secOnly, + obsOnly, + globalRead, + superUser, + secOnlyRead, + obsOnlyRead, + obsSecRead, + noKibanaPrivileges, + obsSec, +} from '../../../../common/lib/authentication/users'; +import { getUserInfo } from '../../../../common/lib/authentication'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const es = getService('es'); + + describe('get_case', () => { + afterEach(async () => { + await deleteCasesByESQuery(es); + }); + + it('should return a case with no comments', async () => { + const postedCase = await createCase(supertest, getPostCaseRequest()); + const theCase = await getCase({ supertest, caseId: postedCase.id, includeComments: true }); + + const data = removeServerGeneratedPropertiesFromCase(theCase); + expect(data).to.eql(postCaseResp()); + expect(data.comments?.length).to.eql(0); + }); + + it('should return a case with comments', async () => { + const postedCase = await createCase(supertest, postCaseReq); + await createComment({ supertest, caseId: postedCase.id, params: postCommentUserReq }); + const theCase = await getCase({ supertest, caseId: postedCase.id, includeComments: true }); + + const comment = removeServerGeneratedPropertiesFromSavedObject( + theCase.comments![0] as AttributesTypeUser + ); + + expect(theCase.comments?.length).to.eql(1); + expect(comment).to.eql({ + type: postCommentUserReq.type, + comment: postCommentUserReq.comment, + associationType: 'case', + created_by: defaultUser, + pushed_at: null, + pushed_by: null, + updated_by: null, + owner: 'securitySolutionFixture', + }); + }); + + it('should return a 400 when passing the includeSubCaseComments', async () => { + const { body } = await supertest + .get(`${CASES_URL}/case-id?includeSubCaseComments=true`) + .set('kbn-xsrf', 'true') + .send() + .expect(400); + + expect(body.message).to.contain('disabled'); + }); + + it('unhappy path - 404s when case is not there', async () => { + await supertest.get(`${CASES_URL}/fake-id`).set('kbn-xsrf', 'true').send().expect(404); + }); + + describe('rbac', () => { + it('should get a case', async () => { + const newCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { + user: superUser, + space: 'space1', + } + ); + + for (const user of [globalRead, superUser, secOnly, secOnlyRead, obsSec, obsSecRead]) { + const theCase = await getCase({ + supertest: supertestWithoutAuth, + caseId: newCase.id, + auth: { user, space: 'space1' }, + }); + + expect(theCase.owner).to.eql('securitySolutionFixture'); + } + }); + + it('should get a case with comments', async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { + user: secOnly, + space: 'space1', + } + ); + + await createComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + params: postCommentUserReq, + expectedHttpCode: 200, + auth: { + user: secOnly, + space: 'space1', + }, + }); + + const theCase = await getCase({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + includeComments: true, + auth: { user: secOnly, space: 'space1' }, + }); + + const comment = removeServerGeneratedPropertiesFromSavedObject( + theCase.comments![0] as AttributesTypeUser + ); + + expect(theCase.comments?.length).to.eql(1); + expect(comment).to.eql({ + type: postCommentUserReq.type, + comment: postCommentUserReq.comment, + associationType: 'case', + created_by: getUserInfo(secOnly), + pushed_at: null, + pushed_by: null, + updated_by: null, + owner: 'securitySolutionFixture', + }); + }); + + it('should not get a case when the user does not have access to owner', async () => { + const newCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { + user: superUser, + space: 'space1', + } + ); + + for (const user of [noKibanaPrivileges, obsOnly, obsOnlyRead]) { + await getCase({ + supertest: supertestWithoutAuth, + caseId: newCase.id, + expectedHttpCode: 403, + auth: { user, space: 'space1' }, + }); + } + }); + + it('should NOT get a case in a space with no permissions', async () => { + const newCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { + user: superUser, + space: 'space2', + } + ); + + await getCase({ + supertest: supertestWithoutAuth, + caseId: newCase.id, + expectedHttpCode: 403, + auth: { user: secOnly, space: 'space2' }, + }); + }); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/spaces_only/tests/common/cases/migrations.ts b/x-pack/test/case_api_integration/spaces_only/tests/common/cases/migrations.ts new file mode 100644 index 00000000000000..42fcace768b157 --- /dev/null +++ b/x-pack/test/case_api_integration/spaces_only/tests/common/cases/migrations.ts @@ -0,0 +1,111 @@ +/* + * 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 { FtrProviderContext } from '../../../../../common/ftr_provider_context'; +import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; + +// eslint-disable-next-line import/no-default-export +export default function createGetTests({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + describe('migrations', () => { + // tests upgrading a 7.10.0 saved object to the latest version + describe('7.10.0 -> latest stack version', () => { + before(async () => { + await esArchiver.load('cases/migrations/7.10.0'); + }); + + after(async () => { + await esArchiver.unload('cases/migrations/7.10.0'); + }); + + it('migrates cases connector', async () => { + const { body } = await supertest + .get(`${CASES_URL}/e1900ac0-017f-11eb-93f8-d161651bf509`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(body).key('connector'); + expect(body).not.key('connector_id'); + expect(body.connector).to.eql({ + id: 'connector-1', + name: 'none', + type: '.none', + fields: null, + }); + }); + + it('migrates cases settings', async () => { + const { body } = await supertest + .get(`${CASES_URL}/e1900ac0-017f-11eb-93f8-d161651bf509`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(body).key('settings'); + expect(body.settings).to.eql({ + syncAlerts: true, + }); + }); + }); + + // tests upgrading a 7.11.1 saved object to the latest version + describe('7.11.1 -> latest stack version', () => { + before(async () => { + await esArchiver.load('cases/migrations/7.11.1'); + }); + + after(async () => { + await esArchiver.unload('cases/migrations/7.11.1'); + }); + + it('adds rule info to only alert comments for 7.12', async () => { + const caseID = '2ea28c10-7855-11eb-9ca6-83ec5acb735f'; + // user comment + let { body } = await supertest + .get(`${CASES_URL}/${caseID}/comments/34a20a00-7855-11eb-9ca6-83ec5acb735f`) + .expect(200); + + expect(body).not.key('rule'); + expect(body.rule).to.eql(undefined); + + // alert comment + ({ body } = await supertest + .get(`${CASES_URL}/${caseID}/comments/3178f2b0-7857-11eb-9ca6-83ec5acb735f`) + .expect(200)); + + expect(body).key('rule'); + expect(body.rule).to.eql({ id: null, name: null }); + }); + + it('adds category and subcategory to the ITSM connector', async () => { + const { body } = await supertest + .get(`${CASES_URL}/6f973440-7abd-11eb-9ca6-83ec5acb735f`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(body).key('connector'); + expect(body.connector).to.eql({ + id: '444ebab0-7abd-11eb-9ca6-83ec5acb735f', + name: 'SN', + type: '.servicenow', + fields: { + impact: '2', + severity: '2', + urgency: '2', + category: null, + subcategory: null, + }, + }); + }); + }); + }); +} diff --git a/x-pack/test/case_api_integration/spaces_only/tests/common/cases/patch_cases.ts b/x-pack/test/case_api_integration/spaces_only/tests/common/cases/patch_cases.ts new file mode 100644 index 00000000000000..674c2c68381b8c --- /dev/null +++ b/x-pack/test/case_api_integration/spaces_only/tests/common/cases/patch_cases.ts @@ -0,0 +1,1240 @@ +/* + * 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 { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +import { DETECTION_ENGINE_QUERY_SIGNALS_URL } from '../../../../../../plugins/security_solution/common/constants'; +import { + CasesResponse, + CaseStatuses, + CaseType, + CommentType, + ConnectorTypes, +} from '../../../../../../plugins/cases/common/api'; +import { + defaultUser, + getPostCaseRequest, + postCaseReq, + postCaseResp, + postCollectionReq, + postCommentAlertReq, + postCommentUserReq, +} from '../../../../common/lib/mock'; +import { + deleteAllCaseItems, + getSignalsWithES, + setStatus, + createCase, + createComment, + updateCase, + getAllUserAction, + removeServerGeneratedPropertiesFromCase, + removeServerGeneratedPropertiesFromUserAction, + findCases, +} from '../../../../common/lib/utils'; +import { + createSignalsIndex, + deleteSignalsIndex, + deleteAllAlerts, + getRuleForSignalTesting, + waitForRuleSuccessOrStatus, + waitForSignalsToBePresent, + getSignalsByIds, + createRule, + getQuerySignalIds, +} from '../../../../../detection_engine_api_integration/utils'; +import { + globalRead, + noKibanaPrivileges, + obsOnly, + obsOnlyRead, + obsSecRead, + secOnly, + secOnlyRead, + superUser, +} from '../../../../common/lib/authentication/users'; +import { superUserSpace1Auth } from '../../../../common/lib/authentication'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + const es = getService('es'); + + describe('patch_cases', () => { + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + describe('happy path', () => { + it('should patch a case', async () => { + const postedCase = await createCase(supertest, postCaseReq); + const patchedCases = await updateCase({ + supertest, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + title: 'new title', + }, + ], + }, + }); + + const data = removeServerGeneratedPropertiesFromCase(patchedCases[0]); + expect(data).to.eql({ + ...postCaseResp(), + title: 'new title', + updated_by: defaultUser, + }); + }); + + it('should closes the case correctly', async () => { + const postedCase = await createCase(supertest, postCaseReq); + const patchedCases = await updateCase({ + supertest, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + status: CaseStatuses.closed, + }, + ], + }, + }); + + const userActions = await getAllUserAction(supertest, postedCase.id); + const statusUserAction = removeServerGeneratedPropertiesFromUserAction(userActions[1]); + const data = removeServerGeneratedPropertiesFromCase(patchedCases[0]); + + expect(data).to.eql({ + ...postCaseResp(), + status: CaseStatuses.closed, + closed_by: defaultUser, + updated_by: defaultUser, + }); + + expect(statusUserAction).to.eql({ + action_field: ['status'], + action: 'update', + action_by: defaultUser, + new_value: CaseStatuses.closed, + old_value: CaseStatuses.open, + case_id: `${postedCase.id}`, + comment_id: null, + sub_case_id: '', + owner: 'securitySolutionFixture', + }); + }); + + it('should change the status of case to in-progress correctly', async () => { + const postedCase = await createCase(supertest, postCaseReq); + const patchedCases = await updateCase({ + supertest, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + status: CaseStatuses['in-progress'], + }, + ], + }, + }); + + const userActions = await getAllUserAction(supertest, postedCase.id); + const statusUserAction = removeServerGeneratedPropertiesFromUserAction(userActions[1]); + const data = removeServerGeneratedPropertiesFromCase(patchedCases[0]); + + expect(data).to.eql({ + ...postCaseResp(), + status: CaseStatuses['in-progress'], + updated_by: defaultUser, + }); + + expect(statusUserAction).to.eql({ + action_field: ['status'], + action: 'update', + action_by: defaultUser, + new_value: CaseStatuses['in-progress'], + old_value: CaseStatuses.open, + case_id: `${postedCase.id}`, + comment_id: null, + sub_case_id: '', + owner: 'securitySolutionFixture', + }); + }); + + it('should patch a case with new connector', async () => { + const postedCase = await createCase(supertest, postCaseReq); + const patchedCases = await updateCase({ + supertest, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + connector: { + id: 'jira', + name: 'Jira', + type: ConnectorTypes.jira, + fields: { issueType: 'Task', priority: null, parent: null }, + }, + }, + ], + }, + }); + + const data = removeServerGeneratedPropertiesFromCase(patchedCases[0]); + expect(data).to.eql({ + ...postCaseResp(), + connector: { + id: 'jira', + name: 'Jira', + type: '.jira', + fields: { issueType: 'Task', priority: null, parent: null }, + }, + updated_by: defaultUser, + }); + }); + + // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests + it.skip('should allow converting an individual case to a collection when it does not have alerts', async () => { + const postedCase = await createCase(supertest, postCaseReq); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + }); + await updateCase({ + supertest, + params: { + cases: [ + { + id: patchedCase.id, + version: patchedCase.version, + type: CaseType.collection, + }, + ], + }, + }); + }); + }); + + describe('unhappy path', () => { + it('400s when attempting to change the owner of a case', async () => { + const postedCase = await createCase(supertest, postCaseReq); + await updateCase({ + supertest, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + owner: 'observabilityFixture', + }, + ], + }, + expectedHttpCode: 400, + }); + }); + + it('404s when case is not there', async () => { + await updateCase({ + supertest, + params: { + cases: [ + { + id: 'not-real', + version: 'version', + status: CaseStatuses.closed, + }, + ], + }, + expectedHttpCode: 404, + }); + }); + + it('400s when id is missing', async () => { + await updateCase({ + supertest, + params: { + cases: [ + // @ts-expect-error + { + version: 'version', + status: CaseStatuses.closed, + }, + ], + }, + expectedHttpCode: 400, + }); + }); + + it('406s when fields are identical', async () => { + const postedCase = await createCase(supertest, postCaseReq); + await updateCase({ + supertest, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + status: CaseStatuses.open, + }, + ], + }, + expectedHttpCode: 406, + }); + }); + + it('400s when version is missing', async () => { + await updateCase({ + supertest, + params: { + cases: [ + // @ts-expect-error + { + id: 'not-real', + status: CaseStatuses.closed, + }, + ], + }, + expectedHttpCode: 400, + }); + }); + + // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests + it.skip('should 400 and not allow converting a collection back to an individual case', async () => { + const postedCase = await createCase(supertest, postCollectionReq); + await updateCase({ + supertest, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + type: CaseType.individual, + }, + ], + }, + expectedHttpCode: 400, + }); + }); + + it('406s when excess data sent', async () => { + const postedCase = await createCase(supertest, postCaseReq); + await updateCase({ + supertest, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + // @ts-expect-error + badKey: 'closed', + }, + ], + }, + expectedHttpCode: 406, + }); + }); + + it('400s when bad data sent', async () => { + const postedCase = await createCase(supertest, postCaseReq); + await updateCase({ + supertest, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + // @ts-expect-error + status: true, + }, + ], + }, + expectedHttpCode: 400, + }); + }); + + it('400s when unsupported status sent', async () => { + const postedCase = await createCase(supertest, postCaseReq); + await updateCase({ + supertest, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + // @ts-expect-error + status: 'not-supported', + }, + ], + }, + expectedHttpCode: 400, + }); + }); + + it('400s when bad connector type sent', async () => { + const postedCase = await createCase(supertest, postCaseReq); + await updateCase({ + supertest, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + // @ts-expect-error + connector: { id: 'none', name: 'none', type: '.not-exists', fields: null }, + }, + ], + }, + expectedHttpCode: 400, + }); + }); + + it('400s when bad connector sent', async () => { + const postedCase = await createCase(supertest, postCaseReq); + await updateCase({ + supertest, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + connector: { + id: 'jira', + name: 'Jira', + // @ts-expect-error + type: ConnectorTypes.jira, + // @ts-expect-error + fields: { unsupported: 'value' }, + }, + }, + ], + }, + expectedHttpCode: 400, + }); + }); + + it('409s when version does not match', async () => { + const postedCase = await createCase(supertest, postCaseReq); + await updateCase({ + supertest, + params: { + cases: [ + { + id: postedCase.id, + version: 'version', + // @ts-expect-error + status: 'closed', + }, + ], + }, + expectedHttpCode: 409, + }); + }); + + it('should 400 when attempting to update an individual case to a collection when it has alerts attached to it', async () => { + const postedCase = await createCase(supertest, postCaseReq); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentAlertReq, + }); + await updateCase({ + supertest, + params: { + cases: [ + { + id: patchedCase.id, + version: patchedCase.version, + type: CaseType.collection, + }, + ], + }, + expectedHttpCode: 400, + }); + }); + + // ENABLE_CASE_CONNECTOR: once the case connector feature is completed delete these tests + it('should 400 when attempting to update the case type when the case connector feature is disabled', async () => { + const postedCase = await createCase(supertest, postCaseReq); + await updateCase({ + supertest, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + type: CaseType.collection, + }, + ], + }, + expectedHttpCode: 400, + }); + }); + + // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests + it.skip("should 400 when attempting to update a collection case's status", async () => { + const postedCase = await createCase(supertest, postCollectionReq); + await updateCase({ + supertest, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + status: CaseStatuses.closed, + }, + ], + }, + expectedHttpCode: 400, + }); + }); + }); + + describe('alerts', () => { + describe('esArchiver', () => { + const defaultSignalsIndex = '.siem-signals-default-000001'; + + beforeEach(async () => { + await esArchiver.load('cases/signals/default'); + }); + afterEach(async () => { + await esArchiver.unload('cases/signals/default'); + await deleteAllCaseItems(es); + }); + + it('should update the status of multiple alerts attached to multiple cases', async () => { + const signalID = '5f2b9ec41f8febb1c06b5d1045aeabb9874733b7617e88a370510f2fb3a41a5d'; + const signalID2 = '4d0f4b1533e46b66b43bdd0330d23f39f2cf42a7253153270e38d30cce9ff0c6'; + + // does NOT updates alert status when adding comments and syncAlerts=false + const individualCase1 = await createCase(supertest, { + ...postCaseReq, + settings: { + syncAlerts: false, + }, + }); + + const updatedInd1WithComment = await createComment({ + supertest, + caseId: individualCase1.id, + params: { + alertId: signalID, + index: defaultSignalsIndex, + rule: { id: 'test-rule-id', name: 'test-index-id' }, + type: CommentType.alert, + owner: 'securitySolutionFixture', + }, + }); + + const individualCase2 = await createCase(supertest, { + ...postCaseReq, + settings: { + syncAlerts: false, + }, + }); + + const updatedInd2WithComment = await createComment({ + supertest, + caseId: individualCase2.id, + params: { + alertId: signalID2, + index: defaultSignalsIndex, + rule: { id: 'test-rule-id', name: 'test-index-id' }, + type: CommentType.alert, + owner: 'securitySolutionFixture', + }, + }); + + await es.indices.refresh({ index: defaultSignalsIndex }); + + let signals = await getSignalsWithES({ + es, + indices: defaultSignalsIndex, + ids: [signalID, signalID2], + }); + + // There should be no change in their status since syncing is disabled + expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( + CaseStatuses.open + ); + expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source?.signal.status).to.be( + CaseStatuses.open + ); + + // does NOT updates alert status when the status is updated and syncAlerts=false + const updatedIndWithStatus: CasesResponse = (await setStatus({ + supertest, + cases: [ + { + id: updatedInd1WithComment.id, + version: updatedInd1WithComment.version, + status: CaseStatuses.closed, + }, + { + id: updatedInd2WithComment.id, + version: updatedInd2WithComment.version, + status: CaseStatuses['in-progress'], + }, + ], + type: 'case', + })) as CasesResponse; + + await es.indices.refresh({ index: defaultSignalsIndex }); + + signals = await getSignalsWithES({ + es, + indices: defaultSignalsIndex, + ids: [signalID, signalID2], + }); + + // There should still be no change in their status since syncing is disabled + expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( + CaseStatuses.open + ); + expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source?.signal.status).to.be( + CaseStatuses.open + ); + + // it updates alert status when syncAlerts is turned on + // turn on the sync settings + await updateCase({ + supertest, + params: { + cases: updatedIndWithStatus.map((caseInfo) => ({ + id: caseInfo.id, + version: caseInfo.version, + settings: { syncAlerts: true }, + })), + }, + }); + + await es.indices.refresh({ index: defaultSignalsIndex }); + + signals = await getSignalsWithES({ + es, + indices: defaultSignalsIndex, + ids: [signalID, signalID2], + }); + + // alerts should be updated now that the + expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( + CaseStatuses.closed + ); + expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source?.signal.status).to.be( + CaseStatuses['in-progress'] + ); + }); + }); + + describe('esArchiver', () => { + const defaultSignalsIndex = '.siem-signals-default-000001'; + + beforeEach(async () => { + await esArchiver.load('cases/signals/duplicate_ids'); + }); + afterEach(async () => { + await esArchiver.unload('cases/signals/duplicate_ids'); + await deleteAllCaseItems(es); + }); + + it('should not update the status of duplicate alert ids in separate indices', async () => { + const getSignals = async () => { + return getSignalsWithES({ + es, + indices: [defaultSignalsIndex, signalsIndex2], + ids: [signalIDInFirstIndex, signalIDInSecondIndex], + }); + }; + + // this id exists only in .siem-signals-default-000001 + const signalIDInFirstIndex = + 'cae78067e65582a3b277c1ad46ba3cb29044242fe0d24bbf3fcde757fdd31d1c'; + // This id exists in both .siem-signals-default-000001 and .siem-signals-default-000002 + const signalIDInSecondIndex = 'duplicate-signal-id'; + const signalsIndex2 = '.siem-signals-default-000002'; + + const individualCase = await createCase(supertest, { + ...postCaseReq, + settings: { + syncAlerts: false, + }, + }); + + const updatedIndWithComment = await createComment({ + supertest, + caseId: individualCase.id, + params: { + alertId: signalIDInFirstIndex, + index: defaultSignalsIndex, + rule: { id: 'test-rule-id', name: 'test-index-id' }, + type: CommentType.alert, + owner: 'securitySolutionFixture', + }, + }); + + const updatedIndWithComment2 = await createComment({ + supertest, + caseId: updatedIndWithComment.id, + params: { + alertId: signalIDInSecondIndex, + index: signalsIndex2, + rule: { id: 'test-rule-id', name: 'test-index-id' }, + type: CommentType.alert, + owner: 'securitySolutionFixture', + }, + }); + + await es.indices.refresh({ index: defaultSignalsIndex }); + + let signals = await getSignals(); + // There should be no change in their status since syncing is disabled + expect( + signals.get(defaultSignalsIndex)?.get(signalIDInFirstIndex)?._source?.signal.status + ).to.be(CaseStatuses.open); + expect( + signals.get(signalsIndex2)?.get(signalIDInSecondIndex)?._source?.signal.status + ).to.be(CaseStatuses.open); + + const updatedIndWithStatus: CasesResponse = (await setStatus({ + supertest, + cases: [ + { + id: updatedIndWithComment2.id, + version: updatedIndWithComment2.version, + status: CaseStatuses.closed, + }, + ], + type: 'case', + })) as CasesResponse; + + await es.indices.refresh({ index: defaultSignalsIndex }); + + signals = await getSignals(); + + // There should still be no change in their status since syncing is disabled + expect( + signals.get(defaultSignalsIndex)?.get(signalIDInFirstIndex)?._source?.signal.status + ).to.be(CaseStatuses.open); + expect( + signals.get(signalsIndex2)?.get(signalIDInSecondIndex)?._source?.signal.status + ).to.be(CaseStatuses.open); + + // turn on the sync settings + await updateCase({ + supertest, + params: { + cases: [ + { + id: updatedIndWithStatus[0].id, + version: updatedIndWithStatus[0].version, + settings: { syncAlerts: true }, + }, + ], + }, + }); + await es.indices.refresh({ index: defaultSignalsIndex }); + + signals = await getSignals(); + + // alerts should be updated now that the + expect( + signals.get(defaultSignalsIndex)?.get(signalIDInFirstIndex)?._source?.signal.status + ).to.be(CaseStatuses.closed); + expect( + signals.get(signalsIndex2)?.get(signalIDInSecondIndex)?._source?.signal.status + ).to.be(CaseStatuses.closed); + + // the duplicate signal id in the other index should not be affect (so its status should be open) + expect( + signals.get(defaultSignalsIndex)?.get(signalIDInSecondIndex)?._source?.signal.status + ).to.be(CaseStatuses.open); + }); + }); + + describe('detections rule', () => { + beforeEach(async () => { + await esArchiver.load('auditbeat/hosts'); + await createSignalsIndex(supertest); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest); + await deleteAllAlerts(supertest); + await esArchiver.unload('auditbeat/hosts'); + }); + + it('updates alert status when the status is updated and syncAlerts=true', async () => { + const rule = getRuleForSignalTesting(['auditbeat-*']); + const postedCase = await createCase(supertest, postCaseReq); + + const { id } = await createRule(supertest, rule); + await waitForRuleSuccessOrStatus(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signals = await getSignalsByIds(supertest, [id]); + + const alert = signals.hits.hits[0]; + expect(alert._source.signal.status).eql('open'); + + const caseUpdated = await createComment({ + supertest, + caseId: postedCase.id, + params: { + alertId: alert._id, + index: alert._index, + rule: { + id: 'id', + name: 'name', + }, + type: CommentType.alert, + owner: 'securitySolutionFixture', + }, + }); + + await es.indices.refresh({ index: alert._index }); + await updateCase({ + supertest, + params: { + cases: [ + { + id: caseUpdated.id, + version: caseUpdated.version, + status: CaseStatuses['in-progress'], + }, + ], + }, + }); + + // force a refresh on the index that the signal is stored in so that we can search for it and get the correct + // status + await es.indices.refresh({ index: alert._index }); + + const { body: updatedAlert } = await supertest + .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) + .set('kbn-xsrf', 'true') + .send(getQuerySignalIds([alert._id])) + .expect(200); + + expect(updatedAlert.hits.hits[0]._source.signal.status).eql('in-progress'); + }); + + it('does NOT updates alert status when the status is updated and syncAlerts=false', async () => { + const rule = getRuleForSignalTesting(['auditbeat-*']); + + const postedCase = await createCase(supertest, { + ...postCaseReq, + settings: { syncAlerts: false }, + }); + + const { id } = await createRule(supertest, rule); + await waitForRuleSuccessOrStatus(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signals = await getSignalsByIds(supertest, [id]); + + const alert = signals.hits.hits[0]; + expect(alert._source.signal.status).eql('open'); + + const caseUpdated = await createComment({ + supertest, + caseId: postedCase.id, + params: { + alertId: alert._id, + index: alert._index, + type: CommentType.alert, + rule: { + id: 'id', + name: 'name', + }, + owner: 'securitySolutionFixture', + }, + }); + + await updateCase({ + supertest, + params: { + cases: [ + { + id: caseUpdated.id, + version: caseUpdated.version, + status: CaseStatuses['in-progress'], + }, + ], + }, + }); + + const { body: updatedAlert } = await supertest + .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) + .set('kbn-xsrf', 'true') + .send(getQuerySignalIds([alert._id])) + .expect(200); + + expect(updatedAlert.hits.hits[0]._source.signal.status).eql('open'); + }); + + it('it updates alert status when syncAlerts is turned on', async () => { + const rule = getRuleForSignalTesting(['auditbeat-*']); + + const postedCase = await createCase(supertest, { + ...postCaseReq, + settings: { syncAlerts: false }, + }); + + const { id } = await createRule(supertest, rule); + await waitForRuleSuccessOrStatus(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signals = await getSignalsByIds(supertest, [id]); + + const alert = signals.hits.hits[0]; + expect(alert._source.signal.status).eql('open'); + + const caseUpdated = await createComment({ + supertest, + caseId: postedCase.id, + params: { + alertId: alert._id, + index: alert._index, + rule: { + id: 'id', + name: 'name', + }, + type: CommentType.alert, + owner: 'securitySolutionFixture', + }, + }); + + // Update the status of the case with sync alerts off + const caseStatusUpdated = await updateCase({ + supertest, + params: { + cases: [ + { + id: caseUpdated.id, + version: caseUpdated.version, + status: CaseStatuses['in-progress'], + }, + ], + }, + }); + + // Turn sync alerts on + await updateCase({ + supertest, + params: { + cases: [ + { + id: caseStatusUpdated[0].id, + version: caseStatusUpdated[0].version, + settings: { syncAlerts: true }, + }, + ], + }, + }); + + // refresh the index because syncAlerts was set to true so the alert's status should have been updated + await es.indices.refresh({ index: alert._index }); + + const { body: updatedAlert } = await supertest + .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) + .set('kbn-xsrf', 'true') + .send(getQuerySignalIds([alert._id])) + .expect(200); + + expect(updatedAlert.hits.hits[0]._source.signal.status).eql('in-progress'); + }); + + it('it does NOT updates alert status when syncAlerts is turned off', async () => { + const rule = getRuleForSignalTesting(['auditbeat-*']); + + const postedCase = await createCase(supertest, postCaseReq); + const { id } = await createRule(supertest, rule); + await waitForRuleSuccessOrStatus(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signals = await getSignalsByIds(supertest, [id]); + + const alert = signals.hits.hits[0]; + expect(alert._source.signal.status).eql('open'); + + const caseUpdated = await createComment({ + supertest, + caseId: postedCase.id, + params: { + alertId: alert._id, + index: alert._index, + type: CommentType.alert, + rule: { + id: 'id', + name: 'name', + }, + owner: 'securitySolutionFixture', + }, + }); + + // Turn sync alerts off + const caseSettingsUpdated = await updateCase({ + supertest, + params: { + cases: [ + { + id: caseUpdated.id, + version: caseUpdated.version, + settings: { syncAlerts: false }, + }, + ], + }, + }); + + // Update the status of the case with sync alerts off + await updateCase({ + supertest, + params: { + cases: [ + { + id: caseSettingsUpdated[0].id, + version: caseSettingsUpdated[0].version, + status: CaseStatuses['in-progress'], + }, + ], + }, + }); + + const { body: updatedAlert } = await supertest + .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) + .set('kbn-xsrf', 'true') + .send(getQuerySignalIds([alert._id])) + .expect(200); + + expect(updatedAlert.hits.hits[0]._source.signal.status).eql('open'); + }); + }); + }); + + describe('rbac', () => { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + it('should update a case when the user has the correct permissions', async () => { + const postedCase = await createCase(supertestWithoutAuth, postCaseReq, 200, { + user: secOnly, + space: 'space1', + }); + + const patchedCases = await updateCase({ + supertest: supertestWithoutAuth, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + title: 'new title', + }, + ], + }, + auth: { user: secOnly, space: 'space1' }, + }); + + expect(patchedCases[0].owner).to.eql('securitySolutionFixture'); + }); + + it('should update multiple cases when the user has the correct permissions', async () => { + const [case1, case2, case3] = await Promise.all([ + createCase(supertestWithoutAuth, postCaseReq, 200, { + user: superUser, + space: 'space1', + }), + createCase(supertestWithoutAuth, postCaseReq, 200, { + user: superUser, + space: 'space1', + }), + createCase(supertestWithoutAuth, postCaseReq, 200, { + user: superUser, + space: 'space1', + }), + ]); + + const patchedCases = await updateCase({ + supertest: supertestWithoutAuth, + params: { + cases: [ + { + id: case1.id, + version: case1.version, + title: 'new title', + }, + { + id: case2.id, + version: case2.version, + title: 'new title', + }, + { + id: case3.id, + version: case3.version, + title: 'new title', + }, + ], + }, + auth: { user: secOnly, space: 'space1' }, + }); + + expect(patchedCases[0].owner).to.eql('securitySolutionFixture'); + expect(patchedCases[1].owner).to.eql('securitySolutionFixture'); + expect(patchedCases[2].owner).to.eql('securitySolutionFixture'); + }); + + it('should not update a case when the user does not have the correct ownership', async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + { user: obsOnly, space: 'space1' } + ); + + await updateCase({ + supertest: supertestWithoutAuth, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + title: 'new title', + }, + ], + }, + auth: { user: secOnly, space: 'space1' }, + expectedHttpCode: 403, + }); + }); + + it('should not update any cases when the user does not have the correct ownership', async () => { + const [case1, case2, case3] = await Promise.all([ + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + { + user: superUser, + space: 'space1', + } + ), + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + { + user: superUser, + space: 'space1', + } + ), + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + { + user: superUser, + space: 'space1', + } + ), + ]); + + await updateCase({ + supertest: supertestWithoutAuth, + params: { + cases: [ + { + id: case1.id, + version: case1.version, + title: 'new title', + }, + { + id: case2.id, + version: case2.version, + title: 'new title', + }, + { + id: case3.id, + version: case3.version, + title: 'new title', + }, + ], + }, + auth: { user: secOnly, space: 'space1' }, + expectedHttpCode: 403, + }); + + const resp = await findCases({ supertest, auth: superUserSpace1Auth }); + expect(resp.cases.length).to.eql(3); + // the update should have failed and none of the title should have been changed + expect(resp.cases[0].title).to.eql(postCaseReq.title); + expect(resp.cases[1].title).to.eql(postCaseReq.title); + expect(resp.cases[2].title).to.eql(postCaseReq.title); + }); + + for (const user of [globalRead, secOnlyRead, obsOnlyRead, obsSecRead, noKibanaPrivileges]) { + it(`User ${ + user.username + } with role(s) ${user.roles.join()} - should NOT update a case`, async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { + user: superUser, + space: 'space1', + } + ); + + await updateCase({ + supertest: supertestWithoutAuth, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + title: 'new title', + }, + ], + }, + auth: { user, space: 'space1' }, + expectedHttpCode: 403, + }); + }); + } + + it('should NOT create a case in a space with no permissions', async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { + user: superUser, + space: 'space2', + } + ); + + await updateCase({ + supertest: supertestWithoutAuth, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + title: 'new title', + }, + ], + }, + auth: { user: secOnly, space: 'space2' }, + expectedHttpCode: 403, + }); + }); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/spaces_only/tests/common/cases/post_case.ts b/x-pack/test/case_api_integration/spaces_only/tests/common/cases/post_case.ts new file mode 100644 index 00000000000000..91fb03604b3c4b --- /dev/null +++ b/x-pack/test/case_api_integration/spaces_only/tests/common/cases/post_case.ts @@ -0,0 +1,292 @@ +/* + * 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. + */ + +/* eslint-disable @typescript-eslint/naming-convention */ + +import expect from '@kbn/expect'; + +import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; +import { + ConnectorTypes, + ConnectorJiraTypeFields, + CaseStatuses, + CaseUserActionResponse, +} from '../../../../../../plugins/cases/common/api'; +import { getPostCaseRequest, postCaseResp, defaultUser } from '../../../../common/lib/mock'; +import { + deleteCasesByESQuery, + createCase, + removeServerGeneratedPropertiesFromCase, + removeServerGeneratedPropertiesFromUserAction, + getAllUserAction, +} from '../../../../common/lib/utils'; +import { + secOnly, + secOnlyRead, + globalRead, + obsOnlyRead, + obsSecRead, + noKibanaPrivileges, +} from '../../../../common/lib/authentication/users'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + describe('post_case', () => { + afterEach(async () => { + await deleteCasesByESQuery(es); + }); + + describe('happy path', () => { + it('should post a case', async () => { + const postedCase = await createCase( + supertest, + getPostCaseRequest({ + connector: { + id: '123', + name: 'Jira', + type: ConnectorTypes.jira, + fields: { issueType: 'Task', priority: 'High', parent: null }, + }, + }) + ); + const data = removeServerGeneratedPropertiesFromCase(postedCase); + + expect(data).to.eql( + postCaseResp( + null, + getPostCaseRequest({ + connector: { + id: '123', + name: 'Jira', + type: ConnectorTypes.jira, + fields: { issueType: 'Task', priority: 'High', parent: null }, + }, + }) + ) + ); + }); + + it('should post a case: none connector', async () => { + const postedCase = await createCase( + supertest, + getPostCaseRequest({ + connector: { + id: 'none', + name: 'none', + type: ConnectorTypes.none, + fields: null, + }, + }) + ); + const data = removeServerGeneratedPropertiesFromCase(postedCase); + + expect(data).to.eql( + postCaseResp( + null, + getPostCaseRequest({ + connector: { + id: 'none', + name: 'none', + type: ConnectorTypes.none, + fields: null, + }, + }) + ) + ); + }); + + it('should create a user action when creating a case', async () => { + const postedCase = await createCase(supertest, getPostCaseRequest()); + const userActions = await getAllUserAction(supertest, postedCase.id); + const creationUserAction = removeServerGeneratedPropertiesFromUserAction(userActions[0]); + + const { new_value, ...rest } = creationUserAction as CaseUserActionResponse; + const parsedNewValue = JSON.parse(new_value!); + + expect(rest).to.eql({ + action_field: [ + 'description', + 'status', + 'tags', + 'title', + 'connector', + 'settings', + 'owner', + ], + action: 'create', + action_by: defaultUser, + old_value: null, + case_id: `${postedCase.id}`, + comment_id: null, + sub_case_id: '', + owner: 'securitySolutionFixture', + }); + + expect(parsedNewValue).to.eql({ + type: postedCase.type, + description: postedCase.description, + title: postedCase.title, + tags: postedCase.tags, + connector: postedCase.connector, + settings: postedCase.settings, + owner: postedCase.owner, + }); + }); + + it('creates the case without connector in the configuration', async () => { + const postedCase = await createCase(supertest, getPostCaseRequest()); + const data = removeServerGeneratedPropertiesFromCase(postedCase); + + expect(data).to.eql(postCaseResp()); + }); + }); + + describe('unhappy path', () => { + it('400s when bad query supplied', async () => { + await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + // @ts-expect-error + .send({ ...getPostCaseRequest({ badKey: true }) }) + .expect(400); + }); + + it('400s when connector is not supplied', async () => { + const { connector, ...caseWithoutConnector } = getPostCaseRequest(); + + await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(caseWithoutConnector) + .expect(400); + }); + + it('400s when connector has wrong type', async () => { + await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send({ + ...getPostCaseRequest({ + // @ts-expect-error + connector: { id: 'wrong', name: 'wrong', type: '.not-exists', fields: null }, + }), + }) + .expect(400); + }); + + it('400s when connector has wrong fields', async () => { + await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send({ + ...getPostCaseRequest({ + // @ts-expect-error + connector: { + id: 'wrong', + name: 'wrong', + type: ConnectorTypes.jira, + fields: { unsupported: 'value' }, + } as ConnectorJiraTypeFields, + }), + }) + .expect(400); + }); + + it('400s when missing title', async () => { + const { title, ...caseWithoutTitle } = getPostCaseRequest(); + + await supertest.post(CASES_URL).set('kbn-xsrf', 'true').send(caseWithoutTitle).expect(400); + }); + + it('400s when missing description', async () => { + const { description, ...caseWithoutDescription } = getPostCaseRequest(); + + await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(caseWithoutDescription) + .expect(400); + }); + + it('400s when missing tags', async () => { + const { tags, ...caseWithoutTags } = getPostCaseRequest(); + + await supertest.post(CASES_URL).set('kbn-xsrf', 'true').send(caseWithoutTags).expect(400); + }); + + it('400s if you passing status for a new case', async () => { + const req = getPostCaseRequest(); + + await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send({ ...req, status: CaseStatuses.open }) + .expect(400); + }); + }); + + describe('rbac', () => { + it('User: security solution only - should create a case', async () => { + const theCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { + user: secOnly, + space: 'space1', + } + ); + expect(theCase.owner).to.eql('securitySolutionFixture'); + }); + + it('User: security solution only - should NOT create a case of different owner', async () => { + await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 403, + { + user: secOnly, + space: 'space1', + } + ); + }); + + for (const user of [globalRead, secOnlyRead, obsOnlyRead, obsSecRead, noKibanaPrivileges]) { + it(`User ${ + user.username + } with role(s) ${user.roles.join()} - should NOT create a case`, async () => { + await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 403, + { + user, + space: 'space1', + } + ); + }); + } + + it('should NOT create a case in a space with no permissions', async () => { + await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 403, + { + user: secOnly, + space: 'space2', + } + ); + }); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/spaces_only/tests/common/cases/reporters/get_reporters.ts b/x-pack/test/case_api_integration/spaces_only/tests/common/cases/reporters/get_reporters.ts new file mode 100644 index 00000000000000..dee239231faca4 --- /dev/null +++ b/x-pack/test/case_api_integration/spaces_only/tests/common/cases/reporters/get_reporters.ts @@ -0,0 +1,41 @@ +/* + * 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 { FtrProviderContext } from '../../../../../common/ftr_provider_context'; + +import { defaultUser, getPostCaseRequest } from '../../../../../common/lib/mock'; +import { + createCase, + deleteCasesByESQuery, + getAuthWithSuperUser, + getReporters, +} from '../../../../../common/lib/utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + const authSpace1 = getAuthWithSuperUser(); + + describe('get_reporters', () => { + afterEach(async () => { + await deleteCasesByESQuery(es); + }); + + it('should return reporters in space1', async () => { + await Promise.all([ + createCase(supertest, getPostCaseRequest(), 200, getAuthWithSuperUser('space2')), + createCase(supertest, getPostCaseRequest(), 200, authSpace1), + ]); + + const reporters = await getReporters({ supertest, auth: authSpace1 }); + + expect(reporters).to.eql([defaultUser]); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/spaces_only/tests/common/cases/status/get_status.ts b/x-pack/test/case_api_integration/spaces_only/tests/common/cases/status/get_status.ts new file mode 100644 index 00000000000000..1f5f68104ffd9f --- /dev/null +++ b/x-pack/test/case_api_integration/spaces_only/tests/common/cases/status/get_status.ts @@ -0,0 +1,75 @@ +/* + * 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 { FtrProviderContext } from '../../../../../common/ftr_provider_context'; + +import { CaseStatuses } from '../../../../../../../plugins/cases/common/api'; +import { postCaseReq } from '../../../../../common/lib/mock'; +import { + createCase, + updateCase, + getAllCasesStatuses, + deleteAllCaseItems, + getAuthWithSuperUser, +} from '../../../../../common/lib/utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + const authSpace1 = getAuthWithSuperUser(); + + describe('get_status', () => { + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + it('should return case statuses in space1', async () => { + /** + * space1: + * open: 1 + * in progress: 1 + * closed: 0 + * space2: + * closed: 1 + */ + const [, inProgressCase, postedCase] = await Promise.all([ + createCase(supertest, postCaseReq, 200, authSpace1), + createCase(supertest, postCaseReq, 200, authSpace1), + createCase(supertest, postCaseReq, 200, getAuthWithSuperUser('space2')), + ]); + + await updateCase({ + supertest, + params: { + cases: [ + { + id: inProgressCase.id, + version: inProgressCase.version, + status: CaseStatuses['in-progress'], + }, + { + id: postedCase.id, + version: postedCase.version, + status: CaseStatuses.closed, + }, + ], + }, + auth: authSpace1, + }); + + const statuses = await getAllCasesStatuses({ supertest, auth: authSpace1 }); + + expect(statuses).to.eql({ + count_open_cases: 1, + count_closed_cases: 0, + count_in_progress_cases: 1, + }); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/spaces_only/tests/common/cases/tags/get_tags.ts b/x-pack/test/case_api_integration/spaces_only/tests/common/cases/tags/get_tags.ts new file mode 100644 index 00000000000000..bae3c61fb5fb72 --- /dev/null +++ b/x-pack/test/case_api_integration/spaces_only/tests/common/cases/tags/get_tags.ts @@ -0,0 +1,43 @@ +/* + * 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 { FtrProviderContext } from '../../../../../common/ftr_provider_context'; + +import { + deleteCasesByESQuery, + createCase, + getTags, + getAuthWithSuperUser, +} from '../../../../../common/lib/utils'; +import { getPostCaseRequest } from '../../../../../common/lib/mock'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + const authSpace1 = getAuthWithSuperUser(); + + describe('get_tags', () => { + afterEach(async () => { + await deleteCasesByESQuery(es); + }); + + it('should return case tags in space1', async () => { + await createCase(supertest, getPostCaseRequest(), 200, authSpace1); + await createCase( + supertest, + getPostCaseRequest({ tags: ['unique'] }), + 200, + getAuthWithSuperUser('space2') + ); + + const tags = await getTags({ supertest }); + expect(tags).to.eql(['defacement']); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/spaces_only/tests/common/comments/delete_comment.ts b/x-pack/test/case_api_integration/spaces_only/tests/common/comments/delete_comment.ts new file mode 100644 index 00000000000000..73b85ef97d1194 --- /dev/null +++ b/x-pack/test/case_api_integration/spaces_only/tests/common/comments/delete_comment.ts @@ -0,0 +1,372 @@ +/* + * 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 { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; +import { getPostCaseRequest, postCaseReq, postCommentUserReq } from '../../../../common/lib/mock'; +import { + createCaseAction, + createSubCase, + deleteAllCaseItems, + deleteCaseAction, + deleteCasesByESQuery, + deleteCasesUserActions, + deleteComments, + createCase, + createComment, + deleteComment, + deleteAllComments, +} from '../../../../common/lib/utils'; +import { + globalRead, + noKibanaPrivileges, + obsOnly, + obsOnlyRead, + obsSecRead, + secOnly, + secOnlyRead, + superUser, +} from '../../../../common/lib/authentication/users'; +import { superUserSpace1Auth } from '../../../../common/lib/authentication'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + + describe('delete_comment', () => { + afterEach(async () => { + await deleteCasesByESQuery(es); + await deleteComments(es); + await deleteCasesUserActions(es); + }); + + describe('happy path', () => { + it('should delete a comment', async () => { + const postedCase = await createCase(supertest, postCaseReq); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + }); + const comment = await deleteComment({ + supertest, + caseId: postedCase.id, + commentId: patchedCase.comments![0].id, + }); + + expect(comment).to.eql({}); + }); + }); + + describe('unhappy path', () => { + it('404s when comment belongs to different case', async () => { + const postedCase = await createCase(supertest, postCaseReq); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + }); + const error = (await deleteComment({ + supertest, + caseId: 'fake-id', + commentId: patchedCase.comments![0].id, + expectedHttpCode: 404, + })) as Error; + + expect(error.message).to.be( + `This comment ${patchedCase.comments![0].id} does not exist in fake-id.` + ); + }); + + it('404s when comment is not there', async () => { + await deleteComment({ + supertest, + caseId: 'fake-id', + commentId: 'fake-id', + expectedHttpCode: 404, + }); + }); + + it('should return a 400 when attempting to delete all comments when passing the `subCaseId` parameter', async () => { + const { body } = await supertest + .delete(`${CASES_URL}/case-id/comments?subCaseId=value`) + .set('kbn-xsrf', 'true') + .send() + .expect(400); + // make sure the failure is because of the subCaseId + expect(body.message).to.contain('disabled'); + }); + + it('should return a 400 when attempting to delete a single comment when passing the `subCaseId` parameter', async () => { + const { body } = await supertest + .delete(`${CASES_URL}/case-id/comments/comment-id?subCaseId=value`) + .set('kbn-xsrf', 'true') + .send() + .expect(400); + // make sure the failure is because of the subCaseId + expect(body.message).to.contain('disabled'); + }); + }); + + // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests + describe.skip('sub case comments', () => { + let actionID: string; + before(async () => { + actionID = await createCaseAction(supertest); + }); + after(async () => { + await deleteCaseAction(supertest, actionID); + }); + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + it('deletes a comment from a sub case', async () => { + const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); + await supertest + .delete( + `${CASES_URL}/${caseInfo.id}/comments/${caseInfo.comments![0].id}?subCaseId=${ + caseInfo.subCases![0].id + }` + ) + .set('kbn-xsrf', 'true') + .send() + .expect(204); + + const { body } = await supertest.get( + `${CASES_URL}/${caseInfo.id}/comments?subCaseId=${caseInfo.subCases![0].id}` + ); + + expect(body.length).to.eql(0); + }); + + it('deletes all comments from a sub case', async () => { + const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); + await supertest + .post(`${CASES_URL}/${caseInfo.id}/comments?subCaseId=${caseInfo.subCases![0].id}`) + .set('kbn-xsrf', 'true') + .send(postCommentUserReq) + .expect(200); + + let { body: allComments } = await supertest.get( + `${CASES_URL}/${caseInfo.id}/comments?subCaseId=${caseInfo.subCases![0].id}` + ); + expect(allComments.length).to.eql(2); + + await supertest + .delete(`${CASES_URL}/${caseInfo.id}/comments?subCaseId=${caseInfo.subCases![0].id}`) + .set('kbn-xsrf', 'true') + .send() + .expect(204); + + ({ body: allComments } = await supertest.get( + `${CASES_URL}/${caseInfo.id}/comments?subCaseId=${caseInfo.subCases![0].id}` + )); + + // no comments for the sub case + expect(allComments.length).to.eql(0); + + ({ body: allComments } = await supertest.get(`${CASES_URL}/${caseInfo.id}/comments`)); + + // no comments for the collection + expect(allComments.length).to.eql(0); + }); + }); + + describe('rbac', () => { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + it('should delete a comment from the appropriate owner', async () => { + const secCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { user: secOnly, space: 'space1' } + ); + + const commentResp = await createComment({ + supertest: supertestWithoutAuth, + caseId: secCase.id, + params: postCommentUserReq, + auth: { user: secOnly, space: 'space1' }, + }); + + await deleteComment({ + supertest: supertestWithoutAuth, + caseId: secCase.id, + commentId: commentResp.comments![0].id, + auth: { user: secOnly, space: 'space1' }, + }); + }); + + 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' } + ); + + const commentResp = await createComment({ + supertest: supertestWithoutAuth, + caseId: secCase.id, + params: postCommentUserReq, + auth: { user: secOnly, space: 'space1' }, + }); + + await deleteComment({ + supertest: supertestWithoutAuth, + caseId: secCase.id, + commentId: commentResp.comments![0].id, + 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]) { + it(`User ${ + user.username + } with role(s) ${user.roles.join()} - should NOT delete a comment`, async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + superUserSpace1Auth + ); + + const commentResp = await createComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + params: postCommentUserReq, + auth: superUserSpace1Auth, + }); + + await deleteComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + commentId: commentResp.comments![0].id, + auth: { user, space: 'space1' }, + expectedHttpCode: 403, + }); + + await deleteAllComments({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + auth: { user, space: 'space1' }, + expectedHttpCode: 403, + }); + }); + } + + it('should not delete a comment with no kibana privileges', async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + superUserSpace1Auth + ); + + const commentResp = await createComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + params: postCommentUserReq, + auth: superUserSpace1Auth, + }); + + await deleteComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + commentId: commentResp.comments![0].id, + auth: { user: noKibanaPrivileges, space: 'space1' }, + expectedHttpCode: 403, + }); + + await deleteAllComments({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + auth: { user: noKibanaPrivileges, space: 'space1' }, + // the find in the delete all will return no results + expectedHttpCode: 404, + }); + }); + + 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' } + ); + + const commentResp = await createComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + params: postCommentUserReq, + auth: { user: superUser, space: 'space2' }, + }); + + await deleteComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + commentId: commentResp.comments![0].id, + auth: { user: secOnly, space: 'space1' }, + expectedHttpCode: 404, + }); + + await deleteAllComments({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + auth: { user: secOnly, space: 'space1' }, + expectedHttpCode: 404, + }); + }); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/spaces_only/tests/common/comments/find_comments.ts b/x-pack/test/case_api_integration/spaces_only/tests/common/comments/find_comments.ts new file mode 100644 index 00000000000000..0f73b1ee7a624a --- /dev/null +++ b/x-pack/test/case_api_integration/spaces_only/tests/common/comments/find_comments.ts @@ -0,0 +1,393 @@ +/* + * 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 { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; +import { CommentsResponse, CommentType } from '../../../../../../plugins/cases/common/api'; +import { + getPostCaseRequest, + postCaseReq, + postCommentAlertReq, + postCommentUserReq, +} from '../../../../common/lib/mock'; +import { + createCaseAction, + createComment, + createSubCase, + deleteAllCaseItems, + deleteCaseAction, + deleteCasesByESQuery, + deleteCasesUserActions, + deleteComments, + ensureSavedObjectIsAuthorized, + getSpaceUrlPrefix, + createCase, +} from '../../../../common/lib/utils'; + +import { + obsOnly, + secOnly, + obsOnlyRead, + secOnlyRead, + noKibanaPrivileges, + superUser, + globalRead, + obsSecRead, +} from '../../../../common/lib/authentication/users'; +import { superUserSpace1Auth } from '../../../../common/lib/authentication'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + + describe('find_comments', () => { + afterEach(async () => { + await deleteCasesByESQuery(es); + await deleteComments(es); + await deleteCasesUserActions(es); + }); + + it('should find all case comment', async () => { + const { body: postedCase } = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCaseReq) + .expect(200); + + // post 2 comments + await supertest + .post(`${CASES_URL}/${postedCase.id}/comments`) + .set('kbn-xsrf', 'true') + .send(postCommentUserReq) + .expect(200); + + const { body: patchedCase } = await supertest + .post(`${CASES_URL}/${postedCase.id}/comments`) + .set('kbn-xsrf', 'true') + .send(postCommentUserReq) + .expect(200); + + const { body: caseComments } = await supertest + .get(`${CASES_URL}/${postedCase.id}/comments/_find`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(caseComments.comments).to.eql(patchedCase.comments); + }); + + it('should filter case comments', async () => { + const { body: postedCase } = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCaseReq) + .expect(200); + + // post 2 comments + await supertest + .post(`${CASES_URL}/${postedCase.id}/comments`) + .set('kbn-xsrf', 'true') + .send(postCommentUserReq) + .expect(200); + + const { body: patchedCase } = await supertest + .post(`${CASES_URL}/${postedCase.id}/comments`) + .set('kbn-xsrf', 'true') + .send({ ...postCommentUserReq, comment: 'unique' }) + .expect(200); + + const { body: caseComments } = await supertest + .get(`${CASES_URL}/${postedCase.id}/comments/_find?search=unique`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(caseComments.comments).to.eql([patchedCase.comments[1]]); + }); + + it('unhappy path - 400s when query is bad', async () => { + const { body: postedCase } = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCaseReq) + .expect(200); + + await supertest + .post(`${CASES_URL}/${postedCase.id}/comments`) + .set('kbn-xsrf', 'true') + .send(postCommentUserReq) + .expect(200); + + await supertest + .get(`${CASES_URL}/${postedCase.id}/comments/_find?perPage=true`) + .set('kbn-xsrf', 'true') + .send() + .expect(400); + }); + + it('should return a 400 when passing the subCaseId parameter', async () => { + const { body } = await supertest + .get(`${CASES_URL}/case-id/comments/_find?search=unique&subCaseId=value`) + .set('kbn-xsrf', 'true') + .send() + .expect(400); + + expect(body.message).to.contain('disabled'); + }); + + // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests + describe.skip('sub case comments', () => { + let actionID: string; + before(async () => { + actionID = await createCaseAction(supertest); + }); + after(async () => { + await deleteCaseAction(supertest, actionID); + }); + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + it('finds comments for a sub case', async () => { + const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); + await supertest + .post(`${CASES_URL}/${caseInfo.id}/comments?subCaseId=${caseInfo.subCases![0].id}`) + .set('kbn-xsrf', 'true') + .send(postCommentUserReq) + .expect(200); + + const { body: subCaseComments }: { body: CommentsResponse } = await supertest + .get(`${CASES_URL}/${caseInfo.id}/comments/_find?subCaseId=${caseInfo.subCases![0].id}`) + .send() + .expect(200); + expect(subCaseComments.total).to.be(2); + expect(subCaseComments.comments[0].type).to.be(CommentType.generatedAlert); + expect(subCaseComments.comments[1].type).to.be(CommentType.user); + }); + }); + + describe('rbac', () => { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + it('should return the correct comments', async () => { + const space1 = 'space1'; + + const [secCase, obsCase] = await Promise.all([ + // Create case owned by the security solution user + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { user: secOnly, space: space1 } + ), + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + { user: obsOnly, space: space1 } + ), + // Create case owned by the observability user + ]); + + await Promise.all([ + createComment({ + supertest: supertestWithoutAuth, + caseId: secCase.id, + params: postCommentUserReq, + auth: { user: secOnly, space: space1 }, + }), + createComment({ + supertest: supertestWithoutAuth, + caseId: obsCase.id, + params: { ...postCommentAlertReq, owner: 'observabilityFixture' }, + auth: { user: obsOnly, space: space1 }, + }), + ]); + + for (const scenario of [ + { + user: globalRead, + numExpectedEntites: 1, + owners: ['securitySolutionFixture', 'observabilityFixture'], + caseID: secCase.id, + }, + { + user: globalRead, + numExpectedEntites: 1, + owners: ['securitySolutionFixture', 'observabilityFixture'], + caseID: obsCase.id, + }, + { + user: superUser, + numExpectedEntites: 1, + owners: ['securitySolutionFixture', 'observabilityFixture'], + caseID: secCase.id, + }, + { + user: superUser, + numExpectedEntites: 1, + owners: ['securitySolutionFixture', 'observabilityFixture'], + caseID: obsCase.id, + }, + { + user: secOnlyRead, + numExpectedEntites: 1, + owners: ['securitySolutionFixture'], + caseID: secCase.id, + }, + { + user: obsOnlyRead, + numExpectedEntites: 1, + owners: ['observabilityFixture'], + caseID: obsCase.id, + }, + { + user: obsSecRead, + numExpectedEntites: 1, + owners: ['securitySolutionFixture', 'observabilityFixture'], + caseID: secCase.id, + }, + { + user: obsSecRead, + numExpectedEntites: 1, + owners: ['securitySolutionFixture', 'observabilityFixture'], + caseID: obsCase.id, + }, + ]) { + const { body: caseComments }: { body: CommentsResponse } = await supertestWithoutAuth + .get(`${getSpaceUrlPrefix(space1)}${CASES_URL}/${scenario.caseID}/comments/_find`) + .auth(scenario.user.username, scenario.user.password) + .expect(200); + + ensureSavedObjectIsAuthorized( + caseComments.comments, + scenario.numExpectedEntites, + scenario.owners + ); + } + }); + + for (const scenario of [ + { user: noKibanaPrivileges, space: 'space1' }, + { user: secOnly, space: 'space2' }, + ]) { + it(`User ${scenario.user.username} with role(s) ${scenario.user.roles.join()} and space ${ + scenario.space + } - should NOT read a comment`, async () => { + // super user creates a case and comment in the appropriate space + const caseInfo = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { user: superUser, space: scenario.space } + ); + + await createComment({ + supertest: supertestWithoutAuth, + auth: { user: superUser, space: scenario.space }, + params: { ...postCommentUserReq, owner: 'securitySolutionFixture' }, + caseId: caseInfo.id, + }); + + // user should not be able to read comments + await supertestWithoutAuth + .get(`${getSpaceUrlPrefix(scenario.space)}${CASES_URL}/${caseInfo.id}/comments/_find`) + .auth(scenario.user.username, scenario.user.password) + .expect(403); + }); + } + + it('should not return any comments when trying to exploit RBAC through the search query parameter', async () => { + const obsCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + superUserSpace1Auth + ); + + await createComment({ + supertest: supertestWithoutAuth, + auth: superUserSpace1Auth, + params: { ...postCommentUserReq, owner: 'observabilityFixture' }, + caseId: obsCase.id, + }); + + const { body: res }: { body: CommentsResponse } = await supertestWithoutAuth + .get( + `${getSpaceUrlPrefix('space1')}${CASES_URL}/${ + obsCase.id + }/comments/_find?search=securitySolutionFixture+observabilityFixture` + ) + .auth(secOnly.username, secOnly.password) + .expect(200); + + // shouldn't find any comments since they were created under the observability ownership + ensureSavedObjectIsAuthorized(res.comments, 0, ['securitySolutionFixture']); + }); + + it('should not allow retrieving unauthorized comments using the filter field', async () => { + const obsCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + superUserSpace1Auth + ); + + await createComment({ + supertest: supertestWithoutAuth, + auth: superUserSpace1Auth, + params: { ...postCommentUserReq, owner: 'observabilityFixture' }, + caseId: obsCase.id, + }); + + const { body: res } = await supertestWithoutAuth + .get( + `${getSpaceUrlPrefix('space1')}${CASES_URL}/${ + obsCase.id + }/comments/_find?filter=cases-comments.attributes.owner:"observabilityFixture"` + ) + .auth(secOnly.username, secOnly.password) + .expect(200); + expect(res.comments.length).to.be(0); + }); + + // This test ensures that the user is not allowed to define the namespaces query param + // so she cannot search across spaces + it('should NOT allow to pass a namespaces query parameter', async () => { + const obsCase = await createCase( + supertest, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200 + ); + + await createComment({ + supertest, + params: { ...postCommentUserReq, owner: 'observabilityFixture' }, + caseId: obsCase.id, + }); + + await supertest + .get(`${CASES_URL}/${obsCase.id}/comments/_find?namespaces[0]=*`) + .expect(400); + + await supertest.get(`${CASES_URL}/${obsCase.id}/comments/_find?namespaces=*`).expect(400); + }); + + it('should NOT allow to pass a non supported query parameter', async () => { + await supertest.get(`${CASES_URL}/id/comments/_find?notExists=papa`).expect(400); + await supertest.get(`${CASES_URL}/id/comments/_find?owner=papa`).expect(400); + }); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/spaces_only/tests/common/comments/get_all_comments.ts b/x-pack/test/case_api_integration/spaces_only/tests/common/comments/get_all_comments.ts new file mode 100644 index 00000000000000..361e72bdc79bf6 --- /dev/null +++ b/x-pack/test/case_api_integration/spaces_only/tests/common/comments/get_all_comments.ts @@ -0,0 +1,231 @@ +/* + * 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 { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; +import { postCaseReq, getPostCaseRequest, postCommentUserReq } from '../../../../common/lib/mock'; +import { + createCaseAction, + createSubCase, + deleteAllCaseItems, + deleteCaseAction, + createCase, + createComment, + getAllComments, +} from '../../../../common/lib/utils'; +import { CommentType } from '../../../../../../plugins/cases/common/api'; +import { + globalRead, + noKibanaPrivileges, + obsOnly, + obsOnlyRead, + obsSec, + obsSecRead, + secOnly, + secOnlyRead, + superUser, +} from '../../../../common/lib/authentication/users'; +import { superUserSpace1Auth } from '../../../../common/lib/authentication'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + + describe('get_all_comments', () => { + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + it('should get multiple comments for a single case', async () => { + const postedCase = await createCase(supertest, postCaseReq); + await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + }); + await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + }); + const comments = await getAllComments({ supertest, caseId: postedCase.id }); + + expect(comments.length).to.eql(2); + }); + + it('should return a 400 when passing the subCaseId parameter', async () => { + const { body } = await supertest + .get(`${CASES_URL}/case-id/comments?subCaseId=value`) + .set('kbn-xsrf', 'true') + .send() + .expect(400); + + expect(body.message).to.contain('disabled'); + }); + + it('should return a 400 when passing the includeSubCaseComments parameter', async () => { + const { body } = await supertest + .get(`${CASES_URL}/case-id/comments?includeSubCaseComments=true`) + .set('kbn-xsrf', 'true') + .send() + .expect(400); + + expect(body.message).to.contain('disabled'); + }); + + // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests + describe.skip('sub cases', () => { + let actionID: string; + before(async () => { + actionID = await createCaseAction(supertest); + }); + after(async () => { + await deleteCaseAction(supertest, actionID); + }); + + it('should get comments from a case and its sub cases', async () => { + const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); + await supertest + .post(`${CASES_URL}/${caseInfo.id}/comments`) + .set('kbn-xsrf', 'true') + .send(postCommentUserReq) + .expect(200); + + const { body: comments } = await supertest + .get(`${CASES_URL}/${caseInfo.id}/comments?includeSubCaseComments=true`) + .expect(200); + + expect(comments.length).to.eql(2); + expect(comments[0].type).to.eql(CommentType.generatedAlert); + expect(comments[1].type).to.eql(CommentType.user); + }); + + it('should get comments from a sub cases', async () => { + const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); + await supertest + .post(`${CASES_URL}/${caseInfo.subCases![0].id}/comments`) + .set('kbn-xsrf', 'true') + .send(postCommentUserReq) + .expect(200); + + const { body: comments } = await supertest + .get(`${CASES_URL}/${caseInfo.id}/comments?subCaseId=${caseInfo.subCases![0].id}`) + .expect(200); + + expect(comments.length).to.eql(2); + expect(comments[0].type).to.eql(CommentType.generatedAlert); + expect(comments[1].type).to.eql(CommentType.user); + }); + + it('should not find any comments for an invalid case id', async () => { + const { body } = await supertest + .get(`${CASES_URL}/fake-id/comments`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + expect(body.length).to.eql(0); + }); + }); + + describe('rbac', () => { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + it('should get all comments when the user has the correct permissions', async () => { + const caseInfo = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + superUserSpace1Auth + ); + + await createComment({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + params: postCommentUserReq, + auth: superUserSpace1Auth, + }); + + await createComment({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + params: postCommentUserReq, + auth: superUserSpace1Auth, + }); + + for (const user of [globalRead, superUser, secOnly, secOnlyRead, obsSec, obsSecRead]) { + const comments = await getAllComments({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + auth: { user, space: 'space1' }, + }); + + expect(comments.length).to.eql(2); + } + }); + + it('should not get comments when the user does not have correct permission', async () => { + const caseInfo = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + superUserSpace1Auth + ); + + await createComment({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + params: postCommentUserReq, + auth: superUserSpace1Auth, + }); + + for (const scenario of [ + { user: noKibanaPrivileges, returnCode: 403 }, + { user: obsOnly, returnCode: 200 }, + { user: obsOnlyRead, returnCode: 200 }, + ]) { + const comments = await getAllComments({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + auth: { user: scenario.user, space: 'space1' }, + expectedHttpCode: scenario.returnCode, + }); + + // only check the length if we get a 200 in response + if (scenario.returnCode === 200) { + expect(comments.length).to.be(0); + } + } + }); + + it('should NOT get a comment in a space with no permissions', async () => { + const caseInfo = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { user: superUser, space: 'space2' } + ); + + await createComment({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + params: postCommentUserReq, + auth: { user: superUser, space: 'space2' }, + }); + + await getAllComments({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + auth: { user: secOnly, space: 'space2' }, + expectedHttpCode: 403, + }); + }); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/spaces_only/tests/common/comments/get_comment.ts b/x-pack/test/case_api_integration/spaces_only/tests/common/comments/get_comment.ts new file mode 100644 index 00000000000000..98b6cc5a7a30c5 --- /dev/null +++ b/x-pack/test/case_api_integration/spaces_only/tests/common/comments/get_comment.ts @@ -0,0 +1,169 @@ +/* + * 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 { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +import { postCaseReq, postCommentUserReq, getPostCaseRequest } from '../../../../common/lib/mock'; +import { + createCaseAction, + createSubCase, + deleteAllCaseItems, + deleteCaseAction, + createCase, + createComment, + getComment, +} from '../../../../common/lib/utils'; +import { CommentType } from '../../../../../../plugins/cases/common/api'; +import { + globalRead, + noKibanaPrivileges, + obsOnly, + obsOnlyRead, + obsSec, + obsSecRead, + secOnly, + secOnlyRead, + superUser, +} from '../../../../common/lib/authentication/users'; +import { superUserSpace1Auth } from '../../../../common/lib/authentication'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + + describe('get_comment', () => { + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + it('should get a comment', async () => { + const postedCase = await createCase(supertest, postCaseReq); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + }); + const comment = await getComment({ + supertest, + caseId: postedCase.id, + commentId: patchedCase.comments![0].id, + }); + + expect(comment).to.eql(patchedCase.comments![0]); + }); + + it('unhappy path - 404s when comment is not there', async () => { + await getComment({ + supertest, + caseId: 'fake-id', + commentId: 'fake-id', + expectedHttpCode: 404, + }); + }); + + // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests + describe.skip('sub cases', () => { + let actionID: string; + before(async () => { + actionID = await createCaseAction(supertest); + }); + after(async () => { + await deleteCaseAction(supertest, actionID); + }); + it('should get a sub case comment', async () => { + const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); + const comment = await getComment({ + supertest, + caseId: caseInfo.id, + commentId: caseInfo.comments![0].id, + }); + expect(comment.type).to.be(CommentType.generatedAlert); + }); + }); + + describe('rbac', () => { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + it('should get a comment when the user has the correct permissions', async () => { + const caseInfo = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + superUserSpace1Auth + ); + + const caseWithComment = await createComment({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + params: postCommentUserReq, + auth: superUserSpace1Auth, + }); + + for (const user of [globalRead, superUser, secOnly, secOnlyRead, obsSec, obsSecRead]) { + await getComment({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + commentId: caseWithComment.comments![0].id, + auth: { user, space: 'space1' }, + }); + } + }); + + it('should not get comment when the user does not have correct permissions', async () => { + const caseInfo = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + superUserSpace1Auth + ); + + const caseWithComment = await createComment({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + params: postCommentUserReq, + auth: superUserSpace1Auth, + }); + + for (const user of [noKibanaPrivileges, obsOnly, obsOnlyRead]) { + await getComment({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + commentId: caseWithComment.comments![0].id, + auth: { user, space: 'space1' }, + expectedHttpCode: 403, + }); + } + }); + + it('should NOT get a case in a space with no permissions', async () => { + const caseInfo = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { user: superUser, space: 'space2' } + ); + + const caseWithComment = await createComment({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + params: postCommentUserReq, + auth: { user: superUser, space: 'space2' }, + }); + + await getComment({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + commentId: caseWithComment.comments![0].id, + auth: { user: secOnly, space: 'space2' }, + expectedHttpCode: 403, + }); + }); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/spaces_only/tests/common/comments/migrations.ts b/x-pack/test/case_api_integration/spaces_only/tests/common/comments/migrations.ts new file mode 100644 index 00000000000000..50a219c5e84b30 --- /dev/null +++ b/x-pack/test/case_api_integration/spaces_only/tests/common/comments/migrations.ts @@ -0,0 +1,37 @@ +/* + * 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 { FtrProviderContext } from '../../../../../common/ftr_provider_context'; +import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; + +// eslint-disable-next-line import/no-default-export +export default function createGetTests({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + describe('migrations', () => { + before(async () => { + await esArchiver.load('cases/migrations/7.10.0'); + }); + + after(async () => { + await esArchiver.unload('cases/migrations/7.10.0'); + }); + + it('7.11.0 migrates cases comments', async () => { + const { body: comment } = await supertest + .get( + `${CASES_URL}/e1900ac0-017f-11eb-93f8-d161651bf509/comments/da677740-1ac7-11eb-b5a3-25ee88122510` + ) + .set('kbn-xsrf', 'true') + .send(); + + expect(comment.type).to.eql('user'); + }); + }); +} diff --git a/x-pack/test/case_api_integration/spaces_only/tests/common/comments/patch_comment.ts b/x-pack/test/case_api_integration/spaces_only/tests/common/comments/patch_comment.ts new file mode 100644 index 00000000000000..c1f37d5eb2f057 --- /dev/null +++ b/x-pack/test/case_api_integration/spaces_only/tests/common/comments/patch_comment.ts @@ -0,0 +1,641 @@ +/* + * 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 { omit } from 'lodash/fp'; +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; +import { + AttributesTypeAlerts, + AttributesTypeUser, + CaseResponse, + CommentType, +} from '../../../../../../plugins/cases/common/api'; +import { + defaultUser, + postCaseReq, + postCommentUserReq, + postCommentAlertReq, + getPostCaseRequest, +} from '../../../../common/lib/mock'; +import { + createCaseAction, + createSubCase, + deleteAllCaseItems, + deleteCaseAction, + deleteCasesByESQuery, + deleteCasesUserActions, + deleteComments, + createCase, + createComment, + updateComment, +} from '../../../../common/lib/utils'; +import { + globalRead, + noKibanaPrivileges, + obsOnly, + obsOnlyRead, + obsSecRead, + secOnly, + secOnlyRead, + superUser, +} from '../../../../common/lib/authentication/users'; +import { superUserSpace1Auth } from '../../../../common/lib/authentication'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + + describe('patch_comment', () => { + afterEach(async () => { + await deleteCasesByESQuery(es); + await deleteComments(es); + await deleteCasesUserActions(es); + }); + + it('should return a 400 when the subCaseId parameter is passed', async () => { + const { body } = await supertest + .patch(`${CASES_URL}/case-id}/comments?subCaseId=value`) + .set('kbn-xsrf', 'true') + .send({ + id: 'id', + version: 'version', + type: CommentType.alert, + alertId: 'test-id', + index: 'test-index', + rule: { + id: 'id', + name: 'name', + }, + owner: 'securitySolutionFixture', + }) + .expect(400); + + expect(body.message).to.contain('disabled'); + }); + + // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests + describe.skip('sub case comments', () => { + let actionID: string; + before(async () => { + actionID = await createCaseAction(supertest); + }); + after(async () => { + await deleteCaseAction(supertest, actionID); + }); + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + it('patches a comment for a sub case', async () => { + const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); + const { body: patchedSubCase }: { body: CaseResponse } = await supertest + .post(`${CASES_URL}/${caseInfo.id}/comments?subCaseId=${caseInfo.subCases![0].id}`) + .set('kbn-xsrf', 'true') + .send(postCommentUserReq) + .expect(200); + + const newComment = 'Well I decided to update my comment. So what? Deal with it.'; + const { body: patchedSubCaseUpdatedComment } = await supertest + .patch(`${CASES_URL}/${caseInfo.id}/comments?subCaseId=${caseInfo.subCases![0].id}`) + .set('kbn-xsrf', 'true') + .send({ + id: patchedSubCase.comments![1].id, + version: patchedSubCase.comments![1].version, + comment: newComment, + type: CommentType.user, + }) + .expect(200); + + expect(patchedSubCaseUpdatedComment.comments.length).to.be(2); + expect(patchedSubCaseUpdatedComment.comments[0].type).to.be(CommentType.generatedAlert); + expect(patchedSubCaseUpdatedComment.comments[1].type).to.be(CommentType.user); + expect(patchedSubCaseUpdatedComment.comments[1].comment).to.be(newComment); + }); + + it('fails to update the generated alert comment type', async () => { + const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); + await supertest + .patch(`${CASES_URL}/${caseInfo.id}/comments?subCaseId=${caseInfo.subCases![0].id}`) + .set('kbn-xsrf', 'true') + .send({ + id: caseInfo.comments![0].id, + version: caseInfo.comments![0].version, + type: CommentType.alert, + alertId: 'test-id', + index: 'test-index', + rule: { + id: 'id', + name: 'name', + }, + }) + .expect(400); + }); + + it('fails to update the generated alert comment by using another generated alert comment', async () => { + const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); + await supertest + .patch(`${CASES_URL}/${caseInfo.id}/comments?subCaseId=${caseInfo.subCases![0].id}`) + .set('kbn-xsrf', 'true') + .send({ + id: caseInfo.comments![0].id, + version: caseInfo.comments![0].version, + type: CommentType.generatedAlert, + alerts: [{ _id: 'id1' }], + index: 'test-index', + rule: { + id: 'id', + name: 'name', + }, + }) + .expect(400); + }); + }); + + it('should patch a comment', async () => { + const postedCase = await createCase(supertest, postCaseReq); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + }); + + const newComment = 'Well I decided to update my comment. So what? Deal with it.'; + const updatedCase = await updateComment({ + supertest, + caseId: postedCase.id, + req: { + id: patchedCase.comments![0].id, + version: patchedCase.comments![0].version, + comment: newComment, + type: CommentType.user, + owner: 'securitySolutionFixture', + }, + }); + + const userComment = updatedCase.comments![0] as AttributesTypeUser; + expect(userComment.comment).to.eql(newComment); + expect(userComment.type).to.eql(CommentType.user); + expect(updatedCase.updated_by).to.eql(defaultUser); + }); + + it('should patch an alert', async () => { + const postedCase = await createCase(supertest, postCaseReq); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentAlertReq, + }); + const updatedCase = await updateComment({ + supertest, + caseId: postedCase.id, + req: { + id: patchedCase.comments![0].id, + version: patchedCase.comments![0].version, + type: CommentType.alert, + alertId: 'new-id', + index: postCommentAlertReq.index, + rule: { + id: 'id', + name: 'name', + }, + owner: 'securitySolutionFixture', + }, + }); + + const alertComment = updatedCase.comments![0] as AttributesTypeAlerts; + expect(alertComment.alertId).to.eql('new-id'); + expect(alertComment.index).to.eql(postCommentAlertReq.index); + expect(alertComment.type).to.eql(CommentType.alert); + expect(alertComment.rule).to.eql({ + id: 'id', + name: 'name', + }); + expect(alertComment.updated_by).to.eql(defaultUser); + }); + + it('should not allow updating the owner of a comment', async () => { + const postedCase = await createCase(supertest, postCaseReq); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + }); + + await updateComment({ + supertest, + caseId: postedCase.id, + req: { + id: patchedCase.comments![0].id, + version: patchedCase.comments![0].version, + type: CommentType.user, + comment: postCommentUserReq.comment, + owner: 'changedOwner', + }, + expectedHttpCode: 400, + }); + }); + + it('unhappy path - 404s when comment is not there', async () => { + const postedCase = await createCase(supertest, postCaseReq); + await updateComment({ + supertest, + caseId: postedCase.id, + req: { + id: 'id', + version: 'version', + type: CommentType.user, + comment: 'comment', + owner: 'securitySolutionFixture', + }, + expectedHttpCode: 404, + }); + }); + + it('unhappy path - 404s when case is not there', async () => { + await updateComment({ + supertest, + caseId: 'fake-id', + req: { + id: 'id', + version: 'version', + type: CommentType.user, + comment: 'comment', + owner: 'securitySolutionFixture', + }, + expectedHttpCode: 404, + }); + }); + + it('unhappy path - 400s when trying to change comment type', async () => { + const postedCase = await createCase(supertest, postCaseReq); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + }); + + await updateComment({ + supertest, + caseId: postedCase.id, + req: { + id: patchedCase.comments![0].id, + version: patchedCase.comments![0].version, + type: CommentType.alert, + alertId: 'test-id', + index: 'test-index', + rule: { + id: 'id', + name: 'name', + }, + owner: 'securitySolutionFixture', + }, + expectedHttpCode: 400, + }); + }); + + it('unhappy path - 400s when missing attributes for type user', async () => { + const postedCase = await createCase(supertest, postCaseReq); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + }); + + await updateComment({ + supertest, + caseId: postedCase.id, + // @ts-expect-error + req: { + id: patchedCase.comments![0].id, + version: patchedCase.comments![0].version, + }, + expectedHttpCode: 400, + }); + }); + + it('unhappy path - 400s when adding excess attributes for type user', async () => { + const postedCase = await createCase(supertest, postCaseReq); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + }); + + for (const attribute of ['alertId', 'index']) { + await updateComment({ + supertest, + caseId: postedCase.id, + req: { + id: patchedCase.comments![0].id, + version: patchedCase.comments![0].version, + comment: 'a comment', + type: CommentType.user, + [attribute]: attribute, + owner: 'securitySolutionFixture', + }, + expectedHttpCode: 400, + }); + } + }); + + it('unhappy path - 400s when missing attributes for type alert', async () => { + const postedCase = await createCase(supertest, postCaseReq); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentAlertReq, + }); + + const allRequestAttributes = { + type: CommentType.alert, + index: 'test-index', + alertId: 'test-id', + rule: { + id: 'id', + name: 'name', + }, + }; + + for (const attribute of ['alertId', 'index']) { + const requestAttributes = omit(attribute, allRequestAttributes); + await updateComment({ + supertest, + caseId: postedCase.id, + // @ts-expect-error + req: { + id: patchedCase.comments![0].id, + version: patchedCase.comments![0].version, + ...requestAttributes, + }, + expectedHttpCode: 400, + }); + } + }); + + it('unhappy path - 400s when adding excess attributes for type alert', async () => { + const postedCase = await createCase(supertest, postCaseReq); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentAlertReq, + }); + + for (const attribute of ['comment']) { + await updateComment({ + supertest, + caseId: postedCase.id, + req: { + id: patchedCase.comments![0].id, + version: patchedCase.comments![0].version, + type: CommentType.alert, + index: 'test-index', + alertId: 'test-id', + rule: { + id: 'id', + name: 'name', + }, + owner: 'securitySolutionFixture', + [attribute]: attribute, + }, + expectedHttpCode: 400, + }); + } + }); + + it('unhappy path - 409s when conflict', async () => { + const postedCase = await createCase(supertest, postCaseReq); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + }); + + const newComment = 'Well I decided to update my comment. So what? Deal with it.'; + await updateComment({ + supertest, + caseId: postedCase.id, + req: { + id: patchedCase.comments![0].id, + version: 'version-mismatch', + type: CommentType.user, + comment: newComment, + owner: 'securitySolutionFixture', + }, + expectedHttpCode: 409, + }); + }); + + describe('alert format', () => { + type AlertComment = CommentType.alert | CommentType.generatedAlert; + + // ENABLE_CASE_CONNECTOR: once the case connector feature is completed create a test case for generated alerts here + for (const [alertId, index, type] of [ + ['1', ['index1', 'index2'], CommentType.alert], + [['1', '2'], 'index', CommentType.alert], + ]) { + it(`throws an error with an alert comment with contents id: ${alertId} indices: ${index} type: ${type}`, async () => { + const postedCase = await createCase(supertest, postCaseReq); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentAlertReq, + }); + + await updateComment({ + supertest, + caseId: patchedCase.id, + req: { + id: patchedCase.comments![0].id, + version: patchedCase.comments![0].version, + type: type as AlertComment, + alertId, + index, + owner: 'securitySolutionFixture', + rule: postCommentAlertReq.rule, + }, + expectedHttpCode: 400, + }); + }); + } + + for (const [alertId, index, type] of [ + ['1', ['index1'], CommentType.alert], + [['1', '2'], ['index', 'other-index'], CommentType.alert], + ]) { + it(`does not throw an error with an alert comment with contents id: ${alertId} indices: ${index} type: ${type}`, async () => { + const postedCase = await createCase(supertest, postCaseReq); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: { + ...postCommentAlertReq, + alertId, + index, + owner: 'securitySolutionFixture', + type: type as AlertComment, + }, + }); + + await updateComment({ + supertest, + caseId: postedCase.id, + req: { + id: patchedCase.comments![0].id, + version: patchedCase.comments![0].version, + type: type as AlertComment, + alertId, + index, + owner: 'securitySolutionFixture', + rule: postCommentAlertReq.rule, + }, + }); + }); + } + }); + + describe('rbac', () => { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + it('should update a comment that the user has permissions for', async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + superUserSpace1Auth + ); + + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + auth: superUserSpace1Auth, + }); + + const newComment = 'Well I decided to update my comment. So what? Deal with it.'; + const updatedCase = await updateComment({ + supertest, + caseId: postedCase.id, + req: { + ...postCommentUserReq, + id: patchedCase.comments![0].id, + version: patchedCase.comments![0].version, + comment: newComment, + }, + auth: { user: secOnly, space: 'space1' }, + }); + + const userComment = updatedCase.comments![0] as AttributesTypeUser; + expect(userComment.comment).to.eql(newComment); + expect(userComment.type).to.eql(CommentType.user); + expect(updatedCase.updated_by).to.eql(defaultUser); + expect(userComment.owner).to.eql('securitySolutionFixture'); + }); + + it('should not update a comment that has a different owner thant he user has access to', async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + superUserSpace1Auth + ); + + const patchedCase = await createComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + params: postCommentUserReq, + auth: superUserSpace1Auth, + }); + + const newComment = 'Well I decided to update my comment. So what? Deal with it.'; + await updateComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + req: { + ...postCommentUserReq, + id: patchedCase.comments![0].id, + version: patchedCase.comments![0].version, + comment: newComment, + }, + 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 update a comment`, async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + superUserSpace1Auth + ); + + const patchedCase = await createComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + params: postCommentUserReq, + auth: superUserSpace1Auth, + }); + + const newComment = 'Well I decided to update my comment. So what? Deal with it.'; + await updateComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + req: { + ...postCommentUserReq, + id: patchedCase.comments![0].id, + version: patchedCase.comments![0].version, + comment: newComment, + }, + auth: { user, space: 'space1' }, + expectedHttpCode: 403, + }); + }); + } + + it('should not update a comment in a space the user does not have permissions', async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { user: superUser, space: 'space2' } + ); + + const patchedCase = await createComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + params: postCommentUserReq, + auth: { user: superUser, space: 'space2' }, + }); + + const newComment = 'Well I decided to update my comment. So what? Deal with it.'; + await updateComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + req: { + ...postCommentUserReq, + id: patchedCase.comments![0].id, + version: patchedCase.comments![0].version, + comment: newComment, + }, + auth: { user: secOnly, space: 'space2' }, + // getting the case will fail in the saved object layer with a 403 + expectedHttpCode: 403, + }); + }); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/spaces_only/tests/common/comments/post_comment.ts b/x-pack/test/case_api_integration/spaces_only/tests/common/comments/post_comment.ts new file mode 100644 index 00000000000000..1fcb49ec10ad4e --- /dev/null +++ b/x-pack/test/case_api_integration/spaces_only/tests/common/comments/post_comment.ts @@ -0,0 +1,605 @@ +/* + * 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 { omit } from 'lodash/fp'; +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; +import { DETECTION_ENGINE_QUERY_SIGNALS_URL } from '../../../../../../plugins/security_solution/common/constants'; +import { + CommentsResponse, + CommentType, + AttributesTypeUser, + AttributesTypeAlerts, +} from '../../../../../../plugins/cases/common/api'; +import { + defaultUser, + postCaseReq, + postCommentUserReq, + postCommentAlertReq, + postCollectionReq, + postCommentGenAlertReq, + getPostCaseRequest, +} from '../../../../common/lib/mock'; +import { + createCaseAction, + createSubCase, + deleteAllCaseItems, + deleteCaseAction, + deleteCasesByESQuery, + deleteCasesUserActions, + deleteComments, + createCase, + createComment, + getAllUserAction, + removeServerGeneratedPropertiesFromUserAction, + removeServerGeneratedPropertiesFromSavedObject, +} from '../../../../common/lib/utils'; +import { + createSignalsIndex, + deleteSignalsIndex, + deleteAllAlerts, + getRuleForSignalTesting, + waitForRuleSuccessOrStatus, + waitForSignalsToBePresent, + getSignalsByIds, + createRule, + getQuerySignalIds, +} from '../../../../../detection_engine_api_integration/utils'; +import { + globalRead, + noKibanaPrivileges, + obsOnly, + obsOnlyRead, + obsSecRead, + secOnly, + secOnlyRead, + superUser, +} from '../../../../common/lib/authentication/users'; +import { superUserSpace1Auth } from '../../../../common/lib/authentication'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + const es = getService('es'); + + describe('post_comment', () => { + afterEach(async () => { + await deleteCasesByESQuery(es); + await deleteComments(es); + await deleteCasesUserActions(es); + }); + + describe('happy path', () => { + it('should post a comment', async () => { + const postedCase = await createCase(supertest, postCaseReq); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + }); + const comment = removeServerGeneratedPropertiesFromSavedObject( + patchedCase.comments![0] as AttributesTypeUser + ); + + expect(comment).to.eql({ + type: postCommentUserReq.type, + comment: postCommentUserReq.comment, + associationType: 'case', + created_by: defaultUser, + pushed_at: null, + pushed_by: null, + updated_by: null, + owner: 'securitySolutionFixture', + }); + + // updates the case correctly after adding a comment + expect(patchedCase.totalComment).to.eql(patchedCase.comments!.length); + expect(patchedCase.updated_by).to.eql(defaultUser); + }); + + it('should post an alert', async () => { + const postedCase = await createCase(supertest, postCaseReq); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentAlertReq, + }); + const comment = removeServerGeneratedPropertiesFromSavedObject( + patchedCase.comments![0] as AttributesTypeAlerts + ); + + expect(comment).to.eql({ + type: postCommentAlertReq.type, + alertId: postCommentAlertReq.alertId, + index: postCommentAlertReq.index, + rule: postCommentAlertReq.rule, + associationType: 'case', + created_by: defaultUser, + pushed_at: null, + pushed_by: null, + updated_by: null, + owner: 'securitySolutionFixture', + }); + + // updates the case correctly after adding a comment + expect(patchedCase.totalComment).to.eql(patchedCase.comments!.length); + expect(patchedCase.updated_by).to.eql(defaultUser); + }); + + it('creates a user action', async () => { + const postedCase = await createCase(supertest, postCaseReq); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + }); + const userActions = await getAllUserAction(supertest, postedCase.id); + const commentUserAction = removeServerGeneratedPropertiesFromUserAction(userActions[1]); + + expect(commentUserAction).to.eql({ + action_field: ['comment'], + action: 'create', + action_by: defaultUser, + new_value: `{"comment":"${postCommentUserReq.comment}","type":"${postCommentUserReq.type}","owner":"securitySolutionFixture"}`, + old_value: null, + case_id: `${postedCase.id}`, + comment_id: `${patchedCase.comments![0].id}`, + sub_case_id: '', + owner: 'securitySolutionFixture', + }); + }); + }); + + describe('unhappy path', () => { + it('400s when attempting to create a comment with a different owner than the case', async () => { + const postedCase = await createCase( + supertest, + getPostCaseRequest({ owner: 'securitySolutionFixture' }) + ); + + await createComment({ + supertest, + caseId: postedCase.id, + params: { ...postCommentUserReq, owner: 'observabilityFixture' }, + expectedHttpCode: 400, + }); + }); + + it('400s when type is missing', async () => { + const postedCase = await createCase(supertest, postCaseReq); + await createComment({ + supertest, + caseId: postedCase.id, + params: { + // @ts-expect-error + bad: 'comment', + }, + expectedHttpCode: 400, + }); + }); + + it('400s when missing attributes for type user', async () => { + const postedCase = await createCase(supertest, postCaseReq); + await createComment({ + supertest, + caseId: postedCase.id, + // @ts-expect-error + params: { + type: CommentType.user, + }, + expectedHttpCode: 400, + }); + }); + + it('400s when adding excess attributes for type user', async () => { + const postedCase = await createCase(supertest, postCaseReq); + + for (const attribute of ['alertId', 'index']) { + await createComment({ + supertest, + caseId: postedCase.id, + params: { + type: CommentType.user, + [attribute]: attribute, + comment: 'a comment', + owner: 'securitySolutionFixture', + }, + expectedHttpCode: 400, + }); + } + }); + + it('400s when missing attributes for type alert', async () => { + const postedCase = await createCase(supertest, postCaseReq); + + const allRequestAttributes = { + type: CommentType.alert, + index: 'test-index', + alertId: 'test-id', + rule: { + id: 'id', + name: 'name', + }, + owner: 'securitySolutionFixture', + }; + + for (const attribute of ['alertId', 'index']) { + const requestAttributes = omit(attribute, allRequestAttributes); + await createComment({ + supertest, + caseId: postedCase.id, + // @ts-expect-error + params: requestAttributes, + expectedHttpCode: 400, + }); + } + }); + + it('400s when adding excess attributes for type alert', async () => { + const postedCase = await createCase(supertest, postCaseReq); + + for (const attribute of ['comment']) { + await createComment({ + supertest, + caseId: postedCase.id, + params: { + type: CommentType.alert, + [attribute]: attribute, + alertId: 'test-id', + index: 'test-index', + rule: { + id: 'id', + name: 'name', + }, + owner: 'securitySolutionFixture', + }, + expectedHttpCode: 400, + }); + } + }); + + it('400s when case is missing', async () => { + await createComment({ + supertest, + caseId: 'not-exists', + params: { + // @ts-expect-error + bad: 'comment', + }, + expectedHttpCode: 400, + }); + }); + + it('400s when adding an alert to a closed case', async () => { + const postedCase = await createCase(supertest, postCaseReq); + + await supertest + .patch(CASES_URL) + .set('kbn-xsrf', 'true') + .send({ + cases: [ + { + id: postedCase.id, + version: postedCase.version, + status: 'closed', + }, + ], + }) + .expect(200); + + await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentAlertReq, + expectedHttpCode: 400, + }); + }); + + // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests + it.skip('400s when adding an alert to a collection case', async () => { + const postedCase = await createCase(supertest, postCollectionReq); + await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentAlertReq, + expectedHttpCode: 400, + }); + }); + + it('400s when adding a generated alert to an individual case', async () => { + const postedCase = await createCase(supertest, postCaseReq); + + await supertest + .post(`${CASES_URL}/${postedCase.id}/comments`) + .set('kbn-xsrf', 'true') + .send(postCommentGenAlertReq) + .expect(400); + }); + + it('should return a 400 when passing the subCaseId', async () => { + const { body } = await supertest + .post(`${CASES_URL}/case-id/comments?subCaseId=value`) + .set('kbn-xsrf', 'true') + .send(postCommentUserReq) + .expect(400); + expect(body.message).to.contain('subCaseId'); + }); + }); + + describe('alerts', () => { + beforeEach(async () => { + await esArchiver.load('auditbeat/hosts'); + await createSignalsIndex(supertest); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest); + await deleteAllAlerts(supertest); + await esArchiver.unload('auditbeat/hosts'); + }); + + it('should change the status of the alert if sync alert is on', async () => { + const rule = getRuleForSignalTesting(['auditbeat-*']); + const postedCase = await createCase(supertest, postCaseReq); + + await supertest + .patch(CASES_URL) + .set('kbn-xsrf', 'true') + .send({ + cases: [ + { + id: postedCase.id, + version: postedCase.version, + status: 'in-progress', + }, + ], + }) + .expect(200); + + const { id } = await createRule(supertest, rule); + await waitForRuleSuccessOrStatus(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signals = await getSignalsByIds(supertest, [id]); + + const alert = signals.hits.hits[0]; + expect(alert._source.signal.status).eql('open'); + + await createComment({ + supertest, + caseId: postedCase.id, + params: { + alertId: alert._id, + index: alert._index, + rule: { + id: 'id', + name: 'name', + }, + owner: 'securitySolutionFixture', + type: CommentType.alert, + }, + }); + + const { body: updatedAlert } = await supertest + .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) + .set('kbn-xsrf', 'true') + .send(getQuerySignalIds([alert._id])) + .expect(200); + + expect(updatedAlert.hits.hits[0]._source.signal.status).eql('in-progress'); + }); + + it('should NOT change the status of the alert if sync alert is off', async () => { + const rule = getRuleForSignalTesting(['auditbeat-*']); + const postedCase = await createCase(supertest, { + ...postCaseReq, + settings: { syncAlerts: false }, + }); + + await supertest + .patch(CASES_URL) + .set('kbn-xsrf', 'true') + .send({ + cases: [ + { + id: postedCase.id, + version: postedCase.version, + status: 'in-progress', + }, + ], + }) + .expect(200); + + const { id } = await createRule(supertest, rule); + await waitForRuleSuccessOrStatus(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signals = await getSignalsByIds(supertest, [id]); + + const alert = signals.hits.hits[0]; + expect(alert._source.signal.status).eql('open'); + + await createComment({ + supertest, + caseId: postedCase.id, + params: { + alertId: alert._id, + index: alert._index, + rule: { + id: 'id', + name: 'name', + }, + owner: 'securitySolutionFixture', + type: CommentType.alert, + }, + }); + + const { body: updatedAlert } = await supertest + .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) + .set('kbn-xsrf', 'true') + .send(getQuerySignalIds([alert._id])) + .expect(200); + + expect(updatedAlert.hits.hits[0]._source.signal.status).eql('open'); + }); + }); + + describe('alert format', () => { + type AlertComment = CommentType.alert | CommentType.generatedAlert; + + for (const [alertId, index, type] of [ + ['1', ['index1', 'index2'], CommentType.alert], + [['1', '2'], 'index', CommentType.alert], + ['1', ['index1', 'index2'], CommentType.generatedAlert], + [['1', '2'], 'index', CommentType.generatedAlert], + ]) { + it(`throws an error with an alert comment with contents id: ${alertId} indices: ${index} type: ${type}`, async () => { + const postedCase = await createCase(supertest, postCaseReq); + await createComment({ + supertest, + caseId: postedCase.id, + params: { ...postCommentAlertReq, alertId, index, type: type as AlertComment }, + expectedHttpCode: 400, + }); + }); + } + + for (const [alertId, index, type] of [ + ['1', ['index1'], CommentType.alert], + [['1', '2'], ['index', 'other-index'], CommentType.alert], + ]) { + it(`does not throw an error with an alert comment with contents id: ${alertId} indices: ${index} type: ${type}`, async () => { + const postedCase = await createCase(supertest, postCaseReq); + await createComment({ + supertest, + caseId: postedCase.id, + params: { + ...postCommentAlertReq, + alertId, + index, + type: type as AlertComment, + }, + expectedHttpCode: 200, + }); + }); + } + }); + + // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests + describe.skip('sub case comments', () => { + let actionID: string; + before(async () => { + actionID = await createCaseAction(supertest); + }); + after(async () => { + await deleteCaseAction(supertest, actionID); + }); + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + it('posts a new comment for a sub case', async () => { + const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); + // create another sub case just to make sure we get the right comments + await createSubCase({ supertest, actionID }); + await supertest + .post(`${CASES_URL}/${caseInfo.id}/comments?subCaseId=${caseInfo.subCases![0].id}`) + .set('kbn-xsrf', 'true') + .send(postCommentUserReq) + .expect(200); + + const { body: subCaseComments }: { body: CommentsResponse } = await supertest + .get(`${CASES_URL}/${caseInfo.id}/comments/_find?subCaseId=${caseInfo.subCases![0].id}`) + .send() + .expect(200); + expect(subCaseComments.total).to.be(2); + expect(subCaseComments.comments[0].type).to.be(CommentType.generatedAlert); + expect(subCaseComments.comments[1].type).to.be(CommentType.user); + }); + }); + + describe('rbac', () => { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + it('should create a comment when the user has the correct permissions for that owner', async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + superUserSpace1Auth + ); + + await createComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + params: postCommentUserReq, + auth: { user: secOnly, space: 'space1' }, + }); + }); + + it('should not create a comment when the user does not have permissions for that owner', async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + { user: obsOnly, space: 'space1' } + ); + + await createComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + params: { ...postCommentUserReq, owner: 'observabilityFixture' }, + auth: { user: secOnly, 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 create a comment`, async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + superUserSpace1Auth + ); + + await createComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + params: postCommentUserReq, + auth: { user, space: 'space1' }, + expectedHttpCode: 403, + }); + }); + } + + it('should not create a comment in a space the user does not have permissions for', 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: secOnly, space: 'space2' }, + expectedHttpCode: 403, + }); + }); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/spaces_only/tests/common/configure/get_configure.ts b/x-pack/test/case_api_integration/spaces_only/tests/common/configure/get_configure.ts new file mode 100644 index 00000000000000..279936ebbef462 --- /dev/null +++ b/x-pack/test/case_api_integration/spaces_only/tests/common/configure/get_configure.ts @@ -0,0 +1,215 @@ +/* + * 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 { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +import { + removeServerGeneratedPropertiesFromSavedObject, + getConfigurationOutput, + deleteConfiguration, + getConfiguration, + createConfiguration, + getConfigurationRequest, + ensureSavedObjectIsAuthorized, +} from '../../../../common/lib/utils'; +import { + obsOnly, + secOnly, + obsOnlyRead, + secOnlyRead, + noKibanaPrivileges, + superUser, + globalRead, + obsSecRead, + obsSec, +} from '../../../../common/lib/authentication/users'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const es = getService('es'); + + describe('get_configure', () => { + afterEach(async () => { + await deleteConfiguration(es); + }); + + it('should return an empty find body correctly if no configuration is loaded', async () => { + const configuration = await getConfiguration({ supertest }); + expect(configuration).to.eql([]); + }); + + it('should return a configuration', async () => { + await createConfiguration(supertest); + const configuration = await getConfiguration({ supertest }); + + const data = removeServerGeneratedPropertiesFromSavedObject(configuration[0]); + expect(data).to.eql(getConfigurationOutput()); + }); + + it('should get a single configuration', async () => { + await createConfiguration(supertest, getConfigurationRequest({ id: 'connector-2' })); + await createConfiguration(supertest); + const res = await getConfiguration({ supertest }); + + expect(res.length).to.eql(1); + const data = removeServerGeneratedPropertiesFromSavedObject(res[0]); + expect(data).to.eql(getConfigurationOutput()); + }); + + it('should return by descending order', async () => { + await createConfiguration(supertest, getConfigurationRequest({ id: 'connector-2' })); + await createConfiguration(supertest); + const res = await getConfiguration({ supertest }); + + const data = removeServerGeneratedPropertiesFromSavedObject(res[0]); + expect(data).to.eql(getConfigurationOutput()); + }); + + describe('rbac', () => { + it('should return the correct configuration', async () => { + await createConfiguration(supertestWithoutAuth, getConfigurationRequest(), 200, { + user: secOnly, + space: 'space1', + }); + + await createConfiguration( + supertestWithoutAuth, + { ...getConfigurationRequest(), owner: 'observabilityFixture' }, + 200, + { + user: obsOnly, + space: 'space1', + } + ); + + for (const scenario of [ + { + user: globalRead, + numberOfExpectedCases: 2, + owners: ['securitySolutionFixture', 'observabilityFixture'], + }, + { + user: superUser, + numberOfExpectedCases: 2, + owners: ['securitySolutionFixture', 'observabilityFixture'], + }, + { user: secOnlyRead, numberOfExpectedCases: 1, owners: ['securitySolutionFixture'] }, + { user: obsOnlyRead, numberOfExpectedCases: 1, owners: ['observabilityFixture'] }, + { + user: obsSecRead, + numberOfExpectedCases: 2, + owners: ['securitySolutionFixture', 'observabilityFixture'], + }, + ]) { + const configuration = await getConfiguration({ + supertest: supertestWithoutAuth, + query: { owner: scenario.owners }, + expectedHttpCode: 200, + auth: { + user: scenario.user, + space: 'space1', + }, + }); + + ensureSavedObjectIsAuthorized( + configuration, + scenario.numberOfExpectedCases, + scenario.owners + ); + } + }); + + for (const scenario of [ + { user: noKibanaPrivileges, space: 'space1' }, + { user: secOnly, space: 'space2' }, + ]) { + it(`User ${scenario.user.username} with role(s) ${scenario.user.roles.join()} and space ${ + scenario.space + } - should NOT read a case configuration`, async () => { + // super user creates a configuration at the appropriate space + await createConfiguration(supertestWithoutAuth, getConfigurationRequest(), 200, { + user: superUser, + space: scenario.space, + }); + + // user should not be able to read configurations at the appropriate space + await getConfiguration({ + supertest: supertestWithoutAuth, + expectedHttpCode: 403, + auth: { + user: scenario.user, + space: scenario.space, + }, + }); + }); + } + + it('should respect the owner filter when having permissions', async () => { + await Promise.all([ + createConfiguration(supertestWithoutAuth, getConfigurationRequest(), 200, { + user: obsSec, + space: 'space1', + }), + createConfiguration( + supertestWithoutAuth, + { ...getConfigurationRequest(), owner: 'observabilityFixture' }, + 200, + { + user: obsSec, + space: 'space1', + } + ), + ]); + + const res = await getConfiguration({ + supertest: supertestWithoutAuth, + query: { owner: 'securitySolutionFixture' }, + auth: { + user: obsSec, + space: 'space1', + }, + }); + + ensureSavedObjectIsAuthorized(res, 1, ['securitySolutionFixture']); + }); + + it('should return the correct cases when trying to exploit RBAC through the owner query parameter', async () => { + await Promise.all([ + createConfiguration(supertestWithoutAuth, getConfigurationRequest(), 200, { + user: obsSec, + space: 'space1', + }), + createConfiguration( + supertestWithoutAuth, + { ...getConfigurationRequest(), owner: 'observabilityFixture' }, + 200, + { + user: obsSec, + space: 'space1', + } + ), + ]); + + // User with permissions only to security solution request cases from observability + const res = await getConfiguration({ + supertest: supertestWithoutAuth, + query: { owner: ['securitySolutionFixture', 'observabilityFixture'] }, + auth: { + user: secOnly, + space: 'space1', + }, + }); + + // Only security solution cases are being returned + ensureSavedObjectIsAuthorized(res, 1, ['securitySolutionFixture']); + }); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/spaces_only/tests/common/configure/get_connectors.ts b/x-pack/test/case_api_integration/spaces_only/tests/common/configure/get_connectors.ts new file mode 100644 index 00000000000000..5156b9537583f3 --- /dev/null +++ b/x-pack/test/case_api_integration/spaces_only/tests/common/configure/get_connectors.ts @@ -0,0 +1,27 @@ +/* + * 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 { FtrProviderContext } from '../../../../../common/ftr_provider_context'; + +import { getCaseConnectors } from '../../../../common/lib/utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + + describe('get_connectors', () => { + it('should return an empty find body correctly if no connectors are loaded', async () => { + const connectors = await getCaseConnectors(supertest); + expect(connectors).to.eql([]); + }); + + it.skip('filters out connectors that are not enabled in license', async () => { + // TODO: Should find a way to downgrade license to gold and upgrade back to trial + }); + }); +}; diff --git a/x-pack/test/case_api_integration/spaces_only/tests/common/configure/migrations.ts b/x-pack/test/case_api_integration/spaces_only/tests/common/configure/migrations.ts new file mode 100644 index 00000000000000..cc2f6c414503d5 --- /dev/null +++ b/x-pack/test/case_api_integration/spaces_only/tests/common/configure/migrations.ts @@ -0,0 +1,44 @@ +/* + * 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 { FtrProviderContext } from '../../../../../common/ftr_provider_context'; +import { CASE_CONFIGURE_URL } from '../../../../../../plugins/cases/common/constants'; + +// eslint-disable-next-line import/no-default-export +export default function createGetTests({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + describe('migrations', () => { + before(async () => { + await esArchiver.load('cases/migrations/7.10.0'); + }); + + after(async () => { + await esArchiver.unload('cases/migrations/7.10.0'); + }); + + it('7.10.0 migrates configure cases connector', async () => { + const { body } = await supertest + .get(`${CASE_CONFIGURE_URL}`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(body.length).to.be(1); + expect(body[0]).key('connector'); + expect(body[0]).not.key('connector_id'); + expect(body[0].connector).to.eql({ + id: 'connector-1', + name: 'Connector 1', + type: '.none', + fields: null, + }); + }); + }); +} diff --git a/x-pack/test/case_api_integration/spaces_only/tests/common/configure/patch_configure.ts b/x-pack/test/case_api_integration/spaces_only/tests/common/configure/patch_configure.ts new file mode 100644 index 00000000000000..ced727f8e4e75e --- /dev/null +++ b/x-pack/test/case_api_integration/spaces_only/tests/common/configure/patch_configure.ts @@ -0,0 +1,243 @@ +/* + * 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 { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { ObjectRemover as ActionsRemover } from '../../../../../alerting_api_integration/common/lib'; + +import { + getConfigurationRequest, + removeServerGeneratedPropertiesFromSavedObject, + getConfigurationOutput, + deleteConfiguration, + createConfiguration, + updateConfiguration, +} from '../../../../common/lib/utils'; +import { + secOnly, + obsOnlyRead, + secOnlyRead, + noKibanaPrivileges, + globalRead, + obsSecRead, + superUser, +} from '../../../../common/lib/authentication/users'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const es = getService('es'); + + describe('patch_configure', () => { + const actionsRemover = new ActionsRemover(supertest); + + afterEach(async () => { + await deleteConfiguration(es); + await actionsRemover.removeAll(); + }); + + it('should patch a configuration', async () => { + const configuration = await createConfiguration(supertest); + const newConfiguration = await updateConfiguration(supertest, configuration.id, { + closure_type: 'close-by-pushing', + version: configuration.version, + }); + + const data = removeServerGeneratedPropertiesFromSavedObject(newConfiguration); + expect(data).to.eql({ ...getConfigurationOutput(true), closure_type: 'close-by-pushing' }); + }); + + it('should not patch a configuration with unsupported connector type', async () => { + const configuration = await createConfiguration(supertest); + await updateConfiguration( + supertest, + configuration.id, + // @ts-expect-error + getConfigurationRequest({ type: '.unsupported' }), + 400 + ); + }); + + it('should not patch a configuration with unsupported connector fields', async () => { + const configuration = await createConfiguration(supertest); + await updateConfiguration( + supertest, + configuration.id, + // @ts-expect-error + getConfigurationRequest({ type: '.jira', fields: { unsupported: 'value' } }), + 400 + ); + }); + + it('should handle patch request when there is no configuration', async () => { + const error = await updateConfiguration( + supertest, + 'not-exist', + { closure_type: 'close-by-pushing', version: 'no-version' }, + 404 + ); + + expect(error).to.eql({ + error: 'Not Found', + message: 'Saved object [cases-configure/not-exist] not found', + statusCode: 404, + }); + }); + + it('should handle patch request when versions are different', async () => { + const configuration = await createConfiguration(supertest); + const error = await updateConfiguration( + supertest, + configuration.id, + { closure_type: 'close-by-pushing', version: 'no-version' }, + 409 + ); + + expect(error).to.eql({ + error: 'Conflict', + message: + 'This configuration has been updated. Please refresh before saving additional updates.', + statusCode: 409, + }); + }); + + it('should not allow to change the owner of the configuration', async () => { + const configuration = await createConfiguration(supertest); + await updateConfiguration( + supertest, + configuration.id, + // @ts-expect-error + { owner: 'observabilityFixture', version: configuration.version }, + 400 + ); + }); + + it('should not allow excess attributes', async () => { + const configuration = await createConfiguration(supertest); + await updateConfiguration( + supertest, + configuration.id, + // @ts-expect-error + { notExist: 'not-exist', version: configuration.version }, + 400 + ); + }); + + describe('rbac', () => { + it('User: security solution only - should update a configuration', async () => { + const configuration = await createConfiguration( + supertestWithoutAuth, + getConfigurationRequest(), + 200, + { + user: secOnly, + space: 'space1', + } + ); + + const newConfiguration = await updateConfiguration( + supertestWithoutAuth, + configuration.id, + { + closure_type: 'close-by-pushing', + version: configuration.version, + }, + 200, + { + user: secOnly, + space: 'space1', + } + ); + + expect(newConfiguration.owner).to.eql('securitySolutionFixture'); + }); + + it('User: security solution only - should NOT update a configuration of different owner', async () => { + const configuration = await createConfiguration( + supertestWithoutAuth, + { ...getConfigurationRequest(), owner: 'observabilityFixture' }, + 200, + { + user: superUser, + space: 'space1', + } + ); + + await updateConfiguration( + supertestWithoutAuth, + configuration.id, + { + closure_type: 'close-by-pushing', + version: configuration.version, + }, + 403, + { + user: secOnly, + space: 'space1', + } + ); + }); + + for (const user of [globalRead, secOnlyRead, obsOnlyRead, obsSecRead, noKibanaPrivileges]) { + it(`User ${ + user.username + } with role(s) ${user.roles.join()} - should NOT update a configuration`, async () => { + const configuration = await createConfiguration( + supertestWithoutAuth, + getConfigurationRequest(), + 200, + { + user: superUser, + space: 'space1', + } + ); + + await updateConfiguration( + supertestWithoutAuth, + configuration.id, + { + closure_type: 'close-by-pushing', + version: configuration.version, + }, + 403, + { + user, + space: 'space1', + } + ); + }); + } + + it('should NOT update a configuration in a space with no permissions', async () => { + const configuration = await createConfiguration( + supertestWithoutAuth, + { ...getConfigurationRequest(), owner: 'securitySolutionFixture' }, + 200, + { + user: superUser, + space: 'space2', + } + ); + + await updateConfiguration( + supertestWithoutAuth, + configuration.id, + { + closure_type: 'close-by-pushing', + version: configuration.version, + }, + 404, + { + user: secOnly, + space: 'space1', + } + ); + }); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/spaces_only/tests/common/configure/post_configure.ts b/x-pack/test/case_api_integration/spaces_only/tests/common/configure/post_configure.ts new file mode 100644 index 00000000000000..f1dae9f319109b --- /dev/null +++ b/x-pack/test/case_api_integration/spaces_only/tests/common/configure/post_configure.ts @@ -0,0 +1,298 @@ +/* + * 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 { ConnectorTypes } from '../../../../../../plugins/cases/common/api'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { ObjectRemover as ActionsRemover } from '../../../../../alerting_api_integration/common/lib'; + +import { + getConfigurationRequest, + removeServerGeneratedPropertiesFromSavedObject, + getConfigurationOutput, + deleteConfiguration, + createConfiguration, + getConfiguration, + ensureSavedObjectIsAuthorized, +} from '../../../../common/lib/utils'; + +import { + secOnly, + obsOnlyRead, + secOnlyRead, + noKibanaPrivileges, + globalRead, + obsSecRead, + superUser, +} from '../../../../common/lib/authentication/users'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const es = getService('es'); + + describe('post_configure', () => { + const actionsRemover = new ActionsRemover(supertest); + + afterEach(async () => { + await deleteConfiguration(es); + await actionsRemover.removeAll(); + }); + + it('should create a configuration', async () => { + const configuration = await createConfiguration(supertest); + + const data = removeServerGeneratedPropertiesFromSavedObject(configuration); + expect(data).to.eql(getConfigurationOutput()); + }); + + it('should keep only the latest configuration', async () => { + await createConfiguration(supertest, getConfigurationRequest({ id: 'connector-2' })); + await createConfiguration(supertest); + const configuration = await getConfiguration({ supertest }); + + expect(configuration.length).to.be(1); + }); + + it('should return an error when failing to get mapping', async () => { + const postRes = await createConfiguration( + supertest, + getConfigurationRequest({ + id: 'not-exists', + name: 'not-exists', + type: ConnectorTypes.jira, + }) + ); + + expect(postRes.error).to.not.be(null); + expect(postRes.mappings).to.eql([]); + }); + + it('should not create a configuration when missing connector.id', async () => { + await createConfiguration( + supertest, + { + // @ts-expect-error + connector: { + name: 'Connector', + type: ConnectorTypes.none, + fields: null, + }, + closure_type: 'close-by-user', + }, + 400 + ); + }); + + it('should not create a configuration when missing connector.name', async () => { + await createConfiguration( + supertest, + { + // @ts-expect-error + connector: { + id: 'test-id', + type: ConnectorTypes.none, + fields: null, + }, + closure_type: 'close-by-user', + }, + 400 + ); + }); + + it('should not create a configuration when missing connector.type', async () => { + await createConfiguration( + supertest, + { + // @ts-expect-error + connector: { + id: 'test-id', + name: 'Connector', + fields: null, + }, + closure_type: 'close-by-user', + }, + 400 + ); + }); + + it('should not create a configuration when missing connector.fields', async () => { + await createConfiguration( + supertest, + { + // @ts-expect-error + connector: { + id: 'test-id', + type: ConnectorTypes.none, + name: 'Connector', + }, + closure_type: 'close-by-user', + }, + 400 + ); + }); + + it('should not create a configuration when when missing closure_type', async () => { + await createConfiguration( + supertest, + // @ts-expect-error + { + connector: { + id: 'test-id', + type: ConnectorTypes.none, + name: 'Connector', + fields: null, + }, + }, + 400 + ); + }); + + it('should not create a configuration when missing connector', async () => { + await createConfiguration( + supertest, + // @ts-expect-error + { + closure_type: 'close-by-user', + }, + 400 + ); + }); + + it('should not create a configuration when fields are not null', async () => { + await createConfiguration( + supertest, + { + connector: { + id: 'test-id', + type: ConnectorTypes.none, + name: 'Connector', + // @ts-expect-error + fields: {}, + }, + closure_type: 'close-by-user', + }, + 400 + ); + }); + + it('should not create a configuration with unsupported connector type', async () => { + // @ts-expect-error + await createConfiguration(supertest, getConfigurationRequest({ type: '.unsupported' }), 400); + }); + + it('should not create a configuration with unsupported connector fields', async () => { + await createConfiguration( + supertest, + // @ts-expect-error + getConfigurationRequest({ type: '.jira', fields: { unsupported: 'value' } }), + 400 + ); + }); + + describe('rbac', () => { + it('User: security solution only - should create a configuration', async () => { + const configuration = await createConfiguration( + supertestWithoutAuth, + getConfigurationRequest(), + 200, + { + user: secOnly, + space: 'space1', + } + ); + + expect(configuration.owner).to.eql('securitySolutionFixture'); + }); + + it('User: security solution only - should NOT create a configuration of different owner', async () => { + await createConfiguration( + supertestWithoutAuth, + { ...getConfigurationRequest(), owner: 'observabilityFixture' }, + 403, + { + user: secOnly, + space: 'space1', + } + ); + }); + + for (const user of [globalRead, secOnlyRead, obsOnlyRead, obsSecRead, noKibanaPrivileges]) { + it(`User ${ + user.username + } with role(s) ${user.roles.join()} - should NOT create a configuration`, async () => { + await createConfiguration( + supertestWithoutAuth, + { ...getConfigurationRequest(), owner: 'securitySolutionFixture' }, + 403, + { + user, + space: 'space1', + } + ); + }); + } + + it('should NOT create a configuration in a space with no permissions', async () => { + await createConfiguration( + supertestWithoutAuth, + { ...getConfigurationRequest(), owner: 'securitySolutionFixture' }, + 403, + { + user: secOnly, + space: 'space2', + } + ); + }); + + it('it deletes the correct configurations', async () => { + await createConfiguration( + supertestWithoutAuth, + { ...getConfigurationRequest(), owner: 'securitySolutionFixture' }, + 200, + { + user: superUser, + space: 'space1', + } + ); + + /** + * This API call should not delete the previously created configuration + * as it belongs to a different owner + */ + await createConfiguration( + supertestWithoutAuth, + { ...getConfigurationRequest(), owner: 'observabilityFixture' }, + 200, + { + user: superUser, + space: 'space1', + } + ); + + const configuration = await getConfiguration({ + supertest: supertestWithoutAuth, + query: { owner: ['securitySolutionFixture', 'observabilityFixture'] }, + auth: { + user: superUser, + space: 'space1', + }, + }); + + /** + * This ensures that both configuration are returned as expected + * and neither of has been deleted + */ + ensureSavedObjectIsAuthorized(configuration, 2, [ + 'securitySolutionFixture', + 'observabilityFixture', + ]); + }); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/spaces_only/tests/common/connectors/case.ts b/x-pack/test/case_api_integration/spaces_only/tests/common/connectors/case.ts new file mode 100644 index 00000000000000..fd9ec8142b49fe --- /dev/null +++ b/x-pack/test/case_api_integration/spaces_only/tests/common/connectors/case.ts @@ -0,0 +1,1078 @@ +/* + * 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 { omit } from 'lodash/fp'; +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; + +import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; +import { CommentType } from '../../../../../../plugins/cases/common/api'; +import { postCaseReq, postCaseResp } from '../../../../common/lib/mock'; +import { + removeServerGeneratedPropertiesFromCase, + removeServerGeneratedPropertiesFromComments, +} from '../../../../common/lib/utils'; +import { + createRule, + createSignalsIndex, + deleteAllAlerts, + deleteSignalsIndex, + getRuleForSignalTesting, + getSignalsByIds, + waitForRuleSuccessOrStatus, + waitForSignalsToBePresent, +} from '../../../../../detection_engine_api_integration/utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + describe('case_connector', () => { + let createdActionId = ''; + + it('should return 400 when creating a case action', async () => { + await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A case connector', + actionTypeId: '.case', + config: {}, + }) + .expect(400); + }); + + // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests + it.skip('should return 200 when creating a case action successfully', async () => { + const { body: createdAction } = await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A case connector', + actionTypeId: '.case', + config: {}, + }) + .expect(200); + + createdActionId = createdAction.id; + + expect(createdAction).to.eql({ + id: createdActionId, + isPreconfigured: false, + name: 'A case connector', + actionTypeId: '.case', + config: {}, + }); + + const { body: fetchedAction } = await supertest + .get(`/api/actions/connector/${createdActionId}`) + .expect(200); + + expect(fetchedAction).to.eql({ + id: fetchedAction.id, + isPreconfigured: false, + name: 'A case connector', + actionTypeId: '.case', + config: {}, + }); + }); + + describe.skip('create', () => { + it('should respond with a 400 Bad Request when creating a case without title', async () => { + const { body: createdAction } = await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A case connector', + actionTypeId: '.case', + config: {}, + }) + .expect(200); + + createdActionId = createdAction.id; + const params = { + subAction: 'create', + subActionParams: { + tags: ['case', 'connector'], + description: 'case description', + connector: { + id: 'jira', + name: 'Jira', + type: '.jira', + fields: { + issueType: '10006', + priority: 'High', + parent: null, + }, + }, + }, + }; + + const caseConnector = await supertest + .post(`/api/actions/connector/${createdActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ params }) + .expect(200); + + expect(caseConnector.body).to.eql({ + status: 'error', + actionId: createdActionId, + message: + 'error validating action params: types that failed validation:\n- [0.subActionParams.title]: expected value of type [string] but got [undefined]\n- [1.subAction]: expected value to equal [update]\n- [2.subAction]: expected value to equal [addComment]', + retry: false, + }); + }); + + it('should respond with a 400 Bad Request when creating a case without description', async () => { + const { body: createdAction } = await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A case connector', + actionTypeId: '.case', + config: {}, + }) + .expect(200); + + createdActionId = createdAction.id; + const params = { + subAction: 'create', + subActionParams: { + title: 'Case from case connector!!', + tags: ['case', 'connector'], + connector: { + id: 'jira', + name: 'Jira', + type: '.jira', + fields: { + issueType: '10006', + priority: 'High', + parent: null, + }, + }, + }, + }; + + const caseConnector = await supertest + .post(`/api/actions/connector/${createdActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ params }) + .expect(200); + + expect(caseConnector.body).to.eql({ + status: 'error', + actionId: createdActionId, + message: + 'error validating action params: types that failed validation:\n- [0.subActionParams.description]: expected value of type [string] but got [undefined]\n- [1.subAction]: expected value to equal [update]\n- [2.subAction]: expected value to equal [addComment]', + retry: false, + }); + }); + + it('should respond with a 400 Bad Request when creating a case without tags', async () => { + const { body: createdAction } = await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A case connector', + actionTypeId: '.case', + config: {}, + }) + .expect(200); + + createdActionId = createdAction.id; + const params = { + subAction: 'create', + subActionParams: { + title: 'Case from case connector!!', + description: 'case description', + connector: { + id: 'jira', + name: 'Jira', + type: '.jira', + fields: { + issueType: '10006', + priority: 'High', + parent: null, + }, + }, + }, + }; + + const caseConnector = await supertest + .post(`/api/actions/connector/${createdActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ params }) + .expect(200); + + expect(caseConnector.body).to.eql({ + status: 'error', + actionId: createdActionId, + message: + 'error validating action params: types that failed validation:\n- [0.subActionParams.tags]: expected value of type [array] but got [undefined]\n- [1.subAction]: expected value to equal [update]\n- [2.subAction]: expected value to equal [addComment]', + retry: false, + }); + }); + + it('should respond with a 400 Bad Request when creating a case without connector', async () => { + const { body: createdAction } = await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A case connector', + actionTypeId: '.case', + config: {}, + }) + .expect(200); + + createdActionId = createdAction.id; + const params = { + subAction: 'create', + subActionParams: { + title: 'Case from case connector!!', + description: 'case description', + tags: ['case', 'connector'], + }, + }; + + const caseConnector = await supertest + .post(`/api/actions/connector/${createdActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ params }) + .expect(200); + + expect(caseConnector.body).to.eql({ + status: 'error', + actionId: createdActionId, + message: + 'error validating action params: types that failed validation:\n- [0.subActionParams.connector.id]: expected value of type [string] but got [undefined]\n- [1.subAction]: expected value to equal [update]\n- [2.subAction]: expected value to equal [addComment]', + retry: false, + }); + }); + + it('should respond with a 400 Bad Request when creating jira without issueType', async () => { + const { body: createdAction } = await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A case connector', + actionTypeId: '.case', + config: {}, + }) + .expect(200); + + createdActionId = createdAction.id; + const params = { + subAction: 'create', + subActionParams: { + title: 'Case from case connector!!', + description: 'case description', + tags: ['case', 'connector'], + connector: { + id: 'jira', + name: 'Jira', + type: '.jira', + fields: { + priority: 'High', + parent: null, + }, + }, + }, + }; + + const caseConnector = await supertest + .post(`/api/actions/connector/${createdActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ params }) + .expect(200); + + expect(caseConnector.body).to.eql({ + status: 'error', + actionId: createdActionId, + message: + 'error validating action params: types that failed validation:\n- [0.subActionParams.connector.fields.issueType]: expected value of type [string] but got [undefined]\n- [1.subAction]: expected value to equal [update]\n- [2.subAction]: expected value to equal [addComment]', + retry: false, + }); + }); + + it('should respond with a 400 Bad Request when creating a connector with wrong fields', async () => { + const { body: createdAction } = await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A case connector', + actionTypeId: '.case', + config: {}, + }) + .expect(200); + + createdActionId = createdAction.id; + const params = { + subAction: 'create', + subActionParams: { + title: 'Case from case connector!!', + description: 'case description', + tags: ['case', 'connector'], + connector: { + id: 'servicenow', + name: 'Servicenow', + type: '.servicenow', + fields: { + impact: 'Medium', + severity: 'Medium', + notExists: 'not-exists', + }, + }, + }, + }; + + const caseConnector = await supertest + .post(`/api/actions/connector/${createdActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ params }) + .expect(200); + + expect(caseConnector.body).to.eql({ + status: 'error', + actionId: createdActionId, + message: + 'error validating action params: types that failed validation:\n- [0.subActionParams.connector.fields.notExists]: definition for this key is missing\n- [1.subAction]: expected value to equal [update]\n- [2.subAction]: expected value to equal [addComment]', + retry: false, + }); + }); + + it('should respond with a 400 Bad Request when creating a none without fields as null', async () => { + const { body: createdAction } = await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A case connector', + actionTypeId: '.case', + config: {}, + }) + .expect(200); + + createdActionId = createdAction.id; + const params = { + subAction: 'create', + subActionParams: { + title: 'Case from case connector!!', + description: 'case description', + tags: ['case', 'connector'], + connector: { + id: 'none', + name: 'None', + type: '.none', + fields: {}, + }, + }, + }; + + const caseConnector = await supertest + .post(`/api/actions/connector/${createdActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ params }) + .expect(200); + + expect(caseConnector.body).to.eql({ + status: 'error', + actionId: createdActionId, + message: + 'error validating action params: types that failed validation:\n- [0.subActionParams.connector]: Fields must be set to null for connectors of type .none\n- [1.subAction]: expected value to equal [update]\n- [2.subAction]: expected value to equal [addComment]', + retry: false, + }); + }); + + it('should create a case', async () => { + const { body: createdAction } = await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A case connector', + actionTypeId: '.case', + config: {}, + }) + .expect(200); + + createdActionId = createdAction.id; + const params = { + subAction: 'create', + subActionParams: { + title: 'Case from case connector!!', + tags: ['case', 'connector'], + description: 'case description', + connector: { + id: 'jira', + name: 'Jira', + type: '.jira', + fields: { + issueType: '10006', + priority: 'High', + parent: null, + }, + }, + settings: { + syncAlerts: true, + }, + }, + }; + + const caseConnector = await supertest + .post(`/api/actions/connector/${createdActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ params }) + .expect(200); + + const { body } = await supertest + .get(`${CASES_URL}/${caseConnector.body.data.id}`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + const data = removeServerGeneratedPropertiesFromCase(body); + expect(data).to.eql({ + ...postCaseResp(caseConnector.body.data.id), + ...params.subActionParams, + created_by: { + email: null, + full_name: null, + username: null, + }, + }); + }); + + it('should create a case with connector with field as null if not provided', async () => { + const { body: createdAction } = await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A case connector', + actionTypeId: '.case', + config: {}, + }) + .expect(200); + + createdActionId = createdAction.id; + const params = { + subAction: 'create', + subActionParams: { + title: 'Case from case connector!!', + tags: ['case', 'connector'], + description: 'case description', + connector: { + id: 'servicenow', + name: 'Servicenow', + type: '.servicenow', + fields: {}, + }, + settings: { + syncAlerts: true, + }, + }, + }; + + const caseConnector = await supertest + .post(`/api/actions/connector/${createdActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ params }) + .expect(200); + + const { body } = await supertest + .get(`${CASES_URL}/${caseConnector.body.data.id}`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + const data = removeServerGeneratedPropertiesFromCase(body); + expect(data).to.eql({ + ...postCaseResp(caseConnector.body.data.id), + ...params.subActionParams, + connector: { + id: 'servicenow', + name: 'Servicenow', + type: '.servicenow', + fields: { + impact: null, + severity: null, + urgency: null, + category: null, + subcategory: null, + }, + }, + created_by: { + email: null, + full_name: null, + username: null, + }, + }); + }); + }); + + describe.skip('update', () => { + it('should respond with a 400 Bad Request when updating a case without id', async () => { + const { body: createdAction } = await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A case connector', + actionTypeId: '.case', + config: {}, + }) + .expect(200); + + createdActionId = createdAction.id; + const params = { + subAction: 'update', + subActionParams: { + version: '123', + title: 'Case from case connector!!', + }, + }; + + const caseConnector = await supertest + .post(`/api/actions/connector/${createdActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ params }) + .expect(200); + + expect(caseConnector.body).to.eql({ + status: 'error', + actionId: createdActionId, + message: + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [create]\n- [1.subActionParams.id]: expected value of type [string] but got [undefined]\n- [2.subAction]: expected value to equal [addComment]', + retry: false, + }); + }); + + it('should respond with a 400 Bad Request when updating a case without version', async () => { + const { body: createdAction } = await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A case connector', + actionTypeId: '.case', + config: {}, + }) + .expect(200); + + createdActionId = createdAction.id; + const params = { + subAction: 'update', + subActionParams: { + id: '123', + title: 'Case from case connector!!', + }, + }; + + const caseConnector = await supertest + .post(`/api/actions/connector/${createdActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ params }) + .expect(200); + + expect(caseConnector.body).to.eql({ + status: 'error', + actionId: createdActionId, + message: + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [create]\n- [1.subActionParams.version]: expected value of type [string] but got [undefined]\n- [2.subAction]: expected value to equal [addComment]', + retry: false, + }); + }); + + it('should update a case', async () => { + const { body: createdAction } = await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A case connector', + actionTypeId: '.case', + config: {}, + }) + .expect(200); + + createdActionId = createdAction.id; + + const caseRes = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCaseReq) + .expect(200); + + const params = { + subAction: 'update', + subActionParams: { + id: caseRes.body.id, + version: caseRes.body.version, + title: 'Case from case connector!!', + }, + }; + + await supertest + .post(`/api/actions/connector/${createdActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ params }) + .expect(200); + + const { body } = await supertest + .get(`${CASES_URL}/${caseRes.body.id}`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + const data = removeServerGeneratedPropertiesFromCase(body); + expect(data).to.eql({ + ...postCaseResp(caseRes.body.id), + title: 'Case from case connector!!', + updated_by: { + email: null, + full_name: null, + username: null, + }, + }); + }); + }); + + describe.skip('addComment', () => { + it('should respond with a 400 Bad Request when adding a comment to a case without caseId', async () => { + const { body: createdAction } = await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A case connector', + actionTypeId: '.case', + config: {}, + }) + .expect(200); + + createdActionId = createdAction.id; + const params = { + subAction: 'addComment', + subActionParams: { + comment: { comment: 'a comment', type: CommentType.user }, + }, + }; + + const caseConnector = await supertest + .post(`/api/actions/connector/${createdActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ params }) + .expect(200); + + expect(caseConnector.body).to.eql({ + status: 'error', + actionId: createdActionId, + message: + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [create]\n- [1.subAction]: expected value to equal [update]\n- [2.subActionParams.caseId]: expected value of type [string] but got [undefined]', + retry: false, + }); + }); + + it('should respond with a 400 Bad Request when missing attributes of type user', async () => { + const { body: createdAction } = await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A case connector', + actionTypeId: '.case', + config: {}, + }) + .expect(200); + + createdActionId = createdAction.id; + const params = { + subAction: 'addComment', + subActionParams: { + caseId: '123', + }, + }; + + const caseConnector = await supertest + .post(`/api/actions/connector/${createdActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ params }) + .expect(200); + + expect(caseConnector.body).to.eql({ + status: 'error', + actionId: createdActionId, + message: + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [create]\n- [1.subAction]: expected value to equal [update]\n- [2.subActionParams.comment]: expected at least one defined value but got [undefined]', + retry: false, + }); + }); + + describe('adding alerts using a connector', () => { + beforeEach(async () => { + await esArchiver.load('auditbeat/hosts'); + await createSignalsIndex(supertest); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest); + await deleteAllAlerts(supertest); + await esArchiver.unload('auditbeat/hosts'); + }); + + it('should add a comment of type alert', async () => { + const rule = getRuleForSignalTesting(['auditbeat-*']); + const { id } = await createRule(supertest, rule); + await waitForRuleSuccessOrStatus(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signals = await getSignalsByIds(supertest, [id]); + const alert = signals.hits.hits[0]; + + const { body: createdAction } = await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A case connector', + actionTypeId: '.case', + config: {}, + }) + .expect(200); + + createdActionId = createdAction.id; + + const caseRes = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCaseReq) + .expect(200); + + const params = { + subAction: 'addComment', + subActionParams: { + caseId: caseRes.body.id, + comment: { + alertId: alert._id, + index: alert._index, + type: CommentType.alert, + rule: { id: 'id', name: 'name' }, + }, + }, + }; + + const caseConnector = await supertest + .post(`/api/actions/connector/${createdActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ params }) + .expect(200); + + expect(caseConnector.body.status).to.eql('ok'); + + const { body } = await supertest + .get(`${CASES_URL}/${caseRes.body.id}`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + const data = removeServerGeneratedPropertiesFromCase(body); + const comments = removeServerGeneratedPropertiesFromComments(data.comments ?? []); + expect({ ...data, comments }).to.eql({ + ...postCaseResp(caseRes.body.id), + comments, + totalAlerts: 1, + totalComment: 1, + updated_by: { + email: null, + full_name: null, + username: null, + }, + }); + }); + }); + + it('should respond with a 400 Bad Request when missing attributes of type alert', async () => { + const { body: createdAction } = await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A case connector', + actionTypeId: '.case', + config: {}, + }) + .expect(200); + + createdActionId = createdAction.id; + const comment = { + alertId: 'test-id', + index: 'test-index', + type: CommentType.alert, + rule: { id: 'id', name: 'name' }, + }; + const params = { + subAction: 'addComment', + subActionParams: { + caseId: '123', + comment, + }, + }; + + // only omitting alertId here since the type for alertId and index differ, the messages will be different + for (const attribute of ['alertId']) { + const requestAttributes = omit(attribute, comment); + const caseConnector = await supertest + .post(`/api/actions/connector/${createdActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + ...params, + subActionParams: { ...params.subActionParams, comment: requestAttributes }, + }, + }) + .expect(200); + + expect(caseConnector.body).to.eql({ + status: 'error', + actionId: createdActionId, + message: `error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [create]\n- [1.subAction]: expected value to equal [update]\n- [2.subActionParams.comment]: types that failed validation:\n - [subActionParams.comment.0.type]: expected value to equal [user]\n - [subActionParams.comment.1.${attribute}]: expected at least one defined value but got [undefined]\n - [subActionParams.comment.2.type]: expected value to equal [generated_alert]`, + retry: false, + }); + } + }); + + it('should respond with a 400 Bad Request when adding excess attributes for type user', async () => { + const { body: createdAction } = await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A case connector', + actionTypeId: '.case', + config: {}, + }) + .expect(200); + + createdActionId = createdAction.id; + const params = { + subAction: 'addComment', + subActionParams: { + caseId: '123', + comment: { comment: 'a comment', type: CommentType.user }, + }, + }; + + for (const attribute of ['blah', 'bogus']) { + const caseConnector = await supertest + .post(`/api/actions/connector/${createdActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + ...params, + subActionParams: { + ...params.subActionParams, + comment: { ...params.subActionParams.comment, [attribute]: attribute }, + }, + }, + }) + .expect(200); + expect(caseConnector.body).to.eql({ + status: 'error', + actionId: createdActionId, + message: `error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [create]\n- [1.subAction]: expected value to equal [update]\n- [2.subActionParams.comment]: types that failed validation:\n - [subActionParams.comment.0.${attribute}]: definition for this key is missing\n - [subActionParams.comment.1.type]: expected value to equal [alert]\n - [subActionParams.comment.2.type]: expected value to equal [generated_alert]`, + retry: false, + }); + } + }); + + it('should respond with a 400 Bad Request when adding excess attributes for type alert', async () => { + const { body: createdAction } = await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A case connector', + actionTypeId: '.case', + config: {}, + }) + .expect(200); + + createdActionId = createdAction.id; + const params = { + subAction: 'addComment', + subActionParams: { + caseId: '123', + comment: { + alertId: 'test-id', + index: 'test-index', + type: CommentType.alert, + rule: { id: 'id', name: 'name' }, + }, + }, + }; + + for (const attribute of ['comment']) { + const caseConnector = await supertest + .post(`/api/actions/connector/${createdActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + ...params, + subActionParams: { + ...params.subActionParams, + comment: { ...params.subActionParams.comment, [attribute]: attribute }, + }, + }, + }) + .expect(200); + + expect(caseConnector.body).to.eql({ + status: 'error', + actionId: createdActionId, + message: `error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [create]\n- [1.subAction]: expected value to equal [update]\n- [2.subActionParams.comment]: types that failed validation:\n - [subActionParams.comment.0.type]: expected value to equal [user]\n - [subActionParams.comment.1.${attribute}]: definition for this key is missing\n - [subActionParams.comment.2.type]: expected value to equal [generated_alert]`, + retry: false, + }); + } + }); + + it('should respond with a 400 Bad Request when adding a comment to a case without type', async () => { + const { body: createdAction } = await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A case connector', + actionTypeId: '.case', + config: {}, + }) + .expect(200); + + createdActionId = createdAction.id; + const params = { + subAction: 'update', + subActionParams: { + caseId: '123', + comment: { comment: 'a comment' }, + }, + }; + + const caseConnector = await supertest + .post(`/api/actions/connector/${createdActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ params }) + .expect(200); + + expect(caseConnector.body).to.eql({ + status: 'error', + actionId: createdActionId, + message: + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [create]\n- [1.subActionParams.id]: expected value of type [string] but got [undefined]\n- [2.subAction]: expected value to equal [addComment]', + retry: false, + }); + }); + + it('should add a comment of type user', async () => { + const { body: createdAction } = await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A case connector', + actionTypeId: '.case', + config: {}, + }) + .expect(200); + + createdActionId = createdAction.id; + + const caseRes = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCaseReq) + .expect(200); + + const params = { + subAction: 'addComment', + subActionParams: { + caseId: caseRes.body.id, + comment: { comment: 'a comment', type: CommentType.user }, + }, + }; + + await supertest + .post(`/api/actions/connector/${createdActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ params }) + .expect(200); + + const { body } = await supertest + .get(`${CASES_URL}/${caseRes.body.id}`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + const data = removeServerGeneratedPropertiesFromCase(body); + const comments = removeServerGeneratedPropertiesFromComments(data.comments ?? []); + expect({ ...data, comments }).to.eql({ + ...postCaseResp(caseRes.body.id), + comments, + totalComment: 1, + updated_by: { + email: null, + full_name: null, + username: null, + }, + }); + }); + + it('should add a comment of type alert', async () => { + const { body: createdAction } = await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A case connector', + actionTypeId: '.case', + config: {}, + }) + .expect(200); + + createdActionId = createdAction.id; + + const caseRes = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCaseReq) + .expect(200); + + const params = { + subAction: 'addComment', + subActionParams: { + caseId: caseRes.body.id, + comment: { + alertId: 'test-id', + index: 'test-index', + type: CommentType.alert, + rule: { id: 'id', name: 'name' }, + }, + }, + }; + + await supertest + .post(`/api/actions/connector/${createdActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ params }) + .expect(200); + + const { body } = await supertest + .get(`${CASES_URL}/${caseRes.body.id}`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + const data = removeServerGeneratedPropertiesFromCase(body); + const comments = removeServerGeneratedPropertiesFromComments(data.comments ?? []); + expect({ ...data, comments }).to.eql({ + ...postCaseResp(caseRes.body.id), + comments, + totalComment: 1, + totalAlerts: 1, + updated_by: { + email: null, + full_name: null, + username: null, + }, + }); + }); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/spaces_only/tests/common/index.ts b/x-pack/test/case_api_integration/spaces_only/tests/common/index.ts new file mode 100644 index 00000000000000..ff2d1b5f37aae6 --- /dev/null +++ b/x-pack/test/case_api_integration/spaces_only/tests/common/index.ts @@ -0,0 +1,41 @@ +/* + * 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 { FtrProviderContext } from '../../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default ({ loadTestFile }: FtrProviderContext): void => { + describe('Common', function () { + loadTestFile(require.resolve('./comments/delete_comment')); + loadTestFile(require.resolve('./comments/find_comments')); + loadTestFile(require.resolve('./comments/get_comment')); + loadTestFile(require.resolve('./comments/get_all_comments')); + loadTestFile(require.resolve('./comments/patch_comment')); + loadTestFile(require.resolve('./comments/post_comment')); + loadTestFile(require.resolve('./cases/delete_cases')); + loadTestFile(require.resolve('./cases/find_cases')); + loadTestFile(require.resolve('./cases/get_case')); + loadTestFile(require.resolve('./cases/patch_cases')); + loadTestFile(require.resolve('./cases/post_case')); + loadTestFile(require.resolve('./cases/reporters/get_reporters')); + loadTestFile(require.resolve('./cases/status/get_status')); + loadTestFile(require.resolve('./cases/tags/get_tags')); + loadTestFile(require.resolve('./user_actions/get_all_user_actions')); + loadTestFile(require.resolve('./configure/get_configure')); + loadTestFile(require.resolve('./configure/get_connectors')); + loadTestFile(require.resolve('./configure/patch_configure')); + loadTestFile(require.resolve('./configure/post_configure')); + loadTestFile(require.resolve('./connectors/case')); + loadTestFile(require.resolve('./sub_cases/patch_sub_cases')); + loadTestFile(require.resolve('./sub_cases/delete_sub_cases')); + loadTestFile(require.resolve('./sub_cases/get_sub_case')); + loadTestFile(require.resolve('./sub_cases/find_sub_cases')); + + // NOTE: Migrations are not included because they can inadvertently remove the .kibana indices which removes the users and spaces + // which causes errors in any tests after them that relies on those + }); +}; diff --git a/x-pack/test/case_api_integration/spaces_only/tests/common/migrations.ts b/x-pack/test/case_api_integration/spaces_only/tests/common/migrations.ts new file mode 100644 index 00000000000000..17d93e76bbdda5 --- /dev/null +++ b/x-pack/test/case_api_integration/spaces_only/tests/common/migrations.ts @@ -0,0 +1,18 @@ +/* + * 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 { FtrProviderContext } from '../../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default ({ loadTestFile }: FtrProviderContext): void => { + describe('Common migrations', function () { + // Migrations + loadTestFile(require.resolve('./cases/migrations')); + loadTestFile(require.resolve('./configure/migrations')); + loadTestFile(require.resolve('./user_actions/migrations')); + }); +}; diff --git a/x-pack/test/case_api_integration/spaces_only/tests/common/sub_cases/delete_sub_cases.ts b/x-pack/test/case_api_integration/spaces_only/tests/common/sub_cases/delete_sub_cases.ts new file mode 100644 index 00000000000000..951db263a6c784 --- /dev/null +++ b/x-pack/test/case_api_integration/spaces_only/tests/common/sub_cases/delete_sub_cases.ts @@ -0,0 +1,112 @@ +/* + * 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 { FtrProviderContext } from '../../../../../common/ftr_provider_context'; + +import { + CASES_URL, + SUB_CASES_PATCH_DEL_URL, +} from '../../../../../../plugins/cases/common/constants'; +import { postCommentUserReq } from '../../../../common/lib/mock'; +import { + createCaseAction, + createSubCase, + deleteAllCaseItems, + deleteCaseAction, +} from '../../../../common/lib/utils'; +import { getSubCaseDetailsUrl } from '../../../../../../plugins/cases/common/api/helpers'; +import { CaseResponse } from '../../../../../../plugins/cases/common/api'; + +// eslint-disable-next-line import/no-default-export +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const es = getService('es'); + + // ENABLE_CASE_CONNECTOR: remove this outer describe once the case connector feature is completed + describe('delete_sub_cases disabled routes', () => { + it('should return a 404 when attempting to access the route and the case connector feature is disabled', async () => { + await supertest + .delete(`${SUB_CASES_PATCH_DEL_URL}?ids=["sub-case-id"]`) + .set('kbn-xsrf', 'true') + .send() + .expect(404); + }); + + // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests + describe.skip('delete_sub_cases', () => { + let actionID: string; + before(async () => { + actionID = await createCaseAction(supertest); + }); + after(async () => { + await deleteCaseAction(supertest, actionID); + }); + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + it('should delete a sub case', async () => { + const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); + expect(caseInfo.subCases![0].id).to.not.eql(undefined); + + const { body: subCase } = await supertest + .get(getSubCaseDetailsUrl(caseInfo.id, caseInfo.subCases![0].id)) + .send() + .expect(200); + + expect(subCase.id).to.not.eql(undefined); + + const { body } = await supertest + .delete(`${SUB_CASES_PATCH_DEL_URL}?ids=["${subCase.id}"]`) + .set('kbn-xsrf', 'true') + .send() + .expect(204); + + expect(body).to.eql({}); + await supertest + .get(getSubCaseDetailsUrl(caseInfo.id, caseInfo.subCases![0].id)) + .send() + .expect(404); + }); + + it(`should delete a sub case's comments when that case gets deleted`, async () => { + const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); + expect(caseInfo.subCases![0].id).to.not.eql(undefined); + + // there should be two comments on the sub case now + const { body: patchedCaseWithSubCase }: { body: CaseResponse } = await supertest + .post(`${CASES_URL}/${caseInfo.id}/comments`) + .set('kbn-xsrf', 'true') + .query({ subCaseId: caseInfo.subCases![0].id }) + .send(postCommentUserReq) + .expect(200); + + const subCaseCommentUrl = `${CASES_URL}/${patchedCaseWithSubCase.id}/comments/${ + patchedCaseWithSubCase.comments![1].id + }`; + // make sure we can get the second comment + await supertest.get(subCaseCommentUrl).set('kbn-xsrf', 'true').send().expect(200); + + await supertest + .delete(`${SUB_CASES_PATCH_DEL_URL}?ids=["${patchedCaseWithSubCase.subCases![0].id}"]`) + .set('kbn-xsrf', 'true') + .send() + .expect(204); + + await supertest.get(subCaseCommentUrl).set('kbn-xsrf', 'true').send().expect(404); + }); + + it('unhappy path - 404s when sub case id is invalid', async () => { + await supertest + .delete(`${SUB_CASES_PATCH_DEL_URL}?ids=["fake-id"]`) + .set('kbn-xsrf', 'true') + .send() + .expect(404); + }); + }); + }); +} diff --git a/x-pack/test/case_api_integration/spaces_only/tests/common/sub_cases/find_sub_cases.ts b/x-pack/test/case_api_integration/spaces_only/tests/common/sub_cases/find_sub_cases.ts new file mode 100644 index 00000000000000..d54523bec0c4d4 --- /dev/null +++ b/x-pack/test/case_api_integration/spaces_only/tests/common/sub_cases/find_sub_cases.ts @@ -0,0 +1,480 @@ +/* + * 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 type { ApiResponse, estypes } from '@elastic/elasticsearch'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +import { findSubCasesResp, postCollectionReq } from '../../../../common/lib/mock'; +import { + createCaseAction, + createSubCase, + CreateSubCaseResp, + deleteAllCaseItems, + deleteCaseAction, + setStatus, +} from '../../../../common/lib/utils'; +import { getSubCasesUrl } from '../../../../../../plugins/cases/common/api/helpers'; +import { + CaseResponse, + CaseStatuses, + CommentType, + SubCasesFindResponse, +} from '../../../../../../plugins/cases/common/api'; +import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; +import { + ContextTypeGeneratedAlertType, + createAlertsString, +} from '../../../../../../plugins/cases/server/connectors'; + +interface SubCaseAttributes { + 'cases-sub-case': { + created_at: string; + }; +} + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + + // ENABLE_CASE_CONNECTOR: remove this outer describe once the case connector feature is completed + describe('find_sub_cases disabled route', () => { + it('should return a 404 when attempting to access the route and the case connector feature is disabled', async () => { + await supertest.get(`${getSubCasesUrl('case-id')}/_find`).expect(404); + }); + + // ENABLE_CASE_CONNECTOR: enable these tests once the case connector feature is completed + describe.skip('create case connector', () => { + let actionID: string; + before(async () => { + actionID = await createCaseAction(supertest); + }); + after(async () => { + await deleteCaseAction(supertest, actionID); + }); + + describe('basic find tests', () => { + afterEach(async () => { + await deleteAllCaseItems(es); + }); + it('should not find any sub cases when none exist', async () => { + const { body: caseResp }: { body: CaseResponse } = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCollectionReq) + .expect(200); + + const { body: findSubCases } = await supertest + .get(`${getSubCasesUrl(caseResp.id)}/_find`) + .expect(200); + + expect(findSubCases).to.eql({ + page: 1, + per_page: 20, + total: 0, + subCases: [], + count_open_cases: 0, + count_closed_cases: 0, + count_in_progress_cases: 0, + }); + }); + + it('should return a sub cases with comment stats', async () => { + const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); + + const { body }: { body: SubCasesFindResponse } = await supertest + .get(`${getSubCasesUrl(caseInfo.id)}/_find`) + .expect(200); + + expect(body).to.eql({ + ...findSubCasesResp, + total: 1, + // find should not return the comments themselves only the stats + subCases: [{ ...caseInfo.subCases![0], comments: [], totalComment: 1, totalAlerts: 2 }], + count_open_cases: 1, + }); + }); + + it('should return multiple sub cases', async () => { + const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); + const subCase2Resp = await createSubCase({ supertest, caseID: caseInfo.id, actionID }); + + const { body }: { body: SubCasesFindResponse } = await supertest + .get(`${getSubCasesUrl(caseInfo.id)}/_find`) + .expect(200); + + expect(body).to.eql({ + ...findSubCasesResp, + total: 2, + // find should not return the comments themselves only the stats + subCases: [ + { + // there should only be 1 closed sub case + ...subCase2Resp.modifiedSubCases![0], + comments: [], + totalComment: 1, + totalAlerts: 2, + status: CaseStatuses.closed, + }, + { + ...subCase2Resp.newSubCaseInfo.subCases![0], + comments: [], + totalComment: 1, + totalAlerts: 2, + }, + ], + count_open_cases: 1, + count_closed_cases: 1, + }); + }); + + it('should only return open when filtering for open', async () => { + const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); + // this will result in one closed case and one open + await createSubCase({ supertest, caseID: caseInfo.id, actionID }); + + const { body }: { body: SubCasesFindResponse } = await supertest + .get(`${getSubCasesUrl(caseInfo.id)}/_find?status=${CaseStatuses.open}`) + .expect(200); + + expect(body.total).to.be(1); + expect(body.subCases[0].status).to.be(CaseStatuses.open); + expect(body.count_closed_cases).to.be(1); + expect(body.count_open_cases).to.be(1); + expect(body.count_in_progress_cases).to.be(0); + }); + + it('should only return closed when filtering for closed', async () => { + const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); + // this will result in one closed case and one open + await createSubCase({ supertest, caseID: caseInfo.id, actionID }); + + const { body }: { body: SubCasesFindResponse } = await supertest + .get(`${getSubCasesUrl(caseInfo.id)}/_find?status=${CaseStatuses.closed}`) + .expect(200); + + expect(body.total).to.be(1); + expect(body.subCases[0].status).to.be(CaseStatuses.closed); + expect(body.count_closed_cases).to.be(1); + expect(body.count_open_cases).to.be(1); + expect(body.count_in_progress_cases).to.be(0); + }); + + it('should only return in progress when filtering for in progress', async () => { + const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); + // this will result in one closed case and one open + const { newSubCaseInfo: secondSub } = await createSubCase({ + supertest, + caseID: caseInfo.id, + actionID, + }); + + await setStatus({ + supertest, + cases: [ + { + id: secondSub.subCases![0].id, + version: secondSub.subCases![0].version, + status: CaseStatuses['in-progress'], + }, + ], + type: 'sub_case', + }); + + const { body }: { body: SubCasesFindResponse } = await supertest + .get(`${getSubCasesUrl(caseInfo.id)}/_find?status=${CaseStatuses['in-progress']}`) + .expect(200); + + expect(body.total).to.be(1); + expect(body.subCases[0].status).to.be(CaseStatuses['in-progress']); + expect(body.count_closed_cases).to.be(1); + expect(body.count_open_cases).to.be(0); + expect(body.count_in_progress_cases).to.be(1); + }); + + it('should sort on createdAt field in descending order', async () => { + const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); + // this will result in one closed case and one open + await createSubCase({ + supertest, + caseID: caseInfo.id, + actionID, + }); + + const { body }: { body: SubCasesFindResponse } = await supertest + .get(`${getSubCasesUrl(caseInfo.id)}/_find?sortField=createdAt&sortOrder=desc`) + .expect(200); + + expect(body.total).to.be(2); + expect(body.subCases[0].status).to.be(CaseStatuses.open); + expect(body.subCases[1].status).to.be(CaseStatuses.closed); + expect(body.count_closed_cases).to.be(1); + expect(body.count_open_cases).to.be(1); + expect(body.count_in_progress_cases).to.be(0); + }); + + it('should sort on createdAt field in ascending order', async () => { + const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); + // this will result in one closed case and one open + await createSubCase({ + supertest, + caseID: caseInfo.id, + actionID, + }); + + const { body }: { body: SubCasesFindResponse } = await supertest + .get(`${getSubCasesUrl(caseInfo.id)}/_find?sortField=createdAt&sortOrder=asc`) + .expect(200); + + expect(body.total).to.be(2); + expect(body.subCases[0].status).to.be(CaseStatuses.closed); + expect(body.subCases[1].status).to.be(CaseStatuses.open); + expect(body.count_closed_cases).to.be(1); + expect(body.count_open_cases).to.be(1); + expect(body.count_in_progress_cases).to.be(0); + }); + + it('should sort on updatedAt field in ascending order', async () => { + const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); + // this will result in one closed case and one open + const { newSubCaseInfo: secondSub } = await createSubCase({ + supertest, + caseID: caseInfo.id, + actionID, + }); + + await setStatus({ + supertest, + cases: [ + { + id: secondSub.subCases![0].id, + version: secondSub.subCases![0].version, + status: CaseStatuses['in-progress'], + }, + ], + type: 'sub_case', + }); + + const { body }: { body: SubCasesFindResponse } = await supertest + .get(`${getSubCasesUrl(caseInfo.id)}/_find?sortField=updatedAt&sortOrder=asc`) + .expect(200); + + expect(body.total).to.be(2); + expect(body.subCases[0].status).to.be(CaseStatuses.closed); + expect(body.subCases[1].status).to.be(CaseStatuses['in-progress']); + expect(body.count_closed_cases).to.be(1); + expect(body.count_open_cases).to.be(0); + expect(body.count_in_progress_cases).to.be(1); + }); + }); + + describe('pagination', () => { + const numCases = 4; + let collection: CaseResponse; + before(async () => { + ({ body: collection } = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCollectionReq) + .expect(200)); + + await createSubCases(numCases, collection.id); + }); + + after(async () => { + await deleteAllCaseItems(es); + }); + + const createSubCases = async (total: number, caseID: string) => { + const responses: CreateSubCaseResp[] = []; + for (let i = 0; i < total; i++) { + const postCommentGenAlertReq: ContextTypeGeneratedAlertType = { + alerts: createAlertsString([ + { _id: `${i}`, _index: 'test-index', ruleId: 'rule-id', ruleName: 'rule name' }, + ]), + type: CommentType.generatedAlert, + owner: 'securitySolutionFixture', + }; + responses.push( + await createSubCase({ + supertest, + actionID, + caseID, + comment: postCommentGenAlertReq, + }) + ); + } + return responses; + }; + + const getAllCasesSortedByCreatedAtAsc = async () => { + const cases: ApiResponse> = await es.search({ + index: '.kibana', + body: { + size: 10000, + sort: [{ 'cases-sub-case.created_at': { unmapped_type: 'date', order: 'asc' } }], + query: { + term: { type: 'cases-sub-case' }, + }, + }, + }); + return cases.body.hits.hits.map((hit) => hit._source); + }; + + it('returns the correct total when perPage is less than the total', async () => { + const { body }: { body: SubCasesFindResponse } = await supertest + .get(`${getSubCasesUrl(collection.id)}/_find`) + .query({ + sortOrder: 'asc', + page: 1, + perPage: 3, + }) + .set('kbn-xsrf', 'true') + .expect(200); + + expect(body.subCases.length).to.eql(3); + expect(body.total).to.eql(4); + expect(body.page).to.eql(1); + expect(body.per_page).to.eql(3); + // there will only be 1 open sub case, all the rest will be closed + expect(body.count_open_cases).to.eql(1); + expect(body.count_closed_cases).to.eql(3); + expect(body.count_in_progress_cases).to.eql(0); + }); + + it('returns the correct total when perPage is greater than the total', async () => { + const { body }: { body: SubCasesFindResponse } = await supertest + .get(`${getSubCasesUrl(collection.id)}/_find`) + .query({ + sortOrder: 'asc', + page: 1, + perPage: 11, + }) + .set('kbn-xsrf', 'true') + .expect(200); + + expect(body.total).to.eql(4); + expect(body.page).to.eql(1); + expect(body.per_page).to.eql(11); + expect(body.subCases.length).to.eql(4); + expect(body.count_open_cases).to.eql(1); + expect(body.count_closed_cases).to.eql(3); + expect(body.count_in_progress_cases).to.eql(0); + }); + + it('returns the correct total when perPage is equal to the total', async () => { + const { body }: { body: SubCasesFindResponse } = await supertest + .get(`${getSubCasesUrl(collection.id)}/_find`) + .query({ + sortOrder: 'asc', + page: 1, + perPage: 4, + }) + .set('kbn-xsrf', 'true') + .expect(200); + + expect(body.total).to.eql(4); + expect(body.page).to.eql(1); + expect(body.per_page).to.eql(4); + expect(body.subCases.length).to.eql(4); + expect(body.count_open_cases).to.eql(1); + expect(body.count_closed_cases).to.eql(3); + expect(body.count_in_progress_cases).to.eql(0); + }); + + it('returns the second page of results', async () => { + const perPage = 2; + const { body }: { body: SubCasesFindResponse } = await supertest + .get(`${getSubCasesUrl(collection.id)}/_find`) + .query({ + sortOrder: 'asc', + page: 2, + perPage, + }) + .set('kbn-xsrf', 'true') + .expect(200); + + expect(body.total).to.eql(4); + expect(body.page).to.eql(2); + expect(body.per_page).to.eql(2); + expect(body.subCases.length).to.eql(2); + expect(body.count_open_cases).to.eql(1); + expect(body.count_closed_cases).to.eql(3); + expect(body.count_in_progress_cases).to.eql(0); + + const allCases = await getAllCasesSortedByCreatedAtAsc(); + + body.subCases.map((subCaseInfo, index) => { + // we started on the second page of 10 cases with a perPage of 5, so the first case should 0 + 5 (index + perPage) + expect(subCaseInfo.created_at).to.eql( + allCases[index + perPage]?.['cases-sub-case'].created_at + ); + }); + }); + + it('paginates with perPage of 2 through 4 total sub cases', async () => { + const total = 4; + const perPage = 2; + + // it's less than or equal here because the page starts at 1, so page 2 is a valid page number + // and should have sub cases titles 3, and 4 + for (let currentPage = 1; currentPage <= total / perPage; currentPage++) { + const { body }: { body: SubCasesFindResponse } = await supertest + .get(`${getSubCasesUrl(collection.id)}/_find`) + .query({ + sortOrder: 'asc', + page: currentPage, + perPage, + }) + .set('kbn-xsrf', 'true') + .expect(200); + + expect(body.total).to.eql(total); + expect(body.page).to.eql(currentPage); + expect(body.per_page).to.eql(perPage); + expect(body.subCases.length).to.eql(perPage); + expect(body.count_open_cases).to.eql(1); + expect(body.count_closed_cases).to.eql(total - 1); + expect(body.count_in_progress_cases).to.eql(0); + + const allCases = await getAllCasesSortedByCreatedAtAsc(); + + body.subCases.map((subCaseInfo, index) => { + // for page 1, the cases tiles should be 0,1,2 for page 2: 3,4,5 etc (assuming the titles were sorted + // correctly) + expect(subCaseInfo.created_at).to.eql( + allCases[index + perPage * (currentPage - 1)]?.['cases-sub-case'].created_at + ); + }); + } + }); + + it('retrieves the last sub case', async () => { + const { body }: { body: SubCasesFindResponse } = await supertest + .get(`${getSubCasesUrl(collection.id)}/_find`) + .query({ + sortOrder: 'asc', + // this should skip the first 3 sub cases and only return the last one + page: 2, + perPage: 3, + }) + .set('kbn-xsrf', 'true') + .expect(200); + + expect(body.total).to.eql(4); + expect(body.page).to.eql(2); + expect(body.per_page).to.eql(3); + expect(body.subCases.length).to.eql(1); + expect(body.count_open_cases).to.eql(1); + expect(body.count_closed_cases).to.eql(3); + expect(body.count_in_progress_cases).to.eql(0); + }); + }); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/spaces_only/tests/common/sub_cases/get_sub_case.ts b/x-pack/test/case_api_integration/spaces_only/tests/common/sub_cases/get_sub_case.ts new file mode 100644 index 00000000000000..35ed4ba5c3c71b --- /dev/null +++ b/x-pack/test/case_api_integration/spaces_only/tests/common/sub_cases/get_sub_case.ts @@ -0,0 +1,119 @@ +/* + * 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 { FtrProviderContext } from '../../../../../common/ftr_provider_context'; + +import { commentsResp, postCommentAlertReq, subCaseResp } from '../../../../common/lib/mock'; +import { + createCaseAction, + createSubCase, + defaultCreateSubComment, + deleteAllCaseItems, + deleteCaseAction, + removeServerGeneratedPropertiesFromComments, + removeServerGeneratedPropertiesFromSubCase, +} from '../../../../common/lib/utils'; +import { + getCaseCommentsUrl, + getSubCaseDetailsUrl, +} from '../../../../../../plugins/cases/common/api/helpers'; +import { + AssociationType, + CaseResponse, + SubCaseResponse, +} from '../../../../../../plugins/cases/common/api'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + + // ENABLE_CASE_CONNECTOR: remove the outer describe once the case connector feature is completed + describe('get_sub_case disabled route', () => { + it('should return a 404 when attempting to access the route and the case connector feature is disabled', async () => { + await supertest.get(getSubCaseDetailsUrl('case-id', 'sub-case-id')).expect(404); + }); + + // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests + describe.skip('get_sub_case', () => { + let actionID: string; + before(async () => { + actionID = await createCaseAction(supertest); + }); + after(async () => { + await deleteCaseAction(supertest, actionID); + }); + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + it('should return a case', async () => { + const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); + + const { body }: { body: SubCaseResponse } = await supertest + .get(getSubCaseDetailsUrl(caseInfo.id, caseInfo.subCases![0].id)) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(removeServerGeneratedPropertiesFromComments(body.comments)).to.eql( + commentsResp({ + comments: [{ comment: defaultCreateSubComment, id: caseInfo.comments![0].id }], + associationType: AssociationType.subCase, + }) + ); + + expect(removeServerGeneratedPropertiesFromSubCase(body)).to.eql( + subCaseResp({ id: body.id, totalComment: 1, totalAlerts: 2 }) + ); + }); + + it('should return the correct number of alerts with multiple types of alerts', async () => { + const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); + + const { body: singleAlert }: { body: CaseResponse } = await supertest + .post(getCaseCommentsUrl(caseInfo.id)) + .query({ subCaseId: caseInfo.subCases![0].id }) + .set('kbn-xsrf', 'true') + .send(postCommentAlertReq) + .expect(200); + + const { body }: { body: SubCaseResponse } = await supertest + .get(getSubCaseDetailsUrl(caseInfo.id, caseInfo.subCases![0].id)) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(removeServerGeneratedPropertiesFromComments(body.comments)).to.eql( + commentsResp({ + comments: [ + { comment: defaultCreateSubComment, id: caseInfo.comments![0].id }, + { + comment: postCommentAlertReq, + id: singleAlert.comments![1].id, + }, + ], + associationType: AssociationType.subCase, + }) + ); + + expect(removeServerGeneratedPropertiesFromSubCase(body)).to.eql( + subCaseResp({ id: body.id, totalComment: 2, totalAlerts: 3 }) + ); + }); + + it('unhappy path - 404s when case is not there', async () => { + await supertest + .get(getSubCaseDetailsUrl('fake-case-id', 'fake-sub-case-id')) + .set('kbn-xsrf', 'true') + .send() + .expect(404); + }); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/spaces_only/tests/common/sub_cases/patch_sub_cases.ts b/x-pack/test/case_api_integration/spaces_only/tests/common/sub_cases/patch_sub_cases.ts new file mode 100644 index 00000000000000..442644463fa38d --- /dev/null +++ b/x-pack/test/case_api_integration/spaces_only/tests/common/sub_cases/patch_sub_cases.ts @@ -0,0 +1,515 @@ +/* + * 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 { FtrProviderContext } from '../../../../../common/ftr_provider_context'; + +import { + CASES_URL, + SUB_CASES_PATCH_DEL_URL, +} from '../../../../../../plugins/cases/common/constants'; +import { + createCaseAction, + createSubCase, + deleteAllCaseItems, + deleteCaseAction, + getSignalsWithES, + setStatus, +} from '../../../../common/lib/utils'; +import { getSubCaseDetailsUrl } from '../../../../../../plugins/cases/common/api/helpers'; +import { + CaseStatuses, + CommentType, + SubCaseResponse, +} from '../../../../../../plugins/cases/common/api'; +import { createAlertsString } from '../../../../../../plugins/cases/server/connectors'; +import { postCaseReq, postCollectionReq } from '../../../../common/lib/mock'; + +const defaultSignalsIndex = '.siem-signals-default-000001'; + +// eslint-disable-next-line import/no-default-export +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const es = getService('es'); + const esArchiver = getService('esArchiver'); + + // ENABLE_CASE_CONNECTOR: remove the outer describe once the case connector feature is completed + describe('patch_sub_cases disabled route', () => { + it('should return a 404 when attempting to access the route and the case connector feature is disabled', async () => { + await supertest + .patch(SUB_CASES_PATCH_DEL_URL) + .set('kbn-xsrf', 'true') + .send({ subCases: [] }) + .expect(404); + }); + + // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests + describe.skip('patch_sub_cases', () => { + let actionID: string; + before(async () => { + actionID = await createCaseAction(supertest); + }); + after(async () => { + await deleteCaseAction(supertest, actionID); + }); + beforeEach(async () => { + await esArchiver.load('cases/signals/default'); + }); + afterEach(async () => { + await esArchiver.unload('cases/signals/default'); + await deleteAllCaseItems(es); + }); + + it('should update the status of a sub case', async () => { + const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); + + await setStatus({ + supertest, + cases: [ + { + id: caseInfo.subCases![0].id, + version: caseInfo.subCases![0].version, + status: CaseStatuses['in-progress'], + }, + ], + type: 'sub_case', + }); + const { body: subCase }: { body: SubCaseResponse } = await supertest + .get(getSubCaseDetailsUrl(caseInfo.id, caseInfo.subCases![0].id)) + .expect(200); + + expect(subCase.status).to.eql(CaseStatuses['in-progress']); + }); + + it('should update the status of one alert attached to a sub case', async () => { + const signalID = '5f2b9ec41f8febb1c06b5d1045aeabb9874733b7617e88a370510f2fb3a41a5d'; + + const { newSubCaseInfo: caseInfo } = await createSubCase({ + supertest, + actionID, + comment: { + alerts: createAlertsString([ + { + _id: signalID, + _index: defaultSignalsIndex, + ruleId: 'id', + ruleName: 'name', + }, + ]), + type: CommentType.generatedAlert, + owner: 'securitySolutionFixture', + }, + }); + + await es.indices.refresh({ index: defaultSignalsIndex }); + + let signals = await getSignalsWithES({ es, indices: defaultSignalsIndex, ids: signalID }); + + expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( + CaseStatuses.open + ); + + await setStatus({ + supertest, + cases: [ + { + id: caseInfo.subCases![0].id, + version: caseInfo.subCases![0].version, + status: CaseStatuses['in-progress'], + }, + ], + type: 'sub_case', + }); + + await es.indices.refresh({ index: defaultSignalsIndex }); + + signals = await getSignalsWithES({ es, indices: defaultSignalsIndex, ids: signalID }); + + expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( + CaseStatuses['in-progress'] + ); + }); + + it('should update the status of multiple alerts attached to a sub case', async () => { + const signalID = '5f2b9ec41f8febb1c06b5d1045aeabb9874733b7617e88a370510f2fb3a41a5d'; + + const signalID2 = '4d0f4b1533e46b66b43bdd0330d23f39f2cf42a7253153270e38d30cce9ff0c6'; + + const { newSubCaseInfo: caseInfo } = await createSubCase({ + supertest, + actionID, + comment: { + alerts: createAlertsString([ + { + _id: signalID, + _index: defaultSignalsIndex, + ruleId: 'id', + ruleName: 'name', + }, + { + _id: signalID2, + _index: defaultSignalsIndex, + ruleId: 'id', + ruleName: 'name', + }, + ]), + type: CommentType.generatedAlert, + owner: 'securitySolutionFixture', + }, + }); + + await es.indices.refresh({ index: defaultSignalsIndex }); + + let signals = await getSignalsWithES({ + es, + indices: defaultSignalsIndex, + ids: [signalID, signalID2], + }); + + expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( + CaseStatuses.open + ); + expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source?.signal.status).to.be( + CaseStatuses.open + ); + + await setStatus({ + supertest, + cases: [ + { + id: caseInfo.subCases![0].id, + version: caseInfo.subCases![0].version, + status: CaseStatuses['in-progress'], + }, + ], + type: 'sub_case', + }); + + await es.indices.refresh({ index: defaultSignalsIndex }); + + signals = await getSignalsWithES({ + es, + indices: defaultSignalsIndex, + ids: [signalID, signalID2], + }); + + expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( + CaseStatuses['in-progress'] + ); + expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source?.signal.status).to.be( + CaseStatuses['in-progress'] + ); + }); + + it('should update the status of multiple alerts attached to multiple sub cases in one collection', async () => { + const signalID = '5f2b9ec41f8febb1c06b5d1045aeabb9874733b7617e88a370510f2fb3a41a5d'; + const signalID2 = '4d0f4b1533e46b66b43bdd0330d23f39f2cf42a7253153270e38d30cce9ff0c6'; + + const { newSubCaseInfo: initialCaseInfo } = await createSubCase({ + supertest, + actionID, + caseInfo: { + ...postCollectionReq, + settings: { + syncAlerts: false, + }, + }, + comment: { + alerts: createAlertsString([ + { + _id: signalID, + _index: defaultSignalsIndex, + ruleId: 'id', + ruleName: 'name', + }, + ]), + type: CommentType.generatedAlert, + owner: 'securitySolutionFixture', + }, + }); + + // This will close the first sub case and create a new one + const { newSubCaseInfo: collectionWithSecondSub } = await createSubCase({ + supertest, + actionID, + caseID: initialCaseInfo.id, + comment: { + alerts: createAlertsString([ + { + _id: signalID2, + _index: defaultSignalsIndex, + ruleId: 'id', + ruleName: 'name', + }, + ]), + type: CommentType.generatedAlert, + owner: 'securitySolutionFixture', + }, + }); + + await es.indices.refresh({ index: defaultSignalsIndex }); + + let signals = await getSignalsWithES({ + es, + indices: defaultSignalsIndex, + ids: [signalID, signalID2], + }); + + // There should be no change in their status since syncing is disabled + expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( + CaseStatuses.open + ); + expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source?.signal.status).to.be( + CaseStatuses.open + ); + + await setStatus({ + supertest, + cases: [ + { + id: collectionWithSecondSub.subCases![0].id, + version: collectionWithSecondSub.subCases![0].version, + status: CaseStatuses['in-progress'], + }, + ], + type: 'sub_case', + }); + + await es.indices.refresh({ index: defaultSignalsIndex }); + + signals = await getSignalsWithES({ + es, + indices: defaultSignalsIndex, + ids: [signalID, signalID2], + }); + + // There still should be no change in their status since syncing is disabled + expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( + CaseStatuses.open + ); + expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source?.signal.status).to.be( + CaseStatuses.open + ); + + // Turn sync alerts on + await supertest + .patch(CASES_URL) + .set('kbn-xsrf', 'true') + .send({ + cases: [ + { + id: collectionWithSecondSub.id, + version: collectionWithSecondSub.version, + settings: { syncAlerts: true }, + }, + ], + }) + .expect(200); + + await es.indices.refresh({ index: defaultSignalsIndex }); + + signals = await getSignalsWithES({ + es, + indices: defaultSignalsIndex, + ids: [signalID, signalID2], + }); + + expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( + CaseStatuses.closed + ); + expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source?.signal.status).to.be( + CaseStatuses['in-progress'] + ); + }); + + it('should update the status of alerts attached to a case and sub case when sync settings is turned on', async () => { + const signalID = '5f2b9ec41f8febb1c06b5d1045aeabb9874733b7617e88a370510f2fb3a41a5d'; + const signalID2 = '4d0f4b1533e46b66b43bdd0330d23f39f2cf42a7253153270e38d30cce9ff0c6'; + + const { body: individualCase } = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send({ + ...postCaseReq, + settings: { + syncAlerts: false, + }, + }); + + const { newSubCaseInfo: caseInfo } = await createSubCase({ + supertest, + actionID, + caseInfo: { + ...postCollectionReq, + settings: { + syncAlerts: false, + }, + }, + comment: { + alerts: createAlertsString([ + { + _id: signalID, + _index: defaultSignalsIndex, + ruleId: 'id', + ruleName: 'name', + }, + ]), + type: CommentType.generatedAlert, + owner: 'securitySolutionFixture', + }, + }); + + const { body: updatedIndWithComment } = await supertest + .post(`${CASES_URL}/${individualCase.id}/comments`) + .set('kbn-xsrf', 'true') + .send({ + alertId: signalID2, + index: defaultSignalsIndex, + rule: { id: 'test-rule-id', name: 'test-index-id' }, + type: CommentType.alert, + }) + .expect(200); + + await es.indices.refresh({ index: defaultSignalsIndex }); + + let signals = await getSignalsWithES({ + es, + indices: defaultSignalsIndex, + ids: [signalID, signalID2], + }); + + // There should be no change in their status since syncing is disabled + expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( + CaseStatuses.open + ); + expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source?.signal.status).to.be( + CaseStatuses.open + ); + + await setStatus({ + supertest, + cases: [ + { + id: caseInfo.subCases![0].id, + version: caseInfo.subCases![0].version, + status: CaseStatuses['in-progress'], + }, + ], + type: 'sub_case', + }); + + const updatedIndWithStatus = ( + await setStatus({ + supertest, + cases: [ + { + id: updatedIndWithComment.id, + version: updatedIndWithComment.version, + status: CaseStatuses.closed, + }, + ], + type: 'case', + }) + )[0]; // there should only be a single entry in the response + + await es.indices.refresh({ index: defaultSignalsIndex }); + + signals = await getSignalsWithES({ + es, + indices: defaultSignalsIndex, + ids: [signalID, signalID2], + }); + + // There should still be no change in their status since syncing is disabled + expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( + CaseStatuses.open + ); + expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source?.signal.status).to.be( + CaseStatuses.open + ); + + // Turn sync alerts on + await supertest + .patch(CASES_URL) + .set('kbn-xsrf', 'true') + .send({ + cases: [ + { + id: caseInfo.id, + version: caseInfo.version, + settings: { syncAlerts: true }, + }, + ], + }) + .expect(200); + + await supertest + .patch(CASES_URL) + .set('kbn-xsrf', 'true') + .send({ + cases: [ + { + id: updatedIndWithStatus.id, + version: updatedIndWithStatus.version, + settings: { syncAlerts: true }, + }, + ], + }) + .expect(200); + + await es.indices.refresh({ index: defaultSignalsIndex }); + + signals = await getSignalsWithES({ + es, + indices: defaultSignalsIndex, + ids: [signalID, signalID2], + }); + + // alerts should be updated now that the + expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( + CaseStatuses['in-progress'] + ); + expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source?.signal.status).to.be( + CaseStatuses.closed + ); + }); + + it('404s when sub case id is invalid', async () => { + await supertest + .patch(`${SUB_CASES_PATCH_DEL_URL}`) + .set('kbn-xsrf', 'true') + .send({ + subCases: [ + { + id: 'fake-id', + version: 'blah', + status: CaseStatuses.open, + }, + ], + }) + .expect(404); + }); + + it('406s when updating invalid fields for a sub case', async () => { + const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); + + await supertest + .patch(`${SUB_CASES_PATCH_DEL_URL}`) + .set('kbn-xsrf', 'true') + .send({ + subCases: [ + { + id: caseInfo.subCases![0].id, + version: caseInfo.subCases![0].version, + type: 'blah', + }, + ], + }) + .expect(406); + }); + }); + }); +} diff --git a/x-pack/test/case_api_integration/spaces_only/tests/common/user_actions/get_all_user_actions.ts b/x-pack/test/case_api_integration/spaces_only/tests/common/user_actions/get_all_user_actions.ts new file mode 100644 index 00000000000000..5cd4082bd3293b --- /dev/null +++ b/x-pack/test/case_api_integration/spaces_only/tests/common/user_actions/get_all_user_actions.ts @@ -0,0 +1,395 @@ +/* + * 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 { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; +import { + CaseResponse, + CaseStatuses, + CommentType, +} from '../../../../../../plugins/cases/common/api'; +import { + userActionPostResp, + postCaseReq, + postCommentUserReq, + getPostCaseRequest, +} from '../../../../common/lib/mock'; +import { + deleteAllCaseItems, + createCase, + updateCase, + getCaseUserActions, +} from '../../../../common/lib/utils'; +import { + globalRead, + noKibanaPrivileges, + obsSec, + obsSecRead, + secOnly, + secOnlyRead, + superUser, +} from '../../../../common/lib/authentication/users'; +import { superUserSpace1Auth } from '../../../../common/lib/authentication'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + + describe('get_all_user_actions', () => { + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + it(`on new case, user action: 'create' should be called with actionFields: ['description', 'status', 'tags', 'title', 'connector', 'settings, owner]`, async () => { + const { body: postedCase } = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCaseReq) + .expect(200); + + const { body } = await supertest + .get(`${CASES_URL}/${postedCase.id}/user_actions`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(body.length).to.eql(1); + + expect(body[0].action_field).to.eql([ + 'description', + 'status', + 'tags', + 'title', + 'connector', + 'settings', + 'owner', + ]); + expect(body[0].action).to.eql('create'); + expect(body[0].old_value).to.eql(null); + expect(JSON.parse(body[0].new_value)).to.eql(userActionPostResp); + }); + + it(`on close case, user action: 'update' should be called with actionFields: ['status']`, async () => { + const { body: postedCase } = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCaseReq) + .expect(200); + + await supertest + .patch(CASES_URL) + .set('kbn-xsrf', 'true') + .send({ + cases: [ + { + id: postedCase.id, + version: postedCase.version, + status: 'closed', + }, + ], + }) + .expect(200); + + const { body } = await supertest + .get(`${CASES_URL}/${postedCase.id}/user_actions`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(body.length).to.eql(2); + expect(body[1].action_field).to.eql(['status']); + expect(body[1].action).to.eql('update'); + expect(body[1].old_value).to.eql('open'); + expect(body[1].new_value).to.eql('closed'); + }); + + it(`on update case connector, user action: 'update' should be called with actionFields: ['connector']`, async () => { + const { body: postedCase } = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCaseReq) + .expect(200); + + const newConnector = { + id: '123', + name: 'Connector', + type: '.jira', + fields: { issueType: 'Task', priority: 'High', parent: null }, + }; + + await supertest + .patch(CASES_URL) + .set('kbn-xsrf', 'true') + .send({ + cases: [ + { + id: postedCase.id, + version: postedCase.version, + connector: newConnector, + }, + ], + }) + .expect(200); + + const { body } = await supertest + .get(`${CASES_URL}/${postedCase.id}/user_actions`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(body.length).to.eql(2); + expect(body[1].action_field).to.eql(['connector']); + expect(body[1].action).to.eql('update'); + expect(JSON.parse(body[1].old_value)).to.eql({ + id: 'none', + name: 'none', + type: '.none', + fields: null, + }); + expect(JSON.parse(body[1].new_value)).to.eql({ + id: '123', + name: 'Connector', + type: '.jira', + fields: { issueType: 'Task', priority: 'High', parent: null }, + }); + }); + + it(`on update tags, user action: 'add' and 'delete' should be called with actionFields: ['tags']`, async () => { + const { body: postedCase } = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCaseReq) + .expect(200); + + await supertest + .patch(CASES_URL) + .set('kbn-xsrf', 'true') + .send({ + cases: [ + { + id: postedCase.id, + version: postedCase.version, + tags: ['cool', 'neat'], + }, + ], + }) + .expect(200); + + const { body } = await supertest + .get(`${CASES_URL}/${postedCase.id}/user_actions`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(body.length).to.eql(3); + expect(body[1].action_field).to.eql(['tags']); + expect(body[1].action).to.eql('add'); + expect(body[1].old_value).to.eql(null); + expect(body[1].new_value).to.eql('cool, neat'); + expect(body[2].action_field).to.eql(['tags']); + expect(body[2].action).to.eql('delete'); + expect(body[2].old_value).to.eql(null); + expect(body[2].new_value).to.eql('defacement'); + }); + + it(`on update title, user action: 'update' should be called with actionFields: ['title']`, async () => { + const { body: postedCase } = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCaseReq) + .expect(200); + + const newTitle = 'Such a great title'; + await supertest + .patch(CASES_URL) + .set('kbn-xsrf', 'true') + .send({ + cases: [ + { + id: postedCase.id, + version: postedCase.version, + title: newTitle, + }, + ], + }) + .expect(200); + + const { body } = await supertest + .get(`${CASES_URL}/${postedCase.id}/user_actions`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(body.length).to.eql(2); + expect(body[1].action_field).to.eql(['title']); + expect(body[1].action).to.eql('update'); + expect(body[1].old_value).to.eql(postCaseReq.title); + expect(body[1].new_value).to.eql(newTitle); + }); + + it(`on update description, user action: 'update' should be called with actionFields: ['description']`, async () => { + const { body: postedCase } = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCaseReq) + .expect(200); + + const newDesc = 'Such a great description'; + await supertest + .patch(CASES_URL) + .set('kbn-xsrf', 'true') + .send({ + cases: [ + { + id: postedCase.id, + version: postedCase.version, + description: newDesc, + }, + ], + }) + .expect(200); + + const { body } = await supertest + .get(`${CASES_URL}/${postedCase.id}/user_actions`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(body.length).to.eql(2); + expect(body[1].action_field).to.eql(['description']); + expect(body[1].action).to.eql('update'); + expect(body[1].old_value).to.eql(postCaseReq.description); + expect(body[1].new_value).to.eql(newDesc); + }); + + it(`on new comment, user action: 'create' should be called with actionFields: ['comments']`, async () => { + const { body: postedCase } = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCaseReq) + .expect(200); + + await supertest + .post(`${CASES_URL}/${postedCase.id}/comments`) + .set('kbn-xsrf', 'true') + .send(postCommentUserReq) + .expect(200); + + const { body } = await supertest + .get(`${CASES_URL}/${postedCase.id}/user_actions`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(body.length).to.eql(2); + expect(body[1].action_field).to.eql(['comment']); + expect(body[1].action).to.eql('create'); + expect(body[1].old_value).to.eql(null); + expect(JSON.parse(body[1].new_value)).to.eql(postCommentUserReq); + }); + + it(`on update comment, user action: 'update' should be called with actionFields: ['comments']`, async () => { + const { body: postedCase } = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCaseReq) + .expect(200); + + const { body: patchedCase } = await supertest + .post(`${CASES_URL}/${postedCase.id}/comments`) + .set('kbn-xsrf', 'true') + .send(postCommentUserReq) + .expect(200); + + const newComment = 'Well I decided to update my comment. So what? Deal with it.'; + await supertest + .patch(`${CASES_URL}/${postedCase.id}/comments`) + .set('kbn-xsrf', 'true') + .send({ + id: patchedCase.comments[0].id, + version: patchedCase.comments[0].version, + comment: newComment, + type: CommentType.user, + owner: 'securitySolutionFixture', + }) + .expect(200); + + const { body } = await supertest + .get(`${CASES_URL}/${postedCase.id}/user_actions`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(body.length).to.eql(3); + expect(body[2].action_field).to.eql(['comment']); + expect(body[2].action).to.eql('update'); + expect(JSON.parse(body[2].old_value)).to.eql(postCommentUserReq); + expect(JSON.parse(body[2].new_value)).to.eql({ + comment: newComment, + type: CommentType.user, + owner: 'securitySolutionFixture', + }); + }); + + describe('rbac', () => { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + let caseInfo: CaseResponse; + beforeEach(async () => { + caseInfo = await createCase(supertestWithoutAuth, getPostCaseRequest(), 200, { + user: superUser, + space: 'space1', + }); + + await updateCase({ + supertest: supertestWithoutAuth, + params: { + cases: [ + { + id: caseInfo.id, + version: caseInfo.version, + status: CaseStatuses.closed, + }, + ], + }, + auth: superUserSpace1Auth, + }); + }); + + it('should get the user actions for a case when the user has the correct permissions', async () => { + for (const user of [globalRead, superUser, secOnly, secOnlyRead, obsSec, obsSecRead]) { + const userActions = await getCaseUserActions({ + supertest: supertestWithoutAuth, + caseID: caseInfo.id, + auth: { user, space: 'space1' }, + }); + + expect(userActions.length).to.eql(2); + } + }); + + for (const scenario of [ + { user: noKibanaPrivileges, space: 'space1' }, + { user: secOnly, space: 'space2' }, + ]) { + it(`should 403 when requesting the user actions of a case with user ${ + scenario.user.username + } with role(s) ${scenario.user.roles.join()} and space ${scenario.space}`, async () => { + await getCaseUserActions({ + supertest: supertestWithoutAuth, + caseID: caseInfo.id, + auth: { user: scenario.user, space: scenario.space }, + expectedHttpCode: 403, + }); + }); + } + }); + }); +}; diff --git a/x-pack/test/case_api_integration/spaces_only/tests/common/user_actions/migrations.ts b/x-pack/test/case_api_integration/spaces_only/tests/common/user_actions/migrations.ts new file mode 100644 index 00000000000000..e198260e88a9c1 --- /dev/null +++ b/x-pack/test/case_api_integration/spaces_only/tests/common/user_actions/migrations.ts @@ -0,0 +1,53 @@ +/* + * 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 { FtrProviderContext } from '../../../../../common/ftr_provider_context'; +import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; + +// eslint-disable-next-line import/no-default-export +export default function createGetTests({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + describe('migrations', () => { + before(async () => { + await esArchiver.load('cases/migrations/7.10.0'); + }); + + after(async () => { + await esArchiver.unload('cases/migrations/7.10.0'); + }); + + it('7.10.0 migrates user actions connector', async () => { + const { body } = await supertest + .get(`${CASES_URL}/e1900ac0-017f-11eb-93f8-d161651bf509/user_actions`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + const connectorUserAction = body[1]; + const oldValue = JSON.parse(connectorUserAction.old_value); + const newValue = JSON.parse(connectorUserAction.new_value); + + expect(connectorUserAction.action_field.length).eql(1); + expect(connectorUserAction.action_field[0]).eql('connector'); + expect(oldValue).to.eql({ + id: 'c1900ac0-017f-11eb-93f8-d161651bf509', + name: 'none', + type: '.none', + fields: null, + }); + expect(newValue).to.eql({ + id: 'b1900ac0-017f-11eb-93f8-d161651bf509', + name: 'none', + type: '.none', + fields: null, + }); + }); + }); +} diff --git a/x-pack/test/case_api_integration/spaces_only/tests/trial/cases/push_case.ts b/x-pack/test/case_api_integration/spaces_only/tests/trial/cases/push_case.ts new file mode 100644 index 00000000000000..3c096cb7557c32 --- /dev/null +++ b/x-pack/test/case_api_integration/spaces_only/tests/trial/cases/push_case.ts @@ -0,0 +1,360 @@ +/* + * 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. + */ + +/* eslint-disable @typescript-eslint/naming-convention */ + +import expect from '@kbn/expect'; +import * as st from 'supertest'; +import supertestAsPromised from 'supertest-as-promised'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { ObjectRemover as ActionsRemover } from '../../../../../alerting_api_integration/common/lib'; + +import { + postCaseReq, + defaultUser, + postCommentUserReq, + getPostCaseRequest, +} from '../../../../common/lib/mock'; +import { + getConfigurationRequest, + getServiceNowConnector, + createConnector, + createConfiguration, + createCase, + pushCase, + createComment, + CreateConnectorResponse, + updateCase, + getAllUserAction, + removeServerGeneratedPropertiesFromUserAction, + deleteAllCaseItems, +} from '../../../../common/lib/utils'; +import { + ExternalServiceSimulator, + getExternalServiceSimulatorPath, +} from '../../../../../alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin'; +import { + CaseConnector, + CasePostRequest, + CaseResponse, + CaseStatuses, + CaseUserActionResponse, + ConnectorTypes, +} from '../../../../../../plugins/cases/common/api'; +import { + globalRead, + noKibanaPrivileges, + obsOnlyRead, + obsSecRead, + secOnly, + secOnlyRead, + superUser, +} from '../../../../common/lib/authentication/users'; +import { User } from '../../../../common/lib/authentication/types'; +import { superUserSpace1Auth } from '../../../../common/lib/authentication'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const kibanaServer = getService('kibanaServer'); + const es = getService('es'); + + describe('push_case', () => { + const actionsRemover = new ActionsRemover(supertest); + + let servicenowSimulatorURL: string = ''; + before(() => { + servicenowSimulatorURL = kibanaServer.resolveUrl( + getExternalServiceSimulatorPath(ExternalServiceSimulator.SERVICENOW) + ); + }); + + afterEach(async () => { + await deleteAllCaseItems(es); + await actionsRemover.removeAll(); + }); + + const createCaseWithConnector = async ({ + testAgent = supertest, + configureReq = {}, + auth = { user: superUser, space: null }, + createCaseReq = getPostCaseRequest(), + }: { + testAgent?: st.SuperTest; + configureReq?: Record; + auth?: { user: User; space: string | null }; + createCaseReq?: CasePostRequest; + } = {}): Promise<{ + postedCase: CaseResponse; + connector: CreateConnectorResponse; + }> => { + const connector = await createConnector({ + supertest: testAgent, + req: { + ...getServiceNowConnector(), + config: { apiUrl: servicenowSimulatorURL }, + }, + auth, + }); + + actionsRemover.add(auth.space ?? 'default', connector.id, 'action', 'actions'); + await createConfiguration( + testAgent, + { + ...getConfigurationRequest({ + id: connector.id, + name: connector.name, + type: connector.connector_type_id as ConnectorTypes, + }), + ...configureReq, + }, + 200, + auth + ); + + const postedCase = await createCase( + testAgent, + { + ...createCaseReq, + connector: { + id: connector.id, + name: connector.name, + type: connector.connector_type_id, + fields: { + urgency: '2', + impact: '2', + severity: '2', + category: 'software', + subcategory: 'os', + }, + } as CaseConnector, + }, + 200, + auth + ); + + return { postedCase, connector }; + }; + + it('should push a case', async () => { + const { postedCase, connector } = await createCaseWithConnector(); + const theCase = await pushCase({ + supertest, + caseId: postedCase.id, + connectorId: connector.id, + }); + + const { pushed_at, external_url, ...rest } = theCase.external_service!; + + expect(rest).to.eql({ + pushed_by: defaultUser, + connector_id: connector.id, + connector_name: connector.name, + external_id: '123', + external_title: 'INC01', + }); + + // external_url is of the form http://elastic:changeme@localhost:5620 which is different between various environments like Jekins + expect( + external_url.includes( + 'api/_actions-FTS-external-service-simulators/servicenow/nav_to.do?uri=incident.do?sys_id=123' + ) + ).to.equal(true); + }); + + it('pushes a comment appropriately', async () => { + const { postedCase, connector } = await createCaseWithConnector(); + await createComment({ supertest, caseId: postedCase.id, params: postCommentUserReq }); + const theCase = await pushCase({ + supertest, + caseId: postedCase.id, + connectorId: connector.id, + }); + + expect(theCase.comments![0].pushed_by).to.eql(defaultUser); + }); + + it('should pushes a case and closes when closure_type: close-by-pushing', async () => { + const { postedCase, connector } = await createCaseWithConnector({ + configureReq: { + closure_type: 'close-by-pushing', + }, + }); + const theCase = await pushCase({ + supertest, + caseId: postedCase.id, + connectorId: connector.id, + }); + + expect(theCase.status).to.eql('closed'); + }); + + it('should create the correct user action', async () => { + const { postedCase, connector } = await createCaseWithConnector(); + const pushedCase = await pushCase({ + supertest, + caseId: postedCase.id, + connectorId: connector.id, + }); + const userActions = await getAllUserAction(supertest, pushedCase.id); + const pushUserAction = removeServerGeneratedPropertiesFromUserAction(userActions[1]); + + const { new_value, ...rest } = pushUserAction as CaseUserActionResponse; + const parsedNewValue = JSON.parse(new_value!); + + expect(rest).to.eql({ + action_field: ['pushed'], + action: 'push-to-service', + action_by: defaultUser, + old_value: null, + case_id: `${postedCase.id}`, + comment_id: null, + sub_case_id: '', + owner: 'securitySolutionFixture', + }); + + expect(parsedNewValue).to.eql({ + pushed_at: pushedCase.external_service!.pushed_at, + pushed_by: defaultUser, + connector_id: connector.id, + connector_name: connector.name, + external_id: '123', + external_title: 'INC01', + external_url: `${servicenowSimulatorURL}/nav_to.do?uri=incident.do?sys_id=123`, + }); + }); + + // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests + it.skip('should push a collection case but not close it when closure_type: close-by-pushing', async () => { + const { postedCase, connector } = await createCaseWithConnector({ + configureReq: { + closure_type: 'close-by-pushing', + }, + }); + + const theCase = await pushCase({ + supertest, + caseId: postedCase.id, + connectorId: connector.id, + }); + expect(theCase.status).to.eql(CaseStatuses.open); + }); + + it('unhappy path - 404s when case does not exist', async () => { + await pushCase({ + supertest, + caseId: 'fake-id', + connectorId: 'fake-connector', + expectedHttpCode: 404, + }); + }); + + it('unhappy path - 404s when connector does not exist', async () => { + const postedCase = await createCase(supertest, { + ...postCaseReq, + connector: getConfigurationRequest().connector, + }); + await pushCase({ + supertest, + caseId: postedCase.id, + connectorId: 'fake-connector', + expectedHttpCode: 404, + }); + }); + + it('unhappy path = 409s when case is closed', async () => { + const { postedCase, connector } = await createCaseWithConnector(); + await updateCase({ + supertest, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + status: CaseStatuses.closed, + }, + ], + }, + }); + + await pushCase({ + supertest, + caseId: postedCase.id, + connectorId: connector.id, + expectedHttpCode: 409, + }); + }); + + describe('rbac', () => { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + it('should push a case that the user has permissions for', async () => { + const { postedCase, connector } = await createCaseWithConnector({ + testAgent: supertestWithoutAuth, + auth: superUserSpace1Auth, + }); + + await pushCase({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + connectorId: connector.id, + auth: { user: secOnly, space: 'space1' }, + }); + }); + + it('should not push a case that the user does not have permissions for', async () => { + const { postedCase, connector } = await createCaseWithConnector({ + testAgent: supertestWithoutAuth, + auth: superUserSpace1Auth, + createCaseReq: getPostCaseRequest({ owner: 'observabilityFixture' }), + }); + + await pushCase({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + connectorId: connector.id, + auth: { user: secOnly, 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 push a case`, async () => { + const { postedCase, connector } = await createCaseWithConnector({ + testAgent: supertestWithoutAuth, + auth: superUserSpace1Auth, + }); + + await pushCase({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + connectorId: connector.id, + auth: { user, space: 'space1' }, + expectedHttpCode: 403, + }); + }); + } + + it('should not push a case in a space that the user does not have permissions for', async () => { + const { postedCase, connector } = await createCaseWithConnector({ + testAgent: supertestWithoutAuth, + auth: { user: superUser, space: 'space2' }, + }); + + await pushCase({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + connectorId: connector.id, + auth: { user: secOnly, space: 'space2' }, + expectedHttpCode: 403, + }); + }); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/spaces_only/tests/trial/cases/user_actions/get_all_user_actions.ts b/x-pack/test/case_api_integration/spaces_only/tests/trial/cases/user_actions/get_all_user_actions.ts new file mode 100644 index 00000000000000..3729b20f82b301 --- /dev/null +++ b/x-pack/test/case_api_integration/spaces_only/tests/trial/cases/user_actions/get_all_user_actions.ts @@ -0,0 +1,115 @@ +/* + * 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 { FtrProviderContext } from '../../../../../../common/ftr_provider_context'; + +import { CASE_CONFIGURE_URL, CASES_URL } from '../../../../../../../plugins/cases/common/constants'; +import { defaultUser, postCaseReq } from '../../../../../common/lib/mock'; +import { + deleteCasesByESQuery, + deleteCasesUserActions, + deleteComments, + deleteConfiguration, + getConfigurationRequest, + getServiceNowConnector, +} from '../../../../../common/lib/utils'; + +import { ObjectRemover as ActionsRemover } from '../../../../../../alerting_api_integration/common/lib'; +import { + ExternalServiceSimulator, + getExternalServiceSimulatorPath, +} from '../../../../../../alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + const actionsRemover = new ActionsRemover(supertest); + const kibanaServer = getService('kibanaServer'); + + describe('get_all_user_actions', () => { + let servicenowSimulatorURL: string = ''; + before(() => { + servicenowSimulatorURL = kibanaServer.resolveUrl( + getExternalServiceSimulatorPath(ExternalServiceSimulator.SERVICENOW) + ); + }); + afterEach(async () => { + await deleteCasesByESQuery(es); + await deleteComments(es); + await deleteConfiguration(es); + await deleteCasesUserActions(es); + await actionsRemover.removeAll(); + }); + + it(`on new push to service, user action: 'push-to-service' should be called with actionFields: ['pushed']`, async () => { + const { body: connector } = await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'true') + .send({ + ...getServiceNowConnector(), + config: { apiUrl: servicenowSimulatorURL }, + }) + .expect(200); + + actionsRemover.add('default', connector.id, 'action', 'actions'); + + const { body: configure } = await supertest + .post(CASE_CONFIGURE_URL) + .set('kbn-xsrf', 'true') + .send( + getConfigurationRequest({ + id: connector.id, + name: connector.name, + type: connector.connector_type_id, + }) + ) + .expect(200); + + const { body: postedCase } = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send({ + ...postCaseReq, + connector: getConfigurationRequest({ + id: connector.id, + name: connector.name, + type: connector.connector_type_id, + fields: { + urgency: '2', + impact: '2', + severity: '2', + category: 'software', + subcategory: 'os', + }, + }).connector, + }) + .expect(200); + + await supertest + .post(`${CASES_URL}/${postedCase.id}/connector/${connector.id}/_push`) + .set('kbn-xsrf', 'true') + .send({}) + .expect(200); + + const { body } = await supertest + .get(`${CASES_URL}/${postedCase.id}/user_actions`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(body.length).to.eql(2); + expect(body[1].action_field).to.eql(['pushed']); + expect(body[1].action).to.eql('push-to-service'); + expect(body[1].old_value).to.eql(null); + const newValue = JSON.parse(body[1].new_value); + expect(newValue.connector_id).to.eql(configure.connector.id); + expect(newValue.pushed_by).to.eql(defaultUser); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/get_configure.ts b/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/get_configure.ts new file mode 100644 index 00000000000000..ff8f1cff884aff --- /dev/null +++ b/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/get_configure.ts @@ -0,0 +1,98 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; + +import { + ExternalServiceSimulator, + getExternalServiceSimulatorPath, +} from '../../../../../alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin'; + +import { ObjectRemover as ActionsRemover } from '../../../../../alerting_api_integration/common/lib'; +import { + getServiceNowConnector, + createConnector, + createConfiguration, + getConfiguration, + getConfigurationRequest, + removeServerGeneratedPropertiesFromSavedObject, + getConfigurationOutput, +} from '../../../../common/lib/utils'; +import { ConnectorTypes } from '../../../../../../plugins/cases/common/api'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const actionsRemover = new ActionsRemover(supertest); + const kibanaServer = getService('kibanaServer'); + + describe('get_configure', () => { + let servicenowSimulatorURL: string = ''; + + before(() => { + servicenowSimulatorURL = kibanaServer.resolveUrl( + getExternalServiceSimulatorPath(ExternalServiceSimulator.SERVICENOW) + ); + }); + + afterEach(async () => { + await actionsRemover.removeAll(); + }); + + it('should return a configuration with mapping', async () => { + const connector = await createConnector({ + supertest, + req: { + ...getServiceNowConnector(), + config: { apiUrl: servicenowSimulatorURL }, + }, + }); + actionsRemover.add('default', connector.id, 'action', 'actions'); + + await createConfiguration( + supertest, + getConfigurationRequest({ + id: connector.id, + name: connector.name, + type: connector.connector_type_id as ConnectorTypes, + }) + ); + + const configuration = await getConfiguration({ supertest }); + + const data = removeServerGeneratedPropertiesFromSavedObject(configuration[0]); + expect(data).to.eql( + getConfigurationOutput(false, { + mappings: [ + { + action_type: 'overwrite', + source: 'title', + target: 'short_description', + }, + { + action_type: 'overwrite', + source: 'description', + target: 'description', + }, + { + action_type: 'append', + source: 'comments', + target: 'work_notes', + }, + ], + connector: { + id: connector.id, + name: connector.name, + type: connector.connector_type_id, + fields: null, + }, + }) + ); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/get_connectors.ts b/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/get_connectors.ts new file mode 100644 index 00000000000000..fb922f8d102434 --- /dev/null +++ b/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/get_connectors.ts @@ -0,0 +1,129 @@ +/* + * 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 { FtrProviderContext } from '../../../../../common/ftr_provider_context'; + +import { CASE_CONFIGURE_CONNECTORS_URL } from '../../../../../../plugins/cases/common/constants'; +import { ObjectRemover as ActionsRemover } from '../../../../../alerting_api_integration/common/lib'; +import { + getServiceNowConnector, + getJiraConnector, + getResilientConnector, + createConnector, + getServiceNowSIRConnector, +} from '../../../../common/lib/utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const actionsRemover = new ActionsRemover(supertest); + + describe('get_connectors', () => { + afterEach(async () => { + await actionsRemover.removeAll(); + }); + + it('should return the correct connectors', async () => { + const { body: snConnector } = await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'true') + .send(getServiceNowConnector()) + .expect(200); + + const { body: emailConnector } = await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'true') + .send({ + name: 'An email action', + connector_type_id: '.email', + config: { + service: '__json', + from: 'bob@example.com', + }, + secrets: { + user: 'bob', + password: 'supersecret', + }, + }) + .expect(200); + + const { body: jiraConnector } = await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'true') + .send(getJiraConnector()) + .expect(200); + + const { body: resilientConnector } = await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'true') + .send(getResilientConnector()) + .expect(200); + + const sir = await createConnector({ supertest, req: getServiceNowSIRConnector() }); + + actionsRemover.add('default', sir.id, 'action', 'actions'); + actionsRemover.add('default', snConnector.id, 'action', 'actions'); + actionsRemover.add('default', emailConnector.id, 'action', 'actions'); + actionsRemover.add('default', jiraConnector.id, 'action', 'actions'); + actionsRemover.add('default', resilientConnector.id, 'action', 'actions'); + + const { body: connectors } = await supertest + .get(`${CASE_CONFIGURE_CONNECTORS_URL}/_find`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(connectors).to.eql([ + { + id: jiraConnector.id, + actionTypeId: '.jira', + name: 'Jira Connector', + config: { + apiUrl: 'http://some.non.existent.com', + projectKey: 'pkey', + }, + isPreconfigured: false, + isMissingSecrets: false, + referencedByCount: 0, + }, + { + id: resilientConnector.id, + actionTypeId: '.resilient', + name: 'Resilient Connector', + config: { + apiUrl: 'http://some.non.existent.com', + orgId: 'pkey', + }, + isPreconfigured: false, + isMissingSecrets: false, + referencedByCount: 0, + }, + { + id: snConnector.id, + actionTypeId: '.servicenow', + name: 'ServiceNow Connector', + config: { + apiUrl: 'http://some.non.existent.com', + }, + isPreconfigured: false, + isMissingSecrets: false, + referencedByCount: 0, + }, + { + id: sir.id, + actionTypeId: '.servicenow-sir', + name: 'ServiceNow Connector', + config: { apiUrl: 'http://some.non.existent.com' }, + isPreconfigured: false, + isMissingSecrets: false, + referencedByCount: 0, + }, + ]); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/index.ts b/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/index.ts new file mode 100644 index 00000000000000..0c8c3931d15774 --- /dev/null +++ b/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/index.ts @@ -0,0 +1,18 @@ +/* + * 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 { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default ({ loadTestFile }: FtrProviderContext): void => { + describe('configuration tests', function () { + loadTestFile(require.resolve('./get_configure')); + loadTestFile(require.resolve('./get_connectors')); + loadTestFile(require.resolve('./patch_configure')); + loadTestFile(require.resolve('./post_configure')); + }); +}; diff --git a/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/patch_configure.ts b/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/patch_configure.ts new file mode 100644 index 00000000000000..789b68b19beb66 --- /dev/null +++ b/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/patch_configure.ts @@ -0,0 +1,168 @@ +/* + * 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 { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { ObjectRemover as ActionsRemover } from '../../../../../alerting_api_integration/common/lib'; +import { + ExternalServiceSimulator, + getExternalServiceSimulatorPath, +} from '../../../../../alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin'; + +import { + getConfigurationRequest, + removeServerGeneratedPropertiesFromSavedObject, + getConfigurationOutput, + deleteConfiguration, + createConfiguration, + updateConfiguration, + getServiceNowConnector, + createConnector, +} from '../../../../common/lib/utils'; +import { ConnectorTypes } from '../../../../../../plugins/cases/common/api'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + const kibanaServer = getService('kibanaServer'); + + describe('patch_configure', () => { + const actionsRemover = new ActionsRemover(supertest); + let servicenowSimulatorURL: string = ''; + + before(() => { + servicenowSimulatorURL = kibanaServer.resolveUrl( + getExternalServiceSimulatorPath(ExternalServiceSimulator.SERVICENOW) + ); + }); + + afterEach(async () => { + await deleteConfiguration(es); + await actionsRemover.removeAll(); + }); + + it('should patch a configuration connector and create mappings', async () => { + const connector = await createConnector({ + supertest, + req: { + ...getServiceNowConnector(), + config: { apiUrl: servicenowSimulatorURL }, + }, + }); + + actionsRemover.add('default', connector.id, 'action', 'actions'); + + // Configuration is created with no connector so the mappings are empty + const configuration = await createConfiguration(supertest); + + // the update request doesn't accept the owner field + const { owner, ...reqWithoutOwner } = getConfigurationRequest({ + id: connector.id, + name: connector.name, + type: connector.connector_type_id as ConnectorTypes, + fields: null, + }); + + const newConfiguration = await updateConfiguration(supertest, configuration.id, { + ...reqWithoutOwner, + version: configuration.version, + }); + + const data = removeServerGeneratedPropertiesFromSavedObject(newConfiguration); + expect(data).to.eql({ + ...getConfigurationOutput(true), + connector: { + id: connector.id, + name: connector.name, + type: connector.connector_type_id as ConnectorTypes, + fields: null, + }, + mappings: [ + { + action_type: 'overwrite', + source: 'title', + target: 'short_description', + }, + { + action_type: 'overwrite', + source: 'description', + target: 'description', + }, + { + action_type: 'append', + source: 'comments', + target: 'work_notes', + }, + ], + }); + }); + + it('should mappings when updating the connector', async () => { + const connector = await createConnector({ + supertest, + req: { + ...getServiceNowConnector(), + config: { apiUrl: servicenowSimulatorURL }, + }, + }); + + actionsRemover.add('default', connector.id, 'action', 'actions'); + + // Configuration is created with connector so the mappings are created + const configuration = await createConfiguration( + supertest, + getConfigurationRequest({ + id: connector.id, + name: connector.name, + type: connector.connector_type_id as ConnectorTypes, + }) + ); + + // the update request doesn't accept the owner field + const { owner, ...rest } = getConfigurationRequest({ + id: connector.id, + name: 'New name', + type: connector.connector_type_id as ConnectorTypes, + fields: null, + }); + + const newConfiguration = await updateConfiguration(supertest, configuration.id, { + ...rest, + version: configuration.version, + }); + + const data = removeServerGeneratedPropertiesFromSavedObject(newConfiguration); + expect(data).to.eql({ + ...getConfigurationOutput(true), + connector: { + id: connector.id, + name: 'New name', + type: connector.connector_type_id as ConnectorTypes, + fields: null, + }, + mappings: [ + { + action_type: 'overwrite', + source: 'title', + target: 'short_description', + }, + { + action_type: 'overwrite', + source: 'description', + target: 'description', + }, + { + action_type: 'append', + source: 'comments', + target: 'work_notes', + }, + ], + }); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/post_configure.ts b/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/post_configure.ts new file mode 100644 index 00000000000000..96ffcf4bc3f5c5 --- /dev/null +++ b/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/post_configure.ts @@ -0,0 +1,98 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { ConnectorTypes } from '../../../../../../plugins/cases/common/api'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { ObjectRemover as ActionsRemover } from '../../../../../alerting_api_integration/common/lib'; +import { + ExternalServiceSimulator, + getExternalServiceSimulatorPath, +} from '../../../../../alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin'; + +import { + getConfigurationRequest, + removeServerGeneratedPropertiesFromSavedObject, + getConfigurationOutput, + deleteConfiguration, + createConfiguration, + createConnector, + getServiceNowConnector, +} from '../../../../common/lib/utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + const kibanaServer = getService('kibanaServer'); + + describe('post_configure', () => { + const actionsRemover = new ActionsRemover(supertest); + let servicenowSimulatorURL: string = ''; + + before(() => { + servicenowSimulatorURL = kibanaServer.resolveUrl( + getExternalServiceSimulatorPath(ExternalServiceSimulator.SERVICENOW) + ); + }); + + afterEach(async () => { + await deleteConfiguration(es); + await actionsRemover.removeAll(); + }); + + it('should create a configuration with mapping', async () => { + const connector = await createConnector({ + supertest, + req: { + ...getServiceNowConnector(), + config: { apiUrl: servicenowSimulatorURL }, + }, + }); + + actionsRemover.add('default', connector.id, 'action', 'actions'); + + const postRes = await createConfiguration( + supertest, + getConfigurationRequest({ + id: connector.id, + name: connector.name, + type: connector.connector_type_id as ConnectorTypes, + }) + ); + + const data = removeServerGeneratedPropertiesFromSavedObject(postRes); + expect(data).to.eql( + getConfigurationOutput(false, { + mappings: [ + { + action_type: 'overwrite', + source: 'title', + target: 'short_description', + }, + { + action_type: 'overwrite', + source: 'description', + target: 'description', + }, + { + action_type: 'append', + source: 'comments', + target: 'work_notes', + }, + ], + connector: { + id: connector.id, + name: connector.name, + type: connector.connector_type_id, + fields: null, + }, + }) + ); + }); + }); +}; From 206756b6293eba0e460f6bfa6ee28ee17cfa74f8 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Wed, 5 May 2021 16:19:26 -0400 Subject: [PATCH 02/12] Finishing space only tests --- x-pack/scripts/functional_tests.js | 1 + .../common/lib/authentication/index.ts | 4 +- .../case_api_integration/common/lib/mock.ts | 5 + .../case_api_integration/common/lib/utils.ts | 109 +- .../tests/common/cases/delete_cases.ts | 7 +- .../tests/common/cases/patch_cases.ts | 8 +- .../tests/common/cases/post_case.ts | 4 +- .../tests/common/cases/status/get_status.ts | 2 +- .../tests/common/comments/delete_comment.ts | 2 +- .../tests/common/comments/find_comments.ts | 2 +- .../tests/common/comments/get_all_comments.ts | 2 +- .../tests/common/comments/get_comment.ts | 2 +- .../tests/common/comments/patch_comment.ts | 2 +- .../tests/common/comments/post_comment.ts | 6 +- .../tests/common/configure/get_connectors.ts | 2 +- .../user_actions/get_all_user_actions.ts | 2 +- .../tests/trial/cases/push_case.ts | 6 +- .../spaces_only/config.ts | 1 + .../tests/common/cases/delete_cases.ts | 281 +--- .../tests/common/cases/find_cases.ts | 812 +---------- .../tests/common/cases/get_case.ts | 186 +-- .../tests/common/cases/migrations.ts | 111 -- .../tests/common/cases/patch_cases.ts | 1238 +---------------- .../tests/common/cases/post_case.ts | 280 +--- .../common/cases/reporters/get_reporters.ts | 6 +- .../tests/common/cases/status/get_status.ts | 14 +- .../tests/common/cases/tags/get_tags.ts | 2 +- .../tests/common/comments/delete_comment.ts | 362 +---- .../tests/common/comments/find_comments.ts | 379 +---- .../tests/common/comments/get_all_comments.ts | 208 +-- .../tests/common/comments/get_comment.ts | 139 +- .../tests/common/comments/migrations.ts | 37 - .../tests/common/comments/patch_comment.ts | 581 +------- .../tests/common/comments/post_comment.ts | 606 +------- .../tests/common/configure/get_configure.ts | 190 +-- .../tests/common/configure/get_connectors.ts | 27 - .../tests/common/configure/migrations.ts | 44 - .../tests/common/configure/patch_configure.ts | 226 +-- .../tests/common/configure/post_configure.ts | 274 +--- .../tests/common/connectors/case.ts | 1078 -------------- .../spaces_only/tests/common/index.ts | 9 - .../spaces_only/tests/common/migrations.ts | 18 - .../common/sub_cases/delete_sub_cases.ts | 112 -- .../tests/common/sub_cases/find_sub_cases.ts | 480 ------- .../tests/common/sub_cases/get_sub_case.ts | 119 -- .../tests/common/sub_cases/patch_sub_cases.ts | 515 ------- .../user_actions/get_all_user_actions.ts | 373 +---- .../tests/common/user_actions/migrations.ts | 53 - .../tests/trial/cases/push_case.ts | 300 +--- .../user_actions/get_all_user_actions.ts | 115 -- .../tests/trial/configure/get_configure.ts | 45 +- .../tests/trial/configure/get_connectors.ts | 126 +- .../tests/trial/configure/patch_configure.ts | 90 +- .../tests/trial/configure/post_configure.ts | 13 +- .../spaces_only/tests/{ => trial}/index.ts | 11 +- 55 files changed, 687 insertions(+), 8940 deletions(-) delete mode 100644 x-pack/test/case_api_integration/spaces_only/tests/common/cases/migrations.ts delete mode 100644 x-pack/test/case_api_integration/spaces_only/tests/common/comments/migrations.ts delete mode 100644 x-pack/test/case_api_integration/spaces_only/tests/common/configure/get_connectors.ts delete mode 100644 x-pack/test/case_api_integration/spaces_only/tests/common/configure/migrations.ts delete mode 100644 x-pack/test/case_api_integration/spaces_only/tests/common/connectors/case.ts delete mode 100644 x-pack/test/case_api_integration/spaces_only/tests/common/migrations.ts delete mode 100644 x-pack/test/case_api_integration/spaces_only/tests/common/sub_cases/delete_sub_cases.ts delete mode 100644 x-pack/test/case_api_integration/spaces_only/tests/common/sub_cases/find_sub_cases.ts delete mode 100644 x-pack/test/case_api_integration/spaces_only/tests/common/sub_cases/get_sub_case.ts delete mode 100644 x-pack/test/case_api_integration/spaces_only/tests/common/sub_cases/patch_sub_cases.ts delete mode 100644 x-pack/test/case_api_integration/spaces_only/tests/common/user_actions/migrations.ts delete mode 100644 x-pack/test/case_api_integration/spaces_only/tests/trial/cases/user_actions/get_all_user_actions.ts rename x-pack/test/case_api_integration/spaces_only/tests/{ => trial}/index.ts (61%) diff --git a/x-pack/scripts/functional_tests.js b/x-pack/scripts/functional_tests.js index ebcdef46ebd57b..e2b3c951b07222 100644 --- a/x-pack/scripts/functional_tests.js +++ b/x-pack/scripts/functional_tests.js @@ -36,6 +36,7 @@ const onlyNotInCoverageTests = [ require.resolve('../test/alerting_api_integration/security_and_spaces/config.ts'), require.resolve('../test/case_api_integration/security_and_spaces/config_basic.ts'), require.resolve('../test/case_api_integration/security_and_spaces/config_trial.ts'), + require.resolve('../test/case_api_integration/spaces_only/config.ts'), require.resolve('../test/apm_api_integration/basic/config.ts'), require.resolve('../test/apm_api_integration/trial/config.ts'), require.resolve('../test/apm_api_integration/rules/config.ts'), diff --git a/x-pack/test/case_api_integration/common/lib/authentication/index.ts b/x-pack/test/case_api_integration/common/lib/authentication/index.ts index dfd151344b40c1..a72141745e5777 100644 --- a/x-pack/test/case_api_integration/common/lib/authentication/index.ts +++ b/x-pack/test/case_api_integration/common/lib/authentication/index.ts @@ -7,7 +7,7 @@ import { FtrProviderContext as CommonFtrProviderContext } from '../../../common/ftr_provider_context'; import { Role, User, UserInfo } from './types'; -import { superUser, users } from './users'; +import { users } from './users'; import { roles } from './roles'; import { spaces } from './spaces'; @@ -90,5 +90,3 @@ export const deleteSpacesAndUsers = async (getService: CommonFtrProviderContext[ await deleteSpaces(getService); await deleteUsersAndRoles(getService); }; - -export const superUserSpace1Auth = { user: superUser, space: 'space1' }; diff --git a/x-pack/test/case_api_integration/common/lib/mock.ts b/x-pack/test/case_api_integration/common/lib/mock.ts index 20511f8daab649..015661b0158a1a 100644 --- a/x-pack/test/case_api_integration/common/lib/mock.ts +++ b/x-pack/test/case_api_integration/common/lib/mock.ts @@ -31,6 +31,11 @@ import { } from '../../../../plugins/cases/common/api'; export const defaultUser = { email: null, full_name: null, username: 'elastic' }; +/** + * A null filled user will occur when the security plugin is disabled + */ +export const nullUser = { email: null, full_name: null, username: null }; + export const postCaseReq: CasePostRequest = { description: 'This is a brand new case of a bad meanie defacing data', title: 'Super Bad Security Issue', diff --git a/x-pack/test/case_api_integration/common/lib/utils.ts b/x-pack/test/case_api_integration/common/lib/utils.ts index 5800de50bb9c41..b5409e530644df 100644 --- a/x-pack/test/case_api_integration/common/lib/utils.ts +++ b/x-pack/test/case_api_integration/common/lib/utils.ts @@ -12,6 +12,7 @@ import type { KibanaClient } from '@elastic/elasticsearch/api/kibana'; import * as st from 'supertest'; import supertestAsPromised from 'supertest-as-promised'; +import { ObjectRemover as ActionsRemover } from '../../../alerting_api_integration/common/lib'; import { CASES_URL, CASE_CONFIGURE_CONNECTORS_URL, @@ -45,7 +46,7 @@ import { CasesConfigurationsResponse, CaseUserActionsResponse, } from '../../../../plugins/cases/common/api'; -import { postCollectionReq, postCommentGenAlertReq } from './mock'; +import { getPostCaseRequest, postCollectionReq, postCommentGenAlertReq } from './mock'; import { getCaseUserActionUrl, getSubCasesUrl } from '../../../../plugins/cases/common/api/helpers'; import { ContextTypeGeneratedAlertType } from '../../../../plugins/cases/server/connectors'; import { SignalHit } from '../../../../plugins/security_solution/server/lib/detection_engine/signals/types'; @@ -539,13 +540,13 @@ export const deleteMappings = async (es: KibanaClient): Promise => { }); }; +export const superUserSpace1Auth = getAuthWithSuperUser(); + /** * Returns an auth object with the specified space and user set as super user. The result can be passed to other utility * functions. */ -export function getAuthWithSuperUser( - space: string = 'space1' -): { user: User; space: string | null } { +export function getAuthWithSuperUser(space: string = 'space1'): { user: User; space: string } { return { user: superUser, space }; } @@ -566,6 +567,72 @@ export const ensureSavedObjectIsAuthorized = ( entities.forEach((entity) => expect(owners.includes(entity.owner)).to.be(true)); }; +export const createCaseWithConnector = async ({ + supertest, + configureReq = {}, + servicenowSimulatorURL, + actionsRemover, + auth = { user: superUser, space: null }, + createCaseReq = getPostCaseRequest(), +}: { + supertest: st.SuperTest; + servicenowSimulatorURL: string; + actionsRemover: ActionsRemover; + configureReq?: Record; + auth?: { user: User; space: string | null }; + createCaseReq?: CasePostRequest; +}): Promise<{ + postedCase: CaseResponse; + connector: CreateConnectorResponse; +}> => { + const connector = await createConnector({ + supertest, + req: { + ...getServiceNowConnector(), + config: { apiUrl: servicenowSimulatorURL }, + }, + auth, + }); + + actionsRemover.add(auth.space ?? 'default', connector.id, 'action', 'actions'); + await createConfiguration( + supertest, + { + ...getConfigurationRequest({ + id: connector.id, + name: connector.name, + type: connector.connector_type_id as ConnectorTypes, + }), + ...configureReq, + }, + 200, + auth + ); + + const postedCase = await createCase( + supertest, + { + ...createCaseReq, + connector: { + id: connector.id, + name: connector.name, + type: connector.connector_type_id, + fields: { + urgency: '2', + impact: '2', + severity: '2', + category: 'software', + subcategory: 'os', + }, + } as CaseConnector, + }, + 200, + auth + ); + + return { postedCase, connector }; +}; + export const createCase = async ( supertest: st.SuperTest, params: CasePostRequest, @@ -632,19 +699,6 @@ export const createComment = async ({ return theCase; }; -export const getAllUserAction = async ( - supertest: st.SuperTest, - caseId: string, - expectedHttpCode: number = 200 -): Promise => { - const { body: userActions } = await supertest - .get(`${CASES_URL}/${caseId}/user_actions`) - .set('kbn-xsrf', 'true') - .expect(expectedHttpCode); - - return userActions; -}; - export const updateCase = async ({ supertest, params, @@ -752,13 +806,13 @@ export const getComment = async ({ caseId, commentId, expectedHttpCode = 200, - auth = { user: superUser }, + auth = { user: superUser, space: null }, }: { supertest: st.SuperTest; caseId: string; commentId: string; expectedHttpCode?: number; - auth?: { user: User; space?: string }; + auth?: { user: User; space: string | null }; }): Promise => { const { body: comment } = await supertest .get(`${getSpaceUrlPrefix(auth.space)}${CASES_URL}/${caseId}/comments/${commentId}`) @@ -853,13 +907,18 @@ export const createConnector = async ({ return connector; }; -export const getCaseConnectors = async ( - supertest: st.SuperTest, - expectedHttpCode: number = 200 -): Promise => { +export const getCaseConnectors = async ({ + supertest, + expectedHttpCode = 200, + auth = { user: superUser, space: null }, +}: { + supertest: st.SuperTest; + expectedHttpCode?: number; + auth?: { user: User; space: string | null }; +}): Promise => { const { body: connectors } = await supertest - .get(`${CASE_CONFIGURE_CONNECTORS_URL}/_find`) - .set('kbn-xsrf', 'true') + .get(`${getSpaceUrlPrefix(auth.space)}${CASE_CONFIGURE_CONNECTORS_URL}/_find`) + .auth(auth.user.username, auth.user.password) .expect(expectedHttpCode); return connectors; diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/delete_cases.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/delete_cases.ts index 17aac2dd7e2859..03bcf0d538fe3b 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/delete_cases.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/delete_cases.ts @@ -22,9 +22,10 @@ import { deleteCases, createComment, getComment, - getAllUserAction, removeServerGeneratedPropertiesFromUserAction, getCase, + superUserSpace1Auth, + getCaseUserActions, } from '../../../../common/lib/utils'; import { getSubCaseDetailsUrl } from '../../../../../../plugins/cases/common/api/helpers'; import { CaseResponse } from '../../../../../../plugins/cases/common/api'; @@ -39,8 +40,6 @@ import { superUser, } from '../../../../common/lib/authentication/users'; -import { superUserSpace1Auth } from '../../../../common/lib/authentication'; - // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { const supertestWithoutAuth = getService('supertestWithoutAuth'); @@ -89,7 +88,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should create a user action when creating a case', async () => { const postedCase = await createCase(supertest, getPostCaseRequest()); await deleteCases({ supertest, caseIDs: [postedCase.id] }); - const userActions = await getAllUserAction(supertest, postedCase.id); + const userActions = await getCaseUserActions({ supertest, caseID: postedCase.id }); const creationUserAction = removeServerGeneratedPropertiesFromUserAction(userActions[1]); expect(creationUserAction).to.eql({ diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/patch_cases.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/patch_cases.ts index 674c2c68381b8c..286e08716ebf1e 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/patch_cases.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/patch_cases.ts @@ -32,10 +32,11 @@ import { createCase, createComment, updateCase, - getAllUserAction, + getCaseUserActions, removeServerGeneratedPropertiesFromCase, removeServerGeneratedPropertiesFromUserAction, findCases, + superUserSpace1Auth, } from '../../../../common/lib/utils'; import { createSignalsIndex, @@ -58,7 +59,6 @@ import { secOnlyRead, superUser, } from '../../../../common/lib/authentication/users'; -import { superUserSpace1Auth } from '../../../../common/lib/authentication'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -110,7 +110,7 @@ export default ({ getService }: FtrProviderContext): void => { }, }); - const userActions = await getAllUserAction(supertest, postedCase.id); + const userActions = await getCaseUserActions({ supertest, caseID: postedCase.id }); const statusUserAction = removeServerGeneratedPropertiesFromUserAction(userActions[1]); const data = removeServerGeneratedPropertiesFromCase(patchedCases[0]); @@ -149,7 +149,7 @@ export default ({ getService }: FtrProviderContext): void => { }, }); - const userActions = await getAllUserAction(supertest, postedCase.id); + const userActions = await getCaseUserActions({ supertest, caseID: postedCase.id }); const statusUserAction = removeServerGeneratedPropertiesFromUserAction(userActions[1]); const data = removeServerGeneratedPropertiesFromCase(patchedCases[0]); diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/post_case.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/post_case.ts index 91fb03604b3c4b..50294201f6fbee 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/post_case.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/post_case.ts @@ -22,7 +22,7 @@ import { createCase, removeServerGeneratedPropertiesFromCase, removeServerGeneratedPropertiesFromUserAction, - getAllUserAction, + getCaseUserActions, } from '../../../../common/lib/utils'; import { secOnly, @@ -106,7 +106,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should create a user action when creating a case', async () => { const postedCase = await createCase(supertest, getPostCaseRequest()); - const userActions = await getAllUserAction(supertest, postedCase.id); + const userActions = await getCaseUserActions({ supertest, caseID: postedCase.id }); const creationUserAction = removeServerGeneratedPropertiesFromUserAction(userActions[0]); const { new_value, ...rest } = creationUserAction as CaseUserActionResponse; diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/status/get_status.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/status/get_status.ts index f58dfa1522d4a8..7a17cf1dd8e081 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/status/get_status.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/status/get_status.ts @@ -15,6 +15,7 @@ import { updateCase, getAllCasesStatuses, deleteAllCaseItems, + superUserSpace1Auth, } from '../../../../../common/lib/utils'; import { globalRead, @@ -25,7 +26,6 @@ import { secOnlyRead, superUser, } from '../../../../../common/lib/authentication/users'; -import { superUserSpace1Auth } from '../../../../../common/lib/authentication'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/delete_comment.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/delete_comment.ts index 73b85ef97d1194..b7b97557dcd250 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/delete_comment.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/delete_comment.ts @@ -22,6 +22,7 @@ import { createComment, deleteComment, deleteAllComments, + superUserSpace1Auth, } from '../../../../common/lib/utils'; import { globalRead, @@ -33,7 +34,6 @@ import { secOnlyRead, superUser, } from '../../../../common/lib/authentication/users'; -import { superUserSpace1Auth } from '../../../../common/lib/authentication'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/find_comments.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/find_comments.ts index 0f73b1ee7a624a..2ec99d039dd000 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/find_comments.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/find_comments.ts @@ -28,6 +28,7 @@ import { ensureSavedObjectIsAuthorized, getSpaceUrlPrefix, createCase, + superUserSpace1Auth, } from '../../../../common/lib/utils'; import { @@ -40,7 +41,6 @@ import { globalRead, obsSecRead, } from '../../../../common/lib/authentication/users'; -import { superUserSpace1Auth } from '../../../../common/lib/authentication'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/get_all_comments.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/get_all_comments.ts index 361e72bdc79bf6..25df715b43e9a5 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/get_all_comments.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/get_all_comments.ts @@ -18,6 +18,7 @@ import { createCase, createComment, getAllComments, + superUserSpace1Auth, } from '../../../../common/lib/utils'; import { CommentType } from '../../../../../../plugins/cases/common/api'; import { @@ -31,7 +32,6 @@ import { secOnlyRead, superUser, } from '../../../../common/lib/authentication/users'; -import { superUserSpace1Auth } from '../../../../common/lib/authentication'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/get_comment.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/get_comment.ts index 98b6cc5a7a30c5..5b606e06e84dfc 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/get_comment.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/get_comment.ts @@ -17,6 +17,7 @@ import { createCase, createComment, getComment, + superUserSpace1Auth, } from '../../../../common/lib/utils'; import { CommentType } from '../../../../../../plugins/cases/common/api'; import { @@ -30,7 +31,6 @@ import { secOnlyRead, superUser, } from '../../../../common/lib/authentication/users'; -import { superUserSpace1Auth } from '../../../../common/lib/authentication'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/patch_comment.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/patch_comment.ts index c1f37d5eb2f057..b00a0382bc7122 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/patch_comment.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/patch_comment.ts @@ -34,6 +34,7 @@ import { createCase, createComment, updateComment, + superUserSpace1Auth, } from '../../../../common/lib/utils'; import { globalRead, @@ -45,7 +46,6 @@ import { secOnlyRead, superUser, } from '../../../../common/lib/authentication/users'; -import { superUserSpace1Auth } from '../../../../common/lib/authentication'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/post_comment.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/post_comment.ts index 1fcb49ec10ad4e..a1f24de1b87daf 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/post_comment.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/comments/post_comment.ts @@ -36,9 +36,10 @@ import { deleteComments, createCase, createComment, - getAllUserAction, + getCaseUserActions, removeServerGeneratedPropertiesFromUserAction, removeServerGeneratedPropertiesFromSavedObject, + superUserSpace1Auth, } from '../../../../common/lib/utils'; import { createSignalsIndex, @@ -61,7 +62,6 @@ import { secOnlyRead, superUser, } from '../../../../common/lib/authentication/users'; -import { superUserSpace1Auth } from '../../../../common/lib/authentication'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -140,7 +140,7 @@ export default ({ getService }: FtrProviderContext): void => { caseId: postedCase.id, params: postCommentUserReq, }); - const userActions = await getAllUserAction(supertest, postedCase.id); + const userActions = await getCaseUserActions({ supertest, caseID: postedCase.id }); const commentUserAction = removeServerGeneratedPropertiesFromUserAction(userActions[1]); expect(commentUserAction).to.eql({ diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/get_connectors.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/get_connectors.ts index 5156b9537583f3..46f712ff84aa32 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/get_connectors.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/configure/get_connectors.ts @@ -16,7 +16,7 @@ export default ({ getService }: FtrProviderContext): void => { describe('get_connectors', () => { it('should return an empty find body correctly if no connectors are loaded', async () => { - const connectors = await getCaseConnectors(supertest); + const connectors = await getCaseConnectors({ supertest }); expect(connectors).to.eql([]); }); diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/user_actions/get_all_user_actions.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/user_actions/get_all_user_actions.ts index 5cd4082bd3293b..35ebb1a4bf7b19 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/user_actions/get_all_user_actions.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/user_actions/get_all_user_actions.ts @@ -25,6 +25,7 @@ import { createCase, updateCase, getCaseUserActions, + superUserSpace1Auth, } from '../../../../common/lib/utils'; import { globalRead, @@ -35,7 +36,6 @@ import { secOnlyRead, superUser, } from '../../../../common/lib/authentication/users'; -import { superUserSpace1Auth } from '../../../../common/lib/authentication'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/push_case.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/push_case.ts index 3c096cb7557c32..3901394d2faaec 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/push_case.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/push_case.ts @@ -29,9 +29,10 @@ import { createComment, CreateConnectorResponse, updateCase, - getAllUserAction, + getCaseUserActions, removeServerGeneratedPropertiesFromUserAction, deleteAllCaseItems, + superUserSpace1Auth, } from '../../../../common/lib/utils'; import { ExternalServiceSimulator, @@ -55,7 +56,6 @@ import { superUser, } from '../../../../common/lib/authentication/users'; import { User } from '../../../../common/lib/authentication/types'; -import { superUserSpace1Auth } from '../../../../common/lib/authentication'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -200,7 +200,7 @@ export default ({ getService }: FtrProviderContext): void => { caseId: postedCase.id, connectorId: connector.id, }); - const userActions = await getAllUserAction(supertest, pushedCase.id); + const userActions = await getCaseUserActions({ supertest, caseID: pushedCase.id }); const pushUserAction = removeServerGeneratedPropertiesFromUserAction(userActions[1]); const { new_value, ...rest } = pushUserAction as CaseUserActionResponse; diff --git a/x-pack/test/case_api_integration/spaces_only/config.ts b/x-pack/test/case_api_integration/spaces_only/config.ts index c848a08b1f3df9..53cfdb6f9285db 100644 --- a/x-pack/test/case_api_integration/spaces_only/config.ts +++ b/x-pack/test/case_api_integration/spaces_only/config.ts @@ -12,4 +12,5 @@ export default createTestConfig('spaces_only', { disabledPlugins: ['security'], license: 'trial', ssl: false, + testFiles: [require.resolve('./tests/trial')], }); diff --git a/x-pack/test/case_api_integration/spaces_only/tests/common/cases/delete_cases.ts b/x-pack/test/case_api_integration/spaces_only/tests/common/cases/delete_cases.ts index 17aac2dd7e2859..9de57a1b7abe24 100644 --- a/x-pack/test/case_api_integration/spaces_only/tests/common/cases/delete_cases.ts +++ b/x-pack/test/case_api_integration/spaces_only/tests/common/cases/delete_cases.ts @@ -8,295 +8,46 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; -import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; -import { defaultUser, getPostCaseRequest, postCommentUserReq } from '../../../../common/lib/mock'; +import { getPostCaseRequest } from '../../../../common/lib/mock'; import { - createCaseAction, - createSubCase, deleteAllCaseItems, - deleteCaseAction, - deleteCasesByESQuery, - deleteCasesUserActions, - deleteComments, createCase, deleteCases, - createComment, - getComment, - getAllUserAction, - removeServerGeneratedPropertiesFromUserAction, getCase, + getAuthWithSuperUser, } from '../../../../common/lib/utils'; -import { getSubCaseDetailsUrl } from '../../../../../../plugins/cases/common/api/helpers'; -import { CaseResponse } from '../../../../../../plugins/cases/common/api'; -import { - secOnly, - secOnlyRead, - globalRead, - obsOnlyRead, - obsSecRead, - noKibanaPrivileges, - obsOnly, - superUser, -} from '../../../../common/lib/authentication/users'; - -import { superUserSpace1Auth } from '../../../../common/lib/authentication'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { - const supertestWithoutAuth = getService('supertestWithoutAuth'); const supertest = getService('supertest'); const es = getService('es'); + const authSpace1 = getAuthWithSuperUser(); describe('delete_cases', () => { afterEach(async () => { - await deleteCasesByESQuery(es); - await deleteComments(es); - await deleteCasesUserActions(es); + await deleteAllCaseItems(es); }); - it('should delete a case', async () => { - const postedCase = await createCase(supertest, getPostCaseRequest()); - const body = await deleteCases({ supertest, caseIDs: [postedCase.id] }); + it('should delete a case in space1', async () => { + const postedCase = await createCase(supertest, getPostCaseRequest(), 200, authSpace1); + const body = await deleteCases({ supertest, caseIDs: [postedCase.id], auth: authSpace1 }); + await getCase({ supertest, caseId: postedCase.id, expectedHttpCode: 404, auth: authSpace1 }); expect(body).to.eql({}); }); - it(`should delete a case's comments when that case gets deleted`, async () => { - const postedCase = await createCase(supertest, getPostCaseRequest()); - const patchedCase = await createComment({ - supertest, - caseId: postedCase.id, - params: postCommentUserReq, - }); - // ensure that we can get the comment before deleting the case - await getComment({ - supertest, - caseId: postedCase.id, - commentId: patchedCase.comments![0].id, - }); - - await deleteCases({ supertest, caseIDs: [postedCase.id] }); - - // make sure the comment is now gone - await getComment({ + it('should not delete a case in a different space', async () => { + const postedCase = await createCase(supertest, getPostCaseRequest(), 200, authSpace1); + await deleteCases({ supertest, - caseId: postedCase.id, - commentId: patchedCase.comments![0].id, + caseIDs: [postedCase.id], + auth: getAuthWithSuperUser('space2'), expectedHttpCode: 404, }); - }); - - it('should create a user action when creating a case', async () => { - const postedCase = await createCase(supertest, getPostCaseRequest()); - await deleteCases({ supertest, caseIDs: [postedCase.id] }); - const userActions = await getAllUserAction(supertest, postedCase.id); - const creationUserAction = removeServerGeneratedPropertiesFromUserAction(userActions[1]); - - expect(creationUserAction).to.eql({ - action_field: [ - 'description', - 'status', - 'tags', - 'title', - 'connector', - 'settings', - 'owner', - 'comment', - ], - action: 'delete', - action_by: defaultUser, - old_value: null, - new_value: null, - case_id: `${postedCase.id}`, - comment_id: null, - sub_case_id: '', - owner: 'securitySolutionFixture', - }); - }); - - it('unhappy path - 404s when case is not there', async () => { - await deleteCases({ supertest, caseIDs: ['fake-id'], expectedHttpCode: 404 }); - }); - - // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests - describe.skip('sub cases', () => { - let actionID: string; - before(async () => { - actionID = await createCaseAction(supertest); - }); - after(async () => { - await deleteCaseAction(supertest, actionID); - }); - afterEach(async () => { - await deleteAllCaseItems(es); - }); - - it('should delete the sub cases when deleting a collection', async () => { - const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - expect(caseInfo.subCases![0].id).to.not.eql(undefined); - - const body = await deleteCases({ supertest, caseIDs: [caseInfo.id] }); - - expect(body).to.eql({}); - await supertest - .get(getSubCaseDetailsUrl(caseInfo.id, caseInfo.subCases![0].id)) - .send() - .expect(404); - }); - it(`should delete a sub case's comments when that case gets deleted`, async () => { - const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - expect(caseInfo.subCases![0].id).to.not.eql(undefined); - - // there should be two comments on the sub case now - const { body: patchedCaseWithSubCase }: { body: CaseResponse } = await supertest - .post(`${CASES_URL}/${caseInfo.id}/comments`) - .set('kbn-xsrf', 'true') - .query({ subCaseId: caseInfo.subCases![0].id }) - .send(postCommentUserReq) - .expect(200); - - const subCaseCommentUrl = `${CASES_URL}/${patchedCaseWithSubCase.id}/comments/${ - patchedCaseWithSubCase.comments![1].id - }`; - // make sure we can get the second comment - await supertest.get(subCaseCommentUrl).set('kbn-xsrf', 'true').send().expect(200); - - await deleteCases({ supertest, caseIDs: [caseInfo.id] }); - - await supertest.get(subCaseCommentUrl).set('kbn-xsrf', 'true').send().expect(404); - }); - }); - - describe('rbac', () => { - it('User: security solution only - should delete a case', async () => { - const postedCase = await createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'securitySolutionFixture' }), - 200, - { - user: secOnly, - space: 'space1', - } - ); - - await deleteCases({ - supertest, - caseIDs: [postedCase.id], - expectedHttpCode: 204, - auth: { user: secOnly, space: 'space1' }, - }); - }); - - it('User: security solution only - should NOT delete a case of different owner', async () => { - const postedCase = await createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'securitySolutionFixture' }), - 200, - { - user: secOnly, - space: 'space1', - } - ); - - await deleteCases({ - supertest: supertestWithoutAuth, - caseIDs: [postedCase.id], - expectedHttpCode: 403, - auth: { user: obsOnly, space: 'space1' }, - }); - }); - - it('should get an error if the user has not permissions to all requested cases', async () => { - const caseSec = await createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'securitySolutionFixture' }), - 200, - { - user: secOnly, - space: 'space1', - } - ); - - const caseObs = await createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'observabilityFixture' }), - 200, - { - user: obsOnly, - space: 'space1', - } - ); - - await deleteCases({ - supertest: supertestWithoutAuth, - caseIDs: [caseSec.id, caseObs.id], - expectedHttpCode: 403, - auth: { user: obsOnly, space: 'space1' }, - }); - - // Cases should have not been deleted. - await getCase({ - supertest: supertestWithoutAuth, - caseId: caseSec.id, - expectedHttpCode: 200, - auth: superUserSpace1Auth, - }); - - await getCase({ - supertest: supertestWithoutAuth, - caseId: caseObs.id, - expectedHttpCode: 200, - auth: superUserSpace1Auth, - }); - }); - - for (const user of [globalRead, secOnlyRead, obsOnlyRead, obsSecRead, noKibanaPrivileges]) { - it(`User ${ - user.username - } with role(s) ${user.roles.join()} - should NOT delete a case`, async () => { - const postedCase = await createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'securitySolutionFixture' }), - 200, - { - user: superUser, - space: 'space1', - } - ); - - await deleteCases({ - supertest: supertestWithoutAuth, - caseIDs: [postedCase.id], - expectedHttpCode: 403, - auth: { user, space: 'space1' }, - }); - }); - } - - it('should NOT delete a case in a space with no permissions', async () => { - const postedCase = await createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'securitySolutionFixture' }), - 200, - { - user: superUser, - space: 'space2', - } - ); - - /** - * We expect a 404 because the bulkGet inside the delete - * route should return a 404 when requesting a case from - * a different space. - * */ - await deleteCases({ - supertest: supertestWithoutAuth, - caseIDs: [postedCase.id], - expectedHttpCode: 404, - auth: { user: secOnly, space: 'space1' }, - }); - }); + // the case should still be there + const caseInfo = await getCase({ supertest, caseId: postedCase.id, auth: authSpace1 }); + expect(caseInfo.id).to.eql(postedCase.id); }); }); }; diff --git a/x-pack/test/case_api_integration/spaces_only/tests/common/cases/find_cases.ts b/x-pack/test/case_api_integration/spaces_only/tests/common/cases/find_cases.ts index b7838dd9299bcd..6513fe25b28e9f 100644 --- a/x-pack/test/case_api_integration/spaces_only/tests/common/cases/find_cases.ts +++ b/x-pack/test/case_api_integration/spaces_only/tests/common/cases/find_cases.ts @@ -6,809 +6,57 @@ */ import expect from '@kbn/expect'; -import type { ApiResponse, estypes } from '@elastic/elasticsearch'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; -import { - CASES_URL, - SUB_CASES_PATCH_DEL_URL, -} from '../../../../../../plugins/cases/common/constants'; -import { - postCaseReq, - postCommentUserReq, - findCasesResp, - getPostCaseRequest, -} from '../../../../common/lib/mock'; +import { postCaseReq, findCasesResp } from '../../../../common/lib/mock'; import { deleteAllCaseItems, - createSubCase, - setStatus, - CreateSubCaseResp, - createCaseAction, - deleteCaseAction, - ensureSavedObjectIsAuthorized, findCases, createCase, - updateCase, - createComment, + getAuthWithSuperUser, } from '../../../../common/lib/utils'; -import { CaseResponse, CaseStatuses, CaseType } from '../../../../../../plugins/cases/common/api'; -import { - obsOnly, - secOnly, - obsOnlyRead, - secOnlyRead, - noKibanaPrivileges, - superUser, - globalRead, - obsSecRead, - obsSec, -} from '../../../../common/lib/authentication/users'; - -interface CaseAttributes { - cases: { - title: string; - }; -} // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); const es = getService('es'); - const supertestWithoutAuth = getService('supertestWithoutAuth'); + const authSpace1 = getAuthWithSuperUser(); describe('find_cases', () => { - describe('basic tests', () => { - afterEach(async () => { - await deleteAllCaseItems(es); - }); - - it('should return empty response', async () => { - const cases = await findCases({ supertest }); - expect(cases).to.eql(findCasesResp); - }); - - it('should return cases', async () => { - const a = await createCase(supertest, postCaseReq); - const b = await createCase(supertest, postCaseReq); - const c = await createCase(supertest, postCaseReq); - - const cases = await findCases({ supertest }); - - expect(cases).to.eql({ - ...findCasesResp, - total: 3, - cases: [a, b, c], - count_open_cases: 3, - }); - }); - - it('filters by tags', async () => { - await createCase(supertest, postCaseReq); - const postedCase = await createCase(supertest, { ...postCaseReq, tags: ['unique'] }); - const cases = await findCases({ supertest, query: { tags: ['unique'] } }); - - expect(cases).to.eql({ - ...findCasesResp, - total: 1, - cases: [postedCase], - count_open_cases: 1, - }); - }); - - it('filters by status', async () => { - await createCase(supertest, postCaseReq); - const toCloseCase = await createCase(supertest, postCaseReq); - const patchedCase = await updateCase({ - supertest, - params: { - cases: [ - { - id: toCloseCase.id, - version: toCloseCase.version, - status: CaseStatuses.closed, - }, - ], - }, - }); - - const cases = await findCases({ supertest, query: { status: CaseStatuses.closed } }); - - expect(cases).to.eql({ - ...findCasesResp, - total: 1, - cases: [patchedCase[0]], - count_open_cases: 1, - count_closed_cases: 1, - count_in_progress_cases: 0, - }); - }); - - it('filters by reporters', async () => { - const postedCase = await createCase(supertest, postCaseReq); - const cases = await findCases({ supertest, query: { reporters: 'elastic' } }); - - expect(cases).to.eql({ - ...findCasesResp, - total: 1, - cases: [postedCase], - count_open_cases: 1, - }); - }); - - it('correctly counts comments', async () => { - const postedCase = await createCase(supertest, postCaseReq); - - // post 2 comments - await createComment({ supertest, caseId: postedCase.id, params: postCommentUserReq }); - const patchedCase = await createComment({ - supertest, - caseId: postedCase.id, - params: postCommentUserReq, - }); - - const cases = await findCases({ supertest }); - expect(cases).to.eql({ - ...findCasesResp, - total: 1, - cases: [ - { - ...patchedCase, - comments: [], - totalComment: 2, - }, - ], - count_open_cases: 1, - }); - }); - - it('correctly counts open/closed/in-progress', async () => { - await createCase(supertest, postCaseReq); - const inProgressCase = await createCase(supertest, postCaseReq); - const postedCase = await createCase(supertest, postCaseReq); - - await updateCase({ - supertest, - params: { - cases: [ - { - id: postedCase.id, - version: postedCase.version, - status: CaseStatuses.closed, - }, - ], - }, - }); - - await updateCase({ - supertest, - params: { - cases: [ - { - id: inProgressCase.id, - version: inProgressCase.version, - status: CaseStatuses['in-progress'], - }, - ], - }, - }); - - const cases = await findCases({ supertest }); - expect(cases.count_open_cases).to.eql(1); - expect(cases.count_closed_cases).to.eql(1); - expect(cases.count_in_progress_cases).to.eql(1); - }); - - it('unhappy path - 400s when bad query supplied', async () => { - await findCases({ supertest, query: { perPage: true }, expectedHttpCode: 400 }); - }); - - // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests - describe.skip('stats with sub cases', () => { - let collection: CreateSubCaseResp; - let actionID: string; - before(async () => { - actionID = await createCaseAction(supertest); - }); - after(async () => { - await deleteCaseAction(supertest, actionID); - }); - beforeEach(async () => { - // create a collection with a sub case that is marked as open - collection = await createSubCase({ supertest, actionID }); - - const [, , { body: toCloseCase }] = await Promise.all([ - // set the sub case to in-progress - setStatus({ - supertest, - cases: [ - { - id: collection.newSubCaseInfo.subCases![0].id, - version: collection.newSubCaseInfo.subCases![0].version, - status: CaseStatuses['in-progress'], - }, - ], - type: 'sub_case', - }), - // create two cases that are both open - supertest.post(CASES_URL).set('kbn-xsrf', 'true').send(postCaseReq), - supertest.post(CASES_URL).set('kbn-xsrf', 'true').send(postCaseReq), - ]); - - // set the third case to closed - await setStatus({ - supertest, - cases: [ - { - id: toCloseCase.id, - version: toCloseCase.version, - status: CaseStatuses.closed, - }, - ], - type: 'case', - }); - }); - it('correctly counts stats without using a filter', async () => { - const cases = await findCases({ supertest }); - - expect(cases.total).to.eql(3); - expect(cases.count_closed_cases).to.eql(1); - expect(cases.count_open_cases).to.eql(1); - expect(cases.count_in_progress_cases).to.eql(1); - }); - - it('correctly counts stats with a filter for open cases', async () => { - const cases = await findCases({ supertest, query: { status: CaseStatuses.open } }); - - expect(cases.cases.length).to.eql(1); - - // since we're filtering on status and the collection only has an in-progress case, it should only return the - // individual case that has the open status and no collections - // ENABLE_CASE_CONNECTOR: this value is not correct because it includes a collection - // that does not have an open case. This is a known issue and will need to be resolved - // when this issue is addressed: https://github.com/elastic/kibana/issues/94115 - expect(cases.total).to.eql(2); - expect(cases.count_closed_cases).to.eql(1); - expect(cases.count_open_cases).to.eql(1); - expect(cases.count_in_progress_cases).to.eql(1); - }); - - it('correctly counts stats with a filter for individual cases', async () => { - const cases = await findCases({ supertest, query: { type: CaseType.individual } }); - - expect(cases.total).to.eql(2); - expect(cases.count_closed_cases).to.eql(1); - expect(cases.count_open_cases).to.eql(1); - expect(cases.count_in_progress_cases).to.eql(0); - }); - - it('correctly counts stats with a filter for collection cases with multiple sub cases', async () => { - // this will force the first sub case attached to the collection to be closed - // so we'll have one closed sub case and one open sub case - await createSubCase({ supertest, caseID: collection.newSubCaseInfo.id, actionID }); - const cases = await findCases({ supertest, query: { type: CaseType.collection } }); - - expect(cases.total).to.eql(1); - expect(cases.cases[0].subCases?.length).to.eql(2); - expect(cases.count_closed_cases).to.eql(1); - expect(cases.count_open_cases).to.eql(1); - expect(cases.count_in_progress_cases).to.eql(0); - }); - - it('correctly counts stats with a filter for collection and open cases with multiple sub cases', async () => { - // this will force the first sub case attached to the collection to be closed - // so we'll have one closed sub case and one open sub case - await createSubCase({ supertest, caseID: collection.newSubCaseInfo.id, actionID }); - const cases = await findCases({ - supertest, - query: { - type: CaseType.collection, - status: CaseStatuses.open, - }, - }); - - expect(cases.total).to.eql(1); - expect(cases.cases[0].subCases?.length).to.eql(1); - expect(cases.count_closed_cases).to.eql(1); - expect(cases.count_open_cases).to.eql(1); - expect(cases.count_in_progress_cases).to.eql(0); - }); - - it('correctly counts stats including a collection without sub cases when not filtering on status', async () => { - // delete the sub case on the collection so that it doesn't have any sub cases - await supertest - .delete( - `${SUB_CASES_PATCH_DEL_URL}?ids=["${collection.newSubCaseInfo.subCases![0].id}"]` - ) - .set('kbn-xsrf', 'true') - .send() - .expect(204); - - const cases = await findCases({ supertest, query: { type: CaseType.collection } }); - - // it should include the collection without sub cases because we did not pass in a filter on status - expect(cases.total).to.eql(3); - expect(cases.count_closed_cases).to.eql(1); - expect(cases.count_open_cases).to.eql(1); - expect(cases.count_in_progress_cases).to.eql(0); - }); - - it('correctly counts stats including a collection without sub cases when filtering on tags', async () => { - // delete the sub case on the collection so that it doesn't have any sub cases - await supertest - .delete( - `${SUB_CASES_PATCH_DEL_URL}?ids=["${collection.newSubCaseInfo.subCases![0].id}"]` - ) - .set('kbn-xsrf', 'true') - .send() - .expect(204); - - const cases = await findCases({ supertest, query: { tags: ['defacement'] } }); - - // it should include the collection without sub cases because we did not pass in a filter on status - expect(cases.total).to.eql(3); - expect(cases.count_closed_cases).to.eql(1); - expect(cases.count_open_cases).to.eql(1); - expect(cases.count_in_progress_cases).to.eql(0); - }); - - it('does not return collections without sub cases matching the requested status', async () => { - const cases = await findCases({ supertest, query: { status: CaseStatuses.closed } }); - - expect(cases.cases.length).to.eql(1); - // it should not include the collection that has a sub case as in-progress - // ENABLE_CASE_CONNECTOR: this value is not correct because it includes collections. This short term - // fix for when sub cases are not enabled. When the feature is completed the _find API - // will need to be fixed as explained in this ticket: https://github.com/elastic/kibana/issues/94115 - expect(cases.total).to.eql(2); - expect(cases.count_closed_cases).to.eql(1); - expect(cases.count_open_cases).to.eql(1); - expect(cases.count_in_progress_cases).to.eql(1); - }); - - it('does not return empty collections when filtering on status', async () => { - // delete the sub case on the collection so that it doesn't have any sub cases - await supertest - .delete( - `${SUB_CASES_PATCH_DEL_URL}?ids=["${collection.newSubCaseInfo.subCases![0].id}"]` - ) - .set('kbn-xsrf', 'true') - .send() - .expect(204); - - const cases = await findCases({ supertest, query: { status: CaseStatuses.closed } }); - - expect(cases.cases.length).to.eql(1); - - // ENABLE_CASE_CONNECTOR: this value is not correct because it includes collections. This short term - // fix for when sub cases are not enabled. When the feature is completed the _find API - // will need to be fixed as explained in this ticket: https://github.com/elastic/kibana/issues/94115 - expect(cases.total).to.eql(2); - expect(cases.count_closed_cases).to.eql(1); - expect(cases.count_open_cases).to.eql(1); - expect(cases.count_in_progress_cases).to.eql(0); - }); - }); + afterEach(async () => { + await deleteAllCaseItems(es); }); - describe('find_cases pagination', () => { - const numCases = 10; - before(async () => { - await createCasesWithTitleAsNumber(numCases); - }); - - after(async () => { - await deleteAllCaseItems(es); - }); - - const createCasesWithTitleAsNumber = async (total: number): Promise => { - const responsePromises = []; - for (let i = 0; i < total; i++) { - // this doesn't guarantee that the cases will be created in order that the for-loop executes, - // for example case with title '2', could be created before the case with title '1' since we're doing a promise all here - // A promise all is just much faster than doing it one by one which would have guaranteed that the cases are - // created in the order that the for-loop executes - responsePromises.push(createCase(supertest, { ...postCaseReq, title: `${i}` })); - } - const responses = await Promise.all(responsePromises); - return responses; - }; - - /** - * This is used to retrieve all the cases in the same sorted order that we're expecting them to come back via the - * _find API so that we have a more true comparison instead of using the _find API to get all the cases which - * could mangle the results if the implementation had a bug. - * - * Ideally we could enforce how the cases are created in reasonable time, waiting for each api call to finish takes - * around 30 seconds which seemed too slow - */ - const getAllCasesSortedByCreatedAtAsc = async () => { - const cases: ApiResponse> = await es.search({ - index: '.kibana', - body: { - size: 10000, - sort: [{ 'cases.created_at': { unmapped_type: 'date', order: 'asc' } }], - query: { - term: { type: 'cases' }, - }, - }, - }); - return cases.body.hits.hits.map((hit) => hit._source); - }; - - it('returns the correct total when perPage is less than the total', async () => { - const cases = await findCases({ - supertest, - query: { - page: 1, - perPage: 5, - }, - }); - - expect(cases.cases.length).to.eql(5); - expect(cases.total).to.eql(10); - expect(cases.page).to.eql(1); - expect(cases.per_page).to.eql(5); - expect(cases.count_open_cases).to.eql(10); - expect(cases.count_closed_cases).to.eql(0); - expect(cases.count_in_progress_cases).to.eql(0); - }); - - it('returns the correct total when perPage is greater than the total', async () => { - const cases = await findCases({ - supertest, - query: { - page: 1, - perPage: 11, - }, - }); - - expect(cases.total).to.eql(10); - expect(cases.page).to.eql(1); - expect(cases.per_page).to.eql(11); - expect(cases.cases.length).to.eql(10); - expect(cases.count_open_cases).to.eql(10); - expect(cases.count_closed_cases).to.eql(0); - expect(cases.count_in_progress_cases).to.eql(0); - }); - - it('returns the correct total when perPage is equal to the total', async () => { - const cases = await findCases({ - supertest, - query: { - page: 1, - perPage: 10, - }, - }); - - expect(cases.total).to.eql(10); - expect(cases.page).to.eql(1); - expect(cases.per_page).to.eql(10); - expect(cases.cases.length).to.eql(10); - expect(cases.count_open_cases).to.eql(10); - expect(cases.count_closed_cases).to.eql(0); - expect(cases.count_in_progress_cases).to.eql(0); - }); - - it('returns the second page of results', async () => { - const perPage = 5; - const cases = await findCases({ - supertest, - query: { - page: 2, - perPage, - }, - }); - - expect(cases.total).to.eql(10); - expect(cases.page).to.eql(2); - expect(cases.per_page).to.eql(5); - expect(cases.cases.length).to.eql(5); - expect(cases.count_open_cases).to.eql(10); - expect(cases.count_closed_cases).to.eql(0); - expect(cases.count_in_progress_cases).to.eql(0); - - const allCases = await getAllCasesSortedByCreatedAtAsc(); - - cases.cases.map((caseInfo, index) => { - // we started on the second page of 10 cases with a perPage of 5, so the first case should 0 + 5 (index + perPage) - expect(caseInfo.title).to.eql(allCases[index + perPage]?.cases.title); - }); - }); - - it('paginates with perPage of 2 through 10 total cases', async () => { - const total = 10; - const perPage = 2; - - // it's less than or equal here because the page starts at 1, so page 5 is a valid page number - // and should have case titles 9, and 10 - for (let currentPage = 1; currentPage <= total / perPage; currentPage++) { - const cases = await findCases({ - supertest, - query: { - page: currentPage, - perPage, - }, - }); - - expect(cases.total).to.eql(total); - expect(cases.page).to.eql(currentPage); - expect(cases.per_page).to.eql(perPage); - expect(cases.cases.length).to.eql(perPage); - expect(cases.count_open_cases).to.eql(total); - expect(cases.count_closed_cases).to.eql(0); - expect(cases.count_in_progress_cases).to.eql(0); - - const allCases = await getAllCasesSortedByCreatedAtAsc(); + it('should return 3 cases in space1', async () => { + const a = await createCase(supertest, postCaseReq, 200, authSpace1); + const b = await createCase(supertest, postCaseReq, 200, authSpace1); + const c = await createCase(supertest, postCaseReq, 200, authSpace1); - cases.cases.map((caseInfo, index) => { - // for page 1, the cases tiles should be 0,1,2 for page 2: 3,4,5 etc (assuming the titles were sorted - // correctly) - expect(caseInfo.title).to.eql( - allCases[index + perPage * (currentPage - 1)]?.cases.title - ); - }); - } - }); - - it('retrieves the last three cases', async () => { - const cases = await findCases({ - supertest, - query: { - // this should skip the first 7 cases and only return the last 3 - page: 2, - perPage: 7, - }, - }); + const cases = await findCases({ supertest, auth: authSpace1 }); - expect(cases.total).to.eql(10); - expect(cases.page).to.eql(2); - expect(cases.per_page).to.eql(7); - expect(cases.cases.length).to.eql(3); - expect(cases.count_open_cases).to.eql(10); - expect(cases.count_closed_cases).to.eql(0); - expect(cases.count_in_progress_cases).to.eql(0); + expect(cases).to.eql({ + ...findCasesResp, + total: 3, + cases: [a, b, c], + count_open_cases: 3, }); }); - describe('rbac', () => { - afterEach(async () => { - await deleteAllCaseItems(es); - }); - - it('should return the correct cases', async () => { - await Promise.all([ - // Create case owned by the security solution user - createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'securitySolutionFixture' }), - 200, - { - user: secOnly, - space: 'space1', - } - ), - // Create case owned by the observability user - createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'observabilityFixture' }), - 200, - { - user: obsOnly, - space: 'space1', - } - ), - ]); - - for (const scenario of [ - { - user: globalRead, - numberOfExpectedCases: 2, - owners: ['securitySolutionFixture', 'observabilityFixture'], - }, - { - user: superUser, - numberOfExpectedCases: 2, - owners: ['securitySolutionFixture', 'observabilityFixture'], - }, - { user: secOnlyRead, numberOfExpectedCases: 1, owners: ['securitySolutionFixture'] }, - { user: obsOnlyRead, numberOfExpectedCases: 1, owners: ['observabilityFixture'] }, - { - user: obsSecRead, - numberOfExpectedCases: 2, - owners: ['securitySolutionFixture', 'observabilityFixture'], - }, - ]) { - const res = await findCases({ - supertest: supertestWithoutAuth, - auth: { - user: scenario.user, - space: 'space1', - }, - }); - - ensureSavedObjectIsAuthorized(res.cases, scenario.numberOfExpectedCases, scenario.owners); - } - }); - - for (const scenario of [ - { user: noKibanaPrivileges, space: 'space1' }, - { user: secOnly, space: 'space2' }, - ]) { - it(`User ${scenario.user.username} with role(s) ${scenario.user.roles.join()} and space ${ - scenario.space - } - should NOT read a case`, async () => { - // super user creates a case at the appropriate space - await createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'securitySolutionFixture' }), - 200, - { - user: superUser, - space: scenario.space, - } - ); - - // user should not be able to read cases at the appropriate space - await findCases({ - supertest: supertestWithoutAuth, - auth: { - user: scenario.user, - space: scenario.space, - }, - expectedHttpCode: 403, - }); - }); - } - - it('should return the correct cases when trying to exploit RBAC through the search query parameter', async () => { - await Promise.all([ - // super user creates a case with owner securitySolutionFixture - createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'securitySolutionFixture' }), - 200, - { - user: superUser, - space: 'space1', - } - ), - // super user creates a case with owner observabilityFixture - createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'observabilityFixture' }), - 200, - { - user: superUser, - space: 'space1', - } - ), - ]); - - const res = await findCases({ - supertest: supertestWithoutAuth, - query: { - search: 'securitySolutionFixture observabilityFixture', - searchFields: 'owner', - }, - auth: { - user: secOnly, - space: 'space1', - }, - }); - - ensureSavedObjectIsAuthorized(res.cases, 1, ['securitySolutionFixture']); - }); - - // This test is to prevent a future developer to add the filter attribute without taking into consideration - // the authorizationFilter produced by the cases authorization class - it('should NOT allow to pass a filter query parameter', async () => { - await supertest - .get( - `${CASES_URL}/_find?sortOrder=asc&filter=cases.attributes.owner:"observabilityFixture"` - ) - .set('kbn-xsrf', 'true') - .send() - .expect(400); - }); - - // This test ensures that the user is not allowed to define the namespaces query param - // so she cannot search across spaces - it('should NOT allow to pass a namespaces query parameter', async () => { - await supertest - .get(`${CASES_URL}/_find?sortOrder=asc&namespaces[0]=*`) - .set('kbn-xsrf', 'true') - .send() - .expect(400); - - await supertest - .get(`${CASES_URL}/_find?sortOrder=asc&namespaces=*`) - .set('kbn-xsrf', 'true') - .send() - .expect(400); - }); - - it('should NOT allow to pass a non supported query parameter', async () => { - await supertest - .get(`${CASES_URL}/_find?notExists=papa`) - .set('kbn-xsrf', 'true') - .send() - .expect(400); - }); - - it('should respect the owner filter when having permissions', async () => { - await Promise.all([ - createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'securitySolutionFixture' }), - 200, - { - user: obsSec, - space: 'space1', - } - ), - createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'observabilityFixture' }), - 200, - { - user: obsSec, - space: 'space1', - } - ), - ]); - - const res = await findCases({ - supertest: supertestWithoutAuth, - query: { - owner: 'securitySolutionFixture', - searchFields: 'owner', - }, - auth: { - user: obsSec, - space: 'space1', - }, - }); - - ensureSavedObjectIsAuthorized(res.cases, 1, ['securitySolutionFixture']); - }); - - it('should return the correct cases when trying to exploit RBAC through the owner query parameter', async () => { - await Promise.all([ - createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'securitySolutionFixture' }), - 200, - { - user: obsSec, - space: 'space1', - } - ), - createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'observabilityFixture' }), - 200, - { - user: obsSec, - space: 'space1', - } - ), - ]); - - // User with permissions only to security solution request cases from observability - const res = await findCases({ - supertest: supertestWithoutAuth, - query: { - owner: ['securitySolutionFixture', 'observabilityFixture'], - }, - auth: { - user: secOnly, - space: 'space1', - }, - }); - - // Only security solution cases are being returned - ensureSavedObjectIsAuthorized(res.cases, 1, ['securitySolutionFixture']); + it('should return 1 case in space2 when 2 cases were created in space1 and 1 in space2', async () => { + const authSpace2 = getAuthWithSuperUser('space2'); + const [, , space2Case] = await Promise.all([ + createCase(supertest, postCaseReq, 200, authSpace1), + createCase(supertest, postCaseReq, 200, authSpace1), + createCase(supertest, postCaseReq, 200, authSpace2), + ]); + + const cases = await findCases({ supertest, auth: authSpace2 }); + + expect(cases).to.eql({ + ...findCasesResp, + total: 1, + cases: [space2Case], + count_open_cases: 1, }); }); }); diff --git a/x-pack/test/case_api_integration/spaces_only/tests/common/cases/get_case.ts b/x-pack/test/case_api_integration/spaces_only/tests/common/cases/get_case.ts index 222632b41c297c..3ea6fac3772edb 100644 --- a/x-pack/test/case_api_integration/spaces_only/tests/common/cases/get_case.ts +++ b/x-pack/test/case_api_integration/spaces_only/tests/common/cases/get_case.ts @@ -8,199 +8,41 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; -import { AttributesTypeUser } from '../../../../../../plugins/cases/common/api'; -import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; -import { - defaultUser, - postCaseReq, - postCaseResp, - postCommentUserReq, - getPostCaseRequest, -} from '../../../../common/lib/mock'; +import { postCaseResp, getPostCaseRequest, nullUser } from '../../../../common/lib/mock'; import { deleteCasesByESQuery, createCase, getCase, - createComment, removeServerGeneratedPropertiesFromCase, - removeServerGeneratedPropertiesFromSavedObject, + getAuthWithSuperUser, } from '../../../../common/lib/utils'; -import { - secOnly, - obsOnly, - globalRead, - superUser, - secOnlyRead, - obsOnlyRead, - obsSecRead, - noKibanaPrivileges, - obsSec, -} from '../../../../common/lib/authentication/users'; -import { getUserInfo } from '../../../../common/lib/authentication'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); - const supertestWithoutAuth = getService('supertestWithoutAuth'); const es = getService('es'); + const authSpace1 = getAuthWithSuperUser(); describe('get_case', () => { afterEach(async () => { await deleteCasesByESQuery(es); }); - it('should return a case with no comments', async () => { - const postedCase = await createCase(supertest, getPostCaseRequest()); - const theCase = await getCase({ supertest, caseId: postedCase.id, includeComments: true }); + it('should return a case in space1', async () => { + const postedCase = await createCase(supertest, getPostCaseRequest(), 200, authSpace1); + const theCase = await getCase({ supertest, caseId: postedCase.id, auth: authSpace1 }); const data = removeServerGeneratedPropertiesFromCase(theCase); - expect(data).to.eql(postCaseResp()); - expect(data.comments?.length).to.eql(0); - }); - - it('should return a case with comments', async () => { - const postedCase = await createCase(supertest, postCaseReq); - await createComment({ supertest, caseId: postedCase.id, params: postCommentUserReq }); - const theCase = await getCase({ supertest, caseId: postedCase.id, includeComments: true }); - - const comment = removeServerGeneratedPropertiesFromSavedObject( - theCase.comments![0] as AttributesTypeUser - ); - - expect(theCase.comments?.length).to.eql(1); - expect(comment).to.eql({ - type: postCommentUserReq.type, - comment: postCommentUserReq.comment, - associationType: 'case', - created_by: defaultUser, - pushed_at: null, - pushed_by: null, - updated_by: null, - owner: 'securitySolutionFixture', - }); + expect(data).to.eql({ ...postCaseResp(), created_by: nullUser }); }); - it('should return a 400 when passing the includeSubCaseComments', async () => { - const { body } = await supertest - .get(`${CASES_URL}/case-id?includeSubCaseComments=true`) - .set('kbn-xsrf', 'true') - .send() - .expect(400); - - expect(body.message).to.contain('disabled'); - }); - - it('unhappy path - 404s when case is not there', async () => { - await supertest.get(`${CASES_URL}/fake-id`).set('kbn-xsrf', 'true').send().expect(404); - }); - - describe('rbac', () => { - it('should get a case', async () => { - const newCase = await createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'securitySolutionFixture' }), - 200, - { - user: superUser, - space: 'space1', - } - ); - - for (const user of [globalRead, superUser, secOnly, secOnlyRead, obsSec, obsSecRead]) { - const theCase = await getCase({ - supertest: supertestWithoutAuth, - caseId: newCase.id, - auth: { user, space: 'space1' }, - }); - - expect(theCase.owner).to.eql('securitySolutionFixture'); - } - }); - - it('should get a case with comments', async () => { - const postedCase = await createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'securitySolutionFixture' }), - 200, - { - user: secOnly, - space: 'space1', - } - ); - - await createComment({ - supertest: supertestWithoutAuth, - caseId: postedCase.id, - params: postCommentUserReq, - expectedHttpCode: 200, - auth: { - user: secOnly, - space: 'space1', - }, - }); - - const theCase = await getCase({ - supertest: supertestWithoutAuth, - caseId: postedCase.id, - includeComments: true, - auth: { user: secOnly, space: 'space1' }, - }); - - const comment = removeServerGeneratedPropertiesFromSavedObject( - theCase.comments![0] as AttributesTypeUser - ); - - expect(theCase.comments?.length).to.eql(1); - expect(comment).to.eql({ - type: postCommentUserReq.type, - comment: postCommentUserReq.comment, - associationType: 'case', - created_by: getUserInfo(secOnly), - pushed_at: null, - pushed_by: null, - updated_by: null, - owner: 'securitySolutionFixture', - }); - }); - - it('should not get a case when the user does not have access to owner', async () => { - const newCase = await createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'securitySolutionFixture' }), - 200, - { - user: superUser, - space: 'space1', - } - ); - - for (const user of [noKibanaPrivileges, obsOnly, obsOnlyRead]) { - await getCase({ - supertest: supertestWithoutAuth, - caseId: newCase.id, - expectedHttpCode: 403, - auth: { user, space: 'space1' }, - }); - } - }); - - it('should NOT get a case in a space with no permissions', async () => { - const newCase = await createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'securitySolutionFixture' }), - 200, - { - user: superUser, - space: 'space2', - } - ); - - await getCase({ - supertest: supertestWithoutAuth, - caseId: newCase.id, - expectedHttpCode: 403, - auth: { user: secOnly, space: 'space2' }, - }); + it('should not return a case in the wrong space', async () => { + const postedCase = await createCase(supertest, getPostCaseRequest(), 200, authSpace1); + await getCase({ + supertest, + caseId: postedCase.id, + auth: getAuthWithSuperUser('space2'), + expectedHttpCode: 404, }); }); }); diff --git a/x-pack/test/case_api_integration/spaces_only/tests/common/cases/migrations.ts b/x-pack/test/case_api_integration/spaces_only/tests/common/cases/migrations.ts deleted file mode 100644 index 42fcace768b157..00000000000000 --- a/x-pack/test/case_api_integration/spaces_only/tests/common/cases/migrations.ts +++ /dev/null @@ -1,111 +0,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 expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; -import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; - -// eslint-disable-next-line import/no-default-export -export default function createGetTests({ getService }: FtrProviderContext) { - const supertest = getService('supertest'); - const esArchiver = getService('esArchiver'); - - describe('migrations', () => { - // tests upgrading a 7.10.0 saved object to the latest version - describe('7.10.0 -> latest stack version', () => { - before(async () => { - await esArchiver.load('cases/migrations/7.10.0'); - }); - - after(async () => { - await esArchiver.unload('cases/migrations/7.10.0'); - }); - - it('migrates cases connector', async () => { - const { body } = await supertest - .get(`${CASES_URL}/e1900ac0-017f-11eb-93f8-d161651bf509`) - .set('kbn-xsrf', 'true') - .send() - .expect(200); - - expect(body).key('connector'); - expect(body).not.key('connector_id'); - expect(body.connector).to.eql({ - id: 'connector-1', - name: 'none', - type: '.none', - fields: null, - }); - }); - - it('migrates cases settings', async () => { - const { body } = await supertest - .get(`${CASES_URL}/e1900ac0-017f-11eb-93f8-d161651bf509`) - .set('kbn-xsrf', 'true') - .send() - .expect(200); - - expect(body).key('settings'); - expect(body.settings).to.eql({ - syncAlerts: true, - }); - }); - }); - - // tests upgrading a 7.11.1 saved object to the latest version - describe('7.11.1 -> latest stack version', () => { - before(async () => { - await esArchiver.load('cases/migrations/7.11.1'); - }); - - after(async () => { - await esArchiver.unload('cases/migrations/7.11.1'); - }); - - it('adds rule info to only alert comments for 7.12', async () => { - const caseID = '2ea28c10-7855-11eb-9ca6-83ec5acb735f'; - // user comment - let { body } = await supertest - .get(`${CASES_URL}/${caseID}/comments/34a20a00-7855-11eb-9ca6-83ec5acb735f`) - .expect(200); - - expect(body).not.key('rule'); - expect(body.rule).to.eql(undefined); - - // alert comment - ({ body } = await supertest - .get(`${CASES_URL}/${caseID}/comments/3178f2b0-7857-11eb-9ca6-83ec5acb735f`) - .expect(200)); - - expect(body).key('rule'); - expect(body.rule).to.eql({ id: null, name: null }); - }); - - it('adds category and subcategory to the ITSM connector', async () => { - const { body } = await supertest - .get(`${CASES_URL}/6f973440-7abd-11eb-9ca6-83ec5acb735f`) - .set('kbn-xsrf', 'true') - .send() - .expect(200); - - expect(body).key('connector'); - expect(body.connector).to.eql({ - id: '444ebab0-7abd-11eb-9ca6-83ec5acb735f', - name: 'SN', - type: '.servicenow', - fields: { - impact: '2', - severity: '2', - urgency: '2', - category: null, - subcategory: null, - }, - }); - }); - }); - }); -} diff --git a/x-pack/test/case_api_integration/spaces_only/tests/common/cases/patch_cases.ts b/x-pack/test/case_api_integration/spaces_only/tests/common/cases/patch_cases.ts index 674c2c68381b8c..361358dc40604f 100644 --- a/x-pack/test/case_api_integration/spaces_only/tests/common/cases/patch_cases.ts +++ b/x-pack/test/case_api_integration/spaces_only/tests/common/cases/patch_cases.ts @@ -8,1232 +8,66 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; -import { DETECTION_ENGINE_QUERY_SIGNALS_URL } from '../../../../../../plugins/security_solution/common/constants'; -import { - CasesResponse, - CaseStatuses, - CaseType, - CommentType, - ConnectorTypes, -} from '../../../../../../plugins/cases/common/api'; -import { - defaultUser, - getPostCaseRequest, - postCaseReq, - postCaseResp, - postCollectionReq, - postCommentAlertReq, - postCommentUserReq, -} from '../../../../common/lib/mock'; +import { nullUser, postCaseReq, postCaseResp } from '../../../../common/lib/mock'; import { deleteAllCaseItems, - getSignalsWithES, - setStatus, createCase, - createComment, updateCase, - getAllUserAction, removeServerGeneratedPropertiesFromCase, - removeServerGeneratedPropertiesFromUserAction, - findCases, + getAuthWithSuperUser, } from '../../../../common/lib/utils'; -import { - createSignalsIndex, - deleteSignalsIndex, - deleteAllAlerts, - getRuleForSignalTesting, - waitForRuleSuccessOrStatus, - waitForSignalsToBePresent, - getSignalsByIds, - createRule, - getQuerySignalIds, -} from '../../../../../detection_engine_api_integration/utils'; -import { - globalRead, - noKibanaPrivileges, - obsOnly, - obsOnlyRead, - obsSecRead, - secOnly, - secOnlyRead, - superUser, -} from '../../../../common/lib/authentication/users'; -import { superUserSpace1Auth } from '../../../../common/lib/authentication'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); - const esArchiver = getService('esArchiver'); const es = getService('es'); + const authSpace1 = getAuthWithSuperUser(); describe('patch_cases', () => { afterEach(async () => { await deleteAllCaseItems(es); }); - describe('happy path', () => { - it('should patch a case', async () => { - const postedCase = await createCase(supertest, postCaseReq); - const patchedCases = await updateCase({ - supertest, - params: { - cases: [ - { - id: postedCase.id, - version: postedCase.version, - title: 'new title', - }, - ], - }, - }); - - const data = removeServerGeneratedPropertiesFromCase(patchedCases[0]); - expect(data).to.eql({ - ...postCaseResp(), - title: 'new title', - updated_by: defaultUser, - }); - }); - - it('should closes the case correctly', async () => { - const postedCase = await createCase(supertest, postCaseReq); - const patchedCases = await updateCase({ - supertest, - params: { - cases: [ - { - id: postedCase.id, - version: postedCase.version, - status: CaseStatuses.closed, - }, - ], - }, - }); - - const userActions = await getAllUserAction(supertest, postedCase.id); - const statusUserAction = removeServerGeneratedPropertiesFromUserAction(userActions[1]); - const data = removeServerGeneratedPropertiesFromCase(patchedCases[0]); - - expect(data).to.eql({ - ...postCaseResp(), - status: CaseStatuses.closed, - closed_by: defaultUser, - updated_by: defaultUser, - }); - - expect(statusUserAction).to.eql({ - action_field: ['status'], - action: 'update', - action_by: defaultUser, - new_value: CaseStatuses.closed, - old_value: CaseStatuses.open, - case_id: `${postedCase.id}`, - comment_id: null, - sub_case_id: '', - owner: 'securitySolutionFixture', - }); - }); - - it('should change the status of case to in-progress correctly', async () => { - const postedCase = await createCase(supertest, postCaseReq); - const patchedCases = await updateCase({ - supertest, - params: { - cases: [ - { - id: postedCase.id, - version: postedCase.version, - status: CaseStatuses['in-progress'], - }, - ], - }, - }); - - const userActions = await getAllUserAction(supertest, postedCase.id); - const statusUserAction = removeServerGeneratedPropertiesFromUserAction(userActions[1]); - const data = removeServerGeneratedPropertiesFromCase(patchedCases[0]); - - expect(data).to.eql({ - ...postCaseResp(), - status: CaseStatuses['in-progress'], - updated_by: defaultUser, - }); - - expect(statusUserAction).to.eql({ - action_field: ['status'], - action: 'update', - action_by: defaultUser, - new_value: CaseStatuses['in-progress'], - old_value: CaseStatuses.open, - case_id: `${postedCase.id}`, - comment_id: null, - sub_case_id: '', - owner: 'securitySolutionFixture', - }); - }); - - it('should patch a case with new connector', async () => { - const postedCase = await createCase(supertest, postCaseReq); - const patchedCases = await updateCase({ - supertest, - params: { - cases: [ - { - id: postedCase.id, - version: postedCase.version, - connector: { - id: 'jira', - name: 'Jira', - type: ConnectorTypes.jira, - fields: { issueType: 'Task', priority: null, parent: null }, - }, - }, - ], - }, - }); - - const data = removeServerGeneratedPropertiesFromCase(patchedCases[0]); - expect(data).to.eql({ - ...postCaseResp(), - connector: { - id: 'jira', - name: 'Jira', - type: '.jira', - fields: { issueType: 'Task', priority: null, parent: null }, - }, - updated_by: defaultUser, - }); - }); - - // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests - it.skip('should allow converting an individual case to a collection when it does not have alerts', async () => { - const postedCase = await createCase(supertest, postCaseReq); - const patchedCase = await createComment({ - supertest, - caseId: postedCase.id, - params: postCommentUserReq, - }); - await updateCase({ - supertest, - params: { - cases: [ - { - id: patchedCase.id, - version: patchedCase.version, - type: CaseType.collection, - }, - ], - }, - }); - }); - }); - - describe('unhappy path', () => { - it('400s when attempting to change the owner of a case', async () => { - const postedCase = await createCase(supertest, postCaseReq); - await updateCase({ - supertest, - params: { - cases: [ - { - id: postedCase.id, - version: postedCase.version, - owner: 'observabilityFixture', - }, - ], - }, - expectedHttpCode: 400, - }); - }); - - it('404s when case is not there', async () => { - await updateCase({ - supertest, - params: { - cases: [ - { - id: 'not-real', - version: 'version', - status: CaseStatuses.closed, - }, - ], - }, - expectedHttpCode: 404, - }); - }); - - it('400s when id is missing', async () => { - await updateCase({ - supertest, - params: { - cases: [ - // @ts-expect-error - { - version: 'version', - status: CaseStatuses.closed, - }, - ], - }, - expectedHttpCode: 400, - }); - }); - - it('406s when fields are identical', async () => { - const postedCase = await createCase(supertest, postCaseReq); - await updateCase({ - supertest, - params: { - cases: [ - { - id: postedCase.id, - version: postedCase.version, - status: CaseStatuses.open, - }, - ], - }, - expectedHttpCode: 406, - }); - }); - - it('400s when version is missing', async () => { - await updateCase({ - supertest, - params: { - cases: [ - // @ts-expect-error - { - id: 'not-real', - status: CaseStatuses.closed, - }, - ], - }, - expectedHttpCode: 400, - }); - }); - - // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests - it.skip('should 400 and not allow converting a collection back to an individual case', async () => { - const postedCase = await createCase(supertest, postCollectionReq); - await updateCase({ - supertest, - params: { - cases: [ - { - id: postedCase.id, - version: postedCase.version, - type: CaseType.individual, - }, - ], - }, - expectedHttpCode: 400, - }); - }); - - it('406s when excess data sent', async () => { - const postedCase = await createCase(supertest, postCaseReq); - await updateCase({ - supertest, - params: { - cases: [ - { - id: postedCase.id, - version: postedCase.version, - // @ts-expect-error - badKey: 'closed', - }, - ], - }, - expectedHttpCode: 406, - }); - }); - - it('400s when bad data sent', async () => { - const postedCase = await createCase(supertest, postCaseReq); - await updateCase({ - supertest, - params: { - cases: [ - { - id: postedCase.id, - version: postedCase.version, - // @ts-expect-error - status: true, - }, - ], - }, - expectedHttpCode: 400, - }); - }); - - it('400s when unsupported status sent', async () => { - const postedCase = await createCase(supertest, postCaseReq); - await updateCase({ - supertest, - params: { - cases: [ - { - id: postedCase.id, - version: postedCase.version, - // @ts-expect-error - status: 'not-supported', - }, - ], - }, - expectedHttpCode: 400, - }); - }); - - it('400s when bad connector type sent', async () => { - const postedCase = await createCase(supertest, postCaseReq); - await updateCase({ - supertest, - params: { - cases: [ - { - id: postedCase.id, - version: postedCase.version, - // @ts-expect-error - connector: { id: 'none', name: 'none', type: '.not-exists', fields: null }, - }, - ], - }, - expectedHttpCode: 400, - }); - }); - - it('400s when bad connector sent', async () => { - const postedCase = await createCase(supertest, postCaseReq); - await updateCase({ - supertest, - params: { - cases: [ - { - id: postedCase.id, - version: postedCase.version, - connector: { - id: 'jira', - name: 'Jira', - // @ts-expect-error - type: ConnectorTypes.jira, - // @ts-expect-error - fields: { unsupported: 'value' }, - }, - }, - ], - }, - expectedHttpCode: 400, - }); - }); - - it('409s when version does not match', async () => { - const postedCase = await createCase(supertest, postCaseReq); - await updateCase({ - supertest, - params: { - cases: [ - { - id: postedCase.id, - version: 'version', - // @ts-expect-error - status: 'closed', - }, - ], - }, - expectedHttpCode: 409, - }); - }); - - it('should 400 when attempting to update an individual case to a collection when it has alerts attached to it', async () => { - const postedCase = await createCase(supertest, postCaseReq); - const patchedCase = await createComment({ - supertest, - caseId: postedCase.id, - params: postCommentAlertReq, - }); - await updateCase({ - supertest, - params: { - cases: [ - { - id: patchedCase.id, - version: patchedCase.version, - type: CaseType.collection, - }, - ], - }, - expectedHttpCode: 400, - }); - }); - - // ENABLE_CASE_CONNECTOR: once the case connector feature is completed delete these tests - it('should 400 when attempting to update the case type when the case connector feature is disabled', async () => { - const postedCase = await createCase(supertest, postCaseReq); - await updateCase({ - supertest, - params: { - cases: [ - { - id: postedCase.id, - version: postedCase.version, - type: CaseType.collection, - }, - ], - }, - expectedHttpCode: 400, - }); - }); - - // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests - it.skip("should 400 when attempting to update a collection case's status", async () => { - const postedCase = await createCase(supertest, postCollectionReq); - await updateCase({ - supertest, - params: { - cases: [ - { - id: postedCase.id, - version: postedCase.version, - status: CaseStatuses.closed, - }, - ], - }, - expectedHttpCode: 400, - }); - }); - }); - - describe('alerts', () => { - describe('esArchiver', () => { - const defaultSignalsIndex = '.siem-signals-default-000001'; - - beforeEach(async () => { - await esArchiver.load('cases/signals/default'); - }); - afterEach(async () => { - await esArchiver.unload('cases/signals/default'); - await deleteAllCaseItems(es); - }); - - it('should update the status of multiple alerts attached to multiple cases', async () => { - const signalID = '5f2b9ec41f8febb1c06b5d1045aeabb9874733b7617e88a370510f2fb3a41a5d'; - const signalID2 = '4d0f4b1533e46b66b43bdd0330d23f39f2cf42a7253153270e38d30cce9ff0c6'; - - // does NOT updates alert status when adding comments and syncAlerts=false - const individualCase1 = await createCase(supertest, { - ...postCaseReq, - settings: { - syncAlerts: false, - }, - }); - - const updatedInd1WithComment = await createComment({ - supertest, - caseId: individualCase1.id, - params: { - alertId: signalID, - index: defaultSignalsIndex, - rule: { id: 'test-rule-id', name: 'test-index-id' }, - type: CommentType.alert, - owner: 'securitySolutionFixture', - }, - }); - - const individualCase2 = await createCase(supertest, { - ...postCaseReq, - settings: { - syncAlerts: false, - }, - }); - - const updatedInd2WithComment = await createComment({ - supertest, - caseId: individualCase2.id, - params: { - alertId: signalID2, - index: defaultSignalsIndex, - rule: { id: 'test-rule-id', name: 'test-index-id' }, - type: CommentType.alert, - owner: 'securitySolutionFixture', - }, - }); - - await es.indices.refresh({ index: defaultSignalsIndex }); - - let signals = await getSignalsWithES({ - es, - indices: defaultSignalsIndex, - ids: [signalID, signalID2], - }); - - // There should be no change in their status since syncing is disabled - expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( - CaseStatuses.open - ); - expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source?.signal.status).to.be( - CaseStatuses.open - ); - - // does NOT updates alert status when the status is updated and syncAlerts=false - const updatedIndWithStatus: CasesResponse = (await setStatus({ - supertest, - cases: [ - { - id: updatedInd1WithComment.id, - version: updatedInd1WithComment.version, - status: CaseStatuses.closed, - }, - { - id: updatedInd2WithComment.id, - version: updatedInd2WithComment.version, - status: CaseStatuses['in-progress'], - }, - ], - type: 'case', - })) as CasesResponse; - - await es.indices.refresh({ index: defaultSignalsIndex }); - - signals = await getSignalsWithES({ - es, - indices: defaultSignalsIndex, - ids: [signalID, signalID2], - }); - - // There should still be no change in their status since syncing is disabled - expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( - CaseStatuses.open - ); - expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source?.signal.status).to.be( - CaseStatuses.open - ); - - // it updates alert status when syncAlerts is turned on - // turn on the sync settings - await updateCase({ - supertest, - params: { - cases: updatedIndWithStatus.map((caseInfo) => ({ - id: caseInfo.id, - version: caseInfo.version, - settings: { syncAlerts: true }, - })), - }, - }); - - await es.indices.refresh({ index: defaultSignalsIndex }); - - signals = await getSignalsWithES({ - es, - indices: defaultSignalsIndex, - ids: [signalID, signalID2], - }); - - // alerts should be updated now that the - expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( - CaseStatuses.closed - ); - expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source?.signal.status).to.be( - CaseStatuses['in-progress'] - ); - }); - }); - - describe('esArchiver', () => { - const defaultSignalsIndex = '.siem-signals-default-000001'; - - beforeEach(async () => { - await esArchiver.load('cases/signals/duplicate_ids'); - }); - afterEach(async () => { - await esArchiver.unload('cases/signals/duplicate_ids'); - await deleteAllCaseItems(es); - }); - - it('should not update the status of duplicate alert ids in separate indices', async () => { - const getSignals = async () => { - return getSignalsWithES({ - es, - indices: [defaultSignalsIndex, signalsIndex2], - ids: [signalIDInFirstIndex, signalIDInSecondIndex], - }); - }; - - // this id exists only in .siem-signals-default-000001 - const signalIDInFirstIndex = - 'cae78067e65582a3b277c1ad46ba3cb29044242fe0d24bbf3fcde757fdd31d1c'; - // This id exists in both .siem-signals-default-000001 and .siem-signals-default-000002 - const signalIDInSecondIndex = 'duplicate-signal-id'; - const signalsIndex2 = '.siem-signals-default-000002'; - - const individualCase = await createCase(supertest, { - ...postCaseReq, - settings: { - syncAlerts: false, - }, - }); - - const updatedIndWithComment = await createComment({ - supertest, - caseId: individualCase.id, - params: { - alertId: signalIDInFirstIndex, - index: defaultSignalsIndex, - rule: { id: 'test-rule-id', name: 'test-index-id' }, - type: CommentType.alert, - owner: 'securitySolutionFixture', - }, - }); - - const updatedIndWithComment2 = await createComment({ - supertest, - caseId: updatedIndWithComment.id, - params: { - alertId: signalIDInSecondIndex, - index: signalsIndex2, - rule: { id: 'test-rule-id', name: 'test-index-id' }, - type: CommentType.alert, - owner: 'securitySolutionFixture', - }, - }); - - await es.indices.refresh({ index: defaultSignalsIndex }); - - let signals = await getSignals(); - // There should be no change in their status since syncing is disabled - expect( - signals.get(defaultSignalsIndex)?.get(signalIDInFirstIndex)?._source?.signal.status - ).to.be(CaseStatuses.open); - expect( - signals.get(signalsIndex2)?.get(signalIDInSecondIndex)?._source?.signal.status - ).to.be(CaseStatuses.open); - - const updatedIndWithStatus: CasesResponse = (await setStatus({ - supertest, - cases: [ - { - id: updatedIndWithComment2.id, - version: updatedIndWithComment2.version, - status: CaseStatuses.closed, - }, - ], - type: 'case', - })) as CasesResponse; - - await es.indices.refresh({ index: defaultSignalsIndex }); - - signals = await getSignals(); - - // There should still be no change in their status since syncing is disabled - expect( - signals.get(defaultSignalsIndex)?.get(signalIDInFirstIndex)?._source?.signal.status - ).to.be(CaseStatuses.open); - expect( - signals.get(signalsIndex2)?.get(signalIDInSecondIndex)?._source?.signal.status - ).to.be(CaseStatuses.open); - - // turn on the sync settings - await updateCase({ - supertest, - params: { - cases: [ - { - id: updatedIndWithStatus[0].id, - version: updatedIndWithStatus[0].version, - settings: { syncAlerts: true }, - }, - ], + it('should patch a case in space1', async () => { + const postedCase = await createCase(supertest, postCaseReq, 200, authSpace1); + const patchedCases = await updateCase({ + supertest, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + title: 'new title', }, - }); - await es.indices.refresh({ index: defaultSignalsIndex }); - - signals = await getSignals(); - - // alerts should be updated now that the - expect( - signals.get(defaultSignalsIndex)?.get(signalIDInFirstIndex)?._source?.signal.status - ).to.be(CaseStatuses.closed); - expect( - signals.get(signalsIndex2)?.get(signalIDInSecondIndex)?._source?.signal.status - ).to.be(CaseStatuses.closed); - - // the duplicate signal id in the other index should not be affect (so its status should be open) - expect( - signals.get(defaultSignalsIndex)?.get(signalIDInSecondIndex)?._source?.signal.status - ).to.be(CaseStatuses.open); - }); + ], + }, + auth: authSpace1, }); - describe('detections rule', () => { - beforeEach(async () => { - await esArchiver.load('auditbeat/hosts'); - await createSignalsIndex(supertest); - }); - - afterEach(async () => { - await deleteSignalsIndex(supertest); - await deleteAllAlerts(supertest); - await esArchiver.unload('auditbeat/hosts'); - }); - - it('updates alert status when the status is updated and syncAlerts=true', async () => { - const rule = getRuleForSignalTesting(['auditbeat-*']); - const postedCase = await createCase(supertest, postCaseReq); - - const { id } = await createRule(supertest, rule); - await waitForRuleSuccessOrStatus(supertest, id); - await waitForSignalsToBePresent(supertest, 1, [id]); - const signals = await getSignalsByIds(supertest, [id]); - - const alert = signals.hits.hits[0]; - expect(alert._source.signal.status).eql('open'); - - const caseUpdated = await createComment({ - supertest, - caseId: postedCase.id, - params: { - alertId: alert._id, - index: alert._index, - rule: { - id: 'id', - name: 'name', - }, - type: CommentType.alert, - owner: 'securitySolutionFixture', - }, - }); - - await es.indices.refresh({ index: alert._index }); - await updateCase({ - supertest, - params: { - cases: [ - { - id: caseUpdated.id, - version: caseUpdated.version, - status: CaseStatuses['in-progress'], - }, - ], - }, - }); - - // force a refresh on the index that the signal is stored in so that we can search for it and get the correct - // status - await es.indices.refresh({ index: alert._index }); - - const { body: updatedAlert } = await supertest - .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) - .set('kbn-xsrf', 'true') - .send(getQuerySignalIds([alert._id])) - .expect(200); - - expect(updatedAlert.hits.hits[0]._source.signal.status).eql('in-progress'); - }); - - it('does NOT updates alert status when the status is updated and syncAlerts=false', async () => { - const rule = getRuleForSignalTesting(['auditbeat-*']); - - const postedCase = await createCase(supertest, { - ...postCaseReq, - settings: { syncAlerts: false }, - }); - - const { id } = await createRule(supertest, rule); - await waitForRuleSuccessOrStatus(supertest, id); - await waitForSignalsToBePresent(supertest, 1, [id]); - const signals = await getSignalsByIds(supertest, [id]); - - const alert = signals.hits.hits[0]; - expect(alert._source.signal.status).eql('open'); - - const caseUpdated = await createComment({ - supertest, - caseId: postedCase.id, - params: { - alertId: alert._id, - index: alert._index, - type: CommentType.alert, - rule: { - id: 'id', - name: 'name', - }, - owner: 'securitySolutionFixture', - }, - }); - - await updateCase({ - supertest, - params: { - cases: [ - { - id: caseUpdated.id, - version: caseUpdated.version, - status: CaseStatuses['in-progress'], - }, - ], - }, - }); - - const { body: updatedAlert } = await supertest - .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) - .set('kbn-xsrf', 'true') - .send(getQuerySignalIds([alert._id])) - .expect(200); - - expect(updatedAlert.hits.hits[0]._source.signal.status).eql('open'); - }); - - it('it updates alert status when syncAlerts is turned on', async () => { - const rule = getRuleForSignalTesting(['auditbeat-*']); - - const postedCase = await createCase(supertest, { - ...postCaseReq, - settings: { syncAlerts: false }, - }); - - const { id } = await createRule(supertest, rule); - await waitForRuleSuccessOrStatus(supertest, id); - await waitForSignalsToBePresent(supertest, 1, [id]); - const signals = await getSignalsByIds(supertest, [id]); - - const alert = signals.hits.hits[0]; - expect(alert._source.signal.status).eql('open'); - - const caseUpdated = await createComment({ - supertest, - caseId: postedCase.id, - params: { - alertId: alert._id, - index: alert._index, - rule: { - id: 'id', - name: 'name', - }, - type: CommentType.alert, - owner: 'securitySolutionFixture', - }, - }); - - // Update the status of the case with sync alerts off - const caseStatusUpdated = await updateCase({ - supertest, - params: { - cases: [ - { - id: caseUpdated.id, - version: caseUpdated.version, - status: CaseStatuses['in-progress'], - }, - ], - }, - }); - - // Turn sync alerts on - await updateCase({ - supertest, - params: { - cases: [ - { - id: caseStatusUpdated[0].id, - version: caseStatusUpdated[0].version, - settings: { syncAlerts: true }, - }, - ], - }, - }); - - // refresh the index because syncAlerts was set to true so the alert's status should have been updated - await es.indices.refresh({ index: alert._index }); - - const { body: updatedAlert } = await supertest - .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) - .set('kbn-xsrf', 'true') - .send(getQuerySignalIds([alert._id])) - .expect(200); - - expect(updatedAlert.hits.hits[0]._source.signal.status).eql('in-progress'); - }); - - it('it does NOT updates alert status when syncAlerts is turned off', async () => { - const rule = getRuleForSignalTesting(['auditbeat-*']); - - const postedCase = await createCase(supertest, postCaseReq); - const { id } = await createRule(supertest, rule); - await waitForRuleSuccessOrStatus(supertest, id); - await waitForSignalsToBePresent(supertest, 1, [id]); - const signals = await getSignalsByIds(supertest, [id]); - - const alert = signals.hits.hits[0]; - expect(alert._source.signal.status).eql('open'); - - const caseUpdated = await createComment({ - supertest, - caseId: postedCase.id, - params: { - alertId: alert._id, - index: alert._index, - type: CommentType.alert, - rule: { - id: 'id', - name: 'name', - }, - owner: 'securitySolutionFixture', - }, - }); - - // Turn sync alerts off - const caseSettingsUpdated = await updateCase({ - supertest, - params: { - cases: [ - { - id: caseUpdated.id, - version: caseUpdated.version, - settings: { syncAlerts: false }, - }, - ], - }, - }); - - // Update the status of the case with sync alerts off - await updateCase({ - supertest, - params: { - cases: [ - { - id: caseSettingsUpdated[0].id, - version: caseSettingsUpdated[0].version, - status: CaseStatuses['in-progress'], - }, - ], - }, - }); - - const { body: updatedAlert } = await supertest - .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) - .set('kbn-xsrf', 'true') - .send(getQuerySignalIds([alert._id])) - .expect(200); - - expect(updatedAlert.hits.hits[0]._source.signal.status).eql('open'); - }); + const data = removeServerGeneratedPropertiesFromCase(patchedCases[0]); + expect(data).to.eql({ + ...postCaseResp(), + title: 'new title', + updated_by: nullUser, + created_by: nullUser, }); }); - describe('rbac', () => { - const supertestWithoutAuth = getService('supertestWithoutAuth'); - - it('should update a case when the user has the correct permissions', async () => { - const postedCase = await createCase(supertestWithoutAuth, postCaseReq, 200, { - user: secOnly, - space: 'space1', - }); - - const patchedCases = await updateCase({ - supertest: supertestWithoutAuth, - params: { - cases: [ - { - id: postedCase.id, - version: postedCase.version, - title: 'new title', - }, - ], - }, - auth: { user: secOnly, space: 'space1' }, - }); - - expect(patchedCases[0].owner).to.eql('securitySolutionFixture'); - }); - - it('should update multiple cases when the user has the correct permissions', async () => { - const [case1, case2, case3] = await Promise.all([ - createCase(supertestWithoutAuth, postCaseReq, 200, { - user: superUser, - space: 'space1', - }), - createCase(supertestWithoutAuth, postCaseReq, 200, { - user: superUser, - space: 'space1', - }), - createCase(supertestWithoutAuth, postCaseReq, 200, { - user: superUser, - space: 'space1', - }), - ]); - - const patchedCases = await updateCase({ - supertest: supertestWithoutAuth, - params: { - cases: [ - { - id: case1.id, - version: case1.version, - title: 'new title', - }, - { - id: case2.id, - version: case2.version, - title: 'new title', - }, - { - id: case3.id, - version: case3.version, - title: 'new title', - }, - ], - }, - auth: { user: secOnly, space: 'space1' }, - }); - - expect(patchedCases[0].owner).to.eql('securitySolutionFixture'); - expect(patchedCases[1].owner).to.eql('securitySolutionFixture'); - expect(patchedCases[2].owner).to.eql('securitySolutionFixture'); - }); - - it('should not update a case when the user does not have the correct ownership', async () => { - const postedCase = await createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'observabilityFixture' }), - 200, - { user: obsOnly, space: 'space1' } - ); - - await updateCase({ - supertest: supertestWithoutAuth, - params: { - cases: [ - { - id: postedCase.id, - version: postedCase.version, - title: 'new title', - }, - ], - }, - auth: { user: secOnly, space: 'space1' }, - expectedHttpCode: 403, - }); - }); - - it('should not update any cases when the user does not have the correct ownership', async () => { - const [case1, case2, case3] = await Promise.all([ - createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'observabilityFixture' }), - 200, - { - user: superUser, - space: 'space1', - } - ), - createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'observabilityFixture' }), - 200, + it('should not patch a case in a different space', async () => { + const postedCase = await createCase(supertest, postCaseReq, 200, authSpace1); + await updateCase({ + supertest, + params: { + cases: [ { - user: superUser, - space: 'space1', - } - ), - createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'observabilityFixture' }), - 200, - { - user: superUser, - space: 'space1', - } - ), - ]); - - await updateCase({ - supertest: supertestWithoutAuth, - params: { - cases: [ - { - id: case1.id, - version: case1.version, - title: 'new title', - }, - { - id: case2.id, - version: case2.version, - title: 'new title', - }, - { - id: case3.id, - version: case3.version, - title: 'new title', - }, - ], - }, - auth: { user: secOnly, space: 'space1' }, - expectedHttpCode: 403, - }); - - const resp = await findCases({ supertest, auth: superUserSpace1Auth }); - expect(resp.cases.length).to.eql(3); - // the update should have failed and none of the title should have been changed - expect(resp.cases[0].title).to.eql(postCaseReq.title); - expect(resp.cases[1].title).to.eql(postCaseReq.title); - expect(resp.cases[2].title).to.eql(postCaseReq.title); - }); - - for (const user of [globalRead, secOnlyRead, obsOnlyRead, obsSecRead, noKibanaPrivileges]) { - it(`User ${ - user.username - } with role(s) ${user.roles.join()} - should NOT update a case`, async () => { - const postedCase = await createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'securitySolutionFixture' }), - 200, - { - user: superUser, - space: 'space1', - } - ); - - await updateCase({ - supertest: supertestWithoutAuth, - params: { - cases: [ - { - id: postedCase.id, - version: postedCase.version, - title: 'new title', - }, - ], - }, - auth: { user, space: 'space1' }, - expectedHttpCode: 403, - }); - }); - } - - it('should NOT create a case in a space with no permissions', async () => { - const postedCase = await createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'securitySolutionFixture' }), - 200, - { - user: superUser, - space: 'space2', - } - ); - - await updateCase({ - supertest: supertestWithoutAuth, - params: { - cases: [ - { - id: postedCase.id, - version: postedCase.version, - title: 'new title', - }, - ], - }, - auth: { user: secOnly, space: 'space2' }, - expectedHttpCode: 403, - }); + id: postedCase.id, + version: postedCase.version, + title: 'new title', + }, + ], + }, + expectedHttpCode: 404, + auth: getAuthWithSuperUser('space2'), }); }); }); diff --git a/x-pack/test/case_api_integration/spaces_only/tests/common/cases/post_case.ts b/x-pack/test/case_api_integration/spaces_only/tests/common/cases/post_case.ts index 91fb03604b3c4b..1fc70b0f97f5d6 100644 --- a/x-pack/test/case_api_integration/spaces_only/tests/common/cases/post_case.ts +++ b/x-pack/test/case_api_integration/spaces_only/tests/common/cases/post_case.ts @@ -5,50 +5,49 @@ * 2.0. */ -/* eslint-disable @typescript-eslint/naming-convention */ - import expect from '@kbn/expect'; -import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; -import { - ConnectorTypes, - ConnectorJiraTypeFields, - CaseStatuses, - CaseUserActionResponse, -} from '../../../../../../plugins/cases/common/api'; -import { getPostCaseRequest, postCaseResp, defaultUser } from '../../../../common/lib/mock'; +import { ConnectorTypes } from '../../../../../../plugins/cases/common/api'; +import { getPostCaseRequest, nullUser, postCaseResp } from '../../../../common/lib/mock'; import { deleteCasesByESQuery, createCase, removeServerGeneratedPropertiesFromCase, - removeServerGeneratedPropertiesFromUserAction, - getAllUserAction, + getAuthWithSuperUser, } from '../../../../common/lib/utils'; -import { - secOnly, - secOnlyRead, - globalRead, - obsOnlyRead, - obsSecRead, - noKibanaPrivileges, -} from '../../../../common/lib/authentication/users'; + import { FtrProviderContext } from '../../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); const es = getService('es'); - const supertestWithoutAuth = getService('supertestWithoutAuth'); + const authSpace1 = getAuthWithSuperUser(); describe('post_case', () => { afterEach(async () => { await deleteCasesByESQuery(es); }); - describe('happy path', () => { - it('should post a case', async () => { - const postedCase = await createCase( - supertest, + it('should post a case in space1', async () => { + const postedCase = await createCase( + supertest, + getPostCaseRequest({ + connector: { + id: '123', + name: 'Jira', + type: ConnectorTypes.jira, + fields: { issueType: 'Task', priority: 'High', parent: null }, + }, + }), + 200, + authSpace1 + ); + const data = removeServerGeneratedPropertiesFromCase(postedCase); + + expect(data).to.eql({ + ...postCaseResp( + null, getPostCaseRequest({ connector: { id: '123', @@ -57,235 +56,8 @@ export default ({ getService }: FtrProviderContext): void => { fields: { issueType: 'Task', priority: 'High', parent: null }, }, }) - ); - const data = removeServerGeneratedPropertiesFromCase(postedCase); - - expect(data).to.eql( - postCaseResp( - null, - getPostCaseRequest({ - connector: { - id: '123', - name: 'Jira', - type: ConnectorTypes.jira, - fields: { issueType: 'Task', priority: 'High', parent: null }, - }, - }) - ) - ); - }); - - it('should post a case: none connector', async () => { - const postedCase = await createCase( - supertest, - getPostCaseRequest({ - connector: { - id: 'none', - name: 'none', - type: ConnectorTypes.none, - fields: null, - }, - }) - ); - const data = removeServerGeneratedPropertiesFromCase(postedCase); - - expect(data).to.eql( - postCaseResp( - null, - getPostCaseRequest({ - connector: { - id: 'none', - name: 'none', - type: ConnectorTypes.none, - fields: null, - }, - }) - ) - ); - }); - - it('should create a user action when creating a case', async () => { - const postedCase = await createCase(supertest, getPostCaseRequest()); - const userActions = await getAllUserAction(supertest, postedCase.id); - const creationUserAction = removeServerGeneratedPropertiesFromUserAction(userActions[0]); - - const { new_value, ...rest } = creationUserAction as CaseUserActionResponse; - const parsedNewValue = JSON.parse(new_value!); - - expect(rest).to.eql({ - action_field: [ - 'description', - 'status', - 'tags', - 'title', - 'connector', - 'settings', - 'owner', - ], - action: 'create', - action_by: defaultUser, - old_value: null, - case_id: `${postedCase.id}`, - comment_id: null, - sub_case_id: '', - owner: 'securitySolutionFixture', - }); - - expect(parsedNewValue).to.eql({ - type: postedCase.type, - description: postedCase.description, - title: postedCase.title, - tags: postedCase.tags, - connector: postedCase.connector, - settings: postedCase.settings, - owner: postedCase.owner, - }); - }); - - it('creates the case without connector in the configuration', async () => { - const postedCase = await createCase(supertest, getPostCaseRequest()); - const data = removeServerGeneratedPropertiesFromCase(postedCase); - - expect(data).to.eql(postCaseResp()); - }); - }); - - describe('unhappy path', () => { - it('400s when bad query supplied', async () => { - await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - // @ts-expect-error - .send({ ...getPostCaseRequest({ badKey: true }) }) - .expect(400); - }); - - it('400s when connector is not supplied', async () => { - const { connector, ...caseWithoutConnector } = getPostCaseRequest(); - - await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send(caseWithoutConnector) - .expect(400); - }); - - it('400s when connector has wrong type', async () => { - await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send({ - ...getPostCaseRequest({ - // @ts-expect-error - connector: { id: 'wrong', name: 'wrong', type: '.not-exists', fields: null }, - }), - }) - .expect(400); - }); - - it('400s when connector has wrong fields', async () => { - await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send({ - ...getPostCaseRequest({ - // @ts-expect-error - connector: { - id: 'wrong', - name: 'wrong', - type: ConnectorTypes.jira, - fields: { unsupported: 'value' }, - } as ConnectorJiraTypeFields, - }), - }) - .expect(400); - }); - - it('400s when missing title', async () => { - const { title, ...caseWithoutTitle } = getPostCaseRequest(); - - await supertest.post(CASES_URL).set('kbn-xsrf', 'true').send(caseWithoutTitle).expect(400); - }); - - it('400s when missing description', async () => { - const { description, ...caseWithoutDescription } = getPostCaseRequest(); - - await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send(caseWithoutDescription) - .expect(400); - }); - - it('400s when missing tags', async () => { - const { tags, ...caseWithoutTags } = getPostCaseRequest(); - - await supertest.post(CASES_URL).set('kbn-xsrf', 'true').send(caseWithoutTags).expect(400); - }); - - it('400s if you passing status for a new case', async () => { - const req = getPostCaseRequest(); - - await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send({ ...req, status: CaseStatuses.open }) - .expect(400); - }); - }); - - describe('rbac', () => { - it('User: security solution only - should create a case', async () => { - const theCase = await createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'securitySolutionFixture' }), - 200, - { - user: secOnly, - space: 'space1', - } - ); - expect(theCase.owner).to.eql('securitySolutionFixture'); - }); - - it('User: security solution only - should NOT create a case of different owner', async () => { - await createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'observabilityFixture' }), - 403, - { - user: secOnly, - space: 'space1', - } - ); - }); - - for (const user of [globalRead, secOnlyRead, obsOnlyRead, obsSecRead, noKibanaPrivileges]) { - it(`User ${ - user.username - } with role(s) ${user.roles.join()} - should NOT create a case`, async () => { - await createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'securitySolutionFixture' }), - 403, - { - user, - space: 'space1', - } - ); - }); - } - - it('should NOT create a case in a space with no permissions', async () => { - await createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'securitySolutionFixture' }), - 403, - { - user: secOnly, - space: 'space2', - } - ); + ), + created_by: nullUser, }); }); }); diff --git a/x-pack/test/case_api_integration/spaces_only/tests/common/cases/reporters/get_reporters.ts b/x-pack/test/case_api_integration/spaces_only/tests/common/cases/reporters/get_reporters.ts index dee239231faca4..74101b4402bbc6 100644 --- a/x-pack/test/case_api_integration/spaces_only/tests/common/cases/reporters/get_reporters.ts +++ b/x-pack/test/case_api_integration/spaces_only/tests/common/cases/reporters/get_reporters.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; -import { defaultUser, getPostCaseRequest } from '../../../../../common/lib/mock'; +import { getPostCaseRequest } from '../../../../../common/lib/mock'; import { createCase, deleteCasesByESQuery, @@ -27,7 +27,7 @@ export default ({ getService }: FtrProviderContext): void => { await deleteCasesByESQuery(es); }); - it('should return reporters in space1', async () => { + it('should not return reporters when security is disabled', async () => { await Promise.all([ createCase(supertest, getPostCaseRequest(), 200, getAuthWithSuperUser('space2')), createCase(supertest, getPostCaseRequest(), 200, authSpace1), @@ -35,7 +35,7 @@ export default ({ getService }: FtrProviderContext): void => { const reporters = await getReporters({ supertest, auth: authSpace1 }); - expect(reporters).to.eql([defaultUser]); + expect(reporters).to.eql([]); }); }); }; diff --git a/x-pack/test/case_api_integration/spaces_only/tests/common/cases/status/get_status.ts b/x-pack/test/case_api_integration/spaces_only/tests/common/cases/status/get_status.ts index 1f5f68104ffd9f..ee00d0568867b6 100644 --- a/x-pack/test/case_api_integration/spaces_only/tests/common/cases/status/get_status.ts +++ b/x-pack/test/case_api_integration/spaces_only/tests/common/cases/status/get_status.ts @@ -23,6 +23,7 @@ export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); const es = getService('es'); const authSpace1 = getAuthWithSuperUser(); + const authSpace2 = getAuthWithSuperUser('space2'); describe('get_status', () => { afterEach(async () => { @@ -41,7 +42,7 @@ export default ({ getService }: FtrProviderContext): void => { const [, inProgressCase, postedCase] = await Promise.all([ createCase(supertest, postCaseReq, 200, authSpace1), createCase(supertest, postCaseReq, 200, authSpace1), - createCase(supertest, postCaseReq, 200, getAuthWithSuperUser('space2')), + createCase(supertest, postCaseReq, 200, authSpace2), ]); await updateCase({ @@ -53,6 +54,15 @@ export default ({ getService }: FtrProviderContext): void => { version: inProgressCase.version, status: CaseStatuses['in-progress'], }, + ], + }, + auth: authSpace1, + }); + + await updateCase({ + supertest, + params: { + cases: [ { id: postedCase.id, version: postedCase.version, @@ -60,7 +70,7 @@ export default ({ getService }: FtrProviderContext): void => { }, ], }, - auth: authSpace1, + auth: authSpace2, }); const statuses = await getAllCasesStatuses({ supertest, auth: authSpace1 }); diff --git a/x-pack/test/case_api_integration/spaces_only/tests/common/cases/tags/get_tags.ts b/x-pack/test/case_api_integration/spaces_only/tests/common/cases/tags/get_tags.ts index bae3c61fb5fb72..1f785567d3f761 100644 --- a/x-pack/test/case_api_integration/spaces_only/tests/common/cases/tags/get_tags.ts +++ b/x-pack/test/case_api_integration/spaces_only/tests/common/cases/tags/get_tags.ts @@ -36,7 +36,7 @@ export default ({ getService }: FtrProviderContext): void => { getAuthWithSuperUser('space2') ); - const tags = await getTags({ supertest }); + const tags = await getTags({ supertest, auth: authSpace1 }); expect(tags).to.eql(['defacement']); }); }); diff --git a/x-pack/test/case_api_integration/spaces_only/tests/common/comments/delete_comment.ts b/x-pack/test/case_api_integration/spaces_only/tests/common/comments/delete_comment.ts index 73b85ef97d1194..7e5abeb7edc2f2 100644 --- a/x-pack/test/case_api_integration/spaces_only/tests/common/comments/delete_comment.ts +++ b/x-pack/test/case_api_integration/spaces_only/tests/common/comments/delete_comment.ts @@ -8,37 +8,22 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; -import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; -import { getPostCaseRequest, postCaseReq, postCommentUserReq } from '../../../../common/lib/mock'; +import { postCaseReq, postCommentUserReq } from '../../../../common/lib/mock'; import { - createCaseAction, - createSubCase, - deleteAllCaseItems, - deleteCaseAction, deleteCasesByESQuery, deleteCasesUserActions, deleteComments, createCase, createComment, deleteComment, - deleteAllComments, + getAuthWithSuperUser, } from '../../../../common/lib/utils'; -import { - globalRead, - noKibanaPrivileges, - obsOnly, - obsOnlyRead, - obsSecRead, - secOnly, - secOnlyRead, - superUser, -} from '../../../../common/lib/authentication/users'; -import { superUserSpace1Auth } from '../../../../common/lib/authentication'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); const es = getService('es'); + const authSpace1 = getAuthWithSuperUser(); describe('delete_comment', () => { afterEach(async () => { @@ -47,325 +32,40 @@ export default ({ getService }: FtrProviderContext): void => { await deleteCasesUserActions(es); }); - describe('happy path', () => { - it('should delete a comment', async () => { - const postedCase = await createCase(supertest, postCaseReq); - const patchedCase = await createComment({ - supertest, - caseId: postedCase.id, - params: postCommentUserReq, - }); - const comment = await deleteComment({ - supertest, - caseId: postedCase.id, - commentId: patchedCase.comments![0].id, - }); - - expect(comment).to.eql({}); - }); - }); - - describe('unhappy path', () => { - it('404s when comment belongs to different case', async () => { - const postedCase = await createCase(supertest, postCaseReq); - const patchedCase = await createComment({ - supertest, - caseId: postedCase.id, - params: postCommentUserReq, - }); - const error = (await deleteComment({ - supertest, - caseId: 'fake-id', - commentId: patchedCase.comments![0].id, - expectedHttpCode: 404, - })) as Error; - - expect(error.message).to.be( - `This comment ${patchedCase.comments![0].id} does not exist in fake-id.` - ); - }); - - it('404s when comment is not there', async () => { - await deleteComment({ - supertest, - caseId: 'fake-id', - commentId: 'fake-id', - expectedHttpCode: 404, - }); - }); - - it('should return a 400 when attempting to delete all comments when passing the `subCaseId` parameter', async () => { - const { body } = await supertest - .delete(`${CASES_URL}/case-id/comments?subCaseId=value`) - .set('kbn-xsrf', 'true') - .send() - .expect(400); - // make sure the failure is because of the subCaseId - expect(body.message).to.contain('disabled'); - }); - - it('should return a 400 when attempting to delete a single comment when passing the `subCaseId` parameter', async () => { - const { body } = await supertest - .delete(`${CASES_URL}/case-id/comments/comment-id?subCaseId=value`) - .set('kbn-xsrf', 'true') - .send() - .expect(400); - // make sure the failure is because of the subCaseId - expect(body.message).to.contain('disabled'); - }); - }); - - // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests - describe.skip('sub case comments', () => { - let actionID: string; - before(async () => { - actionID = await createCaseAction(supertest); - }); - after(async () => { - await deleteCaseAction(supertest, actionID); + it('should delete a comment from space1', async () => { + const postedCase = await createCase(supertest, postCaseReq, 200, authSpace1); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + auth: authSpace1, }); - afterEach(async () => { - await deleteAllCaseItems(es); - }); - - it('deletes a comment from a sub case', async () => { - const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - await supertest - .delete( - `${CASES_URL}/${caseInfo.id}/comments/${caseInfo.comments![0].id}?subCaseId=${ - caseInfo.subCases![0].id - }` - ) - .set('kbn-xsrf', 'true') - .send() - .expect(204); - const { body } = await supertest.get( - `${CASES_URL}/${caseInfo.id}/comments?subCaseId=${caseInfo.subCases![0].id}` - ); - - expect(body.length).to.eql(0); + const comment = await deleteComment({ + supertest, + caseId: postedCase.id, + commentId: patchedCase.comments![0].id, + auth: authSpace1, }); - it('deletes all comments from a sub case', async () => { - const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - await supertest - .post(`${CASES_URL}/${caseInfo.id}/comments?subCaseId=${caseInfo.subCases![0].id}`) - .set('kbn-xsrf', 'true') - .send(postCommentUserReq) - .expect(200); - - let { body: allComments } = await supertest.get( - `${CASES_URL}/${caseInfo.id}/comments?subCaseId=${caseInfo.subCases![0].id}` - ); - expect(allComments.length).to.eql(2); - - await supertest - .delete(`${CASES_URL}/${caseInfo.id}/comments?subCaseId=${caseInfo.subCases![0].id}`) - .set('kbn-xsrf', 'true') - .send() - .expect(204); - - ({ body: allComments } = await supertest.get( - `${CASES_URL}/${caseInfo.id}/comments?subCaseId=${caseInfo.subCases![0].id}` - )); - - // no comments for the sub case - expect(allComments.length).to.eql(0); - - ({ body: allComments } = await supertest.get(`${CASES_URL}/${caseInfo.id}/comments`)); - - // no comments for the collection - expect(allComments.length).to.eql(0); - }); + expect(comment).to.eql({}); }); - describe('rbac', () => { - const supertestWithoutAuth = getService('supertestWithoutAuth'); - - afterEach(async () => { - await deleteAllCaseItems(es); - }); - - it('should delete a comment from the appropriate owner', async () => { - const secCase = await createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'securitySolutionFixture' }), - 200, - { user: secOnly, space: 'space1' } - ); - - const commentResp = await createComment({ - supertest: supertestWithoutAuth, - caseId: secCase.id, - params: postCommentUserReq, - auth: { user: secOnly, space: 'space1' }, - }); - - await deleteComment({ - supertest: supertestWithoutAuth, - caseId: secCase.id, - commentId: commentResp.comments![0].id, - auth: { user: secOnly, space: 'space1' }, - }); - }); - - 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' } - ); - - const commentResp = await createComment({ - supertest: supertestWithoutAuth, - caseId: secCase.id, - params: postCommentUserReq, - auth: { user: secOnly, space: 'space1' }, - }); - - await deleteComment({ - supertest: supertestWithoutAuth, - caseId: secCase.id, - commentId: commentResp.comments![0].id, - 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]) { - it(`User ${ - user.username - } with role(s) ${user.roles.join()} - should NOT delete a comment`, async () => { - const postedCase = await createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'securitySolutionFixture' }), - 200, - superUserSpace1Auth - ); - - const commentResp = await createComment({ - supertest: supertestWithoutAuth, - caseId: postedCase.id, - params: postCommentUserReq, - auth: superUserSpace1Auth, - }); - - await deleteComment({ - supertest: supertestWithoutAuth, - caseId: postedCase.id, - commentId: commentResp.comments![0].id, - auth: { user, space: 'space1' }, - expectedHttpCode: 403, - }); - - await deleteAllComments({ - supertest: supertestWithoutAuth, - caseId: postedCase.id, - auth: { user, space: 'space1' }, - expectedHttpCode: 403, - }); - }); - } - - it('should not delete a comment with no kibana privileges', async () => { - const postedCase = await createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'securitySolutionFixture' }), - 200, - superUserSpace1Auth - ); - - const commentResp = await createComment({ - supertest: supertestWithoutAuth, - caseId: postedCase.id, - params: postCommentUserReq, - auth: superUserSpace1Auth, - }); - - await deleteComment({ - supertest: supertestWithoutAuth, - caseId: postedCase.id, - commentId: commentResp.comments![0].id, - auth: { user: noKibanaPrivileges, space: 'space1' }, - expectedHttpCode: 403, - }); - - await deleteAllComments({ - supertest: supertestWithoutAuth, - caseId: postedCase.id, - auth: { user: noKibanaPrivileges, space: 'space1' }, - // the find in the delete all will return no results - expectedHttpCode: 404, - }); - }); - - 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' } - ); - - const commentResp = await createComment({ - supertest: supertestWithoutAuth, - caseId: postedCase.id, - params: postCommentUserReq, - auth: { user: superUser, space: 'space2' }, - }); - - await deleteComment({ - supertest: supertestWithoutAuth, - caseId: postedCase.id, - commentId: commentResp.comments![0].id, - auth: { user: secOnly, space: 'space1' }, - expectedHttpCode: 404, - }); - - await deleteAllComments({ - supertest: supertestWithoutAuth, - caseId: postedCase.id, - auth: { user: secOnly, space: 'space1' }, - expectedHttpCode: 404, - }); + it('should not delete a comment from a different space', async () => { + const postedCase = await createCase(supertest, postCaseReq, 200, authSpace1); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + auth: authSpace1, + }); + + await deleteComment({ + supertest, + caseId: postedCase.id, + commentId: patchedCase.comments![0].id, + expectedHttpCode: 404, + auth: getAuthWithSuperUser('space2'), }); }); }); diff --git a/x-pack/test/case_api_integration/spaces_only/tests/common/comments/find_comments.ts b/x-pack/test/case_api_integration/spaces_only/tests/common/comments/find_comments.ts index 0f73b1ee7a624a..4df4c560413e8c 100644 --- a/x-pack/test/case_api_integration/spaces_only/tests/common/comments/find_comments.ts +++ b/x-pack/test/case_api_integration/spaces_only/tests/common/comments/find_comments.ts @@ -9,43 +9,22 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; -import { CommentsResponse, CommentType } from '../../../../../../plugins/cases/common/api'; +import { getPostCaseRequest, postCommentUserReq } from '../../../../common/lib/mock'; import { - getPostCaseRequest, - postCaseReq, - postCommentAlertReq, - postCommentUserReq, -} from '../../../../common/lib/mock'; -import { - createCaseAction, createComment, - createSubCase, - deleteAllCaseItems, - deleteCaseAction, deleteCasesByESQuery, deleteCasesUserActions, deleteComments, - ensureSavedObjectIsAuthorized, getSpaceUrlPrefix, createCase, + getAuthWithSuperUser, } from '../../../../common/lib/utils'; -import { - obsOnly, - secOnly, - obsOnlyRead, - secOnlyRead, - noKibanaPrivileges, - superUser, - globalRead, - obsSecRead, -} from '../../../../common/lib/authentication/users'; -import { superUserSpace1Auth } from '../../../../common/lib/authentication'; - // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); const es = getService('es'); + const authSpace1 = getAuthWithSuperUser(); describe('find_comments', () => { afterEach(async () => { @@ -54,340 +33,50 @@ export default ({ getService }: FtrProviderContext): void => { await deleteCasesUserActions(es); }); - it('should find all case comment', async () => { - const { body: postedCase } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send(postCaseReq) - .expect(200); - - // post 2 comments - await supertest - .post(`${CASES_URL}/${postedCase.id}/comments`) - .set('kbn-xsrf', 'true') - .send(postCommentUserReq) - .expect(200); + it('should find all case comments in space1', async () => { + const caseInfo = await createCase(supertest, getPostCaseRequest(), 200, authSpace1); + await createComment({ + supertest, + caseId: caseInfo.id, + params: postCommentUserReq, + auth: authSpace1, + }); - const { body: patchedCase } = await supertest - .post(`${CASES_URL}/${postedCase.id}/comments`) - .set('kbn-xsrf', 'true') - .send(postCommentUserReq) - .expect(200); + const patchedCase = await createComment({ + supertest, + caseId: caseInfo.id, + params: postCommentUserReq, + auth: authSpace1, + }); const { body: caseComments } = await supertest - .get(`${CASES_URL}/${postedCase.id}/comments/_find`) - .set('kbn-xsrf', 'true') - .send() + .get(`${getSpaceUrlPrefix(authSpace1.space)}${CASES_URL}/${caseInfo.id}/comments/_find`) .expect(200); expect(caseComments.comments).to.eql(patchedCase.comments); }); - it('should filter case comments', async () => { - const { body: postedCase } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send(postCaseReq) - .expect(200); - - // post 2 comments - await supertest - .post(`${CASES_URL}/${postedCase.id}/comments`) - .set('kbn-xsrf', 'true') - .send(postCommentUserReq) - .expect(200); - - const { body: patchedCase } = await supertest - .post(`${CASES_URL}/${postedCase.id}/comments`) - .set('kbn-xsrf', 'true') - .send({ ...postCommentUserReq, comment: 'unique' }) - .expect(200); - - const { body: caseComments } = await supertest - .get(`${CASES_URL}/${postedCase.id}/comments/_find?search=unique`) - .set('kbn-xsrf', 'true') - .send() - .expect(200); - - expect(caseComments.comments).to.eql([patchedCase.comments[1]]); - }); - - it('unhappy path - 400s when query is bad', async () => { - const { body: postedCase } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send(postCaseReq) - .expect(200); - - await supertest - .post(`${CASES_URL}/${postedCase.id}/comments`) - .set('kbn-xsrf', 'true') - .send(postCommentUserReq) - .expect(200); - - await supertest - .get(`${CASES_URL}/${postedCase.id}/comments/_find?perPage=true`) - .set('kbn-xsrf', 'true') - .send() - .expect(400); - }); - - it('should return a 400 when passing the subCaseId parameter', async () => { - const { body } = await supertest - .get(`${CASES_URL}/case-id/comments/_find?search=unique&subCaseId=value`) - .set('kbn-xsrf', 'true') - .send() - .expect(400); - - expect(body.message).to.contain('disabled'); - }); - - // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests - describe.skip('sub case comments', () => { - let actionID: string; - before(async () => { - actionID = await createCaseAction(supertest); - }); - after(async () => { - await deleteCaseAction(supertest, actionID); - }); - afterEach(async () => { - await deleteAllCaseItems(es); + it('should not find any case comments in space2', async () => { + const caseInfo = await createCase(supertest, getPostCaseRequest(), 200, authSpace1); + await createComment({ + supertest, + caseId: caseInfo.id, + params: postCommentUserReq, + auth: authSpace1, }); - it('finds comments for a sub case', async () => { - const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - await supertest - .post(`${CASES_URL}/${caseInfo.id}/comments?subCaseId=${caseInfo.subCases![0].id}`) - .set('kbn-xsrf', 'true') - .send(postCommentUserReq) - .expect(200); - - const { body: subCaseComments }: { body: CommentsResponse } = await supertest - .get(`${CASES_URL}/${caseInfo.id}/comments/_find?subCaseId=${caseInfo.subCases![0].id}`) - .send() - .expect(200); - expect(subCaseComments.total).to.be(2); - expect(subCaseComments.comments[0].type).to.be(CommentType.generatedAlert); - expect(subCaseComments.comments[1].type).to.be(CommentType.user); - }); - }); - - describe('rbac', () => { - const supertestWithoutAuth = getService('supertestWithoutAuth'); - - afterEach(async () => { - await deleteAllCaseItems(es); - }); - - it('should return the correct comments', async () => { - const space1 = 'space1'; - - const [secCase, obsCase] = await Promise.all([ - // Create case owned by the security solution user - createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'securitySolutionFixture' }), - 200, - { user: secOnly, space: space1 } - ), - createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'observabilityFixture' }), - 200, - { user: obsOnly, space: space1 } - ), - // Create case owned by the observability user - ]); - - await Promise.all([ - createComment({ - supertest: supertestWithoutAuth, - caseId: secCase.id, - params: postCommentUserReq, - auth: { user: secOnly, space: space1 }, - }), - createComment({ - supertest: supertestWithoutAuth, - caseId: obsCase.id, - params: { ...postCommentAlertReq, owner: 'observabilityFixture' }, - auth: { user: obsOnly, space: space1 }, - }), - ]); - - for (const scenario of [ - { - user: globalRead, - numExpectedEntites: 1, - owners: ['securitySolutionFixture', 'observabilityFixture'], - caseID: secCase.id, - }, - { - user: globalRead, - numExpectedEntites: 1, - owners: ['securitySolutionFixture', 'observabilityFixture'], - caseID: obsCase.id, - }, - { - user: superUser, - numExpectedEntites: 1, - owners: ['securitySolutionFixture', 'observabilityFixture'], - caseID: secCase.id, - }, - { - user: superUser, - numExpectedEntites: 1, - owners: ['securitySolutionFixture', 'observabilityFixture'], - caseID: obsCase.id, - }, - { - user: secOnlyRead, - numExpectedEntites: 1, - owners: ['securitySolutionFixture'], - caseID: secCase.id, - }, - { - user: obsOnlyRead, - numExpectedEntites: 1, - owners: ['observabilityFixture'], - caseID: obsCase.id, - }, - { - user: obsSecRead, - numExpectedEntites: 1, - owners: ['securitySolutionFixture', 'observabilityFixture'], - caseID: secCase.id, - }, - { - user: obsSecRead, - numExpectedEntites: 1, - owners: ['securitySolutionFixture', 'observabilityFixture'], - caseID: obsCase.id, - }, - ]) { - const { body: caseComments }: { body: CommentsResponse } = await supertestWithoutAuth - .get(`${getSpaceUrlPrefix(space1)}${CASES_URL}/${scenario.caseID}/comments/_find`) - .auth(scenario.user.username, scenario.user.password) - .expect(200); - - ensureSavedObjectIsAuthorized( - caseComments.comments, - scenario.numExpectedEntites, - scenario.owners - ); - } - }); - - for (const scenario of [ - { user: noKibanaPrivileges, space: 'space1' }, - { user: secOnly, space: 'space2' }, - ]) { - it(`User ${scenario.user.username} with role(s) ${scenario.user.roles.join()} and space ${ - scenario.space - } - should NOT read a comment`, async () => { - // super user creates a case and comment in the appropriate space - const caseInfo = await createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'securitySolutionFixture' }), - 200, - { user: superUser, space: scenario.space } - ); - - await createComment({ - supertest: supertestWithoutAuth, - auth: { user: superUser, space: scenario.space }, - params: { ...postCommentUserReq, owner: 'securitySolutionFixture' }, - caseId: caseInfo.id, - }); - - // user should not be able to read comments - await supertestWithoutAuth - .get(`${getSpaceUrlPrefix(scenario.space)}${CASES_URL}/${caseInfo.id}/comments/_find`) - .auth(scenario.user.username, scenario.user.password) - .expect(403); - }); - } - - it('should not return any comments when trying to exploit RBAC through the search query parameter', async () => { - const obsCase = await createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'observabilityFixture' }), - 200, - superUserSpace1Auth - ); - - await createComment({ - supertest: supertestWithoutAuth, - auth: superUserSpace1Auth, - params: { ...postCommentUserReq, owner: 'observabilityFixture' }, - caseId: obsCase.id, - }); - - const { body: res }: { body: CommentsResponse } = await supertestWithoutAuth - .get( - `${getSpaceUrlPrefix('space1')}${CASES_URL}/${ - obsCase.id - }/comments/_find?search=securitySolutionFixture+observabilityFixture` - ) - .auth(secOnly.username, secOnly.password) - .expect(200); - - // shouldn't find any comments since they were created under the observability ownership - ensureSavedObjectIsAuthorized(res.comments, 0, ['securitySolutionFixture']); - }); - - it('should not allow retrieving unauthorized comments using the filter field', async () => { - const obsCase = await createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'observabilityFixture' }), - 200, - superUserSpace1Auth - ); - - await createComment({ - supertest: supertestWithoutAuth, - auth: superUserSpace1Auth, - params: { ...postCommentUserReq, owner: 'observabilityFixture' }, - caseId: obsCase.id, - }); - - const { body: res } = await supertestWithoutAuth - .get( - `${getSpaceUrlPrefix('space1')}${CASES_URL}/${ - obsCase.id - }/comments/_find?filter=cases-comments.attributes.owner:"observabilityFixture"` - ) - .auth(secOnly.username, secOnly.password) - .expect(200); - expect(res.comments.length).to.be(0); + await createComment({ + supertest, + caseId: caseInfo.id, + params: postCommentUserReq, + auth: authSpace1, }); - // This test ensures that the user is not allowed to define the namespaces query param - // so she cannot search across spaces - it('should NOT allow to pass a namespaces query parameter', async () => { - const obsCase = await createCase( - supertest, - getPostCaseRequest({ owner: 'observabilityFixture' }), - 200 - ); - - await createComment({ - supertest, - params: { ...postCommentUserReq, owner: 'observabilityFixture' }, - caseId: obsCase.id, - }); - - await supertest - .get(`${CASES_URL}/${obsCase.id}/comments/_find?namespaces[0]=*`) - .expect(400); - - await supertest.get(`${CASES_URL}/${obsCase.id}/comments/_find?namespaces=*`).expect(400); - }); + const { body: caseComments } = await supertest + .get(`${getSpaceUrlPrefix('space2')}${CASES_URL}/${caseInfo.id}/comments/_find`) + .expect(200); - it('should NOT allow to pass a non supported query parameter', async () => { - await supertest.get(`${CASES_URL}/id/comments/_find?notExists=papa`).expect(400); - await supertest.get(`${CASES_URL}/id/comments/_find?owner=papa`).expect(400); - }); + expect(caseComments.comments.length).to.eql(0); }); }); }; diff --git a/x-pack/test/case_api_integration/spaces_only/tests/common/comments/get_all_comments.ts b/x-pack/test/case_api_integration/spaces_only/tests/common/comments/get_all_comments.ts index 361e72bdc79bf6..ea3766b733cdcf 100644 --- a/x-pack/test/case_api_integration/spaces_only/tests/common/comments/get_all_comments.ts +++ b/x-pack/test/case_api_integration/spaces_only/tests/common/comments/get_all_comments.ts @@ -8,224 +8,66 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; -import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; -import { postCaseReq, getPostCaseRequest, postCommentUserReq } from '../../../../common/lib/mock'; +import { postCaseReq, postCommentUserReq } from '../../../../common/lib/mock'; import { - createCaseAction, - createSubCase, deleteAllCaseItems, - deleteCaseAction, createCase, createComment, getAllComments, + getAuthWithSuperUser, } from '../../../../common/lib/utils'; -import { CommentType } from '../../../../../../plugins/cases/common/api'; -import { - globalRead, - noKibanaPrivileges, - obsOnly, - obsOnlyRead, - obsSec, - obsSecRead, - secOnly, - secOnlyRead, - superUser, -} from '../../../../common/lib/authentication/users'; -import { superUserSpace1Auth } from '../../../../common/lib/authentication'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); const es = getService('es'); + const authSpace1 = getAuthWithSuperUser(); describe('get_all_comments', () => { afterEach(async () => { await deleteAllCaseItems(es); }); - it('should get multiple comments for a single case', async () => { - const postedCase = await createCase(supertest, postCaseReq); + it('should get multiple comments for a single case in space1', async () => { + const postedCase = await createCase(supertest, postCaseReq, 200, authSpace1); await createComment({ supertest, caseId: postedCase.id, params: postCommentUserReq, + auth: authSpace1, }); await createComment({ supertest, caseId: postedCase.id, params: postCommentUserReq, + auth: authSpace1, }); - const comments = await getAllComments({ supertest, caseId: postedCase.id }); + const comments = await getAllComments({ supertest, caseId: postedCase.id, auth: authSpace1 }); expect(comments.length).to.eql(2); }); - it('should return a 400 when passing the subCaseId parameter', async () => { - const { body } = await supertest - .get(`${CASES_URL}/case-id/comments?subCaseId=value`) - .set('kbn-xsrf', 'true') - .send() - .expect(400); - - expect(body.message).to.contain('disabled'); - }); - - it('should return a 400 when passing the includeSubCaseComments parameter', async () => { - const { body } = await supertest - .get(`${CASES_URL}/case-id/comments?includeSubCaseComments=true`) - .set('kbn-xsrf', 'true') - .send() - .expect(400); - - expect(body.message).to.contain('disabled'); - }); - - // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests - describe.skip('sub cases', () => { - let actionID: string; - before(async () => { - actionID = await createCaseAction(supertest); - }); - after(async () => { - await deleteCaseAction(supertest, actionID); - }); - - it('should get comments from a case and its sub cases', async () => { - const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - await supertest - .post(`${CASES_URL}/${caseInfo.id}/comments`) - .set('kbn-xsrf', 'true') - .send(postCommentUserReq) - .expect(200); - - const { body: comments } = await supertest - .get(`${CASES_URL}/${caseInfo.id}/comments?includeSubCaseComments=true`) - .expect(200); - - expect(comments.length).to.eql(2); - expect(comments[0].type).to.eql(CommentType.generatedAlert); - expect(comments[1].type).to.eql(CommentType.user); - }); - - it('should get comments from a sub cases', async () => { - const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - await supertest - .post(`${CASES_URL}/${caseInfo.subCases![0].id}/comments`) - .set('kbn-xsrf', 'true') - .send(postCommentUserReq) - .expect(200); - - const { body: comments } = await supertest - .get(`${CASES_URL}/${caseInfo.id}/comments?subCaseId=${caseInfo.subCases![0].id}`) - .expect(200); - - expect(comments.length).to.eql(2); - expect(comments[0].type).to.eql(CommentType.generatedAlert); - expect(comments[1].type).to.eql(CommentType.user); - }); - - it('should not find any comments for an invalid case id', async () => { - const { body } = await supertest - .get(`${CASES_URL}/fake-id/comments`) - .set('kbn-xsrf', 'true') - .send() - .expect(200); - expect(body.length).to.eql(0); + it('should not find any comments in space2', async () => { + const postedCase = await createCase(supertest, postCaseReq, 200, authSpace1); + await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + auth: authSpace1, }); - }); - - describe('rbac', () => { - const supertestWithoutAuth = getService('supertestWithoutAuth'); - - it('should get all comments when the user has the correct permissions', async () => { - const caseInfo = await createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'securitySolutionFixture' }), - 200, - superUserSpace1Auth - ); - - await createComment({ - supertest: supertestWithoutAuth, - caseId: caseInfo.id, - params: postCommentUserReq, - auth: superUserSpace1Auth, - }); - - await createComment({ - supertest: supertestWithoutAuth, - caseId: caseInfo.id, - params: postCommentUserReq, - auth: superUserSpace1Auth, - }); - - for (const user of [globalRead, superUser, secOnly, secOnlyRead, obsSec, obsSecRead]) { - const comments = await getAllComments({ - supertest: supertestWithoutAuth, - caseId: caseInfo.id, - auth: { user, space: 'space1' }, - }); - - expect(comments.length).to.eql(2); - } + await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + auth: authSpace1, }); - - it('should not get comments when the user does not have correct permission', async () => { - const caseInfo = await createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'securitySolutionFixture' }), - 200, - superUserSpace1Auth - ); - - await createComment({ - supertest: supertestWithoutAuth, - caseId: caseInfo.id, - params: postCommentUserReq, - auth: superUserSpace1Auth, - }); - - for (const scenario of [ - { user: noKibanaPrivileges, returnCode: 403 }, - { user: obsOnly, returnCode: 200 }, - { user: obsOnlyRead, returnCode: 200 }, - ]) { - const comments = await getAllComments({ - supertest: supertestWithoutAuth, - caseId: caseInfo.id, - auth: { user: scenario.user, space: 'space1' }, - expectedHttpCode: scenario.returnCode, - }); - - // only check the length if we get a 200 in response - if (scenario.returnCode === 200) { - expect(comments.length).to.be(0); - } - } + const comments = await getAllComments({ + supertest, + caseId: postedCase.id, + auth: getAuthWithSuperUser('space2'), }); - it('should NOT get a comment in a space with no permissions', async () => { - const caseInfo = await createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'securitySolutionFixture' }), - 200, - { user: superUser, space: 'space2' } - ); - - await createComment({ - supertest: supertestWithoutAuth, - caseId: caseInfo.id, - params: postCommentUserReq, - auth: { user: superUser, space: 'space2' }, - }); - - await getAllComments({ - supertest: supertestWithoutAuth, - caseId: caseInfo.id, - auth: { user: secOnly, space: 'space2' }, - expectedHttpCode: 403, - }); - }); + expect(comments.length).to.eql(0); }); }); }; diff --git a/x-pack/test/case_api_integration/spaces_only/tests/common/comments/get_comment.ts b/x-pack/test/case_api_integration/spaces_only/tests/common/comments/get_comment.ts index 98b6cc5a7a30c5..b53b2e6e59cfb4 100644 --- a/x-pack/test/case_api_integration/spaces_only/tests/common/comments/get_comment.ts +++ b/x-pack/test/case_api_integration/spaces_only/tests/common/comments/get_comment.ts @@ -8,162 +8,59 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; -import { postCaseReq, postCommentUserReq, getPostCaseRequest } from '../../../../common/lib/mock'; +import { postCaseReq, postCommentUserReq } from '../../../../common/lib/mock'; import { - createCaseAction, - createSubCase, deleteAllCaseItems, - deleteCaseAction, createCase, createComment, getComment, + getAuthWithSuperUser, } from '../../../../common/lib/utils'; -import { CommentType } from '../../../../../../plugins/cases/common/api'; -import { - globalRead, - noKibanaPrivileges, - obsOnly, - obsOnlyRead, - obsSec, - obsSecRead, - secOnly, - secOnlyRead, - superUser, -} from '../../../../common/lib/authentication/users'; -import { superUserSpace1Auth } from '../../../../common/lib/authentication'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); const es = getService('es'); + const authSpace1 = getAuthWithSuperUser(); describe('get_comment', () => { afterEach(async () => { await deleteAllCaseItems(es); }); - it('should get a comment', async () => { - const postedCase = await createCase(supertest, postCaseReq); + it('should get a comment in space1', async () => { + const postedCase = await createCase(supertest, postCaseReq, 200, authSpace1); const patchedCase = await createComment({ supertest, caseId: postedCase.id, params: postCommentUserReq, + auth: authSpace1, }); const comment = await getComment({ supertest, caseId: postedCase.id, commentId: patchedCase.comments![0].id, + auth: authSpace1, }); expect(comment).to.eql(patchedCase.comments![0]); }); - it('unhappy path - 404s when comment is not there', async () => { + it('should not get a comment in space2', async () => { + const postedCase = await createCase(supertest, postCaseReq, 200, authSpace1); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + auth: authSpace1, + }); await getComment({ supertest, - caseId: 'fake-id', - commentId: 'fake-id', + caseId: postedCase.id, + commentId: patchedCase.comments![0].id, + auth: getAuthWithSuperUser('space2'), expectedHttpCode: 404, }); }); - - // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests - describe.skip('sub cases', () => { - let actionID: string; - before(async () => { - actionID = await createCaseAction(supertest); - }); - after(async () => { - await deleteCaseAction(supertest, actionID); - }); - it('should get a sub case comment', async () => { - const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - const comment = await getComment({ - supertest, - caseId: caseInfo.id, - commentId: caseInfo.comments![0].id, - }); - expect(comment.type).to.be(CommentType.generatedAlert); - }); - }); - - describe('rbac', () => { - const supertestWithoutAuth = getService('supertestWithoutAuth'); - - it('should get a comment when the user has the correct permissions', async () => { - const caseInfo = await createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'securitySolutionFixture' }), - 200, - superUserSpace1Auth - ); - - const caseWithComment = await createComment({ - supertest: supertestWithoutAuth, - caseId: caseInfo.id, - params: postCommentUserReq, - auth: superUserSpace1Auth, - }); - - for (const user of [globalRead, superUser, secOnly, secOnlyRead, obsSec, obsSecRead]) { - await getComment({ - supertest: supertestWithoutAuth, - caseId: caseInfo.id, - commentId: caseWithComment.comments![0].id, - auth: { user, space: 'space1' }, - }); - } - }); - - it('should not get comment when the user does not have correct permissions', async () => { - const caseInfo = await createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'securitySolutionFixture' }), - 200, - superUserSpace1Auth - ); - - const caseWithComment = await createComment({ - supertest: supertestWithoutAuth, - caseId: caseInfo.id, - params: postCommentUserReq, - auth: superUserSpace1Auth, - }); - - for (const user of [noKibanaPrivileges, obsOnly, obsOnlyRead]) { - await getComment({ - supertest: supertestWithoutAuth, - caseId: caseInfo.id, - commentId: caseWithComment.comments![0].id, - auth: { user, space: 'space1' }, - expectedHttpCode: 403, - }); - } - }); - - it('should NOT get a case in a space with no permissions', async () => { - const caseInfo = await createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'securitySolutionFixture' }), - 200, - { user: superUser, space: 'space2' } - ); - - const caseWithComment = await createComment({ - supertest: supertestWithoutAuth, - caseId: caseInfo.id, - params: postCommentUserReq, - auth: { user: superUser, space: 'space2' }, - }); - - await getComment({ - supertest: supertestWithoutAuth, - caseId: caseInfo.id, - commentId: caseWithComment.comments![0].id, - auth: { user: secOnly, space: 'space2' }, - expectedHttpCode: 403, - }); - }); - }); }); }; diff --git a/x-pack/test/case_api_integration/spaces_only/tests/common/comments/migrations.ts b/x-pack/test/case_api_integration/spaces_only/tests/common/comments/migrations.ts deleted file mode 100644 index 50a219c5e84b30..00000000000000 --- a/x-pack/test/case_api_integration/spaces_only/tests/common/comments/migrations.ts +++ /dev/null @@ -1,37 +0,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 expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; -import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; - -// eslint-disable-next-line import/no-default-export -export default function createGetTests({ getService }: FtrProviderContext) { - const supertest = getService('supertest'); - const esArchiver = getService('esArchiver'); - - describe('migrations', () => { - before(async () => { - await esArchiver.load('cases/migrations/7.10.0'); - }); - - after(async () => { - await esArchiver.unload('cases/migrations/7.10.0'); - }); - - it('7.11.0 migrates cases comments', async () => { - const { body: comment } = await supertest - .get( - `${CASES_URL}/e1900ac0-017f-11eb-93f8-d161651bf509/comments/da677740-1ac7-11eb-b5a3-25ee88122510` - ) - .set('kbn-xsrf', 'true') - .send(); - - expect(comment.type).to.eql('user'); - }); - }); -} diff --git a/x-pack/test/case_api_integration/spaces_only/tests/common/comments/patch_comment.ts b/x-pack/test/case_api_integration/spaces_only/tests/common/comments/patch_comment.ts index c1f37d5eb2f057..452d05c9c23628 100644 --- a/x-pack/test/case_api_integration/spaces_only/tests/common/comments/patch_comment.ts +++ b/x-pack/test/case_api_integration/spaces_only/tests/common/comments/patch_comment.ts @@ -5,52 +5,26 @@ * 2.0. */ -import { omit } from 'lodash/fp'; import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; -import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; +import { AttributesTypeUser, CommentType } from '../../../../../../plugins/cases/common/api'; +import { nullUser, postCaseReq, postCommentUserReq } from '../../../../common/lib/mock'; import { - AttributesTypeAlerts, - AttributesTypeUser, - CaseResponse, - CommentType, -} from '../../../../../../plugins/cases/common/api'; -import { - defaultUser, - postCaseReq, - postCommentUserReq, - postCommentAlertReq, - getPostCaseRequest, -} from '../../../../common/lib/mock'; -import { - createCaseAction, - createSubCase, - deleteAllCaseItems, - deleteCaseAction, deleteCasesByESQuery, deleteCasesUserActions, deleteComments, createCase, createComment, updateComment, + getAuthWithSuperUser, } from '../../../../common/lib/utils'; -import { - globalRead, - noKibanaPrivileges, - obsOnly, - obsOnlyRead, - obsSecRead, - secOnly, - secOnlyRead, - superUser, -} from '../../../../common/lib/authentication/users'; -import { superUserSpace1Auth } from '../../../../common/lib/authentication'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); const es = getService('es'); + const authSpace1 = getAuthWithSuperUser(); describe('patch_comment', () => { afterEach(async () => { @@ -59,111 +33,13 @@ export default ({ getService }: FtrProviderContext): void => { await deleteCasesUserActions(es); }); - it('should return a 400 when the subCaseId parameter is passed', async () => { - const { body } = await supertest - .patch(`${CASES_URL}/case-id}/comments?subCaseId=value`) - .set('kbn-xsrf', 'true') - .send({ - id: 'id', - version: 'version', - type: CommentType.alert, - alertId: 'test-id', - index: 'test-index', - rule: { - id: 'id', - name: 'name', - }, - owner: 'securitySolutionFixture', - }) - .expect(400); - - expect(body.message).to.contain('disabled'); - }); - - // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests - describe.skip('sub case comments', () => { - let actionID: string; - before(async () => { - actionID = await createCaseAction(supertest); - }); - after(async () => { - await deleteCaseAction(supertest, actionID); - }); - afterEach(async () => { - await deleteAllCaseItems(es); - }); - - it('patches a comment for a sub case', async () => { - const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - const { body: patchedSubCase }: { body: CaseResponse } = await supertest - .post(`${CASES_URL}/${caseInfo.id}/comments?subCaseId=${caseInfo.subCases![0].id}`) - .set('kbn-xsrf', 'true') - .send(postCommentUserReq) - .expect(200); - - const newComment = 'Well I decided to update my comment. So what? Deal with it.'; - const { body: patchedSubCaseUpdatedComment } = await supertest - .patch(`${CASES_URL}/${caseInfo.id}/comments?subCaseId=${caseInfo.subCases![0].id}`) - .set('kbn-xsrf', 'true') - .send({ - id: patchedSubCase.comments![1].id, - version: patchedSubCase.comments![1].version, - comment: newComment, - type: CommentType.user, - }) - .expect(200); - - expect(patchedSubCaseUpdatedComment.comments.length).to.be(2); - expect(patchedSubCaseUpdatedComment.comments[0].type).to.be(CommentType.generatedAlert); - expect(patchedSubCaseUpdatedComment.comments[1].type).to.be(CommentType.user); - expect(patchedSubCaseUpdatedComment.comments[1].comment).to.be(newComment); - }); - - it('fails to update the generated alert comment type', async () => { - const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - await supertest - .patch(`${CASES_URL}/${caseInfo.id}/comments?subCaseId=${caseInfo.subCases![0].id}`) - .set('kbn-xsrf', 'true') - .send({ - id: caseInfo.comments![0].id, - version: caseInfo.comments![0].version, - type: CommentType.alert, - alertId: 'test-id', - index: 'test-index', - rule: { - id: 'id', - name: 'name', - }, - }) - .expect(400); - }); - - it('fails to update the generated alert comment by using another generated alert comment', async () => { - const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - await supertest - .patch(`${CASES_URL}/${caseInfo.id}/comments?subCaseId=${caseInfo.subCases![0].id}`) - .set('kbn-xsrf', 'true') - .send({ - id: caseInfo.comments![0].id, - version: caseInfo.comments![0].version, - type: CommentType.generatedAlert, - alerts: [{ _id: 'id1' }], - index: 'test-index', - rule: { - id: 'id', - name: 'name', - }, - }) - .expect(400); - }); - }); - - it('should patch a comment', async () => { - const postedCase = await createCase(supertest, postCaseReq); + it('should patch a comment in space1', async () => { + const postedCase = await createCase(supertest, postCaseReq, 200, authSpace1); const patchedCase = await createComment({ supertest, caseId: postedCase.id, params: postCommentUserReq, + auth: authSpace1, }); const newComment = 'Well I decided to update my comment. So what? Deal with it.'; @@ -177,465 +53,38 @@ export default ({ getService }: FtrProviderContext): void => { type: CommentType.user, owner: 'securitySolutionFixture', }, + auth: authSpace1, }); const userComment = updatedCase.comments![0] as AttributesTypeUser; expect(userComment.comment).to.eql(newComment); expect(userComment.type).to.eql(CommentType.user); - expect(updatedCase.updated_by).to.eql(defaultUser); + expect(updatedCase.updated_by).to.eql(nullUser); }); - it('should patch an alert', async () => { - const postedCase = await createCase(supertest, postCaseReq); - const patchedCase = await createComment({ - supertest, - caseId: postedCase.id, - params: postCommentAlertReq, - }); - const updatedCase = await updateComment({ - supertest, - caseId: postedCase.id, - req: { - id: patchedCase.comments![0].id, - version: patchedCase.comments![0].version, - type: CommentType.alert, - alertId: 'new-id', - index: postCommentAlertReq.index, - rule: { - id: 'id', - name: 'name', - }, - owner: 'securitySolutionFixture', - }, - }); - - const alertComment = updatedCase.comments![0] as AttributesTypeAlerts; - expect(alertComment.alertId).to.eql('new-id'); - expect(alertComment.index).to.eql(postCommentAlertReq.index); - expect(alertComment.type).to.eql(CommentType.alert); - expect(alertComment.rule).to.eql({ - id: 'id', - name: 'name', - }); - expect(alertComment.updated_by).to.eql(defaultUser); - }); - - it('should not allow updating the owner of a comment', async () => { - const postedCase = await createCase(supertest, postCaseReq); + it('should not patch a comment in a different space', async () => { + const postedCase = await createCase(supertest, postCaseReq, 200, authSpace1); const patchedCase = await createComment({ supertest, caseId: postedCase.id, params: postCommentUserReq, + auth: authSpace1, }); + const newComment = 'Well I decided to update my comment. So what? Deal with it.'; await updateComment({ supertest, caseId: postedCase.id, req: { id: patchedCase.comments![0].id, version: patchedCase.comments![0].version, + comment: newComment, type: CommentType.user, - comment: postCommentUserReq.comment, - owner: 'changedOwner', - }, - expectedHttpCode: 400, - }); - }); - - it('unhappy path - 404s when comment is not there', async () => { - const postedCase = await createCase(supertest, postCaseReq); - await updateComment({ - supertest, - caseId: postedCase.id, - req: { - id: 'id', - version: 'version', - type: CommentType.user, - comment: 'comment', - owner: 'securitySolutionFixture', - }, - expectedHttpCode: 404, - }); - }); - - it('unhappy path - 404s when case is not there', async () => { - await updateComment({ - supertest, - caseId: 'fake-id', - req: { - id: 'id', - version: 'version', - type: CommentType.user, - comment: 'comment', owner: 'securitySolutionFixture', }, + auth: getAuthWithSuperUser('space2'), expectedHttpCode: 404, }); }); - - it('unhappy path - 400s when trying to change comment type', async () => { - const postedCase = await createCase(supertest, postCaseReq); - const patchedCase = await createComment({ - supertest, - caseId: postedCase.id, - params: postCommentUserReq, - }); - - await updateComment({ - supertest, - caseId: postedCase.id, - req: { - id: patchedCase.comments![0].id, - version: patchedCase.comments![0].version, - type: CommentType.alert, - alertId: 'test-id', - index: 'test-index', - rule: { - id: 'id', - name: 'name', - }, - owner: 'securitySolutionFixture', - }, - expectedHttpCode: 400, - }); - }); - - it('unhappy path - 400s when missing attributes for type user', async () => { - const postedCase = await createCase(supertest, postCaseReq); - const patchedCase = await createComment({ - supertest, - caseId: postedCase.id, - params: postCommentUserReq, - }); - - await updateComment({ - supertest, - caseId: postedCase.id, - // @ts-expect-error - req: { - id: patchedCase.comments![0].id, - version: patchedCase.comments![0].version, - }, - expectedHttpCode: 400, - }); - }); - - it('unhappy path - 400s when adding excess attributes for type user', async () => { - const postedCase = await createCase(supertest, postCaseReq); - const patchedCase = await createComment({ - supertest, - caseId: postedCase.id, - params: postCommentUserReq, - }); - - for (const attribute of ['alertId', 'index']) { - await updateComment({ - supertest, - caseId: postedCase.id, - req: { - id: patchedCase.comments![0].id, - version: patchedCase.comments![0].version, - comment: 'a comment', - type: CommentType.user, - [attribute]: attribute, - owner: 'securitySolutionFixture', - }, - expectedHttpCode: 400, - }); - } - }); - - it('unhappy path - 400s when missing attributes for type alert', async () => { - const postedCase = await createCase(supertest, postCaseReq); - const patchedCase = await createComment({ - supertest, - caseId: postedCase.id, - params: postCommentAlertReq, - }); - - const allRequestAttributes = { - type: CommentType.alert, - index: 'test-index', - alertId: 'test-id', - rule: { - id: 'id', - name: 'name', - }, - }; - - for (const attribute of ['alertId', 'index']) { - const requestAttributes = omit(attribute, allRequestAttributes); - await updateComment({ - supertest, - caseId: postedCase.id, - // @ts-expect-error - req: { - id: patchedCase.comments![0].id, - version: patchedCase.comments![0].version, - ...requestAttributes, - }, - expectedHttpCode: 400, - }); - } - }); - - it('unhappy path - 400s when adding excess attributes for type alert', async () => { - const postedCase = await createCase(supertest, postCaseReq); - const patchedCase = await createComment({ - supertest, - caseId: postedCase.id, - params: postCommentAlertReq, - }); - - for (const attribute of ['comment']) { - await updateComment({ - supertest, - caseId: postedCase.id, - req: { - id: patchedCase.comments![0].id, - version: patchedCase.comments![0].version, - type: CommentType.alert, - index: 'test-index', - alertId: 'test-id', - rule: { - id: 'id', - name: 'name', - }, - owner: 'securitySolutionFixture', - [attribute]: attribute, - }, - expectedHttpCode: 400, - }); - } - }); - - it('unhappy path - 409s when conflict', async () => { - const postedCase = await createCase(supertest, postCaseReq); - const patchedCase = await createComment({ - supertest, - caseId: postedCase.id, - params: postCommentUserReq, - }); - - const newComment = 'Well I decided to update my comment. So what? Deal with it.'; - await updateComment({ - supertest, - caseId: postedCase.id, - req: { - id: patchedCase.comments![0].id, - version: 'version-mismatch', - type: CommentType.user, - comment: newComment, - owner: 'securitySolutionFixture', - }, - expectedHttpCode: 409, - }); - }); - - describe('alert format', () => { - type AlertComment = CommentType.alert | CommentType.generatedAlert; - - // ENABLE_CASE_CONNECTOR: once the case connector feature is completed create a test case for generated alerts here - for (const [alertId, index, type] of [ - ['1', ['index1', 'index2'], CommentType.alert], - [['1', '2'], 'index', CommentType.alert], - ]) { - it(`throws an error with an alert comment with contents id: ${alertId} indices: ${index} type: ${type}`, async () => { - const postedCase = await createCase(supertest, postCaseReq); - const patchedCase = await createComment({ - supertest, - caseId: postedCase.id, - params: postCommentAlertReq, - }); - - await updateComment({ - supertest, - caseId: patchedCase.id, - req: { - id: patchedCase.comments![0].id, - version: patchedCase.comments![0].version, - type: type as AlertComment, - alertId, - index, - owner: 'securitySolutionFixture', - rule: postCommentAlertReq.rule, - }, - expectedHttpCode: 400, - }); - }); - } - - for (const [alertId, index, type] of [ - ['1', ['index1'], CommentType.alert], - [['1', '2'], ['index', 'other-index'], CommentType.alert], - ]) { - it(`does not throw an error with an alert comment with contents id: ${alertId} indices: ${index} type: ${type}`, async () => { - const postedCase = await createCase(supertest, postCaseReq); - const patchedCase = await createComment({ - supertest, - caseId: postedCase.id, - params: { - ...postCommentAlertReq, - alertId, - index, - owner: 'securitySolutionFixture', - type: type as AlertComment, - }, - }); - - await updateComment({ - supertest, - caseId: postedCase.id, - req: { - id: patchedCase.comments![0].id, - version: patchedCase.comments![0].version, - type: type as AlertComment, - alertId, - index, - owner: 'securitySolutionFixture', - rule: postCommentAlertReq.rule, - }, - }); - }); - } - }); - - describe('rbac', () => { - const supertestWithoutAuth = getService('supertestWithoutAuth'); - - afterEach(async () => { - await deleteAllCaseItems(es); - }); - - it('should update a comment that the user has permissions for', async () => { - const postedCase = await createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'securitySolutionFixture' }), - 200, - superUserSpace1Auth - ); - - const patchedCase = await createComment({ - supertest, - caseId: postedCase.id, - params: postCommentUserReq, - auth: superUserSpace1Auth, - }); - - const newComment = 'Well I decided to update my comment. So what? Deal with it.'; - const updatedCase = await updateComment({ - supertest, - caseId: postedCase.id, - req: { - ...postCommentUserReq, - id: patchedCase.comments![0].id, - version: patchedCase.comments![0].version, - comment: newComment, - }, - auth: { user: secOnly, space: 'space1' }, - }); - - const userComment = updatedCase.comments![0] as AttributesTypeUser; - expect(userComment.comment).to.eql(newComment); - expect(userComment.type).to.eql(CommentType.user); - expect(updatedCase.updated_by).to.eql(defaultUser); - expect(userComment.owner).to.eql('securitySolutionFixture'); - }); - - it('should not update a comment that has a different owner thant he user has access to', async () => { - const postedCase = await createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'securitySolutionFixture' }), - 200, - superUserSpace1Auth - ); - - const patchedCase = await createComment({ - supertest: supertestWithoutAuth, - caseId: postedCase.id, - params: postCommentUserReq, - auth: superUserSpace1Auth, - }); - - const newComment = 'Well I decided to update my comment. So what? Deal with it.'; - await updateComment({ - supertest: supertestWithoutAuth, - caseId: postedCase.id, - req: { - ...postCommentUserReq, - id: patchedCase.comments![0].id, - version: patchedCase.comments![0].version, - comment: newComment, - }, - 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 update a comment`, async () => { - const postedCase = await createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'securitySolutionFixture' }), - 200, - superUserSpace1Auth - ); - - const patchedCase = await createComment({ - supertest: supertestWithoutAuth, - caseId: postedCase.id, - params: postCommentUserReq, - auth: superUserSpace1Auth, - }); - - const newComment = 'Well I decided to update my comment. So what? Deal with it.'; - await updateComment({ - supertest: supertestWithoutAuth, - caseId: postedCase.id, - req: { - ...postCommentUserReq, - id: patchedCase.comments![0].id, - version: patchedCase.comments![0].version, - comment: newComment, - }, - auth: { user, space: 'space1' }, - expectedHttpCode: 403, - }); - }); - } - - it('should not update a comment in a space the user does not have permissions', async () => { - const postedCase = await createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'securitySolutionFixture' }), - 200, - { user: superUser, space: 'space2' } - ); - - const patchedCase = await createComment({ - supertest: supertestWithoutAuth, - caseId: postedCase.id, - params: postCommentUserReq, - auth: { user: superUser, space: 'space2' }, - }); - - const newComment = 'Well I decided to update my comment. So what? Deal with it.'; - await updateComment({ - supertest: supertestWithoutAuth, - caseId: postedCase.id, - req: { - ...postCommentUserReq, - id: patchedCase.comments![0].id, - version: patchedCase.comments![0].version, - comment: newComment, - }, - auth: { user: secOnly, space: 'space2' }, - // getting the case will fail in the saved object layer with a 403 - expectedHttpCode: 403, - }); - }); - }); }); }; diff --git a/x-pack/test/case_api_integration/spaces_only/tests/common/comments/post_comment.ts b/x-pack/test/case_api_integration/spaces_only/tests/common/comments/post_comment.ts index 1fcb49ec10ad4e..45175e8dafb041 100644 --- a/x-pack/test/case_api_integration/spaces_only/tests/common/comments/post_comment.ts +++ b/x-pack/test/case_api_integration/spaces_only/tests/common/comments/post_comment.ts @@ -5,69 +5,26 @@ * 2.0. */ -import { omit } from 'lodash/fp'; import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; -import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; -import { DETECTION_ENGINE_QUERY_SIGNALS_URL } from '../../../../../../plugins/security_solution/common/constants'; +import { AttributesTypeUser } from '../../../../../../plugins/cases/common/api'; +import { nullUser, postCaseReq, postCommentUserReq } from '../../../../common/lib/mock'; import { - CommentsResponse, - CommentType, - AttributesTypeUser, - AttributesTypeAlerts, -} from '../../../../../../plugins/cases/common/api'; -import { - defaultUser, - postCaseReq, - postCommentUserReq, - postCommentAlertReq, - postCollectionReq, - postCommentGenAlertReq, - getPostCaseRequest, -} from '../../../../common/lib/mock'; -import { - createCaseAction, - createSubCase, - deleteAllCaseItems, - deleteCaseAction, deleteCasesByESQuery, deleteCasesUserActions, deleteComments, createCase, createComment, - getAllUserAction, - removeServerGeneratedPropertiesFromUserAction, removeServerGeneratedPropertiesFromSavedObject, + getAuthWithSuperUser, } from '../../../../common/lib/utils'; -import { - createSignalsIndex, - deleteSignalsIndex, - deleteAllAlerts, - getRuleForSignalTesting, - waitForRuleSuccessOrStatus, - waitForSignalsToBePresent, - getSignalsByIds, - createRule, - getQuerySignalIds, -} from '../../../../../detection_engine_api_integration/utils'; -import { - globalRead, - noKibanaPrivileges, - obsOnly, - obsOnlyRead, - obsSecRead, - secOnly, - secOnlyRead, - superUser, -} from '../../../../common/lib/authentication/users'; -import { superUserSpace1Auth } from '../../../../common/lib/authentication'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); - const esArchiver = getService('esArchiver'); const es = getService('es'); + const authSpace1 = getAuthWithSuperUser(); describe('post_comment', () => { afterEach(async () => { @@ -76,529 +33,42 @@ export default ({ getService }: FtrProviderContext): void => { await deleteCasesUserActions(es); }); - describe('happy path', () => { - it('should post a comment', async () => { - const postedCase = await createCase(supertest, postCaseReq); - const patchedCase = await createComment({ - supertest, - caseId: postedCase.id, - params: postCommentUserReq, - }); - const comment = removeServerGeneratedPropertiesFromSavedObject( - patchedCase.comments![0] as AttributesTypeUser - ); - - expect(comment).to.eql({ - type: postCommentUserReq.type, - comment: postCommentUserReq.comment, - associationType: 'case', - created_by: defaultUser, - pushed_at: null, - pushed_by: null, - updated_by: null, - owner: 'securitySolutionFixture', - }); - - // updates the case correctly after adding a comment - expect(patchedCase.totalComment).to.eql(patchedCase.comments!.length); - expect(patchedCase.updated_by).to.eql(defaultUser); - }); - - it('should post an alert', async () => { - const postedCase = await createCase(supertest, postCaseReq); - const patchedCase = await createComment({ - supertest, - caseId: postedCase.id, - params: postCommentAlertReq, - }); - const comment = removeServerGeneratedPropertiesFromSavedObject( - patchedCase.comments![0] as AttributesTypeAlerts - ); - - expect(comment).to.eql({ - type: postCommentAlertReq.type, - alertId: postCommentAlertReq.alertId, - index: postCommentAlertReq.index, - rule: postCommentAlertReq.rule, - associationType: 'case', - created_by: defaultUser, - pushed_at: null, - pushed_by: null, - updated_by: null, - owner: 'securitySolutionFixture', - }); - - // updates the case correctly after adding a comment - expect(patchedCase.totalComment).to.eql(patchedCase.comments!.length); - expect(patchedCase.updated_by).to.eql(defaultUser); - }); - - it('creates a user action', async () => { - const postedCase = await createCase(supertest, postCaseReq); - const patchedCase = await createComment({ - supertest, - caseId: postedCase.id, - params: postCommentUserReq, - }); - const userActions = await getAllUserAction(supertest, postedCase.id); - const commentUserAction = removeServerGeneratedPropertiesFromUserAction(userActions[1]); - - expect(commentUserAction).to.eql({ - action_field: ['comment'], - action: 'create', - action_by: defaultUser, - new_value: `{"comment":"${postCommentUserReq.comment}","type":"${postCommentUserReq.type}","owner":"securitySolutionFixture"}`, - old_value: null, - case_id: `${postedCase.id}`, - comment_id: `${patchedCase.comments![0].id}`, - sub_case_id: '', - owner: 'securitySolutionFixture', - }); - }); - }); - - describe('unhappy path', () => { - it('400s when attempting to create a comment with a different owner than the case', async () => { - const postedCase = await createCase( - supertest, - getPostCaseRequest({ owner: 'securitySolutionFixture' }) - ); - - await createComment({ - supertest, - caseId: postedCase.id, - params: { ...postCommentUserReq, owner: 'observabilityFixture' }, - expectedHttpCode: 400, - }); - }); - - it('400s when type is missing', async () => { - const postedCase = await createCase(supertest, postCaseReq); - await createComment({ - supertest, - caseId: postedCase.id, - params: { - // @ts-expect-error - bad: 'comment', - }, - expectedHttpCode: 400, - }); - }); - - it('400s when missing attributes for type user', async () => { - const postedCase = await createCase(supertest, postCaseReq); - await createComment({ - supertest, - caseId: postedCase.id, - // @ts-expect-error - params: { - type: CommentType.user, - }, - expectedHttpCode: 400, - }); - }); - - it('400s when adding excess attributes for type user', async () => { - const postedCase = await createCase(supertest, postCaseReq); - - for (const attribute of ['alertId', 'index']) { - await createComment({ - supertest, - caseId: postedCase.id, - params: { - type: CommentType.user, - [attribute]: attribute, - comment: 'a comment', - owner: 'securitySolutionFixture', - }, - expectedHttpCode: 400, - }); - } - }); - - it('400s when missing attributes for type alert', async () => { - const postedCase = await createCase(supertest, postCaseReq); - - const allRequestAttributes = { - type: CommentType.alert, - index: 'test-index', - alertId: 'test-id', - rule: { - id: 'id', - name: 'name', - }, - owner: 'securitySolutionFixture', - }; - - for (const attribute of ['alertId', 'index']) { - const requestAttributes = omit(attribute, allRequestAttributes); - await createComment({ - supertest, - caseId: postedCase.id, - // @ts-expect-error - params: requestAttributes, - expectedHttpCode: 400, - }); - } - }); - - it('400s when adding excess attributes for type alert', async () => { - const postedCase = await createCase(supertest, postCaseReq); - - for (const attribute of ['comment']) { - await createComment({ - supertest, - caseId: postedCase.id, - params: { - type: CommentType.alert, - [attribute]: attribute, - alertId: 'test-id', - index: 'test-index', - rule: { - id: 'id', - name: 'name', - }, - owner: 'securitySolutionFixture', - }, - expectedHttpCode: 400, - }); - } - }); - - it('400s when case is missing', async () => { - await createComment({ - supertest, - caseId: 'not-exists', - params: { - // @ts-expect-error - bad: 'comment', - }, - expectedHttpCode: 400, - }); - }); - - it('400s when adding an alert to a closed case', async () => { - const postedCase = await createCase(supertest, postCaseReq); - - await supertest - .patch(CASES_URL) - .set('kbn-xsrf', 'true') - .send({ - cases: [ - { - id: postedCase.id, - version: postedCase.version, - status: 'closed', - }, - ], - }) - .expect(200); - - await createComment({ - supertest, - caseId: postedCase.id, - params: postCommentAlertReq, - expectedHttpCode: 400, - }); - }); - - // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests - it.skip('400s when adding an alert to a collection case', async () => { - const postedCase = await createCase(supertest, postCollectionReq); - await createComment({ - supertest, - caseId: postedCase.id, - params: postCommentAlertReq, - expectedHttpCode: 400, - }); - }); - - it('400s when adding a generated alert to an individual case', async () => { - const postedCase = await createCase(supertest, postCaseReq); - - await supertest - .post(`${CASES_URL}/${postedCase.id}/comments`) - .set('kbn-xsrf', 'true') - .send(postCommentGenAlertReq) - .expect(400); - }); - - it('should return a 400 when passing the subCaseId', async () => { - const { body } = await supertest - .post(`${CASES_URL}/case-id/comments?subCaseId=value`) - .set('kbn-xsrf', 'true') - .send(postCommentUserReq) - .expect(400); - expect(body.message).to.contain('subCaseId'); - }); - }); - - describe('alerts', () => { - beforeEach(async () => { - await esArchiver.load('auditbeat/hosts'); - await createSignalsIndex(supertest); - }); - - afterEach(async () => { - await deleteSignalsIndex(supertest); - await deleteAllAlerts(supertest); - await esArchiver.unload('auditbeat/hosts'); - }); - - it('should change the status of the alert if sync alert is on', async () => { - const rule = getRuleForSignalTesting(['auditbeat-*']); - const postedCase = await createCase(supertest, postCaseReq); - - await supertest - .patch(CASES_URL) - .set('kbn-xsrf', 'true') - .send({ - cases: [ - { - id: postedCase.id, - version: postedCase.version, - status: 'in-progress', - }, - ], - }) - .expect(200); - - const { id } = await createRule(supertest, rule); - await waitForRuleSuccessOrStatus(supertest, id); - await waitForSignalsToBePresent(supertest, 1, [id]); - const signals = await getSignalsByIds(supertest, [id]); - - const alert = signals.hits.hits[0]; - expect(alert._source.signal.status).eql('open'); - - await createComment({ - supertest, - caseId: postedCase.id, - params: { - alertId: alert._id, - index: alert._index, - rule: { - id: 'id', - name: 'name', - }, - owner: 'securitySolutionFixture', - type: CommentType.alert, - }, - }); - - const { body: updatedAlert } = await supertest - .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) - .set('kbn-xsrf', 'true') - .send(getQuerySignalIds([alert._id])) - .expect(200); - - expect(updatedAlert.hits.hits[0]._source.signal.status).eql('in-progress'); - }); - - it('should NOT change the status of the alert if sync alert is off', async () => { - const rule = getRuleForSignalTesting(['auditbeat-*']); - const postedCase = await createCase(supertest, { - ...postCaseReq, - settings: { syncAlerts: false }, - }); - - await supertest - .patch(CASES_URL) - .set('kbn-xsrf', 'true') - .send({ - cases: [ - { - id: postedCase.id, - version: postedCase.version, - status: 'in-progress', - }, - ], - }) - .expect(200); - - const { id } = await createRule(supertest, rule); - await waitForRuleSuccessOrStatus(supertest, id); - await waitForSignalsToBePresent(supertest, 1, [id]); - const signals = await getSignalsByIds(supertest, [id]); - - const alert = signals.hits.hits[0]; - expect(alert._source.signal.status).eql('open'); - - await createComment({ - supertest, - caseId: postedCase.id, - params: { - alertId: alert._id, - index: alert._index, - rule: { - id: 'id', - name: 'name', - }, - owner: 'securitySolutionFixture', - type: CommentType.alert, - }, - }); - - const { body: updatedAlert } = await supertest - .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) - .set('kbn-xsrf', 'true') - .send(getQuerySignalIds([alert._id])) - .expect(200); - - expect(updatedAlert.hits.hits[0]._source.signal.status).eql('open'); - }); + it('should post a comment in space1', async () => { + const postedCase = await createCase(supertest, postCaseReq, 200, authSpace1); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + auth: authSpace1, + }); + const comment = removeServerGeneratedPropertiesFromSavedObject( + patchedCase.comments![0] as AttributesTypeUser + ); + + expect(comment).to.eql({ + type: postCommentUserReq.type, + comment: postCommentUserReq.comment, + associationType: 'case', + created_by: nullUser, + pushed_at: null, + pushed_by: null, + updated_by: null, + owner: 'securitySolutionFixture', + }); + + // updates the case correctly after adding a comment + expect(patchedCase.totalComment).to.eql(patchedCase.comments!.length); + expect(patchedCase.updated_by).to.eql(nullUser); }); - describe('alert format', () => { - type AlertComment = CommentType.alert | CommentType.generatedAlert; - - for (const [alertId, index, type] of [ - ['1', ['index1', 'index2'], CommentType.alert], - [['1', '2'], 'index', CommentType.alert], - ['1', ['index1', 'index2'], CommentType.generatedAlert], - [['1', '2'], 'index', CommentType.generatedAlert], - ]) { - it(`throws an error with an alert comment with contents id: ${alertId} indices: ${index} type: ${type}`, async () => { - const postedCase = await createCase(supertest, postCaseReq); - await createComment({ - supertest, - caseId: postedCase.id, - params: { ...postCommentAlertReq, alertId, index, type: type as AlertComment }, - expectedHttpCode: 400, - }); - }); - } - - for (const [alertId, index, type] of [ - ['1', ['index1'], CommentType.alert], - [['1', '2'], ['index', 'other-index'], CommentType.alert], - ]) { - it(`does not throw an error with an alert comment with contents id: ${alertId} indices: ${index} type: ${type}`, async () => { - const postedCase = await createCase(supertest, postCaseReq); - await createComment({ - supertest, - caseId: postedCase.id, - params: { - ...postCommentAlertReq, - alertId, - index, - type: type as AlertComment, - }, - expectedHttpCode: 200, - }); - }); - } - }); - - // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests - describe.skip('sub case comments', () => { - let actionID: string; - before(async () => { - actionID = await createCaseAction(supertest); - }); - after(async () => { - await deleteCaseAction(supertest, actionID); - }); - afterEach(async () => { - await deleteAllCaseItems(es); - }); - - it('posts a new comment for a sub case', async () => { - const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - // create another sub case just to make sure we get the right comments - await createSubCase({ supertest, actionID }); - await supertest - .post(`${CASES_URL}/${caseInfo.id}/comments?subCaseId=${caseInfo.subCases![0].id}`) - .set('kbn-xsrf', 'true') - .send(postCommentUserReq) - .expect(200); - - const { body: subCaseComments }: { body: CommentsResponse } = await supertest - .get(`${CASES_URL}/${caseInfo.id}/comments/_find?subCaseId=${caseInfo.subCases![0].id}`) - .send() - .expect(200); - expect(subCaseComments.total).to.be(2); - expect(subCaseComments.comments[0].type).to.be(CommentType.generatedAlert); - expect(subCaseComments.comments[1].type).to.be(CommentType.user); - }); - }); - - describe('rbac', () => { - const supertestWithoutAuth = getService('supertestWithoutAuth'); - - afterEach(async () => { - await deleteAllCaseItems(es); - }); - - it('should create a comment when the user has the correct permissions for that owner', async () => { - const postedCase = await createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'securitySolutionFixture' }), - 200, - superUserSpace1Auth - ); - - await createComment({ - supertest: supertestWithoutAuth, - caseId: postedCase.id, - params: postCommentUserReq, - auth: { user: secOnly, space: 'space1' }, - }); - }); - - it('should not create a comment when the user does not have permissions for that owner', async () => { - const postedCase = await createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'observabilityFixture' }), - 200, - { user: obsOnly, space: 'space1' } - ); - - await createComment({ - supertest: supertestWithoutAuth, - caseId: postedCase.id, - params: { ...postCommentUserReq, owner: 'observabilityFixture' }, - auth: { user: secOnly, 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 create a comment`, async () => { - const postedCase = await createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'securitySolutionFixture' }), - 200, - superUserSpace1Auth - ); - - await createComment({ - supertest: supertestWithoutAuth, - caseId: postedCase.id, - params: postCommentUserReq, - auth: { user, space: 'space1' }, - expectedHttpCode: 403, - }); - }); - } - - it('should not create a comment in a space the user does not have permissions for', 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: secOnly, space: 'space2' }, - expectedHttpCode: 403, - }); + it('should not post a comment on a case in a different space', async () => { + const postedCase = await createCase(supertest, postCaseReq, 200, authSpace1); + await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + auth: getAuthWithSuperUser('space2'), + expectedHttpCode: 404, }); }); }); diff --git a/x-pack/test/case_api_integration/spaces_only/tests/common/configure/get_configure.ts b/x-pack/test/case_api_integration/spaces_only/tests/common/configure/get_configure.ts index 279936ebbef462..5226df3cde65ba 100644 --- a/x-pack/test/case_api_integration/spaces_only/tests/common/configure/get_configure.ts +++ b/x-pack/test/case_api_integration/spaces_only/tests/common/configure/get_configure.ts @@ -6,6 +6,7 @@ */ import expect from '@kbn/expect'; +import { nullUser } from '../../../../common/lib/mock'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { @@ -15,201 +16,36 @@ import { getConfiguration, createConfiguration, getConfigurationRequest, - ensureSavedObjectIsAuthorized, + getAuthWithSuperUser, } from '../../../../common/lib/utils'; -import { - obsOnly, - secOnly, - obsOnlyRead, - secOnlyRead, - noKibanaPrivileges, - superUser, - globalRead, - obsSecRead, - obsSec, -} from '../../../../common/lib/authentication/users'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); - const supertestWithoutAuth = getService('supertestWithoutAuth'); const es = getService('es'); + const authSpace1 = getAuthWithSuperUser(); describe('get_configure', () => { afterEach(async () => { await deleteConfiguration(es); }); - it('should return an empty find body correctly if no configuration is loaded', async () => { - const configuration = await getConfiguration({ supertest }); - expect(configuration).to.eql([]); - }); - - it('should return a configuration', async () => { - await createConfiguration(supertest); - const configuration = await getConfiguration({ supertest }); + it('should return a configuration in space1', async () => { + await createConfiguration(supertest, getConfigurationRequest(), 200, authSpace1); + const configuration = await getConfiguration({ supertest, auth: authSpace1 }); const data = removeServerGeneratedPropertiesFromSavedObject(configuration[0]); - expect(data).to.eql(getConfigurationOutput()); - }); - - it('should get a single configuration', async () => { - await createConfiguration(supertest, getConfigurationRequest({ id: 'connector-2' })); - await createConfiguration(supertest); - const res = await getConfiguration({ supertest }); - - expect(res.length).to.eql(1); - const data = removeServerGeneratedPropertiesFromSavedObject(res[0]); - expect(data).to.eql(getConfigurationOutput()); - }); - - it('should return by descending order', async () => { - await createConfiguration(supertest, getConfigurationRequest({ id: 'connector-2' })); - await createConfiguration(supertest); - const res = await getConfiguration({ supertest }); - - const data = removeServerGeneratedPropertiesFromSavedObject(res[0]); - expect(data).to.eql(getConfigurationOutput()); + expect(data).to.eql(getConfigurationOutput(false, { created_by: nullUser })); }); - describe('rbac', () => { - it('should return the correct configuration', async () => { - await createConfiguration(supertestWithoutAuth, getConfigurationRequest(), 200, { - user: secOnly, - space: 'space1', - }); - - await createConfiguration( - supertestWithoutAuth, - { ...getConfigurationRequest(), owner: 'observabilityFixture' }, - 200, - { - user: obsOnly, - space: 'space1', - } - ); - - for (const scenario of [ - { - user: globalRead, - numberOfExpectedCases: 2, - owners: ['securitySolutionFixture', 'observabilityFixture'], - }, - { - user: superUser, - numberOfExpectedCases: 2, - owners: ['securitySolutionFixture', 'observabilityFixture'], - }, - { user: secOnlyRead, numberOfExpectedCases: 1, owners: ['securitySolutionFixture'] }, - { user: obsOnlyRead, numberOfExpectedCases: 1, owners: ['observabilityFixture'] }, - { - user: obsSecRead, - numberOfExpectedCases: 2, - owners: ['securitySolutionFixture', 'observabilityFixture'], - }, - ]) { - const configuration = await getConfiguration({ - supertest: supertestWithoutAuth, - query: { owner: scenario.owners }, - expectedHttpCode: 200, - auth: { - user: scenario.user, - space: 'space1', - }, - }); - - ensureSavedObjectIsAuthorized( - configuration, - scenario.numberOfExpectedCases, - scenario.owners - ); - } + it('should not find a configuration in when looking in a different space', async () => { + await createConfiguration(supertest, getConfigurationRequest(), 200, authSpace1); + const configuration = await getConfiguration({ + supertest, + auth: getAuthWithSuperUser('space2'), }); - for (const scenario of [ - { user: noKibanaPrivileges, space: 'space1' }, - { user: secOnly, space: 'space2' }, - ]) { - it(`User ${scenario.user.username} with role(s) ${scenario.user.roles.join()} and space ${ - scenario.space - } - should NOT read a case configuration`, async () => { - // super user creates a configuration at the appropriate space - await createConfiguration(supertestWithoutAuth, getConfigurationRequest(), 200, { - user: superUser, - space: scenario.space, - }); - - // user should not be able to read configurations at the appropriate space - await getConfiguration({ - supertest: supertestWithoutAuth, - expectedHttpCode: 403, - auth: { - user: scenario.user, - space: scenario.space, - }, - }); - }); - } - - it('should respect the owner filter when having permissions', async () => { - await Promise.all([ - createConfiguration(supertestWithoutAuth, getConfigurationRequest(), 200, { - user: obsSec, - space: 'space1', - }), - createConfiguration( - supertestWithoutAuth, - { ...getConfigurationRequest(), owner: 'observabilityFixture' }, - 200, - { - user: obsSec, - space: 'space1', - } - ), - ]); - - const res = await getConfiguration({ - supertest: supertestWithoutAuth, - query: { owner: 'securitySolutionFixture' }, - auth: { - user: obsSec, - space: 'space1', - }, - }); - - ensureSavedObjectIsAuthorized(res, 1, ['securitySolutionFixture']); - }); - - it('should return the correct cases when trying to exploit RBAC through the owner query parameter', async () => { - await Promise.all([ - createConfiguration(supertestWithoutAuth, getConfigurationRequest(), 200, { - user: obsSec, - space: 'space1', - }), - createConfiguration( - supertestWithoutAuth, - { ...getConfigurationRequest(), owner: 'observabilityFixture' }, - 200, - { - user: obsSec, - space: 'space1', - } - ), - ]); - - // User with permissions only to security solution request cases from observability - const res = await getConfiguration({ - supertest: supertestWithoutAuth, - query: { owner: ['securitySolutionFixture', 'observabilityFixture'] }, - auth: { - user: secOnly, - space: 'space1', - }, - }); - - // Only security solution cases are being returned - ensureSavedObjectIsAuthorized(res, 1, ['securitySolutionFixture']); - }); + expect(configuration).to.eql([]); }); }); }; diff --git a/x-pack/test/case_api_integration/spaces_only/tests/common/configure/get_connectors.ts b/x-pack/test/case_api_integration/spaces_only/tests/common/configure/get_connectors.ts deleted file mode 100644 index 5156b9537583f3..00000000000000 --- a/x-pack/test/case_api_integration/spaces_only/tests/common/configure/get_connectors.ts +++ /dev/null @@ -1,27 +0,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 expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; - -import { getCaseConnectors } from '../../../../common/lib/utils'; - -// eslint-disable-next-line import/no-default-export -export default ({ getService }: FtrProviderContext): void => { - const supertest = getService('supertest'); - - describe('get_connectors', () => { - it('should return an empty find body correctly if no connectors are loaded', async () => { - const connectors = await getCaseConnectors(supertest); - expect(connectors).to.eql([]); - }); - - it.skip('filters out connectors that are not enabled in license', async () => { - // TODO: Should find a way to downgrade license to gold and upgrade back to trial - }); - }); -}; diff --git a/x-pack/test/case_api_integration/spaces_only/tests/common/configure/migrations.ts b/x-pack/test/case_api_integration/spaces_only/tests/common/configure/migrations.ts deleted file mode 100644 index cc2f6c414503d5..00000000000000 --- a/x-pack/test/case_api_integration/spaces_only/tests/common/configure/migrations.ts +++ /dev/null @@ -1,44 +0,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 expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; -import { CASE_CONFIGURE_URL } from '../../../../../../plugins/cases/common/constants'; - -// eslint-disable-next-line import/no-default-export -export default function createGetTests({ getService }: FtrProviderContext) { - const supertest = getService('supertest'); - const esArchiver = getService('esArchiver'); - - describe('migrations', () => { - before(async () => { - await esArchiver.load('cases/migrations/7.10.0'); - }); - - after(async () => { - await esArchiver.unload('cases/migrations/7.10.0'); - }); - - it('7.10.0 migrates configure cases connector', async () => { - const { body } = await supertest - .get(`${CASE_CONFIGURE_URL}`) - .set('kbn-xsrf', 'true') - .send() - .expect(200); - - expect(body.length).to.be(1); - expect(body[0]).key('connector'); - expect(body[0]).not.key('connector_id'); - expect(body[0].connector).to.eql({ - id: 'connector-1', - name: 'Connector 1', - type: '.none', - fields: null, - }); - }); - }); -} diff --git a/x-pack/test/case_api_integration/spaces_only/tests/common/configure/patch_configure.ts b/x-pack/test/case_api_integration/spaces_only/tests/common/configure/patch_configure.ts index ced727f8e4e75e..f61e8698c11915 100644 --- a/x-pack/test/case_api_integration/spaces_only/tests/common/configure/patch_configure.ts +++ b/x-pack/test/case_api_integration/spaces_only/tests/common/configure/patch_configure.ts @@ -7,7 +7,6 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; -import { ObjectRemover as ActionsRemover } from '../../../../../alerting_api_integration/common/lib'; import { getConfigurationRequest, @@ -16,228 +15,63 @@ import { deleteConfiguration, createConfiguration, updateConfiguration, + getAuthWithSuperUser, } from '../../../../common/lib/utils'; -import { - secOnly, - obsOnlyRead, - secOnlyRead, - noKibanaPrivileges, - globalRead, - obsSecRead, - superUser, -} from '../../../../common/lib/authentication/users'; +import { nullUser } from '../../../../common/lib/mock'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); - const supertestWithoutAuth = getService('supertestWithoutAuth'); const es = getService('es'); + const authSpace1 = getAuthWithSuperUser(); describe('patch_configure', () => { - const actionsRemover = new ActionsRemover(supertest); - afterEach(async () => { await deleteConfiguration(es); - await actionsRemover.removeAll(); }); - it('should patch a configuration', async () => { - const configuration = await createConfiguration(supertest); - const newConfiguration = await updateConfiguration(supertest, configuration.id, { - closure_type: 'close-by-pushing', - version: configuration.version, - }); - - const data = removeServerGeneratedPropertiesFromSavedObject(newConfiguration); - expect(data).to.eql({ ...getConfigurationOutput(true), closure_type: 'close-by-pushing' }); - }); - - it('should not patch a configuration with unsupported connector type', async () => { - const configuration = await createConfiguration(supertest); - await updateConfiguration( + it('should patch a configuration in space1', async () => { + const configuration = await createConfiguration( supertest, - configuration.id, - // @ts-expect-error - getConfigurationRequest({ type: '.unsupported' }), - 400 + getConfigurationRequest(), + 200, + authSpace1 ); - }); - - it('should not patch a configuration with unsupported connector fields', async () => { - const configuration = await createConfiguration(supertest); - await updateConfiguration( + const newConfiguration = await updateConfiguration( supertest, configuration.id, - // @ts-expect-error - getConfigurationRequest({ type: '.jira', fields: { unsupported: 'value' } }), - 400 - ); - }); - - it('should handle patch request when there is no configuration', async () => { - const error = await updateConfiguration( - supertest, - 'not-exist', - { closure_type: 'close-by-pushing', version: 'no-version' }, - 404 + { + closure_type: 'close-by-pushing', + version: configuration.version, + }, + 200, + authSpace1 ); - expect(error).to.eql({ - error: 'Not Found', - message: 'Saved object [cases-configure/not-exist] not found', - statusCode: 404, - }); - }); - - it('should handle patch request when versions are different', async () => { - const configuration = await createConfiguration(supertest); - const error = await updateConfiguration( - supertest, - configuration.id, - { closure_type: 'close-by-pushing', version: 'no-version' }, - 409 - ); - - expect(error).to.eql({ - error: 'Conflict', - message: - 'This configuration has been updated. Please refresh before saving additional updates.', - statusCode: 409, + const data = removeServerGeneratedPropertiesFromSavedObject(newConfiguration); + expect(data).to.eql({ + ...getConfigurationOutput(false, { created_by: nullUser, updated_by: nullUser }), + closure_type: 'close-by-pushing', }); }); - it('should not allow to change the owner of the configuration', async () => { - const configuration = await createConfiguration(supertest); - await updateConfiguration( + it('should not patch a configuration in a different space', async () => { + const configuration = await createConfiguration( supertest, - configuration.id, - // @ts-expect-error - { owner: 'observabilityFixture', version: configuration.version }, - 400 + getConfigurationRequest(), + 200, + authSpace1 ); - }); - - it('should not allow excess attributes', async () => { - const configuration = await createConfiguration(supertest); await updateConfiguration( supertest, configuration.id, - // @ts-expect-error - { notExist: 'not-exist', version: configuration.version }, - 400 + { + closure_type: 'close-by-pushing', + version: configuration.version, + }, + 404, + getAuthWithSuperUser('space2') ); }); - - describe('rbac', () => { - it('User: security solution only - should update a configuration', async () => { - const configuration = await createConfiguration( - supertestWithoutAuth, - getConfigurationRequest(), - 200, - { - user: secOnly, - space: 'space1', - } - ); - - const newConfiguration = await updateConfiguration( - supertestWithoutAuth, - configuration.id, - { - closure_type: 'close-by-pushing', - version: configuration.version, - }, - 200, - { - user: secOnly, - space: 'space1', - } - ); - - expect(newConfiguration.owner).to.eql('securitySolutionFixture'); - }); - - it('User: security solution only - should NOT update a configuration of different owner', async () => { - const configuration = await createConfiguration( - supertestWithoutAuth, - { ...getConfigurationRequest(), owner: 'observabilityFixture' }, - 200, - { - user: superUser, - space: 'space1', - } - ); - - await updateConfiguration( - supertestWithoutAuth, - configuration.id, - { - closure_type: 'close-by-pushing', - version: configuration.version, - }, - 403, - { - user: secOnly, - space: 'space1', - } - ); - }); - - for (const user of [globalRead, secOnlyRead, obsOnlyRead, obsSecRead, noKibanaPrivileges]) { - it(`User ${ - user.username - } with role(s) ${user.roles.join()} - should NOT update a configuration`, async () => { - const configuration = await createConfiguration( - supertestWithoutAuth, - getConfigurationRequest(), - 200, - { - user: superUser, - space: 'space1', - } - ); - - await updateConfiguration( - supertestWithoutAuth, - configuration.id, - { - closure_type: 'close-by-pushing', - version: configuration.version, - }, - 403, - { - user, - space: 'space1', - } - ); - }); - } - - it('should NOT update a configuration in a space with no permissions', async () => { - const configuration = await createConfiguration( - supertestWithoutAuth, - { ...getConfigurationRequest(), owner: 'securitySolutionFixture' }, - 200, - { - user: superUser, - space: 'space2', - } - ); - - await updateConfiguration( - supertestWithoutAuth, - configuration.id, - { - closure_type: 'close-by-pushing', - version: configuration.version, - }, - 404, - { - user: secOnly, - space: 'space1', - } - ); - }); - }); }); }; diff --git a/x-pack/test/case_api_integration/spaces_only/tests/common/configure/post_configure.ts b/x-pack/test/case_api_integration/spaces_only/tests/common/configure/post_configure.ts index f1dae9f319109b..161075616925cb 100644 --- a/x-pack/test/case_api_integration/spaces_only/tests/common/configure/post_configure.ts +++ b/x-pack/test/case_api_integration/spaces_only/tests/common/configure/post_configure.ts @@ -6,9 +6,7 @@ */ import expect from '@kbn/expect'; -import { ConnectorTypes } from '../../../../../../plugins/cases/common/api'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; -import { ObjectRemover as ActionsRemover } from '../../../../../alerting_api_integration/common/lib'; import { getConfigurationRequest, @@ -16,283 +14,31 @@ import { getConfigurationOutput, deleteConfiguration, createConfiguration, - getConfiguration, - ensureSavedObjectIsAuthorized, + getAuthWithSuperUser, } from '../../../../common/lib/utils'; - -import { - secOnly, - obsOnlyRead, - secOnlyRead, - noKibanaPrivileges, - globalRead, - obsSecRead, - superUser, -} from '../../../../common/lib/authentication/users'; +import { nullUser } from '../../../../common/lib/mock'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); - const supertestWithoutAuth = getService('supertestWithoutAuth'); const es = getService('es'); + const authSpace1 = getAuthWithSuperUser(); describe('post_configure', () => { - const actionsRemover = new ActionsRemover(supertest); - afterEach(async () => { await deleteConfiguration(es); - await actionsRemover.removeAll(); - }); - - it('should create a configuration', async () => { - const configuration = await createConfiguration(supertest); - - const data = removeServerGeneratedPropertiesFromSavedObject(configuration); - expect(data).to.eql(getConfigurationOutput()); - }); - - it('should keep only the latest configuration', async () => { - await createConfiguration(supertest, getConfigurationRequest({ id: 'connector-2' })); - await createConfiguration(supertest); - const configuration = await getConfiguration({ supertest }); - - expect(configuration.length).to.be(1); - }); - - it('should return an error when failing to get mapping', async () => { - const postRes = await createConfiguration( - supertest, - getConfigurationRequest({ - id: 'not-exists', - name: 'not-exists', - type: ConnectorTypes.jira, - }) - ); - - expect(postRes.error).to.not.be(null); - expect(postRes.mappings).to.eql([]); - }); - - it('should not create a configuration when missing connector.id', async () => { - await createConfiguration( - supertest, - { - // @ts-expect-error - connector: { - name: 'Connector', - type: ConnectorTypes.none, - fields: null, - }, - closure_type: 'close-by-user', - }, - 400 - ); - }); - - it('should not create a configuration when missing connector.name', async () => { - await createConfiguration( - supertest, - { - // @ts-expect-error - connector: { - id: 'test-id', - type: ConnectorTypes.none, - fields: null, - }, - closure_type: 'close-by-user', - }, - 400 - ); }); - it('should not create a configuration when missing connector.type', async () => { - await createConfiguration( + it('should create a configuration in space1', async () => { + const configuration = await createConfiguration( supertest, - { - // @ts-expect-error - connector: { - id: 'test-id', - name: 'Connector', - fields: null, - }, - closure_type: 'close-by-user', - }, - 400 + getConfigurationRequest(), + 200, + authSpace1 ); - }); - - it('should not create a configuration when missing connector.fields', async () => { - await createConfiguration( - supertest, - { - // @ts-expect-error - connector: { - id: 'test-id', - type: ConnectorTypes.none, - name: 'Connector', - }, - closure_type: 'close-by-user', - }, - 400 - ); - }); - it('should not create a configuration when when missing closure_type', async () => { - await createConfiguration( - supertest, - // @ts-expect-error - { - connector: { - id: 'test-id', - type: ConnectorTypes.none, - name: 'Connector', - fields: null, - }, - }, - 400 - ); - }); - - it('should not create a configuration when missing connector', async () => { - await createConfiguration( - supertest, - // @ts-expect-error - { - closure_type: 'close-by-user', - }, - 400 - ); - }); - - it('should not create a configuration when fields are not null', async () => { - await createConfiguration( - supertest, - { - connector: { - id: 'test-id', - type: ConnectorTypes.none, - name: 'Connector', - // @ts-expect-error - fields: {}, - }, - closure_type: 'close-by-user', - }, - 400 - ); - }); - - it('should not create a configuration with unsupported connector type', async () => { - // @ts-expect-error - await createConfiguration(supertest, getConfigurationRequest({ type: '.unsupported' }), 400); - }); - - it('should not create a configuration with unsupported connector fields', async () => { - await createConfiguration( - supertest, - // @ts-expect-error - getConfigurationRequest({ type: '.jira', fields: { unsupported: 'value' } }), - 400 - ); - }); - - describe('rbac', () => { - it('User: security solution only - should create a configuration', async () => { - const configuration = await createConfiguration( - supertestWithoutAuth, - getConfigurationRequest(), - 200, - { - user: secOnly, - space: 'space1', - } - ); - - expect(configuration.owner).to.eql('securitySolutionFixture'); - }); - - it('User: security solution only - should NOT create a configuration of different owner', async () => { - await createConfiguration( - supertestWithoutAuth, - { ...getConfigurationRequest(), owner: 'observabilityFixture' }, - 403, - { - user: secOnly, - space: 'space1', - } - ); - }); - - for (const user of [globalRead, secOnlyRead, obsOnlyRead, obsSecRead, noKibanaPrivileges]) { - it(`User ${ - user.username - } with role(s) ${user.roles.join()} - should NOT create a configuration`, async () => { - await createConfiguration( - supertestWithoutAuth, - { ...getConfigurationRequest(), owner: 'securitySolutionFixture' }, - 403, - { - user, - space: 'space1', - } - ); - }); - } - - it('should NOT create a configuration in a space with no permissions', async () => { - await createConfiguration( - supertestWithoutAuth, - { ...getConfigurationRequest(), owner: 'securitySolutionFixture' }, - 403, - { - user: secOnly, - space: 'space2', - } - ); - }); - - it('it deletes the correct configurations', async () => { - await createConfiguration( - supertestWithoutAuth, - { ...getConfigurationRequest(), owner: 'securitySolutionFixture' }, - 200, - { - user: superUser, - space: 'space1', - } - ); - - /** - * This API call should not delete the previously created configuration - * as it belongs to a different owner - */ - await createConfiguration( - supertestWithoutAuth, - { ...getConfigurationRequest(), owner: 'observabilityFixture' }, - 200, - { - user: superUser, - space: 'space1', - } - ); - - const configuration = await getConfiguration({ - supertest: supertestWithoutAuth, - query: { owner: ['securitySolutionFixture', 'observabilityFixture'] }, - auth: { - user: superUser, - space: 'space1', - }, - }); - - /** - * This ensures that both configuration are returned as expected - * and neither of has been deleted - */ - ensureSavedObjectIsAuthorized(configuration, 2, [ - 'securitySolutionFixture', - 'observabilityFixture', - ]); - }); + const data = removeServerGeneratedPropertiesFromSavedObject(configuration); + expect(data).to.eql(getConfigurationOutput(false, { created_by: nullUser })); }); }); }; diff --git a/x-pack/test/case_api_integration/spaces_only/tests/common/connectors/case.ts b/x-pack/test/case_api_integration/spaces_only/tests/common/connectors/case.ts deleted file mode 100644 index fd9ec8142b49fe..00000000000000 --- a/x-pack/test/case_api_integration/spaces_only/tests/common/connectors/case.ts +++ /dev/null @@ -1,1078 +0,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 { omit } from 'lodash/fp'; -import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; - -import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; -import { CommentType } from '../../../../../../plugins/cases/common/api'; -import { postCaseReq, postCaseResp } from '../../../../common/lib/mock'; -import { - removeServerGeneratedPropertiesFromCase, - removeServerGeneratedPropertiesFromComments, -} from '../../../../common/lib/utils'; -import { - createRule, - createSignalsIndex, - deleteAllAlerts, - deleteSignalsIndex, - getRuleForSignalTesting, - getSignalsByIds, - waitForRuleSuccessOrStatus, - waitForSignalsToBePresent, -} from '../../../../../detection_engine_api_integration/utils'; - -// eslint-disable-next-line import/no-default-export -export default ({ getService }: FtrProviderContext): void => { - const supertest = getService('supertest'); - const esArchiver = getService('esArchiver'); - - describe('case_connector', () => { - let createdActionId = ''; - - it('should return 400 when creating a case action', async () => { - await supertest - .post('/api/actions/connector') - .set('kbn-xsrf', 'foo') - .send({ - name: 'A case connector', - actionTypeId: '.case', - config: {}, - }) - .expect(400); - }); - - // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests - it.skip('should return 200 when creating a case action successfully', async () => { - const { body: createdAction } = await supertest - .post('/api/actions/connector') - .set('kbn-xsrf', 'foo') - .send({ - name: 'A case connector', - actionTypeId: '.case', - config: {}, - }) - .expect(200); - - createdActionId = createdAction.id; - - expect(createdAction).to.eql({ - id: createdActionId, - isPreconfigured: false, - name: 'A case connector', - actionTypeId: '.case', - config: {}, - }); - - const { body: fetchedAction } = await supertest - .get(`/api/actions/connector/${createdActionId}`) - .expect(200); - - expect(fetchedAction).to.eql({ - id: fetchedAction.id, - isPreconfigured: false, - name: 'A case connector', - actionTypeId: '.case', - config: {}, - }); - }); - - describe.skip('create', () => { - it('should respond with a 400 Bad Request when creating a case without title', async () => { - const { body: createdAction } = await supertest - .post('/api/actions/connector') - .set('kbn-xsrf', 'foo') - .send({ - name: 'A case connector', - actionTypeId: '.case', - config: {}, - }) - .expect(200); - - createdActionId = createdAction.id; - const params = { - subAction: 'create', - subActionParams: { - tags: ['case', 'connector'], - description: 'case description', - connector: { - id: 'jira', - name: 'Jira', - type: '.jira', - fields: { - issueType: '10006', - priority: 'High', - parent: null, - }, - }, - }, - }; - - const caseConnector = await supertest - .post(`/api/actions/connector/${createdActionId}/_execute`) - .set('kbn-xsrf', 'foo') - .send({ params }) - .expect(200); - - expect(caseConnector.body).to.eql({ - status: 'error', - actionId: createdActionId, - message: - 'error validating action params: types that failed validation:\n- [0.subActionParams.title]: expected value of type [string] but got [undefined]\n- [1.subAction]: expected value to equal [update]\n- [2.subAction]: expected value to equal [addComment]', - retry: false, - }); - }); - - it('should respond with a 400 Bad Request when creating a case without description', async () => { - const { body: createdAction } = await supertest - .post('/api/actions/connector') - .set('kbn-xsrf', 'foo') - .send({ - name: 'A case connector', - actionTypeId: '.case', - config: {}, - }) - .expect(200); - - createdActionId = createdAction.id; - const params = { - subAction: 'create', - subActionParams: { - title: 'Case from case connector!!', - tags: ['case', 'connector'], - connector: { - id: 'jira', - name: 'Jira', - type: '.jira', - fields: { - issueType: '10006', - priority: 'High', - parent: null, - }, - }, - }, - }; - - const caseConnector = await supertest - .post(`/api/actions/connector/${createdActionId}/_execute`) - .set('kbn-xsrf', 'foo') - .send({ params }) - .expect(200); - - expect(caseConnector.body).to.eql({ - status: 'error', - actionId: createdActionId, - message: - 'error validating action params: types that failed validation:\n- [0.subActionParams.description]: expected value of type [string] but got [undefined]\n- [1.subAction]: expected value to equal [update]\n- [2.subAction]: expected value to equal [addComment]', - retry: false, - }); - }); - - it('should respond with a 400 Bad Request when creating a case without tags', async () => { - const { body: createdAction } = await supertest - .post('/api/actions/connector') - .set('kbn-xsrf', 'foo') - .send({ - name: 'A case connector', - actionTypeId: '.case', - config: {}, - }) - .expect(200); - - createdActionId = createdAction.id; - const params = { - subAction: 'create', - subActionParams: { - title: 'Case from case connector!!', - description: 'case description', - connector: { - id: 'jira', - name: 'Jira', - type: '.jira', - fields: { - issueType: '10006', - priority: 'High', - parent: null, - }, - }, - }, - }; - - const caseConnector = await supertest - .post(`/api/actions/connector/${createdActionId}/_execute`) - .set('kbn-xsrf', 'foo') - .send({ params }) - .expect(200); - - expect(caseConnector.body).to.eql({ - status: 'error', - actionId: createdActionId, - message: - 'error validating action params: types that failed validation:\n- [0.subActionParams.tags]: expected value of type [array] but got [undefined]\n- [1.subAction]: expected value to equal [update]\n- [2.subAction]: expected value to equal [addComment]', - retry: false, - }); - }); - - it('should respond with a 400 Bad Request when creating a case without connector', async () => { - const { body: createdAction } = await supertest - .post('/api/actions/connector') - .set('kbn-xsrf', 'foo') - .send({ - name: 'A case connector', - actionTypeId: '.case', - config: {}, - }) - .expect(200); - - createdActionId = createdAction.id; - const params = { - subAction: 'create', - subActionParams: { - title: 'Case from case connector!!', - description: 'case description', - tags: ['case', 'connector'], - }, - }; - - const caseConnector = await supertest - .post(`/api/actions/connector/${createdActionId}/_execute`) - .set('kbn-xsrf', 'foo') - .send({ params }) - .expect(200); - - expect(caseConnector.body).to.eql({ - status: 'error', - actionId: createdActionId, - message: - 'error validating action params: types that failed validation:\n- [0.subActionParams.connector.id]: expected value of type [string] but got [undefined]\n- [1.subAction]: expected value to equal [update]\n- [2.subAction]: expected value to equal [addComment]', - retry: false, - }); - }); - - it('should respond with a 400 Bad Request when creating jira without issueType', async () => { - const { body: createdAction } = await supertest - .post('/api/actions/connector') - .set('kbn-xsrf', 'foo') - .send({ - name: 'A case connector', - actionTypeId: '.case', - config: {}, - }) - .expect(200); - - createdActionId = createdAction.id; - const params = { - subAction: 'create', - subActionParams: { - title: 'Case from case connector!!', - description: 'case description', - tags: ['case', 'connector'], - connector: { - id: 'jira', - name: 'Jira', - type: '.jira', - fields: { - priority: 'High', - parent: null, - }, - }, - }, - }; - - const caseConnector = await supertest - .post(`/api/actions/connector/${createdActionId}/_execute`) - .set('kbn-xsrf', 'foo') - .send({ params }) - .expect(200); - - expect(caseConnector.body).to.eql({ - status: 'error', - actionId: createdActionId, - message: - 'error validating action params: types that failed validation:\n- [0.subActionParams.connector.fields.issueType]: expected value of type [string] but got [undefined]\n- [1.subAction]: expected value to equal [update]\n- [2.subAction]: expected value to equal [addComment]', - retry: false, - }); - }); - - it('should respond with a 400 Bad Request when creating a connector with wrong fields', async () => { - const { body: createdAction } = await supertest - .post('/api/actions/connector') - .set('kbn-xsrf', 'foo') - .send({ - name: 'A case connector', - actionTypeId: '.case', - config: {}, - }) - .expect(200); - - createdActionId = createdAction.id; - const params = { - subAction: 'create', - subActionParams: { - title: 'Case from case connector!!', - description: 'case description', - tags: ['case', 'connector'], - connector: { - id: 'servicenow', - name: 'Servicenow', - type: '.servicenow', - fields: { - impact: 'Medium', - severity: 'Medium', - notExists: 'not-exists', - }, - }, - }, - }; - - const caseConnector = await supertest - .post(`/api/actions/connector/${createdActionId}/_execute`) - .set('kbn-xsrf', 'foo') - .send({ params }) - .expect(200); - - expect(caseConnector.body).to.eql({ - status: 'error', - actionId: createdActionId, - message: - 'error validating action params: types that failed validation:\n- [0.subActionParams.connector.fields.notExists]: definition for this key is missing\n- [1.subAction]: expected value to equal [update]\n- [2.subAction]: expected value to equal [addComment]', - retry: false, - }); - }); - - it('should respond with a 400 Bad Request when creating a none without fields as null', async () => { - const { body: createdAction } = await supertest - .post('/api/actions/connector') - .set('kbn-xsrf', 'foo') - .send({ - name: 'A case connector', - actionTypeId: '.case', - config: {}, - }) - .expect(200); - - createdActionId = createdAction.id; - const params = { - subAction: 'create', - subActionParams: { - title: 'Case from case connector!!', - description: 'case description', - tags: ['case', 'connector'], - connector: { - id: 'none', - name: 'None', - type: '.none', - fields: {}, - }, - }, - }; - - const caseConnector = await supertest - .post(`/api/actions/connector/${createdActionId}/_execute`) - .set('kbn-xsrf', 'foo') - .send({ params }) - .expect(200); - - expect(caseConnector.body).to.eql({ - status: 'error', - actionId: createdActionId, - message: - 'error validating action params: types that failed validation:\n- [0.subActionParams.connector]: Fields must be set to null for connectors of type .none\n- [1.subAction]: expected value to equal [update]\n- [2.subAction]: expected value to equal [addComment]', - retry: false, - }); - }); - - it('should create a case', async () => { - const { body: createdAction } = await supertest - .post('/api/actions/connector') - .set('kbn-xsrf', 'foo') - .send({ - name: 'A case connector', - actionTypeId: '.case', - config: {}, - }) - .expect(200); - - createdActionId = createdAction.id; - const params = { - subAction: 'create', - subActionParams: { - title: 'Case from case connector!!', - tags: ['case', 'connector'], - description: 'case description', - connector: { - id: 'jira', - name: 'Jira', - type: '.jira', - fields: { - issueType: '10006', - priority: 'High', - parent: null, - }, - }, - settings: { - syncAlerts: true, - }, - }, - }; - - const caseConnector = await supertest - .post(`/api/actions/connector/${createdActionId}/_execute`) - .set('kbn-xsrf', 'foo') - .send({ params }) - .expect(200); - - const { body } = await supertest - .get(`${CASES_URL}/${caseConnector.body.data.id}`) - .set('kbn-xsrf', 'true') - .send() - .expect(200); - - const data = removeServerGeneratedPropertiesFromCase(body); - expect(data).to.eql({ - ...postCaseResp(caseConnector.body.data.id), - ...params.subActionParams, - created_by: { - email: null, - full_name: null, - username: null, - }, - }); - }); - - it('should create a case with connector with field as null if not provided', async () => { - const { body: createdAction } = await supertest - .post('/api/actions/connector') - .set('kbn-xsrf', 'foo') - .send({ - name: 'A case connector', - actionTypeId: '.case', - config: {}, - }) - .expect(200); - - createdActionId = createdAction.id; - const params = { - subAction: 'create', - subActionParams: { - title: 'Case from case connector!!', - tags: ['case', 'connector'], - description: 'case description', - connector: { - id: 'servicenow', - name: 'Servicenow', - type: '.servicenow', - fields: {}, - }, - settings: { - syncAlerts: true, - }, - }, - }; - - const caseConnector = await supertest - .post(`/api/actions/connector/${createdActionId}/_execute`) - .set('kbn-xsrf', 'foo') - .send({ params }) - .expect(200); - - const { body } = await supertest - .get(`${CASES_URL}/${caseConnector.body.data.id}`) - .set('kbn-xsrf', 'true') - .send() - .expect(200); - - const data = removeServerGeneratedPropertiesFromCase(body); - expect(data).to.eql({ - ...postCaseResp(caseConnector.body.data.id), - ...params.subActionParams, - connector: { - id: 'servicenow', - name: 'Servicenow', - type: '.servicenow', - fields: { - impact: null, - severity: null, - urgency: null, - category: null, - subcategory: null, - }, - }, - created_by: { - email: null, - full_name: null, - username: null, - }, - }); - }); - }); - - describe.skip('update', () => { - it('should respond with a 400 Bad Request when updating a case without id', async () => { - const { body: createdAction } = await supertest - .post('/api/actions/connector') - .set('kbn-xsrf', 'foo') - .send({ - name: 'A case connector', - actionTypeId: '.case', - config: {}, - }) - .expect(200); - - createdActionId = createdAction.id; - const params = { - subAction: 'update', - subActionParams: { - version: '123', - title: 'Case from case connector!!', - }, - }; - - const caseConnector = await supertest - .post(`/api/actions/connector/${createdActionId}/_execute`) - .set('kbn-xsrf', 'foo') - .send({ params }) - .expect(200); - - expect(caseConnector.body).to.eql({ - status: 'error', - actionId: createdActionId, - message: - 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [create]\n- [1.subActionParams.id]: expected value of type [string] but got [undefined]\n- [2.subAction]: expected value to equal [addComment]', - retry: false, - }); - }); - - it('should respond with a 400 Bad Request when updating a case without version', async () => { - const { body: createdAction } = await supertest - .post('/api/actions/connector') - .set('kbn-xsrf', 'foo') - .send({ - name: 'A case connector', - actionTypeId: '.case', - config: {}, - }) - .expect(200); - - createdActionId = createdAction.id; - const params = { - subAction: 'update', - subActionParams: { - id: '123', - title: 'Case from case connector!!', - }, - }; - - const caseConnector = await supertest - .post(`/api/actions/connector/${createdActionId}/_execute`) - .set('kbn-xsrf', 'foo') - .send({ params }) - .expect(200); - - expect(caseConnector.body).to.eql({ - status: 'error', - actionId: createdActionId, - message: - 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [create]\n- [1.subActionParams.version]: expected value of type [string] but got [undefined]\n- [2.subAction]: expected value to equal [addComment]', - retry: false, - }); - }); - - it('should update a case', async () => { - const { body: createdAction } = await supertest - .post('/api/actions/connector') - .set('kbn-xsrf', 'foo') - .send({ - name: 'A case connector', - actionTypeId: '.case', - config: {}, - }) - .expect(200); - - createdActionId = createdAction.id; - - const caseRes = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send(postCaseReq) - .expect(200); - - const params = { - subAction: 'update', - subActionParams: { - id: caseRes.body.id, - version: caseRes.body.version, - title: 'Case from case connector!!', - }, - }; - - await supertest - .post(`/api/actions/connector/${createdActionId}/_execute`) - .set('kbn-xsrf', 'foo') - .send({ params }) - .expect(200); - - const { body } = await supertest - .get(`${CASES_URL}/${caseRes.body.id}`) - .set('kbn-xsrf', 'true') - .send() - .expect(200); - - const data = removeServerGeneratedPropertiesFromCase(body); - expect(data).to.eql({ - ...postCaseResp(caseRes.body.id), - title: 'Case from case connector!!', - updated_by: { - email: null, - full_name: null, - username: null, - }, - }); - }); - }); - - describe.skip('addComment', () => { - it('should respond with a 400 Bad Request when adding a comment to a case without caseId', async () => { - const { body: createdAction } = await supertest - .post('/api/actions/connector') - .set('kbn-xsrf', 'foo') - .send({ - name: 'A case connector', - actionTypeId: '.case', - config: {}, - }) - .expect(200); - - createdActionId = createdAction.id; - const params = { - subAction: 'addComment', - subActionParams: { - comment: { comment: 'a comment', type: CommentType.user }, - }, - }; - - const caseConnector = await supertest - .post(`/api/actions/connector/${createdActionId}/_execute`) - .set('kbn-xsrf', 'foo') - .send({ params }) - .expect(200); - - expect(caseConnector.body).to.eql({ - status: 'error', - actionId: createdActionId, - message: - 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [create]\n- [1.subAction]: expected value to equal [update]\n- [2.subActionParams.caseId]: expected value of type [string] but got [undefined]', - retry: false, - }); - }); - - it('should respond with a 400 Bad Request when missing attributes of type user', async () => { - const { body: createdAction } = await supertest - .post('/api/actions/connector') - .set('kbn-xsrf', 'foo') - .send({ - name: 'A case connector', - actionTypeId: '.case', - config: {}, - }) - .expect(200); - - createdActionId = createdAction.id; - const params = { - subAction: 'addComment', - subActionParams: { - caseId: '123', - }, - }; - - const caseConnector = await supertest - .post(`/api/actions/connector/${createdActionId}/_execute`) - .set('kbn-xsrf', 'foo') - .send({ params }) - .expect(200); - - expect(caseConnector.body).to.eql({ - status: 'error', - actionId: createdActionId, - message: - 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [create]\n- [1.subAction]: expected value to equal [update]\n- [2.subActionParams.comment]: expected at least one defined value but got [undefined]', - retry: false, - }); - }); - - describe('adding alerts using a connector', () => { - beforeEach(async () => { - await esArchiver.load('auditbeat/hosts'); - await createSignalsIndex(supertest); - }); - - afterEach(async () => { - await deleteSignalsIndex(supertest); - await deleteAllAlerts(supertest); - await esArchiver.unload('auditbeat/hosts'); - }); - - it('should add a comment of type alert', async () => { - const rule = getRuleForSignalTesting(['auditbeat-*']); - const { id } = await createRule(supertest, rule); - await waitForRuleSuccessOrStatus(supertest, id); - await waitForSignalsToBePresent(supertest, 1, [id]); - const signals = await getSignalsByIds(supertest, [id]); - const alert = signals.hits.hits[0]; - - const { body: createdAction } = await supertest - .post('/api/actions/connector') - .set('kbn-xsrf', 'foo') - .send({ - name: 'A case connector', - actionTypeId: '.case', - config: {}, - }) - .expect(200); - - createdActionId = createdAction.id; - - const caseRes = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send(postCaseReq) - .expect(200); - - const params = { - subAction: 'addComment', - subActionParams: { - caseId: caseRes.body.id, - comment: { - alertId: alert._id, - index: alert._index, - type: CommentType.alert, - rule: { id: 'id', name: 'name' }, - }, - }, - }; - - const caseConnector = await supertest - .post(`/api/actions/connector/${createdActionId}/_execute`) - .set('kbn-xsrf', 'foo') - .send({ params }) - .expect(200); - - expect(caseConnector.body.status).to.eql('ok'); - - const { body } = await supertest - .get(`${CASES_URL}/${caseRes.body.id}`) - .set('kbn-xsrf', 'true') - .send() - .expect(200); - - const data = removeServerGeneratedPropertiesFromCase(body); - const comments = removeServerGeneratedPropertiesFromComments(data.comments ?? []); - expect({ ...data, comments }).to.eql({ - ...postCaseResp(caseRes.body.id), - comments, - totalAlerts: 1, - totalComment: 1, - updated_by: { - email: null, - full_name: null, - username: null, - }, - }); - }); - }); - - it('should respond with a 400 Bad Request when missing attributes of type alert', async () => { - const { body: createdAction } = await supertest - .post('/api/actions/connector') - .set('kbn-xsrf', 'foo') - .send({ - name: 'A case connector', - actionTypeId: '.case', - config: {}, - }) - .expect(200); - - createdActionId = createdAction.id; - const comment = { - alertId: 'test-id', - index: 'test-index', - type: CommentType.alert, - rule: { id: 'id', name: 'name' }, - }; - const params = { - subAction: 'addComment', - subActionParams: { - caseId: '123', - comment, - }, - }; - - // only omitting alertId here since the type for alertId and index differ, the messages will be different - for (const attribute of ['alertId']) { - const requestAttributes = omit(attribute, comment); - const caseConnector = await supertest - .post(`/api/actions/connector/${createdActionId}/_execute`) - .set('kbn-xsrf', 'foo') - .send({ - params: { - ...params, - subActionParams: { ...params.subActionParams, comment: requestAttributes }, - }, - }) - .expect(200); - - expect(caseConnector.body).to.eql({ - status: 'error', - actionId: createdActionId, - message: `error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [create]\n- [1.subAction]: expected value to equal [update]\n- [2.subActionParams.comment]: types that failed validation:\n - [subActionParams.comment.0.type]: expected value to equal [user]\n - [subActionParams.comment.1.${attribute}]: expected at least one defined value but got [undefined]\n - [subActionParams.comment.2.type]: expected value to equal [generated_alert]`, - retry: false, - }); - } - }); - - it('should respond with a 400 Bad Request when adding excess attributes for type user', async () => { - const { body: createdAction } = await supertest - .post('/api/actions/connector') - .set('kbn-xsrf', 'foo') - .send({ - name: 'A case connector', - actionTypeId: '.case', - config: {}, - }) - .expect(200); - - createdActionId = createdAction.id; - const params = { - subAction: 'addComment', - subActionParams: { - caseId: '123', - comment: { comment: 'a comment', type: CommentType.user }, - }, - }; - - for (const attribute of ['blah', 'bogus']) { - const caseConnector = await supertest - .post(`/api/actions/connector/${createdActionId}/_execute`) - .set('kbn-xsrf', 'foo') - .send({ - params: { - ...params, - subActionParams: { - ...params.subActionParams, - comment: { ...params.subActionParams.comment, [attribute]: attribute }, - }, - }, - }) - .expect(200); - expect(caseConnector.body).to.eql({ - status: 'error', - actionId: createdActionId, - message: `error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [create]\n- [1.subAction]: expected value to equal [update]\n- [2.subActionParams.comment]: types that failed validation:\n - [subActionParams.comment.0.${attribute}]: definition for this key is missing\n - [subActionParams.comment.1.type]: expected value to equal [alert]\n - [subActionParams.comment.2.type]: expected value to equal [generated_alert]`, - retry: false, - }); - } - }); - - it('should respond with a 400 Bad Request when adding excess attributes for type alert', async () => { - const { body: createdAction } = await supertest - .post('/api/actions/connector') - .set('kbn-xsrf', 'foo') - .send({ - name: 'A case connector', - actionTypeId: '.case', - config: {}, - }) - .expect(200); - - createdActionId = createdAction.id; - const params = { - subAction: 'addComment', - subActionParams: { - caseId: '123', - comment: { - alertId: 'test-id', - index: 'test-index', - type: CommentType.alert, - rule: { id: 'id', name: 'name' }, - }, - }, - }; - - for (const attribute of ['comment']) { - const caseConnector = await supertest - .post(`/api/actions/connector/${createdActionId}/_execute`) - .set('kbn-xsrf', 'foo') - .send({ - params: { - ...params, - subActionParams: { - ...params.subActionParams, - comment: { ...params.subActionParams.comment, [attribute]: attribute }, - }, - }, - }) - .expect(200); - - expect(caseConnector.body).to.eql({ - status: 'error', - actionId: createdActionId, - message: `error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [create]\n- [1.subAction]: expected value to equal [update]\n- [2.subActionParams.comment]: types that failed validation:\n - [subActionParams.comment.0.type]: expected value to equal [user]\n - [subActionParams.comment.1.${attribute}]: definition for this key is missing\n - [subActionParams.comment.2.type]: expected value to equal [generated_alert]`, - retry: false, - }); - } - }); - - it('should respond with a 400 Bad Request when adding a comment to a case without type', async () => { - const { body: createdAction } = await supertest - .post('/api/actions/connector') - .set('kbn-xsrf', 'foo') - .send({ - name: 'A case connector', - actionTypeId: '.case', - config: {}, - }) - .expect(200); - - createdActionId = createdAction.id; - const params = { - subAction: 'update', - subActionParams: { - caseId: '123', - comment: { comment: 'a comment' }, - }, - }; - - const caseConnector = await supertest - .post(`/api/actions/connector/${createdActionId}/_execute`) - .set('kbn-xsrf', 'foo') - .send({ params }) - .expect(200); - - expect(caseConnector.body).to.eql({ - status: 'error', - actionId: createdActionId, - message: - 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [create]\n- [1.subActionParams.id]: expected value of type [string] but got [undefined]\n- [2.subAction]: expected value to equal [addComment]', - retry: false, - }); - }); - - it('should add a comment of type user', async () => { - const { body: createdAction } = await supertest - .post('/api/actions/connector') - .set('kbn-xsrf', 'foo') - .send({ - name: 'A case connector', - actionTypeId: '.case', - config: {}, - }) - .expect(200); - - createdActionId = createdAction.id; - - const caseRes = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send(postCaseReq) - .expect(200); - - const params = { - subAction: 'addComment', - subActionParams: { - caseId: caseRes.body.id, - comment: { comment: 'a comment', type: CommentType.user }, - }, - }; - - await supertest - .post(`/api/actions/connector/${createdActionId}/_execute`) - .set('kbn-xsrf', 'foo') - .send({ params }) - .expect(200); - - const { body } = await supertest - .get(`${CASES_URL}/${caseRes.body.id}`) - .set('kbn-xsrf', 'true') - .send() - .expect(200); - - const data = removeServerGeneratedPropertiesFromCase(body); - const comments = removeServerGeneratedPropertiesFromComments(data.comments ?? []); - expect({ ...data, comments }).to.eql({ - ...postCaseResp(caseRes.body.id), - comments, - totalComment: 1, - updated_by: { - email: null, - full_name: null, - username: null, - }, - }); - }); - - it('should add a comment of type alert', async () => { - const { body: createdAction } = await supertest - .post('/api/actions/connector') - .set('kbn-xsrf', 'foo') - .send({ - name: 'A case connector', - actionTypeId: '.case', - config: {}, - }) - .expect(200); - - createdActionId = createdAction.id; - - const caseRes = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send(postCaseReq) - .expect(200); - - const params = { - subAction: 'addComment', - subActionParams: { - caseId: caseRes.body.id, - comment: { - alertId: 'test-id', - index: 'test-index', - type: CommentType.alert, - rule: { id: 'id', name: 'name' }, - }, - }, - }; - - await supertest - .post(`/api/actions/connector/${createdActionId}/_execute`) - .set('kbn-xsrf', 'foo') - .send({ params }) - .expect(200); - - const { body } = await supertest - .get(`${CASES_URL}/${caseRes.body.id}`) - .set('kbn-xsrf', 'true') - .send() - .expect(200); - - const data = removeServerGeneratedPropertiesFromCase(body); - const comments = removeServerGeneratedPropertiesFromComments(data.comments ?? []); - expect({ ...data, comments }).to.eql({ - ...postCaseResp(caseRes.body.id), - comments, - totalComment: 1, - totalAlerts: 1, - updated_by: { - email: null, - full_name: null, - username: null, - }, - }); - }); - }); - }); -}; diff --git a/x-pack/test/case_api_integration/spaces_only/tests/common/index.ts b/x-pack/test/case_api_integration/spaces_only/tests/common/index.ts index ff2d1b5f37aae6..990f045895dcfb 100644 --- a/x-pack/test/case_api_integration/spaces_only/tests/common/index.ts +++ b/x-pack/test/case_api_integration/spaces_only/tests/common/index.ts @@ -26,16 +26,7 @@ export default ({ loadTestFile }: FtrProviderContext): void => { loadTestFile(require.resolve('./cases/tags/get_tags')); loadTestFile(require.resolve('./user_actions/get_all_user_actions')); loadTestFile(require.resolve('./configure/get_configure')); - loadTestFile(require.resolve('./configure/get_connectors')); loadTestFile(require.resolve('./configure/patch_configure')); loadTestFile(require.resolve('./configure/post_configure')); - loadTestFile(require.resolve('./connectors/case')); - loadTestFile(require.resolve('./sub_cases/patch_sub_cases')); - loadTestFile(require.resolve('./sub_cases/delete_sub_cases')); - loadTestFile(require.resolve('./sub_cases/get_sub_case')); - loadTestFile(require.resolve('./sub_cases/find_sub_cases')); - - // NOTE: Migrations are not included because they can inadvertently remove the .kibana indices which removes the users and spaces - // which causes errors in any tests after them that relies on those }); }; diff --git a/x-pack/test/case_api_integration/spaces_only/tests/common/migrations.ts b/x-pack/test/case_api_integration/spaces_only/tests/common/migrations.ts deleted file mode 100644 index 17d93e76bbdda5..00000000000000 --- a/x-pack/test/case_api_integration/spaces_only/tests/common/migrations.ts +++ /dev/null @@ -1,18 +0,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 { FtrProviderContext } from '../../../common/ftr_provider_context'; - -// eslint-disable-next-line import/no-default-export -export default ({ loadTestFile }: FtrProviderContext): void => { - describe('Common migrations', function () { - // Migrations - loadTestFile(require.resolve('./cases/migrations')); - loadTestFile(require.resolve('./configure/migrations')); - loadTestFile(require.resolve('./user_actions/migrations')); - }); -}; diff --git a/x-pack/test/case_api_integration/spaces_only/tests/common/sub_cases/delete_sub_cases.ts b/x-pack/test/case_api_integration/spaces_only/tests/common/sub_cases/delete_sub_cases.ts deleted file mode 100644 index 951db263a6c784..00000000000000 --- a/x-pack/test/case_api_integration/spaces_only/tests/common/sub_cases/delete_sub_cases.ts +++ /dev/null @@ -1,112 +0,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 expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; - -import { - CASES_URL, - SUB_CASES_PATCH_DEL_URL, -} from '../../../../../../plugins/cases/common/constants'; -import { postCommentUserReq } from '../../../../common/lib/mock'; -import { - createCaseAction, - createSubCase, - deleteAllCaseItems, - deleteCaseAction, -} from '../../../../common/lib/utils'; -import { getSubCaseDetailsUrl } from '../../../../../../plugins/cases/common/api/helpers'; -import { CaseResponse } from '../../../../../../plugins/cases/common/api'; - -// eslint-disable-next-line import/no-default-export -export default function ({ getService }: FtrProviderContext) { - const supertest = getService('supertest'); - const es = getService('es'); - - // ENABLE_CASE_CONNECTOR: remove this outer describe once the case connector feature is completed - describe('delete_sub_cases disabled routes', () => { - it('should return a 404 when attempting to access the route and the case connector feature is disabled', async () => { - await supertest - .delete(`${SUB_CASES_PATCH_DEL_URL}?ids=["sub-case-id"]`) - .set('kbn-xsrf', 'true') - .send() - .expect(404); - }); - - // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests - describe.skip('delete_sub_cases', () => { - let actionID: string; - before(async () => { - actionID = await createCaseAction(supertest); - }); - after(async () => { - await deleteCaseAction(supertest, actionID); - }); - afterEach(async () => { - await deleteAllCaseItems(es); - }); - - it('should delete a sub case', async () => { - const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - expect(caseInfo.subCases![0].id).to.not.eql(undefined); - - const { body: subCase } = await supertest - .get(getSubCaseDetailsUrl(caseInfo.id, caseInfo.subCases![0].id)) - .send() - .expect(200); - - expect(subCase.id).to.not.eql(undefined); - - const { body } = await supertest - .delete(`${SUB_CASES_PATCH_DEL_URL}?ids=["${subCase.id}"]`) - .set('kbn-xsrf', 'true') - .send() - .expect(204); - - expect(body).to.eql({}); - await supertest - .get(getSubCaseDetailsUrl(caseInfo.id, caseInfo.subCases![0].id)) - .send() - .expect(404); - }); - - it(`should delete a sub case's comments when that case gets deleted`, async () => { - const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - expect(caseInfo.subCases![0].id).to.not.eql(undefined); - - // there should be two comments on the sub case now - const { body: patchedCaseWithSubCase }: { body: CaseResponse } = await supertest - .post(`${CASES_URL}/${caseInfo.id}/comments`) - .set('kbn-xsrf', 'true') - .query({ subCaseId: caseInfo.subCases![0].id }) - .send(postCommentUserReq) - .expect(200); - - const subCaseCommentUrl = `${CASES_URL}/${patchedCaseWithSubCase.id}/comments/${ - patchedCaseWithSubCase.comments![1].id - }`; - // make sure we can get the second comment - await supertest.get(subCaseCommentUrl).set('kbn-xsrf', 'true').send().expect(200); - - await supertest - .delete(`${SUB_CASES_PATCH_DEL_URL}?ids=["${patchedCaseWithSubCase.subCases![0].id}"]`) - .set('kbn-xsrf', 'true') - .send() - .expect(204); - - await supertest.get(subCaseCommentUrl).set('kbn-xsrf', 'true').send().expect(404); - }); - - it('unhappy path - 404s when sub case id is invalid', async () => { - await supertest - .delete(`${SUB_CASES_PATCH_DEL_URL}?ids=["fake-id"]`) - .set('kbn-xsrf', 'true') - .send() - .expect(404); - }); - }); - }); -} diff --git a/x-pack/test/case_api_integration/spaces_only/tests/common/sub_cases/find_sub_cases.ts b/x-pack/test/case_api_integration/spaces_only/tests/common/sub_cases/find_sub_cases.ts deleted file mode 100644 index d54523bec0c4d4..00000000000000 --- a/x-pack/test/case_api_integration/spaces_only/tests/common/sub_cases/find_sub_cases.ts +++ /dev/null @@ -1,480 +0,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 expect from '@kbn/expect'; -import type { ApiResponse, estypes } from '@elastic/elasticsearch'; -import { FtrProviderContext } from '../../../../common/ftr_provider_context'; - -import { findSubCasesResp, postCollectionReq } from '../../../../common/lib/mock'; -import { - createCaseAction, - createSubCase, - CreateSubCaseResp, - deleteAllCaseItems, - deleteCaseAction, - setStatus, -} from '../../../../common/lib/utils'; -import { getSubCasesUrl } from '../../../../../../plugins/cases/common/api/helpers'; -import { - CaseResponse, - CaseStatuses, - CommentType, - SubCasesFindResponse, -} from '../../../../../../plugins/cases/common/api'; -import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; -import { - ContextTypeGeneratedAlertType, - createAlertsString, -} from '../../../../../../plugins/cases/server/connectors'; - -interface SubCaseAttributes { - 'cases-sub-case': { - created_at: string; - }; -} - -// eslint-disable-next-line import/no-default-export -export default ({ getService }: FtrProviderContext): void => { - const supertest = getService('supertest'); - const es = getService('es'); - - // ENABLE_CASE_CONNECTOR: remove this outer describe once the case connector feature is completed - describe('find_sub_cases disabled route', () => { - it('should return a 404 when attempting to access the route and the case connector feature is disabled', async () => { - await supertest.get(`${getSubCasesUrl('case-id')}/_find`).expect(404); - }); - - // ENABLE_CASE_CONNECTOR: enable these tests once the case connector feature is completed - describe.skip('create case connector', () => { - let actionID: string; - before(async () => { - actionID = await createCaseAction(supertest); - }); - after(async () => { - await deleteCaseAction(supertest, actionID); - }); - - describe('basic find tests', () => { - afterEach(async () => { - await deleteAllCaseItems(es); - }); - it('should not find any sub cases when none exist', async () => { - const { body: caseResp }: { body: CaseResponse } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send(postCollectionReq) - .expect(200); - - const { body: findSubCases } = await supertest - .get(`${getSubCasesUrl(caseResp.id)}/_find`) - .expect(200); - - expect(findSubCases).to.eql({ - page: 1, - per_page: 20, - total: 0, - subCases: [], - count_open_cases: 0, - count_closed_cases: 0, - count_in_progress_cases: 0, - }); - }); - - it('should return a sub cases with comment stats', async () => { - const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - - const { body }: { body: SubCasesFindResponse } = await supertest - .get(`${getSubCasesUrl(caseInfo.id)}/_find`) - .expect(200); - - expect(body).to.eql({ - ...findSubCasesResp, - total: 1, - // find should not return the comments themselves only the stats - subCases: [{ ...caseInfo.subCases![0], comments: [], totalComment: 1, totalAlerts: 2 }], - count_open_cases: 1, - }); - }); - - it('should return multiple sub cases', async () => { - const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - const subCase2Resp = await createSubCase({ supertest, caseID: caseInfo.id, actionID }); - - const { body }: { body: SubCasesFindResponse } = await supertest - .get(`${getSubCasesUrl(caseInfo.id)}/_find`) - .expect(200); - - expect(body).to.eql({ - ...findSubCasesResp, - total: 2, - // find should not return the comments themselves only the stats - subCases: [ - { - // there should only be 1 closed sub case - ...subCase2Resp.modifiedSubCases![0], - comments: [], - totalComment: 1, - totalAlerts: 2, - status: CaseStatuses.closed, - }, - { - ...subCase2Resp.newSubCaseInfo.subCases![0], - comments: [], - totalComment: 1, - totalAlerts: 2, - }, - ], - count_open_cases: 1, - count_closed_cases: 1, - }); - }); - - it('should only return open when filtering for open', async () => { - const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - // this will result in one closed case and one open - await createSubCase({ supertest, caseID: caseInfo.id, actionID }); - - const { body }: { body: SubCasesFindResponse } = await supertest - .get(`${getSubCasesUrl(caseInfo.id)}/_find?status=${CaseStatuses.open}`) - .expect(200); - - expect(body.total).to.be(1); - expect(body.subCases[0].status).to.be(CaseStatuses.open); - expect(body.count_closed_cases).to.be(1); - expect(body.count_open_cases).to.be(1); - expect(body.count_in_progress_cases).to.be(0); - }); - - it('should only return closed when filtering for closed', async () => { - const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - // this will result in one closed case and one open - await createSubCase({ supertest, caseID: caseInfo.id, actionID }); - - const { body }: { body: SubCasesFindResponse } = await supertest - .get(`${getSubCasesUrl(caseInfo.id)}/_find?status=${CaseStatuses.closed}`) - .expect(200); - - expect(body.total).to.be(1); - expect(body.subCases[0].status).to.be(CaseStatuses.closed); - expect(body.count_closed_cases).to.be(1); - expect(body.count_open_cases).to.be(1); - expect(body.count_in_progress_cases).to.be(0); - }); - - it('should only return in progress when filtering for in progress', async () => { - const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - // this will result in one closed case and one open - const { newSubCaseInfo: secondSub } = await createSubCase({ - supertest, - caseID: caseInfo.id, - actionID, - }); - - await setStatus({ - supertest, - cases: [ - { - id: secondSub.subCases![0].id, - version: secondSub.subCases![0].version, - status: CaseStatuses['in-progress'], - }, - ], - type: 'sub_case', - }); - - const { body }: { body: SubCasesFindResponse } = await supertest - .get(`${getSubCasesUrl(caseInfo.id)}/_find?status=${CaseStatuses['in-progress']}`) - .expect(200); - - expect(body.total).to.be(1); - expect(body.subCases[0].status).to.be(CaseStatuses['in-progress']); - expect(body.count_closed_cases).to.be(1); - expect(body.count_open_cases).to.be(0); - expect(body.count_in_progress_cases).to.be(1); - }); - - it('should sort on createdAt field in descending order', async () => { - const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - // this will result in one closed case and one open - await createSubCase({ - supertest, - caseID: caseInfo.id, - actionID, - }); - - const { body }: { body: SubCasesFindResponse } = await supertest - .get(`${getSubCasesUrl(caseInfo.id)}/_find?sortField=createdAt&sortOrder=desc`) - .expect(200); - - expect(body.total).to.be(2); - expect(body.subCases[0].status).to.be(CaseStatuses.open); - expect(body.subCases[1].status).to.be(CaseStatuses.closed); - expect(body.count_closed_cases).to.be(1); - expect(body.count_open_cases).to.be(1); - expect(body.count_in_progress_cases).to.be(0); - }); - - it('should sort on createdAt field in ascending order', async () => { - const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - // this will result in one closed case and one open - await createSubCase({ - supertest, - caseID: caseInfo.id, - actionID, - }); - - const { body }: { body: SubCasesFindResponse } = await supertest - .get(`${getSubCasesUrl(caseInfo.id)}/_find?sortField=createdAt&sortOrder=asc`) - .expect(200); - - expect(body.total).to.be(2); - expect(body.subCases[0].status).to.be(CaseStatuses.closed); - expect(body.subCases[1].status).to.be(CaseStatuses.open); - expect(body.count_closed_cases).to.be(1); - expect(body.count_open_cases).to.be(1); - expect(body.count_in_progress_cases).to.be(0); - }); - - it('should sort on updatedAt field in ascending order', async () => { - const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - // this will result in one closed case and one open - const { newSubCaseInfo: secondSub } = await createSubCase({ - supertest, - caseID: caseInfo.id, - actionID, - }); - - await setStatus({ - supertest, - cases: [ - { - id: secondSub.subCases![0].id, - version: secondSub.subCases![0].version, - status: CaseStatuses['in-progress'], - }, - ], - type: 'sub_case', - }); - - const { body }: { body: SubCasesFindResponse } = await supertest - .get(`${getSubCasesUrl(caseInfo.id)}/_find?sortField=updatedAt&sortOrder=asc`) - .expect(200); - - expect(body.total).to.be(2); - expect(body.subCases[0].status).to.be(CaseStatuses.closed); - expect(body.subCases[1].status).to.be(CaseStatuses['in-progress']); - expect(body.count_closed_cases).to.be(1); - expect(body.count_open_cases).to.be(0); - expect(body.count_in_progress_cases).to.be(1); - }); - }); - - describe('pagination', () => { - const numCases = 4; - let collection: CaseResponse; - before(async () => { - ({ body: collection } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send(postCollectionReq) - .expect(200)); - - await createSubCases(numCases, collection.id); - }); - - after(async () => { - await deleteAllCaseItems(es); - }); - - const createSubCases = async (total: number, caseID: string) => { - const responses: CreateSubCaseResp[] = []; - for (let i = 0; i < total; i++) { - const postCommentGenAlertReq: ContextTypeGeneratedAlertType = { - alerts: createAlertsString([ - { _id: `${i}`, _index: 'test-index', ruleId: 'rule-id', ruleName: 'rule name' }, - ]), - type: CommentType.generatedAlert, - owner: 'securitySolutionFixture', - }; - responses.push( - await createSubCase({ - supertest, - actionID, - caseID, - comment: postCommentGenAlertReq, - }) - ); - } - return responses; - }; - - const getAllCasesSortedByCreatedAtAsc = async () => { - const cases: ApiResponse> = await es.search({ - index: '.kibana', - body: { - size: 10000, - sort: [{ 'cases-sub-case.created_at': { unmapped_type: 'date', order: 'asc' } }], - query: { - term: { type: 'cases-sub-case' }, - }, - }, - }); - return cases.body.hits.hits.map((hit) => hit._source); - }; - - it('returns the correct total when perPage is less than the total', async () => { - const { body }: { body: SubCasesFindResponse } = await supertest - .get(`${getSubCasesUrl(collection.id)}/_find`) - .query({ - sortOrder: 'asc', - page: 1, - perPage: 3, - }) - .set('kbn-xsrf', 'true') - .expect(200); - - expect(body.subCases.length).to.eql(3); - expect(body.total).to.eql(4); - expect(body.page).to.eql(1); - expect(body.per_page).to.eql(3); - // there will only be 1 open sub case, all the rest will be closed - expect(body.count_open_cases).to.eql(1); - expect(body.count_closed_cases).to.eql(3); - expect(body.count_in_progress_cases).to.eql(0); - }); - - it('returns the correct total when perPage is greater than the total', async () => { - const { body }: { body: SubCasesFindResponse } = await supertest - .get(`${getSubCasesUrl(collection.id)}/_find`) - .query({ - sortOrder: 'asc', - page: 1, - perPage: 11, - }) - .set('kbn-xsrf', 'true') - .expect(200); - - expect(body.total).to.eql(4); - expect(body.page).to.eql(1); - expect(body.per_page).to.eql(11); - expect(body.subCases.length).to.eql(4); - expect(body.count_open_cases).to.eql(1); - expect(body.count_closed_cases).to.eql(3); - expect(body.count_in_progress_cases).to.eql(0); - }); - - it('returns the correct total when perPage is equal to the total', async () => { - const { body }: { body: SubCasesFindResponse } = await supertest - .get(`${getSubCasesUrl(collection.id)}/_find`) - .query({ - sortOrder: 'asc', - page: 1, - perPage: 4, - }) - .set('kbn-xsrf', 'true') - .expect(200); - - expect(body.total).to.eql(4); - expect(body.page).to.eql(1); - expect(body.per_page).to.eql(4); - expect(body.subCases.length).to.eql(4); - expect(body.count_open_cases).to.eql(1); - expect(body.count_closed_cases).to.eql(3); - expect(body.count_in_progress_cases).to.eql(0); - }); - - it('returns the second page of results', async () => { - const perPage = 2; - const { body }: { body: SubCasesFindResponse } = await supertest - .get(`${getSubCasesUrl(collection.id)}/_find`) - .query({ - sortOrder: 'asc', - page: 2, - perPage, - }) - .set('kbn-xsrf', 'true') - .expect(200); - - expect(body.total).to.eql(4); - expect(body.page).to.eql(2); - expect(body.per_page).to.eql(2); - expect(body.subCases.length).to.eql(2); - expect(body.count_open_cases).to.eql(1); - expect(body.count_closed_cases).to.eql(3); - expect(body.count_in_progress_cases).to.eql(0); - - const allCases = await getAllCasesSortedByCreatedAtAsc(); - - body.subCases.map((subCaseInfo, index) => { - // we started on the second page of 10 cases with a perPage of 5, so the first case should 0 + 5 (index + perPage) - expect(subCaseInfo.created_at).to.eql( - allCases[index + perPage]?.['cases-sub-case'].created_at - ); - }); - }); - - it('paginates with perPage of 2 through 4 total sub cases', async () => { - const total = 4; - const perPage = 2; - - // it's less than or equal here because the page starts at 1, so page 2 is a valid page number - // and should have sub cases titles 3, and 4 - for (let currentPage = 1; currentPage <= total / perPage; currentPage++) { - const { body }: { body: SubCasesFindResponse } = await supertest - .get(`${getSubCasesUrl(collection.id)}/_find`) - .query({ - sortOrder: 'asc', - page: currentPage, - perPage, - }) - .set('kbn-xsrf', 'true') - .expect(200); - - expect(body.total).to.eql(total); - expect(body.page).to.eql(currentPage); - expect(body.per_page).to.eql(perPage); - expect(body.subCases.length).to.eql(perPage); - expect(body.count_open_cases).to.eql(1); - expect(body.count_closed_cases).to.eql(total - 1); - expect(body.count_in_progress_cases).to.eql(0); - - const allCases = await getAllCasesSortedByCreatedAtAsc(); - - body.subCases.map((subCaseInfo, index) => { - // for page 1, the cases tiles should be 0,1,2 for page 2: 3,4,5 etc (assuming the titles were sorted - // correctly) - expect(subCaseInfo.created_at).to.eql( - allCases[index + perPage * (currentPage - 1)]?.['cases-sub-case'].created_at - ); - }); - } - }); - - it('retrieves the last sub case', async () => { - const { body }: { body: SubCasesFindResponse } = await supertest - .get(`${getSubCasesUrl(collection.id)}/_find`) - .query({ - sortOrder: 'asc', - // this should skip the first 3 sub cases and only return the last one - page: 2, - perPage: 3, - }) - .set('kbn-xsrf', 'true') - .expect(200); - - expect(body.total).to.eql(4); - expect(body.page).to.eql(2); - expect(body.per_page).to.eql(3); - expect(body.subCases.length).to.eql(1); - expect(body.count_open_cases).to.eql(1); - expect(body.count_closed_cases).to.eql(3); - expect(body.count_in_progress_cases).to.eql(0); - }); - }); - }); - }); -}; diff --git a/x-pack/test/case_api_integration/spaces_only/tests/common/sub_cases/get_sub_case.ts b/x-pack/test/case_api_integration/spaces_only/tests/common/sub_cases/get_sub_case.ts deleted file mode 100644 index 35ed4ba5c3c71b..00000000000000 --- a/x-pack/test/case_api_integration/spaces_only/tests/common/sub_cases/get_sub_case.ts +++ /dev/null @@ -1,119 +0,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 expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; - -import { commentsResp, postCommentAlertReq, subCaseResp } from '../../../../common/lib/mock'; -import { - createCaseAction, - createSubCase, - defaultCreateSubComment, - deleteAllCaseItems, - deleteCaseAction, - removeServerGeneratedPropertiesFromComments, - removeServerGeneratedPropertiesFromSubCase, -} from '../../../../common/lib/utils'; -import { - getCaseCommentsUrl, - getSubCaseDetailsUrl, -} from '../../../../../../plugins/cases/common/api/helpers'; -import { - AssociationType, - CaseResponse, - SubCaseResponse, -} from '../../../../../../plugins/cases/common/api'; - -// eslint-disable-next-line import/no-default-export -export default ({ getService }: FtrProviderContext): void => { - const supertest = getService('supertest'); - const es = getService('es'); - - // ENABLE_CASE_CONNECTOR: remove the outer describe once the case connector feature is completed - describe('get_sub_case disabled route', () => { - it('should return a 404 when attempting to access the route and the case connector feature is disabled', async () => { - await supertest.get(getSubCaseDetailsUrl('case-id', 'sub-case-id')).expect(404); - }); - - // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests - describe.skip('get_sub_case', () => { - let actionID: string; - before(async () => { - actionID = await createCaseAction(supertest); - }); - after(async () => { - await deleteCaseAction(supertest, actionID); - }); - afterEach(async () => { - await deleteAllCaseItems(es); - }); - - it('should return a case', async () => { - const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - - const { body }: { body: SubCaseResponse } = await supertest - .get(getSubCaseDetailsUrl(caseInfo.id, caseInfo.subCases![0].id)) - .set('kbn-xsrf', 'true') - .send() - .expect(200); - - expect(removeServerGeneratedPropertiesFromComments(body.comments)).to.eql( - commentsResp({ - comments: [{ comment: defaultCreateSubComment, id: caseInfo.comments![0].id }], - associationType: AssociationType.subCase, - }) - ); - - expect(removeServerGeneratedPropertiesFromSubCase(body)).to.eql( - subCaseResp({ id: body.id, totalComment: 1, totalAlerts: 2 }) - ); - }); - - it('should return the correct number of alerts with multiple types of alerts', async () => { - const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - - const { body: singleAlert }: { body: CaseResponse } = await supertest - .post(getCaseCommentsUrl(caseInfo.id)) - .query({ subCaseId: caseInfo.subCases![0].id }) - .set('kbn-xsrf', 'true') - .send(postCommentAlertReq) - .expect(200); - - const { body }: { body: SubCaseResponse } = await supertest - .get(getSubCaseDetailsUrl(caseInfo.id, caseInfo.subCases![0].id)) - .set('kbn-xsrf', 'true') - .send() - .expect(200); - - expect(removeServerGeneratedPropertiesFromComments(body.comments)).to.eql( - commentsResp({ - comments: [ - { comment: defaultCreateSubComment, id: caseInfo.comments![0].id }, - { - comment: postCommentAlertReq, - id: singleAlert.comments![1].id, - }, - ], - associationType: AssociationType.subCase, - }) - ); - - expect(removeServerGeneratedPropertiesFromSubCase(body)).to.eql( - subCaseResp({ id: body.id, totalComment: 2, totalAlerts: 3 }) - ); - }); - - it('unhappy path - 404s when case is not there', async () => { - await supertest - .get(getSubCaseDetailsUrl('fake-case-id', 'fake-sub-case-id')) - .set('kbn-xsrf', 'true') - .send() - .expect(404); - }); - }); - }); -}; diff --git a/x-pack/test/case_api_integration/spaces_only/tests/common/sub_cases/patch_sub_cases.ts b/x-pack/test/case_api_integration/spaces_only/tests/common/sub_cases/patch_sub_cases.ts deleted file mode 100644 index 442644463fa38d..00000000000000 --- a/x-pack/test/case_api_integration/spaces_only/tests/common/sub_cases/patch_sub_cases.ts +++ /dev/null @@ -1,515 +0,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 expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; - -import { - CASES_URL, - SUB_CASES_PATCH_DEL_URL, -} from '../../../../../../plugins/cases/common/constants'; -import { - createCaseAction, - createSubCase, - deleteAllCaseItems, - deleteCaseAction, - getSignalsWithES, - setStatus, -} from '../../../../common/lib/utils'; -import { getSubCaseDetailsUrl } from '../../../../../../plugins/cases/common/api/helpers'; -import { - CaseStatuses, - CommentType, - SubCaseResponse, -} from '../../../../../../plugins/cases/common/api'; -import { createAlertsString } from '../../../../../../plugins/cases/server/connectors'; -import { postCaseReq, postCollectionReq } from '../../../../common/lib/mock'; - -const defaultSignalsIndex = '.siem-signals-default-000001'; - -// eslint-disable-next-line import/no-default-export -export default function ({ getService }: FtrProviderContext) { - const supertest = getService('supertest'); - const es = getService('es'); - const esArchiver = getService('esArchiver'); - - // ENABLE_CASE_CONNECTOR: remove the outer describe once the case connector feature is completed - describe('patch_sub_cases disabled route', () => { - it('should return a 404 when attempting to access the route and the case connector feature is disabled', async () => { - await supertest - .patch(SUB_CASES_PATCH_DEL_URL) - .set('kbn-xsrf', 'true') - .send({ subCases: [] }) - .expect(404); - }); - - // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests - describe.skip('patch_sub_cases', () => { - let actionID: string; - before(async () => { - actionID = await createCaseAction(supertest); - }); - after(async () => { - await deleteCaseAction(supertest, actionID); - }); - beforeEach(async () => { - await esArchiver.load('cases/signals/default'); - }); - afterEach(async () => { - await esArchiver.unload('cases/signals/default'); - await deleteAllCaseItems(es); - }); - - it('should update the status of a sub case', async () => { - const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - - await setStatus({ - supertest, - cases: [ - { - id: caseInfo.subCases![0].id, - version: caseInfo.subCases![0].version, - status: CaseStatuses['in-progress'], - }, - ], - type: 'sub_case', - }); - const { body: subCase }: { body: SubCaseResponse } = await supertest - .get(getSubCaseDetailsUrl(caseInfo.id, caseInfo.subCases![0].id)) - .expect(200); - - expect(subCase.status).to.eql(CaseStatuses['in-progress']); - }); - - it('should update the status of one alert attached to a sub case', async () => { - const signalID = '5f2b9ec41f8febb1c06b5d1045aeabb9874733b7617e88a370510f2fb3a41a5d'; - - const { newSubCaseInfo: caseInfo } = await createSubCase({ - supertest, - actionID, - comment: { - alerts: createAlertsString([ - { - _id: signalID, - _index: defaultSignalsIndex, - ruleId: 'id', - ruleName: 'name', - }, - ]), - type: CommentType.generatedAlert, - owner: 'securitySolutionFixture', - }, - }); - - await es.indices.refresh({ index: defaultSignalsIndex }); - - let signals = await getSignalsWithES({ es, indices: defaultSignalsIndex, ids: signalID }); - - expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( - CaseStatuses.open - ); - - await setStatus({ - supertest, - cases: [ - { - id: caseInfo.subCases![0].id, - version: caseInfo.subCases![0].version, - status: CaseStatuses['in-progress'], - }, - ], - type: 'sub_case', - }); - - await es.indices.refresh({ index: defaultSignalsIndex }); - - signals = await getSignalsWithES({ es, indices: defaultSignalsIndex, ids: signalID }); - - expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( - CaseStatuses['in-progress'] - ); - }); - - it('should update the status of multiple alerts attached to a sub case', async () => { - const signalID = '5f2b9ec41f8febb1c06b5d1045aeabb9874733b7617e88a370510f2fb3a41a5d'; - - const signalID2 = '4d0f4b1533e46b66b43bdd0330d23f39f2cf42a7253153270e38d30cce9ff0c6'; - - const { newSubCaseInfo: caseInfo } = await createSubCase({ - supertest, - actionID, - comment: { - alerts: createAlertsString([ - { - _id: signalID, - _index: defaultSignalsIndex, - ruleId: 'id', - ruleName: 'name', - }, - { - _id: signalID2, - _index: defaultSignalsIndex, - ruleId: 'id', - ruleName: 'name', - }, - ]), - type: CommentType.generatedAlert, - owner: 'securitySolutionFixture', - }, - }); - - await es.indices.refresh({ index: defaultSignalsIndex }); - - let signals = await getSignalsWithES({ - es, - indices: defaultSignalsIndex, - ids: [signalID, signalID2], - }); - - expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( - CaseStatuses.open - ); - expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source?.signal.status).to.be( - CaseStatuses.open - ); - - await setStatus({ - supertest, - cases: [ - { - id: caseInfo.subCases![0].id, - version: caseInfo.subCases![0].version, - status: CaseStatuses['in-progress'], - }, - ], - type: 'sub_case', - }); - - await es.indices.refresh({ index: defaultSignalsIndex }); - - signals = await getSignalsWithES({ - es, - indices: defaultSignalsIndex, - ids: [signalID, signalID2], - }); - - expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( - CaseStatuses['in-progress'] - ); - expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source?.signal.status).to.be( - CaseStatuses['in-progress'] - ); - }); - - it('should update the status of multiple alerts attached to multiple sub cases in one collection', async () => { - const signalID = '5f2b9ec41f8febb1c06b5d1045aeabb9874733b7617e88a370510f2fb3a41a5d'; - const signalID2 = '4d0f4b1533e46b66b43bdd0330d23f39f2cf42a7253153270e38d30cce9ff0c6'; - - const { newSubCaseInfo: initialCaseInfo } = await createSubCase({ - supertest, - actionID, - caseInfo: { - ...postCollectionReq, - settings: { - syncAlerts: false, - }, - }, - comment: { - alerts: createAlertsString([ - { - _id: signalID, - _index: defaultSignalsIndex, - ruleId: 'id', - ruleName: 'name', - }, - ]), - type: CommentType.generatedAlert, - owner: 'securitySolutionFixture', - }, - }); - - // This will close the first sub case and create a new one - const { newSubCaseInfo: collectionWithSecondSub } = await createSubCase({ - supertest, - actionID, - caseID: initialCaseInfo.id, - comment: { - alerts: createAlertsString([ - { - _id: signalID2, - _index: defaultSignalsIndex, - ruleId: 'id', - ruleName: 'name', - }, - ]), - type: CommentType.generatedAlert, - owner: 'securitySolutionFixture', - }, - }); - - await es.indices.refresh({ index: defaultSignalsIndex }); - - let signals = await getSignalsWithES({ - es, - indices: defaultSignalsIndex, - ids: [signalID, signalID2], - }); - - // There should be no change in their status since syncing is disabled - expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( - CaseStatuses.open - ); - expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source?.signal.status).to.be( - CaseStatuses.open - ); - - await setStatus({ - supertest, - cases: [ - { - id: collectionWithSecondSub.subCases![0].id, - version: collectionWithSecondSub.subCases![0].version, - status: CaseStatuses['in-progress'], - }, - ], - type: 'sub_case', - }); - - await es.indices.refresh({ index: defaultSignalsIndex }); - - signals = await getSignalsWithES({ - es, - indices: defaultSignalsIndex, - ids: [signalID, signalID2], - }); - - // There still should be no change in their status since syncing is disabled - expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( - CaseStatuses.open - ); - expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source?.signal.status).to.be( - CaseStatuses.open - ); - - // Turn sync alerts on - await supertest - .patch(CASES_URL) - .set('kbn-xsrf', 'true') - .send({ - cases: [ - { - id: collectionWithSecondSub.id, - version: collectionWithSecondSub.version, - settings: { syncAlerts: true }, - }, - ], - }) - .expect(200); - - await es.indices.refresh({ index: defaultSignalsIndex }); - - signals = await getSignalsWithES({ - es, - indices: defaultSignalsIndex, - ids: [signalID, signalID2], - }); - - expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( - CaseStatuses.closed - ); - expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source?.signal.status).to.be( - CaseStatuses['in-progress'] - ); - }); - - it('should update the status of alerts attached to a case and sub case when sync settings is turned on', async () => { - const signalID = '5f2b9ec41f8febb1c06b5d1045aeabb9874733b7617e88a370510f2fb3a41a5d'; - const signalID2 = '4d0f4b1533e46b66b43bdd0330d23f39f2cf42a7253153270e38d30cce9ff0c6'; - - const { body: individualCase } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send({ - ...postCaseReq, - settings: { - syncAlerts: false, - }, - }); - - const { newSubCaseInfo: caseInfo } = await createSubCase({ - supertest, - actionID, - caseInfo: { - ...postCollectionReq, - settings: { - syncAlerts: false, - }, - }, - comment: { - alerts: createAlertsString([ - { - _id: signalID, - _index: defaultSignalsIndex, - ruleId: 'id', - ruleName: 'name', - }, - ]), - type: CommentType.generatedAlert, - owner: 'securitySolutionFixture', - }, - }); - - const { body: updatedIndWithComment } = await supertest - .post(`${CASES_URL}/${individualCase.id}/comments`) - .set('kbn-xsrf', 'true') - .send({ - alertId: signalID2, - index: defaultSignalsIndex, - rule: { id: 'test-rule-id', name: 'test-index-id' }, - type: CommentType.alert, - }) - .expect(200); - - await es.indices.refresh({ index: defaultSignalsIndex }); - - let signals = await getSignalsWithES({ - es, - indices: defaultSignalsIndex, - ids: [signalID, signalID2], - }); - - // There should be no change in their status since syncing is disabled - expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( - CaseStatuses.open - ); - expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source?.signal.status).to.be( - CaseStatuses.open - ); - - await setStatus({ - supertest, - cases: [ - { - id: caseInfo.subCases![0].id, - version: caseInfo.subCases![0].version, - status: CaseStatuses['in-progress'], - }, - ], - type: 'sub_case', - }); - - const updatedIndWithStatus = ( - await setStatus({ - supertest, - cases: [ - { - id: updatedIndWithComment.id, - version: updatedIndWithComment.version, - status: CaseStatuses.closed, - }, - ], - type: 'case', - }) - )[0]; // there should only be a single entry in the response - - await es.indices.refresh({ index: defaultSignalsIndex }); - - signals = await getSignalsWithES({ - es, - indices: defaultSignalsIndex, - ids: [signalID, signalID2], - }); - - // There should still be no change in their status since syncing is disabled - expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( - CaseStatuses.open - ); - expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source?.signal.status).to.be( - CaseStatuses.open - ); - - // Turn sync alerts on - await supertest - .patch(CASES_URL) - .set('kbn-xsrf', 'true') - .send({ - cases: [ - { - id: caseInfo.id, - version: caseInfo.version, - settings: { syncAlerts: true }, - }, - ], - }) - .expect(200); - - await supertest - .patch(CASES_URL) - .set('kbn-xsrf', 'true') - .send({ - cases: [ - { - id: updatedIndWithStatus.id, - version: updatedIndWithStatus.version, - settings: { syncAlerts: true }, - }, - ], - }) - .expect(200); - - await es.indices.refresh({ index: defaultSignalsIndex }); - - signals = await getSignalsWithES({ - es, - indices: defaultSignalsIndex, - ids: [signalID, signalID2], - }); - - // alerts should be updated now that the - expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( - CaseStatuses['in-progress'] - ); - expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source?.signal.status).to.be( - CaseStatuses.closed - ); - }); - - it('404s when sub case id is invalid', async () => { - await supertest - .patch(`${SUB_CASES_PATCH_DEL_URL}`) - .set('kbn-xsrf', 'true') - .send({ - subCases: [ - { - id: 'fake-id', - version: 'blah', - status: CaseStatuses.open, - }, - ], - }) - .expect(404); - }); - - it('406s when updating invalid fields for a sub case', async () => { - const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - - await supertest - .patch(`${SUB_CASES_PATCH_DEL_URL}`) - .set('kbn-xsrf', 'true') - .send({ - subCases: [ - { - id: caseInfo.subCases![0].id, - version: caseInfo.subCases![0].version, - type: 'blah', - }, - ], - }) - .expect(406); - }); - }); - }); -} diff --git a/x-pack/test/case_api_integration/spaces_only/tests/common/user_actions/get_all_user_actions.ts b/x-pack/test/case_api_integration/spaces_only/tests/common/user_actions/get_all_user_actions.ts index 5cd4082bd3293b..199e53ebd1bb55 100644 --- a/x-pack/test/case_api_integration/spaces_only/tests/common/user_actions/get_all_user_actions.ts +++ b/x-pack/test/case_api_integration/spaces_only/tests/common/user_actions/get_all_user_actions.ts @@ -8,388 +8,41 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; -import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; -import { - CaseResponse, - CaseStatuses, - CommentType, -} from '../../../../../../plugins/cases/common/api'; -import { - userActionPostResp, - postCaseReq, - postCommentUserReq, - getPostCaseRequest, -} from '../../../../common/lib/mock'; +import { getPostCaseRequest } from '../../../../common/lib/mock'; import { deleteAllCaseItems, createCase, - updateCase, getCaseUserActions, + getAuthWithSuperUser, } from '../../../../common/lib/utils'; -import { - globalRead, - noKibanaPrivileges, - obsSec, - obsSecRead, - secOnly, - secOnlyRead, - superUser, -} from '../../../../common/lib/authentication/users'; -import { superUserSpace1Auth } from '../../../../common/lib/authentication'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); const es = getService('es'); + const authSpace1 = getAuthWithSuperUser(); describe('get_all_user_actions', () => { afterEach(async () => { await deleteAllCaseItems(es); }); - it(`on new case, user action: 'create' should be called with actionFields: ['description', 'status', 'tags', 'title', 'connector', 'settings, owner]`, async () => { - const { body: postedCase } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send(postCaseReq) - .expect(200); - - const { body } = await supertest - .get(`${CASES_URL}/${postedCase.id}/user_actions`) - .set('kbn-xsrf', 'true') - .send() - .expect(200); + it(`should get user actions in space1`, async () => { + const postedCase = await createCase(supertest, getPostCaseRequest(), 200, authSpace1); + const body = await getCaseUserActions({ supertest, caseID: postedCase.id, auth: authSpace1 }); expect(body.length).to.eql(1); - - expect(body[0].action_field).to.eql([ - 'description', - 'status', - 'tags', - 'title', - 'connector', - 'settings', - 'owner', - ]); - expect(body[0].action).to.eql('create'); - expect(body[0].old_value).to.eql(null); - expect(JSON.parse(body[0].new_value)).to.eql(userActionPostResp); - }); - - it(`on close case, user action: 'update' should be called with actionFields: ['status']`, async () => { - const { body: postedCase } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send(postCaseReq) - .expect(200); - - await supertest - .patch(CASES_URL) - .set('kbn-xsrf', 'true') - .send({ - cases: [ - { - id: postedCase.id, - version: postedCase.version, - status: 'closed', - }, - ], - }) - .expect(200); - - const { body } = await supertest - .get(`${CASES_URL}/${postedCase.id}/user_actions`) - .set('kbn-xsrf', 'true') - .send() - .expect(200); - - expect(body.length).to.eql(2); - expect(body[1].action_field).to.eql(['status']); - expect(body[1].action).to.eql('update'); - expect(body[1].old_value).to.eql('open'); - expect(body[1].new_value).to.eql('closed'); - }); - - it(`on update case connector, user action: 'update' should be called with actionFields: ['connector']`, async () => { - const { body: postedCase } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send(postCaseReq) - .expect(200); - - const newConnector = { - id: '123', - name: 'Connector', - type: '.jira', - fields: { issueType: 'Task', priority: 'High', parent: null }, - }; - - await supertest - .patch(CASES_URL) - .set('kbn-xsrf', 'true') - .send({ - cases: [ - { - id: postedCase.id, - version: postedCase.version, - connector: newConnector, - }, - ], - }) - .expect(200); - - const { body } = await supertest - .get(`${CASES_URL}/${postedCase.id}/user_actions`) - .set('kbn-xsrf', 'true') - .send() - .expect(200); - - expect(body.length).to.eql(2); - expect(body[1].action_field).to.eql(['connector']); - expect(body[1].action).to.eql('update'); - expect(JSON.parse(body[1].old_value)).to.eql({ - id: 'none', - name: 'none', - type: '.none', - fields: null, - }); - expect(JSON.parse(body[1].new_value)).to.eql({ - id: '123', - name: 'Connector', - type: '.jira', - fields: { issueType: 'Task', priority: 'High', parent: null }, - }); - }); - - it(`on update tags, user action: 'add' and 'delete' should be called with actionFields: ['tags']`, async () => { - const { body: postedCase } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send(postCaseReq) - .expect(200); - - await supertest - .patch(CASES_URL) - .set('kbn-xsrf', 'true') - .send({ - cases: [ - { - id: postedCase.id, - version: postedCase.version, - tags: ['cool', 'neat'], - }, - ], - }) - .expect(200); - - const { body } = await supertest - .get(`${CASES_URL}/${postedCase.id}/user_actions`) - .set('kbn-xsrf', 'true') - .send() - .expect(200); - - expect(body.length).to.eql(3); - expect(body[1].action_field).to.eql(['tags']); - expect(body[1].action).to.eql('add'); - expect(body[1].old_value).to.eql(null); - expect(body[1].new_value).to.eql('cool, neat'); - expect(body[2].action_field).to.eql(['tags']); - expect(body[2].action).to.eql('delete'); - expect(body[2].old_value).to.eql(null); - expect(body[2].new_value).to.eql('defacement'); - }); - - it(`on update title, user action: 'update' should be called with actionFields: ['title']`, async () => { - const { body: postedCase } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send(postCaseReq) - .expect(200); - - const newTitle = 'Such a great title'; - await supertest - .patch(CASES_URL) - .set('kbn-xsrf', 'true') - .send({ - cases: [ - { - id: postedCase.id, - version: postedCase.version, - title: newTitle, - }, - ], - }) - .expect(200); - - const { body } = await supertest - .get(`${CASES_URL}/${postedCase.id}/user_actions`) - .set('kbn-xsrf', 'true') - .send() - .expect(200); - - expect(body.length).to.eql(2); - expect(body[1].action_field).to.eql(['title']); - expect(body[1].action).to.eql('update'); - expect(body[1].old_value).to.eql(postCaseReq.title); - expect(body[1].new_value).to.eql(newTitle); - }); - - it(`on update description, user action: 'update' should be called with actionFields: ['description']`, async () => { - const { body: postedCase } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send(postCaseReq) - .expect(200); - - const newDesc = 'Such a great description'; - await supertest - .patch(CASES_URL) - .set('kbn-xsrf', 'true') - .send({ - cases: [ - { - id: postedCase.id, - version: postedCase.version, - description: newDesc, - }, - ], - }) - .expect(200); - - const { body } = await supertest - .get(`${CASES_URL}/${postedCase.id}/user_actions`) - .set('kbn-xsrf', 'true') - .send() - .expect(200); - - expect(body.length).to.eql(2); - expect(body[1].action_field).to.eql(['description']); - expect(body[1].action).to.eql('update'); - expect(body[1].old_value).to.eql(postCaseReq.description); - expect(body[1].new_value).to.eql(newDesc); - }); - - it(`on new comment, user action: 'create' should be called with actionFields: ['comments']`, async () => { - const { body: postedCase } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send(postCaseReq) - .expect(200); - - await supertest - .post(`${CASES_URL}/${postedCase.id}/comments`) - .set('kbn-xsrf', 'true') - .send(postCommentUserReq) - .expect(200); - - const { body } = await supertest - .get(`${CASES_URL}/${postedCase.id}/user_actions`) - .set('kbn-xsrf', 'true') - .send() - .expect(200); - - expect(body.length).to.eql(2); - expect(body[1].action_field).to.eql(['comment']); - expect(body[1].action).to.eql('create'); - expect(body[1].old_value).to.eql(null); - expect(JSON.parse(body[1].new_value)).to.eql(postCommentUserReq); }); - it(`on update comment, user action: 'update' should be called with actionFields: ['comments']`, async () => { - const { body: postedCase } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send(postCaseReq) - .expect(200); - - const { body: patchedCase } = await supertest - .post(`${CASES_URL}/${postedCase.id}/comments`) - .set('kbn-xsrf', 'true') - .send(postCommentUserReq) - .expect(200); - - const newComment = 'Well I decided to update my comment. So what? Deal with it.'; - await supertest - .patch(`${CASES_URL}/${postedCase.id}/comments`) - .set('kbn-xsrf', 'true') - .send({ - id: patchedCase.comments[0].id, - version: patchedCase.comments[0].version, - comment: newComment, - type: CommentType.user, - owner: 'securitySolutionFixture', - }) - .expect(200); - - const { body } = await supertest - .get(`${CASES_URL}/${postedCase.id}/user_actions`) - .set('kbn-xsrf', 'true') - .send() - .expect(200); - - expect(body.length).to.eql(3); - expect(body[2].action_field).to.eql(['comment']); - expect(body[2].action).to.eql('update'); - expect(JSON.parse(body[2].old_value)).to.eql(postCommentUserReq); - expect(JSON.parse(body[2].new_value)).to.eql({ - comment: newComment, - type: CommentType.user, - owner: 'securitySolutionFixture', - }); - }); - - describe('rbac', () => { - const supertestWithoutAuth = getService('supertestWithoutAuth'); - - let caseInfo: CaseResponse; - beforeEach(async () => { - caseInfo = await createCase(supertestWithoutAuth, getPostCaseRequest(), 200, { - user: superUser, - space: 'space1', - }); - - await updateCase({ - supertest: supertestWithoutAuth, - params: { - cases: [ - { - id: caseInfo.id, - version: caseInfo.version, - status: CaseStatuses.closed, - }, - ], - }, - auth: superUserSpace1Auth, - }); - }); - - it('should get the user actions for a case when the user has the correct permissions', async () => { - for (const user of [globalRead, superUser, secOnly, secOnlyRead, obsSec, obsSecRead]) { - const userActions = await getCaseUserActions({ - supertest: supertestWithoutAuth, - caseID: caseInfo.id, - auth: { user, space: 'space1' }, - }); - - expect(userActions.length).to.eql(2); - } + it(`should not get user actions in the wrong space`, async () => { + const postedCase = await createCase(supertest, getPostCaseRequest(), 200, authSpace1); + const body = await getCaseUserActions({ + supertest, + caseID: postedCase.id, + auth: getAuthWithSuperUser('space2'), }); - for (const scenario of [ - { user: noKibanaPrivileges, space: 'space1' }, - { user: secOnly, space: 'space2' }, - ]) { - it(`should 403 when requesting the user actions of a case with user ${ - scenario.user.username - } with role(s) ${scenario.user.roles.join()} and space ${scenario.space}`, async () => { - await getCaseUserActions({ - supertest: supertestWithoutAuth, - caseID: caseInfo.id, - auth: { user: scenario.user, space: scenario.space }, - expectedHttpCode: 403, - }); - }); - } + expect(body.length).to.eql(0); }); }); }; diff --git a/x-pack/test/case_api_integration/spaces_only/tests/common/user_actions/migrations.ts b/x-pack/test/case_api_integration/spaces_only/tests/common/user_actions/migrations.ts deleted file mode 100644 index e198260e88a9c1..00000000000000 --- a/x-pack/test/case_api_integration/spaces_only/tests/common/user_actions/migrations.ts +++ /dev/null @@ -1,53 +0,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 expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; -import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; - -// eslint-disable-next-line import/no-default-export -export default function createGetTests({ getService }: FtrProviderContext) { - const supertest = getService('supertest'); - const esArchiver = getService('esArchiver'); - - describe('migrations', () => { - before(async () => { - await esArchiver.load('cases/migrations/7.10.0'); - }); - - after(async () => { - await esArchiver.unload('cases/migrations/7.10.0'); - }); - - it('7.10.0 migrates user actions connector', async () => { - const { body } = await supertest - .get(`${CASES_URL}/e1900ac0-017f-11eb-93f8-d161651bf509/user_actions`) - .set('kbn-xsrf', 'true') - .send() - .expect(200); - - const connectorUserAction = body[1]; - const oldValue = JSON.parse(connectorUserAction.old_value); - const newValue = JSON.parse(connectorUserAction.new_value); - - expect(connectorUserAction.action_field.length).eql(1); - expect(connectorUserAction.action_field[0]).eql('connector'); - expect(oldValue).to.eql({ - id: 'c1900ac0-017f-11eb-93f8-d161651bf509', - name: 'none', - type: '.none', - fields: null, - }); - expect(newValue).to.eql({ - id: 'b1900ac0-017f-11eb-93f8-d161651bf509', - name: 'none', - type: '.none', - fields: null, - }); - }); - }); -} diff --git a/x-pack/test/case_api_integration/spaces_only/tests/trial/cases/push_case.ts b/x-pack/test/case_api_integration/spaces_only/tests/trial/cases/push_case.ts index 3c096cb7557c32..28b7fe60955074 100644 --- a/x-pack/test/case_api_integration/spaces_only/tests/trial/cases/push_case.ts +++ b/x-pack/test/case_api_integration/spaces_only/tests/trial/cases/push_case.ts @@ -8,60 +8,27 @@ /* eslint-disable @typescript-eslint/naming-convention */ import expect from '@kbn/expect'; -import * as st from 'supertest'; -import supertestAsPromised from 'supertest-as-promised'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { ObjectRemover as ActionsRemover } from '../../../../../alerting_api_integration/common/lib'; +import { nullUser } from '../../../../common/lib/mock'; import { - postCaseReq, - defaultUser, - postCommentUserReq, - getPostCaseRequest, -} from '../../../../common/lib/mock'; -import { - getConfigurationRequest, - getServiceNowConnector, - createConnector, - createConfiguration, - createCase, pushCase, - createComment, - CreateConnectorResponse, - updateCase, - getAllUserAction, - removeServerGeneratedPropertiesFromUserAction, deleteAllCaseItems, + createCaseWithConnector, + getAuthWithSuperUser, } from '../../../../common/lib/utils'; import { ExternalServiceSimulator, getExternalServiceSimulatorPath, } from '../../../../../alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin'; -import { - CaseConnector, - CasePostRequest, - CaseResponse, - CaseStatuses, - CaseUserActionResponse, - ConnectorTypes, -} from '../../../../../../plugins/cases/common/api'; -import { - globalRead, - noKibanaPrivileges, - obsOnlyRead, - obsSecRead, - secOnly, - secOnlyRead, - superUser, -} from '../../../../common/lib/authentication/users'; -import { User } from '../../../../common/lib/authentication/types'; -import { superUserSpace1Auth } from '../../../../common/lib/authentication'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); const kibanaServer = getService('kibanaServer'); const es = getService('es'); + const authSpace1 = getAuthWithSuperUser(); describe('push_case', () => { const actionsRemover = new ActionsRemover(supertest); @@ -78,80 +45,24 @@ export default ({ getService }: FtrProviderContext): void => { await actionsRemover.removeAll(); }); - const createCaseWithConnector = async ({ - testAgent = supertest, - configureReq = {}, - auth = { user: superUser, space: null }, - createCaseReq = getPostCaseRequest(), - }: { - testAgent?: st.SuperTest; - configureReq?: Record; - auth?: { user: User; space: string | null }; - createCaseReq?: CasePostRequest; - } = {}): Promise<{ - postedCase: CaseResponse; - connector: CreateConnectorResponse; - }> => { - const connector = await createConnector({ - supertest: testAgent, - req: { - ...getServiceNowConnector(), - config: { apiUrl: servicenowSimulatorURL }, - }, - auth, + it('should push a case in space1', async () => { + const { postedCase, connector } = await createCaseWithConnector({ + supertest, + servicenowSimulatorURL, + actionsRemover, + auth: authSpace1, }); - - actionsRemover.add(auth.space ?? 'default', connector.id, 'action', 'actions'); - await createConfiguration( - testAgent, - { - ...getConfigurationRequest({ - id: connector.id, - name: connector.name, - type: connector.connector_type_id as ConnectorTypes, - }), - ...configureReq, - }, - 200, - auth - ); - - const postedCase = await createCase( - testAgent, - { - ...createCaseReq, - connector: { - id: connector.id, - name: connector.name, - type: connector.connector_type_id, - fields: { - urgency: '2', - impact: '2', - severity: '2', - category: 'software', - subcategory: 'os', - }, - } as CaseConnector, - }, - 200, - auth - ); - - return { postedCase, connector }; - }; - - it('should push a case', async () => { - const { postedCase, connector } = await createCaseWithConnector(); const theCase = await pushCase({ supertest, caseId: postedCase.id, connectorId: connector.id, + auth: authSpace1, }); const { pushed_at, external_url, ...rest } = theCase.external_service!; expect(rest).to.eql({ - pushed_by: defaultUser, + pushed_by: nullUser, connector_id: connector.id, connector_name: connector.name, external_id: '123', @@ -166,194 +77,19 @@ export default ({ getService }: FtrProviderContext): void => { ).to.equal(true); }); - it('pushes a comment appropriately', async () => { - const { postedCase, connector } = await createCaseWithConnector(); - await createComment({ supertest, caseId: postedCase.id, params: postCommentUserReq }); - const theCase = await pushCase({ - supertest, - caseId: postedCase.id, - connectorId: connector.id, - }); - - expect(theCase.comments![0].pushed_by).to.eql(defaultUser); - }); - - it('should pushes a case and closes when closure_type: close-by-pushing', async () => { + it('should not push a case in a different space', async () => { const { postedCase, connector } = await createCaseWithConnector({ - configureReq: { - closure_type: 'close-by-pushing', - }, - }); - const theCase = await pushCase({ supertest, - caseId: postedCase.id, - connectorId: connector.id, - }); - - expect(theCase.status).to.eql('closed'); - }); - - it('should create the correct user action', async () => { - const { postedCase, connector } = await createCaseWithConnector(); - const pushedCase = await pushCase({ - supertest, - caseId: postedCase.id, - connectorId: connector.id, - }); - const userActions = await getAllUserAction(supertest, pushedCase.id); - const pushUserAction = removeServerGeneratedPropertiesFromUserAction(userActions[1]); - - const { new_value, ...rest } = pushUserAction as CaseUserActionResponse; - const parsedNewValue = JSON.parse(new_value!); - - expect(rest).to.eql({ - action_field: ['pushed'], - action: 'push-to-service', - action_by: defaultUser, - old_value: null, - case_id: `${postedCase.id}`, - comment_id: null, - sub_case_id: '', - owner: 'securitySolutionFixture', - }); - - expect(parsedNewValue).to.eql({ - pushed_at: pushedCase.external_service!.pushed_at, - pushed_by: defaultUser, - connector_id: connector.id, - connector_name: connector.name, - external_id: '123', - external_title: 'INC01', - external_url: `${servicenowSimulatorURL}/nav_to.do?uri=incident.do?sys_id=123`, - }); - }); - - // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests - it.skip('should push a collection case but not close it when closure_type: close-by-pushing', async () => { - const { postedCase, connector } = await createCaseWithConnector({ - configureReq: { - closure_type: 'close-by-pushing', - }, - }); - - const theCase = await pushCase({ - supertest, - caseId: postedCase.id, - connectorId: connector.id, - }); - expect(theCase.status).to.eql(CaseStatuses.open); - }); - - it('unhappy path - 404s when case does not exist', async () => { - await pushCase({ - supertest, - caseId: 'fake-id', - connectorId: 'fake-connector', - expectedHttpCode: 404, - }); - }); - - it('unhappy path - 404s when connector does not exist', async () => { - const postedCase = await createCase(supertest, { - ...postCaseReq, - connector: getConfigurationRequest().connector, + servicenowSimulatorURL, + actionsRemover, + auth: authSpace1, }); - await pushCase({ - supertest, - caseId: postedCase.id, - connectorId: 'fake-connector', - expectedHttpCode: 404, - }); - }); - - it('unhappy path = 409s when case is closed', async () => { - const { postedCase, connector } = await createCaseWithConnector(); - await updateCase({ - supertest, - params: { - cases: [ - { - id: postedCase.id, - version: postedCase.version, - status: CaseStatuses.closed, - }, - ], - }, - }); - await pushCase({ supertest, caseId: postedCase.id, connectorId: connector.id, - expectedHttpCode: 409, - }); - }); - - describe('rbac', () => { - const supertestWithoutAuth = getService('supertestWithoutAuth'); - - it('should push a case that the user has permissions for', async () => { - const { postedCase, connector } = await createCaseWithConnector({ - testAgent: supertestWithoutAuth, - auth: superUserSpace1Auth, - }); - - await pushCase({ - supertest: supertestWithoutAuth, - caseId: postedCase.id, - connectorId: connector.id, - auth: { user: secOnly, space: 'space1' }, - }); - }); - - it('should not push a case that the user does not have permissions for', async () => { - const { postedCase, connector } = await createCaseWithConnector({ - testAgent: supertestWithoutAuth, - auth: superUserSpace1Auth, - createCaseReq: getPostCaseRequest({ owner: 'observabilityFixture' }), - }); - - await pushCase({ - supertest: supertestWithoutAuth, - caseId: postedCase.id, - connectorId: connector.id, - auth: { user: secOnly, 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 push a case`, async () => { - const { postedCase, connector } = await createCaseWithConnector({ - testAgent: supertestWithoutAuth, - auth: superUserSpace1Auth, - }); - - await pushCase({ - supertest: supertestWithoutAuth, - caseId: postedCase.id, - connectorId: connector.id, - auth: { user, space: 'space1' }, - expectedHttpCode: 403, - }); - }); - } - - it('should not push a case in a space that the user does not have permissions for', async () => { - const { postedCase, connector } = await createCaseWithConnector({ - testAgent: supertestWithoutAuth, - auth: { user: superUser, space: 'space2' }, - }); - - await pushCase({ - supertest: supertestWithoutAuth, - caseId: postedCase.id, - connectorId: connector.id, - auth: { user: secOnly, space: 'space2' }, - expectedHttpCode: 403, - }); + auth: getAuthWithSuperUser('space2'), + expectedHttpCode: 404, }); }); }); diff --git a/x-pack/test/case_api_integration/spaces_only/tests/trial/cases/user_actions/get_all_user_actions.ts b/x-pack/test/case_api_integration/spaces_only/tests/trial/cases/user_actions/get_all_user_actions.ts deleted file mode 100644 index 3729b20f82b301..00000000000000 --- a/x-pack/test/case_api_integration/spaces_only/tests/trial/cases/user_actions/get_all_user_actions.ts +++ /dev/null @@ -1,115 +0,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 expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../../../../common/ftr_provider_context'; - -import { CASE_CONFIGURE_URL, CASES_URL } from '../../../../../../../plugins/cases/common/constants'; -import { defaultUser, postCaseReq } from '../../../../../common/lib/mock'; -import { - deleteCasesByESQuery, - deleteCasesUserActions, - deleteComments, - deleteConfiguration, - getConfigurationRequest, - getServiceNowConnector, -} from '../../../../../common/lib/utils'; - -import { ObjectRemover as ActionsRemover } from '../../../../../../alerting_api_integration/common/lib'; -import { - ExternalServiceSimulator, - getExternalServiceSimulatorPath, -} from '../../../../../../alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin'; - -// eslint-disable-next-line import/no-default-export -export default ({ getService }: FtrProviderContext): void => { - const supertest = getService('supertest'); - const es = getService('es'); - const actionsRemover = new ActionsRemover(supertest); - const kibanaServer = getService('kibanaServer'); - - describe('get_all_user_actions', () => { - let servicenowSimulatorURL: string = ''; - before(() => { - servicenowSimulatorURL = kibanaServer.resolveUrl( - getExternalServiceSimulatorPath(ExternalServiceSimulator.SERVICENOW) - ); - }); - afterEach(async () => { - await deleteCasesByESQuery(es); - await deleteComments(es); - await deleteConfiguration(es); - await deleteCasesUserActions(es); - await actionsRemover.removeAll(); - }); - - it(`on new push to service, user action: 'push-to-service' should be called with actionFields: ['pushed']`, async () => { - const { body: connector } = await supertest - .post('/api/actions/connector') - .set('kbn-xsrf', 'true') - .send({ - ...getServiceNowConnector(), - config: { apiUrl: servicenowSimulatorURL }, - }) - .expect(200); - - actionsRemover.add('default', connector.id, 'action', 'actions'); - - const { body: configure } = await supertest - .post(CASE_CONFIGURE_URL) - .set('kbn-xsrf', 'true') - .send( - getConfigurationRequest({ - id: connector.id, - name: connector.name, - type: connector.connector_type_id, - }) - ) - .expect(200); - - const { body: postedCase } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send({ - ...postCaseReq, - connector: getConfigurationRequest({ - id: connector.id, - name: connector.name, - type: connector.connector_type_id, - fields: { - urgency: '2', - impact: '2', - severity: '2', - category: 'software', - subcategory: 'os', - }, - }).connector, - }) - .expect(200); - - await supertest - .post(`${CASES_URL}/${postedCase.id}/connector/${connector.id}/_push`) - .set('kbn-xsrf', 'true') - .send({}) - .expect(200); - - const { body } = await supertest - .get(`${CASES_URL}/${postedCase.id}/user_actions`) - .set('kbn-xsrf', 'true') - .send() - .expect(200); - - expect(body.length).to.eql(2); - expect(body[1].action_field).to.eql(['pushed']); - expect(body[1].action).to.eql('push-to-service'); - expect(body[1].old_value).to.eql(null); - const newValue = JSON.parse(body[1].new_value); - expect(newValue.connector_id).to.eql(configure.connector.id); - expect(newValue.pushed_by).to.eql(defaultUser); - }); - }); -}; diff --git a/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/get_configure.ts b/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/get_configure.ts index ff8f1cff884aff..97349d6c25962f 100644 --- a/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/get_configure.ts +++ b/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/get_configure.ts @@ -22,14 +22,17 @@ import { getConfigurationRequest, removeServerGeneratedPropertiesFromSavedObject, getConfigurationOutput, + getAuthWithSuperUser, } from '../../../../common/lib/utils'; import { ConnectorTypes } from '../../../../../../plugins/cases/common/api'; +import { nullUser } from '../../../../common/lib/mock'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); const actionsRemover = new ActionsRemover(supertest); const kibanaServer = getService('kibanaServer'); + const authSpace1 = getAuthWithSuperUser(); describe('get_configure', () => { let servicenowSimulatorURL: string = ''; @@ -44,15 +47,16 @@ export default ({ getService }: FtrProviderContext): void => { await actionsRemover.removeAll(); }); - it('should return a configuration with mapping', async () => { + it('should return a configuration with mapping from space1', async () => { const connector = await createConnector({ supertest, req: { ...getServiceNowConnector(), config: { apiUrl: servicenowSimulatorURL }, }, + auth: authSpace1, }); - actionsRemover.add('default', connector.id, 'action', 'actions'); + actionsRemover.add('space1', connector.id, 'action', 'actions'); await createConfiguration( supertest, @@ -60,10 +64,12 @@ export default ({ getService }: FtrProviderContext): void => { id: connector.id, name: connector.name, type: connector.connector_type_id as ConnectorTypes, - }) + }), + 200, + authSpace1 ); - const configuration = await getConfiguration({ supertest }); + const configuration = await getConfiguration({ supertest, auth: authSpace1 }); const data = removeServerGeneratedPropertiesFromSavedObject(configuration[0]); expect(data).to.eql( @@ -91,8 +97,39 @@ export default ({ getService }: FtrProviderContext): void => { type: connector.connector_type_id, fields: null, }, + created_by: nullUser, }) ); }); + + it('should not return a configuration with mapping from a different space', async () => { + const connector = await createConnector({ + supertest, + req: { + ...getServiceNowConnector(), + config: { apiUrl: servicenowSimulatorURL }, + }, + auth: authSpace1, + }); + actionsRemover.add('space1', connector.id, 'action', 'actions'); + + await createConfiguration( + supertest, + getConfigurationRequest({ + id: connector.id, + name: connector.name, + type: connector.connector_type_id as ConnectorTypes, + }), + 200, + authSpace1 + ); + + const configuration = await getConfiguration({ + supertest, + auth: getAuthWithSuperUser('space2'), + }); + + expect(configuration).to.eql([]); + }); }); }; diff --git a/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/get_connectors.ts b/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/get_connectors.ts index fb922f8d102434..66759a4dcb39ad 100644 --- a/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/get_connectors.ts +++ b/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/get_connectors.ts @@ -8,7 +8,6 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; -import { CASE_CONFIGURE_CONNECTORS_URL } from '../../../../../../plugins/cases/common/constants'; import { ObjectRemover as ActionsRemover } from '../../../../../alerting_api_integration/common/lib'; import { getServiceNowConnector, @@ -16,29 +15,30 @@ import { getResilientConnector, createConnector, getServiceNowSIRConnector, + getAuthWithSuperUser, + getCaseConnectors, } from '../../../../common/lib/utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); const actionsRemover = new ActionsRemover(supertest); + const authSpace1 = getAuthWithSuperUser(); describe('get_connectors', () => { afterEach(async () => { await actionsRemover.removeAll(); }); - it('should return the correct connectors', async () => { - const { body: snConnector } = await supertest - .post('/api/actions/connector') - .set('kbn-xsrf', 'true') - .send(getServiceNowConnector()) - .expect(200); - - const { body: emailConnector } = await supertest - .post('/api/actions/connector') - .set('kbn-xsrf', 'true') - .send({ + it('should return the correct connectors in space1', async () => { + const snConnector = await createConnector({ + supertest, + req: getServiceNowConnector(), + auth: authSpace1, + }); + const emailConnector = await createConnector({ + supertest, + req: { name: 'An email action', connector_type_id: '.email', config: { @@ -49,34 +49,32 @@ export default ({ getService }: FtrProviderContext): void => { user: 'bob', password: 'supersecret', }, - }) - .expect(200); - - const { body: jiraConnector } = await supertest - .post('/api/actions/connector') - .set('kbn-xsrf', 'true') - .send(getJiraConnector()) - .expect(200); - - const { body: resilientConnector } = await supertest - .post('/api/actions/connector') - .set('kbn-xsrf', 'true') - .send(getResilientConnector()) - .expect(200); - - const sir = await createConnector({ supertest, req: getServiceNowSIRConnector() }); + }, + auth: authSpace1, + }); + const jiraConnector = await createConnector({ + supertest, + req: getJiraConnector(), + auth: authSpace1, + }); + const resilientConnector = await createConnector({ + supertest, + req: getResilientConnector(), + auth: authSpace1, + }); + const sir = await createConnector({ + supertest, + req: getServiceNowSIRConnector(), + auth: authSpace1, + }); - actionsRemover.add('default', sir.id, 'action', 'actions'); - actionsRemover.add('default', snConnector.id, 'action', 'actions'); - actionsRemover.add('default', emailConnector.id, 'action', 'actions'); - actionsRemover.add('default', jiraConnector.id, 'action', 'actions'); - actionsRemover.add('default', resilientConnector.id, 'action', 'actions'); + actionsRemover.add(authSpace1.space, sir.id, 'action', 'actions'); + actionsRemover.add(authSpace1.space, snConnector.id, 'action', 'actions'); + actionsRemover.add(authSpace1.space, emailConnector.id, 'action', 'actions'); + actionsRemover.add(authSpace1.space, jiraConnector.id, 'action', 'actions'); + actionsRemover.add(authSpace1.space, resilientConnector.id, 'action', 'actions'); - const { body: connectors } = await supertest - .get(`${CASE_CONFIGURE_CONNECTORS_URL}/_find`) - .set('kbn-xsrf', 'true') - .send() - .expect(200); + const connectors = await getCaseConnectors({ supertest, auth: authSpace1 }); expect(connectors).to.eql([ { @@ -125,5 +123,57 @@ export default ({ getService }: FtrProviderContext): void => { }, ]); }); + + it('should not return any connectors when looking in the wrong space', async () => { + const snConnector = await createConnector({ + supertest, + req: getServiceNowConnector(), + auth: authSpace1, + }); + const emailConnector = await createConnector({ + supertest, + req: { + name: 'An email action', + connector_type_id: '.email', + config: { + service: '__json', + from: 'bob@example.com', + }, + secrets: { + user: 'bob', + password: 'supersecret', + }, + }, + auth: authSpace1, + }); + const jiraConnector = await createConnector({ + supertest, + req: getJiraConnector(), + auth: authSpace1, + }); + const resilientConnector = await createConnector({ + supertest, + req: getResilientConnector(), + auth: authSpace1, + }); + const sir = await createConnector({ + supertest, + req: getServiceNowSIRConnector(), + auth: authSpace1, + }); + + actionsRemover.add(authSpace1.space, sir.id, 'action', 'actions'); + actionsRemover.add(authSpace1.space, snConnector.id, 'action', 'actions'); + actionsRemover.add(authSpace1.space, emailConnector.id, 'action', 'actions'); + actionsRemover.add(authSpace1.space, jiraConnector.id, 'action', 'actions'); + actionsRemover.add(authSpace1.space, resilientConnector.id, 'action', 'actions'); + + const connectors = await getCaseConnectors({ + supertest, + auth: getAuthWithSuperUser('space2'), + }); + + expect(connectors).to.eql([]); + }); }); }; diff --git a/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/patch_configure.ts b/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/patch_configure.ts index 789b68b19beb66..5015b9c638617b 100644 --- a/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/patch_configure.ts +++ b/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/patch_configure.ts @@ -22,14 +22,17 @@ import { updateConfiguration, getServiceNowConnector, createConnector, + getAuthWithSuperUser, } from '../../../../common/lib/utils'; import { ConnectorTypes } from '../../../../../../plugins/cases/common/api'; +import { nullUser } from '../../../../common/lib/mock'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); const es = getService('es'); const kibanaServer = getService('kibanaServer'); + const authSpace1 = getAuthWithSuperUser(); describe('patch_configure', () => { const actionsRemover = new ActionsRemover(supertest); @@ -46,19 +49,25 @@ export default ({ getService }: FtrProviderContext): void => { await actionsRemover.removeAll(); }); - it('should patch a configuration connector and create mappings', async () => { + it('should patch a configuration connector and create mappings in space1', async () => { const connector = await createConnector({ supertest, req: { ...getServiceNowConnector(), config: { apiUrl: servicenowSimulatorURL }, }, + auth: authSpace1, }); - actionsRemover.add('default', connector.id, 'action', 'actions'); + actionsRemover.add(authSpace1.space, connector.id, 'action', 'actions'); // Configuration is created with no connector so the mappings are empty - const configuration = await createConfiguration(supertest); + const configuration = await createConfiguration( + supertest, + getConfigurationRequest(), + 200, + authSpace1 + ); // the update request doesn't accept the owner field const { owner, ...reqWithoutOwner } = getConfigurationRequest({ @@ -68,14 +77,22 @@ export default ({ getService }: FtrProviderContext): void => { fields: null, }); - const newConfiguration = await updateConfiguration(supertest, configuration.id, { - ...reqWithoutOwner, - version: configuration.version, - }); + const newConfiguration = await updateConfiguration( + supertest, + configuration.id, + { + ...reqWithoutOwner, + version: configuration.version, + }, + 200, + authSpace1 + ); const data = removeServerGeneratedPropertiesFromSavedObject(newConfiguration); expect(data).to.eql({ ...getConfigurationOutput(true), + created_by: nullUser, + updated_by: nullUser, connector: { id: connector.id, name: connector.name, @@ -102,67 +119,44 @@ export default ({ getService }: FtrProviderContext): void => { }); }); - it('should mappings when updating the connector', async () => { + it('should not patch a configuration connector when it is in a different space', async () => { const connector = await createConnector({ supertest, req: { ...getServiceNowConnector(), config: { apiUrl: servicenowSimulatorURL }, }, + auth: authSpace1, }); - actionsRemover.add('default', connector.id, 'action', 'actions'); + actionsRemover.add(authSpace1.space, connector.id, 'action', 'actions'); - // Configuration is created with connector so the mappings are created + // Configuration is created with no connector so the mappings are empty const configuration = await createConfiguration( supertest, - getConfigurationRequest({ - id: connector.id, - name: connector.name, - type: connector.connector_type_id as ConnectorTypes, - }) + getConfigurationRequest(), + 200, + authSpace1 ); // the update request doesn't accept the owner field - const { owner, ...rest } = getConfigurationRequest({ + const { owner, ...reqWithoutOwner } = getConfigurationRequest({ id: connector.id, - name: 'New name', + name: connector.name, type: connector.connector_type_id as ConnectorTypes, fields: null, }); - const newConfiguration = await updateConfiguration(supertest, configuration.id, { - ...rest, - version: configuration.version, - }); - - const data = removeServerGeneratedPropertiesFromSavedObject(newConfiguration); - expect(data).to.eql({ - ...getConfigurationOutput(true), - connector: { - id: connector.id, - name: 'New name', - type: connector.connector_type_id as ConnectorTypes, - fields: null, + await updateConfiguration( + supertest, + configuration.id, + { + ...reqWithoutOwner, + version: configuration.version, }, - mappings: [ - { - action_type: 'overwrite', - source: 'title', - target: 'short_description', - }, - { - action_type: 'overwrite', - source: 'description', - target: 'description', - }, - { - action_type: 'append', - source: 'comments', - target: 'work_notes', - }, - ], - }); + 404, + getAuthWithSuperUser('space2') + ); }); }); }; diff --git a/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/post_configure.ts b/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/post_configure.ts index 96ffcf4bc3f5c5..897b11fb751513 100644 --- a/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/post_configure.ts +++ b/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/post_configure.ts @@ -22,13 +22,16 @@ import { createConfiguration, createConnector, getServiceNowConnector, + getAuthWithSuperUser, } from '../../../../common/lib/utils'; +import { nullUser } from '../../../../common/lib/mock'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); const es = getService('es'); const kibanaServer = getService('kibanaServer'); + const authSpace1 = getAuthWithSuperUser(); describe('post_configure', () => { const actionsRemover = new ActionsRemover(supertest); @@ -45,16 +48,17 @@ export default ({ getService }: FtrProviderContext): void => { await actionsRemover.removeAll(); }); - it('should create a configuration with mapping', async () => { + it('should create a configuration with mapping in space1', async () => { const connector = await createConnector({ supertest, req: { ...getServiceNowConnector(), config: { apiUrl: servicenowSimulatorURL }, }, + auth: authSpace1, }); - actionsRemover.add('default', connector.id, 'action', 'actions'); + actionsRemover.add(authSpace1.space, connector.id, 'action', 'actions'); const postRes = await createConfiguration( supertest, @@ -62,12 +66,15 @@ export default ({ getService }: FtrProviderContext): void => { id: connector.id, name: connector.name, type: connector.connector_type_id as ConnectorTypes, - }) + }), + 200, + authSpace1 ); const data = removeServerGeneratedPropertiesFromSavedObject(postRes); expect(data).to.eql( getConfigurationOutput(false, { + created_by: nullUser, mappings: [ { action_type: 'overwrite', diff --git a/x-pack/test/case_api_integration/spaces_only/tests/index.ts b/x-pack/test/case_api_integration/spaces_only/tests/trial/index.ts similarity index 61% rename from x-pack/test/case_api_integration/spaces_only/tests/index.ts rename to x-pack/test/case_api_integration/spaces_only/tests/trial/index.ts index d35743ea0c7d97..346640aa6b9de1 100644 --- a/x-pack/test/case_api_integration/spaces_only/tests/index.ts +++ b/x-pack/test/case_api_integration/spaces_only/tests/trial/index.ts @@ -5,12 +5,12 @@ * 2.0. */ -import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { createSpaces, deleteSpaces } from '../../common/lib/authentication'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { createSpaces, deleteSpaces } from '../../../common/lib/authentication'; // eslint-disable-next-line import/no-default-export export default ({ loadTestFile, getService }: FtrProviderContext): void => { - describe('cases spaces only enabled', function () { + describe('cases spaces only enabled: trial', function () { // Fastest ciGroup for the moment. this.tags('ciGroup5'); @@ -21,5 +21,10 @@ export default ({ loadTestFile, getService }: FtrProviderContext): void => { after(async () => { await deleteSpaces(getService); }); + + loadTestFile(require.resolve('../common')); + + loadTestFile(require.resolve('./cases/push_case')); + loadTestFile(require.resolve('./configure')); }); }; From 1a685ca3af16b64b97ab3c5a46ac888bd81bfab7 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Wed, 5 May 2021 16:27:52 -0400 Subject: [PATCH 03/12] Refactoring createCaseWithConnector --- .../tests/trial/cases/push_case.ts | 125 ++++++------------ 1 file changed, 40 insertions(+), 85 deletions(-) diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/push_case.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/push_case.ts index 3901394d2faaec..8a58c59718feb6 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/push_case.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/push_case.ts @@ -8,8 +8,6 @@ /* eslint-disable @typescript-eslint/naming-convention */ import expect from '@kbn/expect'; -import * as st from 'supertest'; -import supertestAsPromised from 'supertest-as-promised'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { ObjectRemover as ActionsRemover } from '../../../../../alerting_api_integration/common/lib'; @@ -21,31 +19,21 @@ import { } from '../../../../common/lib/mock'; import { getConfigurationRequest, - getServiceNowConnector, - createConnector, - createConfiguration, createCase, pushCase, createComment, - CreateConnectorResponse, updateCase, getCaseUserActions, removeServerGeneratedPropertiesFromUserAction, deleteAllCaseItems, superUserSpace1Auth, + createCaseWithConnector, } from '../../../../common/lib/utils'; import { ExternalServiceSimulator, getExternalServiceSimulatorPath, } from '../../../../../alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin'; -import { - CaseConnector, - CasePostRequest, - CaseResponse, - CaseStatuses, - CaseUserActionResponse, - ConnectorTypes, -} from '../../../../../../plugins/cases/common/api'; +import { CaseStatuses, CaseUserActionResponse } from '../../../../../../plugins/cases/common/api'; import { globalRead, noKibanaPrivileges, @@ -55,7 +43,6 @@ import { secOnlyRead, superUser, } from '../../../../common/lib/authentication/users'; -import { User } from '../../../../common/lib/authentication/types'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -78,70 +65,12 @@ export default ({ getService }: FtrProviderContext): void => { await actionsRemover.removeAll(); }); - const createCaseWithConnector = async ({ - testAgent = supertest, - configureReq = {}, - auth = { user: superUser, space: null }, - createCaseReq = getPostCaseRequest(), - }: { - testAgent?: st.SuperTest; - configureReq?: Record; - auth?: { user: User; space: string | null }; - createCaseReq?: CasePostRequest; - } = {}): Promise<{ - postedCase: CaseResponse; - connector: CreateConnectorResponse; - }> => { - const connector = await createConnector({ - supertest: testAgent, - req: { - ...getServiceNowConnector(), - config: { apiUrl: servicenowSimulatorURL }, - }, - auth, - }); - - actionsRemover.add(auth.space ?? 'default', connector.id, 'action', 'actions'); - await createConfiguration( - testAgent, - { - ...getConfigurationRequest({ - id: connector.id, - name: connector.name, - type: connector.connector_type_id as ConnectorTypes, - }), - ...configureReq, - }, - 200, - auth - ); - - const postedCase = await createCase( - testAgent, - { - ...createCaseReq, - connector: { - id: connector.id, - name: connector.name, - type: connector.connector_type_id, - fields: { - urgency: '2', - impact: '2', - severity: '2', - category: 'software', - subcategory: 'os', - }, - } as CaseConnector, - }, - 200, - auth - ); - - return { postedCase, connector }; - }; - it('should push a case', async () => { - const { postedCase, connector } = await createCaseWithConnector(); + const { postedCase, connector } = await createCaseWithConnector({ + supertest, + servicenowSimulatorURL, + actionsRemover, + }); const theCase = await pushCase({ supertest, caseId: postedCase.id, @@ -167,7 +96,11 @@ export default ({ getService }: FtrProviderContext): void => { }); it('pushes a comment appropriately', async () => { - const { postedCase, connector } = await createCaseWithConnector(); + const { postedCase, connector } = await createCaseWithConnector({ + supertest, + servicenowSimulatorURL, + actionsRemover, + }); await createComment({ supertest, caseId: postedCase.id, params: postCommentUserReq }); const theCase = await pushCase({ supertest, @@ -183,6 +116,9 @@ export default ({ getService }: FtrProviderContext): void => { configureReq: { closure_type: 'close-by-pushing', }, + supertest, + servicenowSimulatorURL, + actionsRemover, }); const theCase = await pushCase({ supertest, @@ -194,7 +130,11 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should create the correct user action', async () => { - const { postedCase, connector } = await createCaseWithConnector(); + const { postedCase, connector } = await createCaseWithConnector({ + supertest, + servicenowSimulatorURL, + actionsRemover, + }); const pushedCase = await pushCase({ supertest, caseId: postedCase.id, @@ -231,6 +171,9 @@ export default ({ getService }: FtrProviderContext): void => { // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests it.skip('should push a collection case but not close it when closure_type: close-by-pushing', async () => { const { postedCase, connector } = await createCaseWithConnector({ + supertest, + servicenowSimulatorURL, + actionsRemover, configureReq: { closure_type: 'close-by-pushing', }, @@ -267,7 +210,11 @@ export default ({ getService }: FtrProviderContext): void => { }); it('unhappy path = 409s when case is closed', async () => { - const { postedCase, connector } = await createCaseWithConnector(); + const { postedCase, connector } = await createCaseWithConnector({ + supertest, + servicenowSimulatorURL, + actionsRemover, + }); await updateCase({ supertest, params: { @@ -294,7 +241,9 @@ export default ({ getService }: FtrProviderContext): void => { it('should push a case that the user has permissions for', async () => { const { postedCase, connector } = await createCaseWithConnector({ - testAgent: supertestWithoutAuth, + supertest, + servicenowSimulatorURL, + actionsRemover, auth: superUserSpace1Auth, }); @@ -308,7 +257,9 @@ export default ({ getService }: FtrProviderContext): void => { it('should not push a case that the user does not have permissions for', async () => { const { postedCase, connector } = await createCaseWithConnector({ - testAgent: supertestWithoutAuth, + supertest, + servicenowSimulatorURL, + actionsRemover, auth: superUserSpace1Auth, createCaseReq: getPostCaseRequest({ owner: 'observabilityFixture' }), }); @@ -327,7 +278,9 @@ export default ({ getService }: FtrProviderContext): void => { user.username } with role(s) ${user.roles.join()} - should NOT push a case`, async () => { const { postedCase, connector } = await createCaseWithConnector({ - testAgent: supertestWithoutAuth, + supertest, + servicenowSimulatorURL, + actionsRemover, auth: superUserSpace1Auth, }); @@ -343,7 +296,9 @@ export default ({ getService }: FtrProviderContext): void => { it('should not push a case in a space that the user does not have permissions for', async () => { const { postedCase, connector } = await createCaseWithConnector({ - testAgent: supertestWithoutAuth, + supertest, + servicenowSimulatorURL, + actionsRemover, auth: { user: superUser, space: 'space2' }, }); From 5609c46661e192056516a779b2d4d9edcd504a61 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Wed, 5 May 2021 16:36:08 -0400 Subject: [PATCH 04/12] Fixing spelling --- .../spaces_only/tests/common/configure/get_configure.ts | 2 +- .../spaces_only/tests/trial/configure/get_configure.ts | 4 ++-- .../spaces_only/tests/trial/configure/post_configure.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/x-pack/test/case_api_integration/spaces_only/tests/common/configure/get_configure.ts b/x-pack/test/case_api_integration/spaces_only/tests/common/configure/get_configure.ts index 5226df3cde65ba..573b96d71af4ad 100644 --- a/x-pack/test/case_api_integration/spaces_only/tests/common/configure/get_configure.ts +++ b/x-pack/test/case_api_integration/spaces_only/tests/common/configure/get_configure.ts @@ -38,7 +38,7 @@ export default ({ getService }: FtrProviderContext): void => { expect(data).to.eql(getConfigurationOutput(false, { created_by: nullUser })); }); - it('should not find a configuration in when looking in a different space', async () => { + it('should not find a configuration when looking in a different space', async () => { await createConfiguration(supertest, getConfigurationRequest(), 200, authSpace1); const configuration = await getConfiguration({ supertest, diff --git a/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/get_configure.ts b/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/get_configure.ts index 97349d6c25962f..a142e6470ae933 100644 --- a/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/get_configure.ts +++ b/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/get_configure.ts @@ -47,7 +47,7 @@ export default ({ getService }: FtrProviderContext): void => { await actionsRemover.removeAll(); }); - it('should return a configuration with mapping from space1', async () => { + it('should return a configuration with a mapping from space1', async () => { const connector = await createConnector({ supertest, req: { @@ -102,7 +102,7 @@ export default ({ getService }: FtrProviderContext): void => { ); }); - it('should not return a configuration with mapping from a different space', async () => { + it('should not return a configuration with a mapping from a different space', async () => { const connector = await createConnector({ supertest, req: { diff --git a/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/post_configure.ts b/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/post_configure.ts index 897b11fb751513..d67ca29229dd10 100644 --- a/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/post_configure.ts +++ b/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/post_configure.ts @@ -48,7 +48,7 @@ export default ({ getService }: FtrProviderContext): void => { await actionsRemover.removeAll(); }); - it('should create a configuration with mapping in space1', async () => { + it('should create a configuration with a mapping in space1', async () => { const connector = await createConnector({ supertest, req: { From 7184b0ced404fca82e22e0842d9815a2978ced7a Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Thu, 6 May 2021 15:49:28 -0400 Subject: [PATCH 05/12] Addressing PR feedback and creating alert tests --- .../tests/common/alerts/get_cases.ts | 110 ++++++++++++++++++ .../common/cases/reporters/get_reporters.ts | 12 +- .../tests/common/cases/status/get_status.ts | 11 +- .../tests/common/cases/tags/get_tags.ts | 15 ++- .../spaces_only/tests/common/index.ts | 1 + 5 files changed, 136 insertions(+), 13 deletions(-) create mode 100644 x-pack/test/case_api_integration/spaces_only/tests/common/alerts/get_cases.ts diff --git a/x-pack/test/case_api_integration/spaces_only/tests/common/alerts/get_cases.ts b/x-pack/test/case_api_integration/spaces_only/tests/common/alerts/get_cases.ts new file mode 100644 index 00000000000000..9587502fb642ce --- /dev/null +++ b/x-pack/test/case_api_integration/spaces_only/tests/common/alerts/get_cases.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. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +import { getPostCaseRequest, postCommentAlertReq } from '../../../../common/lib/mock'; +import { + createCase, + createComment, + getCaseIDsByAlert, + deleteAllCaseItems, + getAuthWithSuperUser, +} from '../../../../common/lib/utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + const authSpace1 = getAuthWithSuperUser(); + const authSpace2 = getAuthWithSuperUser('space2'); + + describe('get_cases using alertID', () => { + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + it('should return all cases with the same alert ID attached to them in space1', async () => { + const [case1, case2, case3] = await Promise.all([ + createCase(supertest, getPostCaseRequest(), 200, authSpace1), + createCase(supertest, getPostCaseRequest(), 200, authSpace1), + createCase(supertest, getPostCaseRequest(), 200, authSpace1), + ]); + + await Promise.all([ + createComment({ + supertest, + caseId: case1.id, + params: postCommentAlertReq, + auth: authSpace1, + }), + createComment({ + supertest, + caseId: case2.id, + params: postCommentAlertReq, + auth: authSpace1, + }), + createComment({ + supertest, + caseId: case3.id, + params: postCommentAlertReq, + auth: authSpace1, + }), + ]); + + const caseIDsWithAlert = await getCaseIDsByAlert({ + supertest, + alertID: 'test-id', + auth: authSpace1, + }); + + expect(caseIDsWithAlert.length).to.eql(3); + expect(caseIDsWithAlert).to.contain(case1.id); + expect(caseIDsWithAlert).to.contain(case2.id); + expect(caseIDsWithAlert).to.contain(case3.id); + }); + + it('should return 1 case in space2 when 2 cases were created in space1 and 1 in space2', async () => { + const [case1, case2, case3] = await Promise.all([ + createCase(supertest, getPostCaseRequest(), 200, authSpace1), + createCase(supertest, getPostCaseRequest(), 200, authSpace1), + createCase(supertest, getPostCaseRequest(), 200, authSpace2), + ]); + + await Promise.all([ + createComment({ + supertest, + caseId: case1.id, + params: postCommentAlertReq, + auth: authSpace1, + }), + createComment({ + supertest, + caseId: case2.id, + params: postCommentAlertReq, + auth: authSpace1, + }), + createComment({ + supertest, + caseId: case3.id, + params: postCommentAlertReq, + auth: authSpace2, + }), + ]); + + const caseIDsWithAlert = await getCaseIDsByAlert({ + supertest, + alertID: 'test-id', + auth: authSpace2, + }); + + expect(caseIDsWithAlert.length).to.eql(1); + expect(caseIDsWithAlert).to.eql([case3.id]); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/spaces_only/tests/common/cases/reporters/get_reporters.ts b/x-pack/test/case_api_integration/spaces_only/tests/common/cases/reporters/get_reporters.ts index 74101b4402bbc6..d3c3176f4649fa 100644 --- a/x-pack/test/case_api_integration/spaces_only/tests/common/cases/reporters/get_reporters.ts +++ b/x-pack/test/case_api_integration/spaces_only/tests/common/cases/reporters/get_reporters.ts @@ -21,6 +21,7 @@ export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); const es = getService('es'); const authSpace1 = getAuthWithSuperUser(); + const authSpace2 = getAuthWithSuperUser('space2'); describe('get_reporters', () => { afterEach(async () => { @@ -29,13 +30,18 @@ export default ({ getService }: FtrProviderContext): void => { it('should not return reporters when security is disabled', async () => { await Promise.all([ - createCase(supertest, getPostCaseRequest(), 200, getAuthWithSuperUser('space2')), + createCase(supertest, getPostCaseRequest(), 200, authSpace2), createCase(supertest, getPostCaseRequest(), 200, authSpace1), ]); - const reporters = await getReporters({ supertest, auth: authSpace1 }); + const reportersSpace1 = await getReporters({ supertest, auth: authSpace1 }); + const reportersSpace2 = await getReporters({ + supertest, + auth: authSpace2, + }); - expect(reporters).to.eql([]); + expect(reportersSpace1).to.eql([]); + expect(reportersSpace2).to.eql([]); }); }); }; diff --git a/x-pack/test/case_api_integration/spaces_only/tests/common/cases/status/get_status.ts b/x-pack/test/case_api_integration/spaces_only/tests/common/cases/status/get_status.ts index ee00d0568867b6..7f2a774c28f397 100644 --- a/x-pack/test/case_api_integration/spaces_only/tests/common/cases/status/get_status.ts +++ b/x-pack/test/case_api_integration/spaces_only/tests/common/cases/status/get_status.ts @@ -73,13 +73,20 @@ export default ({ getService }: FtrProviderContext): void => { auth: authSpace2, }); - const statuses = await getAllCasesStatuses({ supertest, auth: authSpace1 }); + const statusesSpace1 = await getAllCasesStatuses({ supertest, auth: authSpace1 }); + const statusesSpace2 = await getAllCasesStatuses({ supertest, auth: authSpace2 }); - expect(statuses).to.eql({ + expect(statusesSpace1).to.eql({ count_open_cases: 1, count_closed_cases: 0, count_in_progress_cases: 1, }); + + expect(statusesSpace2).to.eql({ + count_open_cases: 0, + count_closed_cases: 1, + count_in_progress_cases: 0, + }); }); }); }; diff --git a/x-pack/test/case_api_integration/spaces_only/tests/common/cases/tags/get_tags.ts b/x-pack/test/case_api_integration/spaces_only/tests/common/cases/tags/get_tags.ts index 1f785567d3f761..630628a13b6b9b 100644 --- a/x-pack/test/case_api_integration/spaces_only/tests/common/cases/tags/get_tags.ts +++ b/x-pack/test/case_api_integration/spaces_only/tests/common/cases/tags/get_tags.ts @@ -21,6 +21,7 @@ export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); const es = getService('es'); const authSpace1 = getAuthWithSuperUser(); + const authSpace2 = getAuthWithSuperUser('space2'); describe('get_tags', () => { afterEach(async () => { @@ -29,15 +30,13 @@ export default ({ getService }: FtrProviderContext): void => { it('should return case tags in space1', async () => { await createCase(supertest, getPostCaseRequest(), 200, authSpace1); - await createCase( - supertest, - getPostCaseRequest({ tags: ['unique'] }), - 200, - getAuthWithSuperUser('space2') - ); + await createCase(supertest, getPostCaseRequest({ tags: ['unique'] }), 200, authSpace2); - const tags = await getTags({ supertest, auth: authSpace1 }); - expect(tags).to.eql(['defacement']); + const tagsSpace1 = await getTags({ supertest, auth: authSpace1 }); + const tagsSpace2 = await getTags({ supertest, auth: authSpace2 }); + + expect(tagsSpace1).to.eql(['defacement']); + expect(tagsSpace2).to.eql(['unique']); }); }); }; diff --git a/x-pack/test/case_api_integration/spaces_only/tests/common/index.ts b/x-pack/test/case_api_integration/spaces_only/tests/common/index.ts index 990f045895dcfb..251a545f106818 100644 --- a/x-pack/test/case_api_integration/spaces_only/tests/common/index.ts +++ b/x-pack/test/case_api_integration/spaces_only/tests/common/index.ts @@ -10,6 +10,7 @@ import { FtrProviderContext } from '../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default ({ loadTestFile }: FtrProviderContext): void => { describe('Common', function () { + loadTestFile(require.resolve('./alerts/get_cases')); loadTestFile(require.resolve('./comments/delete_comment')); loadTestFile(require.resolve('./comments/find_comments')); loadTestFile(require.resolve('./comments/get_comment')); From 3b3da5c38509d5c729958530ca37104247c2a5c2 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Thu, 6 May 2021 16:06:03 -0400 Subject: [PATCH 06/12] Fixing mocks --- x-pack/plugins/cases/server/client/mocks.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugins/cases/server/client/mocks.ts b/x-pack/plugins/cases/server/client/mocks.ts index 7db3d62c491e7a..10b298995f87ac 100644 --- a/x-pack/plugins/cases/server/client/mocks.ts +++ b/x-pack/plugins/cases/server/client/mocks.ts @@ -28,6 +28,7 @@ const createCasesSubClientMock = (): CasesSubClientMock => { delete: jest.fn(), getTags: jest.fn(), getReporters: jest.fn(), + getCaseIDsByAlertID: jest.fn(), }; }; From e454d814c281933efee804cea99abb62d1b75cf8 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Thu, 6 May 2021 17:13:37 -0400 Subject: [PATCH 07/12] Starting security only tests --- x-pack/scripts/functional_tests.js | 1 + .../common/lib/authentication/index.ts | 19 +- .../security_only/config.ts | 16 + .../tests/common/alerts/get_cases.ts | 246 ++++ .../tests/common/cases/delete_cases.ts | 301 ++++ .../tests/common/cases/find_cases.ts | 815 +++++++++++ .../tests/common/cases/get_case.ts | 207 +++ .../tests/common/cases/patch_cases.ts | 1240 +++++++++++++++++ .../tests/common/cases/post_case.ts | 292 ++++ .../common/cases/reporters/get_reporters.ts | 201 +++ .../tests/common/cases/status/get_status.ts | 172 +++ .../tests/common/cases/tags/get_tags.ts | 201 +++ .../tests/common/comments/delete_comment.ts | 372 +++++ .../tests/common/comments/find_comments.ts | 393 ++++++ .../tests/common/comments/get_all_comments.ts | 231 +++ .../tests/common/comments/get_comment.ts | 169 +++ .../tests/common/comments/migrations.ts | 37 + .../tests/common/comments/patch_comment.ts | 641 +++++++++ .../tests/common/comments/post_comment.ts | 605 ++++++++ .../tests/common/configure/get_configure.ts | 215 +++ .../tests/common/configure/get_connectors.ts | 27 + .../tests/common/configure/patch_configure.ts | 243 ++++ .../tests/common/configure/post_configure.ts | 298 ++++ .../tests/common/connectors/case.ts | 1078 ++++++++++++++ .../security_only/tests/common/index.ts | 39 + .../common/sub_cases/delete_sub_cases.ts | 112 ++ .../tests/common/sub_cases/find_sub_cases.ts | 480 +++++++ .../tests/common/sub_cases/get_sub_case.ts | 119 ++ .../tests/common/sub_cases/patch_sub_cases.ts | 515 +++++++ .../user_actions/get_all_user_actions.ts | 395 ++++++ .../tests/trial/cases/push_case.ts | 315 +++++ .../user_actions/get_all_user_actions.ts | 115 ++ .../tests/trial/configure/get_configure.ts | 98 ++ .../tests/trial/configure/get_connectors.ts | 129 ++ .../tests/trial/configure/index.ts | 18 + .../tests/trial/configure/patch_configure.ts | 168 +++ .../tests/trial/configure/post_configure.ts | 98 ++ .../security_only/tests/trial/index.ts | 34 + 38 files changed, 10652 insertions(+), 3 deletions(-) create mode 100644 x-pack/test/case_api_integration/security_only/config.ts create mode 100644 x-pack/test/case_api_integration/security_only/tests/common/alerts/get_cases.ts create mode 100644 x-pack/test/case_api_integration/security_only/tests/common/cases/delete_cases.ts create mode 100644 x-pack/test/case_api_integration/security_only/tests/common/cases/find_cases.ts create mode 100644 x-pack/test/case_api_integration/security_only/tests/common/cases/get_case.ts create mode 100644 x-pack/test/case_api_integration/security_only/tests/common/cases/patch_cases.ts create mode 100644 x-pack/test/case_api_integration/security_only/tests/common/cases/post_case.ts create mode 100644 x-pack/test/case_api_integration/security_only/tests/common/cases/reporters/get_reporters.ts create mode 100644 x-pack/test/case_api_integration/security_only/tests/common/cases/status/get_status.ts create mode 100644 x-pack/test/case_api_integration/security_only/tests/common/cases/tags/get_tags.ts create mode 100644 x-pack/test/case_api_integration/security_only/tests/common/comments/delete_comment.ts create mode 100644 x-pack/test/case_api_integration/security_only/tests/common/comments/find_comments.ts create mode 100644 x-pack/test/case_api_integration/security_only/tests/common/comments/get_all_comments.ts create mode 100644 x-pack/test/case_api_integration/security_only/tests/common/comments/get_comment.ts create mode 100644 x-pack/test/case_api_integration/security_only/tests/common/comments/migrations.ts create mode 100644 x-pack/test/case_api_integration/security_only/tests/common/comments/patch_comment.ts create mode 100644 x-pack/test/case_api_integration/security_only/tests/common/comments/post_comment.ts create mode 100644 x-pack/test/case_api_integration/security_only/tests/common/configure/get_configure.ts create mode 100644 x-pack/test/case_api_integration/security_only/tests/common/configure/get_connectors.ts create mode 100644 x-pack/test/case_api_integration/security_only/tests/common/configure/patch_configure.ts create mode 100644 x-pack/test/case_api_integration/security_only/tests/common/configure/post_configure.ts create mode 100644 x-pack/test/case_api_integration/security_only/tests/common/connectors/case.ts create mode 100644 x-pack/test/case_api_integration/security_only/tests/common/index.ts create mode 100644 x-pack/test/case_api_integration/security_only/tests/common/sub_cases/delete_sub_cases.ts create mode 100644 x-pack/test/case_api_integration/security_only/tests/common/sub_cases/find_sub_cases.ts create mode 100644 x-pack/test/case_api_integration/security_only/tests/common/sub_cases/get_sub_case.ts create mode 100644 x-pack/test/case_api_integration/security_only/tests/common/sub_cases/patch_sub_cases.ts create mode 100644 x-pack/test/case_api_integration/security_only/tests/common/user_actions/get_all_user_actions.ts create mode 100644 x-pack/test/case_api_integration/security_only/tests/trial/cases/push_case.ts create mode 100644 x-pack/test/case_api_integration/security_only/tests/trial/cases/user_actions/get_all_user_actions.ts create mode 100644 x-pack/test/case_api_integration/security_only/tests/trial/configure/get_configure.ts create mode 100644 x-pack/test/case_api_integration/security_only/tests/trial/configure/get_connectors.ts create mode 100644 x-pack/test/case_api_integration/security_only/tests/trial/configure/index.ts create mode 100644 x-pack/test/case_api_integration/security_only/tests/trial/configure/patch_configure.ts create mode 100644 x-pack/test/case_api_integration/security_only/tests/trial/configure/post_configure.ts create mode 100644 x-pack/test/case_api_integration/security_only/tests/trial/index.ts diff --git a/x-pack/scripts/functional_tests.js b/x-pack/scripts/functional_tests.js index e2b3c951b07222..1a7f9acc9f1a32 100644 --- a/x-pack/scripts/functional_tests.js +++ b/x-pack/scripts/functional_tests.js @@ -37,6 +37,7 @@ const onlyNotInCoverageTests = [ require.resolve('../test/case_api_integration/security_and_spaces/config_basic.ts'), require.resolve('../test/case_api_integration/security_and_spaces/config_trial.ts'), require.resolve('../test/case_api_integration/spaces_only/config.ts'), + require.resolve('../test/case_api_integration/security_only/config.ts'), require.resolve('../test/apm_api_integration/basic/config.ts'), require.resolve('../test/apm_api_integration/trial/config.ts'), require.resolve('../test/apm_api_integration/rules/config.ts'), diff --git a/x-pack/test/case_api_integration/common/lib/authentication/index.ts b/x-pack/test/case_api_integration/common/lib/authentication/index.ts index a72141745e5777..0a23982becd3be 100644 --- a/x-pack/test/case_api_integration/common/lib/authentication/index.ts +++ b/x-pack/test/case_api_integration/common/lib/authentication/index.ts @@ -24,11 +24,23 @@ export const createSpaces = async (getService: CommonFtrProviderContext['getServ } }; -const createUsersAndRoles = async (getService: CommonFtrProviderContext['getService']) => { +export const createUsersAndRoles = async ( + getService: CommonFtrProviderContext['getService'], + overrideSpaces?: string[] +) => { const security = getService('security'); const createRole = async ({ name, privileges }: Role) => { - return await security.role.create(name, privileges); + const modifiedPrivileges = { + ...privileges, + // for roles that don't have kibana set this will just return undefined + kibana: privileges.kibana?.map((kibanaEntry) => ({ + ...kibanaEntry, + spaces: overrideSpaces != null ? overrideSpaces : kibanaEntry.spaces, + })), + }; + + return await security.role.create(name, modifiedPrivileges); }; const createUser = async (user: User) => { @@ -61,7 +73,8 @@ export const deleteSpaces = async (getService: CommonFtrProviderContext['getServ } } }; -const deleteUsersAndRoles = async (getService: CommonFtrProviderContext['getService']) => { + +export const deleteUsersAndRoles = async (getService: CommonFtrProviderContext['getService']) => { const security = getService('security'); for (const user of users) { diff --git a/x-pack/test/case_api_integration/security_only/config.ts b/x-pack/test/case_api_integration/security_only/config.ts new file mode 100644 index 00000000000000..5946b8d25b4641 --- /dev/null +++ b/x-pack/test/case_api_integration/security_only/config.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 { createTestConfig } from '../common/config'; + +// eslint-disable-next-line import/no-default-export +export default createTestConfig('security_only', { + disabledPlugins: ['spaces'], + license: 'trial', + ssl: true, + testFiles: [require.resolve('./tests/trial')], +}); diff --git a/x-pack/test/case_api_integration/security_only/tests/common/alerts/get_cases.ts b/x-pack/test/case_api_integration/security_only/tests/common/alerts/get_cases.ts new file mode 100644 index 00000000000000..d8b3bd4ae7c126 --- /dev/null +++ b/x-pack/test/case_api_integration/security_only/tests/common/alerts/get_cases.ts @@ -0,0 +1,246 @@ +/* + * 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 { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +import { getPostCaseRequest, postCommentAlertReq } from '../../../../common/lib/mock'; +import { + createCase, + createComment, + getCaseIDsByAlert, + deleteAllCaseItems, +} from '../../../../common/lib/utils'; +import { + globalRead, + noKibanaPrivileges, + obsOnly, + obsOnlyRead, + obsSec, + obsSecRead, + secOnly, + secOnlyRead, + 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'); + + describe('get_cases using alertID', () => { + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + it('should return the correct case IDs', async () => { + const secOnlyAuth = { user: secOnly, space: null }; + const obsOnlyAuth = { user: obsOnly, space: null }; + + const [case1, case2, case3] = await Promise.all([ + createCase(supertestWithoutAuth, getPostCaseRequest(), 200, secOnlyAuth), + createCase(supertestWithoutAuth, getPostCaseRequest(), 200, secOnlyAuth), + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + obsOnlyAuth + ), + ]); + + await Promise.all([ + createComment({ + supertest: supertestWithoutAuth, + caseId: case1.id, + params: postCommentAlertReq, + auth: secOnlyAuth, + }), + createComment({ + supertest: supertestWithoutAuth, + caseId: case2.id, + params: postCommentAlertReq, + auth: secOnlyAuth, + }), + createComment({ + supertest: supertestWithoutAuth, + caseId: case3.id, + params: { ...postCommentAlertReq, owner: 'observabilityFixture' }, + auth: obsOnlyAuth, + }), + ]); + + for (const scenario of [ + { + user: globalRead, + caseIDs: [case1.id, case2.id, case3.id], + }, + { + user: superUser, + caseIDs: [case1.id, case2.id, case3.id], + }, + { user: secOnlyRead, caseIDs: [case1.id, case2.id] }, + { user: obsOnlyRead, caseIDs: [case3.id] }, + { + user: obsSecRead, + caseIDs: [case1.id, case2.id, case3.id], + }, + ]) { + const res = await getCaseIDsByAlert({ + supertest: supertestWithoutAuth, + // cast because the official type is string | string[] but the ids will always be a single value in the tests + alertID: postCommentAlertReq.alertId as string, + auth: { + user: scenario.user, + space: null, + }, + }); + expect(res.length).to.eql(scenario.caseIDs.length); + for (const caseID of scenario.caseIDs) { + expect(res).to.contain(caseID); + } + } + }); + + for (const scenario of [{ user: noKibanaPrivileges, space: null }]) { + it(`User ${scenario.user.username} with role(s) ${scenario.user.roles.join()} and space ${ + scenario.space + } - should not get cases`, async () => { + const caseInfo = await createCase(supertest, getPostCaseRequest(), 200, { + user: superUser, + space: scenario.space, + }); + + await createComment({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + params: postCommentAlertReq, + auth: { user: superUser, space: scenario.space }, + }); + + await getCaseIDsByAlert({ + supertest: supertestWithoutAuth, + alertID: postCommentAlertReq.alertId as string, + auth: { user: scenario.user, space: scenario.space }, + expectedHttpCode: 403, + }); + }); + } + + it('should return a 404 when attempting to access a space', async () => { + const auth = { user: obsSec, space: null }; + const [case1, case2] = await Promise.all([ + createCase(supertestWithoutAuth, getPostCaseRequest(), 200, auth), + createCase( + supertestWithoutAuth, + { ...getPostCaseRequest(), owner: 'observabilityFixture' }, + 200, + auth + ), + ]); + + await Promise.all([ + createComment({ + supertest: supertestWithoutAuth, + caseId: case1.id, + params: postCommentAlertReq, + auth, + }), + createComment({ + supertest: supertestWithoutAuth, + caseId: case2.id, + params: { ...postCommentAlertReq, owner: 'observabilityFixture' }, + auth, + }), + ]); + + await getCaseIDsByAlert({ + supertest: supertestWithoutAuth, + alertID: postCommentAlertReq.alertId as string, + auth: { user: obsSec, space: 'space1' }, + query: { owner: 'securitySolutionFixture' }, + expectedHttpCode: 404, + }); + }); + + it('should respect the owner filter when have permissions', async () => { + const auth = { user: obsSec, space: null }; + const [case1, case2] = await Promise.all([ + createCase(supertestWithoutAuth, getPostCaseRequest(), 200, auth), + createCase( + supertestWithoutAuth, + { ...getPostCaseRequest(), owner: 'observabilityFixture' }, + 200, + auth + ), + ]); + + await Promise.all([ + createComment({ + supertest: supertestWithoutAuth, + caseId: case1.id, + params: postCommentAlertReq, + auth, + }), + createComment({ + supertest: supertestWithoutAuth, + caseId: case2.id, + params: { ...postCommentAlertReq, owner: 'observabilityFixture' }, + auth, + }), + ]); + + const res = await getCaseIDsByAlert({ + supertest: supertestWithoutAuth, + alertID: postCommentAlertReq.alertId as string, + auth, + query: { owner: 'securitySolutionFixture' }, + }); + + expect(res).to.eql([case1.id]); + }); + + it('should return the correct case IDs when the owner query parameter contains unprivileged values', async () => { + const auth = { user: obsSec, space: null }; + const [case1, case2] = await Promise.all([ + createCase(supertestWithoutAuth, getPostCaseRequest(), 200, auth), + createCase( + supertestWithoutAuth, + { ...getPostCaseRequest(), owner: 'observabilityFixture' }, + 200, + auth + ), + ]); + + await Promise.all([ + createComment({ + supertest: supertestWithoutAuth, + caseId: case1.id, + params: postCommentAlertReq, + auth, + }), + createComment({ + supertest: supertestWithoutAuth, + caseId: case2.id, + params: { ...postCommentAlertReq, owner: 'observabilityFixture' }, + auth, + }), + ]); + + const res = await getCaseIDsByAlert({ + supertest: supertestWithoutAuth, + alertID: postCommentAlertReq.alertId as string, + auth: { user: secOnly, space: null }, + // The secOnly user does not have permissions for observability cases, so it should only return the security solution one + query: { owner: ['securitySolutionFixture', 'observabilityFixture'] }, + }); + + expect(res).to.eql([case1.id]); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/security_only/tests/common/cases/delete_cases.ts b/x-pack/test/case_api_integration/security_only/tests/common/cases/delete_cases.ts new file mode 100644 index 00000000000000..03bcf0d538fe3b --- /dev/null +++ b/x-pack/test/case_api_integration/security_only/tests/common/cases/delete_cases.ts @@ -0,0 +1,301 @@ +/* + * 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 { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; +import { defaultUser, getPostCaseRequest, postCommentUserReq } from '../../../../common/lib/mock'; +import { + createCaseAction, + createSubCase, + deleteAllCaseItems, + deleteCaseAction, + deleteCasesByESQuery, + deleteCasesUserActions, + deleteComments, + createCase, + deleteCases, + createComment, + getComment, + removeServerGeneratedPropertiesFromUserAction, + getCase, + superUserSpace1Auth, + getCaseUserActions, +} from '../../../../common/lib/utils'; +import { getSubCaseDetailsUrl } from '../../../../../../plugins/cases/common/api/helpers'; +import { CaseResponse } from '../../../../../../plugins/cases/common/api'; +import { + secOnly, + secOnlyRead, + globalRead, + obsOnlyRead, + obsSecRead, + noKibanaPrivileges, + obsOnly, + superUser, +} from '../../../../common/lib/authentication/users'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const supertest = getService('supertest'); + const es = getService('es'); + + describe('delete_cases', () => { + afterEach(async () => { + await deleteCasesByESQuery(es); + await deleteComments(es); + await deleteCasesUserActions(es); + }); + + it('should delete a case', async () => { + const postedCase = await createCase(supertest, getPostCaseRequest()); + const body = await deleteCases({ supertest, caseIDs: [postedCase.id] }); + + expect(body).to.eql({}); + }); + + it(`should delete a case's comments when that case gets deleted`, async () => { + const postedCase = await createCase(supertest, getPostCaseRequest()); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + }); + // ensure that we can get the comment before deleting the case + await getComment({ + supertest, + caseId: postedCase.id, + commentId: patchedCase.comments![0].id, + }); + + await deleteCases({ supertest, caseIDs: [postedCase.id] }); + + // make sure the comment is now gone + await getComment({ + supertest, + caseId: postedCase.id, + commentId: patchedCase.comments![0].id, + expectedHttpCode: 404, + }); + }); + + it('should create a user action when creating a case', async () => { + const postedCase = await createCase(supertest, getPostCaseRequest()); + await deleteCases({ supertest, caseIDs: [postedCase.id] }); + const userActions = await getCaseUserActions({ supertest, caseID: postedCase.id }); + const creationUserAction = removeServerGeneratedPropertiesFromUserAction(userActions[1]); + + expect(creationUserAction).to.eql({ + action_field: [ + 'description', + 'status', + 'tags', + 'title', + 'connector', + 'settings', + 'owner', + 'comment', + ], + action: 'delete', + action_by: defaultUser, + old_value: null, + new_value: null, + case_id: `${postedCase.id}`, + comment_id: null, + sub_case_id: '', + owner: 'securitySolutionFixture', + }); + }); + + it('unhappy path - 404s when case is not there', async () => { + await deleteCases({ supertest, caseIDs: ['fake-id'], expectedHttpCode: 404 }); + }); + + // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests + describe.skip('sub cases', () => { + let actionID: string; + before(async () => { + actionID = await createCaseAction(supertest); + }); + after(async () => { + await deleteCaseAction(supertest, actionID); + }); + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + it('should delete the sub cases when deleting a collection', async () => { + const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); + expect(caseInfo.subCases![0].id).to.not.eql(undefined); + + const body = await deleteCases({ supertest, caseIDs: [caseInfo.id] }); + + expect(body).to.eql({}); + await supertest + .get(getSubCaseDetailsUrl(caseInfo.id, caseInfo.subCases![0].id)) + .send() + .expect(404); + }); + + it(`should delete a sub case's comments when that case gets deleted`, async () => { + const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); + expect(caseInfo.subCases![0].id).to.not.eql(undefined); + + // there should be two comments on the sub case now + const { body: patchedCaseWithSubCase }: { body: CaseResponse } = await supertest + .post(`${CASES_URL}/${caseInfo.id}/comments`) + .set('kbn-xsrf', 'true') + .query({ subCaseId: caseInfo.subCases![0].id }) + .send(postCommentUserReq) + .expect(200); + + const subCaseCommentUrl = `${CASES_URL}/${patchedCaseWithSubCase.id}/comments/${ + patchedCaseWithSubCase.comments![1].id + }`; + // make sure we can get the second comment + await supertest.get(subCaseCommentUrl).set('kbn-xsrf', 'true').send().expect(200); + + await deleteCases({ supertest, caseIDs: [caseInfo.id] }); + + await supertest.get(subCaseCommentUrl).set('kbn-xsrf', 'true').send().expect(404); + }); + }); + + describe('rbac', () => { + it('User: security solution only - should delete a case', async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { + user: secOnly, + space: 'space1', + } + ); + + await deleteCases({ + supertest, + caseIDs: [postedCase.id], + expectedHttpCode: 204, + auth: { user: secOnly, space: 'space1' }, + }); + }); + + it('User: security solution only - should NOT delete a case of different owner', async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { + user: secOnly, + space: 'space1', + } + ); + + await deleteCases({ + supertest: supertestWithoutAuth, + caseIDs: [postedCase.id], + expectedHttpCode: 403, + auth: { user: obsOnly, space: 'space1' }, + }); + }); + + it('should get an error if the user has not permissions to all requested cases', async () => { + const caseSec = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { + user: secOnly, + space: 'space1', + } + ); + + const caseObs = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + { + user: obsOnly, + space: 'space1', + } + ); + + await deleteCases({ + supertest: supertestWithoutAuth, + caseIDs: [caseSec.id, caseObs.id], + expectedHttpCode: 403, + auth: { user: obsOnly, space: 'space1' }, + }); + + // Cases should have not been deleted. + await getCase({ + supertest: supertestWithoutAuth, + caseId: caseSec.id, + expectedHttpCode: 200, + auth: superUserSpace1Auth, + }); + + await getCase({ + supertest: supertestWithoutAuth, + caseId: caseObs.id, + expectedHttpCode: 200, + auth: superUserSpace1Auth, + }); + }); + + for (const user of [globalRead, secOnlyRead, obsOnlyRead, obsSecRead, noKibanaPrivileges]) { + it(`User ${ + user.username + } with role(s) ${user.roles.join()} - should NOT delete a case`, async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { + user: superUser, + space: 'space1', + } + ); + + await deleteCases({ + supertest: supertestWithoutAuth, + caseIDs: [postedCase.id], + expectedHttpCode: 403, + auth: { user, space: 'space1' }, + }); + }); + } + + it('should NOT delete a case in a space with no permissions', async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { + user: superUser, + space: 'space2', + } + ); + + /** + * We expect a 404 because the bulkGet inside the delete + * route should return a 404 when requesting a case from + * a different space. + * */ + await deleteCases({ + supertest: supertestWithoutAuth, + caseIDs: [postedCase.id], + expectedHttpCode: 404, + auth: { user: secOnly, space: 'space1' }, + }); + }); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/security_only/tests/common/cases/find_cases.ts b/x-pack/test/case_api_integration/security_only/tests/common/cases/find_cases.ts new file mode 100644 index 00000000000000..b7838dd9299bcd --- /dev/null +++ b/x-pack/test/case_api_integration/security_only/tests/common/cases/find_cases.ts @@ -0,0 +1,815 @@ +/* + * 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 type { ApiResponse, estypes } from '@elastic/elasticsearch'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +import { + CASES_URL, + SUB_CASES_PATCH_DEL_URL, +} from '../../../../../../plugins/cases/common/constants'; +import { + postCaseReq, + postCommentUserReq, + findCasesResp, + getPostCaseRequest, +} from '../../../../common/lib/mock'; +import { + deleteAllCaseItems, + createSubCase, + setStatus, + CreateSubCaseResp, + createCaseAction, + deleteCaseAction, + ensureSavedObjectIsAuthorized, + findCases, + createCase, + updateCase, + createComment, +} from '../../../../common/lib/utils'; +import { CaseResponse, CaseStatuses, CaseType } from '../../../../../../plugins/cases/common/api'; +import { + obsOnly, + secOnly, + obsOnlyRead, + secOnlyRead, + noKibanaPrivileges, + superUser, + globalRead, + obsSecRead, + obsSec, +} from '../../../../common/lib/authentication/users'; + +interface CaseAttributes { + cases: { + title: string; + }; +} + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + describe('find_cases', () => { + describe('basic tests', () => { + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + it('should return empty response', async () => { + const cases = await findCases({ supertest }); + expect(cases).to.eql(findCasesResp); + }); + + it('should return cases', async () => { + const a = await createCase(supertest, postCaseReq); + const b = await createCase(supertest, postCaseReq); + const c = await createCase(supertest, postCaseReq); + + const cases = await findCases({ supertest }); + + expect(cases).to.eql({ + ...findCasesResp, + total: 3, + cases: [a, b, c], + count_open_cases: 3, + }); + }); + + it('filters by tags', async () => { + await createCase(supertest, postCaseReq); + const postedCase = await createCase(supertest, { ...postCaseReq, tags: ['unique'] }); + const cases = await findCases({ supertest, query: { tags: ['unique'] } }); + + expect(cases).to.eql({ + ...findCasesResp, + total: 1, + cases: [postedCase], + count_open_cases: 1, + }); + }); + + it('filters by status', async () => { + await createCase(supertest, postCaseReq); + const toCloseCase = await createCase(supertest, postCaseReq); + const patchedCase = await updateCase({ + supertest, + params: { + cases: [ + { + id: toCloseCase.id, + version: toCloseCase.version, + status: CaseStatuses.closed, + }, + ], + }, + }); + + const cases = await findCases({ supertest, query: { status: CaseStatuses.closed } }); + + expect(cases).to.eql({ + ...findCasesResp, + total: 1, + cases: [patchedCase[0]], + count_open_cases: 1, + count_closed_cases: 1, + count_in_progress_cases: 0, + }); + }); + + it('filters by reporters', async () => { + const postedCase = await createCase(supertest, postCaseReq); + const cases = await findCases({ supertest, query: { reporters: 'elastic' } }); + + expect(cases).to.eql({ + ...findCasesResp, + total: 1, + cases: [postedCase], + count_open_cases: 1, + }); + }); + + it('correctly counts comments', async () => { + const postedCase = await createCase(supertest, postCaseReq); + + // post 2 comments + await createComment({ supertest, caseId: postedCase.id, params: postCommentUserReq }); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + }); + + const cases = await findCases({ supertest }); + expect(cases).to.eql({ + ...findCasesResp, + total: 1, + cases: [ + { + ...patchedCase, + comments: [], + totalComment: 2, + }, + ], + count_open_cases: 1, + }); + }); + + it('correctly counts open/closed/in-progress', async () => { + await createCase(supertest, postCaseReq); + const inProgressCase = await createCase(supertest, postCaseReq); + const postedCase = await createCase(supertest, postCaseReq); + + await updateCase({ + supertest, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + status: CaseStatuses.closed, + }, + ], + }, + }); + + await updateCase({ + supertest, + params: { + cases: [ + { + id: inProgressCase.id, + version: inProgressCase.version, + status: CaseStatuses['in-progress'], + }, + ], + }, + }); + + const cases = await findCases({ supertest }); + expect(cases.count_open_cases).to.eql(1); + expect(cases.count_closed_cases).to.eql(1); + expect(cases.count_in_progress_cases).to.eql(1); + }); + + it('unhappy path - 400s when bad query supplied', async () => { + await findCases({ supertest, query: { perPage: true }, expectedHttpCode: 400 }); + }); + + // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests + describe.skip('stats with sub cases', () => { + let collection: CreateSubCaseResp; + let actionID: string; + before(async () => { + actionID = await createCaseAction(supertest); + }); + after(async () => { + await deleteCaseAction(supertest, actionID); + }); + beforeEach(async () => { + // create a collection with a sub case that is marked as open + collection = await createSubCase({ supertest, actionID }); + + const [, , { body: toCloseCase }] = await Promise.all([ + // set the sub case to in-progress + setStatus({ + supertest, + cases: [ + { + id: collection.newSubCaseInfo.subCases![0].id, + version: collection.newSubCaseInfo.subCases![0].version, + status: CaseStatuses['in-progress'], + }, + ], + type: 'sub_case', + }), + // create two cases that are both open + supertest.post(CASES_URL).set('kbn-xsrf', 'true').send(postCaseReq), + supertest.post(CASES_URL).set('kbn-xsrf', 'true').send(postCaseReq), + ]); + + // set the third case to closed + await setStatus({ + supertest, + cases: [ + { + id: toCloseCase.id, + version: toCloseCase.version, + status: CaseStatuses.closed, + }, + ], + type: 'case', + }); + }); + it('correctly counts stats without using a filter', async () => { + const cases = await findCases({ supertest }); + + expect(cases.total).to.eql(3); + expect(cases.count_closed_cases).to.eql(1); + expect(cases.count_open_cases).to.eql(1); + expect(cases.count_in_progress_cases).to.eql(1); + }); + + it('correctly counts stats with a filter for open cases', async () => { + const cases = await findCases({ supertest, query: { status: CaseStatuses.open } }); + + expect(cases.cases.length).to.eql(1); + + // since we're filtering on status and the collection only has an in-progress case, it should only return the + // individual case that has the open status and no collections + // ENABLE_CASE_CONNECTOR: this value is not correct because it includes a collection + // that does not have an open case. This is a known issue and will need to be resolved + // when this issue is addressed: https://github.com/elastic/kibana/issues/94115 + expect(cases.total).to.eql(2); + expect(cases.count_closed_cases).to.eql(1); + expect(cases.count_open_cases).to.eql(1); + expect(cases.count_in_progress_cases).to.eql(1); + }); + + it('correctly counts stats with a filter for individual cases', async () => { + const cases = await findCases({ supertest, query: { type: CaseType.individual } }); + + expect(cases.total).to.eql(2); + expect(cases.count_closed_cases).to.eql(1); + expect(cases.count_open_cases).to.eql(1); + expect(cases.count_in_progress_cases).to.eql(0); + }); + + it('correctly counts stats with a filter for collection cases with multiple sub cases', async () => { + // this will force the first sub case attached to the collection to be closed + // so we'll have one closed sub case and one open sub case + await createSubCase({ supertest, caseID: collection.newSubCaseInfo.id, actionID }); + const cases = await findCases({ supertest, query: { type: CaseType.collection } }); + + expect(cases.total).to.eql(1); + expect(cases.cases[0].subCases?.length).to.eql(2); + expect(cases.count_closed_cases).to.eql(1); + expect(cases.count_open_cases).to.eql(1); + expect(cases.count_in_progress_cases).to.eql(0); + }); + + it('correctly counts stats with a filter for collection and open cases with multiple sub cases', async () => { + // this will force the first sub case attached to the collection to be closed + // so we'll have one closed sub case and one open sub case + await createSubCase({ supertest, caseID: collection.newSubCaseInfo.id, actionID }); + const cases = await findCases({ + supertest, + query: { + type: CaseType.collection, + status: CaseStatuses.open, + }, + }); + + expect(cases.total).to.eql(1); + expect(cases.cases[0].subCases?.length).to.eql(1); + expect(cases.count_closed_cases).to.eql(1); + expect(cases.count_open_cases).to.eql(1); + expect(cases.count_in_progress_cases).to.eql(0); + }); + + it('correctly counts stats including a collection without sub cases when not filtering on status', async () => { + // delete the sub case on the collection so that it doesn't have any sub cases + await supertest + .delete( + `${SUB_CASES_PATCH_DEL_URL}?ids=["${collection.newSubCaseInfo.subCases![0].id}"]` + ) + .set('kbn-xsrf', 'true') + .send() + .expect(204); + + const cases = await findCases({ supertest, query: { type: CaseType.collection } }); + + // it should include the collection without sub cases because we did not pass in a filter on status + expect(cases.total).to.eql(3); + expect(cases.count_closed_cases).to.eql(1); + expect(cases.count_open_cases).to.eql(1); + expect(cases.count_in_progress_cases).to.eql(0); + }); + + it('correctly counts stats including a collection without sub cases when filtering on tags', async () => { + // delete the sub case on the collection so that it doesn't have any sub cases + await supertest + .delete( + `${SUB_CASES_PATCH_DEL_URL}?ids=["${collection.newSubCaseInfo.subCases![0].id}"]` + ) + .set('kbn-xsrf', 'true') + .send() + .expect(204); + + const cases = await findCases({ supertest, query: { tags: ['defacement'] } }); + + // it should include the collection without sub cases because we did not pass in a filter on status + expect(cases.total).to.eql(3); + expect(cases.count_closed_cases).to.eql(1); + expect(cases.count_open_cases).to.eql(1); + expect(cases.count_in_progress_cases).to.eql(0); + }); + + it('does not return collections without sub cases matching the requested status', async () => { + const cases = await findCases({ supertest, query: { status: CaseStatuses.closed } }); + + expect(cases.cases.length).to.eql(1); + // it should not include the collection that has a sub case as in-progress + // ENABLE_CASE_CONNECTOR: this value is not correct because it includes collections. This short term + // fix for when sub cases are not enabled. When the feature is completed the _find API + // will need to be fixed as explained in this ticket: https://github.com/elastic/kibana/issues/94115 + expect(cases.total).to.eql(2); + expect(cases.count_closed_cases).to.eql(1); + expect(cases.count_open_cases).to.eql(1); + expect(cases.count_in_progress_cases).to.eql(1); + }); + + it('does not return empty collections when filtering on status', async () => { + // delete the sub case on the collection so that it doesn't have any sub cases + await supertest + .delete( + `${SUB_CASES_PATCH_DEL_URL}?ids=["${collection.newSubCaseInfo.subCases![0].id}"]` + ) + .set('kbn-xsrf', 'true') + .send() + .expect(204); + + const cases = await findCases({ supertest, query: { status: CaseStatuses.closed } }); + + expect(cases.cases.length).to.eql(1); + + // ENABLE_CASE_CONNECTOR: this value is not correct because it includes collections. This short term + // fix for when sub cases are not enabled. When the feature is completed the _find API + // will need to be fixed as explained in this ticket: https://github.com/elastic/kibana/issues/94115 + expect(cases.total).to.eql(2); + expect(cases.count_closed_cases).to.eql(1); + expect(cases.count_open_cases).to.eql(1); + expect(cases.count_in_progress_cases).to.eql(0); + }); + }); + }); + + describe('find_cases pagination', () => { + const numCases = 10; + before(async () => { + await createCasesWithTitleAsNumber(numCases); + }); + + after(async () => { + await deleteAllCaseItems(es); + }); + + const createCasesWithTitleAsNumber = async (total: number): Promise => { + const responsePromises = []; + for (let i = 0; i < total; i++) { + // this doesn't guarantee that the cases will be created in order that the for-loop executes, + // for example case with title '2', could be created before the case with title '1' since we're doing a promise all here + // A promise all is just much faster than doing it one by one which would have guaranteed that the cases are + // created in the order that the for-loop executes + responsePromises.push(createCase(supertest, { ...postCaseReq, title: `${i}` })); + } + const responses = await Promise.all(responsePromises); + return responses; + }; + + /** + * This is used to retrieve all the cases in the same sorted order that we're expecting them to come back via the + * _find API so that we have a more true comparison instead of using the _find API to get all the cases which + * could mangle the results if the implementation had a bug. + * + * Ideally we could enforce how the cases are created in reasonable time, waiting for each api call to finish takes + * around 30 seconds which seemed too slow + */ + const getAllCasesSortedByCreatedAtAsc = async () => { + const cases: ApiResponse> = await es.search({ + index: '.kibana', + body: { + size: 10000, + sort: [{ 'cases.created_at': { unmapped_type: 'date', order: 'asc' } }], + query: { + term: { type: 'cases' }, + }, + }, + }); + return cases.body.hits.hits.map((hit) => hit._source); + }; + + it('returns the correct total when perPage is less than the total', async () => { + const cases = await findCases({ + supertest, + query: { + page: 1, + perPage: 5, + }, + }); + + expect(cases.cases.length).to.eql(5); + expect(cases.total).to.eql(10); + expect(cases.page).to.eql(1); + expect(cases.per_page).to.eql(5); + expect(cases.count_open_cases).to.eql(10); + expect(cases.count_closed_cases).to.eql(0); + expect(cases.count_in_progress_cases).to.eql(0); + }); + + it('returns the correct total when perPage is greater than the total', async () => { + const cases = await findCases({ + supertest, + query: { + page: 1, + perPage: 11, + }, + }); + + expect(cases.total).to.eql(10); + expect(cases.page).to.eql(1); + expect(cases.per_page).to.eql(11); + expect(cases.cases.length).to.eql(10); + expect(cases.count_open_cases).to.eql(10); + expect(cases.count_closed_cases).to.eql(0); + expect(cases.count_in_progress_cases).to.eql(0); + }); + + it('returns the correct total when perPage is equal to the total', async () => { + const cases = await findCases({ + supertest, + query: { + page: 1, + perPage: 10, + }, + }); + + expect(cases.total).to.eql(10); + expect(cases.page).to.eql(1); + expect(cases.per_page).to.eql(10); + expect(cases.cases.length).to.eql(10); + expect(cases.count_open_cases).to.eql(10); + expect(cases.count_closed_cases).to.eql(0); + expect(cases.count_in_progress_cases).to.eql(0); + }); + + it('returns the second page of results', async () => { + const perPage = 5; + const cases = await findCases({ + supertest, + query: { + page: 2, + perPage, + }, + }); + + expect(cases.total).to.eql(10); + expect(cases.page).to.eql(2); + expect(cases.per_page).to.eql(5); + expect(cases.cases.length).to.eql(5); + expect(cases.count_open_cases).to.eql(10); + expect(cases.count_closed_cases).to.eql(0); + expect(cases.count_in_progress_cases).to.eql(0); + + const allCases = await getAllCasesSortedByCreatedAtAsc(); + + cases.cases.map((caseInfo, index) => { + // we started on the second page of 10 cases with a perPage of 5, so the first case should 0 + 5 (index + perPage) + expect(caseInfo.title).to.eql(allCases[index + perPage]?.cases.title); + }); + }); + + it('paginates with perPage of 2 through 10 total cases', async () => { + const total = 10; + const perPage = 2; + + // it's less than or equal here because the page starts at 1, so page 5 is a valid page number + // and should have case titles 9, and 10 + for (let currentPage = 1; currentPage <= total / perPage; currentPage++) { + const cases = await findCases({ + supertest, + query: { + page: currentPage, + perPage, + }, + }); + + expect(cases.total).to.eql(total); + expect(cases.page).to.eql(currentPage); + expect(cases.per_page).to.eql(perPage); + expect(cases.cases.length).to.eql(perPage); + expect(cases.count_open_cases).to.eql(total); + expect(cases.count_closed_cases).to.eql(0); + expect(cases.count_in_progress_cases).to.eql(0); + + const allCases = await getAllCasesSortedByCreatedAtAsc(); + + cases.cases.map((caseInfo, index) => { + // for page 1, the cases tiles should be 0,1,2 for page 2: 3,4,5 etc (assuming the titles were sorted + // correctly) + expect(caseInfo.title).to.eql( + allCases[index + perPage * (currentPage - 1)]?.cases.title + ); + }); + } + }); + + it('retrieves the last three cases', async () => { + const cases = await findCases({ + supertest, + query: { + // this should skip the first 7 cases and only return the last 3 + page: 2, + perPage: 7, + }, + }); + + expect(cases.total).to.eql(10); + expect(cases.page).to.eql(2); + expect(cases.per_page).to.eql(7); + expect(cases.cases.length).to.eql(3); + expect(cases.count_open_cases).to.eql(10); + expect(cases.count_closed_cases).to.eql(0); + expect(cases.count_in_progress_cases).to.eql(0); + }); + }); + + describe('rbac', () => { + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + it('should return the correct cases', async () => { + await Promise.all([ + // Create case owned by the security solution user + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { + user: secOnly, + space: 'space1', + } + ), + // Create case owned by the observability user + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + { + user: obsOnly, + space: 'space1', + } + ), + ]); + + for (const scenario of [ + { + user: globalRead, + numberOfExpectedCases: 2, + owners: ['securitySolutionFixture', 'observabilityFixture'], + }, + { + user: superUser, + numberOfExpectedCases: 2, + owners: ['securitySolutionFixture', 'observabilityFixture'], + }, + { user: secOnlyRead, numberOfExpectedCases: 1, owners: ['securitySolutionFixture'] }, + { user: obsOnlyRead, numberOfExpectedCases: 1, owners: ['observabilityFixture'] }, + { + user: obsSecRead, + numberOfExpectedCases: 2, + owners: ['securitySolutionFixture', 'observabilityFixture'], + }, + ]) { + const res = await findCases({ + supertest: supertestWithoutAuth, + auth: { + user: scenario.user, + space: 'space1', + }, + }); + + ensureSavedObjectIsAuthorized(res.cases, scenario.numberOfExpectedCases, scenario.owners); + } + }); + + for (const scenario of [ + { user: noKibanaPrivileges, space: 'space1' }, + { user: secOnly, space: 'space2' }, + ]) { + it(`User ${scenario.user.username} with role(s) ${scenario.user.roles.join()} and space ${ + scenario.space + } - should NOT read a case`, async () => { + // super user creates a case at the appropriate space + await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { + user: superUser, + space: scenario.space, + } + ); + + // user should not be able to read cases at the appropriate space + await findCases({ + supertest: supertestWithoutAuth, + auth: { + user: scenario.user, + space: scenario.space, + }, + expectedHttpCode: 403, + }); + }); + } + + it('should return the correct cases when trying to exploit RBAC through the search query parameter', async () => { + await Promise.all([ + // super user creates a case with owner securitySolutionFixture + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { + user: superUser, + space: 'space1', + } + ), + // super user creates a case with owner observabilityFixture + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + { + user: superUser, + space: 'space1', + } + ), + ]); + + const res = await findCases({ + supertest: supertestWithoutAuth, + query: { + search: 'securitySolutionFixture observabilityFixture', + searchFields: 'owner', + }, + auth: { + user: secOnly, + space: 'space1', + }, + }); + + ensureSavedObjectIsAuthorized(res.cases, 1, ['securitySolutionFixture']); + }); + + // This test is to prevent a future developer to add the filter attribute without taking into consideration + // the authorizationFilter produced by the cases authorization class + it('should NOT allow to pass a filter query parameter', async () => { + await supertest + .get( + `${CASES_URL}/_find?sortOrder=asc&filter=cases.attributes.owner:"observabilityFixture"` + ) + .set('kbn-xsrf', 'true') + .send() + .expect(400); + }); + + // This test ensures that the user is not allowed to define the namespaces query param + // so she cannot search across spaces + it('should NOT allow to pass a namespaces query parameter', async () => { + await supertest + .get(`${CASES_URL}/_find?sortOrder=asc&namespaces[0]=*`) + .set('kbn-xsrf', 'true') + .send() + .expect(400); + + await supertest + .get(`${CASES_URL}/_find?sortOrder=asc&namespaces=*`) + .set('kbn-xsrf', 'true') + .send() + .expect(400); + }); + + it('should NOT allow to pass a non supported query parameter', async () => { + await supertest + .get(`${CASES_URL}/_find?notExists=papa`) + .set('kbn-xsrf', 'true') + .send() + .expect(400); + }); + + it('should respect the owner filter when having permissions', async () => { + await Promise.all([ + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { + user: obsSec, + space: 'space1', + } + ), + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + { + user: obsSec, + space: 'space1', + } + ), + ]); + + const res = await findCases({ + supertest: supertestWithoutAuth, + query: { + owner: 'securitySolutionFixture', + searchFields: 'owner', + }, + auth: { + user: obsSec, + space: 'space1', + }, + }); + + ensureSavedObjectIsAuthorized(res.cases, 1, ['securitySolutionFixture']); + }); + + it('should return the correct cases when trying to exploit RBAC through the owner query parameter', async () => { + await Promise.all([ + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { + user: obsSec, + space: 'space1', + } + ), + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + { + user: obsSec, + space: 'space1', + } + ), + ]); + + // User with permissions only to security solution request cases from observability + const res = await findCases({ + supertest: supertestWithoutAuth, + query: { + owner: ['securitySolutionFixture', 'observabilityFixture'], + }, + auth: { + user: secOnly, + space: 'space1', + }, + }); + + // Only security solution cases are being returned + ensureSavedObjectIsAuthorized(res.cases, 1, ['securitySolutionFixture']); + }); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/security_only/tests/common/cases/get_case.ts b/x-pack/test/case_api_integration/security_only/tests/common/cases/get_case.ts new file mode 100644 index 00000000000000..222632b41c297c --- /dev/null +++ b/x-pack/test/case_api_integration/security_only/tests/common/cases/get_case.ts @@ -0,0 +1,207 @@ +/* + * 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 { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +import { AttributesTypeUser } from '../../../../../../plugins/cases/common/api'; +import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; +import { + defaultUser, + postCaseReq, + postCaseResp, + postCommentUserReq, + getPostCaseRequest, +} from '../../../../common/lib/mock'; +import { + deleteCasesByESQuery, + createCase, + getCase, + createComment, + removeServerGeneratedPropertiesFromCase, + removeServerGeneratedPropertiesFromSavedObject, +} from '../../../../common/lib/utils'; +import { + secOnly, + obsOnly, + globalRead, + superUser, + secOnlyRead, + obsOnlyRead, + obsSecRead, + noKibanaPrivileges, + obsSec, +} from '../../../../common/lib/authentication/users'; +import { getUserInfo } from '../../../../common/lib/authentication'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const es = getService('es'); + + describe('get_case', () => { + afterEach(async () => { + await deleteCasesByESQuery(es); + }); + + it('should return a case with no comments', async () => { + const postedCase = await createCase(supertest, getPostCaseRequest()); + const theCase = await getCase({ supertest, caseId: postedCase.id, includeComments: true }); + + const data = removeServerGeneratedPropertiesFromCase(theCase); + expect(data).to.eql(postCaseResp()); + expect(data.comments?.length).to.eql(0); + }); + + it('should return a case with comments', async () => { + const postedCase = await createCase(supertest, postCaseReq); + await createComment({ supertest, caseId: postedCase.id, params: postCommentUserReq }); + const theCase = await getCase({ supertest, caseId: postedCase.id, includeComments: true }); + + const comment = removeServerGeneratedPropertiesFromSavedObject( + theCase.comments![0] as AttributesTypeUser + ); + + expect(theCase.comments?.length).to.eql(1); + expect(comment).to.eql({ + type: postCommentUserReq.type, + comment: postCommentUserReq.comment, + associationType: 'case', + created_by: defaultUser, + pushed_at: null, + pushed_by: null, + updated_by: null, + owner: 'securitySolutionFixture', + }); + }); + + it('should return a 400 when passing the includeSubCaseComments', async () => { + const { body } = await supertest + .get(`${CASES_URL}/case-id?includeSubCaseComments=true`) + .set('kbn-xsrf', 'true') + .send() + .expect(400); + + expect(body.message).to.contain('disabled'); + }); + + it('unhappy path - 404s when case is not there', async () => { + await supertest.get(`${CASES_URL}/fake-id`).set('kbn-xsrf', 'true').send().expect(404); + }); + + describe('rbac', () => { + it('should get a case', async () => { + const newCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { + user: superUser, + space: 'space1', + } + ); + + for (const user of [globalRead, superUser, secOnly, secOnlyRead, obsSec, obsSecRead]) { + const theCase = await getCase({ + supertest: supertestWithoutAuth, + caseId: newCase.id, + auth: { user, space: 'space1' }, + }); + + expect(theCase.owner).to.eql('securitySolutionFixture'); + } + }); + + it('should get a case with comments', async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { + user: secOnly, + space: 'space1', + } + ); + + await createComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + params: postCommentUserReq, + expectedHttpCode: 200, + auth: { + user: secOnly, + space: 'space1', + }, + }); + + const theCase = await getCase({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + includeComments: true, + auth: { user: secOnly, space: 'space1' }, + }); + + const comment = removeServerGeneratedPropertiesFromSavedObject( + theCase.comments![0] as AttributesTypeUser + ); + + expect(theCase.comments?.length).to.eql(1); + expect(comment).to.eql({ + type: postCommentUserReq.type, + comment: postCommentUserReq.comment, + associationType: 'case', + created_by: getUserInfo(secOnly), + pushed_at: null, + pushed_by: null, + updated_by: null, + owner: 'securitySolutionFixture', + }); + }); + + it('should not get a case when the user does not have access to owner', async () => { + const newCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { + user: superUser, + space: 'space1', + } + ); + + for (const user of [noKibanaPrivileges, obsOnly, obsOnlyRead]) { + await getCase({ + supertest: supertestWithoutAuth, + caseId: newCase.id, + expectedHttpCode: 403, + auth: { user, space: 'space1' }, + }); + } + }); + + it('should NOT get a case in a space with no permissions', async () => { + const newCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { + user: superUser, + space: 'space2', + } + ); + + await getCase({ + supertest: supertestWithoutAuth, + caseId: newCase.id, + expectedHttpCode: 403, + auth: { user: secOnly, space: 'space2' }, + }); + }); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/security_only/tests/common/cases/patch_cases.ts b/x-pack/test/case_api_integration/security_only/tests/common/cases/patch_cases.ts new file mode 100644 index 00000000000000..286e08716ebf1e --- /dev/null +++ b/x-pack/test/case_api_integration/security_only/tests/common/cases/patch_cases.ts @@ -0,0 +1,1240 @@ +/* + * 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 { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +import { DETECTION_ENGINE_QUERY_SIGNALS_URL } from '../../../../../../plugins/security_solution/common/constants'; +import { + CasesResponse, + CaseStatuses, + CaseType, + CommentType, + ConnectorTypes, +} from '../../../../../../plugins/cases/common/api'; +import { + defaultUser, + getPostCaseRequest, + postCaseReq, + postCaseResp, + postCollectionReq, + postCommentAlertReq, + postCommentUserReq, +} from '../../../../common/lib/mock'; +import { + deleteAllCaseItems, + getSignalsWithES, + setStatus, + createCase, + createComment, + updateCase, + getCaseUserActions, + removeServerGeneratedPropertiesFromCase, + removeServerGeneratedPropertiesFromUserAction, + findCases, + superUserSpace1Auth, +} from '../../../../common/lib/utils'; +import { + createSignalsIndex, + deleteSignalsIndex, + deleteAllAlerts, + getRuleForSignalTesting, + waitForRuleSuccessOrStatus, + waitForSignalsToBePresent, + getSignalsByIds, + createRule, + getQuerySignalIds, +} from '../../../../../detection_engine_api_integration/utils'; +import { + globalRead, + noKibanaPrivileges, + obsOnly, + obsOnlyRead, + obsSecRead, + secOnly, + secOnlyRead, + superUser, +} from '../../../../common/lib/authentication/users'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + const es = getService('es'); + + describe('patch_cases', () => { + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + describe('happy path', () => { + it('should patch a case', async () => { + const postedCase = await createCase(supertest, postCaseReq); + const patchedCases = await updateCase({ + supertest, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + title: 'new title', + }, + ], + }, + }); + + const data = removeServerGeneratedPropertiesFromCase(patchedCases[0]); + expect(data).to.eql({ + ...postCaseResp(), + title: 'new title', + updated_by: defaultUser, + }); + }); + + it('should closes the case correctly', async () => { + const postedCase = await createCase(supertest, postCaseReq); + const patchedCases = await updateCase({ + supertest, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + status: CaseStatuses.closed, + }, + ], + }, + }); + + const userActions = await getCaseUserActions({ supertest, caseID: postedCase.id }); + const statusUserAction = removeServerGeneratedPropertiesFromUserAction(userActions[1]); + const data = removeServerGeneratedPropertiesFromCase(patchedCases[0]); + + expect(data).to.eql({ + ...postCaseResp(), + status: CaseStatuses.closed, + closed_by: defaultUser, + updated_by: defaultUser, + }); + + expect(statusUserAction).to.eql({ + action_field: ['status'], + action: 'update', + action_by: defaultUser, + new_value: CaseStatuses.closed, + old_value: CaseStatuses.open, + case_id: `${postedCase.id}`, + comment_id: null, + sub_case_id: '', + owner: 'securitySolutionFixture', + }); + }); + + it('should change the status of case to in-progress correctly', async () => { + const postedCase = await createCase(supertest, postCaseReq); + const patchedCases = await updateCase({ + supertest, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + status: CaseStatuses['in-progress'], + }, + ], + }, + }); + + const userActions = await getCaseUserActions({ supertest, caseID: postedCase.id }); + const statusUserAction = removeServerGeneratedPropertiesFromUserAction(userActions[1]); + const data = removeServerGeneratedPropertiesFromCase(patchedCases[0]); + + expect(data).to.eql({ + ...postCaseResp(), + status: CaseStatuses['in-progress'], + updated_by: defaultUser, + }); + + expect(statusUserAction).to.eql({ + action_field: ['status'], + action: 'update', + action_by: defaultUser, + new_value: CaseStatuses['in-progress'], + old_value: CaseStatuses.open, + case_id: `${postedCase.id}`, + comment_id: null, + sub_case_id: '', + owner: 'securitySolutionFixture', + }); + }); + + it('should patch a case with new connector', async () => { + const postedCase = await createCase(supertest, postCaseReq); + const patchedCases = await updateCase({ + supertest, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + connector: { + id: 'jira', + name: 'Jira', + type: ConnectorTypes.jira, + fields: { issueType: 'Task', priority: null, parent: null }, + }, + }, + ], + }, + }); + + const data = removeServerGeneratedPropertiesFromCase(patchedCases[0]); + expect(data).to.eql({ + ...postCaseResp(), + connector: { + id: 'jira', + name: 'Jira', + type: '.jira', + fields: { issueType: 'Task', priority: null, parent: null }, + }, + updated_by: defaultUser, + }); + }); + + // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests + it.skip('should allow converting an individual case to a collection when it does not have alerts', async () => { + const postedCase = await createCase(supertest, postCaseReq); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + }); + await updateCase({ + supertest, + params: { + cases: [ + { + id: patchedCase.id, + version: patchedCase.version, + type: CaseType.collection, + }, + ], + }, + }); + }); + }); + + describe('unhappy path', () => { + it('400s when attempting to change the owner of a case', async () => { + const postedCase = await createCase(supertest, postCaseReq); + await updateCase({ + supertest, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + owner: 'observabilityFixture', + }, + ], + }, + expectedHttpCode: 400, + }); + }); + + it('404s when case is not there', async () => { + await updateCase({ + supertest, + params: { + cases: [ + { + id: 'not-real', + version: 'version', + status: CaseStatuses.closed, + }, + ], + }, + expectedHttpCode: 404, + }); + }); + + it('400s when id is missing', async () => { + await updateCase({ + supertest, + params: { + cases: [ + // @ts-expect-error + { + version: 'version', + status: CaseStatuses.closed, + }, + ], + }, + expectedHttpCode: 400, + }); + }); + + it('406s when fields are identical', async () => { + const postedCase = await createCase(supertest, postCaseReq); + await updateCase({ + supertest, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + status: CaseStatuses.open, + }, + ], + }, + expectedHttpCode: 406, + }); + }); + + it('400s when version is missing', async () => { + await updateCase({ + supertest, + params: { + cases: [ + // @ts-expect-error + { + id: 'not-real', + status: CaseStatuses.closed, + }, + ], + }, + expectedHttpCode: 400, + }); + }); + + // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests + it.skip('should 400 and not allow converting a collection back to an individual case', async () => { + const postedCase = await createCase(supertest, postCollectionReq); + await updateCase({ + supertest, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + type: CaseType.individual, + }, + ], + }, + expectedHttpCode: 400, + }); + }); + + it('406s when excess data sent', async () => { + const postedCase = await createCase(supertest, postCaseReq); + await updateCase({ + supertest, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + // @ts-expect-error + badKey: 'closed', + }, + ], + }, + expectedHttpCode: 406, + }); + }); + + it('400s when bad data sent', async () => { + const postedCase = await createCase(supertest, postCaseReq); + await updateCase({ + supertest, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + // @ts-expect-error + status: true, + }, + ], + }, + expectedHttpCode: 400, + }); + }); + + it('400s when unsupported status sent', async () => { + const postedCase = await createCase(supertest, postCaseReq); + await updateCase({ + supertest, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + // @ts-expect-error + status: 'not-supported', + }, + ], + }, + expectedHttpCode: 400, + }); + }); + + it('400s when bad connector type sent', async () => { + const postedCase = await createCase(supertest, postCaseReq); + await updateCase({ + supertest, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + // @ts-expect-error + connector: { id: 'none', name: 'none', type: '.not-exists', fields: null }, + }, + ], + }, + expectedHttpCode: 400, + }); + }); + + it('400s when bad connector sent', async () => { + const postedCase = await createCase(supertest, postCaseReq); + await updateCase({ + supertest, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + connector: { + id: 'jira', + name: 'Jira', + // @ts-expect-error + type: ConnectorTypes.jira, + // @ts-expect-error + fields: { unsupported: 'value' }, + }, + }, + ], + }, + expectedHttpCode: 400, + }); + }); + + it('409s when version does not match', async () => { + const postedCase = await createCase(supertest, postCaseReq); + await updateCase({ + supertest, + params: { + cases: [ + { + id: postedCase.id, + version: 'version', + // @ts-expect-error + status: 'closed', + }, + ], + }, + expectedHttpCode: 409, + }); + }); + + it('should 400 when attempting to update an individual case to a collection when it has alerts attached to it', async () => { + const postedCase = await createCase(supertest, postCaseReq); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentAlertReq, + }); + await updateCase({ + supertest, + params: { + cases: [ + { + id: patchedCase.id, + version: patchedCase.version, + type: CaseType.collection, + }, + ], + }, + expectedHttpCode: 400, + }); + }); + + // ENABLE_CASE_CONNECTOR: once the case connector feature is completed delete these tests + it('should 400 when attempting to update the case type when the case connector feature is disabled', async () => { + const postedCase = await createCase(supertest, postCaseReq); + await updateCase({ + supertest, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + type: CaseType.collection, + }, + ], + }, + expectedHttpCode: 400, + }); + }); + + // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests + it.skip("should 400 when attempting to update a collection case's status", async () => { + const postedCase = await createCase(supertest, postCollectionReq); + await updateCase({ + supertest, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + status: CaseStatuses.closed, + }, + ], + }, + expectedHttpCode: 400, + }); + }); + }); + + describe('alerts', () => { + describe('esArchiver', () => { + const defaultSignalsIndex = '.siem-signals-default-000001'; + + beforeEach(async () => { + await esArchiver.load('cases/signals/default'); + }); + afterEach(async () => { + await esArchiver.unload('cases/signals/default'); + await deleteAllCaseItems(es); + }); + + it('should update the status of multiple alerts attached to multiple cases', async () => { + const signalID = '5f2b9ec41f8febb1c06b5d1045aeabb9874733b7617e88a370510f2fb3a41a5d'; + const signalID2 = '4d0f4b1533e46b66b43bdd0330d23f39f2cf42a7253153270e38d30cce9ff0c6'; + + // does NOT updates alert status when adding comments and syncAlerts=false + const individualCase1 = await createCase(supertest, { + ...postCaseReq, + settings: { + syncAlerts: false, + }, + }); + + const updatedInd1WithComment = await createComment({ + supertest, + caseId: individualCase1.id, + params: { + alertId: signalID, + index: defaultSignalsIndex, + rule: { id: 'test-rule-id', name: 'test-index-id' }, + type: CommentType.alert, + owner: 'securitySolutionFixture', + }, + }); + + const individualCase2 = await createCase(supertest, { + ...postCaseReq, + settings: { + syncAlerts: false, + }, + }); + + const updatedInd2WithComment = await createComment({ + supertest, + caseId: individualCase2.id, + params: { + alertId: signalID2, + index: defaultSignalsIndex, + rule: { id: 'test-rule-id', name: 'test-index-id' }, + type: CommentType.alert, + owner: 'securitySolutionFixture', + }, + }); + + await es.indices.refresh({ index: defaultSignalsIndex }); + + let signals = await getSignalsWithES({ + es, + indices: defaultSignalsIndex, + ids: [signalID, signalID2], + }); + + // There should be no change in their status since syncing is disabled + expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( + CaseStatuses.open + ); + expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source?.signal.status).to.be( + CaseStatuses.open + ); + + // does NOT updates alert status when the status is updated and syncAlerts=false + const updatedIndWithStatus: CasesResponse = (await setStatus({ + supertest, + cases: [ + { + id: updatedInd1WithComment.id, + version: updatedInd1WithComment.version, + status: CaseStatuses.closed, + }, + { + id: updatedInd2WithComment.id, + version: updatedInd2WithComment.version, + status: CaseStatuses['in-progress'], + }, + ], + type: 'case', + })) as CasesResponse; + + await es.indices.refresh({ index: defaultSignalsIndex }); + + signals = await getSignalsWithES({ + es, + indices: defaultSignalsIndex, + ids: [signalID, signalID2], + }); + + // There should still be no change in their status since syncing is disabled + expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( + CaseStatuses.open + ); + expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source?.signal.status).to.be( + CaseStatuses.open + ); + + // it updates alert status when syncAlerts is turned on + // turn on the sync settings + await updateCase({ + supertest, + params: { + cases: updatedIndWithStatus.map((caseInfo) => ({ + id: caseInfo.id, + version: caseInfo.version, + settings: { syncAlerts: true }, + })), + }, + }); + + await es.indices.refresh({ index: defaultSignalsIndex }); + + signals = await getSignalsWithES({ + es, + indices: defaultSignalsIndex, + ids: [signalID, signalID2], + }); + + // alerts should be updated now that the + expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( + CaseStatuses.closed + ); + expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source?.signal.status).to.be( + CaseStatuses['in-progress'] + ); + }); + }); + + describe('esArchiver', () => { + const defaultSignalsIndex = '.siem-signals-default-000001'; + + beforeEach(async () => { + await esArchiver.load('cases/signals/duplicate_ids'); + }); + afterEach(async () => { + await esArchiver.unload('cases/signals/duplicate_ids'); + await deleteAllCaseItems(es); + }); + + it('should not update the status of duplicate alert ids in separate indices', async () => { + const getSignals = async () => { + return getSignalsWithES({ + es, + indices: [defaultSignalsIndex, signalsIndex2], + ids: [signalIDInFirstIndex, signalIDInSecondIndex], + }); + }; + + // this id exists only in .siem-signals-default-000001 + const signalIDInFirstIndex = + 'cae78067e65582a3b277c1ad46ba3cb29044242fe0d24bbf3fcde757fdd31d1c'; + // This id exists in both .siem-signals-default-000001 and .siem-signals-default-000002 + const signalIDInSecondIndex = 'duplicate-signal-id'; + const signalsIndex2 = '.siem-signals-default-000002'; + + const individualCase = await createCase(supertest, { + ...postCaseReq, + settings: { + syncAlerts: false, + }, + }); + + const updatedIndWithComment = await createComment({ + supertest, + caseId: individualCase.id, + params: { + alertId: signalIDInFirstIndex, + index: defaultSignalsIndex, + rule: { id: 'test-rule-id', name: 'test-index-id' }, + type: CommentType.alert, + owner: 'securitySolutionFixture', + }, + }); + + const updatedIndWithComment2 = await createComment({ + supertest, + caseId: updatedIndWithComment.id, + params: { + alertId: signalIDInSecondIndex, + index: signalsIndex2, + rule: { id: 'test-rule-id', name: 'test-index-id' }, + type: CommentType.alert, + owner: 'securitySolutionFixture', + }, + }); + + await es.indices.refresh({ index: defaultSignalsIndex }); + + let signals = await getSignals(); + // There should be no change in their status since syncing is disabled + expect( + signals.get(defaultSignalsIndex)?.get(signalIDInFirstIndex)?._source?.signal.status + ).to.be(CaseStatuses.open); + expect( + signals.get(signalsIndex2)?.get(signalIDInSecondIndex)?._source?.signal.status + ).to.be(CaseStatuses.open); + + const updatedIndWithStatus: CasesResponse = (await setStatus({ + supertest, + cases: [ + { + id: updatedIndWithComment2.id, + version: updatedIndWithComment2.version, + status: CaseStatuses.closed, + }, + ], + type: 'case', + })) as CasesResponse; + + await es.indices.refresh({ index: defaultSignalsIndex }); + + signals = await getSignals(); + + // There should still be no change in their status since syncing is disabled + expect( + signals.get(defaultSignalsIndex)?.get(signalIDInFirstIndex)?._source?.signal.status + ).to.be(CaseStatuses.open); + expect( + signals.get(signalsIndex2)?.get(signalIDInSecondIndex)?._source?.signal.status + ).to.be(CaseStatuses.open); + + // turn on the sync settings + await updateCase({ + supertest, + params: { + cases: [ + { + id: updatedIndWithStatus[0].id, + version: updatedIndWithStatus[0].version, + settings: { syncAlerts: true }, + }, + ], + }, + }); + await es.indices.refresh({ index: defaultSignalsIndex }); + + signals = await getSignals(); + + // alerts should be updated now that the + expect( + signals.get(defaultSignalsIndex)?.get(signalIDInFirstIndex)?._source?.signal.status + ).to.be(CaseStatuses.closed); + expect( + signals.get(signalsIndex2)?.get(signalIDInSecondIndex)?._source?.signal.status + ).to.be(CaseStatuses.closed); + + // the duplicate signal id in the other index should not be affect (so its status should be open) + expect( + signals.get(defaultSignalsIndex)?.get(signalIDInSecondIndex)?._source?.signal.status + ).to.be(CaseStatuses.open); + }); + }); + + describe('detections rule', () => { + beforeEach(async () => { + await esArchiver.load('auditbeat/hosts'); + await createSignalsIndex(supertest); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest); + await deleteAllAlerts(supertest); + await esArchiver.unload('auditbeat/hosts'); + }); + + it('updates alert status when the status is updated and syncAlerts=true', async () => { + const rule = getRuleForSignalTesting(['auditbeat-*']); + const postedCase = await createCase(supertest, postCaseReq); + + const { id } = await createRule(supertest, rule); + await waitForRuleSuccessOrStatus(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signals = await getSignalsByIds(supertest, [id]); + + const alert = signals.hits.hits[0]; + expect(alert._source.signal.status).eql('open'); + + const caseUpdated = await createComment({ + supertest, + caseId: postedCase.id, + params: { + alertId: alert._id, + index: alert._index, + rule: { + id: 'id', + name: 'name', + }, + type: CommentType.alert, + owner: 'securitySolutionFixture', + }, + }); + + await es.indices.refresh({ index: alert._index }); + await updateCase({ + supertest, + params: { + cases: [ + { + id: caseUpdated.id, + version: caseUpdated.version, + status: CaseStatuses['in-progress'], + }, + ], + }, + }); + + // force a refresh on the index that the signal is stored in so that we can search for it and get the correct + // status + await es.indices.refresh({ index: alert._index }); + + const { body: updatedAlert } = await supertest + .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) + .set('kbn-xsrf', 'true') + .send(getQuerySignalIds([alert._id])) + .expect(200); + + expect(updatedAlert.hits.hits[0]._source.signal.status).eql('in-progress'); + }); + + it('does NOT updates alert status when the status is updated and syncAlerts=false', async () => { + const rule = getRuleForSignalTesting(['auditbeat-*']); + + const postedCase = await createCase(supertest, { + ...postCaseReq, + settings: { syncAlerts: false }, + }); + + const { id } = await createRule(supertest, rule); + await waitForRuleSuccessOrStatus(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signals = await getSignalsByIds(supertest, [id]); + + const alert = signals.hits.hits[0]; + expect(alert._source.signal.status).eql('open'); + + const caseUpdated = await createComment({ + supertest, + caseId: postedCase.id, + params: { + alertId: alert._id, + index: alert._index, + type: CommentType.alert, + rule: { + id: 'id', + name: 'name', + }, + owner: 'securitySolutionFixture', + }, + }); + + await updateCase({ + supertest, + params: { + cases: [ + { + id: caseUpdated.id, + version: caseUpdated.version, + status: CaseStatuses['in-progress'], + }, + ], + }, + }); + + const { body: updatedAlert } = await supertest + .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) + .set('kbn-xsrf', 'true') + .send(getQuerySignalIds([alert._id])) + .expect(200); + + expect(updatedAlert.hits.hits[0]._source.signal.status).eql('open'); + }); + + it('it updates alert status when syncAlerts is turned on', async () => { + const rule = getRuleForSignalTesting(['auditbeat-*']); + + const postedCase = await createCase(supertest, { + ...postCaseReq, + settings: { syncAlerts: false }, + }); + + const { id } = await createRule(supertest, rule); + await waitForRuleSuccessOrStatus(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signals = await getSignalsByIds(supertest, [id]); + + const alert = signals.hits.hits[0]; + expect(alert._source.signal.status).eql('open'); + + const caseUpdated = await createComment({ + supertest, + caseId: postedCase.id, + params: { + alertId: alert._id, + index: alert._index, + rule: { + id: 'id', + name: 'name', + }, + type: CommentType.alert, + owner: 'securitySolutionFixture', + }, + }); + + // Update the status of the case with sync alerts off + const caseStatusUpdated = await updateCase({ + supertest, + params: { + cases: [ + { + id: caseUpdated.id, + version: caseUpdated.version, + status: CaseStatuses['in-progress'], + }, + ], + }, + }); + + // Turn sync alerts on + await updateCase({ + supertest, + params: { + cases: [ + { + id: caseStatusUpdated[0].id, + version: caseStatusUpdated[0].version, + settings: { syncAlerts: true }, + }, + ], + }, + }); + + // refresh the index because syncAlerts was set to true so the alert's status should have been updated + await es.indices.refresh({ index: alert._index }); + + const { body: updatedAlert } = await supertest + .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) + .set('kbn-xsrf', 'true') + .send(getQuerySignalIds([alert._id])) + .expect(200); + + expect(updatedAlert.hits.hits[0]._source.signal.status).eql('in-progress'); + }); + + it('it does NOT updates alert status when syncAlerts is turned off', async () => { + const rule = getRuleForSignalTesting(['auditbeat-*']); + + const postedCase = await createCase(supertest, postCaseReq); + const { id } = await createRule(supertest, rule); + await waitForRuleSuccessOrStatus(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signals = await getSignalsByIds(supertest, [id]); + + const alert = signals.hits.hits[0]; + expect(alert._source.signal.status).eql('open'); + + const caseUpdated = await createComment({ + supertest, + caseId: postedCase.id, + params: { + alertId: alert._id, + index: alert._index, + type: CommentType.alert, + rule: { + id: 'id', + name: 'name', + }, + owner: 'securitySolutionFixture', + }, + }); + + // Turn sync alerts off + const caseSettingsUpdated = await updateCase({ + supertest, + params: { + cases: [ + { + id: caseUpdated.id, + version: caseUpdated.version, + settings: { syncAlerts: false }, + }, + ], + }, + }); + + // Update the status of the case with sync alerts off + await updateCase({ + supertest, + params: { + cases: [ + { + id: caseSettingsUpdated[0].id, + version: caseSettingsUpdated[0].version, + status: CaseStatuses['in-progress'], + }, + ], + }, + }); + + const { body: updatedAlert } = await supertest + .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) + .set('kbn-xsrf', 'true') + .send(getQuerySignalIds([alert._id])) + .expect(200); + + expect(updatedAlert.hits.hits[0]._source.signal.status).eql('open'); + }); + }); + }); + + describe('rbac', () => { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + it('should update a case when the user has the correct permissions', async () => { + const postedCase = await createCase(supertestWithoutAuth, postCaseReq, 200, { + user: secOnly, + space: 'space1', + }); + + const patchedCases = await updateCase({ + supertest: supertestWithoutAuth, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + title: 'new title', + }, + ], + }, + auth: { user: secOnly, space: 'space1' }, + }); + + expect(patchedCases[0].owner).to.eql('securitySolutionFixture'); + }); + + it('should update multiple cases when the user has the correct permissions', async () => { + const [case1, case2, case3] = await Promise.all([ + createCase(supertestWithoutAuth, postCaseReq, 200, { + user: superUser, + space: 'space1', + }), + createCase(supertestWithoutAuth, postCaseReq, 200, { + user: superUser, + space: 'space1', + }), + createCase(supertestWithoutAuth, postCaseReq, 200, { + user: superUser, + space: 'space1', + }), + ]); + + const patchedCases = await updateCase({ + supertest: supertestWithoutAuth, + params: { + cases: [ + { + id: case1.id, + version: case1.version, + title: 'new title', + }, + { + id: case2.id, + version: case2.version, + title: 'new title', + }, + { + id: case3.id, + version: case3.version, + title: 'new title', + }, + ], + }, + auth: { user: secOnly, space: 'space1' }, + }); + + expect(patchedCases[0].owner).to.eql('securitySolutionFixture'); + expect(patchedCases[1].owner).to.eql('securitySolutionFixture'); + expect(patchedCases[2].owner).to.eql('securitySolutionFixture'); + }); + + it('should not update a case when the user does not have the correct ownership', async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + { user: obsOnly, space: 'space1' } + ); + + await updateCase({ + supertest: supertestWithoutAuth, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + title: 'new title', + }, + ], + }, + auth: { user: secOnly, space: 'space1' }, + expectedHttpCode: 403, + }); + }); + + it('should not update any cases when the user does not have the correct ownership', async () => { + const [case1, case2, case3] = await Promise.all([ + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + { + user: superUser, + space: 'space1', + } + ), + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + { + user: superUser, + space: 'space1', + } + ), + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + { + user: superUser, + space: 'space1', + } + ), + ]); + + await updateCase({ + supertest: supertestWithoutAuth, + params: { + cases: [ + { + id: case1.id, + version: case1.version, + title: 'new title', + }, + { + id: case2.id, + version: case2.version, + title: 'new title', + }, + { + id: case3.id, + version: case3.version, + title: 'new title', + }, + ], + }, + auth: { user: secOnly, space: 'space1' }, + expectedHttpCode: 403, + }); + + const resp = await findCases({ supertest, auth: superUserSpace1Auth }); + expect(resp.cases.length).to.eql(3); + // the update should have failed and none of the title should have been changed + expect(resp.cases[0].title).to.eql(postCaseReq.title); + expect(resp.cases[1].title).to.eql(postCaseReq.title); + expect(resp.cases[2].title).to.eql(postCaseReq.title); + }); + + for (const user of [globalRead, secOnlyRead, obsOnlyRead, obsSecRead, noKibanaPrivileges]) { + it(`User ${ + user.username + } with role(s) ${user.roles.join()} - should NOT update a case`, async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { + user: superUser, + space: 'space1', + } + ); + + await updateCase({ + supertest: supertestWithoutAuth, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + title: 'new title', + }, + ], + }, + auth: { user, space: 'space1' }, + expectedHttpCode: 403, + }); + }); + } + + it('should NOT create a case in a space with no permissions', async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { + user: superUser, + space: 'space2', + } + ); + + await updateCase({ + supertest: supertestWithoutAuth, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + title: 'new title', + }, + ], + }, + auth: { user: secOnly, space: 'space2' }, + expectedHttpCode: 403, + }); + }); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/security_only/tests/common/cases/post_case.ts b/x-pack/test/case_api_integration/security_only/tests/common/cases/post_case.ts new file mode 100644 index 00000000000000..50294201f6fbee --- /dev/null +++ b/x-pack/test/case_api_integration/security_only/tests/common/cases/post_case.ts @@ -0,0 +1,292 @@ +/* + * 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. + */ + +/* eslint-disable @typescript-eslint/naming-convention */ + +import expect from '@kbn/expect'; + +import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; +import { + ConnectorTypes, + ConnectorJiraTypeFields, + CaseStatuses, + CaseUserActionResponse, +} from '../../../../../../plugins/cases/common/api'; +import { getPostCaseRequest, postCaseResp, defaultUser } from '../../../../common/lib/mock'; +import { + deleteCasesByESQuery, + createCase, + removeServerGeneratedPropertiesFromCase, + removeServerGeneratedPropertiesFromUserAction, + getCaseUserActions, +} from '../../../../common/lib/utils'; +import { + secOnly, + secOnlyRead, + globalRead, + obsOnlyRead, + obsSecRead, + noKibanaPrivileges, +} from '../../../../common/lib/authentication/users'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + describe('post_case', () => { + afterEach(async () => { + await deleteCasesByESQuery(es); + }); + + describe('happy path', () => { + it('should post a case', async () => { + const postedCase = await createCase( + supertest, + getPostCaseRequest({ + connector: { + id: '123', + name: 'Jira', + type: ConnectorTypes.jira, + fields: { issueType: 'Task', priority: 'High', parent: null }, + }, + }) + ); + const data = removeServerGeneratedPropertiesFromCase(postedCase); + + expect(data).to.eql( + postCaseResp( + null, + getPostCaseRequest({ + connector: { + id: '123', + name: 'Jira', + type: ConnectorTypes.jira, + fields: { issueType: 'Task', priority: 'High', parent: null }, + }, + }) + ) + ); + }); + + it('should post a case: none connector', async () => { + const postedCase = await createCase( + supertest, + getPostCaseRequest({ + connector: { + id: 'none', + name: 'none', + type: ConnectorTypes.none, + fields: null, + }, + }) + ); + const data = removeServerGeneratedPropertiesFromCase(postedCase); + + expect(data).to.eql( + postCaseResp( + null, + getPostCaseRequest({ + connector: { + id: 'none', + name: 'none', + type: ConnectorTypes.none, + fields: null, + }, + }) + ) + ); + }); + + it('should create a user action when creating a case', async () => { + const postedCase = await createCase(supertest, getPostCaseRequest()); + const userActions = await getCaseUserActions({ supertest, caseID: postedCase.id }); + const creationUserAction = removeServerGeneratedPropertiesFromUserAction(userActions[0]); + + const { new_value, ...rest } = creationUserAction as CaseUserActionResponse; + const parsedNewValue = JSON.parse(new_value!); + + expect(rest).to.eql({ + action_field: [ + 'description', + 'status', + 'tags', + 'title', + 'connector', + 'settings', + 'owner', + ], + action: 'create', + action_by: defaultUser, + old_value: null, + case_id: `${postedCase.id}`, + comment_id: null, + sub_case_id: '', + owner: 'securitySolutionFixture', + }); + + expect(parsedNewValue).to.eql({ + type: postedCase.type, + description: postedCase.description, + title: postedCase.title, + tags: postedCase.tags, + connector: postedCase.connector, + settings: postedCase.settings, + owner: postedCase.owner, + }); + }); + + it('creates the case without connector in the configuration', async () => { + const postedCase = await createCase(supertest, getPostCaseRequest()); + const data = removeServerGeneratedPropertiesFromCase(postedCase); + + expect(data).to.eql(postCaseResp()); + }); + }); + + describe('unhappy path', () => { + it('400s when bad query supplied', async () => { + await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + // @ts-expect-error + .send({ ...getPostCaseRequest({ badKey: true }) }) + .expect(400); + }); + + it('400s when connector is not supplied', async () => { + const { connector, ...caseWithoutConnector } = getPostCaseRequest(); + + await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(caseWithoutConnector) + .expect(400); + }); + + it('400s when connector has wrong type', async () => { + await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send({ + ...getPostCaseRequest({ + // @ts-expect-error + connector: { id: 'wrong', name: 'wrong', type: '.not-exists', fields: null }, + }), + }) + .expect(400); + }); + + it('400s when connector has wrong fields', async () => { + await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send({ + ...getPostCaseRequest({ + // @ts-expect-error + connector: { + id: 'wrong', + name: 'wrong', + type: ConnectorTypes.jira, + fields: { unsupported: 'value' }, + } as ConnectorJiraTypeFields, + }), + }) + .expect(400); + }); + + it('400s when missing title', async () => { + const { title, ...caseWithoutTitle } = getPostCaseRequest(); + + await supertest.post(CASES_URL).set('kbn-xsrf', 'true').send(caseWithoutTitle).expect(400); + }); + + it('400s when missing description', async () => { + const { description, ...caseWithoutDescription } = getPostCaseRequest(); + + await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(caseWithoutDescription) + .expect(400); + }); + + it('400s when missing tags', async () => { + const { tags, ...caseWithoutTags } = getPostCaseRequest(); + + await supertest.post(CASES_URL).set('kbn-xsrf', 'true').send(caseWithoutTags).expect(400); + }); + + it('400s if you passing status for a new case', async () => { + const req = getPostCaseRequest(); + + await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send({ ...req, status: CaseStatuses.open }) + .expect(400); + }); + }); + + describe('rbac', () => { + it('User: security solution only - should create a case', async () => { + const theCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { + user: secOnly, + space: 'space1', + } + ); + expect(theCase.owner).to.eql('securitySolutionFixture'); + }); + + it('User: security solution only - should NOT create a case of different owner', async () => { + await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 403, + { + user: secOnly, + space: 'space1', + } + ); + }); + + for (const user of [globalRead, secOnlyRead, obsOnlyRead, obsSecRead, noKibanaPrivileges]) { + it(`User ${ + user.username + } with role(s) ${user.roles.join()} - should NOT create a case`, async () => { + await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 403, + { + user, + space: 'space1', + } + ); + }); + } + + it('should NOT create a case in a space with no permissions', async () => { + await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 403, + { + user: secOnly, + space: 'space2', + } + ); + }); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/security_only/tests/common/cases/reporters/get_reporters.ts b/x-pack/test/case_api_integration/security_only/tests/common/cases/reporters/get_reporters.ts new file mode 100644 index 00000000000000..e34d9ccad39ac6 --- /dev/null +++ b/x-pack/test/case_api_integration/security_only/tests/common/cases/reporters/get_reporters.ts @@ -0,0 +1,201 @@ +/* + * 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 { FtrProviderContext } from '../../../../../common/ftr_provider_context'; + +import { defaultUser, getPostCaseRequest } from '../../../../../common/lib/mock'; +import { createCase, deleteCasesByESQuery, getReporters } from '../../../../../common/lib/utils'; +import { + secOnly, + obsOnly, + globalRead, + superUser, + secOnlyRead, + obsOnlyRead, + obsSecRead, + noKibanaPrivileges, + obsSec, +} from '../../../../../common/lib/authentication/users'; +import { getUserInfo } from '../../../../../common/lib/authentication'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const es = getService('es'); + + describe('get_reporters', () => { + afterEach(async () => { + await deleteCasesByESQuery(es); + }); + + it('should return reporters', async () => { + await createCase(supertest, getPostCaseRequest()); + const reporters = await getReporters({ supertest: supertestWithoutAuth }); + + expect(reporters).to.eql([defaultUser]); + }); + + it('should return unique reporters', async () => { + await createCase(supertest, getPostCaseRequest()); + await createCase(supertest, getPostCaseRequest()); + const reporters = await getReporters({ supertest: supertestWithoutAuth }); + + expect(reporters).to.eql([defaultUser]); + }); + + describe('rbac', () => { + it('User: security solution only - should read the correct reporters', async () => { + await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { + user: secOnly, + space: 'space1', + } + ); + + await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + { + user: obsOnly, + space: 'space1', + } + ); + + for (const scenario of [ + { + user: globalRead, + expectedReporters: [getUserInfo(secOnly), getUserInfo(obsOnly)], + }, + { + user: superUser, + expectedReporters: [getUserInfo(secOnly), getUserInfo(obsOnly)], + }, + { user: secOnlyRead, expectedReporters: [getUserInfo(secOnly)] }, + { user: obsOnlyRead, expectedReporters: [getUserInfo(obsOnly)] }, + { + user: obsSecRead, + expectedReporters: [getUserInfo(secOnly), getUserInfo(obsOnly)], + }, + ]) { + const reporters = await getReporters({ + supertest: supertestWithoutAuth, + expectedHttpCode: 200, + auth: { + user: scenario.user, + space: 'space1', + }, + }); + + expect(reporters).to.eql(scenario.expectedReporters); + } + }); + + for (const scenario of [ + { user: noKibanaPrivileges, space: 'space1' }, + { user: secOnly, space: 'space2' }, + ]) { + it(`User ${scenario.user.username} with role(s) ${scenario.user.roles.join()} and space ${ + scenario.space + } - should NOT get all reporters`, async () => { + // super user creates a case at the appropriate space + await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { + user: superUser, + space: scenario.space, + } + ); + + // user should not be able to get all reporters at the appropriate space + await getReporters({ + supertest: supertestWithoutAuth, + expectedHttpCode: 403, + auth: { user: scenario.user, space: scenario.space }, + }); + }); + } + + it('should respect the owner filter when having permissions', async () => { + await Promise.all([ + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { + user: secOnly, + space: 'space1', + } + ), + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + { + user: obsOnly, + space: 'space1', + } + ), + ]); + + const reporters = await getReporters({ + supertest: supertestWithoutAuth, + auth: { + user: obsSec, + space: 'space1', + }, + query: { owner: 'securitySolutionFixture' }, + }); + + expect(reporters).to.eql([getUserInfo(secOnly)]); + }); + + it('should return the correct cases when trying to exploit RBAC through the owner query parameter', async () => { + await Promise.all([ + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { + user: secOnly, + space: 'space1', + } + ), + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + { + user: obsOnly, + space: 'space1', + } + ), + ]); + + // User with permissions only to security solution request reporters from observability + const reporters = await getReporters({ + supertest: supertestWithoutAuth, + auth: { + user: secOnly, + space: 'space1', + }, + query: { owner: ['securitySolutionFixture', 'observabilityFixture'] }, + }); + + // Only security solution reporters are being returned + expect(reporters).to.eql([getUserInfo(secOnly)]); + }); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/security_only/tests/common/cases/status/get_status.ts b/x-pack/test/case_api_integration/security_only/tests/common/cases/status/get_status.ts new file mode 100644 index 00000000000000..7a17cf1dd8e081 --- /dev/null +++ b/x-pack/test/case_api_integration/security_only/tests/common/cases/status/get_status.ts @@ -0,0 +1,172 @@ +/* + * 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 { FtrProviderContext } from '../../../../../common/ftr_provider_context'; + +import { CaseStatuses } from '../../../../../../../plugins/cases/common/api'; +import { getPostCaseRequest, postCaseReq } from '../../../../../common/lib/mock'; +import { + createCase, + updateCase, + getAllCasesStatuses, + deleteAllCaseItems, + superUserSpace1Auth, +} from '../../../../../common/lib/utils'; +import { + globalRead, + noKibanaPrivileges, + obsOnlyRead, + obsSecRead, + secOnly, + secOnlyRead, + 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'); + + describe('get_status', () => { + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + it('should return case statuses', async () => { + const [, inProgressCase, postedCase] = await Promise.all([ + createCase(supertest, postCaseReq), + createCase(supertest, postCaseReq), + createCase(supertest, postCaseReq), + ]); + + await updateCase({ + supertest, + params: { + cases: [ + { + id: inProgressCase.id, + version: inProgressCase.version, + status: CaseStatuses['in-progress'], + }, + { + id: postedCase.id, + version: postedCase.version, + status: CaseStatuses.closed, + }, + ], + }, + }); + + const statuses = await getAllCasesStatuses({ supertest }); + + expect(statuses).to.eql({ + count_open_cases: 1, + count_closed_cases: 1, + count_in_progress_cases: 1, + }); + }); + + describe('rbac', () => { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + it('should return the correct status stats', async () => { + /** + * Owner: Sec + * open: 0, in-prog: 1, closed: 1 + * Owner: Obs + * open: 1, in-prog: 1 + */ + const [inProgressSec, closedSec, , inProgressObs] = await Promise.all([ + createCase(supertestWithoutAuth, getPostCaseRequest(), 200, { + user: superUser, + space: 'space1', + }), + createCase(supertestWithoutAuth, getPostCaseRequest(), 200, { + user: superUser, + space: 'space1', + }), + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + superUserSpace1Auth + ), + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + superUserSpace1Auth + ), + ]); + + await updateCase({ + supertest: supertestWithoutAuth, + params: { + cases: [ + { + id: inProgressSec.id, + version: inProgressSec.version, + status: CaseStatuses['in-progress'], + }, + { + id: closedSec.id, + version: closedSec.version, + status: CaseStatuses.closed, + }, + { + id: inProgressObs.id, + version: inProgressObs.version, + status: CaseStatuses['in-progress'], + }, + ], + }, + auth: superUserSpace1Auth, + }); + + for (const scenario of [ + { user: globalRead, stats: { open: 1, inProgress: 2, closed: 1 } }, + { user: superUser, stats: { open: 1, inProgress: 2, closed: 1 } }, + { user: secOnlyRead, stats: { open: 0, inProgress: 1, closed: 1 } }, + { user: obsOnlyRead, stats: { open: 1, inProgress: 1, closed: 0 } }, + { user: obsSecRead, stats: { open: 1, inProgress: 2, closed: 1 } }, + ]) { + const statuses = await getAllCasesStatuses({ + supertest: supertestWithoutAuth, + auth: { user: scenario.user, space: 'space1' }, + }); + + expect(statuses).to.eql({ + count_open_cases: scenario.stats.open, + count_closed_cases: scenario.stats.closed, + count_in_progress_cases: scenario.stats.inProgress, + }); + } + }); + + for (const scenario of [ + { user: noKibanaPrivileges, space: 'space1' }, + { user: secOnly, space: 'space2' }, + ]) { + it(`should return a 403 when retrieving the statuses when the user ${ + scenario.user.username + } with role(s) ${scenario.user.roles.join()} and space ${scenario.space}`, async () => { + await createCase(supertestWithoutAuth, getPostCaseRequest(), 200, { + user: superUser, + space: scenario.space, + }); + + await getAllCasesStatuses({ + supertest: supertestWithoutAuth, + auth: { user: scenario.user, space: scenario.space }, + expectedHttpCode: 403, + }); + }); + } + }); + }); +}; diff --git a/x-pack/test/case_api_integration/security_only/tests/common/cases/tags/get_tags.ts b/x-pack/test/case_api_integration/security_only/tests/common/cases/tags/get_tags.ts new file mode 100644 index 00000000000000..0c7237683666fd --- /dev/null +++ b/x-pack/test/case_api_integration/security_only/tests/common/cases/tags/get_tags.ts @@ -0,0 +1,201 @@ +/* + * 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 { FtrProviderContext } from '../../../../../common/ftr_provider_context'; + +import { deleteCasesByESQuery, createCase, getTags } from '../../../../../common/lib/utils'; +import { getPostCaseRequest } from '../../../../../common/lib/mock'; +import { + secOnly, + obsOnly, + globalRead, + superUser, + secOnlyRead, + obsOnlyRead, + obsSecRead, + noKibanaPrivileges, + obsSec, +} from '../../../../../common/lib/authentication/users'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const es = getService('es'); + + describe('get_tags', () => { + afterEach(async () => { + await deleteCasesByESQuery(es); + }); + + it('should return case tags', async () => { + await createCase(supertest, getPostCaseRequest()); + await createCase(supertest, getPostCaseRequest({ tags: ['unique'] })); + + const tags = await getTags({ supertest }); + expect(tags).to.eql(['defacement', 'unique']); + }); + + it('should return unique tags', async () => { + await createCase(supertest, getPostCaseRequest()); + await createCase(supertest, getPostCaseRequest()); + + const tags = await getTags({ supertest }); + expect(tags).to.eql(['defacement']); + }); + + describe('rbac', () => { + it('should read the correct tags', async () => { + await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture', tags: ['sec'] }), + 200, + { + user: secOnly, + space: 'space1', + } + ); + + await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture', tags: ['obs'] }), + 200, + { + user: obsOnly, + space: 'space1', + } + ); + + for (const scenario of [ + { + user: globalRead, + expectedTags: ['sec', 'obs'], + }, + { + user: superUser, + expectedTags: ['sec', 'obs'], + }, + { user: secOnlyRead, expectedTags: ['sec'] }, + { user: obsOnlyRead, expectedTags: ['obs'] }, + { + user: obsSecRead, + expectedTags: ['sec', 'obs'], + }, + ]) { + const tags = await getTags({ + supertest: supertestWithoutAuth, + expectedHttpCode: 200, + auth: { + user: scenario.user, + space: 'space1', + }, + }); + + expect(tags).to.eql(scenario.expectedTags); + } + }); + + for (const scenario of [ + { user: noKibanaPrivileges, space: 'space1' }, + { user: secOnly, space: 'space2' }, + ]) { + it(`User ${scenario.user.username} with role(s) ${scenario.user.roles.join()} and space ${ + scenario.space + } - should NOT get all tags`, async () => { + // super user creates a case at the appropriate space + await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture', tags: ['sec'] }), + 200, + { + user: superUser, + space: scenario.space, + } + ); + + // user should not be able to get all tags at the appropriate space + await getTags({ + supertest: supertestWithoutAuth, + expectedHttpCode: 403, + auth: { user: scenario.user, space: scenario.space }, + }); + }); + } + + it('should respect the owner filter when having permissions', async () => { + await Promise.all([ + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture', tags: ['sec'] }), + 200, + { + user: obsSec, + space: 'space1', + } + ), + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture', tags: ['obs'] }), + 200, + { + user: obsSec, + space: 'space1', + } + ), + ]); + + const tags = await getTags({ + supertest: supertestWithoutAuth, + auth: { + user: obsSec, + space: 'space1', + }, + query: { owner: 'securitySolutionFixture' }, + }); + + expect(tags).to.eql(['sec']); + }); + + it('should return the correct cases when trying to exploit RBAC through the owner query parameter', async () => { + await Promise.all([ + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture', tags: ['sec'] }), + 200, + { + user: obsSec, + space: 'space1', + } + ), + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture', tags: ['obs'] }), + 200, + { + user: obsSec, + space: 'space1', + } + ), + ]); + + // User with permissions only to security solution request tags from observability + const tags = await getTags({ + supertest: supertestWithoutAuth, + auth: { + user: secOnly, + space: 'space1', + }, + query: { owner: ['securitySolutionFixture', 'observabilityFixture'] }, + }); + + // Only security solution tags are being returned + expect(tags).to.eql(['sec']); + }); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/security_only/tests/common/comments/delete_comment.ts b/x-pack/test/case_api_integration/security_only/tests/common/comments/delete_comment.ts new file mode 100644 index 00000000000000..b7b97557dcd250 --- /dev/null +++ b/x-pack/test/case_api_integration/security_only/tests/common/comments/delete_comment.ts @@ -0,0 +1,372 @@ +/* + * 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 { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; +import { getPostCaseRequest, postCaseReq, postCommentUserReq } from '../../../../common/lib/mock'; +import { + createCaseAction, + createSubCase, + deleteAllCaseItems, + deleteCaseAction, + deleteCasesByESQuery, + deleteCasesUserActions, + deleteComments, + createCase, + createComment, + deleteComment, + deleteAllComments, + superUserSpace1Auth, +} from '../../../../common/lib/utils'; +import { + globalRead, + noKibanaPrivileges, + obsOnly, + obsOnlyRead, + obsSecRead, + secOnly, + secOnlyRead, + 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'); + + describe('delete_comment', () => { + afterEach(async () => { + await deleteCasesByESQuery(es); + await deleteComments(es); + await deleteCasesUserActions(es); + }); + + describe('happy path', () => { + it('should delete a comment', async () => { + const postedCase = await createCase(supertest, postCaseReq); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + }); + const comment = await deleteComment({ + supertest, + caseId: postedCase.id, + commentId: patchedCase.comments![0].id, + }); + + expect(comment).to.eql({}); + }); + }); + + describe('unhappy path', () => { + it('404s when comment belongs to different case', async () => { + const postedCase = await createCase(supertest, postCaseReq); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + }); + const error = (await deleteComment({ + supertest, + caseId: 'fake-id', + commentId: patchedCase.comments![0].id, + expectedHttpCode: 404, + })) as Error; + + expect(error.message).to.be( + `This comment ${patchedCase.comments![0].id} does not exist in fake-id.` + ); + }); + + it('404s when comment is not there', async () => { + await deleteComment({ + supertest, + caseId: 'fake-id', + commentId: 'fake-id', + expectedHttpCode: 404, + }); + }); + + it('should return a 400 when attempting to delete all comments when passing the `subCaseId` parameter', async () => { + const { body } = await supertest + .delete(`${CASES_URL}/case-id/comments?subCaseId=value`) + .set('kbn-xsrf', 'true') + .send() + .expect(400); + // make sure the failure is because of the subCaseId + expect(body.message).to.contain('disabled'); + }); + + it('should return a 400 when attempting to delete a single comment when passing the `subCaseId` parameter', async () => { + const { body } = await supertest + .delete(`${CASES_URL}/case-id/comments/comment-id?subCaseId=value`) + .set('kbn-xsrf', 'true') + .send() + .expect(400); + // make sure the failure is because of the subCaseId + expect(body.message).to.contain('disabled'); + }); + }); + + // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests + describe.skip('sub case comments', () => { + let actionID: string; + before(async () => { + actionID = await createCaseAction(supertest); + }); + after(async () => { + await deleteCaseAction(supertest, actionID); + }); + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + it('deletes a comment from a sub case', async () => { + const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); + await supertest + .delete( + `${CASES_URL}/${caseInfo.id}/comments/${caseInfo.comments![0].id}?subCaseId=${ + caseInfo.subCases![0].id + }` + ) + .set('kbn-xsrf', 'true') + .send() + .expect(204); + + const { body } = await supertest.get( + `${CASES_URL}/${caseInfo.id}/comments?subCaseId=${caseInfo.subCases![0].id}` + ); + + expect(body.length).to.eql(0); + }); + + it('deletes all comments from a sub case', async () => { + const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); + await supertest + .post(`${CASES_URL}/${caseInfo.id}/comments?subCaseId=${caseInfo.subCases![0].id}`) + .set('kbn-xsrf', 'true') + .send(postCommentUserReq) + .expect(200); + + let { body: allComments } = await supertest.get( + `${CASES_URL}/${caseInfo.id}/comments?subCaseId=${caseInfo.subCases![0].id}` + ); + expect(allComments.length).to.eql(2); + + await supertest + .delete(`${CASES_URL}/${caseInfo.id}/comments?subCaseId=${caseInfo.subCases![0].id}`) + .set('kbn-xsrf', 'true') + .send() + .expect(204); + + ({ body: allComments } = await supertest.get( + `${CASES_URL}/${caseInfo.id}/comments?subCaseId=${caseInfo.subCases![0].id}` + )); + + // no comments for the sub case + expect(allComments.length).to.eql(0); + + ({ body: allComments } = await supertest.get(`${CASES_URL}/${caseInfo.id}/comments`)); + + // no comments for the collection + expect(allComments.length).to.eql(0); + }); + }); + + describe('rbac', () => { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + it('should delete a comment from the appropriate owner', async () => { + const secCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { user: secOnly, space: 'space1' } + ); + + const commentResp = await createComment({ + supertest: supertestWithoutAuth, + caseId: secCase.id, + params: postCommentUserReq, + auth: { user: secOnly, space: 'space1' }, + }); + + await deleteComment({ + supertest: supertestWithoutAuth, + caseId: secCase.id, + commentId: commentResp.comments![0].id, + auth: { user: secOnly, space: 'space1' }, + }); + }); + + 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' } + ); + + const commentResp = await createComment({ + supertest: supertestWithoutAuth, + caseId: secCase.id, + params: postCommentUserReq, + auth: { user: secOnly, space: 'space1' }, + }); + + await deleteComment({ + supertest: supertestWithoutAuth, + caseId: secCase.id, + commentId: commentResp.comments![0].id, + 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]) { + it(`User ${ + user.username + } with role(s) ${user.roles.join()} - should NOT delete a comment`, async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + superUserSpace1Auth + ); + + const commentResp = await createComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + params: postCommentUserReq, + auth: superUserSpace1Auth, + }); + + await deleteComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + commentId: commentResp.comments![0].id, + auth: { user, space: 'space1' }, + expectedHttpCode: 403, + }); + + await deleteAllComments({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + auth: { user, space: 'space1' }, + expectedHttpCode: 403, + }); + }); + } + + it('should not delete a comment with no kibana privileges', async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + superUserSpace1Auth + ); + + const commentResp = await createComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + params: postCommentUserReq, + auth: superUserSpace1Auth, + }); + + await deleteComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + commentId: commentResp.comments![0].id, + auth: { user: noKibanaPrivileges, space: 'space1' }, + expectedHttpCode: 403, + }); + + await deleteAllComments({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + auth: { user: noKibanaPrivileges, space: 'space1' }, + // the find in the delete all will return no results + expectedHttpCode: 404, + }); + }); + + 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' } + ); + + const commentResp = await createComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + params: postCommentUserReq, + auth: { user: superUser, space: 'space2' }, + }); + + await deleteComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + commentId: commentResp.comments![0].id, + auth: { user: secOnly, space: 'space1' }, + expectedHttpCode: 404, + }); + + await deleteAllComments({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + auth: { user: secOnly, space: 'space1' }, + expectedHttpCode: 404, + }); + }); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/security_only/tests/common/comments/find_comments.ts b/x-pack/test/case_api_integration/security_only/tests/common/comments/find_comments.ts new file mode 100644 index 00000000000000..2ec99d039dd000 --- /dev/null +++ b/x-pack/test/case_api_integration/security_only/tests/common/comments/find_comments.ts @@ -0,0 +1,393 @@ +/* + * 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 { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; +import { CommentsResponse, CommentType } from '../../../../../../plugins/cases/common/api'; +import { + getPostCaseRequest, + postCaseReq, + postCommentAlertReq, + postCommentUserReq, +} from '../../../../common/lib/mock'; +import { + createCaseAction, + createComment, + createSubCase, + deleteAllCaseItems, + deleteCaseAction, + deleteCasesByESQuery, + deleteCasesUserActions, + deleteComments, + ensureSavedObjectIsAuthorized, + getSpaceUrlPrefix, + createCase, + superUserSpace1Auth, +} from '../../../../common/lib/utils'; + +import { + obsOnly, + secOnly, + obsOnlyRead, + secOnlyRead, + noKibanaPrivileges, + superUser, + globalRead, + obsSecRead, +} 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'); + + describe('find_comments', () => { + afterEach(async () => { + await deleteCasesByESQuery(es); + await deleteComments(es); + await deleteCasesUserActions(es); + }); + + it('should find all case comment', async () => { + const { body: postedCase } = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCaseReq) + .expect(200); + + // post 2 comments + await supertest + .post(`${CASES_URL}/${postedCase.id}/comments`) + .set('kbn-xsrf', 'true') + .send(postCommentUserReq) + .expect(200); + + const { body: patchedCase } = await supertest + .post(`${CASES_URL}/${postedCase.id}/comments`) + .set('kbn-xsrf', 'true') + .send(postCommentUserReq) + .expect(200); + + const { body: caseComments } = await supertest + .get(`${CASES_URL}/${postedCase.id}/comments/_find`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(caseComments.comments).to.eql(patchedCase.comments); + }); + + it('should filter case comments', async () => { + const { body: postedCase } = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCaseReq) + .expect(200); + + // post 2 comments + await supertest + .post(`${CASES_URL}/${postedCase.id}/comments`) + .set('kbn-xsrf', 'true') + .send(postCommentUserReq) + .expect(200); + + const { body: patchedCase } = await supertest + .post(`${CASES_URL}/${postedCase.id}/comments`) + .set('kbn-xsrf', 'true') + .send({ ...postCommentUserReq, comment: 'unique' }) + .expect(200); + + const { body: caseComments } = await supertest + .get(`${CASES_URL}/${postedCase.id}/comments/_find?search=unique`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(caseComments.comments).to.eql([patchedCase.comments[1]]); + }); + + it('unhappy path - 400s when query is bad', async () => { + const { body: postedCase } = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCaseReq) + .expect(200); + + await supertest + .post(`${CASES_URL}/${postedCase.id}/comments`) + .set('kbn-xsrf', 'true') + .send(postCommentUserReq) + .expect(200); + + await supertest + .get(`${CASES_URL}/${postedCase.id}/comments/_find?perPage=true`) + .set('kbn-xsrf', 'true') + .send() + .expect(400); + }); + + it('should return a 400 when passing the subCaseId parameter', async () => { + const { body } = await supertest + .get(`${CASES_URL}/case-id/comments/_find?search=unique&subCaseId=value`) + .set('kbn-xsrf', 'true') + .send() + .expect(400); + + expect(body.message).to.contain('disabled'); + }); + + // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests + describe.skip('sub case comments', () => { + let actionID: string; + before(async () => { + actionID = await createCaseAction(supertest); + }); + after(async () => { + await deleteCaseAction(supertest, actionID); + }); + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + it('finds comments for a sub case', async () => { + const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); + await supertest + .post(`${CASES_URL}/${caseInfo.id}/comments?subCaseId=${caseInfo.subCases![0].id}`) + .set('kbn-xsrf', 'true') + .send(postCommentUserReq) + .expect(200); + + const { body: subCaseComments }: { body: CommentsResponse } = await supertest + .get(`${CASES_URL}/${caseInfo.id}/comments/_find?subCaseId=${caseInfo.subCases![0].id}`) + .send() + .expect(200); + expect(subCaseComments.total).to.be(2); + expect(subCaseComments.comments[0].type).to.be(CommentType.generatedAlert); + expect(subCaseComments.comments[1].type).to.be(CommentType.user); + }); + }); + + describe('rbac', () => { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + it('should return the correct comments', async () => { + const space1 = 'space1'; + + const [secCase, obsCase] = await Promise.all([ + // Create case owned by the security solution user + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { user: secOnly, space: space1 } + ), + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + { user: obsOnly, space: space1 } + ), + // Create case owned by the observability user + ]); + + await Promise.all([ + createComment({ + supertest: supertestWithoutAuth, + caseId: secCase.id, + params: postCommentUserReq, + auth: { user: secOnly, space: space1 }, + }), + createComment({ + supertest: supertestWithoutAuth, + caseId: obsCase.id, + params: { ...postCommentAlertReq, owner: 'observabilityFixture' }, + auth: { user: obsOnly, space: space1 }, + }), + ]); + + for (const scenario of [ + { + user: globalRead, + numExpectedEntites: 1, + owners: ['securitySolutionFixture', 'observabilityFixture'], + caseID: secCase.id, + }, + { + user: globalRead, + numExpectedEntites: 1, + owners: ['securitySolutionFixture', 'observabilityFixture'], + caseID: obsCase.id, + }, + { + user: superUser, + numExpectedEntites: 1, + owners: ['securitySolutionFixture', 'observabilityFixture'], + caseID: secCase.id, + }, + { + user: superUser, + numExpectedEntites: 1, + owners: ['securitySolutionFixture', 'observabilityFixture'], + caseID: obsCase.id, + }, + { + user: secOnlyRead, + numExpectedEntites: 1, + owners: ['securitySolutionFixture'], + caseID: secCase.id, + }, + { + user: obsOnlyRead, + numExpectedEntites: 1, + owners: ['observabilityFixture'], + caseID: obsCase.id, + }, + { + user: obsSecRead, + numExpectedEntites: 1, + owners: ['securitySolutionFixture', 'observabilityFixture'], + caseID: secCase.id, + }, + { + user: obsSecRead, + numExpectedEntites: 1, + owners: ['securitySolutionFixture', 'observabilityFixture'], + caseID: obsCase.id, + }, + ]) { + const { body: caseComments }: { body: CommentsResponse } = await supertestWithoutAuth + .get(`${getSpaceUrlPrefix(space1)}${CASES_URL}/${scenario.caseID}/comments/_find`) + .auth(scenario.user.username, scenario.user.password) + .expect(200); + + ensureSavedObjectIsAuthorized( + caseComments.comments, + scenario.numExpectedEntites, + scenario.owners + ); + } + }); + + for (const scenario of [ + { user: noKibanaPrivileges, space: 'space1' }, + { user: secOnly, space: 'space2' }, + ]) { + it(`User ${scenario.user.username} with role(s) ${scenario.user.roles.join()} and space ${ + scenario.space + } - should NOT read a comment`, async () => { + // super user creates a case and comment in the appropriate space + const caseInfo = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { user: superUser, space: scenario.space } + ); + + await createComment({ + supertest: supertestWithoutAuth, + auth: { user: superUser, space: scenario.space }, + params: { ...postCommentUserReq, owner: 'securitySolutionFixture' }, + caseId: caseInfo.id, + }); + + // user should not be able to read comments + await supertestWithoutAuth + .get(`${getSpaceUrlPrefix(scenario.space)}${CASES_URL}/${caseInfo.id}/comments/_find`) + .auth(scenario.user.username, scenario.user.password) + .expect(403); + }); + } + + it('should not return any comments when trying to exploit RBAC through the search query parameter', async () => { + const obsCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + superUserSpace1Auth + ); + + await createComment({ + supertest: supertestWithoutAuth, + auth: superUserSpace1Auth, + params: { ...postCommentUserReq, owner: 'observabilityFixture' }, + caseId: obsCase.id, + }); + + const { body: res }: { body: CommentsResponse } = await supertestWithoutAuth + .get( + `${getSpaceUrlPrefix('space1')}${CASES_URL}/${ + obsCase.id + }/comments/_find?search=securitySolutionFixture+observabilityFixture` + ) + .auth(secOnly.username, secOnly.password) + .expect(200); + + // shouldn't find any comments since they were created under the observability ownership + ensureSavedObjectIsAuthorized(res.comments, 0, ['securitySolutionFixture']); + }); + + it('should not allow retrieving unauthorized comments using the filter field', async () => { + const obsCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + superUserSpace1Auth + ); + + await createComment({ + supertest: supertestWithoutAuth, + auth: superUserSpace1Auth, + params: { ...postCommentUserReq, owner: 'observabilityFixture' }, + caseId: obsCase.id, + }); + + const { body: res } = await supertestWithoutAuth + .get( + `${getSpaceUrlPrefix('space1')}${CASES_URL}/${ + obsCase.id + }/comments/_find?filter=cases-comments.attributes.owner:"observabilityFixture"` + ) + .auth(secOnly.username, secOnly.password) + .expect(200); + expect(res.comments.length).to.be(0); + }); + + // This test ensures that the user is not allowed to define the namespaces query param + // so she cannot search across spaces + it('should NOT allow to pass a namespaces query parameter', async () => { + const obsCase = await createCase( + supertest, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200 + ); + + await createComment({ + supertest, + params: { ...postCommentUserReq, owner: 'observabilityFixture' }, + caseId: obsCase.id, + }); + + await supertest + .get(`${CASES_URL}/${obsCase.id}/comments/_find?namespaces[0]=*`) + .expect(400); + + await supertest.get(`${CASES_URL}/${obsCase.id}/comments/_find?namespaces=*`).expect(400); + }); + + it('should NOT allow to pass a non supported query parameter', async () => { + await supertest.get(`${CASES_URL}/id/comments/_find?notExists=papa`).expect(400); + await supertest.get(`${CASES_URL}/id/comments/_find?owner=papa`).expect(400); + }); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/security_only/tests/common/comments/get_all_comments.ts b/x-pack/test/case_api_integration/security_only/tests/common/comments/get_all_comments.ts new file mode 100644 index 00000000000000..25df715b43e9a5 --- /dev/null +++ b/x-pack/test/case_api_integration/security_only/tests/common/comments/get_all_comments.ts @@ -0,0 +1,231 @@ +/* + * 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 { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; +import { postCaseReq, getPostCaseRequest, postCommentUserReq } from '../../../../common/lib/mock'; +import { + createCaseAction, + createSubCase, + deleteAllCaseItems, + deleteCaseAction, + createCase, + createComment, + getAllComments, + superUserSpace1Auth, +} from '../../../../common/lib/utils'; +import { CommentType } from '../../../../../../plugins/cases/common/api'; +import { + globalRead, + noKibanaPrivileges, + obsOnly, + obsOnlyRead, + obsSec, + obsSecRead, + secOnly, + secOnlyRead, + 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'); + + describe('get_all_comments', () => { + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + it('should get multiple comments for a single case', async () => { + const postedCase = await createCase(supertest, postCaseReq); + await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + }); + await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + }); + const comments = await getAllComments({ supertest, caseId: postedCase.id }); + + expect(comments.length).to.eql(2); + }); + + it('should return a 400 when passing the subCaseId parameter', async () => { + const { body } = await supertest + .get(`${CASES_URL}/case-id/comments?subCaseId=value`) + .set('kbn-xsrf', 'true') + .send() + .expect(400); + + expect(body.message).to.contain('disabled'); + }); + + it('should return a 400 when passing the includeSubCaseComments parameter', async () => { + const { body } = await supertest + .get(`${CASES_URL}/case-id/comments?includeSubCaseComments=true`) + .set('kbn-xsrf', 'true') + .send() + .expect(400); + + expect(body.message).to.contain('disabled'); + }); + + // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests + describe.skip('sub cases', () => { + let actionID: string; + before(async () => { + actionID = await createCaseAction(supertest); + }); + after(async () => { + await deleteCaseAction(supertest, actionID); + }); + + it('should get comments from a case and its sub cases', async () => { + const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); + await supertest + .post(`${CASES_URL}/${caseInfo.id}/comments`) + .set('kbn-xsrf', 'true') + .send(postCommentUserReq) + .expect(200); + + const { body: comments } = await supertest + .get(`${CASES_URL}/${caseInfo.id}/comments?includeSubCaseComments=true`) + .expect(200); + + expect(comments.length).to.eql(2); + expect(comments[0].type).to.eql(CommentType.generatedAlert); + expect(comments[1].type).to.eql(CommentType.user); + }); + + it('should get comments from a sub cases', async () => { + const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); + await supertest + .post(`${CASES_URL}/${caseInfo.subCases![0].id}/comments`) + .set('kbn-xsrf', 'true') + .send(postCommentUserReq) + .expect(200); + + const { body: comments } = await supertest + .get(`${CASES_URL}/${caseInfo.id}/comments?subCaseId=${caseInfo.subCases![0].id}`) + .expect(200); + + expect(comments.length).to.eql(2); + expect(comments[0].type).to.eql(CommentType.generatedAlert); + expect(comments[1].type).to.eql(CommentType.user); + }); + + it('should not find any comments for an invalid case id', async () => { + const { body } = await supertest + .get(`${CASES_URL}/fake-id/comments`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + expect(body.length).to.eql(0); + }); + }); + + describe('rbac', () => { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + it('should get all comments when the user has the correct permissions', async () => { + const caseInfo = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + superUserSpace1Auth + ); + + await createComment({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + params: postCommentUserReq, + auth: superUserSpace1Auth, + }); + + await createComment({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + params: postCommentUserReq, + auth: superUserSpace1Auth, + }); + + for (const user of [globalRead, superUser, secOnly, secOnlyRead, obsSec, obsSecRead]) { + const comments = await getAllComments({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + auth: { user, space: 'space1' }, + }); + + expect(comments.length).to.eql(2); + } + }); + + it('should not get comments when the user does not have correct permission', async () => { + const caseInfo = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + superUserSpace1Auth + ); + + await createComment({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + params: postCommentUserReq, + auth: superUserSpace1Auth, + }); + + for (const scenario of [ + { user: noKibanaPrivileges, returnCode: 403 }, + { user: obsOnly, returnCode: 200 }, + { user: obsOnlyRead, returnCode: 200 }, + ]) { + const comments = await getAllComments({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + auth: { user: scenario.user, space: 'space1' }, + expectedHttpCode: scenario.returnCode, + }); + + // only check the length if we get a 200 in response + if (scenario.returnCode === 200) { + expect(comments.length).to.be(0); + } + } + }); + + it('should NOT get a comment in a space with no permissions', async () => { + const caseInfo = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { user: superUser, space: 'space2' } + ); + + await createComment({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + params: postCommentUserReq, + auth: { user: superUser, space: 'space2' }, + }); + + await getAllComments({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + auth: { user: secOnly, space: 'space2' }, + expectedHttpCode: 403, + }); + }); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/security_only/tests/common/comments/get_comment.ts b/x-pack/test/case_api_integration/security_only/tests/common/comments/get_comment.ts new file mode 100644 index 00000000000000..5b606e06e84dfc --- /dev/null +++ b/x-pack/test/case_api_integration/security_only/tests/common/comments/get_comment.ts @@ -0,0 +1,169 @@ +/* + * 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 { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +import { postCaseReq, postCommentUserReq, getPostCaseRequest } from '../../../../common/lib/mock'; +import { + createCaseAction, + createSubCase, + deleteAllCaseItems, + deleteCaseAction, + createCase, + createComment, + getComment, + superUserSpace1Auth, +} from '../../../../common/lib/utils'; +import { CommentType } from '../../../../../../plugins/cases/common/api'; +import { + globalRead, + noKibanaPrivileges, + obsOnly, + obsOnlyRead, + obsSec, + obsSecRead, + secOnly, + secOnlyRead, + 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'); + + describe('get_comment', () => { + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + it('should get a comment', async () => { + const postedCase = await createCase(supertest, postCaseReq); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + }); + const comment = await getComment({ + supertest, + caseId: postedCase.id, + commentId: patchedCase.comments![0].id, + }); + + expect(comment).to.eql(patchedCase.comments![0]); + }); + + it('unhappy path - 404s when comment is not there', async () => { + await getComment({ + supertest, + caseId: 'fake-id', + commentId: 'fake-id', + expectedHttpCode: 404, + }); + }); + + // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests + describe.skip('sub cases', () => { + let actionID: string; + before(async () => { + actionID = await createCaseAction(supertest); + }); + after(async () => { + await deleteCaseAction(supertest, actionID); + }); + it('should get a sub case comment', async () => { + const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); + const comment = await getComment({ + supertest, + caseId: caseInfo.id, + commentId: caseInfo.comments![0].id, + }); + expect(comment.type).to.be(CommentType.generatedAlert); + }); + }); + + describe('rbac', () => { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + it('should get a comment when the user has the correct permissions', async () => { + const caseInfo = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + superUserSpace1Auth + ); + + const caseWithComment = await createComment({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + params: postCommentUserReq, + auth: superUserSpace1Auth, + }); + + for (const user of [globalRead, superUser, secOnly, secOnlyRead, obsSec, obsSecRead]) { + await getComment({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + commentId: caseWithComment.comments![0].id, + auth: { user, space: 'space1' }, + }); + } + }); + + it('should not get comment when the user does not have correct permissions', async () => { + const caseInfo = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + superUserSpace1Auth + ); + + const caseWithComment = await createComment({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + params: postCommentUserReq, + auth: superUserSpace1Auth, + }); + + for (const user of [noKibanaPrivileges, obsOnly, obsOnlyRead]) { + await getComment({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + commentId: caseWithComment.comments![0].id, + auth: { user, space: 'space1' }, + expectedHttpCode: 403, + }); + } + }); + + it('should NOT get a case in a space with no permissions', async () => { + const caseInfo = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { user: superUser, space: 'space2' } + ); + + const caseWithComment = await createComment({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + params: postCommentUserReq, + auth: { user: superUser, space: 'space2' }, + }); + + await getComment({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + commentId: caseWithComment.comments![0].id, + auth: { user: secOnly, space: 'space2' }, + expectedHttpCode: 403, + }); + }); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/security_only/tests/common/comments/migrations.ts b/x-pack/test/case_api_integration/security_only/tests/common/comments/migrations.ts new file mode 100644 index 00000000000000..50a219c5e84b30 --- /dev/null +++ b/x-pack/test/case_api_integration/security_only/tests/common/comments/migrations.ts @@ -0,0 +1,37 @@ +/* + * 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 { FtrProviderContext } from '../../../../../common/ftr_provider_context'; +import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; + +// eslint-disable-next-line import/no-default-export +export default function createGetTests({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + describe('migrations', () => { + before(async () => { + await esArchiver.load('cases/migrations/7.10.0'); + }); + + after(async () => { + await esArchiver.unload('cases/migrations/7.10.0'); + }); + + it('7.11.0 migrates cases comments', async () => { + const { body: comment } = await supertest + .get( + `${CASES_URL}/e1900ac0-017f-11eb-93f8-d161651bf509/comments/da677740-1ac7-11eb-b5a3-25ee88122510` + ) + .set('kbn-xsrf', 'true') + .send(); + + expect(comment.type).to.eql('user'); + }); + }); +} diff --git a/x-pack/test/case_api_integration/security_only/tests/common/comments/patch_comment.ts b/x-pack/test/case_api_integration/security_only/tests/common/comments/patch_comment.ts new file mode 100644 index 00000000000000..b00a0382bc7122 --- /dev/null +++ b/x-pack/test/case_api_integration/security_only/tests/common/comments/patch_comment.ts @@ -0,0 +1,641 @@ +/* + * 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 { omit } from 'lodash/fp'; +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; +import { + AttributesTypeAlerts, + AttributesTypeUser, + CaseResponse, + CommentType, +} from '../../../../../../plugins/cases/common/api'; +import { + defaultUser, + postCaseReq, + postCommentUserReq, + postCommentAlertReq, + getPostCaseRequest, +} from '../../../../common/lib/mock'; +import { + createCaseAction, + createSubCase, + deleteAllCaseItems, + deleteCaseAction, + deleteCasesByESQuery, + deleteCasesUserActions, + deleteComments, + createCase, + createComment, + updateComment, + superUserSpace1Auth, +} from '../../../../common/lib/utils'; +import { + globalRead, + noKibanaPrivileges, + obsOnly, + obsOnlyRead, + obsSecRead, + secOnly, + secOnlyRead, + 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'); + + describe('patch_comment', () => { + afterEach(async () => { + await deleteCasesByESQuery(es); + await deleteComments(es); + await deleteCasesUserActions(es); + }); + + it('should return a 400 when the subCaseId parameter is passed', async () => { + const { body } = await supertest + .patch(`${CASES_URL}/case-id}/comments?subCaseId=value`) + .set('kbn-xsrf', 'true') + .send({ + id: 'id', + version: 'version', + type: CommentType.alert, + alertId: 'test-id', + index: 'test-index', + rule: { + id: 'id', + name: 'name', + }, + owner: 'securitySolutionFixture', + }) + .expect(400); + + expect(body.message).to.contain('disabled'); + }); + + // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests + describe.skip('sub case comments', () => { + let actionID: string; + before(async () => { + actionID = await createCaseAction(supertest); + }); + after(async () => { + await deleteCaseAction(supertest, actionID); + }); + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + it('patches a comment for a sub case', async () => { + const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); + const { body: patchedSubCase }: { body: CaseResponse } = await supertest + .post(`${CASES_URL}/${caseInfo.id}/comments?subCaseId=${caseInfo.subCases![0].id}`) + .set('kbn-xsrf', 'true') + .send(postCommentUserReq) + .expect(200); + + const newComment = 'Well I decided to update my comment. So what? Deal with it.'; + const { body: patchedSubCaseUpdatedComment } = await supertest + .patch(`${CASES_URL}/${caseInfo.id}/comments?subCaseId=${caseInfo.subCases![0].id}`) + .set('kbn-xsrf', 'true') + .send({ + id: patchedSubCase.comments![1].id, + version: patchedSubCase.comments![1].version, + comment: newComment, + type: CommentType.user, + }) + .expect(200); + + expect(patchedSubCaseUpdatedComment.comments.length).to.be(2); + expect(patchedSubCaseUpdatedComment.comments[0].type).to.be(CommentType.generatedAlert); + expect(patchedSubCaseUpdatedComment.comments[1].type).to.be(CommentType.user); + expect(patchedSubCaseUpdatedComment.comments[1].comment).to.be(newComment); + }); + + it('fails to update the generated alert comment type', async () => { + const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); + await supertest + .patch(`${CASES_URL}/${caseInfo.id}/comments?subCaseId=${caseInfo.subCases![0].id}`) + .set('kbn-xsrf', 'true') + .send({ + id: caseInfo.comments![0].id, + version: caseInfo.comments![0].version, + type: CommentType.alert, + alertId: 'test-id', + index: 'test-index', + rule: { + id: 'id', + name: 'name', + }, + }) + .expect(400); + }); + + it('fails to update the generated alert comment by using another generated alert comment', async () => { + const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); + await supertest + .patch(`${CASES_URL}/${caseInfo.id}/comments?subCaseId=${caseInfo.subCases![0].id}`) + .set('kbn-xsrf', 'true') + .send({ + id: caseInfo.comments![0].id, + version: caseInfo.comments![0].version, + type: CommentType.generatedAlert, + alerts: [{ _id: 'id1' }], + index: 'test-index', + rule: { + id: 'id', + name: 'name', + }, + }) + .expect(400); + }); + }); + + it('should patch a comment', async () => { + const postedCase = await createCase(supertest, postCaseReq); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + }); + + const newComment = 'Well I decided to update my comment. So what? Deal with it.'; + const updatedCase = await updateComment({ + supertest, + caseId: postedCase.id, + req: { + id: patchedCase.comments![0].id, + version: patchedCase.comments![0].version, + comment: newComment, + type: CommentType.user, + owner: 'securitySolutionFixture', + }, + }); + + const userComment = updatedCase.comments![0] as AttributesTypeUser; + expect(userComment.comment).to.eql(newComment); + expect(userComment.type).to.eql(CommentType.user); + expect(updatedCase.updated_by).to.eql(defaultUser); + }); + + it('should patch an alert', async () => { + const postedCase = await createCase(supertest, postCaseReq); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentAlertReq, + }); + const updatedCase = await updateComment({ + supertest, + caseId: postedCase.id, + req: { + id: patchedCase.comments![0].id, + version: patchedCase.comments![0].version, + type: CommentType.alert, + alertId: 'new-id', + index: postCommentAlertReq.index, + rule: { + id: 'id', + name: 'name', + }, + owner: 'securitySolutionFixture', + }, + }); + + const alertComment = updatedCase.comments![0] as AttributesTypeAlerts; + expect(alertComment.alertId).to.eql('new-id'); + expect(alertComment.index).to.eql(postCommentAlertReq.index); + expect(alertComment.type).to.eql(CommentType.alert); + expect(alertComment.rule).to.eql({ + id: 'id', + name: 'name', + }); + expect(alertComment.updated_by).to.eql(defaultUser); + }); + + it('should not allow updating the owner of a comment', async () => { + const postedCase = await createCase(supertest, postCaseReq); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + }); + + await updateComment({ + supertest, + caseId: postedCase.id, + req: { + id: patchedCase.comments![0].id, + version: patchedCase.comments![0].version, + type: CommentType.user, + comment: postCommentUserReq.comment, + owner: 'changedOwner', + }, + expectedHttpCode: 400, + }); + }); + + it('unhappy path - 404s when comment is not there', async () => { + const postedCase = await createCase(supertest, postCaseReq); + await updateComment({ + supertest, + caseId: postedCase.id, + req: { + id: 'id', + version: 'version', + type: CommentType.user, + comment: 'comment', + owner: 'securitySolutionFixture', + }, + expectedHttpCode: 404, + }); + }); + + it('unhappy path - 404s when case is not there', async () => { + await updateComment({ + supertest, + caseId: 'fake-id', + req: { + id: 'id', + version: 'version', + type: CommentType.user, + comment: 'comment', + owner: 'securitySolutionFixture', + }, + expectedHttpCode: 404, + }); + }); + + it('unhappy path - 400s when trying to change comment type', async () => { + const postedCase = await createCase(supertest, postCaseReq); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + }); + + await updateComment({ + supertest, + caseId: postedCase.id, + req: { + id: patchedCase.comments![0].id, + version: patchedCase.comments![0].version, + type: CommentType.alert, + alertId: 'test-id', + index: 'test-index', + rule: { + id: 'id', + name: 'name', + }, + owner: 'securitySolutionFixture', + }, + expectedHttpCode: 400, + }); + }); + + it('unhappy path - 400s when missing attributes for type user', async () => { + const postedCase = await createCase(supertest, postCaseReq); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + }); + + await updateComment({ + supertest, + caseId: postedCase.id, + // @ts-expect-error + req: { + id: patchedCase.comments![0].id, + version: patchedCase.comments![0].version, + }, + expectedHttpCode: 400, + }); + }); + + it('unhappy path - 400s when adding excess attributes for type user', async () => { + const postedCase = await createCase(supertest, postCaseReq); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + }); + + for (const attribute of ['alertId', 'index']) { + await updateComment({ + supertest, + caseId: postedCase.id, + req: { + id: patchedCase.comments![0].id, + version: patchedCase.comments![0].version, + comment: 'a comment', + type: CommentType.user, + [attribute]: attribute, + owner: 'securitySolutionFixture', + }, + expectedHttpCode: 400, + }); + } + }); + + it('unhappy path - 400s when missing attributes for type alert', async () => { + const postedCase = await createCase(supertest, postCaseReq); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentAlertReq, + }); + + const allRequestAttributes = { + type: CommentType.alert, + index: 'test-index', + alertId: 'test-id', + rule: { + id: 'id', + name: 'name', + }, + }; + + for (const attribute of ['alertId', 'index']) { + const requestAttributes = omit(attribute, allRequestAttributes); + await updateComment({ + supertest, + caseId: postedCase.id, + // @ts-expect-error + req: { + id: patchedCase.comments![0].id, + version: patchedCase.comments![0].version, + ...requestAttributes, + }, + expectedHttpCode: 400, + }); + } + }); + + it('unhappy path - 400s when adding excess attributes for type alert', async () => { + const postedCase = await createCase(supertest, postCaseReq); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentAlertReq, + }); + + for (const attribute of ['comment']) { + await updateComment({ + supertest, + caseId: postedCase.id, + req: { + id: patchedCase.comments![0].id, + version: patchedCase.comments![0].version, + type: CommentType.alert, + index: 'test-index', + alertId: 'test-id', + rule: { + id: 'id', + name: 'name', + }, + owner: 'securitySolutionFixture', + [attribute]: attribute, + }, + expectedHttpCode: 400, + }); + } + }); + + it('unhappy path - 409s when conflict', async () => { + const postedCase = await createCase(supertest, postCaseReq); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + }); + + const newComment = 'Well I decided to update my comment. So what? Deal with it.'; + await updateComment({ + supertest, + caseId: postedCase.id, + req: { + id: patchedCase.comments![0].id, + version: 'version-mismatch', + type: CommentType.user, + comment: newComment, + owner: 'securitySolutionFixture', + }, + expectedHttpCode: 409, + }); + }); + + describe('alert format', () => { + type AlertComment = CommentType.alert | CommentType.generatedAlert; + + // ENABLE_CASE_CONNECTOR: once the case connector feature is completed create a test case for generated alerts here + for (const [alertId, index, type] of [ + ['1', ['index1', 'index2'], CommentType.alert], + [['1', '2'], 'index', CommentType.alert], + ]) { + it(`throws an error with an alert comment with contents id: ${alertId} indices: ${index} type: ${type}`, async () => { + const postedCase = await createCase(supertest, postCaseReq); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentAlertReq, + }); + + await updateComment({ + supertest, + caseId: patchedCase.id, + req: { + id: patchedCase.comments![0].id, + version: patchedCase.comments![0].version, + type: type as AlertComment, + alertId, + index, + owner: 'securitySolutionFixture', + rule: postCommentAlertReq.rule, + }, + expectedHttpCode: 400, + }); + }); + } + + for (const [alertId, index, type] of [ + ['1', ['index1'], CommentType.alert], + [['1', '2'], ['index', 'other-index'], CommentType.alert], + ]) { + it(`does not throw an error with an alert comment with contents id: ${alertId} indices: ${index} type: ${type}`, async () => { + const postedCase = await createCase(supertest, postCaseReq); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: { + ...postCommentAlertReq, + alertId, + index, + owner: 'securitySolutionFixture', + type: type as AlertComment, + }, + }); + + await updateComment({ + supertest, + caseId: postedCase.id, + req: { + id: patchedCase.comments![0].id, + version: patchedCase.comments![0].version, + type: type as AlertComment, + alertId, + index, + owner: 'securitySolutionFixture', + rule: postCommentAlertReq.rule, + }, + }); + }); + } + }); + + describe('rbac', () => { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + it('should update a comment that the user has permissions for', async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + superUserSpace1Auth + ); + + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + auth: superUserSpace1Auth, + }); + + const newComment = 'Well I decided to update my comment. So what? Deal with it.'; + const updatedCase = await updateComment({ + supertest, + caseId: postedCase.id, + req: { + ...postCommentUserReq, + id: patchedCase.comments![0].id, + version: patchedCase.comments![0].version, + comment: newComment, + }, + auth: { user: secOnly, space: 'space1' }, + }); + + const userComment = updatedCase.comments![0] as AttributesTypeUser; + expect(userComment.comment).to.eql(newComment); + expect(userComment.type).to.eql(CommentType.user); + expect(updatedCase.updated_by).to.eql(defaultUser); + expect(userComment.owner).to.eql('securitySolutionFixture'); + }); + + it('should not update a comment that has a different owner thant he user has access to', async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + superUserSpace1Auth + ); + + const patchedCase = await createComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + params: postCommentUserReq, + auth: superUserSpace1Auth, + }); + + const newComment = 'Well I decided to update my comment. So what? Deal with it.'; + await updateComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + req: { + ...postCommentUserReq, + id: patchedCase.comments![0].id, + version: patchedCase.comments![0].version, + comment: newComment, + }, + 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 update a comment`, async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + superUserSpace1Auth + ); + + const patchedCase = await createComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + params: postCommentUserReq, + auth: superUserSpace1Auth, + }); + + const newComment = 'Well I decided to update my comment. So what? Deal with it.'; + await updateComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + req: { + ...postCommentUserReq, + id: patchedCase.comments![0].id, + version: patchedCase.comments![0].version, + comment: newComment, + }, + auth: { user, space: 'space1' }, + expectedHttpCode: 403, + }); + }); + } + + it('should not update a comment in a space the user does not have permissions', async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + { user: superUser, space: 'space2' } + ); + + const patchedCase = await createComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + params: postCommentUserReq, + auth: { user: superUser, space: 'space2' }, + }); + + const newComment = 'Well I decided to update my comment. So what? Deal with it.'; + await updateComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + req: { + ...postCommentUserReq, + id: patchedCase.comments![0].id, + version: patchedCase.comments![0].version, + comment: newComment, + }, + auth: { user: secOnly, space: 'space2' }, + // getting the case will fail in the saved object layer with a 403 + expectedHttpCode: 403, + }); + }); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/security_only/tests/common/comments/post_comment.ts b/x-pack/test/case_api_integration/security_only/tests/common/comments/post_comment.ts new file mode 100644 index 00000000000000..a1f24de1b87daf --- /dev/null +++ b/x-pack/test/case_api_integration/security_only/tests/common/comments/post_comment.ts @@ -0,0 +1,605 @@ +/* + * 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 { omit } from 'lodash/fp'; +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; +import { DETECTION_ENGINE_QUERY_SIGNALS_URL } from '../../../../../../plugins/security_solution/common/constants'; +import { + CommentsResponse, + CommentType, + AttributesTypeUser, + AttributesTypeAlerts, +} from '../../../../../../plugins/cases/common/api'; +import { + defaultUser, + postCaseReq, + postCommentUserReq, + postCommentAlertReq, + postCollectionReq, + postCommentGenAlertReq, + getPostCaseRequest, +} from '../../../../common/lib/mock'; +import { + createCaseAction, + createSubCase, + deleteAllCaseItems, + deleteCaseAction, + deleteCasesByESQuery, + deleteCasesUserActions, + deleteComments, + createCase, + createComment, + getCaseUserActions, + removeServerGeneratedPropertiesFromUserAction, + removeServerGeneratedPropertiesFromSavedObject, + superUserSpace1Auth, +} from '../../../../common/lib/utils'; +import { + createSignalsIndex, + deleteSignalsIndex, + deleteAllAlerts, + getRuleForSignalTesting, + waitForRuleSuccessOrStatus, + waitForSignalsToBePresent, + getSignalsByIds, + createRule, + getQuerySignalIds, +} from '../../../../../detection_engine_api_integration/utils'; +import { + globalRead, + noKibanaPrivileges, + obsOnly, + obsOnlyRead, + obsSecRead, + secOnly, + secOnlyRead, + superUser, +} from '../../../../common/lib/authentication/users'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + const es = getService('es'); + + describe('post_comment', () => { + afterEach(async () => { + await deleteCasesByESQuery(es); + await deleteComments(es); + await deleteCasesUserActions(es); + }); + + describe('happy path', () => { + it('should post a comment', async () => { + const postedCase = await createCase(supertest, postCaseReq); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + }); + const comment = removeServerGeneratedPropertiesFromSavedObject( + patchedCase.comments![0] as AttributesTypeUser + ); + + expect(comment).to.eql({ + type: postCommentUserReq.type, + comment: postCommentUserReq.comment, + associationType: 'case', + created_by: defaultUser, + pushed_at: null, + pushed_by: null, + updated_by: null, + owner: 'securitySolutionFixture', + }); + + // updates the case correctly after adding a comment + expect(patchedCase.totalComment).to.eql(patchedCase.comments!.length); + expect(patchedCase.updated_by).to.eql(defaultUser); + }); + + it('should post an alert', async () => { + const postedCase = await createCase(supertest, postCaseReq); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentAlertReq, + }); + const comment = removeServerGeneratedPropertiesFromSavedObject( + patchedCase.comments![0] as AttributesTypeAlerts + ); + + expect(comment).to.eql({ + type: postCommentAlertReq.type, + alertId: postCommentAlertReq.alertId, + index: postCommentAlertReq.index, + rule: postCommentAlertReq.rule, + associationType: 'case', + created_by: defaultUser, + pushed_at: null, + pushed_by: null, + updated_by: null, + owner: 'securitySolutionFixture', + }); + + // updates the case correctly after adding a comment + expect(patchedCase.totalComment).to.eql(patchedCase.comments!.length); + expect(patchedCase.updated_by).to.eql(defaultUser); + }); + + it('creates a user action', async () => { + const postedCase = await createCase(supertest, postCaseReq); + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + }); + const userActions = await getCaseUserActions({ supertest, caseID: postedCase.id }); + const commentUserAction = removeServerGeneratedPropertiesFromUserAction(userActions[1]); + + expect(commentUserAction).to.eql({ + action_field: ['comment'], + action: 'create', + action_by: defaultUser, + new_value: `{"comment":"${postCommentUserReq.comment}","type":"${postCommentUserReq.type}","owner":"securitySolutionFixture"}`, + old_value: null, + case_id: `${postedCase.id}`, + comment_id: `${patchedCase.comments![0].id}`, + sub_case_id: '', + owner: 'securitySolutionFixture', + }); + }); + }); + + describe('unhappy path', () => { + it('400s when attempting to create a comment with a different owner than the case', async () => { + const postedCase = await createCase( + supertest, + getPostCaseRequest({ owner: 'securitySolutionFixture' }) + ); + + await createComment({ + supertest, + caseId: postedCase.id, + params: { ...postCommentUserReq, owner: 'observabilityFixture' }, + expectedHttpCode: 400, + }); + }); + + it('400s when type is missing', async () => { + const postedCase = await createCase(supertest, postCaseReq); + await createComment({ + supertest, + caseId: postedCase.id, + params: { + // @ts-expect-error + bad: 'comment', + }, + expectedHttpCode: 400, + }); + }); + + it('400s when missing attributes for type user', async () => { + const postedCase = await createCase(supertest, postCaseReq); + await createComment({ + supertest, + caseId: postedCase.id, + // @ts-expect-error + params: { + type: CommentType.user, + }, + expectedHttpCode: 400, + }); + }); + + it('400s when adding excess attributes for type user', async () => { + const postedCase = await createCase(supertest, postCaseReq); + + for (const attribute of ['alertId', 'index']) { + await createComment({ + supertest, + caseId: postedCase.id, + params: { + type: CommentType.user, + [attribute]: attribute, + comment: 'a comment', + owner: 'securitySolutionFixture', + }, + expectedHttpCode: 400, + }); + } + }); + + it('400s when missing attributes for type alert', async () => { + const postedCase = await createCase(supertest, postCaseReq); + + const allRequestAttributes = { + type: CommentType.alert, + index: 'test-index', + alertId: 'test-id', + rule: { + id: 'id', + name: 'name', + }, + owner: 'securitySolutionFixture', + }; + + for (const attribute of ['alertId', 'index']) { + const requestAttributes = omit(attribute, allRequestAttributes); + await createComment({ + supertest, + caseId: postedCase.id, + // @ts-expect-error + params: requestAttributes, + expectedHttpCode: 400, + }); + } + }); + + it('400s when adding excess attributes for type alert', async () => { + const postedCase = await createCase(supertest, postCaseReq); + + for (const attribute of ['comment']) { + await createComment({ + supertest, + caseId: postedCase.id, + params: { + type: CommentType.alert, + [attribute]: attribute, + alertId: 'test-id', + index: 'test-index', + rule: { + id: 'id', + name: 'name', + }, + owner: 'securitySolutionFixture', + }, + expectedHttpCode: 400, + }); + } + }); + + it('400s when case is missing', async () => { + await createComment({ + supertest, + caseId: 'not-exists', + params: { + // @ts-expect-error + bad: 'comment', + }, + expectedHttpCode: 400, + }); + }); + + it('400s when adding an alert to a closed case', async () => { + const postedCase = await createCase(supertest, postCaseReq); + + await supertest + .patch(CASES_URL) + .set('kbn-xsrf', 'true') + .send({ + cases: [ + { + id: postedCase.id, + version: postedCase.version, + status: 'closed', + }, + ], + }) + .expect(200); + + await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentAlertReq, + expectedHttpCode: 400, + }); + }); + + // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests + it.skip('400s when adding an alert to a collection case', async () => { + const postedCase = await createCase(supertest, postCollectionReq); + await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentAlertReq, + expectedHttpCode: 400, + }); + }); + + it('400s when adding a generated alert to an individual case', async () => { + const postedCase = await createCase(supertest, postCaseReq); + + await supertest + .post(`${CASES_URL}/${postedCase.id}/comments`) + .set('kbn-xsrf', 'true') + .send(postCommentGenAlertReq) + .expect(400); + }); + + it('should return a 400 when passing the subCaseId', async () => { + const { body } = await supertest + .post(`${CASES_URL}/case-id/comments?subCaseId=value`) + .set('kbn-xsrf', 'true') + .send(postCommentUserReq) + .expect(400); + expect(body.message).to.contain('subCaseId'); + }); + }); + + describe('alerts', () => { + beforeEach(async () => { + await esArchiver.load('auditbeat/hosts'); + await createSignalsIndex(supertest); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest); + await deleteAllAlerts(supertest); + await esArchiver.unload('auditbeat/hosts'); + }); + + it('should change the status of the alert if sync alert is on', async () => { + const rule = getRuleForSignalTesting(['auditbeat-*']); + const postedCase = await createCase(supertest, postCaseReq); + + await supertest + .patch(CASES_URL) + .set('kbn-xsrf', 'true') + .send({ + cases: [ + { + id: postedCase.id, + version: postedCase.version, + status: 'in-progress', + }, + ], + }) + .expect(200); + + const { id } = await createRule(supertest, rule); + await waitForRuleSuccessOrStatus(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signals = await getSignalsByIds(supertest, [id]); + + const alert = signals.hits.hits[0]; + expect(alert._source.signal.status).eql('open'); + + await createComment({ + supertest, + caseId: postedCase.id, + params: { + alertId: alert._id, + index: alert._index, + rule: { + id: 'id', + name: 'name', + }, + owner: 'securitySolutionFixture', + type: CommentType.alert, + }, + }); + + const { body: updatedAlert } = await supertest + .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) + .set('kbn-xsrf', 'true') + .send(getQuerySignalIds([alert._id])) + .expect(200); + + expect(updatedAlert.hits.hits[0]._source.signal.status).eql('in-progress'); + }); + + it('should NOT change the status of the alert if sync alert is off', async () => { + const rule = getRuleForSignalTesting(['auditbeat-*']); + const postedCase = await createCase(supertest, { + ...postCaseReq, + settings: { syncAlerts: false }, + }); + + await supertest + .patch(CASES_URL) + .set('kbn-xsrf', 'true') + .send({ + cases: [ + { + id: postedCase.id, + version: postedCase.version, + status: 'in-progress', + }, + ], + }) + .expect(200); + + const { id } = await createRule(supertest, rule); + await waitForRuleSuccessOrStatus(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signals = await getSignalsByIds(supertest, [id]); + + const alert = signals.hits.hits[0]; + expect(alert._source.signal.status).eql('open'); + + await createComment({ + supertest, + caseId: postedCase.id, + params: { + alertId: alert._id, + index: alert._index, + rule: { + id: 'id', + name: 'name', + }, + owner: 'securitySolutionFixture', + type: CommentType.alert, + }, + }); + + const { body: updatedAlert } = await supertest + .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) + .set('kbn-xsrf', 'true') + .send(getQuerySignalIds([alert._id])) + .expect(200); + + expect(updatedAlert.hits.hits[0]._source.signal.status).eql('open'); + }); + }); + + describe('alert format', () => { + type AlertComment = CommentType.alert | CommentType.generatedAlert; + + for (const [alertId, index, type] of [ + ['1', ['index1', 'index2'], CommentType.alert], + [['1', '2'], 'index', CommentType.alert], + ['1', ['index1', 'index2'], CommentType.generatedAlert], + [['1', '2'], 'index', CommentType.generatedAlert], + ]) { + it(`throws an error with an alert comment with contents id: ${alertId} indices: ${index} type: ${type}`, async () => { + const postedCase = await createCase(supertest, postCaseReq); + await createComment({ + supertest, + caseId: postedCase.id, + params: { ...postCommentAlertReq, alertId, index, type: type as AlertComment }, + expectedHttpCode: 400, + }); + }); + } + + for (const [alertId, index, type] of [ + ['1', ['index1'], CommentType.alert], + [['1', '2'], ['index', 'other-index'], CommentType.alert], + ]) { + it(`does not throw an error with an alert comment with contents id: ${alertId} indices: ${index} type: ${type}`, async () => { + const postedCase = await createCase(supertest, postCaseReq); + await createComment({ + supertest, + caseId: postedCase.id, + params: { + ...postCommentAlertReq, + alertId, + index, + type: type as AlertComment, + }, + expectedHttpCode: 200, + }); + }); + } + }); + + // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests + describe.skip('sub case comments', () => { + let actionID: string; + before(async () => { + actionID = await createCaseAction(supertest); + }); + after(async () => { + await deleteCaseAction(supertest, actionID); + }); + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + it('posts a new comment for a sub case', async () => { + const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); + // create another sub case just to make sure we get the right comments + await createSubCase({ supertest, actionID }); + await supertest + .post(`${CASES_URL}/${caseInfo.id}/comments?subCaseId=${caseInfo.subCases![0].id}`) + .set('kbn-xsrf', 'true') + .send(postCommentUserReq) + .expect(200); + + const { body: subCaseComments }: { body: CommentsResponse } = await supertest + .get(`${CASES_URL}/${caseInfo.id}/comments/_find?subCaseId=${caseInfo.subCases![0].id}`) + .send() + .expect(200); + expect(subCaseComments.total).to.be(2); + expect(subCaseComments.comments[0].type).to.be(CommentType.generatedAlert); + expect(subCaseComments.comments[1].type).to.be(CommentType.user); + }); + }); + + describe('rbac', () => { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + it('should create a comment when the user has the correct permissions for that owner', async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + superUserSpace1Auth + ); + + await createComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + params: postCommentUserReq, + auth: { user: secOnly, space: 'space1' }, + }); + }); + + it('should not create a comment when the user does not have permissions for that owner', async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + { user: obsOnly, space: 'space1' } + ); + + await createComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + params: { ...postCommentUserReq, owner: 'observabilityFixture' }, + auth: { user: secOnly, 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 create a comment`, async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + superUserSpace1Auth + ); + + await createComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + params: postCommentUserReq, + auth: { user, space: 'space1' }, + expectedHttpCode: 403, + }); + }); + } + + it('should not create a comment in a space the user does not have permissions for', 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: secOnly, space: 'space2' }, + expectedHttpCode: 403, + }); + }); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/security_only/tests/common/configure/get_configure.ts b/x-pack/test/case_api_integration/security_only/tests/common/configure/get_configure.ts new file mode 100644 index 00000000000000..279936ebbef462 --- /dev/null +++ b/x-pack/test/case_api_integration/security_only/tests/common/configure/get_configure.ts @@ -0,0 +1,215 @@ +/* + * 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 { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +import { + removeServerGeneratedPropertiesFromSavedObject, + getConfigurationOutput, + deleteConfiguration, + getConfiguration, + createConfiguration, + getConfigurationRequest, + ensureSavedObjectIsAuthorized, +} from '../../../../common/lib/utils'; +import { + obsOnly, + secOnly, + obsOnlyRead, + secOnlyRead, + noKibanaPrivileges, + superUser, + globalRead, + obsSecRead, + obsSec, +} from '../../../../common/lib/authentication/users'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const es = getService('es'); + + describe('get_configure', () => { + afterEach(async () => { + await deleteConfiguration(es); + }); + + it('should return an empty find body correctly if no configuration is loaded', async () => { + const configuration = await getConfiguration({ supertest }); + expect(configuration).to.eql([]); + }); + + it('should return a configuration', async () => { + await createConfiguration(supertest); + const configuration = await getConfiguration({ supertest }); + + const data = removeServerGeneratedPropertiesFromSavedObject(configuration[0]); + expect(data).to.eql(getConfigurationOutput()); + }); + + it('should get a single configuration', async () => { + await createConfiguration(supertest, getConfigurationRequest({ id: 'connector-2' })); + await createConfiguration(supertest); + const res = await getConfiguration({ supertest }); + + expect(res.length).to.eql(1); + const data = removeServerGeneratedPropertiesFromSavedObject(res[0]); + expect(data).to.eql(getConfigurationOutput()); + }); + + it('should return by descending order', async () => { + await createConfiguration(supertest, getConfigurationRequest({ id: 'connector-2' })); + await createConfiguration(supertest); + const res = await getConfiguration({ supertest }); + + const data = removeServerGeneratedPropertiesFromSavedObject(res[0]); + expect(data).to.eql(getConfigurationOutput()); + }); + + describe('rbac', () => { + it('should return the correct configuration', async () => { + await createConfiguration(supertestWithoutAuth, getConfigurationRequest(), 200, { + user: secOnly, + space: 'space1', + }); + + await createConfiguration( + supertestWithoutAuth, + { ...getConfigurationRequest(), owner: 'observabilityFixture' }, + 200, + { + user: obsOnly, + space: 'space1', + } + ); + + for (const scenario of [ + { + user: globalRead, + numberOfExpectedCases: 2, + owners: ['securitySolutionFixture', 'observabilityFixture'], + }, + { + user: superUser, + numberOfExpectedCases: 2, + owners: ['securitySolutionFixture', 'observabilityFixture'], + }, + { user: secOnlyRead, numberOfExpectedCases: 1, owners: ['securitySolutionFixture'] }, + { user: obsOnlyRead, numberOfExpectedCases: 1, owners: ['observabilityFixture'] }, + { + user: obsSecRead, + numberOfExpectedCases: 2, + owners: ['securitySolutionFixture', 'observabilityFixture'], + }, + ]) { + const configuration = await getConfiguration({ + supertest: supertestWithoutAuth, + query: { owner: scenario.owners }, + expectedHttpCode: 200, + auth: { + user: scenario.user, + space: 'space1', + }, + }); + + ensureSavedObjectIsAuthorized( + configuration, + scenario.numberOfExpectedCases, + scenario.owners + ); + } + }); + + for (const scenario of [ + { user: noKibanaPrivileges, space: 'space1' }, + { user: secOnly, space: 'space2' }, + ]) { + it(`User ${scenario.user.username} with role(s) ${scenario.user.roles.join()} and space ${ + scenario.space + } - should NOT read a case configuration`, async () => { + // super user creates a configuration at the appropriate space + await createConfiguration(supertestWithoutAuth, getConfigurationRequest(), 200, { + user: superUser, + space: scenario.space, + }); + + // user should not be able to read configurations at the appropriate space + await getConfiguration({ + supertest: supertestWithoutAuth, + expectedHttpCode: 403, + auth: { + user: scenario.user, + space: scenario.space, + }, + }); + }); + } + + it('should respect the owner filter when having permissions', async () => { + await Promise.all([ + createConfiguration(supertestWithoutAuth, getConfigurationRequest(), 200, { + user: obsSec, + space: 'space1', + }), + createConfiguration( + supertestWithoutAuth, + { ...getConfigurationRequest(), owner: 'observabilityFixture' }, + 200, + { + user: obsSec, + space: 'space1', + } + ), + ]); + + const res = await getConfiguration({ + supertest: supertestWithoutAuth, + query: { owner: 'securitySolutionFixture' }, + auth: { + user: obsSec, + space: 'space1', + }, + }); + + ensureSavedObjectIsAuthorized(res, 1, ['securitySolutionFixture']); + }); + + it('should return the correct cases when trying to exploit RBAC through the owner query parameter', async () => { + await Promise.all([ + createConfiguration(supertestWithoutAuth, getConfigurationRequest(), 200, { + user: obsSec, + space: 'space1', + }), + createConfiguration( + supertestWithoutAuth, + { ...getConfigurationRequest(), owner: 'observabilityFixture' }, + 200, + { + user: obsSec, + space: 'space1', + } + ), + ]); + + // User with permissions only to security solution request cases from observability + const res = await getConfiguration({ + supertest: supertestWithoutAuth, + query: { owner: ['securitySolutionFixture', 'observabilityFixture'] }, + auth: { + user: secOnly, + space: 'space1', + }, + }); + + // Only security solution cases are being returned + ensureSavedObjectIsAuthorized(res, 1, ['securitySolutionFixture']); + }); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/security_only/tests/common/configure/get_connectors.ts b/x-pack/test/case_api_integration/security_only/tests/common/configure/get_connectors.ts new file mode 100644 index 00000000000000..46f712ff84aa32 --- /dev/null +++ b/x-pack/test/case_api_integration/security_only/tests/common/configure/get_connectors.ts @@ -0,0 +1,27 @@ +/* + * 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 { FtrProviderContext } from '../../../../../common/ftr_provider_context'; + +import { getCaseConnectors } from '../../../../common/lib/utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + + describe('get_connectors', () => { + it('should return an empty find body correctly if no connectors are loaded', async () => { + const connectors = await getCaseConnectors({ supertest }); + expect(connectors).to.eql([]); + }); + + it.skip('filters out connectors that are not enabled in license', async () => { + // TODO: Should find a way to downgrade license to gold and upgrade back to trial + }); + }); +}; diff --git a/x-pack/test/case_api_integration/security_only/tests/common/configure/patch_configure.ts b/x-pack/test/case_api_integration/security_only/tests/common/configure/patch_configure.ts new file mode 100644 index 00000000000000..ced727f8e4e75e --- /dev/null +++ b/x-pack/test/case_api_integration/security_only/tests/common/configure/patch_configure.ts @@ -0,0 +1,243 @@ +/* + * 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 { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { ObjectRemover as ActionsRemover } from '../../../../../alerting_api_integration/common/lib'; + +import { + getConfigurationRequest, + removeServerGeneratedPropertiesFromSavedObject, + getConfigurationOutput, + deleteConfiguration, + createConfiguration, + updateConfiguration, +} from '../../../../common/lib/utils'; +import { + secOnly, + obsOnlyRead, + secOnlyRead, + noKibanaPrivileges, + globalRead, + obsSecRead, + superUser, +} from '../../../../common/lib/authentication/users'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const es = getService('es'); + + describe('patch_configure', () => { + const actionsRemover = new ActionsRemover(supertest); + + afterEach(async () => { + await deleteConfiguration(es); + await actionsRemover.removeAll(); + }); + + it('should patch a configuration', async () => { + const configuration = await createConfiguration(supertest); + const newConfiguration = await updateConfiguration(supertest, configuration.id, { + closure_type: 'close-by-pushing', + version: configuration.version, + }); + + const data = removeServerGeneratedPropertiesFromSavedObject(newConfiguration); + expect(data).to.eql({ ...getConfigurationOutput(true), closure_type: 'close-by-pushing' }); + }); + + it('should not patch a configuration with unsupported connector type', async () => { + const configuration = await createConfiguration(supertest); + await updateConfiguration( + supertest, + configuration.id, + // @ts-expect-error + getConfigurationRequest({ type: '.unsupported' }), + 400 + ); + }); + + it('should not patch a configuration with unsupported connector fields', async () => { + const configuration = await createConfiguration(supertest); + await updateConfiguration( + supertest, + configuration.id, + // @ts-expect-error + getConfigurationRequest({ type: '.jira', fields: { unsupported: 'value' } }), + 400 + ); + }); + + it('should handle patch request when there is no configuration', async () => { + const error = await updateConfiguration( + supertest, + 'not-exist', + { closure_type: 'close-by-pushing', version: 'no-version' }, + 404 + ); + + expect(error).to.eql({ + error: 'Not Found', + message: 'Saved object [cases-configure/not-exist] not found', + statusCode: 404, + }); + }); + + it('should handle patch request when versions are different', async () => { + const configuration = await createConfiguration(supertest); + const error = await updateConfiguration( + supertest, + configuration.id, + { closure_type: 'close-by-pushing', version: 'no-version' }, + 409 + ); + + expect(error).to.eql({ + error: 'Conflict', + message: + 'This configuration has been updated. Please refresh before saving additional updates.', + statusCode: 409, + }); + }); + + it('should not allow to change the owner of the configuration', async () => { + const configuration = await createConfiguration(supertest); + await updateConfiguration( + supertest, + configuration.id, + // @ts-expect-error + { owner: 'observabilityFixture', version: configuration.version }, + 400 + ); + }); + + it('should not allow excess attributes', async () => { + const configuration = await createConfiguration(supertest); + await updateConfiguration( + supertest, + configuration.id, + // @ts-expect-error + { notExist: 'not-exist', version: configuration.version }, + 400 + ); + }); + + describe('rbac', () => { + it('User: security solution only - should update a configuration', async () => { + const configuration = await createConfiguration( + supertestWithoutAuth, + getConfigurationRequest(), + 200, + { + user: secOnly, + space: 'space1', + } + ); + + const newConfiguration = await updateConfiguration( + supertestWithoutAuth, + configuration.id, + { + closure_type: 'close-by-pushing', + version: configuration.version, + }, + 200, + { + user: secOnly, + space: 'space1', + } + ); + + expect(newConfiguration.owner).to.eql('securitySolutionFixture'); + }); + + it('User: security solution only - should NOT update a configuration of different owner', async () => { + const configuration = await createConfiguration( + supertestWithoutAuth, + { ...getConfigurationRequest(), owner: 'observabilityFixture' }, + 200, + { + user: superUser, + space: 'space1', + } + ); + + await updateConfiguration( + supertestWithoutAuth, + configuration.id, + { + closure_type: 'close-by-pushing', + version: configuration.version, + }, + 403, + { + user: secOnly, + space: 'space1', + } + ); + }); + + for (const user of [globalRead, secOnlyRead, obsOnlyRead, obsSecRead, noKibanaPrivileges]) { + it(`User ${ + user.username + } with role(s) ${user.roles.join()} - should NOT update a configuration`, async () => { + const configuration = await createConfiguration( + supertestWithoutAuth, + getConfigurationRequest(), + 200, + { + user: superUser, + space: 'space1', + } + ); + + await updateConfiguration( + supertestWithoutAuth, + configuration.id, + { + closure_type: 'close-by-pushing', + version: configuration.version, + }, + 403, + { + user, + space: 'space1', + } + ); + }); + } + + it('should NOT update a configuration in a space with no permissions', async () => { + const configuration = await createConfiguration( + supertestWithoutAuth, + { ...getConfigurationRequest(), owner: 'securitySolutionFixture' }, + 200, + { + user: superUser, + space: 'space2', + } + ); + + await updateConfiguration( + supertestWithoutAuth, + configuration.id, + { + closure_type: 'close-by-pushing', + version: configuration.version, + }, + 404, + { + user: secOnly, + space: 'space1', + } + ); + }); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/security_only/tests/common/configure/post_configure.ts b/x-pack/test/case_api_integration/security_only/tests/common/configure/post_configure.ts new file mode 100644 index 00000000000000..f1dae9f319109b --- /dev/null +++ b/x-pack/test/case_api_integration/security_only/tests/common/configure/post_configure.ts @@ -0,0 +1,298 @@ +/* + * 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 { ConnectorTypes } from '../../../../../../plugins/cases/common/api'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { ObjectRemover as ActionsRemover } from '../../../../../alerting_api_integration/common/lib'; + +import { + getConfigurationRequest, + removeServerGeneratedPropertiesFromSavedObject, + getConfigurationOutput, + deleteConfiguration, + createConfiguration, + getConfiguration, + ensureSavedObjectIsAuthorized, +} from '../../../../common/lib/utils'; + +import { + secOnly, + obsOnlyRead, + secOnlyRead, + noKibanaPrivileges, + globalRead, + obsSecRead, + superUser, +} from '../../../../common/lib/authentication/users'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const es = getService('es'); + + describe('post_configure', () => { + const actionsRemover = new ActionsRemover(supertest); + + afterEach(async () => { + await deleteConfiguration(es); + await actionsRemover.removeAll(); + }); + + it('should create a configuration', async () => { + const configuration = await createConfiguration(supertest); + + const data = removeServerGeneratedPropertiesFromSavedObject(configuration); + expect(data).to.eql(getConfigurationOutput()); + }); + + it('should keep only the latest configuration', async () => { + await createConfiguration(supertest, getConfigurationRequest({ id: 'connector-2' })); + await createConfiguration(supertest); + const configuration = await getConfiguration({ supertest }); + + expect(configuration.length).to.be(1); + }); + + it('should return an error when failing to get mapping', async () => { + const postRes = await createConfiguration( + supertest, + getConfigurationRequest({ + id: 'not-exists', + name: 'not-exists', + type: ConnectorTypes.jira, + }) + ); + + expect(postRes.error).to.not.be(null); + expect(postRes.mappings).to.eql([]); + }); + + it('should not create a configuration when missing connector.id', async () => { + await createConfiguration( + supertest, + { + // @ts-expect-error + connector: { + name: 'Connector', + type: ConnectorTypes.none, + fields: null, + }, + closure_type: 'close-by-user', + }, + 400 + ); + }); + + it('should not create a configuration when missing connector.name', async () => { + await createConfiguration( + supertest, + { + // @ts-expect-error + connector: { + id: 'test-id', + type: ConnectorTypes.none, + fields: null, + }, + closure_type: 'close-by-user', + }, + 400 + ); + }); + + it('should not create a configuration when missing connector.type', async () => { + await createConfiguration( + supertest, + { + // @ts-expect-error + connector: { + id: 'test-id', + name: 'Connector', + fields: null, + }, + closure_type: 'close-by-user', + }, + 400 + ); + }); + + it('should not create a configuration when missing connector.fields', async () => { + await createConfiguration( + supertest, + { + // @ts-expect-error + connector: { + id: 'test-id', + type: ConnectorTypes.none, + name: 'Connector', + }, + closure_type: 'close-by-user', + }, + 400 + ); + }); + + it('should not create a configuration when when missing closure_type', async () => { + await createConfiguration( + supertest, + // @ts-expect-error + { + connector: { + id: 'test-id', + type: ConnectorTypes.none, + name: 'Connector', + fields: null, + }, + }, + 400 + ); + }); + + it('should not create a configuration when missing connector', async () => { + await createConfiguration( + supertest, + // @ts-expect-error + { + closure_type: 'close-by-user', + }, + 400 + ); + }); + + it('should not create a configuration when fields are not null', async () => { + await createConfiguration( + supertest, + { + connector: { + id: 'test-id', + type: ConnectorTypes.none, + name: 'Connector', + // @ts-expect-error + fields: {}, + }, + closure_type: 'close-by-user', + }, + 400 + ); + }); + + it('should not create a configuration with unsupported connector type', async () => { + // @ts-expect-error + await createConfiguration(supertest, getConfigurationRequest({ type: '.unsupported' }), 400); + }); + + it('should not create a configuration with unsupported connector fields', async () => { + await createConfiguration( + supertest, + // @ts-expect-error + getConfigurationRequest({ type: '.jira', fields: { unsupported: 'value' } }), + 400 + ); + }); + + describe('rbac', () => { + it('User: security solution only - should create a configuration', async () => { + const configuration = await createConfiguration( + supertestWithoutAuth, + getConfigurationRequest(), + 200, + { + user: secOnly, + space: 'space1', + } + ); + + expect(configuration.owner).to.eql('securitySolutionFixture'); + }); + + it('User: security solution only - should NOT create a configuration of different owner', async () => { + await createConfiguration( + supertestWithoutAuth, + { ...getConfigurationRequest(), owner: 'observabilityFixture' }, + 403, + { + user: secOnly, + space: 'space1', + } + ); + }); + + for (const user of [globalRead, secOnlyRead, obsOnlyRead, obsSecRead, noKibanaPrivileges]) { + it(`User ${ + user.username + } with role(s) ${user.roles.join()} - should NOT create a configuration`, async () => { + await createConfiguration( + supertestWithoutAuth, + { ...getConfigurationRequest(), owner: 'securitySolutionFixture' }, + 403, + { + user, + space: 'space1', + } + ); + }); + } + + it('should NOT create a configuration in a space with no permissions', async () => { + await createConfiguration( + supertestWithoutAuth, + { ...getConfigurationRequest(), owner: 'securitySolutionFixture' }, + 403, + { + user: secOnly, + space: 'space2', + } + ); + }); + + it('it deletes the correct configurations', async () => { + await createConfiguration( + supertestWithoutAuth, + { ...getConfigurationRequest(), owner: 'securitySolutionFixture' }, + 200, + { + user: superUser, + space: 'space1', + } + ); + + /** + * This API call should not delete the previously created configuration + * as it belongs to a different owner + */ + await createConfiguration( + supertestWithoutAuth, + { ...getConfigurationRequest(), owner: 'observabilityFixture' }, + 200, + { + user: superUser, + space: 'space1', + } + ); + + const configuration = await getConfiguration({ + supertest: supertestWithoutAuth, + query: { owner: ['securitySolutionFixture', 'observabilityFixture'] }, + auth: { + user: superUser, + space: 'space1', + }, + }); + + /** + * This ensures that both configuration are returned as expected + * and neither of has been deleted + */ + ensureSavedObjectIsAuthorized(configuration, 2, [ + 'securitySolutionFixture', + 'observabilityFixture', + ]); + }); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/security_only/tests/common/connectors/case.ts b/x-pack/test/case_api_integration/security_only/tests/common/connectors/case.ts new file mode 100644 index 00000000000000..fd9ec8142b49fe --- /dev/null +++ b/x-pack/test/case_api_integration/security_only/tests/common/connectors/case.ts @@ -0,0 +1,1078 @@ +/* + * 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 { omit } from 'lodash/fp'; +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; + +import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; +import { CommentType } from '../../../../../../plugins/cases/common/api'; +import { postCaseReq, postCaseResp } from '../../../../common/lib/mock'; +import { + removeServerGeneratedPropertiesFromCase, + removeServerGeneratedPropertiesFromComments, +} from '../../../../common/lib/utils'; +import { + createRule, + createSignalsIndex, + deleteAllAlerts, + deleteSignalsIndex, + getRuleForSignalTesting, + getSignalsByIds, + waitForRuleSuccessOrStatus, + waitForSignalsToBePresent, +} from '../../../../../detection_engine_api_integration/utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + describe('case_connector', () => { + let createdActionId = ''; + + it('should return 400 when creating a case action', async () => { + await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A case connector', + actionTypeId: '.case', + config: {}, + }) + .expect(400); + }); + + // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests + it.skip('should return 200 when creating a case action successfully', async () => { + const { body: createdAction } = await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A case connector', + actionTypeId: '.case', + config: {}, + }) + .expect(200); + + createdActionId = createdAction.id; + + expect(createdAction).to.eql({ + id: createdActionId, + isPreconfigured: false, + name: 'A case connector', + actionTypeId: '.case', + config: {}, + }); + + const { body: fetchedAction } = await supertest + .get(`/api/actions/connector/${createdActionId}`) + .expect(200); + + expect(fetchedAction).to.eql({ + id: fetchedAction.id, + isPreconfigured: false, + name: 'A case connector', + actionTypeId: '.case', + config: {}, + }); + }); + + describe.skip('create', () => { + it('should respond with a 400 Bad Request when creating a case without title', async () => { + const { body: createdAction } = await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A case connector', + actionTypeId: '.case', + config: {}, + }) + .expect(200); + + createdActionId = createdAction.id; + const params = { + subAction: 'create', + subActionParams: { + tags: ['case', 'connector'], + description: 'case description', + connector: { + id: 'jira', + name: 'Jira', + type: '.jira', + fields: { + issueType: '10006', + priority: 'High', + parent: null, + }, + }, + }, + }; + + const caseConnector = await supertest + .post(`/api/actions/connector/${createdActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ params }) + .expect(200); + + expect(caseConnector.body).to.eql({ + status: 'error', + actionId: createdActionId, + message: + 'error validating action params: types that failed validation:\n- [0.subActionParams.title]: expected value of type [string] but got [undefined]\n- [1.subAction]: expected value to equal [update]\n- [2.subAction]: expected value to equal [addComment]', + retry: false, + }); + }); + + it('should respond with a 400 Bad Request when creating a case without description', async () => { + const { body: createdAction } = await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A case connector', + actionTypeId: '.case', + config: {}, + }) + .expect(200); + + createdActionId = createdAction.id; + const params = { + subAction: 'create', + subActionParams: { + title: 'Case from case connector!!', + tags: ['case', 'connector'], + connector: { + id: 'jira', + name: 'Jira', + type: '.jira', + fields: { + issueType: '10006', + priority: 'High', + parent: null, + }, + }, + }, + }; + + const caseConnector = await supertest + .post(`/api/actions/connector/${createdActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ params }) + .expect(200); + + expect(caseConnector.body).to.eql({ + status: 'error', + actionId: createdActionId, + message: + 'error validating action params: types that failed validation:\n- [0.subActionParams.description]: expected value of type [string] but got [undefined]\n- [1.subAction]: expected value to equal [update]\n- [2.subAction]: expected value to equal [addComment]', + retry: false, + }); + }); + + it('should respond with a 400 Bad Request when creating a case without tags', async () => { + const { body: createdAction } = await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A case connector', + actionTypeId: '.case', + config: {}, + }) + .expect(200); + + createdActionId = createdAction.id; + const params = { + subAction: 'create', + subActionParams: { + title: 'Case from case connector!!', + description: 'case description', + connector: { + id: 'jira', + name: 'Jira', + type: '.jira', + fields: { + issueType: '10006', + priority: 'High', + parent: null, + }, + }, + }, + }; + + const caseConnector = await supertest + .post(`/api/actions/connector/${createdActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ params }) + .expect(200); + + expect(caseConnector.body).to.eql({ + status: 'error', + actionId: createdActionId, + message: + 'error validating action params: types that failed validation:\n- [0.subActionParams.tags]: expected value of type [array] but got [undefined]\n- [1.subAction]: expected value to equal [update]\n- [2.subAction]: expected value to equal [addComment]', + retry: false, + }); + }); + + it('should respond with a 400 Bad Request when creating a case without connector', async () => { + const { body: createdAction } = await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A case connector', + actionTypeId: '.case', + config: {}, + }) + .expect(200); + + createdActionId = createdAction.id; + const params = { + subAction: 'create', + subActionParams: { + title: 'Case from case connector!!', + description: 'case description', + tags: ['case', 'connector'], + }, + }; + + const caseConnector = await supertest + .post(`/api/actions/connector/${createdActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ params }) + .expect(200); + + expect(caseConnector.body).to.eql({ + status: 'error', + actionId: createdActionId, + message: + 'error validating action params: types that failed validation:\n- [0.subActionParams.connector.id]: expected value of type [string] but got [undefined]\n- [1.subAction]: expected value to equal [update]\n- [2.subAction]: expected value to equal [addComment]', + retry: false, + }); + }); + + it('should respond with a 400 Bad Request when creating jira without issueType', async () => { + const { body: createdAction } = await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A case connector', + actionTypeId: '.case', + config: {}, + }) + .expect(200); + + createdActionId = createdAction.id; + const params = { + subAction: 'create', + subActionParams: { + title: 'Case from case connector!!', + description: 'case description', + tags: ['case', 'connector'], + connector: { + id: 'jira', + name: 'Jira', + type: '.jira', + fields: { + priority: 'High', + parent: null, + }, + }, + }, + }; + + const caseConnector = await supertest + .post(`/api/actions/connector/${createdActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ params }) + .expect(200); + + expect(caseConnector.body).to.eql({ + status: 'error', + actionId: createdActionId, + message: + 'error validating action params: types that failed validation:\n- [0.subActionParams.connector.fields.issueType]: expected value of type [string] but got [undefined]\n- [1.subAction]: expected value to equal [update]\n- [2.subAction]: expected value to equal [addComment]', + retry: false, + }); + }); + + it('should respond with a 400 Bad Request when creating a connector with wrong fields', async () => { + const { body: createdAction } = await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A case connector', + actionTypeId: '.case', + config: {}, + }) + .expect(200); + + createdActionId = createdAction.id; + const params = { + subAction: 'create', + subActionParams: { + title: 'Case from case connector!!', + description: 'case description', + tags: ['case', 'connector'], + connector: { + id: 'servicenow', + name: 'Servicenow', + type: '.servicenow', + fields: { + impact: 'Medium', + severity: 'Medium', + notExists: 'not-exists', + }, + }, + }, + }; + + const caseConnector = await supertest + .post(`/api/actions/connector/${createdActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ params }) + .expect(200); + + expect(caseConnector.body).to.eql({ + status: 'error', + actionId: createdActionId, + message: + 'error validating action params: types that failed validation:\n- [0.subActionParams.connector.fields.notExists]: definition for this key is missing\n- [1.subAction]: expected value to equal [update]\n- [2.subAction]: expected value to equal [addComment]', + retry: false, + }); + }); + + it('should respond with a 400 Bad Request when creating a none without fields as null', async () => { + const { body: createdAction } = await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A case connector', + actionTypeId: '.case', + config: {}, + }) + .expect(200); + + createdActionId = createdAction.id; + const params = { + subAction: 'create', + subActionParams: { + title: 'Case from case connector!!', + description: 'case description', + tags: ['case', 'connector'], + connector: { + id: 'none', + name: 'None', + type: '.none', + fields: {}, + }, + }, + }; + + const caseConnector = await supertest + .post(`/api/actions/connector/${createdActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ params }) + .expect(200); + + expect(caseConnector.body).to.eql({ + status: 'error', + actionId: createdActionId, + message: + 'error validating action params: types that failed validation:\n- [0.subActionParams.connector]: Fields must be set to null for connectors of type .none\n- [1.subAction]: expected value to equal [update]\n- [2.subAction]: expected value to equal [addComment]', + retry: false, + }); + }); + + it('should create a case', async () => { + const { body: createdAction } = await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A case connector', + actionTypeId: '.case', + config: {}, + }) + .expect(200); + + createdActionId = createdAction.id; + const params = { + subAction: 'create', + subActionParams: { + title: 'Case from case connector!!', + tags: ['case', 'connector'], + description: 'case description', + connector: { + id: 'jira', + name: 'Jira', + type: '.jira', + fields: { + issueType: '10006', + priority: 'High', + parent: null, + }, + }, + settings: { + syncAlerts: true, + }, + }, + }; + + const caseConnector = await supertest + .post(`/api/actions/connector/${createdActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ params }) + .expect(200); + + const { body } = await supertest + .get(`${CASES_URL}/${caseConnector.body.data.id}`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + const data = removeServerGeneratedPropertiesFromCase(body); + expect(data).to.eql({ + ...postCaseResp(caseConnector.body.data.id), + ...params.subActionParams, + created_by: { + email: null, + full_name: null, + username: null, + }, + }); + }); + + it('should create a case with connector with field as null if not provided', async () => { + const { body: createdAction } = await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A case connector', + actionTypeId: '.case', + config: {}, + }) + .expect(200); + + createdActionId = createdAction.id; + const params = { + subAction: 'create', + subActionParams: { + title: 'Case from case connector!!', + tags: ['case', 'connector'], + description: 'case description', + connector: { + id: 'servicenow', + name: 'Servicenow', + type: '.servicenow', + fields: {}, + }, + settings: { + syncAlerts: true, + }, + }, + }; + + const caseConnector = await supertest + .post(`/api/actions/connector/${createdActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ params }) + .expect(200); + + const { body } = await supertest + .get(`${CASES_URL}/${caseConnector.body.data.id}`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + const data = removeServerGeneratedPropertiesFromCase(body); + expect(data).to.eql({ + ...postCaseResp(caseConnector.body.data.id), + ...params.subActionParams, + connector: { + id: 'servicenow', + name: 'Servicenow', + type: '.servicenow', + fields: { + impact: null, + severity: null, + urgency: null, + category: null, + subcategory: null, + }, + }, + created_by: { + email: null, + full_name: null, + username: null, + }, + }); + }); + }); + + describe.skip('update', () => { + it('should respond with a 400 Bad Request when updating a case without id', async () => { + const { body: createdAction } = await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A case connector', + actionTypeId: '.case', + config: {}, + }) + .expect(200); + + createdActionId = createdAction.id; + const params = { + subAction: 'update', + subActionParams: { + version: '123', + title: 'Case from case connector!!', + }, + }; + + const caseConnector = await supertest + .post(`/api/actions/connector/${createdActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ params }) + .expect(200); + + expect(caseConnector.body).to.eql({ + status: 'error', + actionId: createdActionId, + message: + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [create]\n- [1.subActionParams.id]: expected value of type [string] but got [undefined]\n- [2.subAction]: expected value to equal [addComment]', + retry: false, + }); + }); + + it('should respond with a 400 Bad Request when updating a case without version', async () => { + const { body: createdAction } = await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A case connector', + actionTypeId: '.case', + config: {}, + }) + .expect(200); + + createdActionId = createdAction.id; + const params = { + subAction: 'update', + subActionParams: { + id: '123', + title: 'Case from case connector!!', + }, + }; + + const caseConnector = await supertest + .post(`/api/actions/connector/${createdActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ params }) + .expect(200); + + expect(caseConnector.body).to.eql({ + status: 'error', + actionId: createdActionId, + message: + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [create]\n- [1.subActionParams.version]: expected value of type [string] but got [undefined]\n- [2.subAction]: expected value to equal [addComment]', + retry: false, + }); + }); + + it('should update a case', async () => { + const { body: createdAction } = await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A case connector', + actionTypeId: '.case', + config: {}, + }) + .expect(200); + + createdActionId = createdAction.id; + + const caseRes = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCaseReq) + .expect(200); + + const params = { + subAction: 'update', + subActionParams: { + id: caseRes.body.id, + version: caseRes.body.version, + title: 'Case from case connector!!', + }, + }; + + await supertest + .post(`/api/actions/connector/${createdActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ params }) + .expect(200); + + const { body } = await supertest + .get(`${CASES_URL}/${caseRes.body.id}`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + const data = removeServerGeneratedPropertiesFromCase(body); + expect(data).to.eql({ + ...postCaseResp(caseRes.body.id), + title: 'Case from case connector!!', + updated_by: { + email: null, + full_name: null, + username: null, + }, + }); + }); + }); + + describe.skip('addComment', () => { + it('should respond with a 400 Bad Request when adding a comment to a case without caseId', async () => { + const { body: createdAction } = await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A case connector', + actionTypeId: '.case', + config: {}, + }) + .expect(200); + + createdActionId = createdAction.id; + const params = { + subAction: 'addComment', + subActionParams: { + comment: { comment: 'a comment', type: CommentType.user }, + }, + }; + + const caseConnector = await supertest + .post(`/api/actions/connector/${createdActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ params }) + .expect(200); + + expect(caseConnector.body).to.eql({ + status: 'error', + actionId: createdActionId, + message: + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [create]\n- [1.subAction]: expected value to equal [update]\n- [2.subActionParams.caseId]: expected value of type [string] but got [undefined]', + retry: false, + }); + }); + + it('should respond with a 400 Bad Request when missing attributes of type user', async () => { + const { body: createdAction } = await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A case connector', + actionTypeId: '.case', + config: {}, + }) + .expect(200); + + createdActionId = createdAction.id; + const params = { + subAction: 'addComment', + subActionParams: { + caseId: '123', + }, + }; + + const caseConnector = await supertest + .post(`/api/actions/connector/${createdActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ params }) + .expect(200); + + expect(caseConnector.body).to.eql({ + status: 'error', + actionId: createdActionId, + message: + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [create]\n- [1.subAction]: expected value to equal [update]\n- [2.subActionParams.comment]: expected at least one defined value but got [undefined]', + retry: false, + }); + }); + + describe('adding alerts using a connector', () => { + beforeEach(async () => { + await esArchiver.load('auditbeat/hosts'); + await createSignalsIndex(supertest); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest); + await deleteAllAlerts(supertest); + await esArchiver.unload('auditbeat/hosts'); + }); + + it('should add a comment of type alert', async () => { + const rule = getRuleForSignalTesting(['auditbeat-*']); + const { id } = await createRule(supertest, rule); + await waitForRuleSuccessOrStatus(supertest, id); + await waitForSignalsToBePresent(supertest, 1, [id]); + const signals = await getSignalsByIds(supertest, [id]); + const alert = signals.hits.hits[0]; + + const { body: createdAction } = await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A case connector', + actionTypeId: '.case', + config: {}, + }) + .expect(200); + + createdActionId = createdAction.id; + + const caseRes = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCaseReq) + .expect(200); + + const params = { + subAction: 'addComment', + subActionParams: { + caseId: caseRes.body.id, + comment: { + alertId: alert._id, + index: alert._index, + type: CommentType.alert, + rule: { id: 'id', name: 'name' }, + }, + }, + }; + + const caseConnector = await supertest + .post(`/api/actions/connector/${createdActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ params }) + .expect(200); + + expect(caseConnector.body.status).to.eql('ok'); + + const { body } = await supertest + .get(`${CASES_URL}/${caseRes.body.id}`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + const data = removeServerGeneratedPropertiesFromCase(body); + const comments = removeServerGeneratedPropertiesFromComments(data.comments ?? []); + expect({ ...data, comments }).to.eql({ + ...postCaseResp(caseRes.body.id), + comments, + totalAlerts: 1, + totalComment: 1, + updated_by: { + email: null, + full_name: null, + username: null, + }, + }); + }); + }); + + it('should respond with a 400 Bad Request when missing attributes of type alert', async () => { + const { body: createdAction } = await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A case connector', + actionTypeId: '.case', + config: {}, + }) + .expect(200); + + createdActionId = createdAction.id; + const comment = { + alertId: 'test-id', + index: 'test-index', + type: CommentType.alert, + rule: { id: 'id', name: 'name' }, + }; + const params = { + subAction: 'addComment', + subActionParams: { + caseId: '123', + comment, + }, + }; + + // only omitting alertId here since the type for alertId and index differ, the messages will be different + for (const attribute of ['alertId']) { + const requestAttributes = omit(attribute, comment); + const caseConnector = await supertest + .post(`/api/actions/connector/${createdActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + ...params, + subActionParams: { ...params.subActionParams, comment: requestAttributes }, + }, + }) + .expect(200); + + expect(caseConnector.body).to.eql({ + status: 'error', + actionId: createdActionId, + message: `error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [create]\n- [1.subAction]: expected value to equal [update]\n- [2.subActionParams.comment]: types that failed validation:\n - [subActionParams.comment.0.type]: expected value to equal [user]\n - [subActionParams.comment.1.${attribute}]: expected at least one defined value but got [undefined]\n - [subActionParams.comment.2.type]: expected value to equal [generated_alert]`, + retry: false, + }); + } + }); + + it('should respond with a 400 Bad Request when adding excess attributes for type user', async () => { + const { body: createdAction } = await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A case connector', + actionTypeId: '.case', + config: {}, + }) + .expect(200); + + createdActionId = createdAction.id; + const params = { + subAction: 'addComment', + subActionParams: { + caseId: '123', + comment: { comment: 'a comment', type: CommentType.user }, + }, + }; + + for (const attribute of ['blah', 'bogus']) { + const caseConnector = await supertest + .post(`/api/actions/connector/${createdActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + ...params, + subActionParams: { + ...params.subActionParams, + comment: { ...params.subActionParams.comment, [attribute]: attribute }, + }, + }, + }) + .expect(200); + expect(caseConnector.body).to.eql({ + status: 'error', + actionId: createdActionId, + message: `error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [create]\n- [1.subAction]: expected value to equal [update]\n- [2.subActionParams.comment]: types that failed validation:\n - [subActionParams.comment.0.${attribute}]: definition for this key is missing\n - [subActionParams.comment.1.type]: expected value to equal [alert]\n - [subActionParams.comment.2.type]: expected value to equal [generated_alert]`, + retry: false, + }); + } + }); + + it('should respond with a 400 Bad Request when adding excess attributes for type alert', async () => { + const { body: createdAction } = await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A case connector', + actionTypeId: '.case', + config: {}, + }) + .expect(200); + + createdActionId = createdAction.id; + const params = { + subAction: 'addComment', + subActionParams: { + caseId: '123', + comment: { + alertId: 'test-id', + index: 'test-index', + type: CommentType.alert, + rule: { id: 'id', name: 'name' }, + }, + }, + }; + + for (const attribute of ['comment']) { + const caseConnector = await supertest + .post(`/api/actions/connector/${createdActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + ...params, + subActionParams: { + ...params.subActionParams, + comment: { ...params.subActionParams.comment, [attribute]: attribute }, + }, + }, + }) + .expect(200); + + expect(caseConnector.body).to.eql({ + status: 'error', + actionId: createdActionId, + message: `error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [create]\n- [1.subAction]: expected value to equal [update]\n- [2.subActionParams.comment]: types that failed validation:\n - [subActionParams.comment.0.type]: expected value to equal [user]\n - [subActionParams.comment.1.${attribute}]: definition for this key is missing\n - [subActionParams.comment.2.type]: expected value to equal [generated_alert]`, + retry: false, + }); + } + }); + + it('should respond with a 400 Bad Request when adding a comment to a case without type', async () => { + const { body: createdAction } = await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A case connector', + actionTypeId: '.case', + config: {}, + }) + .expect(200); + + createdActionId = createdAction.id; + const params = { + subAction: 'update', + subActionParams: { + caseId: '123', + comment: { comment: 'a comment' }, + }, + }; + + const caseConnector = await supertest + .post(`/api/actions/connector/${createdActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ params }) + .expect(200); + + expect(caseConnector.body).to.eql({ + status: 'error', + actionId: createdActionId, + message: + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [create]\n- [1.subActionParams.id]: expected value of type [string] but got [undefined]\n- [2.subAction]: expected value to equal [addComment]', + retry: false, + }); + }); + + it('should add a comment of type user', async () => { + const { body: createdAction } = await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A case connector', + actionTypeId: '.case', + config: {}, + }) + .expect(200); + + createdActionId = createdAction.id; + + const caseRes = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCaseReq) + .expect(200); + + const params = { + subAction: 'addComment', + subActionParams: { + caseId: caseRes.body.id, + comment: { comment: 'a comment', type: CommentType.user }, + }, + }; + + await supertest + .post(`/api/actions/connector/${createdActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ params }) + .expect(200); + + const { body } = await supertest + .get(`${CASES_URL}/${caseRes.body.id}`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + const data = removeServerGeneratedPropertiesFromCase(body); + const comments = removeServerGeneratedPropertiesFromComments(data.comments ?? []); + expect({ ...data, comments }).to.eql({ + ...postCaseResp(caseRes.body.id), + comments, + totalComment: 1, + updated_by: { + email: null, + full_name: null, + username: null, + }, + }); + }); + + it('should add a comment of type alert', async () => { + const { body: createdAction } = await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A case connector', + actionTypeId: '.case', + config: {}, + }) + .expect(200); + + createdActionId = createdAction.id; + + const caseRes = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCaseReq) + .expect(200); + + const params = { + subAction: 'addComment', + subActionParams: { + caseId: caseRes.body.id, + comment: { + alertId: 'test-id', + index: 'test-index', + type: CommentType.alert, + rule: { id: 'id', name: 'name' }, + }, + }, + }; + + await supertest + .post(`/api/actions/connector/${createdActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ params }) + .expect(200); + + const { body } = await supertest + .get(`${CASES_URL}/${caseRes.body.id}`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + const data = removeServerGeneratedPropertiesFromCase(body); + const comments = removeServerGeneratedPropertiesFromComments(data.comments ?? []); + expect({ ...data, comments }).to.eql({ + ...postCaseResp(caseRes.body.id), + comments, + totalComment: 1, + totalAlerts: 1, + updated_by: { + email: null, + full_name: null, + username: null, + }, + }); + }); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/security_only/tests/common/index.ts b/x-pack/test/case_api_integration/security_only/tests/common/index.ts new file mode 100644 index 00000000000000..fda3a82dfd3570 --- /dev/null +++ b/x-pack/test/case_api_integration/security_only/tests/common/index.ts @@ -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 { FtrProviderContext } from '../../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default ({ loadTestFile }: FtrProviderContext): void => { + describe('Common', function () { + loadTestFile(require.resolve('./comments/delete_comment')); + loadTestFile(require.resolve('./comments/find_comments')); + loadTestFile(require.resolve('./comments/get_comment')); + loadTestFile(require.resolve('./comments/get_all_comments')); + loadTestFile(require.resolve('./comments/patch_comment')); + loadTestFile(require.resolve('./comments/post_comment')); + loadTestFile(require.resolve('./alerts/get_cases')); + loadTestFile(require.resolve('./cases/delete_cases')); + loadTestFile(require.resolve('./cases/find_cases')); + loadTestFile(require.resolve('./cases/get_case')); + loadTestFile(require.resolve('./cases/patch_cases')); + loadTestFile(require.resolve('./cases/post_case')); + loadTestFile(require.resolve('./cases/reporters/get_reporters')); + loadTestFile(require.resolve('./cases/status/get_status')); + loadTestFile(require.resolve('./cases/tags/get_tags')); + loadTestFile(require.resolve('./user_actions/get_all_user_actions')); + loadTestFile(require.resolve('./configure/get_configure')); + loadTestFile(require.resolve('./configure/get_connectors')); + loadTestFile(require.resolve('./configure/patch_configure')); + loadTestFile(require.resolve('./configure/post_configure')); + loadTestFile(require.resolve('./connectors/case')); + loadTestFile(require.resolve('./sub_cases/patch_sub_cases')); + loadTestFile(require.resolve('./sub_cases/delete_sub_cases')); + loadTestFile(require.resolve('./sub_cases/get_sub_case')); + loadTestFile(require.resolve('./sub_cases/find_sub_cases')); + }); +}; diff --git a/x-pack/test/case_api_integration/security_only/tests/common/sub_cases/delete_sub_cases.ts b/x-pack/test/case_api_integration/security_only/tests/common/sub_cases/delete_sub_cases.ts new file mode 100644 index 00000000000000..951db263a6c784 --- /dev/null +++ b/x-pack/test/case_api_integration/security_only/tests/common/sub_cases/delete_sub_cases.ts @@ -0,0 +1,112 @@ +/* + * 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 { FtrProviderContext } from '../../../../../common/ftr_provider_context'; + +import { + CASES_URL, + SUB_CASES_PATCH_DEL_URL, +} from '../../../../../../plugins/cases/common/constants'; +import { postCommentUserReq } from '../../../../common/lib/mock'; +import { + createCaseAction, + createSubCase, + deleteAllCaseItems, + deleteCaseAction, +} from '../../../../common/lib/utils'; +import { getSubCaseDetailsUrl } from '../../../../../../plugins/cases/common/api/helpers'; +import { CaseResponse } from '../../../../../../plugins/cases/common/api'; + +// eslint-disable-next-line import/no-default-export +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const es = getService('es'); + + // ENABLE_CASE_CONNECTOR: remove this outer describe once the case connector feature is completed + describe('delete_sub_cases disabled routes', () => { + it('should return a 404 when attempting to access the route and the case connector feature is disabled', async () => { + await supertest + .delete(`${SUB_CASES_PATCH_DEL_URL}?ids=["sub-case-id"]`) + .set('kbn-xsrf', 'true') + .send() + .expect(404); + }); + + // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests + describe.skip('delete_sub_cases', () => { + let actionID: string; + before(async () => { + actionID = await createCaseAction(supertest); + }); + after(async () => { + await deleteCaseAction(supertest, actionID); + }); + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + it('should delete a sub case', async () => { + const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); + expect(caseInfo.subCases![0].id).to.not.eql(undefined); + + const { body: subCase } = await supertest + .get(getSubCaseDetailsUrl(caseInfo.id, caseInfo.subCases![0].id)) + .send() + .expect(200); + + expect(subCase.id).to.not.eql(undefined); + + const { body } = await supertest + .delete(`${SUB_CASES_PATCH_DEL_URL}?ids=["${subCase.id}"]`) + .set('kbn-xsrf', 'true') + .send() + .expect(204); + + expect(body).to.eql({}); + await supertest + .get(getSubCaseDetailsUrl(caseInfo.id, caseInfo.subCases![0].id)) + .send() + .expect(404); + }); + + it(`should delete a sub case's comments when that case gets deleted`, async () => { + const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); + expect(caseInfo.subCases![0].id).to.not.eql(undefined); + + // there should be two comments on the sub case now + const { body: patchedCaseWithSubCase }: { body: CaseResponse } = await supertest + .post(`${CASES_URL}/${caseInfo.id}/comments`) + .set('kbn-xsrf', 'true') + .query({ subCaseId: caseInfo.subCases![0].id }) + .send(postCommentUserReq) + .expect(200); + + const subCaseCommentUrl = `${CASES_URL}/${patchedCaseWithSubCase.id}/comments/${ + patchedCaseWithSubCase.comments![1].id + }`; + // make sure we can get the second comment + await supertest.get(subCaseCommentUrl).set('kbn-xsrf', 'true').send().expect(200); + + await supertest + .delete(`${SUB_CASES_PATCH_DEL_URL}?ids=["${patchedCaseWithSubCase.subCases![0].id}"]`) + .set('kbn-xsrf', 'true') + .send() + .expect(204); + + await supertest.get(subCaseCommentUrl).set('kbn-xsrf', 'true').send().expect(404); + }); + + it('unhappy path - 404s when sub case id is invalid', async () => { + await supertest + .delete(`${SUB_CASES_PATCH_DEL_URL}?ids=["fake-id"]`) + .set('kbn-xsrf', 'true') + .send() + .expect(404); + }); + }); + }); +} diff --git a/x-pack/test/case_api_integration/security_only/tests/common/sub_cases/find_sub_cases.ts b/x-pack/test/case_api_integration/security_only/tests/common/sub_cases/find_sub_cases.ts new file mode 100644 index 00000000000000..d54523bec0c4d4 --- /dev/null +++ b/x-pack/test/case_api_integration/security_only/tests/common/sub_cases/find_sub_cases.ts @@ -0,0 +1,480 @@ +/* + * 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 type { ApiResponse, estypes } from '@elastic/elasticsearch'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +import { findSubCasesResp, postCollectionReq } from '../../../../common/lib/mock'; +import { + createCaseAction, + createSubCase, + CreateSubCaseResp, + deleteAllCaseItems, + deleteCaseAction, + setStatus, +} from '../../../../common/lib/utils'; +import { getSubCasesUrl } from '../../../../../../plugins/cases/common/api/helpers'; +import { + CaseResponse, + CaseStatuses, + CommentType, + SubCasesFindResponse, +} from '../../../../../../plugins/cases/common/api'; +import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; +import { + ContextTypeGeneratedAlertType, + createAlertsString, +} from '../../../../../../plugins/cases/server/connectors'; + +interface SubCaseAttributes { + 'cases-sub-case': { + created_at: string; + }; +} + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + + // ENABLE_CASE_CONNECTOR: remove this outer describe once the case connector feature is completed + describe('find_sub_cases disabled route', () => { + it('should return a 404 when attempting to access the route and the case connector feature is disabled', async () => { + await supertest.get(`${getSubCasesUrl('case-id')}/_find`).expect(404); + }); + + // ENABLE_CASE_CONNECTOR: enable these tests once the case connector feature is completed + describe.skip('create case connector', () => { + let actionID: string; + before(async () => { + actionID = await createCaseAction(supertest); + }); + after(async () => { + await deleteCaseAction(supertest, actionID); + }); + + describe('basic find tests', () => { + afterEach(async () => { + await deleteAllCaseItems(es); + }); + it('should not find any sub cases when none exist', async () => { + const { body: caseResp }: { body: CaseResponse } = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCollectionReq) + .expect(200); + + const { body: findSubCases } = await supertest + .get(`${getSubCasesUrl(caseResp.id)}/_find`) + .expect(200); + + expect(findSubCases).to.eql({ + page: 1, + per_page: 20, + total: 0, + subCases: [], + count_open_cases: 0, + count_closed_cases: 0, + count_in_progress_cases: 0, + }); + }); + + it('should return a sub cases with comment stats', async () => { + const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); + + const { body }: { body: SubCasesFindResponse } = await supertest + .get(`${getSubCasesUrl(caseInfo.id)}/_find`) + .expect(200); + + expect(body).to.eql({ + ...findSubCasesResp, + total: 1, + // find should not return the comments themselves only the stats + subCases: [{ ...caseInfo.subCases![0], comments: [], totalComment: 1, totalAlerts: 2 }], + count_open_cases: 1, + }); + }); + + it('should return multiple sub cases', async () => { + const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); + const subCase2Resp = await createSubCase({ supertest, caseID: caseInfo.id, actionID }); + + const { body }: { body: SubCasesFindResponse } = await supertest + .get(`${getSubCasesUrl(caseInfo.id)}/_find`) + .expect(200); + + expect(body).to.eql({ + ...findSubCasesResp, + total: 2, + // find should not return the comments themselves only the stats + subCases: [ + { + // there should only be 1 closed sub case + ...subCase2Resp.modifiedSubCases![0], + comments: [], + totalComment: 1, + totalAlerts: 2, + status: CaseStatuses.closed, + }, + { + ...subCase2Resp.newSubCaseInfo.subCases![0], + comments: [], + totalComment: 1, + totalAlerts: 2, + }, + ], + count_open_cases: 1, + count_closed_cases: 1, + }); + }); + + it('should only return open when filtering for open', async () => { + const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); + // this will result in one closed case and one open + await createSubCase({ supertest, caseID: caseInfo.id, actionID }); + + const { body }: { body: SubCasesFindResponse } = await supertest + .get(`${getSubCasesUrl(caseInfo.id)}/_find?status=${CaseStatuses.open}`) + .expect(200); + + expect(body.total).to.be(1); + expect(body.subCases[0].status).to.be(CaseStatuses.open); + expect(body.count_closed_cases).to.be(1); + expect(body.count_open_cases).to.be(1); + expect(body.count_in_progress_cases).to.be(0); + }); + + it('should only return closed when filtering for closed', async () => { + const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); + // this will result in one closed case and one open + await createSubCase({ supertest, caseID: caseInfo.id, actionID }); + + const { body }: { body: SubCasesFindResponse } = await supertest + .get(`${getSubCasesUrl(caseInfo.id)}/_find?status=${CaseStatuses.closed}`) + .expect(200); + + expect(body.total).to.be(1); + expect(body.subCases[0].status).to.be(CaseStatuses.closed); + expect(body.count_closed_cases).to.be(1); + expect(body.count_open_cases).to.be(1); + expect(body.count_in_progress_cases).to.be(0); + }); + + it('should only return in progress when filtering for in progress', async () => { + const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); + // this will result in one closed case and one open + const { newSubCaseInfo: secondSub } = await createSubCase({ + supertest, + caseID: caseInfo.id, + actionID, + }); + + await setStatus({ + supertest, + cases: [ + { + id: secondSub.subCases![0].id, + version: secondSub.subCases![0].version, + status: CaseStatuses['in-progress'], + }, + ], + type: 'sub_case', + }); + + const { body }: { body: SubCasesFindResponse } = await supertest + .get(`${getSubCasesUrl(caseInfo.id)}/_find?status=${CaseStatuses['in-progress']}`) + .expect(200); + + expect(body.total).to.be(1); + expect(body.subCases[0].status).to.be(CaseStatuses['in-progress']); + expect(body.count_closed_cases).to.be(1); + expect(body.count_open_cases).to.be(0); + expect(body.count_in_progress_cases).to.be(1); + }); + + it('should sort on createdAt field in descending order', async () => { + const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); + // this will result in one closed case and one open + await createSubCase({ + supertest, + caseID: caseInfo.id, + actionID, + }); + + const { body }: { body: SubCasesFindResponse } = await supertest + .get(`${getSubCasesUrl(caseInfo.id)}/_find?sortField=createdAt&sortOrder=desc`) + .expect(200); + + expect(body.total).to.be(2); + expect(body.subCases[0].status).to.be(CaseStatuses.open); + expect(body.subCases[1].status).to.be(CaseStatuses.closed); + expect(body.count_closed_cases).to.be(1); + expect(body.count_open_cases).to.be(1); + expect(body.count_in_progress_cases).to.be(0); + }); + + it('should sort on createdAt field in ascending order', async () => { + const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); + // this will result in one closed case and one open + await createSubCase({ + supertest, + caseID: caseInfo.id, + actionID, + }); + + const { body }: { body: SubCasesFindResponse } = await supertest + .get(`${getSubCasesUrl(caseInfo.id)}/_find?sortField=createdAt&sortOrder=asc`) + .expect(200); + + expect(body.total).to.be(2); + expect(body.subCases[0].status).to.be(CaseStatuses.closed); + expect(body.subCases[1].status).to.be(CaseStatuses.open); + expect(body.count_closed_cases).to.be(1); + expect(body.count_open_cases).to.be(1); + expect(body.count_in_progress_cases).to.be(0); + }); + + it('should sort on updatedAt field in ascending order', async () => { + const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); + // this will result in one closed case and one open + const { newSubCaseInfo: secondSub } = await createSubCase({ + supertest, + caseID: caseInfo.id, + actionID, + }); + + await setStatus({ + supertest, + cases: [ + { + id: secondSub.subCases![0].id, + version: secondSub.subCases![0].version, + status: CaseStatuses['in-progress'], + }, + ], + type: 'sub_case', + }); + + const { body }: { body: SubCasesFindResponse } = await supertest + .get(`${getSubCasesUrl(caseInfo.id)}/_find?sortField=updatedAt&sortOrder=asc`) + .expect(200); + + expect(body.total).to.be(2); + expect(body.subCases[0].status).to.be(CaseStatuses.closed); + expect(body.subCases[1].status).to.be(CaseStatuses['in-progress']); + expect(body.count_closed_cases).to.be(1); + expect(body.count_open_cases).to.be(0); + expect(body.count_in_progress_cases).to.be(1); + }); + }); + + describe('pagination', () => { + const numCases = 4; + let collection: CaseResponse; + before(async () => { + ({ body: collection } = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCollectionReq) + .expect(200)); + + await createSubCases(numCases, collection.id); + }); + + after(async () => { + await deleteAllCaseItems(es); + }); + + const createSubCases = async (total: number, caseID: string) => { + const responses: CreateSubCaseResp[] = []; + for (let i = 0; i < total; i++) { + const postCommentGenAlertReq: ContextTypeGeneratedAlertType = { + alerts: createAlertsString([ + { _id: `${i}`, _index: 'test-index', ruleId: 'rule-id', ruleName: 'rule name' }, + ]), + type: CommentType.generatedAlert, + owner: 'securitySolutionFixture', + }; + responses.push( + await createSubCase({ + supertest, + actionID, + caseID, + comment: postCommentGenAlertReq, + }) + ); + } + return responses; + }; + + const getAllCasesSortedByCreatedAtAsc = async () => { + const cases: ApiResponse> = await es.search({ + index: '.kibana', + body: { + size: 10000, + sort: [{ 'cases-sub-case.created_at': { unmapped_type: 'date', order: 'asc' } }], + query: { + term: { type: 'cases-sub-case' }, + }, + }, + }); + return cases.body.hits.hits.map((hit) => hit._source); + }; + + it('returns the correct total when perPage is less than the total', async () => { + const { body }: { body: SubCasesFindResponse } = await supertest + .get(`${getSubCasesUrl(collection.id)}/_find`) + .query({ + sortOrder: 'asc', + page: 1, + perPage: 3, + }) + .set('kbn-xsrf', 'true') + .expect(200); + + expect(body.subCases.length).to.eql(3); + expect(body.total).to.eql(4); + expect(body.page).to.eql(1); + expect(body.per_page).to.eql(3); + // there will only be 1 open sub case, all the rest will be closed + expect(body.count_open_cases).to.eql(1); + expect(body.count_closed_cases).to.eql(3); + expect(body.count_in_progress_cases).to.eql(0); + }); + + it('returns the correct total when perPage is greater than the total', async () => { + const { body }: { body: SubCasesFindResponse } = await supertest + .get(`${getSubCasesUrl(collection.id)}/_find`) + .query({ + sortOrder: 'asc', + page: 1, + perPage: 11, + }) + .set('kbn-xsrf', 'true') + .expect(200); + + expect(body.total).to.eql(4); + expect(body.page).to.eql(1); + expect(body.per_page).to.eql(11); + expect(body.subCases.length).to.eql(4); + expect(body.count_open_cases).to.eql(1); + expect(body.count_closed_cases).to.eql(3); + expect(body.count_in_progress_cases).to.eql(0); + }); + + it('returns the correct total when perPage is equal to the total', async () => { + const { body }: { body: SubCasesFindResponse } = await supertest + .get(`${getSubCasesUrl(collection.id)}/_find`) + .query({ + sortOrder: 'asc', + page: 1, + perPage: 4, + }) + .set('kbn-xsrf', 'true') + .expect(200); + + expect(body.total).to.eql(4); + expect(body.page).to.eql(1); + expect(body.per_page).to.eql(4); + expect(body.subCases.length).to.eql(4); + expect(body.count_open_cases).to.eql(1); + expect(body.count_closed_cases).to.eql(3); + expect(body.count_in_progress_cases).to.eql(0); + }); + + it('returns the second page of results', async () => { + const perPage = 2; + const { body }: { body: SubCasesFindResponse } = await supertest + .get(`${getSubCasesUrl(collection.id)}/_find`) + .query({ + sortOrder: 'asc', + page: 2, + perPage, + }) + .set('kbn-xsrf', 'true') + .expect(200); + + expect(body.total).to.eql(4); + expect(body.page).to.eql(2); + expect(body.per_page).to.eql(2); + expect(body.subCases.length).to.eql(2); + expect(body.count_open_cases).to.eql(1); + expect(body.count_closed_cases).to.eql(3); + expect(body.count_in_progress_cases).to.eql(0); + + const allCases = await getAllCasesSortedByCreatedAtAsc(); + + body.subCases.map((subCaseInfo, index) => { + // we started on the second page of 10 cases with a perPage of 5, so the first case should 0 + 5 (index + perPage) + expect(subCaseInfo.created_at).to.eql( + allCases[index + perPage]?.['cases-sub-case'].created_at + ); + }); + }); + + it('paginates with perPage of 2 through 4 total sub cases', async () => { + const total = 4; + const perPage = 2; + + // it's less than or equal here because the page starts at 1, so page 2 is a valid page number + // and should have sub cases titles 3, and 4 + for (let currentPage = 1; currentPage <= total / perPage; currentPage++) { + const { body }: { body: SubCasesFindResponse } = await supertest + .get(`${getSubCasesUrl(collection.id)}/_find`) + .query({ + sortOrder: 'asc', + page: currentPage, + perPage, + }) + .set('kbn-xsrf', 'true') + .expect(200); + + expect(body.total).to.eql(total); + expect(body.page).to.eql(currentPage); + expect(body.per_page).to.eql(perPage); + expect(body.subCases.length).to.eql(perPage); + expect(body.count_open_cases).to.eql(1); + expect(body.count_closed_cases).to.eql(total - 1); + expect(body.count_in_progress_cases).to.eql(0); + + const allCases = await getAllCasesSortedByCreatedAtAsc(); + + body.subCases.map((subCaseInfo, index) => { + // for page 1, the cases tiles should be 0,1,2 for page 2: 3,4,5 etc (assuming the titles were sorted + // correctly) + expect(subCaseInfo.created_at).to.eql( + allCases[index + perPage * (currentPage - 1)]?.['cases-sub-case'].created_at + ); + }); + } + }); + + it('retrieves the last sub case', async () => { + const { body }: { body: SubCasesFindResponse } = await supertest + .get(`${getSubCasesUrl(collection.id)}/_find`) + .query({ + sortOrder: 'asc', + // this should skip the first 3 sub cases and only return the last one + page: 2, + perPage: 3, + }) + .set('kbn-xsrf', 'true') + .expect(200); + + expect(body.total).to.eql(4); + expect(body.page).to.eql(2); + expect(body.per_page).to.eql(3); + expect(body.subCases.length).to.eql(1); + expect(body.count_open_cases).to.eql(1); + expect(body.count_closed_cases).to.eql(3); + expect(body.count_in_progress_cases).to.eql(0); + }); + }); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/security_only/tests/common/sub_cases/get_sub_case.ts b/x-pack/test/case_api_integration/security_only/tests/common/sub_cases/get_sub_case.ts new file mode 100644 index 00000000000000..35ed4ba5c3c71b --- /dev/null +++ b/x-pack/test/case_api_integration/security_only/tests/common/sub_cases/get_sub_case.ts @@ -0,0 +1,119 @@ +/* + * 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 { FtrProviderContext } from '../../../../../common/ftr_provider_context'; + +import { commentsResp, postCommentAlertReq, subCaseResp } from '../../../../common/lib/mock'; +import { + createCaseAction, + createSubCase, + defaultCreateSubComment, + deleteAllCaseItems, + deleteCaseAction, + removeServerGeneratedPropertiesFromComments, + removeServerGeneratedPropertiesFromSubCase, +} from '../../../../common/lib/utils'; +import { + getCaseCommentsUrl, + getSubCaseDetailsUrl, +} from '../../../../../../plugins/cases/common/api/helpers'; +import { + AssociationType, + CaseResponse, + SubCaseResponse, +} from '../../../../../../plugins/cases/common/api'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + + // ENABLE_CASE_CONNECTOR: remove the outer describe once the case connector feature is completed + describe('get_sub_case disabled route', () => { + it('should return a 404 when attempting to access the route and the case connector feature is disabled', async () => { + await supertest.get(getSubCaseDetailsUrl('case-id', 'sub-case-id')).expect(404); + }); + + // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests + describe.skip('get_sub_case', () => { + let actionID: string; + before(async () => { + actionID = await createCaseAction(supertest); + }); + after(async () => { + await deleteCaseAction(supertest, actionID); + }); + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + it('should return a case', async () => { + const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); + + const { body }: { body: SubCaseResponse } = await supertest + .get(getSubCaseDetailsUrl(caseInfo.id, caseInfo.subCases![0].id)) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(removeServerGeneratedPropertiesFromComments(body.comments)).to.eql( + commentsResp({ + comments: [{ comment: defaultCreateSubComment, id: caseInfo.comments![0].id }], + associationType: AssociationType.subCase, + }) + ); + + expect(removeServerGeneratedPropertiesFromSubCase(body)).to.eql( + subCaseResp({ id: body.id, totalComment: 1, totalAlerts: 2 }) + ); + }); + + it('should return the correct number of alerts with multiple types of alerts', async () => { + const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); + + const { body: singleAlert }: { body: CaseResponse } = await supertest + .post(getCaseCommentsUrl(caseInfo.id)) + .query({ subCaseId: caseInfo.subCases![0].id }) + .set('kbn-xsrf', 'true') + .send(postCommentAlertReq) + .expect(200); + + const { body }: { body: SubCaseResponse } = await supertest + .get(getSubCaseDetailsUrl(caseInfo.id, caseInfo.subCases![0].id)) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(removeServerGeneratedPropertiesFromComments(body.comments)).to.eql( + commentsResp({ + comments: [ + { comment: defaultCreateSubComment, id: caseInfo.comments![0].id }, + { + comment: postCommentAlertReq, + id: singleAlert.comments![1].id, + }, + ], + associationType: AssociationType.subCase, + }) + ); + + expect(removeServerGeneratedPropertiesFromSubCase(body)).to.eql( + subCaseResp({ id: body.id, totalComment: 2, totalAlerts: 3 }) + ); + }); + + it('unhappy path - 404s when case is not there', async () => { + await supertest + .get(getSubCaseDetailsUrl('fake-case-id', 'fake-sub-case-id')) + .set('kbn-xsrf', 'true') + .send() + .expect(404); + }); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/security_only/tests/common/sub_cases/patch_sub_cases.ts b/x-pack/test/case_api_integration/security_only/tests/common/sub_cases/patch_sub_cases.ts new file mode 100644 index 00000000000000..442644463fa38d --- /dev/null +++ b/x-pack/test/case_api_integration/security_only/tests/common/sub_cases/patch_sub_cases.ts @@ -0,0 +1,515 @@ +/* + * 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 { FtrProviderContext } from '../../../../../common/ftr_provider_context'; + +import { + CASES_URL, + SUB_CASES_PATCH_DEL_URL, +} from '../../../../../../plugins/cases/common/constants'; +import { + createCaseAction, + createSubCase, + deleteAllCaseItems, + deleteCaseAction, + getSignalsWithES, + setStatus, +} from '../../../../common/lib/utils'; +import { getSubCaseDetailsUrl } from '../../../../../../plugins/cases/common/api/helpers'; +import { + CaseStatuses, + CommentType, + SubCaseResponse, +} from '../../../../../../plugins/cases/common/api'; +import { createAlertsString } from '../../../../../../plugins/cases/server/connectors'; +import { postCaseReq, postCollectionReq } from '../../../../common/lib/mock'; + +const defaultSignalsIndex = '.siem-signals-default-000001'; + +// eslint-disable-next-line import/no-default-export +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const es = getService('es'); + const esArchiver = getService('esArchiver'); + + // ENABLE_CASE_CONNECTOR: remove the outer describe once the case connector feature is completed + describe('patch_sub_cases disabled route', () => { + it('should return a 404 when attempting to access the route and the case connector feature is disabled', async () => { + await supertest + .patch(SUB_CASES_PATCH_DEL_URL) + .set('kbn-xsrf', 'true') + .send({ subCases: [] }) + .expect(404); + }); + + // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests + describe.skip('patch_sub_cases', () => { + let actionID: string; + before(async () => { + actionID = await createCaseAction(supertest); + }); + after(async () => { + await deleteCaseAction(supertest, actionID); + }); + beforeEach(async () => { + await esArchiver.load('cases/signals/default'); + }); + afterEach(async () => { + await esArchiver.unload('cases/signals/default'); + await deleteAllCaseItems(es); + }); + + it('should update the status of a sub case', async () => { + const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); + + await setStatus({ + supertest, + cases: [ + { + id: caseInfo.subCases![0].id, + version: caseInfo.subCases![0].version, + status: CaseStatuses['in-progress'], + }, + ], + type: 'sub_case', + }); + const { body: subCase }: { body: SubCaseResponse } = await supertest + .get(getSubCaseDetailsUrl(caseInfo.id, caseInfo.subCases![0].id)) + .expect(200); + + expect(subCase.status).to.eql(CaseStatuses['in-progress']); + }); + + it('should update the status of one alert attached to a sub case', async () => { + const signalID = '5f2b9ec41f8febb1c06b5d1045aeabb9874733b7617e88a370510f2fb3a41a5d'; + + const { newSubCaseInfo: caseInfo } = await createSubCase({ + supertest, + actionID, + comment: { + alerts: createAlertsString([ + { + _id: signalID, + _index: defaultSignalsIndex, + ruleId: 'id', + ruleName: 'name', + }, + ]), + type: CommentType.generatedAlert, + owner: 'securitySolutionFixture', + }, + }); + + await es.indices.refresh({ index: defaultSignalsIndex }); + + let signals = await getSignalsWithES({ es, indices: defaultSignalsIndex, ids: signalID }); + + expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( + CaseStatuses.open + ); + + await setStatus({ + supertest, + cases: [ + { + id: caseInfo.subCases![0].id, + version: caseInfo.subCases![0].version, + status: CaseStatuses['in-progress'], + }, + ], + type: 'sub_case', + }); + + await es.indices.refresh({ index: defaultSignalsIndex }); + + signals = await getSignalsWithES({ es, indices: defaultSignalsIndex, ids: signalID }); + + expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( + CaseStatuses['in-progress'] + ); + }); + + it('should update the status of multiple alerts attached to a sub case', async () => { + const signalID = '5f2b9ec41f8febb1c06b5d1045aeabb9874733b7617e88a370510f2fb3a41a5d'; + + const signalID2 = '4d0f4b1533e46b66b43bdd0330d23f39f2cf42a7253153270e38d30cce9ff0c6'; + + const { newSubCaseInfo: caseInfo } = await createSubCase({ + supertest, + actionID, + comment: { + alerts: createAlertsString([ + { + _id: signalID, + _index: defaultSignalsIndex, + ruleId: 'id', + ruleName: 'name', + }, + { + _id: signalID2, + _index: defaultSignalsIndex, + ruleId: 'id', + ruleName: 'name', + }, + ]), + type: CommentType.generatedAlert, + owner: 'securitySolutionFixture', + }, + }); + + await es.indices.refresh({ index: defaultSignalsIndex }); + + let signals = await getSignalsWithES({ + es, + indices: defaultSignalsIndex, + ids: [signalID, signalID2], + }); + + expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( + CaseStatuses.open + ); + expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source?.signal.status).to.be( + CaseStatuses.open + ); + + await setStatus({ + supertest, + cases: [ + { + id: caseInfo.subCases![0].id, + version: caseInfo.subCases![0].version, + status: CaseStatuses['in-progress'], + }, + ], + type: 'sub_case', + }); + + await es.indices.refresh({ index: defaultSignalsIndex }); + + signals = await getSignalsWithES({ + es, + indices: defaultSignalsIndex, + ids: [signalID, signalID2], + }); + + expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( + CaseStatuses['in-progress'] + ); + expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source?.signal.status).to.be( + CaseStatuses['in-progress'] + ); + }); + + it('should update the status of multiple alerts attached to multiple sub cases in one collection', async () => { + const signalID = '5f2b9ec41f8febb1c06b5d1045aeabb9874733b7617e88a370510f2fb3a41a5d'; + const signalID2 = '4d0f4b1533e46b66b43bdd0330d23f39f2cf42a7253153270e38d30cce9ff0c6'; + + const { newSubCaseInfo: initialCaseInfo } = await createSubCase({ + supertest, + actionID, + caseInfo: { + ...postCollectionReq, + settings: { + syncAlerts: false, + }, + }, + comment: { + alerts: createAlertsString([ + { + _id: signalID, + _index: defaultSignalsIndex, + ruleId: 'id', + ruleName: 'name', + }, + ]), + type: CommentType.generatedAlert, + owner: 'securitySolutionFixture', + }, + }); + + // This will close the first sub case and create a new one + const { newSubCaseInfo: collectionWithSecondSub } = await createSubCase({ + supertest, + actionID, + caseID: initialCaseInfo.id, + comment: { + alerts: createAlertsString([ + { + _id: signalID2, + _index: defaultSignalsIndex, + ruleId: 'id', + ruleName: 'name', + }, + ]), + type: CommentType.generatedAlert, + owner: 'securitySolutionFixture', + }, + }); + + await es.indices.refresh({ index: defaultSignalsIndex }); + + let signals = await getSignalsWithES({ + es, + indices: defaultSignalsIndex, + ids: [signalID, signalID2], + }); + + // There should be no change in their status since syncing is disabled + expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( + CaseStatuses.open + ); + expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source?.signal.status).to.be( + CaseStatuses.open + ); + + await setStatus({ + supertest, + cases: [ + { + id: collectionWithSecondSub.subCases![0].id, + version: collectionWithSecondSub.subCases![0].version, + status: CaseStatuses['in-progress'], + }, + ], + type: 'sub_case', + }); + + await es.indices.refresh({ index: defaultSignalsIndex }); + + signals = await getSignalsWithES({ + es, + indices: defaultSignalsIndex, + ids: [signalID, signalID2], + }); + + // There still should be no change in their status since syncing is disabled + expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( + CaseStatuses.open + ); + expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source?.signal.status).to.be( + CaseStatuses.open + ); + + // Turn sync alerts on + await supertest + .patch(CASES_URL) + .set('kbn-xsrf', 'true') + .send({ + cases: [ + { + id: collectionWithSecondSub.id, + version: collectionWithSecondSub.version, + settings: { syncAlerts: true }, + }, + ], + }) + .expect(200); + + await es.indices.refresh({ index: defaultSignalsIndex }); + + signals = await getSignalsWithES({ + es, + indices: defaultSignalsIndex, + ids: [signalID, signalID2], + }); + + expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( + CaseStatuses.closed + ); + expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source?.signal.status).to.be( + CaseStatuses['in-progress'] + ); + }); + + it('should update the status of alerts attached to a case and sub case when sync settings is turned on', async () => { + const signalID = '5f2b9ec41f8febb1c06b5d1045aeabb9874733b7617e88a370510f2fb3a41a5d'; + const signalID2 = '4d0f4b1533e46b66b43bdd0330d23f39f2cf42a7253153270e38d30cce9ff0c6'; + + const { body: individualCase } = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send({ + ...postCaseReq, + settings: { + syncAlerts: false, + }, + }); + + const { newSubCaseInfo: caseInfo } = await createSubCase({ + supertest, + actionID, + caseInfo: { + ...postCollectionReq, + settings: { + syncAlerts: false, + }, + }, + comment: { + alerts: createAlertsString([ + { + _id: signalID, + _index: defaultSignalsIndex, + ruleId: 'id', + ruleName: 'name', + }, + ]), + type: CommentType.generatedAlert, + owner: 'securitySolutionFixture', + }, + }); + + const { body: updatedIndWithComment } = await supertest + .post(`${CASES_URL}/${individualCase.id}/comments`) + .set('kbn-xsrf', 'true') + .send({ + alertId: signalID2, + index: defaultSignalsIndex, + rule: { id: 'test-rule-id', name: 'test-index-id' }, + type: CommentType.alert, + }) + .expect(200); + + await es.indices.refresh({ index: defaultSignalsIndex }); + + let signals = await getSignalsWithES({ + es, + indices: defaultSignalsIndex, + ids: [signalID, signalID2], + }); + + // There should be no change in their status since syncing is disabled + expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( + CaseStatuses.open + ); + expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source?.signal.status).to.be( + CaseStatuses.open + ); + + await setStatus({ + supertest, + cases: [ + { + id: caseInfo.subCases![0].id, + version: caseInfo.subCases![0].version, + status: CaseStatuses['in-progress'], + }, + ], + type: 'sub_case', + }); + + const updatedIndWithStatus = ( + await setStatus({ + supertest, + cases: [ + { + id: updatedIndWithComment.id, + version: updatedIndWithComment.version, + status: CaseStatuses.closed, + }, + ], + type: 'case', + }) + )[0]; // there should only be a single entry in the response + + await es.indices.refresh({ index: defaultSignalsIndex }); + + signals = await getSignalsWithES({ + es, + indices: defaultSignalsIndex, + ids: [signalID, signalID2], + }); + + // There should still be no change in their status since syncing is disabled + expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( + CaseStatuses.open + ); + expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source?.signal.status).to.be( + CaseStatuses.open + ); + + // Turn sync alerts on + await supertest + .patch(CASES_URL) + .set('kbn-xsrf', 'true') + .send({ + cases: [ + { + id: caseInfo.id, + version: caseInfo.version, + settings: { syncAlerts: true }, + }, + ], + }) + .expect(200); + + await supertest + .patch(CASES_URL) + .set('kbn-xsrf', 'true') + .send({ + cases: [ + { + id: updatedIndWithStatus.id, + version: updatedIndWithStatus.version, + settings: { syncAlerts: true }, + }, + ], + }) + .expect(200); + + await es.indices.refresh({ index: defaultSignalsIndex }); + + signals = await getSignalsWithES({ + es, + indices: defaultSignalsIndex, + ids: [signalID, signalID2], + }); + + // alerts should be updated now that the + expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( + CaseStatuses['in-progress'] + ); + expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source?.signal.status).to.be( + CaseStatuses.closed + ); + }); + + it('404s when sub case id is invalid', async () => { + await supertest + .patch(`${SUB_CASES_PATCH_DEL_URL}`) + .set('kbn-xsrf', 'true') + .send({ + subCases: [ + { + id: 'fake-id', + version: 'blah', + status: CaseStatuses.open, + }, + ], + }) + .expect(404); + }); + + it('406s when updating invalid fields for a sub case', async () => { + const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); + + await supertest + .patch(`${SUB_CASES_PATCH_DEL_URL}`) + .set('kbn-xsrf', 'true') + .send({ + subCases: [ + { + id: caseInfo.subCases![0].id, + version: caseInfo.subCases![0].version, + type: 'blah', + }, + ], + }) + .expect(406); + }); + }); + }); +} diff --git a/x-pack/test/case_api_integration/security_only/tests/common/user_actions/get_all_user_actions.ts b/x-pack/test/case_api_integration/security_only/tests/common/user_actions/get_all_user_actions.ts new file mode 100644 index 00000000000000..35ebb1a4bf7b19 --- /dev/null +++ b/x-pack/test/case_api_integration/security_only/tests/common/user_actions/get_all_user_actions.ts @@ -0,0 +1,395 @@ +/* + * 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 { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; +import { + CaseResponse, + CaseStatuses, + CommentType, +} from '../../../../../../plugins/cases/common/api'; +import { + userActionPostResp, + postCaseReq, + postCommentUserReq, + getPostCaseRequest, +} from '../../../../common/lib/mock'; +import { + deleteAllCaseItems, + createCase, + updateCase, + getCaseUserActions, + superUserSpace1Auth, +} from '../../../../common/lib/utils'; +import { + globalRead, + noKibanaPrivileges, + obsSec, + obsSecRead, + secOnly, + secOnlyRead, + 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'); + + describe('get_all_user_actions', () => { + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + it(`on new case, user action: 'create' should be called with actionFields: ['description', 'status', 'tags', 'title', 'connector', 'settings, owner]`, async () => { + const { body: postedCase } = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCaseReq) + .expect(200); + + const { body } = await supertest + .get(`${CASES_URL}/${postedCase.id}/user_actions`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(body.length).to.eql(1); + + expect(body[0].action_field).to.eql([ + 'description', + 'status', + 'tags', + 'title', + 'connector', + 'settings', + 'owner', + ]); + expect(body[0].action).to.eql('create'); + expect(body[0].old_value).to.eql(null); + expect(JSON.parse(body[0].new_value)).to.eql(userActionPostResp); + }); + + it(`on close case, user action: 'update' should be called with actionFields: ['status']`, async () => { + const { body: postedCase } = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCaseReq) + .expect(200); + + await supertest + .patch(CASES_URL) + .set('kbn-xsrf', 'true') + .send({ + cases: [ + { + id: postedCase.id, + version: postedCase.version, + status: 'closed', + }, + ], + }) + .expect(200); + + const { body } = await supertest + .get(`${CASES_URL}/${postedCase.id}/user_actions`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(body.length).to.eql(2); + expect(body[1].action_field).to.eql(['status']); + expect(body[1].action).to.eql('update'); + expect(body[1].old_value).to.eql('open'); + expect(body[1].new_value).to.eql('closed'); + }); + + it(`on update case connector, user action: 'update' should be called with actionFields: ['connector']`, async () => { + const { body: postedCase } = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCaseReq) + .expect(200); + + const newConnector = { + id: '123', + name: 'Connector', + type: '.jira', + fields: { issueType: 'Task', priority: 'High', parent: null }, + }; + + await supertest + .patch(CASES_URL) + .set('kbn-xsrf', 'true') + .send({ + cases: [ + { + id: postedCase.id, + version: postedCase.version, + connector: newConnector, + }, + ], + }) + .expect(200); + + const { body } = await supertest + .get(`${CASES_URL}/${postedCase.id}/user_actions`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(body.length).to.eql(2); + expect(body[1].action_field).to.eql(['connector']); + expect(body[1].action).to.eql('update'); + expect(JSON.parse(body[1].old_value)).to.eql({ + id: 'none', + name: 'none', + type: '.none', + fields: null, + }); + expect(JSON.parse(body[1].new_value)).to.eql({ + id: '123', + name: 'Connector', + type: '.jira', + fields: { issueType: 'Task', priority: 'High', parent: null }, + }); + }); + + it(`on update tags, user action: 'add' and 'delete' should be called with actionFields: ['tags']`, async () => { + const { body: postedCase } = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCaseReq) + .expect(200); + + await supertest + .patch(CASES_URL) + .set('kbn-xsrf', 'true') + .send({ + cases: [ + { + id: postedCase.id, + version: postedCase.version, + tags: ['cool', 'neat'], + }, + ], + }) + .expect(200); + + const { body } = await supertest + .get(`${CASES_URL}/${postedCase.id}/user_actions`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(body.length).to.eql(3); + expect(body[1].action_field).to.eql(['tags']); + expect(body[1].action).to.eql('add'); + expect(body[1].old_value).to.eql(null); + expect(body[1].new_value).to.eql('cool, neat'); + expect(body[2].action_field).to.eql(['tags']); + expect(body[2].action).to.eql('delete'); + expect(body[2].old_value).to.eql(null); + expect(body[2].new_value).to.eql('defacement'); + }); + + it(`on update title, user action: 'update' should be called with actionFields: ['title']`, async () => { + const { body: postedCase } = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCaseReq) + .expect(200); + + const newTitle = 'Such a great title'; + await supertest + .patch(CASES_URL) + .set('kbn-xsrf', 'true') + .send({ + cases: [ + { + id: postedCase.id, + version: postedCase.version, + title: newTitle, + }, + ], + }) + .expect(200); + + const { body } = await supertest + .get(`${CASES_URL}/${postedCase.id}/user_actions`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(body.length).to.eql(2); + expect(body[1].action_field).to.eql(['title']); + expect(body[1].action).to.eql('update'); + expect(body[1].old_value).to.eql(postCaseReq.title); + expect(body[1].new_value).to.eql(newTitle); + }); + + it(`on update description, user action: 'update' should be called with actionFields: ['description']`, async () => { + const { body: postedCase } = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCaseReq) + .expect(200); + + const newDesc = 'Such a great description'; + await supertest + .patch(CASES_URL) + .set('kbn-xsrf', 'true') + .send({ + cases: [ + { + id: postedCase.id, + version: postedCase.version, + description: newDesc, + }, + ], + }) + .expect(200); + + const { body } = await supertest + .get(`${CASES_URL}/${postedCase.id}/user_actions`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(body.length).to.eql(2); + expect(body[1].action_field).to.eql(['description']); + expect(body[1].action).to.eql('update'); + expect(body[1].old_value).to.eql(postCaseReq.description); + expect(body[1].new_value).to.eql(newDesc); + }); + + it(`on new comment, user action: 'create' should be called with actionFields: ['comments']`, async () => { + const { body: postedCase } = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCaseReq) + .expect(200); + + await supertest + .post(`${CASES_URL}/${postedCase.id}/comments`) + .set('kbn-xsrf', 'true') + .send(postCommentUserReq) + .expect(200); + + const { body } = await supertest + .get(`${CASES_URL}/${postedCase.id}/user_actions`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(body.length).to.eql(2); + expect(body[1].action_field).to.eql(['comment']); + expect(body[1].action).to.eql('create'); + expect(body[1].old_value).to.eql(null); + expect(JSON.parse(body[1].new_value)).to.eql(postCommentUserReq); + }); + + it(`on update comment, user action: 'update' should be called with actionFields: ['comments']`, async () => { + const { body: postedCase } = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCaseReq) + .expect(200); + + const { body: patchedCase } = await supertest + .post(`${CASES_URL}/${postedCase.id}/comments`) + .set('kbn-xsrf', 'true') + .send(postCommentUserReq) + .expect(200); + + const newComment = 'Well I decided to update my comment. So what? Deal with it.'; + await supertest + .patch(`${CASES_URL}/${postedCase.id}/comments`) + .set('kbn-xsrf', 'true') + .send({ + id: patchedCase.comments[0].id, + version: patchedCase.comments[0].version, + comment: newComment, + type: CommentType.user, + owner: 'securitySolutionFixture', + }) + .expect(200); + + const { body } = await supertest + .get(`${CASES_URL}/${postedCase.id}/user_actions`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(body.length).to.eql(3); + expect(body[2].action_field).to.eql(['comment']); + expect(body[2].action).to.eql('update'); + expect(JSON.parse(body[2].old_value)).to.eql(postCommentUserReq); + expect(JSON.parse(body[2].new_value)).to.eql({ + comment: newComment, + type: CommentType.user, + owner: 'securitySolutionFixture', + }); + }); + + describe('rbac', () => { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + let caseInfo: CaseResponse; + beforeEach(async () => { + caseInfo = await createCase(supertestWithoutAuth, getPostCaseRequest(), 200, { + user: superUser, + space: 'space1', + }); + + await updateCase({ + supertest: supertestWithoutAuth, + params: { + cases: [ + { + id: caseInfo.id, + version: caseInfo.version, + status: CaseStatuses.closed, + }, + ], + }, + auth: superUserSpace1Auth, + }); + }); + + it('should get the user actions for a case when the user has the correct permissions', async () => { + for (const user of [globalRead, superUser, secOnly, secOnlyRead, obsSec, obsSecRead]) { + const userActions = await getCaseUserActions({ + supertest: supertestWithoutAuth, + caseID: caseInfo.id, + auth: { user, space: 'space1' }, + }); + + expect(userActions.length).to.eql(2); + } + }); + + for (const scenario of [ + { user: noKibanaPrivileges, space: 'space1' }, + { user: secOnly, space: 'space2' }, + ]) { + it(`should 403 when requesting the user actions of a case with user ${ + scenario.user.username + } with role(s) ${scenario.user.roles.join()} and space ${scenario.space}`, async () => { + await getCaseUserActions({ + supertest: supertestWithoutAuth, + caseID: caseInfo.id, + auth: { user: scenario.user, space: scenario.space }, + expectedHttpCode: 403, + }); + }); + } + }); + }); +}; diff --git a/x-pack/test/case_api_integration/security_only/tests/trial/cases/push_case.ts b/x-pack/test/case_api_integration/security_only/tests/trial/cases/push_case.ts new file mode 100644 index 00000000000000..8a58c59718feb6 --- /dev/null +++ b/x-pack/test/case_api_integration/security_only/tests/trial/cases/push_case.ts @@ -0,0 +1,315 @@ +/* + * 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. + */ + +/* eslint-disable @typescript-eslint/naming-convention */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { ObjectRemover as ActionsRemover } from '../../../../../alerting_api_integration/common/lib'; + +import { + postCaseReq, + defaultUser, + postCommentUserReq, + getPostCaseRequest, +} from '../../../../common/lib/mock'; +import { + getConfigurationRequest, + createCase, + pushCase, + createComment, + updateCase, + getCaseUserActions, + removeServerGeneratedPropertiesFromUserAction, + deleteAllCaseItems, + superUserSpace1Auth, + createCaseWithConnector, +} from '../../../../common/lib/utils'; +import { + ExternalServiceSimulator, + getExternalServiceSimulatorPath, +} from '../../../../../alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin'; +import { CaseStatuses, CaseUserActionResponse } from '../../../../../../plugins/cases/common/api'; +import { + globalRead, + noKibanaPrivileges, + obsOnlyRead, + obsSecRead, + secOnly, + secOnlyRead, + superUser, +} from '../../../../common/lib/authentication/users'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const kibanaServer = getService('kibanaServer'); + const es = getService('es'); + + describe('push_case', () => { + const actionsRemover = new ActionsRemover(supertest); + + let servicenowSimulatorURL: string = ''; + before(() => { + servicenowSimulatorURL = kibanaServer.resolveUrl( + getExternalServiceSimulatorPath(ExternalServiceSimulator.SERVICENOW) + ); + }); + + afterEach(async () => { + await deleteAllCaseItems(es); + await actionsRemover.removeAll(); + }); + + it('should push a case', async () => { + const { postedCase, connector } = await createCaseWithConnector({ + supertest, + servicenowSimulatorURL, + actionsRemover, + }); + const theCase = await pushCase({ + supertest, + caseId: postedCase.id, + connectorId: connector.id, + }); + + const { pushed_at, external_url, ...rest } = theCase.external_service!; + + expect(rest).to.eql({ + pushed_by: defaultUser, + connector_id: connector.id, + connector_name: connector.name, + external_id: '123', + external_title: 'INC01', + }); + + // external_url is of the form http://elastic:changeme@localhost:5620 which is different between various environments like Jekins + expect( + external_url.includes( + 'api/_actions-FTS-external-service-simulators/servicenow/nav_to.do?uri=incident.do?sys_id=123' + ) + ).to.equal(true); + }); + + it('pushes a comment appropriately', async () => { + const { postedCase, connector } = await createCaseWithConnector({ + supertest, + servicenowSimulatorURL, + actionsRemover, + }); + await createComment({ supertest, caseId: postedCase.id, params: postCommentUserReq }); + const theCase = await pushCase({ + supertest, + caseId: postedCase.id, + connectorId: connector.id, + }); + + expect(theCase.comments![0].pushed_by).to.eql(defaultUser); + }); + + it('should pushes a case and closes when closure_type: close-by-pushing', async () => { + const { postedCase, connector } = await createCaseWithConnector({ + configureReq: { + closure_type: 'close-by-pushing', + }, + supertest, + servicenowSimulatorURL, + actionsRemover, + }); + const theCase = await pushCase({ + supertest, + caseId: postedCase.id, + connectorId: connector.id, + }); + + expect(theCase.status).to.eql('closed'); + }); + + it('should create the correct user action', async () => { + const { postedCase, connector } = await createCaseWithConnector({ + supertest, + servicenowSimulatorURL, + actionsRemover, + }); + const pushedCase = await pushCase({ + supertest, + caseId: postedCase.id, + connectorId: connector.id, + }); + const userActions = await getCaseUserActions({ supertest, caseID: pushedCase.id }); + const pushUserAction = removeServerGeneratedPropertiesFromUserAction(userActions[1]); + + const { new_value, ...rest } = pushUserAction as CaseUserActionResponse; + const parsedNewValue = JSON.parse(new_value!); + + expect(rest).to.eql({ + action_field: ['pushed'], + action: 'push-to-service', + action_by: defaultUser, + old_value: null, + case_id: `${postedCase.id}`, + comment_id: null, + sub_case_id: '', + owner: 'securitySolutionFixture', + }); + + expect(parsedNewValue).to.eql({ + pushed_at: pushedCase.external_service!.pushed_at, + pushed_by: defaultUser, + connector_id: connector.id, + connector_name: connector.name, + external_id: '123', + external_title: 'INC01', + external_url: `${servicenowSimulatorURL}/nav_to.do?uri=incident.do?sys_id=123`, + }); + }); + + // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests + it.skip('should push a collection case but not close it when closure_type: close-by-pushing', async () => { + const { postedCase, connector } = await createCaseWithConnector({ + supertest, + servicenowSimulatorURL, + actionsRemover, + configureReq: { + closure_type: 'close-by-pushing', + }, + }); + + const theCase = await pushCase({ + supertest, + caseId: postedCase.id, + connectorId: connector.id, + }); + expect(theCase.status).to.eql(CaseStatuses.open); + }); + + it('unhappy path - 404s when case does not exist', async () => { + await pushCase({ + supertest, + caseId: 'fake-id', + connectorId: 'fake-connector', + expectedHttpCode: 404, + }); + }); + + it('unhappy path - 404s when connector does not exist', async () => { + const postedCase = await createCase(supertest, { + ...postCaseReq, + connector: getConfigurationRequest().connector, + }); + await pushCase({ + supertest, + caseId: postedCase.id, + connectorId: 'fake-connector', + expectedHttpCode: 404, + }); + }); + + it('unhappy path = 409s when case is closed', async () => { + const { postedCase, connector } = await createCaseWithConnector({ + supertest, + servicenowSimulatorURL, + actionsRemover, + }); + await updateCase({ + supertest, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + status: CaseStatuses.closed, + }, + ], + }, + }); + + await pushCase({ + supertest, + caseId: postedCase.id, + connectorId: connector.id, + expectedHttpCode: 409, + }); + }); + + describe('rbac', () => { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + it('should push a case that the user has permissions for', async () => { + const { postedCase, connector } = await createCaseWithConnector({ + supertest, + servicenowSimulatorURL, + actionsRemover, + auth: superUserSpace1Auth, + }); + + await pushCase({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + connectorId: connector.id, + auth: { user: secOnly, space: 'space1' }, + }); + }); + + it('should not push a case that the user does not have permissions for', async () => { + const { postedCase, connector } = await createCaseWithConnector({ + supertest, + servicenowSimulatorURL, + actionsRemover, + auth: superUserSpace1Auth, + createCaseReq: getPostCaseRequest({ owner: 'observabilityFixture' }), + }); + + await pushCase({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + connectorId: connector.id, + auth: { user: secOnly, 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 push a case`, async () => { + const { postedCase, connector } = await createCaseWithConnector({ + supertest, + servicenowSimulatorURL, + actionsRemover, + auth: superUserSpace1Auth, + }); + + await pushCase({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + connectorId: connector.id, + auth: { user, space: 'space1' }, + expectedHttpCode: 403, + }); + }); + } + + it('should not push a case in a space that the user does not have permissions for', async () => { + const { postedCase, connector } = await createCaseWithConnector({ + supertest, + servicenowSimulatorURL, + actionsRemover, + auth: { user: superUser, space: 'space2' }, + }); + + await pushCase({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + connectorId: connector.id, + auth: { user: secOnly, space: 'space2' }, + expectedHttpCode: 403, + }); + }); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/security_only/tests/trial/cases/user_actions/get_all_user_actions.ts b/x-pack/test/case_api_integration/security_only/tests/trial/cases/user_actions/get_all_user_actions.ts new file mode 100644 index 00000000000000..3729b20f82b301 --- /dev/null +++ b/x-pack/test/case_api_integration/security_only/tests/trial/cases/user_actions/get_all_user_actions.ts @@ -0,0 +1,115 @@ +/* + * 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 { FtrProviderContext } from '../../../../../../common/ftr_provider_context'; + +import { CASE_CONFIGURE_URL, CASES_URL } from '../../../../../../../plugins/cases/common/constants'; +import { defaultUser, postCaseReq } from '../../../../../common/lib/mock'; +import { + deleteCasesByESQuery, + deleteCasesUserActions, + deleteComments, + deleteConfiguration, + getConfigurationRequest, + getServiceNowConnector, +} from '../../../../../common/lib/utils'; + +import { ObjectRemover as ActionsRemover } from '../../../../../../alerting_api_integration/common/lib'; +import { + ExternalServiceSimulator, + getExternalServiceSimulatorPath, +} from '../../../../../../alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + const actionsRemover = new ActionsRemover(supertest); + const kibanaServer = getService('kibanaServer'); + + describe('get_all_user_actions', () => { + let servicenowSimulatorURL: string = ''; + before(() => { + servicenowSimulatorURL = kibanaServer.resolveUrl( + getExternalServiceSimulatorPath(ExternalServiceSimulator.SERVICENOW) + ); + }); + afterEach(async () => { + await deleteCasesByESQuery(es); + await deleteComments(es); + await deleteConfiguration(es); + await deleteCasesUserActions(es); + await actionsRemover.removeAll(); + }); + + it(`on new push to service, user action: 'push-to-service' should be called with actionFields: ['pushed']`, async () => { + const { body: connector } = await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'true') + .send({ + ...getServiceNowConnector(), + config: { apiUrl: servicenowSimulatorURL }, + }) + .expect(200); + + actionsRemover.add('default', connector.id, 'action', 'actions'); + + const { body: configure } = await supertest + .post(CASE_CONFIGURE_URL) + .set('kbn-xsrf', 'true') + .send( + getConfigurationRequest({ + id: connector.id, + name: connector.name, + type: connector.connector_type_id, + }) + ) + .expect(200); + + const { body: postedCase } = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send({ + ...postCaseReq, + connector: getConfigurationRequest({ + id: connector.id, + name: connector.name, + type: connector.connector_type_id, + fields: { + urgency: '2', + impact: '2', + severity: '2', + category: 'software', + subcategory: 'os', + }, + }).connector, + }) + .expect(200); + + await supertest + .post(`${CASES_URL}/${postedCase.id}/connector/${connector.id}/_push`) + .set('kbn-xsrf', 'true') + .send({}) + .expect(200); + + const { body } = await supertest + .get(`${CASES_URL}/${postedCase.id}/user_actions`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(body.length).to.eql(2); + expect(body[1].action_field).to.eql(['pushed']); + expect(body[1].action).to.eql('push-to-service'); + expect(body[1].old_value).to.eql(null); + const newValue = JSON.parse(body[1].new_value); + expect(newValue.connector_id).to.eql(configure.connector.id); + expect(newValue.pushed_by).to.eql(defaultUser); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/security_only/tests/trial/configure/get_configure.ts b/x-pack/test/case_api_integration/security_only/tests/trial/configure/get_configure.ts new file mode 100644 index 00000000000000..ff8f1cff884aff --- /dev/null +++ b/x-pack/test/case_api_integration/security_only/tests/trial/configure/get_configure.ts @@ -0,0 +1,98 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; + +import { + ExternalServiceSimulator, + getExternalServiceSimulatorPath, +} from '../../../../../alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin'; + +import { ObjectRemover as ActionsRemover } from '../../../../../alerting_api_integration/common/lib'; +import { + getServiceNowConnector, + createConnector, + createConfiguration, + getConfiguration, + getConfigurationRequest, + removeServerGeneratedPropertiesFromSavedObject, + getConfigurationOutput, +} from '../../../../common/lib/utils'; +import { ConnectorTypes } from '../../../../../../plugins/cases/common/api'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const actionsRemover = new ActionsRemover(supertest); + const kibanaServer = getService('kibanaServer'); + + describe('get_configure', () => { + let servicenowSimulatorURL: string = ''; + + before(() => { + servicenowSimulatorURL = kibanaServer.resolveUrl( + getExternalServiceSimulatorPath(ExternalServiceSimulator.SERVICENOW) + ); + }); + + afterEach(async () => { + await actionsRemover.removeAll(); + }); + + it('should return a configuration with mapping', async () => { + const connector = await createConnector({ + supertest, + req: { + ...getServiceNowConnector(), + config: { apiUrl: servicenowSimulatorURL }, + }, + }); + actionsRemover.add('default', connector.id, 'action', 'actions'); + + await createConfiguration( + supertest, + getConfigurationRequest({ + id: connector.id, + name: connector.name, + type: connector.connector_type_id as ConnectorTypes, + }) + ); + + const configuration = await getConfiguration({ supertest }); + + const data = removeServerGeneratedPropertiesFromSavedObject(configuration[0]); + expect(data).to.eql( + getConfigurationOutput(false, { + mappings: [ + { + action_type: 'overwrite', + source: 'title', + target: 'short_description', + }, + { + action_type: 'overwrite', + source: 'description', + target: 'description', + }, + { + action_type: 'append', + source: 'comments', + target: 'work_notes', + }, + ], + connector: { + id: connector.id, + name: connector.name, + type: connector.connector_type_id, + fields: null, + }, + }) + ); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/security_only/tests/trial/configure/get_connectors.ts b/x-pack/test/case_api_integration/security_only/tests/trial/configure/get_connectors.ts new file mode 100644 index 00000000000000..fb922f8d102434 --- /dev/null +++ b/x-pack/test/case_api_integration/security_only/tests/trial/configure/get_connectors.ts @@ -0,0 +1,129 @@ +/* + * 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 { FtrProviderContext } from '../../../../../common/ftr_provider_context'; + +import { CASE_CONFIGURE_CONNECTORS_URL } from '../../../../../../plugins/cases/common/constants'; +import { ObjectRemover as ActionsRemover } from '../../../../../alerting_api_integration/common/lib'; +import { + getServiceNowConnector, + getJiraConnector, + getResilientConnector, + createConnector, + getServiceNowSIRConnector, +} from '../../../../common/lib/utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const actionsRemover = new ActionsRemover(supertest); + + describe('get_connectors', () => { + afterEach(async () => { + await actionsRemover.removeAll(); + }); + + it('should return the correct connectors', async () => { + const { body: snConnector } = await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'true') + .send(getServiceNowConnector()) + .expect(200); + + const { body: emailConnector } = await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'true') + .send({ + name: 'An email action', + connector_type_id: '.email', + config: { + service: '__json', + from: 'bob@example.com', + }, + secrets: { + user: 'bob', + password: 'supersecret', + }, + }) + .expect(200); + + const { body: jiraConnector } = await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'true') + .send(getJiraConnector()) + .expect(200); + + const { body: resilientConnector } = await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'true') + .send(getResilientConnector()) + .expect(200); + + const sir = await createConnector({ supertest, req: getServiceNowSIRConnector() }); + + actionsRemover.add('default', sir.id, 'action', 'actions'); + actionsRemover.add('default', snConnector.id, 'action', 'actions'); + actionsRemover.add('default', emailConnector.id, 'action', 'actions'); + actionsRemover.add('default', jiraConnector.id, 'action', 'actions'); + actionsRemover.add('default', resilientConnector.id, 'action', 'actions'); + + const { body: connectors } = await supertest + .get(`${CASE_CONFIGURE_CONNECTORS_URL}/_find`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(connectors).to.eql([ + { + id: jiraConnector.id, + actionTypeId: '.jira', + name: 'Jira Connector', + config: { + apiUrl: 'http://some.non.existent.com', + projectKey: 'pkey', + }, + isPreconfigured: false, + isMissingSecrets: false, + referencedByCount: 0, + }, + { + id: resilientConnector.id, + actionTypeId: '.resilient', + name: 'Resilient Connector', + config: { + apiUrl: 'http://some.non.existent.com', + orgId: 'pkey', + }, + isPreconfigured: false, + isMissingSecrets: false, + referencedByCount: 0, + }, + { + id: snConnector.id, + actionTypeId: '.servicenow', + name: 'ServiceNow Connector', + config: { + apiUrl: 'http://some.non.existent.com', + }, + isPreconfigured: false, + isMissingSecrets: false, + referencedByCount: 0, + }, + { + id: sir.id, + actionTypeId: '.servicenow-sir', + name: 'ServiceNow Connector', + config: { apiUrl: 'http://some.non.existent.com' }, + isPreconfigured: false, + isMissingSecrets: false, + referencedByCount: 0, + }, + ]); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/security_only/tests/trial/configure/index.ts b/x-pack/test/case_api_integration/security_only/tests/trial/configure/index.ts new file mode 100644 index 00000000000000..0c8c3931d15774 --- /dev/null +++ b/x-pack/test/case_api_integration/security_only/tests/trial/configure/index.ts @@ -0,0 +1,18 @@ +/* + * 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 { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default ({ loadTestFile }: FtrProviderContext): void => { + describe('configuration tests', function () { + loadTestFile(require.resolve('./get_configure')); + loadTestFile(require.resolve('./get_connectors')); + loadTestFile(require.resolve('./patch_configure')); + loadTestFile(require.resolve('./post_configure')); + }); +}; diff --git a/x-pack/test/case_api_integration/security_only/tests/trial/configure/patch_configure.ts b/x-pack/test/case_api_integration/security_only/tests/trial/configure/patch_configure.ts new file mode 100644 index 00000000000000..789b68b19beb66 --- /dev/null +++ b/x-pack/test/case_api_integration/security_only/tests/trial/configure/patch_configure.ts @@ -0,0 +1,168 @@ +/* + * 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 { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { ObjectRemover as ActionsRemover } from '../../../../../alerting_api_integration/common/lib'; +import { + ExternalServiceSimulator, + getExternalServiceSimulatorPath, +} from '../../../../../alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin'; + +import { + getConfigurationRequest, + removeServerGeneratedPropertiesFromSavedObject, + getConfigurationOutput, + deleteConfiguration, + createConfiguration, + updateConfiguration, + getServiceNowConnector, + createConnector, +} from '../../../../common/lib/utils'; +import { ConnectorTypes } from '../../../../../../plugins/cases/common/api'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + const kibanaServer = getService('kibanaServer'); + + describe('patch_configure', () => { + const actionsRemover = new ActionsRemover(supertest); + let servicenowSimulatorURL: string = ''; + + before(() => { + servicenowSimulatorURL = kibanaServer.resolveUrl( + getExternalServiceSimulatorPath(ExternalServiceSimulator.SERVICENOW) + ); + }); + + afterEach(async () => { + await deleteConfiguration(es); + await actionsRemover.removeAll(); + }); + + it('should patch a configuration connector and create mappings', async () => { + const connector = await createConnector({ + supertest, + req: { + ...getServiceNowConnector(), + config: { apiUrl: servicenowSimulatorURL }, + }, + }); + + actionsRemover.add('default', connector.id, 'action', 'actions'); + + // Configuration is created with no connector so the mappings are empty + const configuration = await createConfiguration(supertest); + + // the update request doesn't accept the owner field + const { owner, ...reqWithoutOwner } = getConfigurationRequest({ + id: connector.id, + name: connector.name, + type: connector.connector_type_id as ConnectorTypes, + fields: null, + }); + + const newConfiguration = await updateConfiguration(supertest, configuration.id, { + ...reqWithoutOwner, + version: configuration.version, + }); + + const data = removeServerGeneratedPropertiesFromSavedObject(newConfiguration); + expect(data).to.eql({ + ...getConfigurationOutput(true), + connector: { + id: connector.id, + name: connector.name, + type: connector.connector_type_id as ConnectorTypes, + fields: null, + }, + mappings: [ + { + action_type: 'overwrite', + source: 'title', + target: 'short_description', + }, + { + action_type: 'overwrite', + source: 'description', + target: 'description', + }, + { + action_type: 'append', + source: 'comments', + target: 'work_notes', + }, + ], + }); + }); + + it('should mappings when updating the connector', async () => { + const connector = await createConnector({ + supertest, + req: { + ...getServiceNowConnector(), + config: { apiUrl: servicenowSimulatorURL }, + }, + }); + + actionsRemover.add('default', connector.id, 'action', 'actions'); + + // Configuration is created with connector so the mappings are created + const configuration = await createConfiguration( + supertest, + getConfigurationRequest({ + id: connector.id, + name: connector.name, + type: connector.connector_type_id as ConnectorTypes, + }) + ); + + // the update request doesn't accept the owner field + const { owner, ...rest } = getConfigurationRequest({ + id: connector.id, + name: 'New name', + type: connector.connector_type_id as ConnectorTypes, + fields: null, + }); + + const newConfiguration = await updateConfiguration(supertest, configuration.id, { + ...rest, + version: configuration.version, + }); + + const data = removeServerGeneratedPropertiesFromSavedObject(newConfiguration); + expect(data).to.eql({ + ...getConfigurationOutput(true), + connector: { + id: connector.id, + name: 'New name', + type: connector.connector_type_id as ConnectorTypes, + fields: null, + }, + mappings: [ + { + action_type: 'overwrite', + source: 'title', + target: 'short_description', + }, + { + action_type: 'overwrite', + source: 'description', + target: 'description', + }, + { + action_type: 'append', + source: 'comments', + target: 'work_notes', + }, + ], + }); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/security_only/tests/trial/configure/post_configure.ts b/x-pack/test/case_api_integration/security_only/tests/trial/configure/post_configure.ts new file mode 100644 index 00000000000000..96ffcf4bc3f5c5 --- /dev/null +++ b/x-pack/test/case_api_integration/security_only/tests/trial/configure/post_configure.ts @@ -0,0 +1,98 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { ConnectorTypes } from '../../../../../../plugins/cases/common/api'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { ObjectRemover as ActionsRemover } from '../../../../../alerting_api_integration/common/lib'; +import { + ExternalServiceSimulator, + getExternalServiceSimulatorPath, +} from '../../../../../alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin'; + +import { + getConfigurationRequest, + removeServerGeneratedPropertiesFromSavedObject, + getConfigurationOutput, + deleteConfiguration, + createConfiguration, + createConnector, + getServiceNowConnector, +} from '../../../../common/lib/utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + const kibanaServer = getService('kibanaServer'); + + describe('post_configure', () => { + const actionsRemover = new ActionsRemover(supertest); + let servicenowSimulatorURL: string = ''; + + before(() => { + servicenowSimulatorURL = kibanaServer.resolveUrl( + getExternalServiceSimulatorPath(ExternalServiceSimulator.SERVICENOW) + ); + }); + + afterEach(async () => { + await deleteConfiguration(es); + await actionsRemover.removeAll(); + }); + + it('should create a configuration with mapping', async () => { + const connector = await createConnector({ + supertest, + req: { + ...getServiceNowConnector(), + config: { apiUrl: servicenowSimulatorURL }, + }, + }); + + actionsRemover.add('default', connector.id, 'action', 'actions'); + + const postRes = await createConfiguration( + supertest, + getConfigurationRequest({ + id: connector.id, + name: connector.name, + type: connector.connector_type_id as ConnectorTypes, + }) + ); + + const data = removeServerGeneratedPropertiesFromSavedObject(postRes); + expect(data).to.eql( + getConfigurationOutput(false, { + mappings: [ + { + action_type: 'overwrite', + source: 'title', + target: 'short_description', + }, + { + action_type: 'overwrite', + source: 'description', + target: 'description', + }, + { + action_type: 'append', + source: 'comments', + target: 'work_notes', + }, + ], + connector: { + id: connector.id, + name: connector.name, + type: connector.connector_type_id, + fields: null, + }, + }) + ); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/security_only/tests/trial/index.ts b/x-pack/test/case_api_integration/security_only/tests/trial/index.ts new file mode 100644 index 00000000000000..0768db843d0ed7 --- /dev/null +++ b/x-pack/test/case_api_integration/security_only/tests/trial/index.ts @@ -0,0 +1,34 @@ +/* + * 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 { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { createUsersAndRoles, deleteUsersAndRoles } from '../../../common/lib/authentication'; + +// eslint-disable-next-line import/no-default-export +export default ({ loadTestFile, getService }: FtrProviderContext): void => { + describe('cases security and spaces enabled: trial', function () { + // Fastest ciGroup for the moment. + this.tags('ciGroup5'); + + before(async () => { + // since spaces are disabled this changes each role to have access to all available spaces (it'll just be the default one) + await createUsersAndRoles(getService, ['*']); + }); + + after(async () => { + await deleteUsersAndRoles(getService); + }); + + // Trial + loadTestFile(require.resolve('./cases/push_case')); + loadTestFile(require.resolve('./cases/user_actions/get_all_user_actions')); + loadTestFile(require.resolve('./configure/index')); + + // Common + loadTestFile(require.resolve('../common')); + }); +}; From 3f8fcd81f3ead2162573dab8c892bc3614ab8ecf Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Mon, 10 May 2021 15:16:52 -0400 Subject: [PATCH 08/12] Adding remainder security only tests --- .../case_api_integration/common/lib/utils.ts | 4 +- .../tests/common/alerts/get_cases.ts | 64 +- .../tests/common/cases/delete_cases.ts | 306 +--- .../tests/common/cases/find_cases.ts | 937 +++--------- .../tests/common/cases/get_case.ts | 194 +-- .../tests/common/cases/patch_cases.ts | 1327 +++-------------- .../tests/common/cases/post_case.ts | 284 +--- .../common/cases/reporters/get_reporters.ts | 253 ++-- .../tests/common/cases/status/get_status.ts | 182 +-- .../tests/common/cases/tags/get_tags.ts | 268 ++-- .../tests/common/comments/delete_comment.ts | 417 ++---- .../tests/common/comments/find_comments.ts | 503 +++---- .../tests/common/comments/get_all_comments.ts | 235 +-- .../tests/common/comments/get_comment.ts | 173 +-- .../tests/common/comments/migrations.ts | 37 - .../tests/common/comments/patch_comment.ts | 592 +------- .../tests/common/comments/post_comment.ts | 591 +------- .../tests/common/configure/get_configure.ts | 290 ++-- .../tests/common/configure/get_connectors.ts | 27 - .../tests/common/configure/patch_configure.ts | 225 +-- .../tests/common/configure/post_configure.ts | 291 +--- .../tests/common/connectors/case.ts | 1078 ------------- .../security_only/tests/common/index.ts | 6 - .../common/sub_cases/delete_sub_cases.ts | 112 -- .../tests/common/sub_cases/find_sub_cases.ts | 480 ------ .../tests/common/sub_cases/get_sub_case.ts | 119 -- .../tests/common/sub_cases/patch_sub_cases.ts | 515 ------- .../user_actions/get_all_user_actions.ts | 384 +---- .../tests/trial/cases/push_case.ts | 253 +--- .../user_actions/get_all_user_actions.ts | 115 -- .../tests/trial/configure/get_configure.ts | 98 -- .../tests/trial/configure/get_connectors.ts | 129 -- .../tests/trial/configure/index.ts | 18 - .../tests/trial/configure/patch_configure.ts | 168 --- .../tests/trial/configure/post_configure.ts | 98 -- .../security_only/tests/trial/index.ts | 2 - .../security_only/utils.ts | 14 + 37 files changed, 1758 insertions(+), 9031 deletions(-) delete mode 100644 x-pack/test/case_api_integration/security_only/tests/common/comments/migrations.ts delete mode 100644 x-pack/test/case_api_integration/security_only/tests/common/configure/get_connectors.ts delete mode 100644 x-pack/test/case_api_integration/security_only/tests/common/connectors/case.ts delete mode 100644 x-pack/test/case_api_integration/security_only/tests/common/sub_cases/delete_sub_cases.ts delete mode 100644 x-pack/test/case_api_integration/security_only/tests/common/sub_cases/find_sub_cases.ts delete mode 100644 x-pack/test/case_api_integration/security_only/tests/common/sub_cases/get_sub_case.ts delete mode 100644 x-pack/test/case_api_integration/security_only/tests/common/sub_cases/patch_sub_cases.ts delete mode 100644 x-pack/test/case_api_integration/security_only/tests/trial/cases/user_actions/get_all_user_actions.ts delete mode 100644 x-pack/test/case_api_integration/security_only/tests/trial/configure/get_configure.ts delete mode 100644 x-pack/test/case_api_integration/security_only/tests/trial/configure/get_connectors.ts delete mode 100644 x-pack/test/case_api_integration/security_only/tests/trial/configure/index.ts delete mode 100644 x-pack/test/case_api_integration/security_only/tests/trial/configure/patch_configure.ts delete mode 100644 x-pack/test/case_api_integration/security_only/tests/trial/configure/post_configure.ts create mode 100644 x-pack/test/case_api_integration/security_only/utils.ts diff --git a/x-pack/test/case_api_integration/common/lib/utils.ts b/x-pack/test/case_api_integration/common/lib/utils.ts index b7a713b6316cb8..daa6de208abae8 100644 --- a/x-pack/test/case_api_integration/common/lib/utils.ts +++ b/x-pack/test/case_api_integration/common/lib/utils.ts @@ -546,7 +546,9 @@ export const superUserSpace1Auth = getAuthWithSuperUser(); * Returns an auth object with the specified space and user set as super user. The result can be passed to other utility * functions. */ -export function getAuthWithSuperUser(space: string = 'space1'): { user: User; space: string } { +export function getAuthWithSuperUser( + space: string | null = 'space1' +): { user: User; space: string | null } { return { user: superUser, space }; } diff --git a/x-pack/test/case_api_integration/security_only/tests/common/alerts/get_cases.ts b/x-pack/test/case_api_integration/security_only/tests/common/alerts/get_cases.ts index d8b3bd4ae7c126..568786184cb84b 100644 --- a/x-pack/test/case_api_integration/security_only/tests/common/alerts/get_cases.ts +++ b/x-pack/test/case_api_integration/security_only/tests/common/alerts/get_cases.ts @@ -26,6 +26,7 @@ import { secOnlyRead, superUser, } from '../../../../common/lib/authentication/users'; +import { secOnlyNoSpaceAuth } from '../../../utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -38,6 +39,7 @@ export default ({ getService }: FtrProviderContext): void => { }); const supertestWithoutAuth = getService('supertestWithoutAuth'); + const obsSecAuth = { user: obsSec, space: null }; it('should return the correct case IDs', async () => { const secOnlyAuth = { user: secOnly, space: null }; @@ -107,30 +109,28 @@ export default ({ getService }: FtrProviderContext): void => { } }); - for (const scenario of [{ user: noKibanaPrivileges, space: null }]) { - it(`User ${scenario.user.username} with role(s) ${scenario.user.roles.join()} and space ${ - scenario.space - } - should not get cases`, async () => { - const caseInfo = await createCase(supertest, getPostCaseRequest(), 200, { - user: superUser, - space: scenario.space, - }); + it(`User ${ + noKibanaPrivileges.username + } with role(s) ${noKibanaPrivileges.roles.join()} - should not get cases`, async () => { + const caseInfo = await createCase(supertest, getPostCaseRequest(), 200, { + user: superUser, + space: null, + }); - await createComment({ - supertest: supertestWithoutAuth, - caseId: caseInfo.id, - params: postCommentAlertReq, - auth: { user: superUser, space: scenario.space }, - }); + await createComment({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + params: postCommentAlertReq, + auth: { user: superUser, space: null }, + }); - await getCaseIDsByAlert({ - supertest: supertestWithoutAuth, - alertID: postCommentAlertReq.alertId as string, - auth: { user: scenario.user, space: scenario.space }, - expectedHttpCode: 403, - }); + await getCaseIDsByAlert({ + supertest: supertestWithoutAuth, + alertID: postCommentAlertReq.alertId as string, + auth: { user: noKibanaPrivileges, space: null }, + expectedHttpCode: 403, }); - } + }); it('should return a 404 when attempting to access a space', async () => { const auth = { user: obsSec, space: null }; @@ -169,14 +169,13 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should respect the owner filter when have permissions', async () => { - const auth = { user: obsSec, space: null }; const [case1, case2] = await Promise.all([ - createCase(supertestWithoutAuth, getPostCaseRequest(), 200, auth), + createCase(supertestWithoutAuth, getPostCaseRequest(), 200, obsSecAuth), createCase( supertestWithoutAuth, { ...getPostCaseRequest(), owner: 'observabilityFixture' }, 200, - auth + obsSecAuth ), ]); @@ -185,20 +184,20 @@ export default ({ getService }: FtrProviderContext): void => { supertest: supertestWithoutAuth, caseId: case1.id, params: postCommentAlertReq, - auth, + auth: obsSecAuth, }), createComment({ supertest: supertestWithoutAuth, caseId: case2.id, params: { ...postCommentAlertReq, owner: 'observabilityFixture' }, - auth, + auth: obsSecAuth, }), ]); const res = await getCaseIDsByAlert({ supertest: supertestWithoutAuth, alertID: postCommentAlertReq.alertId as string, - auth, + auth: obsSecAuth, query: { owner: 'securitySolutionFixture' }, }); @@ -206,14 +205,13 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should return the correct case IDs when the owner query parameter contains unprivileged values', async () => { - const auth = { user: obsSec, space: null }; const [case1, case2] = await Promise.all([ - createCase(supertestWithoutAuth, getPostCaseRequest(), 200, auth), + createCase(supertestWithoutAuth, getPostCaseRequest(), 200, obsSecAuth), createCase( supertestWithoutAuth, { ...getPostCaseRequest(), owner: 'observabilityFixture' }, 200, - auth + obsSecAuth ), ]); @@ -222,20 +220,20 @@ export default ({ getService }: FtrProviderContext): void => { supertest: supertestWithoutAuth, caseId: case1.id, params: postCommentAlertReq, - auth, + auth: obsSecAuth, }), createComment({ supertest: supertestWithoutAuth, caseId: case2.id, params: { ...postCommentAlertReq, owner: 'observabilityFixture' }, - auth, + auth: obsSecAuth, }), ]); const res = await getCaseIDsByAlert({ supertest: supertestWithoutAuth, alertID: postCommentAlertReq.alertId as string, - auth: { user: secOnly, space: null }, + auth: secOnlyNoSpaceAuth, // The secOnly user does not have permissions for observability cases, so it should only return the security solution one query: { owner: ['securitySolutionFixture', 'observabilityFixture'] }, }); diff --git a/x-pack/test/case_api_integration/security_only/tests/common/cases/delete_cases.ts b/x-pack/test/case_api_integration/security_only/tests/common/cases/delete_cases.ts index 03bcf0d538fe3b..d60e1b011d9925 100644 --- a/x-pack/test/case_api_integration/security_only/tests/common/cases/delete_cases.ts +++ b/x-pack/test/case_api_integration/security_only/tests/common/cases/delete_cases.ts @@ -5,30 +5,17 @@ * 2.0. */ -import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; -import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; -import { defaultUser, getPostCaseRequest, postCommentUserReq } from '../../../../common/lib/mock'; +import { getPostCaseRequest } from '../../../../common/lib/mock'; import { - createCaseAction, - createSubCase, - deleteAllCaseItems, - deleteCaseAction, deleteCasesByESQuery, deleteCasesUserActions, deleteComments, createCase, deleteCases, - createComment, - getComment, - removeServerGeneratedPropertiesFromUserAction, getCase, - superUserSpace1Auth, - getCaseUserActions, } from '../../../../common/lib/utils'; -import { getSubCaseDetailsUrl } from '../../../../../../plugins/cases/common/api/helpers'; -import { CaseResponse } from '../../../../../../plugins/cases/common/api'; import { secOnly, secOnlyRead, @@ -36,9 +23,9 @@ import { obsOnlyRead, obsSecRead, noKibanaPrivileges, - obsOnly, superUser, } from '../../../../common/lib/authentication/users'; +import { obsOnlyNoSpaceAuth, secOnlyNoSpaceAuth } from '../../../utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -53,248 +40,105 @@ export default ({ getService }: FtrProviderContext): void => { await deleteCasesUserActions(es); }); - it('should delete a case', async () => { - const postedCase = await createCase(supertest, getPostCaseRequest()); - const body = await deleteCases({ supertest, caseIDs: [postedCase.id] }); + it('User: security solution only - should delete a case', async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest(), + 200, + secOnlyNoSpaceAuth + ); - expect(body).to.eql({}); - }); - - it(`should delete a case's comments when that case gets deleted`, async () => { - const postedCase = await createCase(supertest, getPostCaseRequest()); - const patchedCase = await createComment({ - supertest, - caseId: postedCase.id, - params: postCommentUserReq, - }); - // ensure that we can get the comment before deleting the case - await getComment({ - supertest, - caseId: postedCase.id, - commentId: patchedCase.comments![0].id, - }); - - await deleteCases({ supertest, caseIDs: [postedCase.id] }); - - // make sure the comment is now gone - await getComment({ + await deleteCases({ supertest, - caseId: postedCase.id, - commentId: patchedCase.comments![0].id, - expectedHttpCode: 404, + caseIDs: [postedCase.id], + expectedHttpCode: 204, + auth: secOnlyNoSpaceAuth, }); }); - it('should create a user action when creating a case', async () => { - const postedCase = await createCase(supertest, getPostCaseRequest()); - await deleteCases({ supertest, caseIDs: [postedCase.id] }); - const userActions = await getCaseUserActions({ supertest, caseID: postedCase.id }); - const creationUserAction = removeServerGeneratedPropertiesFromUserAction(userActions[1]); - - expect(creationUserAction).to.eql({ - action_field: [ - 'description', - 'status', - 'tags', - 'title', - 'connector', - 'settings', - 'owner', - 'comment', - ], - action: 'delete', - action_by: defaultUser, - old_value: null, - new_value: null, - case_id: `${postedCase.id}`, - comment_id: null, - sub_case_id: '', - owner: 'securitySolutionFixture', + it('User: security solution only - should NOT delete a case of different owner', async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest(), + 200, + secOnlyNoSpaceAuth + ); + + await deleteCases({ + supertest: supertestWithoutAuth, + caseIDs: [postedCase.id], + expectedHttpCode: 403, + auth: obsOnlyNoSpaceAuth, }); }); - it('unhappy path - 404s when case is not there', async () => { - await deleteCases({ supertest, caseIDs: ['fake-id'], expectedHttpCode: 404 }); - }); - - // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests - describe.skip('sub cases', () => { - let actionID: string; - before(async () => { - actionID = await createCaseAction(supertest); + it('should get an error if the user has not permissions to all requested cases', async () => { + const caseSec = await createCase( + supertestWithoutAuth, + getPostCaseRequest(), + 200, + secOnlyNoSpaceAuth + ); + + const caseObs = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + obsOnlyNoSpaceAuth + ); + + await deleteCases({ + supertest: supertestWithoutAuth, + caseIDs: [caseSec.id, caseObs.id], + expectedHttpCode: 403, + auth: obsOnlyNoSpaceAuth, }); - after(async () => { - await deleteCaseAction(supertest, actionID); - }); - afterEach(async () => { - await deleteAllCaseItems(es); - }); - - it('should delete the sub cases when deleting a collection', async () => { - const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - expect(caseInfo.subCases![0].id).to.not.eql(undefined); - const body = await deleteCases({ supertest, caseIDs: [caseInfo.id] }); - - expect(body).to.eql({}); - await supertest - .get(getSubCaseDetailsUrl(caseInfo.id, caseInfo.subCases![0].id)) - .send() - .expect(404); + // Cases should have not been deleted. + await getCase({ + supertest: supertestWithoutAuth, + caseId: caseSec.id, + expectedHttpCode: 200, + auth: { user: superUser, space: null }, }); - it(`should delete a sub case's comments when that case gets deleted`, async () => { - const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - expect(caseInfo.subCases![0].id).to.not.eql(undefined); - - // there should be two comments on the sub case now - const { body: patchedCaseWithSubCase }: { body: CaseResponse } = await supertest - .post(`${CASES_URL}/${caseInfo.id}/comments`) - .set('kbn-xsrf', 'true') - .query({ subCaseId: caseInfo.subCases![0].id }) - .send(postCommentUserReq) - .expect(200); - - const subCaseCommentUrl = `${CASES_URL}/${patchedCaseWithSubCase.id}/comments/${ - patchedCaseWithSubCase.comments![1].id - }`; - // make sure we can get the second comment - await supertest.get(subCaseCommentUrl).set('kbn-xsrf', 'true').send().expect(200); - - await deleteCases({ supertest, caseIDs: [caseInfo.id] }); - - await supertest.get(subCaseCommentUrl).set('kbn-xsrf', 'true').send().expect(404); + await getCase({ + supertest: supertestWithoutAuth, + caseId: caseObs.id, + expectedHttpCode: 200, + auth: { user: superUser, space: null }, }); }); - describe('rbac', () => { - it('User: security solution only - should delete a case', async () => { - const postedCase = await createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'securitySolutionFixture' }), - 200, - { - user: secOnly, - space: 'space1', - } - ); - - await deleteCases({ - supertest, - caseIDs: [postedCase.id], - expectedHttpCode: 204, - auth: { user: secOnly, space: 'space1' }, + for (const user of [globalRead, secOnlyRead, obsOnlyRead, obsSecRead, noKibanaPrivileges]) { + it(`User ${ + user.username + } with role(s) ${user.roles.join()} - should NOT delete a case`, async () => { + const postedCase = await createCase(supertestWithoutAuth, getPostCaseRequest(), 200, { + user: superUser, + space: null, }); - }); - - it('User: security solution only - should NOT delete a case of different owner', async () => { - const postedCase = await createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'securitySolutionFixture' }), - 200, - { - user: secOnly, - space: 'space1', - } - ); await deleteCases({ supertest: supertestWithoutAuth, caseIDs: [postedCase.id], expectedHttpCode: 403, - auth: { user: obsOnly, space: 'space1' }, + auth: { user, space: null }, }); }); + } - it('should get an error if the user has not permissions to all requested cases', async () => { - const caseSec = await createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'securitySolutionFixture' }), - 200, - { - user: secOnly, - space: 'space1', - } - ); - - const caseObs = await createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'observabilityFixture' }), - 200, - { - user: obsOnly, - space: 'space1', - } - ); - - await deleteCases({ - supertest: supertestWithoutAuth, - caseIDs: [caseSec.id, caseObs.id], - expectedHttpCode: 403, - auth: { user: obsOnly, space: 'space1' }, - }); - - // Cases should have not been deleted. - await getCase({ - supertest: supertestWithoutAuth, - caseId: caseSec.id, - expectedHttpCode: 200, - auth: superUserSpace1Auth, - }); - - await getCase({ - supertest: supertestWithoutAuth, - caseId: caseObs.id, - expectedHttpCode: 200, - auth: superUserSpace1Auth, - }); + it('should return a 404 when attempting to access a space', async () => { + const postedCase = await createCase(supertestWithoutAuth, getPostCaseRequest(), 200, { + user: superUser, + space: null, }); - for (const user of [globalRead, secOnlyRead, obsOnlyRead, obsSecRead, noKibanaPrivileges]) { - it(`User ${ - user.username - } with role(s) ${user.roles.join()} - should NOT delete a case`, async () => { - const postedCase = await createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'securitySolutionFixture' }), - 200, - { - user: superUser, - space: 'space1', - } - ); - - await deleteCases({ - supertest: supertestWithoutAuth, - caseIDs: [postedCase.id], - expectedHttpCode: 403, - auth: { user, space: 'space1' }, - }); - }); - } - - it('should NOT delete a case in a space with no permissions', async () => { - const postedCase = await createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'securitySolutionFixture' }), - 200, - { - user: superUser, - space: 'space2', - } - ); - - /** - * We expect a 404 because the bulkGet inside the delete - * route should return a 404 when requesting a case from - * a different space. - * */ - await deleteCases({ - supertest: supertestWithoutAuth, - caseIDs: [postedCase.id], - expectedHttpCode: 404, - auth: { user: secOnly, space: 'space1' }, - }); + await deleteCases({ + supertest: supertestWithoutAuth, + caseIDs: [postedCase.id], + expectedHttpCode: 404, + auth: { user: secOnly, space: 'space1' }, }); }); }); diff --git a/x-pack/test/case_api_integration/security_only/tests/common/cases/find_cases.ts b/x-pack/test/case_api_integration/security_only/tests/common/cases/find_cases.ts index b7838dd9299bcd..abac7ffb0addd1 100644 --- a/x-pack/test/case_api_integration/security_only/tests/common/cases/find_cases.ts +++ b/x-pack/test/case_api_integration/security_only/tests/common/cases/find_cases.ts @@ -5,36 +5,17 @@ * 2.0. */ -import expect from '@kbn/expect'; -import type { ApiResponse, estypes } from '@elastic/elasticsearch'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; -import { - CASES_URL, - SUB_CASES_PATCH_DEL_URL, -} from '../../../../../../plugins/cases/common/constants'; -import { - postCaseReq, - postCommentUserReq, - findCasesResp, - getPostCaseRequest, -} from '../../../../common/lib/mock'; +import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; +import { getPostCaseRequest } from '../../../../common/lib/mock'; import { deleteAllCaseItems, - createSubCase, - setStatus, - CreateSubCaseResp, - createCaseAction, - deleteCaseAction, ensureSavedObjectIsAuthorized, findCases, createCase, - updateCase, - createComment, } from '../../../../common/lib/utils'; -import { CaseResponse, CaseStatuses, CaseType } from '../../../../../../plugins/cases/common/api'; import { - obsOnly, secOnly, obsOnlyRead, secOnlyRead, @@ -42,14 +23,8 @@ import { superUser, globalRead, obsSecRead, - obsSec, } from '../../../../common/lib/authentication/users'; - -interface CaseAttributes { - cases: { - title: string; - }; -} +import { obsOnlyNoSpaceAuth, obsSecNoSpaceAuth, secOnlyNoSpaceAuth } from '../../../utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -58,758 +33,212 @@ export default ({ getService }: FtrProviderContext): void => { const supertestWithoutAuth = getService('supertestWithoutAuth'); describe('find_cases', () => { - describe('basic tests', () => { - afterEach(async () => { - await deleteAllCaseItems(es); - }); - - it('should return empty response', async () => { - const cases = await findCases({ supertest }); - expect(cases).to.eql(findCasesResp); - }); - - it('should return cases', async () => { - const a = await createCase(supertest, postCaseReq); - const b = await createCase(supertest, postCaseReq); - const c = await createCase(supertest, postCaseReq); - - const cases = await findCases({ supertest }); - - expect(cases).to.eql({ - ...findCasesResp, - total: 3, - cases: [a, b, c], - count_open_cases: 3, - }); - }); - - it('filters by tags', async () => { - await createCase(supertest, postCaseReq); - const postedCase = await createCase(supertest, { ...postCaseReq, tags: ['unique'] }); - const cases = await findCases({ supertest, query: { tags: ['unique'] } }); - - expect(cases).to.eql({ - ...findCasesResp, - total: 1, - cases: [postedCase], - count_open_cases: 1, - }); - }); - - it('filters by status', async () => { - await createCase(supertest, postCaseReq); - const toCloseCase = await createCase(supertest, postCaseReq); - const patchedCase = await updateCase({ - supertest, - params: { - cases: [ - { - id: toCloseCase.id, - version: toCloseCase.version, - status: CaseStatuses.closed, - }, - ], - }, - }); - - const cases = await findCases({ supertest, query: { status: CaseStatuses.closed } }); - - expect(cases).to.eql({ - ...findCasesResp, - total: 1, - cases: [patchedCase[0]], - count_open_cases: 1, - count_closed_cases: 1, - count_in_progress_cases: 0, - }); - }); - - it('filters by reporters', async () => { - const postedCase = await createCase(supertest, postCaseReq); - const cases = await findCases({ supertest, query: { reporters: 'elastic' } }); - - expect(cases).to.eql({ - ...findCasesResp, - total: 1, - cases: [postedCase], - count_open_cases: 1, - }); - }); - - it('correctly counts comments', async () => { - const postedCase = await createCase(supertest, postCaseReq); - - // post 2 comments - await createComment({ supertest, caseId: postedCase.id, params: postCommentUserReq }); - const patchedCase = await createComment({ - supertest, - caseId: postedCase.id, - params: postCommentUserReq, - }); - - const cases = await findCases({ supertest }); - expect(cases).to.eql({ - ...findCasesResp, - total: 1, - cases: [ - { - ...patchedCase, - comments: [], - totalComment: 2, - }, - ], - count_open_cases: 1, - }); - }); - - it('correctly counts open/closed/in-progress', async () => { - await createCase(supertest, postCaseReq); - const inProgressCase = await createCase(supertest, postCaseReq); - const postedCase = await createCase(supertest, postCaseReq); - - await updateCase({ - supertest, - params: { - cases: [ - { - id: postedCase.id, - version: postedCase.version, - status: CaseStatuses.closed, - }, - ], - }, - }); - - await updateCase({ - supertest, - params: { - cases: [ - { - id: inProgressCase.id, - version: inProgressCase.version, - status: CaseStatuses['in-progress'], - }, - ], - }, - }); - - const cases = await findCases({ supertest }); - expect(cases.count_open_cases).to.eql(1); - expect(cases.count_closed_cases).to.eql(1); - expect(cases.count_in_progress_cases).to.eql(1); - }); - - it('unhappy path - 400s when bad query supplied', async () => { - await findCases({ supertest, query: { perPage: true }, expectedHttpCode: 400 }); - }); - - // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests - describe.skip('stats with sub cases', () => { - let collection: CreateSubCaseResp; - let actionID: string; - before(async () => { - actionID = await createCaseAction(supertest); - }); - after(async () => { - await deleteCaseAction(supertest, actionID); - }); - beforeEach(async () => { - // create a collection with a sub case that is marked as open - collection = await createSubCase({ supertest, actionID }); - - const [, , { body: toCloseCase }] = await Promise.all([ - // set the sub case to in-progress - setStatus({ - supertest, - cases: [ - { - id: collection.newSubCaseInfo.subCases![0].id, - version: collection.newSubCaseInfo.subCases![0].version, - status: CaseStatuses['in-progress'], - }, - ], - type: 'sub_case', - }), - // create two cases that are both open - supertest.post(CASES_URL).set('kbn-xsrf', 'true').send(postCaseReq), - supertest.post(CASES_URL).set('kbn-xsrf', 'true').send(postCaseReq), - ]); - - // set the third case to closed - await setStatus({ - supertest, - cases: [ - { - id: toCloseCase.id, - version: toCloseCase.version, - status: CaseStatuses.closed, - }, - ], - type: 'case', - }); - }); - it('correctly counts stats without using a filter', async () => { - const cases = await findCases({ supertest }); - - expect(cases.total).to.eql(3); - expect(cases.count_closed_cases).to.eql(1); - expect(cases.count_open_cases).to.eql(1); - expect(cases.count_in_progress_cases).to.eql(1); - }); - - it('correctly counts stats with a filter for open cases', async () => { - const cases = await findCases({ supertest, query: { status: CaseStatuses.open } }); - - expect(cases.cases.length).to.eql(1); - - // since we're filtering on status and the collection only has an in-progress case, it should only return the - // individual case that has the open status and no collections - // ENABLE_CASE_CONNECTOR: this value is not correct because it includes a collection - // that does not have an open case. This is a known issue and will need to be resolved - // when this issue is addressed: https://github.com/elastic/kibana/issues/94115 - expect(cases.total).to.eql(2); - expect(cases.count_closed_cases).to.eql(1); - expect(cases.count_open_cases).to.eql(1); - expect(cases.count_in_progress_cases).to.eql(1); - }); - - it('correctly counts stats with a filter for individual cases', async () => { - const cases = await findCases({ supertest, query: { type: CaseType.individual } }); - - expect(cases.total).to.eql(2); - expect(cases.count_closed_cases).to.eql(1); - expect(cases.count_open_cases).to.eql(1); - expect(cases.count_in_progress_cases).to.eql(0); - }); - - it('correctly counts stats with a filter for collection cases with multiple sub cases', async () => { - // this will force the first sub case attached to the collection to be closed - // so we'll have one closed sub case and one open sub case - await createSubCase({ supertest, caseID: collection.newSubCaseInfo.id, actionID }); - const cases = await findCases({ supertest, query: { type: CaseType.collection } }); - - expect(cases.total).to.eql(1); - expect(cases.cases[0].subCases?.length).to.eql(2); - expect(cases.count_closed_cases).to.eql(1); - expect(cases.count_open_cases).to.eql(1); - expect(cases.count_in_progress_cases).to.eql(0); - }); - - it('correctly counts stats with a filter for collection and open cases with multiple sub cases', async () => { - // this will force the first sub case attached to the collection to be closed - // so we'll have one closed sub case and one open sub case - await createSubCase({ supertest, caseID: collection.newSubCaseInfo.id, actionID }); - const cases = await findCases({ - supertest, - query: { - type: CaseType.collection, - status: CaseStatuses.open, - }, - }); - - expect(cases.total).to.eql(1); - expect(cases.cases[0].subCases?.length).to.eql(1); - expect(cases.count_closed_cases).to.eql(1); - expect(cases.count_open_cases).to.eql(1); - expect(cases.count_in_progress_cases).to.eql(0); - }); - - it('correctly counts stats including a collection without sub cases when not filtering on status', async () => { - // delete the sub case on the collection so that it doesn't have any sub cases - await supertest - .delete( - `${SUB_CASES_PATCH_DEL_URL}?ids=["${collection.newSubCaseInfo.subCases![0].id}"]` - ) - .set('kbn-xsrf', 'true') - .send() - .expect(204); - - const cases = await findCases({ supertest, query: { type: CaseType.collection } }); - - // it should include the collection without sub cases because we did not pass in a filter on status - expect(cases.total).to.eql(3); - expect(cases.count_closed_cases).to.eql(1); - expect(cases.count_open_cases).to.eql(1); - expect(cases.count_in_progress_cases).to.eql(0); - }); - - it('correctly counts stats including a collection without sub cases when filtering on tags', async () => { - // delete the sub case on the collection so that it doesn't have any sub cases - await supertest - .delete( - `${SUB_CASES_PATCH_DEL_URL}?ids=["${collection.newSubCaseInfo.subCases![0].id}"]` - ) - .set('kbn-xsrf', 'true') - .send() - .expect(204); - - const cases = await findCases({ supertest, query: { tags: ['defacement'] } }); - - // it should include the collection without sub cases because we did not pass in a filter on status - expect(cases.total).to.eql(3); - expect(cases.count_closed_cases).to.eql(1); - expect(cases.count_open_cases).to.eql(1); - expect(cases.count_in_progress_cases).to.eql(0); - }); - - it('does not return collections without sub cases matching the requested status', async () => { - const cases = await findCases({ supertest, query: { status: CaseStatuses.closed } }); - - expect(cases.cases.length).to.eql(1); - // it should not include the collection that has a sub case as in-progress - // ENABLE_CASE_CONNECTOR: this value is not correct because it includes collections. This short term - // fix for when sub cases are not enabled. When the feature is completed the _find API - // will need to be fixed as explained in this ticket: https://github.com/elastic/kibana/issues/94115 - expect(cases.total).to.eql(2); - expect(cases.count_closed_cases).to.eql(1); - expect(cases.count_open_cases).to.eql(1); - expect(cases.count_in_progress_cases).to.eql(1); - }); - - it('does not return empty collections when filtering on status', async () => { - // delete the sub case on the collection so that it doesn't have any sub cases - await supertest - .delete( - `${SUB_CASES_PATCH_DEL_URL}?ids=["${collection.newSubCaseInfo.subCases![0].id}"]` - ) - .set('kbn-xsrf', 'true') - .send() - .expect(204); - - const cases = await findCases({ supertest, query: { status: CaseStatuses.closed } }); - - expect(cases.cases.length).to.eql(1); - - // ENABLE_CASE_CONNECTOR: this value is not correct because it includes collections. This short term - // fix for when sub cases are not enabled. When the feature is completed the _find API - // will need to be fixed as explained in this ticket: https://github.com/elastic/kibana/issues/94115 - expect(cases.total).to.eql(2); - expect(cases.count_closed_cases).to.eql(1); - expect(cases.count_open_cases).to.eql(1); - expect(cases.count_in_progress_cases).to.eql(0); - }); - }); + afterEach(async () => { + await deleteAllCaseItems(es); }); - describe('find_cases pagination', () => { - const numCases = 10; - before(async () => { - await createCasesWithTitleAsNumber(numCases); - }); - - after(async () => { - await deleteAllCaseItems(es); - }); - - const createCasesWithTitleAsNumber = async (total: number): Promise => { - const responsePromises = []; - for (let i = 0; i < total; i++) { - // this doesn't guarantee that the cases will be created in order that the for-loop executes, - // for example case with title '2', could be created before the case with title '1' since we're doing a promise all here - // A promise all is just much faster than doing it one by one which would have guaranteed that the cases are - // created in the order that the for-loop executes - responsePromises.push(createCase(supertest, { ...postCaseReq, title: `${i}` })); - } - const responses = await Promise.all(responsePromises); - return responses; - }; - - /** - * This is used to retrieve all the cases in the same sorted order that we're expecting them to come back via the - * _find API so that we have a more true comparison instead of using the _find API to get all the cases which - * could mangle the results if the implementation had a bug. - * - * Ideally we could enforce how the cases are created in reasonable time, waiting for each api call to finish takes - * around 30 seconds which seemed too slow - */ - const getAllCasesSortedByCreatedAtAsc = async () => { - const cases: ApiResponse> = await es.search({ - index: '.kibana', - body: { - size: 10000, - sort: [{ 'cases.created_at': { unmapped_type: 'date', order: 'asc' } }], - query: { - term: { type: 'cases' }, - }, - }, - }); - return cases.body.hits.hits.map((hit) => hit._source); - }; + it('should return the correct cases', async () => { + await Promise.all([ + // Create case owned by the security solution user + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + secOnlyNoSpaceAuth + ), + // Create case owned by the observability user + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + obsOnlyNoSpaceAuth + ), + ]); - it('returns the correct total when perPage is less than the total', async () => { - const cases = await findCases({ - supertest, - query: { - page: 1, - perPage: 5, - }, - }); - - expect(cases.cases.length).to.eql(5); - expect(cases.total).to.eql(10); - expect(cases.page).to.eql(1); - expect(cases.per_page).to.eql(5); - expect(cases.count_open_cases).to.eql(10); - expect(cases.count_closed_cases).to.eql(0); - expect(cases.count_in_progress_cases).to.eql(0); - }); - - it('returns the correct total when perPage is greater than the total', async () => { - const cases = await findCases({ - supertest, - query: { - page: 1, - perPage: 11, + for (const scenario of [ + { + user: globalRead, + numberOfExpectedCases: 2, + owners: ['securitySolutionFixture', 'observabilityFixture'], + }, + { + user: superUser, + numberOfExpectedCases: 2, + owners: ['securitySolutionFixture', 'observabilityFixture'], + }, + { user: secOnlyRead, numberOfExpectedCases: 1, owners: ['securitySolutionFixture'] }, + { user: obsOnlyRead, numberOfExpectedCases: 1, owners: ['observabilityFixture'] }, + { + user: obsSecRead, + numberOfExpectedCases: 2, + owners: ['securitySolutionFixture', 'observabilityFixture'], + }, + ]) { + const res = await findCases({ + supertest: supertestWithoutAuth, + auth: { + user: scenario.user, + space: null, }, }); - expect(cases.total).to.eql(10); - expect(cases.page).to.eql(1); - expect(cases.per_page).to.eql(11); - expect(cases.cases.length).to.eql(10); - expect(cases.count_open_cases).to.eql(10); - expect(cases.count_closed_cases).to.eql(0); - expect(cases.count_in_progress_cases).to.eql(0); - }); - - it('returns the correct total when perPage is equal to the total', async () => { - const cases = await findCases({ - supertest, - query: { - page: 1, - perPage: 10, - }, - }); + ensureSavedObjectIsAuthorized(res.cases, scenario.numberOfExpectedCases, scenario.owners); + } + }); - expect(cases.total).to.eql(10); - expect(cases.page).to.eql(1); - expect(cases.per_page).to.eql(10); - expect(cases.cases.length).to.eql(10); - expect(cases.count_open_cases).to.eql(10); - expect(cases.count_closed_cases).to.eql(0); - expect(cases.count_in_progress_cases).to.eql(0); + it(`User ${ + noKibanaPrivileges.username + } with role(s) ${noKibanaPrivileges.roles.join()} - should NOT read a case`, async () => { + await createCase(supertestWithoutAuth, getPostCaseRequest(), 200, { + user: superUser, + space: null, }); - it('returns the second page of results', async () => { - const perPage = 5; - const cases = await findCases({ - supertest, - query: { - page: 2, - perPage, - }, - }); - - expect(cases.total).to.eql(10); - expect(cases.page).to.eql(2); - expect(cases.per_page).to.eql(5); - expect(cases.cases.length).to.eql(5); - expect(cases.count_open_cases).to.eql(10); - expect(cases.count_closed_cases).to.eql(0); - expect(cases.count_in_progress_cases).to.eql(0); - - const allCases = await getAllCasesSortedByCreatedAtAsc(); - - cases.cases.map((caseInfo, index) => { - // we started on the second page of 10 cases with a perPage of 5, so the first case should 0 + 5 (index + perPage) - expect(caseInfo.title).to.eql(allCases[index + perPage]?.cases.title); - }); + await findCases({ + supertest: supertestWithoutAuth, + auth: { + user: noKibanaPrivileges, + space: null, + }, + expectedHttpCode: 403, }); + }); - it('paginates with perPage of 2 through 10 total cases', async () => { - const total = 10; - const perPage = 2; - - // it's less than or equal here because the page starts at 1, so page 5 is a valid page number - // and should have case titles 9, and 10 - for (let currentPage = 1; currentPage <= total / perPage; currentPage++) { - const cases = await findCases({ - supertest, - query: { - page: currentPage, - perPage, - }, - }); - - expect(cases.total).to.eql(total); - expect(cases.page).to.eql(currentPage); - expect(cases.per_page).to.eql(perPage); - expect(cases.cases.length).to.eql(perPage); - expect(cases.count_open_cases).to.eql(total); - expect(cases.count_closed_cases).to.eql(0); - expect(cases.count_in_progress_cases).to.eql(0); - - const allCases = await getAllCasesSortedByCreatedAtAsc(); - - cases.cases.map((caseInfo, index) => { - // for page 1, the cases tiles should be 0,1,2 for page 2: 3,4,5 etc (assuming the titles were sorted - // correctly) - expect(caseInfo.title).to.eql( - allCases[index + perPage * (currentPage - 1)]?.cases.title - ); - }); - } + it('should return a 404 when attempting to access a space', async () => { + await createCase(supertestWithoutAuth, getPostCaseRequest(), 200, { + user: superUser, + space: null, }); - it('retrieves the last three cases', async () => { - const cases = await findCases({ - supertest, - query: { - // this should skip the first 7 cases and only return the last 3 - page: 2, - perPage: 7, - }, - }); - - expect(cases.total).to.eql(10); - expect(cases.page).to.eql(2); - expect(cases.per_page).to.eql(7); - expect(cases.cases.length).to.eql(3); - expect(cases.count_open_cases).to.eql(10); - expect(cases.count_closed_cases).to.eql(0); - expect(cases.count_in_progress_cases).to.eql(0); + await findCases({ + supertest: supertestWithoutAuth, + auth: { user: secOnly, space: 'space1' }, + expectedHttpCode: 404, }); }); - describe('rbac', () => { - afterEach(async () => { - await deleteAllCaseItems(es); - }); - - it('should return the correct cases', async () => { - await Promise.all([ - // Create case owned by the security solution user - createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'securitySolutionFixture' }), - 200, - { - user: secOnly, - space: 'space1', - } - ), - // Create case owned by the observability user - createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'observabilityFixture' }), - 200, - { - user: obsOnly, - space: 'space1', - } - ), - ]); - - for (const scenario of [ - { - user: globalRead, - numberOfExpectedCases: 2, - owners: ['securitySolutionFixture', 'observabilityFixture'], - }, + it('should return the correct cases when trying to exploit RBAC through the search query parameter', async () => { + await Promise.all([ + // super user creates a case with owner securitySolutionFixture + createCase(supertestWithoutAuth, getPostCaseRequest(), 200, { + user: superUser, + space: null, + }), + // super user creates a case with owner observabilityFixture + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, { user: superUser, - numberOfExpectedCases: 2, - owners: ['securitySolutionFixture', 'observabilityFixture'], - }, - { user: secOnlyRead, numberOfExpectedCases: 1, owners: ['securitySolutionFixture'] }, - { user: obsOnlyRead, numberOfExpectedCases: 1, owners: ['observabilityFixture'] }, - { - user: obsSecRead, - numberOfExpectedCases: 2, - owners: ['securitySolutionFixture', 'observabilityFixture'], - }, - ]) { - const res = await findCases({ - supertest: supertestWithoutAuth, - auth: { - user: scenario.user, - space: 'space1', - }, - }); + space: null, + } + ), + ]); - ensureSavedObjectIsAuthorized(res.cases, scenario.numberOfExpectedCases, scenario.owners); - } + const res = await findCases({ + supertest: supertestWithoutAuth, + query: { + search: 'securitySolutionFixture observabilityFixture', + searchFields: 'owner', + }, + auth: secOnlyNoSpaceAuth, }); - for (const scenario of [ - { user: noKibanaPrivileges, space: 'space1' }, - { user: secOnly, space: 'space2' }, - ]) { - it(`User ${scenario.user.username} with role(s) ${scenario.user.roles.join()} and space ${ - scenario.space - } - should NOT read a case`, async () => { - // super user creates a case at the appropriate space - await createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'securitySolutionFixture' }), - 200, - { - user: superUser, - space: scenario.space, - } - ); - - // user should not be able to read cases at the appropriate space - await findCases({ - supertest: supertestWithoutAuth, - auth: { - user: scenario.user, - space: scenario.space, - }, - expectedHttpCode: 403, - }); - }); - } - - it('should return the correct cases when trying to exploit RBAC through the search query parameter', async () => { - await Promise.all([ - // super user creates a case with owner securitySolutionFixture - createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'securitySolutionFixture' }), - 200, - { - user: superUser, - space: 'space1', - } - ), - // super user creates a case with owner observabilityFixture - createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'observabilityFixture' }), - 200, - { - user: superUser, - space: 'space1', - } - ), - ]); - - const res = await findCases({ - supertest: supertestWithoutAuth, - query: { - search: 'securitySolutionFixture observabilityFixture', - searchFields: 'owner', - }, - auth: { - user: secOnly, - space: 'space1', - }, - }); - - ensureSavedObjectIsAuthorized(res.cases, 1, ['securitySolutionFixture']); - }); - - // This test is to prevent a future developer to add the filter attribute without taking into consideration - // the authorizationFilter produced by the cases authorization class - it('should NOT allow to pass a filter query parameter', async () => { - await supertest - .get( - `${CASES_URL}/_find?sortOrder=asc&filter=cases.attributes.owner:"observabilityFixture"` - ) - .set('kbn-xsrf', 'true') - .send() - .expect(400); - }); - - // This test ensures that the user is not allowed to define the namespaces query param - // so she cannot search across spaces - it('should NOT allow to pass a namespaces query parameter', async () => { - await supertest - .get(`${CASES_URL}/_find?sortOrder=asc&namespaces[0]=*`) - .set('kbn-xsrf', 'true') - .send() - .expect(400); - - await supertest - .get(`${CASES_URL}/_find?sortOrder=asc&namespaces=*`) - .set('kbn-xsrf', 'true') - .send() - .expect(400); - }); - - it('should NOT allow to pass a non supported query parameter', async () => { - await supertest - .get(`${CASES_URL}/_find?notExists=papa`) - .set('kbn-xsrf', 'true') - .send() - .expect(400); - }); - - it('should respect the owner filter when having permissions', async () => { - await Promise.all([ - createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'securitySolutionFixture' }), - 200, - { - user: obsSec, - space: 'space1', - } - ), - createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'observabilityFixture' }), - 200, - { - user: obsSec, - space: 'space1', - } - ), - ]); + ensureSavedObjectIsAuthorized(res.cases, 1, ['securitySolutionFixture']); + }); - const res = await findCases({ - supertest: supertestWithoutAuth, - query: { - owner: 'securitySolutionFixture', - searchFields: 'owner', - }, - auth: { - user: obsSec, - space: 'space1', - }, - }); + // This test is to prevent a future developer to add the filter attribute without taking into consideration + // the authorizationFilter produced by the cases authorization class + it('should NOT allow to pass a filter query parameter', async () => { + await supertest + .get( + `${CASES_URL}/_find?sortOrder=asc&filter=cases.attributes.owner:"observabilityFixture"` + ) + .set('kbn-xsrf', 'true') + .send() + .expect(400); + }); - ensureSavedObjectIsAuthorized(res.cases, 1, ['securitySolutionFixture']); - }); + // This test ensures that the user is not allowed to define the namespaces query param + // so she cannot search across spaces + it('should NOT allow to pass a namespaces query parameter', async () => { + await supertest + .get(`${CASES_URL}/_find?sortOrder=asc&namespaces[0]=*`) + .set('kbn-xsrf', 'true') + .send() + .expect(400); + + await supertest + .get(`${CASES_URL}/_find?sortOrder=asc&namespaces=*`) + .set('kbn-xsrf', 'true') + .send() + .expect(400); + }); - it('should return the correct cases when trying to exploit RBAC through the owner query parameter', async () => { - await Promise.all([ - createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'securitySolutionFixture' }), - 200, - { - user: obsSec, - space: 'space1', - } - ), - createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'observabilityFixture' }), - 200, - { - user: obsSec, - space: 'space1', - } - ), - ]); + it('should NOT allow to pass a non supported query parameter', async () => { + await supertest + .get(`${CASES_URL}/_find?notExists=papa`) + .set('kbn-xsrf', 'true') + .send() + .expect(400); + }); - // User with permissions only to security solution request cases from observability - const res = await findCases({ - supertest: supertestWithoutAuth, - query: { - owner: ['securitySolutionFixture', 'observabilityFixture'], - }, - auth: { - user: secOnly, - space: 'space1', - }, - }); + it('should respect the owner filter when having permissions', async () => { + await Promise.all([ + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + obsSecNoSpaceAuth + ), + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + obsOnlyNoSpaceAuth + ), + ]); + + const res = await findCases({ + supertest: supertestWithoutAuth, + query: { + owner: 'securitySolutionFixture', + searchFields: 'owner', + }, + auth: obsSecNoSpaceAuth, + }); + + ensureSavedObjectIsAuthorized(res.cases, 1, ['securitySolutionFixture']); + }); - // Only security solution cases are being returned - ensureSavedObjectIsAuthorized(res.cases, 1, ['securitySolutionFixture']); - }); + it('should return the correct cases when trying to exploit RBAC through the owner query parameter', async () => { + await Promise.all([ + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + obsSecNoSpaceAuth + ), + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + obsSecNoSpaceAuth + ), + ]); + + // User with permissions only to security solution request cases from observability + const res = await findCases({ + supertest: supertestWithoutAuth, + query: { + owner: ['securitySolutionFixture', 'observabilityFixture'], + }, + auth: secOnlyNoSpaceAuth, + }); + + // Only security solution cases are being returned + ensureSavedObjectIsAuthorized(res.cases, 1, ['securitySolutionFixture']); }); }); }; diff --git a/x-pack/test/case_api_integration/security_only/tests/common/cases/get_case.ts b/x-pack/test/case_api_integration/security_only/tests/common/cases/get_case.ts index 222632b41c297c..21f34714eb2342 100644 --- a/x-pack/test/case_api_integration/security_only/tests/common/cases/get_case.ts +++ b/x-pack/test/case_api_integration/security_only/tests/common/cases/get_case.ts @@ -9,20 +9,12 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { AttributesTypeUser } from '../../../../../../plugins/cases/common/api'; -import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; -import { - defaultUser, - postCaseReq, - postCaseResp, - postCommentUserReq, - getPostCaseRequest, -} from '../../../../common/lib/mock'; +import { postCommentUserReq, getPostCaseRequest } from '../../../../common/lib/mock'; import { deleteCasesByESQuery, createCase, getCase, createComment, - removeServerGeneratedPropertiesFromCase, removeServerGeneratedPropertiesFromSavedObject, } from '../../../../common/lib/utils'; import { @@ -37,10 +29,10 @@ import { obsSec, } from '../../../../common/lib/authentication/users'; import { getUserInfo } from '../../../../common/lib/authentication'; +import { secOnlyNoSpaceAuth } from '../../../utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { - const supertest = getService('supertest'); const supertestWithoutAuth = getService('supertestWithoutAuth'); const es = getService('es'); @@ -49,19 +41,45 @@ export default ({ getService }: FtrProviderContext): void => { await deleteCasesByESQuery(es); }); - it('should return a case with no comments', async () => { - const postedCase = await createCase(supertest, getPostCaseRequest()); - const theCase = await getCase({ supertest, caseId: postedCase.id, includeComments: true }); + it('should get a case', async () => { + const newCase = await createCase(supertestWithoutAuth, getPostCaseRequest(), 200, { + user: superUser, + space: null, + }); - const data = removeServerGeneratedPropertiesFromCase(theCase); - expect(data).to.eql(postCaseResp()); - expect(data.comments?.length).to.eql(0); + for (const user of [globalRead, superUser, secOnly, secOnlyRead, obsSec, obsSecRead]) { + const theCase = await getCase({ + supertest: supertestWithoutAuth, + caseId: newCase.id, + auth: { user, space: null }, + }); + + expect(theCase.owner).to.eql('securitySolutionFixture'); + } }); - it('should return a case with comments', async () => { - const postedCase = await createCase(supertest, postCaseReq); - await createComment({ supertest, caseId: postedCase.id, params: postCommentUserReq }); - const theCase = await getCase({ supertest, caseId: postedCase.id, includeComments: true }); + it('should get a case with comments', async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest(), + 200, + secOnlyNoSpaceAuth + ); + + await createComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + params: postCommentUserReq, + expectedHttpCode: 200, + auth: secOnlyNoSpaceAuth, + }); + + const theCase = await getCase({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + includeComments: true, + auth: secOnlyNoSpaceAuth, + }); const comment = removeServerGeneratedPropertiesFromSavedObject( theCase.comments![0] as AttributesTypeUser @@ -72,7 +90,7 @@ export default ({ getService }: FtrProviderContext): void => { type: postCommentUserReq.type, comment: postCommentUserReq.comment, associationType: 'case', - created_by: defaultUser, + created_by: getUserInfo(secOnly), pushed_at: null, pushed_by: null, updated_by: null, @@ -80,127 +98,33 @@ export default ({ getService }: FtrProviderContext): void => { }); }); - it('should return a 400 when passing the includeSubCaseComments', async () => { - const { body } = await supertest - .get(`${CASES_URL}/case-id?includeSubCaseComments=true`) - .set('kbn-xsrf', 'true') - .send() - .expect(400); - - expect(body.message).to.contain('disabled'); - }); - - it('unhappy path - 404s when case is not there', async () => { - await supertest.get(`${CASES_URL}/fake-id`).set('kbn-xsrf', 'true').send().expect(404); - }); - - describe('rbac', () => { - it('should get a case', async () => { - const newCase = await createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'securitySolutionFixture' }), - 200, - { - user: superUser, - space: 'space1', - } - ); - - for (const user of [globalRead, superUser, secOnly, secOnlyRead, obsSec, obsSecRead]) { - const theCase = await getCase({ - supertest: supertestWithoutAuth, - caseId: newCase.id, - auth: { user, space: 'space1' }, - }); - - expect(theCase.owner).to.eql('securitySolutionFixture'); - } + it('should not get a case when the user does not have access to owner', async () => { + const newCase = await createCase(supertestWithoutAuth, getPostCaseRequest(), 200, { + user: superUser, + space: null, }); - it('should get a case with comments', async () => { - const postedCase = await createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'securitySolutionFixture' }), - 200, - { - user: secOnly, - space: 'space1', - } - ); - - await createComment({ - supertest: supertestWithoutAuth, - caseId: postedCase.id, - params: postCommentUserReq, - expectedHttpCode: 200, - auth: { - user: secOnly, - space: 'space1', - }, - }); - - const theCase = await getCase({ - supertest: supertestWithoutAuth, - caseId: postedCase.id, - includeComments: true, - auth: { user: secOnly, space: 'space1' }, - }); - - const comment = removeServerGeneratedPropertiesFromSavedObject( - theCase.comments![0] as AttributesTypeUser - ); - - expect(theCase.comments?.length).to.eql(1); - expect(comment).to.eql({ - type: postCommentUserReq.type, - comment: postCommentUserReq.comment, - associationType: 'case', - created_by: getUserInfo(secOnly), - pushed_at: null, - pushed_by: null, - updated_by: null, - owner: 'securitySolutionFixture', - }); - }); - - it('should not get a case when the user does not have access to owner', async () => { - const newCase = await createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'securitySolutionFixture' }), - 200, - { - user: superUser, - space: 'space1', - } - ); - - for (const user of [noKibanaPrivileges, obsOnly, obsOnlyRead]) { - await getCase({ - supertest: supertestWithoutAuth, - caseId: newCase.id, - expectedHttpCode: 403, - auth: { user, space: 'space1' }, - }); - } - }); - - it('should NOT get a case in a space with no permissions', async () => { - const newCase = await createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'securitySolutionFixture' }), - 200, - { - user: superUser, - space: 'space2', - } - ); - + for (const user of [noKibanaPrivileges, obsOnly, obsOnlyRead]) { await getCase({ supertest: supertestWithoutAuth, caseId: newCase.id, expectedHttpCode: 403, - auth: { user: secOnly, space: 'space2' }, + auth: { user, space: null }, }); + } + }); + + it('should return a 404 when attempting to access a space', async () => { + const newCase = await createCase(supertestWithoutAuth, getPostCaseRequest(), 200, { + user: superUser, + space: null, + }); + + await getCase({ + supertest: supertestWithoutAuth, + caseId: newCase.id, + expectedHttpCode: 404, + auth: { user: secOnly, space: 'space1' }, }); }); }); diff --git a/x-pack/test/case_api_integration/security_only/tests/common/cases/patch_cases.ts b/x-pack/test/case_api_integration/security_only/tests/common/cases/patch_cases.ts index 286e08716ebf1e..8d80885dce4e05 100644 --- a/x-pack/test/case_api_integration/security_only/tests/common/cases/patch_cases.ts +++ b/x-pack/test/case_api_integration/security_only/tests/common/cases/patch_cases.ts @@ -8,62 +8,29 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; -import { DETECTION_ENGINE_QUERY_SIGNALS_URL } from '../../../../../../plugins/security_solution/common/constants'; -import { - CasesResponse, - CaseStatuses, - CaseType, - CommentType, - ConnectorTypes, -} from '../../../../../../plugins/cases/common/api'; -import { - defaultUser, - getPostCaseRequest, - postCaseReq, - postCaseResp, - postCollectionReq, - postCommentAlertReq, - postCommentUserReq, -} from '../../../../common/lib/mock'; +import { getPostCaseRequest, postCaseReq } from '../../../../common/lib/mock'; import { deleteAllCaseItems, - getSignalsWithES, - setStatus, createCase, - createComment, updateCase, - getCaseUserActions, - removeServerGeneratedPropertiesFromCase, - removeServerGeneratedPropertiesFromUserAction, findCases, - superUserSpace1Auth, + getAuthWithSuperUser, } from '../../../../common/lib/utils'; -import { - createSignalsIndex, - deleteSignalsIndex, - deleteAllAlerts, - getRuleForSignalTesting, - waitForRuleSuccessOrStatus, - waitForSignalsToBePresent, - getSignalsByIds, - createRule, - getQuerySignalIds, -} from '../../../../../detection_engine_api_integration/utils'; + import { globalRead, noKibanaPrivileges, - obsOnly, obsOnlyRead, obsSecRead, secOnly, secOnlyRead, superUser, } from '../../../../common/lib/authentication/users'; +import { obsOnlyNoSpaceAuth, secOnlyNoSpaceAuth } from '../../../utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); - const esArchiver = getService('esArchiver'); const es = getService('es'); describe('patch_cases', () => { @@ -71,1154 +38,174 @@ export default ({ getService }: FtrProviderContext): void => { await deleteAllCaseItems(es); }); - describe('happy path', () => { - it('should patch a case', async () => { - const postedCase = await createCase(supertest, postCaseReq); - const patchedCases = await updateCase({ - supertest, - params: { - cases: [ - { - id: postedCase.id, - version: postedCase.version, - title: 'new title', - }, - ], - }, - }); - - const data = removeServerGeneratedPropertiesFromCase(patchedCases[0]); - expect(data).to.eql({ - ...postCaseResp(), - title: 'new title', - updated_by: defaultUser, - }); - }); - - it('should closes the case correctly', async () => { - const postedCase = await createCase(supertest, postCaseReq); - const patchedCases = await updateCase({ - supertest, - params: { - cases: [ - { - id: postedCase.id, - version: postedCase.version, - status: CaseStatuses.closed, - }, - ], - }, - }); - - const userActions = await getCaseUserActions({ supertest, caseID: postedCase.id }); - const statusUserAction = removeServerGeneratedPropertiesFromUserAction(userActions[1]); - const data = removeServerGeneratedPropertiesFromCase(patchedCases[0]); - - expect(data).to.eql({ - ...postCaseResp(), - status: CaseStatuses.closed, - closed_by: defaultUser, - updated_by: defaultUser, - }); - - expect(statusUserAction).to.eql({ - action_field: ['status'], - action: 'update', - action_by: defaultUser, - new_value: CaseStatuses.closed, - old_value: CaseStatuses.open, - case_id: `${postedCase.id}`, - comment_id: null, - sub_case_id: '', - owner: 'securitySolutionFixture', - }); - }); - - it('should change the status of case to in-progress correctly', async () => { - const postedCase = await createCase(supertest, postCaseReq); - const patchedCases = await updateCase({ - supertest, - params: { - cases: [ - { - id: postedCase.id, - version: postedCase.version, - status: CaseStatuses['in-progress'], - }, - ], - }, - }); - - const userActions = await getCaseUserActions({ supertest, caseID: postedCase.id }); - const statusUserAction = removeServerGeneratedPropertiesFromUserAction(userActions[1]); - const data = removeServerGeneratedPropertiesFromCase(patchedCases[0]); - - expect(data).to.eql({ - ...postCaseResp(), - status: CaseStatuses['in-progress'], - updated_by: defaultUser, - }); - - expect(statusUserAction).to.eql({ - action_field: ['status'], - action: 'update', - action_by: defaultUser, - new_value: CaseStatuses['in-progress'], - old_value: CaseStatuses.open, - case_id: `${postedCase.id}`, - comment_id: null, - sub_case_id: '', - owner: 'securitySolutionFixture', - }); - }); - - it('should patch a case with new connector', async () => { - const postedCase = await createCase(supertest, postCaseReq); - const patchedCases = await updateCase({ - supertest, - params: { - cases: [ - { - id: postedCase.id, - version: postedCase.version, - connector: { - id: 'jira', - name: 'Jira', - type: ConnectorTypes.jira, - fields: { issueType: 'Task', priority: null, parent: null }, - }, - }, - ], - }, - }); - - const data = removeServerGeneratedPropertiesFromCase(patchedCases[0]); - expect(data).to.eql({ - ...postCaseResp(), - connector: { - id: 'jira', - name: 'Jira', - type: '.jira', - fields: { issueType: 'Task', priority: null, parent: null }, - }, - updated_by: defaultUser, - }); - }); - - // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests - it.skip('should allow converting an individual case to a collection when it does not have alerts', async () => { - const postedCase = await createCase(supertest, postCaseReq); - const patchedCase = await createComment({ - supertest, - caseId: postedCase.id, - params: postCommentUserReq, - }); - await updateCase({ - supertest, - params: { - cases: [ - { - id: patchedCase.id, - version: patchedCase.version, - type: CaseType.collection, - }, - ], - }, - }); - }); - }); - - describe('unhappy path', () => { - it('400s when attempting to change the owner of a case', async () => { - const postedCase = await createCase(supertest, postCaseReq); - await updateCase({ - supertest, - params: { - cases: [ - { - id: postedCase.id, - version: postedCase.version, - owner: 'observabilityFixture', - }, - ], - }, - expectedHttpCode: 400, - }); - }); - - it('404s when case is not there', async () => { - await updateCase({ - supertest, - params: { - cases: [ - { - id: 'not-real', - version: 'version', - status: CaseStatuses.closed, - }, - ], - }, - expectedHttpCode: 404, - }); - }); - - it('400s when id is missing', async () => { - await updateCase({ - supertest, - params: { - cases: [ - // @ts-expect-error - { - version: 'version', - status: CaseStatuses.closed, - }, - ], - }, - expectedHttpCode: 400, - }); - }); - - it('406s when fields are identical', async () => { - const postedCase = await createCase(supertest, postCaseReq); - await updateCase({ - supertest, - params: { - cases: [ - { - id: postedCase.id, - version: postedCase.version, - status: CaseStatuses.open, - }, - ], - }, - expectedHttpCode: 406, - }); - }); - - it('400s when version is missing', async () => { - await updateCase({ - supertest, - params: { - cases: [ - // @ts-expect-error - { - id: 'not-real', - status: CaseStatuses.closed, - }, - ], - }, - expectedHttpCode: 400, - }); - }); - - // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests - it.skip('should 400 and not allow converting a collection back to an individual case', async () => { - const postedCase = await createCase(supertest, postCollectionReq); - await updateCase({ - supertest, - params: { - cases: [ - { - id: postedCase.id, - version: postedCase.version, - type: CaseType.individual, - }, - ], - }, - expectedHttpCode: 400, - }); - }); - - it('406s when excess data sent', async () => { - const postedCase = await createCase(supertest, postCaseReq); - await updateCase({ - supertest, - params: { - cases: [ - { - id: postedCase.id, - version: postedCase.version, - // @ts-expect-error - badKey: 'closed', - }, - ], - }, - expectedHttpCode: 406, - }); - }); - - it('400s when bad data sent', async () => { - const postedCase = await createCase(supertest, postCaseReq); - await updateCase({ - supertest, - params: { - cases: [ - { - id: postedCase.id, - version: postedCase.version, - // @ts-expect-error - status: true, - }, - ], - }, - expectedHttpCode: 400, - }); - }); - - it('400s when unsupported status sent', async () => { - const postedCase = await createCase(supertest, postCaseReq); - await updateCase({ - supertest, - params: { - cases: [ - { - id: postedCase.id, - version: postedCase.version, - // @ts-expect-error - status: 'not-supported', - }, - ], - }, - expectedHttpCode: 400, - }); - }); - - it('400s when bad connector type sent', async () => { - const postedCase = await createCase(supertest, postCaseReq); - await updateCase({ - supertest, - params: { - cases: [ - { - id: postedCase.id, - version: postedCase.version, - // @ts-expect-error - connector: { id: 'none', name: 'none', type: '.not-exists', fields: null }, - }, - ], - }, - expectedHttpCode: 400, - }); - }); - - it('400s when bad connector sent', async () => { - const postedCase = await createCase(supertest, postCaseReq); - await updateCase({ - supertest, - params: { - cases: [ - { - id: postedCase.id, - version: postedCase.version, - connector: { - id: 'jira', - name: 'Jira', - // @ts-expect-error - type: ConnectorTypes.jira, - // @ts-expect-error - fields: { unsupported: 'value' }, - }, - }, - ], - }, - expectedHttpCode: 400, - }); - }); - - it('409s when version does not match', async () => { - const postedCase = await createCase(supertest, postCaseReq); - await updateCase({ - supertest, - params: { - cases: [ - { - id: postedCase.id, - version: 'version', - // @ts-expect-error - status: 'closed', - }, - ], - }, - expectedHttpCode: 409, - }); - }); - - it('should 400 when attempting to update an individual case to a collection when it has alerts attached to it', async () => { - const postedCase = await createCase(supertest, postCaseReq); - const patchedCase = await createComment({ - supertest, - caseId: postedCase.id, - params: postCommentAlertReq, - }); - await updateCase({ - supertest, - params: { - cases: [ - { - id: patchedCase.id, - version: patchedCase.version, - type: CaseType.collection, - }, - ], - }, - expectedHttpCode: 400, - }); - }); - - // ENABLE_CASE_CONNECTOR: once the case connector feature is completed delete these tests - it('should 400 when attempting to update the case type when the case connector feature is disabled', async () => { - const postedCase = await createCase(supertest, postCaseReq); - await updateCase({ - supertest, - params: { - cases: [ - { - id: postedCase.id, - version: postedCase.version, - type: CaseType.collection, - }, - ], - }, - expectedHttpCode: 400, - }); - }); - - // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests - it.skip("should 400 when attempting to update a collection case's status", async () => { - const postedCase = await createCase(supertest, postCollectionReq); - await updateCase({ - supertest, - params: { - cases: [ - { - id: postedCase.id, - version: postedCase.version, - status: CaseStatuses.closed, - }, - ], - }, - expectedHttpCode: 400, - }); - }); - }); - - describe('alerts', () => { - describe('esArchiver', () => { - const defaultSignalsIndex = '.siem-signals-default-000001'; - - beforeEach(async () => { - await esArchiver.load('cases/signals/default'); - }); - afterEach(async () => { - await esArchiver.unload('cases/signals/default'); - await deleteAllCaseItems(es); - }); - - it('should update the status of multiple alerts attached to multiple cases', async () => { - const signalID = '5f2b9ec41f8febb1c06b5d1045aeabb9874733b7617e88a370510f2fb3a41a5d'; - const signalID2 = '4d0f4b1533e46b66b43bdd0330d23f39f2cf42a7253153270e38d30cce9ff0c6'; - - // does NOT updates alert status when adding comments and syncAlerts=false - const individualCase1 = await createCase(supertest, { - ...postCaseReq, - settings: { - syncAlerts: false, - }, - }); - - const updatedInd1WithComment = await createComment({ - supertest, - caseId: individualCase1.id, - params: { - alertId: signalID, - index: defaultSignalsIndex, - rule: { id: 'test-rule-id', name: 'test-index-id' }, - type: CommentType.alert, - owner: 'securitySolutionFixture', - }, - }); - - const individualCase2 = await createCase(supertest, { - ...postCaseReq, - settings: { - syncAlerts: false, - }, - }); - - const updatedInd2WithComment = await createComment({ - supertest, - caseId: individualCase2.id, - params: { - alertId: signalID2, - index: defaultSignalsIndex, - rule: { id: 'test-rule-id', name: 'test-index-id' }, - type: CommentType.alert, - owner: 'securitySolutionFixture', - }, - }); - - await es.indices.refresh({ index: defaultSignalsIndex }); + const supertestWithoutAuth = getService('supertestWithoutAuth'); - let signals = await getSignalsWithES({ - es, - indices: defaultSignalsIndex, - ids: [signalID, signalID2], - }); + it('should update a case when the user has the correct permissions', async () => { + const postedCase = await createCase( + supertestWithoutAuth, + postCaseReq, + 200, + secOnlyNoSpaceAuth + ); - // There should be no change in their status since syncing is disabled - expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( - CaseStatuses.open - ); - expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source?.signal.status).to.be( - CaseStatuses.open - ); - - // does NOT updates alert status when the status is updated and syncAlerts=false - const updatedIndWithStatus: CasesResponse = (await setStatus({ - supertest, - cases: [ - { - id: updatedInd1WithComment.id, - version: updatedInd1WithComment.version, - status: CaseStatuses.closed, - }, - { - id: updatedInd2WithComment.id, - version: updatedInd2WithComment.version, - status: CaseStatuses['in-progress'], - }, - ], - type: 'case', - })) as CasesResponse; - - await es.indices.refresh({ index: defaultSignalsIndex }); - - signals = await getSignalsWithES({ - es, - indices: defaultSignalsIndex, - ids: [signalID, signalID2], - }); - - // There should still be no change in their status since syncing is disabled - expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( - CaseStatuses.open - ); - expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source?.signal.status).to.be( - CaseStatuses.open - ); - - // it updates alert status when syncAlerts is turned on - // turn on the sync settings - await updateCase({ - supertest, - params: { - cases: updatedIndWithStatus.map((caseInfo) => ({ - id: caseInfo.id, - version: caseInfo.version, - settings: { syncAlerts: true }, - })), + const patchedCases = await updateCase({ + supertest: supertestWithoutAuth, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + title: 'new title', }, - }); - - await es.indices.refresh({ index: defaultSignalsIndex }); - - signals = await getSignalsWithES({ - es, - indices: defaultSignalsIndex, - ids: [signalID, signalID2], - }); - - // alerts should be updated now that the - expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( - CaseStatuses.closed - ); - expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source?.signal.status).to.be( - CaseStatuses['in-progress'] - ); - }); + ], + }, + auth: secOnlyNoSpaceAuth, }); - describe('esArchiver', () => { - const defaultSignalsIndex = '.siem-signals-default-000001'; - - beforeEach(async () => { - await esArchiver.load('cases/signals/duplicate_ids'); - }); - afterEach(async () => { - await esArchiver.unload('cases/signals/duplicate_ids'); - await deleteAllCaseItems(es); - }); - - it('should not update the status of duplicate alert ids in separate indices', async () => { - const getSignals = async () => { - return getSignalsWithES({ - es, - indices: [defaultSignalsIndex, signalsIndex2], - ids: [signalIDInFirstIndex, signalIDInSecondIndex], - }); - }; - - // this id exists only in .siem-signals-default-000001 - const signalIDInFirstIndex = - 'cae78067e65582a3b277c1ad46ba3cb29044242fe0d24bbf3fcde757fdd31d1c'; - // This id exists in both .siem-signals-default-000001 and .siem-signals-default-000002 - const signalIDInSecondIndex = 'duplicate-signal-id'; - const signalsIndex2 = '.siem-signals-default-000002'; - - const individualCase = await createCase(supertest, { - ...postCaseReq, - settings: { - syncAlerts: false, - }, - }); + expect(patchedCases[0].owner).to.eql('securitySolutionFixture'); + }); - const updatedIndWithComment = await createComment({ - supertest, - caseId: individualCase.id, - params: { - alertId: signalIDInFirstIndex, - index: defaultSignalsIndex, - rule: { id: 'test-rule-id', name: 'test-index-id' }, - type: CommentType.alert, - owner: 'securitySolutionFixture', + it('should update multiple cases when the user has the correct permissions', async () => { + const [case1, case2, case3] = await Promise.all([ + createCase(supertestWithoutAuth, postCaseReq, 200, { + user: superUser, + space: null, + }), + createCase(supertestWithoutAuth, postCaseReq, 200, { + user: superUser, + space: null, + }), + createCase(supertestWithoutAuth, postCaseReq, 200, { + user: superUser, + space: null, + }), + ]); + + const patchedCases = await updateCase({ + supertest: supertestWithoutAuth, + params: { + cases: [ + { + id: case1.id, + version: case1.version, + title: 'new title', }, - }); - - const updatedIndWithComment2 = await createComment({ - supertest, - caseId: updatedIndWithComment.id, - params: { - alertId: signalIDInSecondIndex, - index: signalsIndex2, - rule: { id: 'test-rule-id', name: 'test-index-id' }, - type: CommentType.alert, - owner: 'securitySolutionFixture', + { + id: case2.id, + version: case2.version, + title: 'new title', }, - }); - - await es.indices.refresh({ index: defaultSignalsIndex }); - - let signals = await getSignals(); - // There should be no change in their status since syncing is disabled - expect( - signals.get(defaultSignalsIndex)?.get(signalIDInFirstIndex)?._source?.signal.status - ).to.be(CaseStatuses.open); - expect( - signals.get(signalsIndex2)?.get(signalIDInSecondIndex)?._source?.signal.status - ).to.be(CaseStatuses.open); - - const updatedIndWithStatus: CasesResponse = (await setStatus({ - supertest, - cases: [ - { - id: updatedIndWithComment2.id, - version: updatedIndWithComment2.version, - status: CaseStatuses.closed, - }, - ], - type: 'case', - })) as CasesResponse; - - await es.indices.refresh({ index: defaultSignalsIndex }); - - signals = await getSignals(); - - // There should still be no change in their status since syncing is disabled - expect( - signals.get(defaultSignalsIndex)?.get(signalIDInFirstIndex)?._source?.signal.status - ).to.be(CaseStatuses.open); - expect( - signals.get(signalsIndex2)?.get(signalIDInSecondIndex)?._source?.signal.status - ).to.be(CaseStatuses.open); - - // turn on the sync settings - await updateCase({ - supertest, - params: { - cases: [ - { - id: updatedIndWithStatus[0].id, - version: updatedIndWithStatus[0].version, - settings: { syncAlerts: true }, - }, - ], + { + id: case3.id, + version: case3.version, + title: 'new title', }, - }); - await es.indices.refresh({ index: defaultSignalsIndex }); - - signals = await getSignals(); - - // alerts should be updated now that the - expect( - signals.get(defaultSignalsIndex)?.get(signalIDInFirstIndex)?._source?.signal.status - ).to.be(CaseStatuses.closed); - expect( - signals.get(signalsIndex2)?.get(signalIDInSecondIndex)?._source?.signal.status - ).to.be(CaseStatuses.closed); - - // the duplicate signal id in the other index should not be affect (so its status should be open) - expect( - signals.get(defaultSignalsIndex)?.get(signalIDInSecondIndex)?._source?.signal.status - ).to.be(CaseStatuses.open); - }); + ], + }, + auth: secOnlyNoSpaceAuth, }); - describe('detections rule', () => { - beforeEach(async () => { - await esArchiver.load('auditbeat/hosts'); - await createSignalsIndex(supertest); - }); - - afterEach(async () => { - await deleteSignalsIndex(supertest); - await deleteAllAlerts(supertest); - await esArchiver.unload('auditbeat/hosts'); - }); - - it('updates alert status when the status is updated and syncAlerts=true', async () => { - const rule = getRuleForSignalTesting(['auditbeat-*']); - const postedCase = await createCase(supertest, postCaseReq); - - const { id } = await createRule(supertest, rule); - await waitForRuleSuccessOrStatus(supertest, id); - await waitForSignalsToBePresent(supertest, 1, [id]); - const signals = await getSignalsByIds(supertest, [id]); - - const alert = signals.hits.hits[0]; - expect(alert._source.signal.status).eql('open'); - - const caseUpdated = await createComment({ - supertest, - caseId: postedCase.id, - params: { - alertId: alert._id, - index: alert._index, - rule: { - id: 'id', - name: 'name', - }, - type: CommentType.alert, - owner: 'securitySolutionFixture', - }, - }); - - await es.indices.refresh({ index: alert._index }); - await updateCase({ - supertest, - params: { - cases: [ - { - id: caseUpdated.id, - version: caseUpdated.version, - status: CaseStatuses['in-progress'], - }, - ], - }, - }); - - // force a refresh on the index that the signal is stored in so that we can search for it and get the correct - // status - await es.indices.refresh({ index: alert._index }); - - const { body: updatedAlert } = await supertest - .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) - .set('kbn-xsrf', 'true') - .send(getQuerySignalIds([alert._id])) - .expect(200); - - expect(updatedAlert.hits.hits[0]._source.signal.status).eql('in-progress'); - }); - - it('does NOT updates alert status when the status is updated and syncAlerts=false', async () => { - const rule = getRuleForSignalTesting(['auditbeat-*']); - - const postedCase = await createCase(supertest, { - ...postCaseReq, - settings: { syncAlerts: false }, - }); - - const { id } = await createRule(supertest, rule); - await waitForRuleSuccessOrStatus(supertest, id); - await waitForSignalsToBePresent(supertest, 1, [id]); - const signals = await getSignalsByIds(supertest, [id]); - - const alert = signals.hits.hits[0]; - expect(alert._source.signal.status).eql('open'); - - const caseUpdated = await createComment({ - supertest, - caseId: postedCase.id, - params: { - alertId: alert._id, - index: alert._index, - type: CommentType.alert, - rule: { - id: 'id', - name: 'name', - }, - owner: 'securitySolutionFixture', - }, - }); - - await updateCase({ - supertest, - params: { - cases: [ - { - id: caseUpdated.id, - version: caseUpdated.version, - status: CaseStatuses['in-progress'], - }, - ], - }, - }); - - const { body: updatedAlert } = await supertest - .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) - .set('kbn-xsrf', 'true') - .send(getQuerySignalIds([alert._id])) - .expect(200); - - expect(updatedAlert.hits.hits[0]._source.signal.status).eql('open'); - }); - - it('it updates alert status when syncAlerts is turned on', async () => { - const rule = getRuleForSignalTesting(['auditbeat-*']); - - const postedCase = await createCase(supertest, { - ...postCaseReq, - settings: { syncAlerts: false }, - }); - - const { id } = await createRule(supertest, rule); - await waitForRuleSuccessOrStatus(supertest, id); - await waitForSignalsToBePresent(supertest, 1, [id]); - const signals = await getSignalsByIds(supertest, [id]); - - const alert = signals.hits.hits[0]; - expect(alert._source.signal.status).eql('open'); - - const caseUpdated = await createComment({ - supertest, - caseId: postedCase.id, - params: { - alertId: alert._id, - index: alert._index, - rule: { - id: 'id', - name: 'name', - }, - type: CommentType.alert, - owner: 'securitySolutionFixture', - }, - }); - - // Update the status of the case with sync alerts off - const caseStatusUpdated = await updateCase({ - supertest, - params: { - cases: [ - { - id: caseUpdated.id, - version: caseUpdated.version, - status: CaseStatuses['in-progress'], - }, - ], - }, - }); - - // Turn sync alerts on - await updateCase({ - supertest, - params: { - cases: [ - { - id: caseStatusUpdated[0].id, - version: caseStatusUpdated[0].version, - settings: { syncAlerts: true }, - }, - ], - }, - }); - - // refresh the index because syncAlerts was set to true so the alert's status should have been updated - await es.indices.refresh({ index: alert._index }); - - const { body: updatedAlert } = await supertest - .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) - .set('kbn-xsrf', 'true') - .send(getQuerySignalIds([alert._id])) - .expect(200); - - expect(updatedAlert.hits.hits[0]._source.signal.status).eql('in-progress'); - }); - - it('it does NOT updates alert status when syncAlerts is turned off', async () => { - const rule = getRuleForSignalTesting(['auditbeat-*']); - - const postedCase = await createCase(supertest, postCaseReq); - const { id } = await createRule(supertest, rule); - await waitForRuleSuccessOrStatus(supertest, id); - await waitForSignalsToBePresent(supertest, 1, [id]); - const signals = await getSignalsByIds(supertest, [id]); - - const alert = signals.hits.hits[0]; - expect(alert._source.signal.status).eql('open'); - - const caseUpdated = await createComment({ - supertest, - caseId: postedCase.id, - params: { - alertId: alert._id, - index: alert._index, - type: CommentType.alert, - rule: { - id: 'id', - name: 'name', - }, - owner: 'securitySolutionFixture', - }, - }); - - // Turn sync alerts off - const caseSettingsUpdated = await updateCase({ - supertest, - params: { - cases: [ - { - id: caseUpdated.id, - version: caseUpdated.version, - settings: { syncAlerts: false }, - }, - ], - }, - }); + expect(patchedCases[0].owner).to.eql('securitySolutionFixture'); + expect(patchedCases[1].owner).to.eql('securitySolutionFixture'); + expect(patchedCases[2].owner).to.eql('securitySolutionFixture'); + }); - // Update the status of the case with sync alerts off - await updateCase({ - supertest, - params: { - cases: [ - { - id: caseSettingsUpdated[0].id, - version: caseSettingsUpdated[0].version, - status: CaseStatuses['in-progress'], - }, - ], + it('should not update a case when the user does not have the correct ownership', async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + obsOnlyNoSpaceAuth + ); + + await updateCase({ + supertest: supertestWithoutAuth, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + title: 'new title', }, - }); - - const { body: updatedAlert } = await supertest - .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) - .set('kbn-xsrf', 'true') - .send(getQuerySignalIds([alert._id])) - .expect(200); - - expect(updatedAlert.hits.hits[0]._source.signal.status).eql('open'); - }); + ], + }, + auth: secOnlyNoSpaceAuth, + expectedHttpCode: 403, }); }); - describe('rbac', () => { - const supertestWithoutAuth = getService('supertestWithoutAuth'); - - it('should update a case when the user has the correct permissions', async () => { - const postedCase = await createCase(supertestWithoutAuth, postCaseReq, 200, { - user: secOnly, - space: 'space1', - }); - - const patchedCases = await updateCase({ - supertest: supertestWithoutAuth, - params: { - cases: [ - { - id: postedCase.id, - version: postedCase.version, - title: 'new title', - }, - ], - }, - auth: { user: secOnly, space: 'space1' }, - }); - - expect(patchedCases[0].owner).to.eql('securitySolutionFixture'); - }); - - it('should update multiple cases when the user has the correct permissions', async () => { - const [case1, case2, case3] = await Promise.all([ - createCase(supertestWithoutAuth, postCaseReq, 200, { - user: superUser, - space: 'space1', - }), - createCase(supertestWithoutAuth, postCaseReq, 200, { + it('should not update any cases when the user does not have the correct ownership', async () => { + const [case1, case2, case3] = await Promise.all([ + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + { user: superUser, - space: 'space1', - }), - createCase(supertestWithoutAuth, postCaseReq, 200, { + space: null, + } + ), + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + { user: superUser, - space: 'space1', - }), - ]); - - const patchedCases = await updateCase({ - supertest: supertestWithoutAuth, - params: { - cases: [ - { - id: case1.id, - version: case1.version, - title: 'new title', - }, - { - id: case2.id, - version: case2.version, - title: 'new title', - }, - { - id: case3.id, - version: case3.version, - title: 'new title', - }, - ], - }, - auth: { user: secOnly, space: 'space1' }, - }); - - expect(patchedCases[0].owner).to.eql('securitySolutionFixture'); - expect(patchedCases[1].owner).to.eql('securitySolutionFixture'); - expect(patchedCases[2].owner).to.eql('securitySolutionFixture'); - }); - - it('should not update a case when the user does not have the correct ownership', async () => { - const postedCase = await createCase( + space: null, + } + ), + createCase( supertestWithoutAuth, getPostCaseRequest({ owner: 'observabilityFixture' }), 200, - { user: obsOnly, space: 'space1' } - ); - - await updateCase({ - supertest: supertestWithoutAuth, - params: { - cases: [ - { - id: postedCase.id, - version: postedCase.version, - title: 'new title', - }, - ], - }, - auth: { user: secOnly, space: 'space1' }, - expectedHttpCode: 403, - }); - }); + { + user: superUser, + space: null, + } + ), + ]); - it('should not update any cases when the user does not have the correct ownership', async () => { - const [case1, case2, case3] = await Promise.all([ - createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'observabilityFixture' }), - 200, - { - user: superUser, - space: 'space1', - } - ), - createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'observabilityFixture' }), - 200, + await updateCase({ + supertest: supertestWithoutAuth, + params: { + cases: [ { - user: superUser, - space: 'space1', - } - ), - createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'observabilityFixture' }), - 200, + id: case1.id, + version: case1.version, + title: 'new title', + }, { - user: superUser, - space: 'space1', - } - ), - ]); - - await updateCase({ - supertest: supertestWithoutAuth, - params: { - cases: [ - { - id: case1.id, - version: case1.version, - title: 'new title', - }, - { - id: case2.id, - version: case2.version, - title: 'new title', - }, - { - id: case3.id, - version: case3.version, - title: 'new title', - }, - ], - }, - auth: { user: secOnly, space: 'space1' }, - expectedHttpCode: 403, - }); - - const resp = await findCases({ supertest, auth: superUserSpace1Auth }); - expect(resp.cases.length).to.eql(3); - // the update should have failed and none of the title should have been changed - expect(resp.cases[0].title).to.eql(postCaseReq.title); - expect(resp.cases[1].title).to.eql(postCaseReq.title); - expect(resp.cases[2].title).to.eql(postCaseReq.title); - }); - - for (const user of [globalRead, secOnlyRead, obsOnlyRead, obsSecRead, noKibanaPrivileges]) { - it(`User ${ - user.username - } with role(s) ${user.roles.join()} - should NOT update a case`, async () => { - const postedCase = await createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'securitySolutionFixture' }), - 200, + id: case2.id, + version: case2.version, + title: 'new title', + }, { - user: superUser, - space: 'space1', - } - ); + id: case3.id, + version: case3.version, + title: 'new title', + }, + ], + }, + auth: secOnlyNoSpaceAuth, + expectedHttpCode: 403, + }); + + const resp = await findCases({ supertest, auth: getAuthWithSuperUser(null) }); + expect(resp.cases.length).to.eql(3); + // the update should have failed and none of the title should have been changed + expect(resp.cases[0].title).to.eql(postCaseReq.title); + expect(resp.cases[1].title).to.eql(postCaseReq.title); + expect(resp.cases[2].title).to.eql(postCaseReq.title); + }); - await updateCase({ - supertest: supertestWithoutAuth, - params: { - cases: [ - { - id: postedCase.id, - version: postedCase.version, - title: 'new title', - }, - ], - }, - auth: { user, 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 update a case`, async () => { + const postedCase = await createCase(supertestWithoutAuth, getPostCaseRequest(), 200, { + user: superUser, + space: null, }); - } - - it('should NOT create a case in a space with no permissions', async () => { - const postedCase = await createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'securitySolutionFixture' }), - 200, - { - user: superUser, - space: 'space2', - } - ); await updateCase({ supertest: supertestWithoutAuth, @@ -1231,10 +218,32 @@ export default ({ getService }: FtrProviderContext): void => { }, ], }, - auth: { user: secOnly, space: 'space2' }, + auth: { user, space: null }, expectedHttpCode: 403, }); }); + } + + it('should return a 404 when attempting to access a space', async () => { + const postedCase = await createCase(supertestWithoutAuth, getPostCaseRequest(), 200, { + user: superUser, + space: null, + }); + + await updateCase({ + supertest: supertestWithoutAuth, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + title: 'new title', + }, + ], + }, + auth: { user: secOnly, space: 'space1' }, + expectedHttpCode: 404, + }); }); }); }; diff --git a/x-pack/test/case_api_integration/security_only/tests/common/cases/post_case.ts b/x-pack/test/case_api_integration/security_only/tests/common/cases/post_case.ts index 50294201f6fbee..7f718aad6094bc 100644 --- a/x-pack/test/case_api_integration/security_only/tests/common/cases/post_case.ts +++ b/x-pack/test/case_api_integration/security_only/tests/common/cases/post_case.ts @@ -5,25 +5,10 @@ * 2.0. */ -/* eslint-disable @typescript-eslint/naming-convention */ - import expect from '@kbn/expect'; -import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; -import { - ConnectorTypes, - ConnectorJiraTypeFields, - CaseStatuses, - CaseUserActionResponse, -} from '../../../../../../plugins/cases/common/api'; -import { getPostCaseRequest, postCaseResp, defaultUser } from '../../../../common/lib/mock'; -import { - deleteCasesByESQuery, - createCase, - removeServerGeneratedPropertiesFromCase, - removeServerGeneratedPropertiesFromUserAction, - getCaseUserActions, -} from '../../../../common/lib/utils'; +import { getPostCaseRequest } from '../../../../common/lib/mock'; +import { deleteCasesByESQuery, createCase } from '../../../../common/lib/utils'; import { secOnly, secOnlyRead, @@ -33,10 +18,10 @@ import { noKibanaPrivileges, } from '../../../../common/lib/authentication/users'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { secOnlyNoSpaceAuth } from '../../../utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { - const supertest = getService('supertest'); const es = getService('es'); const supertestWithoutAuth = getService('supertestWithoutAuth'); @@ -45,248 +30,51 @@ export default ({ getService }: FtrProviderContext): void => { await deleteCasesByESQuery(es); }); - describe('happy path', () => { - it('should post a case', async () => { - const postedCase = await createCase( - supertest, - getPostCaseRequest({ - connector: { - id: '123', - name: 'Jira', - type: ConnectorTypes.jira, - fields: { issueType: 'Task', priority: 'High', parent: null }, - }, - }) - ); - const data = removeServerGeneratedPropertiesFromCase(postedCase); - - expect(data).to.eql( - postCaseResp( - null, - getPostCaseRequest({ - connector: { - id: '123', - name: 'Jira', - type: ConnectorTypes.jira, - fields: { issueType: 'Task', priority: 'High', parent: null }, - }, - }) - ) - ); - }); - - it('should post a case: none connector', async () => { - const postedCase = await createCase( - supertest, - getPostCaseRequest({ - connector: { - id: 'none', - name: 'none', - type: ConnectorTypes.none, - fields: null, - }, - }) - ); - const data = removeServerGeneratedPropertiesFromCase(postedCase); - - expect(data).to.eql( - postCaseResp( - null, - getPostCaseRequest({ - connector: { - id: 'none', - name: 'none', - type: ConnectorTypes.none, - fields: null, - }, - }) - ) - ); - }); - - it('should create a user action when creating a case', async () => { - const postedCase = await createCase(supertest, getPostCaseRequest()); - const userActions = await getCaseUserActions({ supertest, caseID: postedCase.id }); - const creationUserAction = removeServerGeneratedPropertiesFromUserAction(userActions[0]); - - const { new_value, ...rest } = creationUserAction as CaseUserActionResponse; - const parsedNewValue = JSON.parse(new_value!); - - expect(rest).to.eql({ - action_field: [ - 'description', - 'status', - 'tags', - 'title', - 'connector', - 'settings', - 'owner', - ], - action: 'create', - action_by: defaultUser, - old_value: null, - case_id: `${postedCase.id}`, - comment_id: null, - sub_case_id: '', - owner: 'securitySolutionFixture', - }); - - expect(parsedNewValue).to.eql({ - type: postedCase.type, - description: postedCase.description, - title: postedCase.title, - tags: postedCase.tags, - connector: postedCase.connector, - settings: postedCase.settings, - owner: postedCase.owner, - }); - }); - - it('creates the case without connector in the configuration', async () => { - const postedCase = await createCase(supertest, getPostCaseRequest()); - const data = removeServerGeneratedPropertiesFromCase(postedCase); - - expect(data).to.eql(postCaseResp()); - }); + it('User: security solution only - should create a case', async () => { + const theCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + secOnlyNoSpaceAuth + ); + expect(theCase.owner).to.eql('securitySolutionFixture'); }); - describe('unhappy path', () => { - it('400s when bad query supplied', async () => { - await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - // @ts-expect-error - .send({ ...getPostCaseRequest({ badKey: true }) }) - .expect(400); - }); - - it('400s when connector is not supplied', async () => { - const { connector, ...caseWithoutConnector } = getPostCaseRequest(); - - await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send(caseWithoutConnector) - .expect(400); - }); - - it('400s when connector has wrong type', async () => { - await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send({ - ...getPostCaseRequest({ - // @ts-expect-error - connector: { id: 'wrong', name: 'wrong', type: '.not-exists', fields: null }, - }), - }) - .expect(400); - }); - - it('400s when connector has wrong fields', async () => { - await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send({ - ...getPostCaseRequest({ - // @ts-expect-error - connector: { - id: 'wrong', - name: 'wrong', - type: ConnectorTypes.jira, - fields: { unsupported: 'value' }, - } as ConnectorJiraTypeFields, - }), - }) - .expect(400); - }); - - it('400s when missing title', async () => { - const { title, ...caseWithoutTitle } = getPostCaseRequest(); - - await supertest.post(CASES_URL).set('kbn-xsrf', 'true').send(caseWithoutTitle).expect(400); - }); - - it('400s when missing description', async () => { - const { description, ...caseWithoutDescription } = getPostCaseRequest(); - - await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send(caseWithoutDescription) - .expect(400); - }); - - it('400s when missing tags', async () => { - const { tags, ...caseWithoutTags } = getPostCaseRequest(); - - await supertest.post(CASES_URL).set('kbn-xsrf', 'true').send(caseWithoutTags).expect(400); - }); - - it('400s if you passing status for a new case', async () => { - const req = getPostCaseRequest(); - - await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send({ ...req, status: CaseStatuses.open }) - .expect(400); - }); + it('User: security solution only - should NOT create a case of different owner', async () => { + await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 403, + secOnlyNoSpaceAuth + ); }); - describe('rbac', () => { - it('User: security solution only - should create a case', async () => { - const theCase = await createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'securitySolutionFixture' }), - 200, - { - user: secOnly, - space: 'space1', - } - ); - expect(theCase.owner).to.eql('securitySolutionFixture'); - }); - - it('User: security solution only - should NOT create a case of different owner', async () => { - await createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'observabilityFixture' }), - 403, - { - user: secOnly, - space: 'space1', - } - ); - }); - - for (const user of [globalRead, secOnlyRead, obsOnlyRead, obsSecRead, noKibanaPrivileges]) { - it(`User ${ - user.username - } with role(s) ${user.roles.join()} - should NOT create a case`, async () => { - await createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'securitySolutionFixture' }), - 403, - { - user, - space: 'space1', - } - ); - }); - } - - it('should NOT create a case in a space with no permissions', async () => { + for (const user of [globalRead, secOnlyRead, obsOnlyRead, obsSecRead, noKibanaPrivileges]) { + it(`User ${ + user.username + } with role(s) ${user.roles.join()} - should NOT create a case`, async () => { await createCase( supertestWithoutAuth, getPostCaseRequest({ owner: 'securitySolutionFixture' }), 403, { - user: secOnly, - space: 'space2', + user, + space: null, } ); }); + } + + it('should return a 404 when attempting to access a space', async () => { + await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 404, + { + user: secOnly, + space: 'space1', + } + ); }); }); }; diff --git a/x-pack/test/case_api_integration/security_only/tests/common/cases/reporters/get_reporters.ts b/x-pack/test/case_api_integration/security_only/tests/common/cases/reporters/get_reporters.ts index e34d9ccad39ac6..2f9bb62837192a 100644 --- a/x-pack/test/case_api_integration/security_only/tests/common/cases/reporters/get_reporters.ts +++ b/x-pack/test/case_api_integration/security_only/tests/common/cases/reporters/get_reporters.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; -import { defaultUser, getPostCaseRequest } from '../../../../../common/lib/mock'; +import { getPostCaseRequest } from '../../../../../common/lib/mock'; import { createCase, deleteCasesByESQuery, getReporters } from '../../../../../common/lib/utils'; import { secOnly, @@ -22,10 +22,10 @@ import { obsSec, } from '../../../../../common/lib/authentication/users'; import { getUserInfo } from '../../../../../common/lib/authentication'; +import { secOnlyNoSpaceAuth, obsOnlyNoSpaceAuth } from '../../../../utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { - const supertest = getService('supertest'); const supertestWithoutAuth = getService('supertestWithoutAuth'); const es = getService('es'); @@ -34,168 +34,123 @@ export default ({ getService }: FtrProviderContext): void => { await deleteCasesByESQuery(es); }); - it('should return reporters', async () => { - await createCase(supertest, getPostCaseRequest()); - const reporters = await getReporters({ supertest: supertestWithoutAuth }); + it('User: security solution only - should read the correct reporters', async () => { + await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + secOnlyNoSpaceAuth + ); + + await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + obsOnlyNoSpaceAuth + ); - expect(reporters).to.eql([defaultUser]); + for (const scenario of [ + { + user: globalRead, + expectedReporters: [getUserInfo(secOnly), getUserInfo(obsOnly)], + }, + { + user: superUser, + expectedReporters: [getUserInfo(secOnly), getUserInfo(obsOnly)], + }, + { user: secOnlyRead, expectedReporters: [getUserInfo(secOnly)] }, + { user: obsOnlyRead, expectedReporters: [getUserInfo(obsOnly)] }, + { + user: obsSecRead, + expectedReporters: [getUserInfo(secOnly), getUserInfo(obsOnly)], + }, + ]) { + const reporters = await getReporters({ + supertest: supertestWithoutAuth, + expectedHttpCode: 200, + auth: { + user: scenario.user, + space: null, + }, + }); + + expect(reporters).to.eql(scenario.expectedReporters); + } }); - it('should return unique reporters', async () => { - await createCase(supertest, getPostCaseRequest()); - await createCase(supertest, getPostCaseRequest()); - const reporters = await getReporters({ supertest: supertestWithoutAuth }); + it(`User ${ + noKibanaPrivileges.username + } with role(s) ${noKibanaPrivileges.roles.join()} - should NOT get all reporters`, async () => { + // super user creates a case at the appropriate space + await createCase(supertestWithoutAuth, getPostCaseRequest(), 200, { + user: superUser, + space: null, + }); - expect(reporters).to.eql([defaultUser]); + // user should not be able to get all reporters at the appropriate space + await getReporters({ + supertest: supertestWithoutAuth, + expectedHttpCode: 403, + auth: { user: noKibanaPrivileges, space: null }, + }); }); - describe('rbac', () => { - it('User: security solution only - should read the correct reporters', async () => { - await createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'securitySolutionFixture' }), - 200, - { - user: secOnly, - space: 'space1', - } - ); + it('should return a 404 when attempting to access a space', async () => { + await createCase(supertestWithoutAuth, getPostCaseRequest(), 200, { + user: superUser, + space: null, + }); + + await getReporters({ + supertest: supertestWithoutAuth, + expectedHttpCode: 404, + auth: { user: obsSec, space: 'space1' }, + }); + }); - await createCase( + it('should respect the owner filter when having permissions', async () => { + await Promise.all([ + createCase(supertestWithoutAuth, getPostCaseRequest(), 200, secOnlyNoSpaceAuth), + createCase( supertestWithoutAuth, getPostCaseRequest({ owner: 'observabilityFixture' }), 200, - { - user: obsOnly, - space: 'space1', - } - ); - - for (const scenario of [ - { - user: globalRead, - expectedReporters: [getUserInfo(secOnly), getUserInfo(obsOnly)], - }, - { - user: superUser, - expectedReporters: [getUserInfo(secOnly), getUserInfo(obsOnly)], - }, - { user: secOnlyRead, expectedReporters: [getUserInfo(secOnly)] }, - { user: obsOnlyRead, expectedReporters: [getUserInfo(obsOnly)] }, - { - user: obsSecRead, - expectedReporters: [getUserInfo(secOnly), getUserInfo(obsOnly)], - }, - ]) { - const reporters = await getReporters({ - supertest: supertestWithoutAuth, - expectedHttpCode: 200, - auth: { - user: scenario.user, - space: 'space1', - }, - }); - - expect(reporters).to.eql(scenario.expectedReporters); - } + obsOnlyNoSpaceAuth + ), + ]); + + const reporters = await getReporters({ + supertest: supertestWithoutAuth, + auth: { + user: obsSec, + space: null, + }, + query: { owner: 'securitySolutionFixture' }, }); - for (const scenario of [ - { user: noKibanaPrivileges, space: 'space1' }, - { user: secOnly, space: 'space2' }, - ]) { - it(`User ${scenario.user.username} with role(s) ${scenario.user.roles.join()} and space ${ - scenario.space - } - should NOT get all reporters`, async () => { - // super user creates a case at the appropriate space - await createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'securitySolutionFixture' }), - 200, - { - user: superUser, - space: scenario.space, - } - ); - - // user should not be able to get all reporters at the appropriate space - await getReporters({ - supertest: supertestWithoutAuth, - expectedHttpCode: 403, - auth: { user: scenario.user, space: scenario.space }, - }); - }); - } - - it('should respect the owner filter when having permissions', async () => { - await Promise.all([ - createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'securitySolutionFixture' }), - 200, - { - user: secOnly, - space: 'space1', - } - ), - createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'observabilityFixture' }), - 200, - { - user: obsOnly, - space: 'space1', - } - ), - ]); - - const reporters = await getReporters({ - supertest: supertestWithoutAuth, - auth: { - user: obsSec, - space: 'space1', - }, - query: { owner: 'securitySolutionFixture' }, - }); + expect(reporters).to.eql([getUserInfo(secOnly)]); + }); - expect(reporters).to.eql([getUserInfo(secOnly)]); + it('should return the correct cases when trying to exploit RBAC through the owner query parameter', async () => { + await Promise.all([ + createCase(supertestWithoutAuth, getPostCaseRequest(), 200, secOnlyNoSpaceAuth), + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + obsOnlyNoSpaceAuth + ), + ]); + + // User with permissions only to security solution request reporters from observability + const reporters = await getReporters({ + supertest: supertestWithoutAuth, + auth: secOnlyNoSpaceAuth, + query: { owner: ['securitySolutionFixture', 'observabilityFixture'] }, }); - it('should return the correct cases when trying to exploit RBAC through the owner query parameter', async () => { - await Promise.all([ - createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'securitySolutionFixture' }), - 200, - { - user: secOnly, - space: 'space1', - } - ), - createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'observabilityFixture' }), - 200, - { - user: obsOnly, - space: 'space1', - } - ), - ]); - - // User with permissions only to security solution request reporters from observability - const reporters = await getReporters({ - supertest: supertestWithoutAuth, - auth: { - user: secOnly, - space: 'space1', - }, - query: { owner: ['securitySolutionFixture', 'observabilityFixture'] }, - }); - - // Only security solution reporters are being returned - expect(reporters).to.eql([getUserInfo(secOnly)]); - }); + // Only security solution reporters are being returned + expect(reporters).to.eql([getUserInfo(secOnly)]); }); }); }; diff --git a/x-pack/test/case_api_integration/security_only/tests/common/cases/status/get_status.ts b/x-pack/test/case_api_integration/security_only/tests/common/cases/status/get_status.ts index 7a17cf1dd8e081..6c11bf5d39ee8f 100644 --- a/x-pack/test/case_api_integration/security_only/tests/common/cases/status/get_status.ts +++ b/x-pack/test/case_api_integration/security_only/tests/common/cases/status/get_status.ts @@ -9,13 +9,13 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; import { CaseStatuses } from '../../../../../../../plugins/cases/common/api'; -import { getPostCaseRequest, postCaseReq } from '../../../../../common/lib/mock'; +import { getPostCaseRequest } from '../../../../../common/lib/mock'; import { createCase, updateCase, getAllCasesStatuses, deleteAllCaseItems, - superUserSpace1Auth, + getAuthWithSuperUser, } from '../../../../../common/lib/utils'; import { globalRead, @@ -29,7 +29,6 @@ import { // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { - const supertest = getService('supertest'); const es = getService('es'); describe('get_status', () => { @@ -37,136 +36,97 @@ export default ({ getService }: FtrProviderContext): void => { await deleteAllCaseItems(es); }); - it('should return case statuses', async () => { - const [, inProgressCase, postedCase] = await Promise.all([ - createCase(supertest, postCaseReq), - createCase(supertest, postCaseReq), - createCase(supertest, postCaseReq), + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const superUserAuth = getAuthWithSuperUser(null); + + it('should return the correct status stats', async () => { + /** + * Owner: Sec + * open: 0, in-prog: 1, closed: 1 + * Owner: Obs + * open: 1, in-prog: 1 + */ + const [inProgressSec, closedSec, , inProgressObs] = await Promise.all([ + createCase(supertestWithoutAuth, getPostCaseRequest(), 200, superUserAuth), + createCase(supertestWithoutAuth, getPostCaseRequest(), 200, superUserAuth), + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + superUserAuth + ), + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + superUserAuth + ), ]); await updateCase({ - supertest, + supertest: supertestWithoutAuth, params: { cases: [ { - id: inProgressCase.id, - version: inProgressCase.version, + id: inProgressSec.id, + version: inProgressSec.version, status: CaseStatuses['in-progress'], }, { - id: postedCase.id, - version: postedCase.version, + id: closedSec.id, + version: closedSec.version, status: CaseStatuses.closed, }, + { + id: inProgressObs.id, + version: inProgressObs.version, + status: CaseStatuses['in-progress'], + }, ], }, + auth: superUserAuth, }); - const statuses = await getAllCasesStatuses({ supertest }); - - expect(statuses).to.eql({ - count_open_cases: 1, - count_closed_cases: 1, - count_in_progress_cases: 1, - }); - }); - - describe('rbac', () => { - const supertestWithoutAuth = getService('supertestWithoutAuth'); - - it('should return the correct status stats', async () => { - /** - * Owner: Sec - * open: 0, in-prog: 1, closed: 1 - * Owner: Obs - * open: 1, in-prog: 1 - */ - const [inProgressSec, closedSec, , inProgressObs] = await Promise.all([ - createCase(supertestWithoutAuth, getPostCaseRequest(), 200, { - user: superUser, - space: 'space1', - }), - createCase(supertestWithoutAuth, getPostCaseRequest(), 200, { - user: superUser, - space: 'space1', - }), - createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'observabilityFixture' }), - 200, - superUserSpace1Auth - ), - createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'observabilityFixture' }), - 200, - superUserSpace1Auth - ), - ]); - - await updateCase({ + for (const scenario of [ + { user: globalRead, stats: { open: 1, inProgress: 2, closed: 1 } }, + { user: superUser, stats: { open: 1, inProgress: 2, closed: 1 } }, + { user: secOnlyRead, stats: { open: 0, inProgress: 1, closed: 1 } }, + { user: obsOnlyRead, stats: { open: 1, inProgress: 1, closed: 0 } }, + { user: obsSecRead, stats: { open: 1, inProgress: 2, closed: 1 } }, + ]) { + const statuses = await getAllCasesStatuses({ supertest: supertestWithoutAuth, - params: { - cases: [ - { - id: inProgressSec.id, - version: inProgressSec.version, - status: CaseStatuses['in-progress'], - }, - { - id: closedSec.id, - version: closedSec.version, - status: CaseStatuses.closed, - }, - { - id: inProgressObs.id, - version: inProgressObs.version, - status: CaseStatuses['in-progress'], - }, - ], - }, - auth: superUserSpace1Auth, + auth: { user: scenario.user, space: null }, + }); + + expect(statuses).to.eql({ + count_open_cases: scenario.stats.open, + count_closed_cases: scenario.stats.closed, + count_in_progress_cases: scenario.stats.inProgress, }); + } + }); - for (const scenario of [ - { user: globalRead, stats: { open: 1, inProgress: 2, closed: 1 } }, - { user: superUser, stats: { open: 1, inProgress: 2, closed: 1 } }, - { user: secOnlyRead, stats: { open: 0, inProgress: 1, closed: 1 } }, - { user: obsOnlyRead, stats: { open: 1, inProgress: 1, closed: 0 } }, - { user: obsSecRead, stats: { open: 1, inProgress: 2, closed: 1 } }, - ]) { - const statuses = await getAllCasesStatuses({ - supertest: supertestWithoutAuth, - auth: { user: scenario.user, space: 'space1' }, - }); + it(`should return a 403 when retrieving the statuses when the user ${ + noKibanaPrivileges.username + } with role(s) ${noKibanaPrivileges.roles.join()}`, async () => { + await createCase(supertestWithoutAuth, getPostCaseRequest(), 200, superUserAuth); - expect(statuses).to.eql({ - count_open_cases: scenario.stats.open, - count_closed_cases: scenario.stats.closed, - count_in_progress_cases: scenario.stats.inProgress, - }); - } + await getAllCasesStatuses({ + supertest: supertestWithoutAuth, + auth: { user: noKibanaPrivileges, space: null }, + expectedHttpCode: 403, }); + }); - for (const scenario of [ - { user: noKibanaPrivileges, space: 'space1' }, - { user: secOnly, space: 'space2' }, - ]) { - it(`should return a 403 when retrieving the statuses when the user ${ - scenario.user.username - } with role(s) ${scenario.user.roles.join()} and space ${scenario.space}`, async () => { - await createCase(supertestWithoutAuth, getPostCaseRequest(), 200, { - user: superUser, - space: scenario.space, - }); + it('should return a 404 when attempting to access a space', async () => { + await createCase(supertestWithoutAuth, getPostCaseRequest(), 200, superUserAuth); - await getAllCasesStatuses({ - supertest: supertestWithoutAuth, - auth: { user: scenario.user, space: scenario.space }, - expectedHttpCode: 403, - }); - }); - } + await getAllCasesStatuses({ + supertest: supertestWithoutAuth, + auth: { user: secOnly, space: 'space1' }, + expectedHttpCode: 404, + }); }); }); }; diff --git a/x-pack/test/case_api_integration/security_only/tests/common/cases/tags/get_tags.ts b/x-pack/test/case_api_integration/security_only/tests/common/cases/tags/get_tags.ts index 0c7237683666fd..8e225dff71eade 100644 --- a/x-pack/test/case_api_integration/security_only/tests/common/cases/tags/get_tags.ts +++ b/x-pack/test/case_api_integration/security_only/tests/common/cases/tags/get_tags.ts @@ -12,19 +12,17 @@ import { deleteCasesByESQuery, createCase, getTags } from '../../../../../common import { getPostCaseRequest } from '../../../../../common/lib/mock'; import { secOnly, - obsOnly, globalRead, superUser, secOnlyRead, obsOnlyRead, obsSecRead, noKibanaPrivileges, - obsSec, } from '../../../../../common/lib/authentication/users'; +import { secOnlyNoSpaceAuth, obsOnlyNoSpaceAuth, obsSecNoSpaceAuth } from '../../../../utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { - const supertest = getService('supertest'); const supertestWithoutAuth = getService('supertestWithoutAuth'); const es = getService('es'); @@ -33,169 +31,141 @@ export default ({ getService }: FtrProviderContext): void => { await deleteCasesByESQuery(es); }); - it('should return case tags', async () => { - await createCase(supertest, getPostCaseRequest()); - await createCase(supertest, getPostCaseRequest({ tags: ['unique'] })); + it('should read the correct tags', async () => { + await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture', tags: ['sec'] }), + 200, + secOnlyNoSpaceAuth + ); + + await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture', tags: ['obs'] }), + 200, + obsOnlyNoSpaceAuth + ); - const tags = await getTags({ supertest }); - expect(tags).to.eql(['defacement', 'unique']); + for (const scenario of [ + { + user: globalRead, + expectedTags: ['sec', 'obs'], + }, + { + user: superUser, + expectedTags: ['sec', 'obs'], + }, + { user: secOnlyRead, expectedTags: ['sec'] }, + { user: obsOnlyRead, expectedTags: ['obs'] }, + { + user: obsSecRead, + expectedTags: ['sec', 'obs'], + }, + ]) { + const tags = await getTags({ + supertest: supertestWithoutAuth, + expectedHttpCode: 200, + auth: { + user: scenario.user, + space: null, + }, + }); + + expect(tags).to.eql(scenario.expectedTags); + } }); - it('should return unique tags', async () => { - await createCase(supertest, getPostCaseRequest()); - await createCase(supertest, getPostCaseRequest()); + it(`User ${ + noKibanaPrivileges.username + } with role(s) ${noKibanaPrivileges.roles.join()} - should NOT get all tags`, async () => { + // super user creates a case at the appropriate space + await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture', tags: ['sec'] }), + 200, + { + user: superUser, + space: null, + } + ); - const tags = await getTags({ supertest }); - expect(tags).to.eql(['defacement']); + // user should not be able to get all tags at the appropriate space + await getTags({ + supertest: supertestWithoutAuth, + expectedHttpCode: 403, + auth: { user: noKibanaPrivileges, space: null }, + }); }); - describe('rbac', () => { - it('should read the correct tags', async () => { - await createCase( + it('should return a 404 when attempting to access a space', async () => { + // super user creates a case at the appropriate space + await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture', tags: ['sec'] }), + 200, + { + user: superUser, + space: null, + } + ); + + await getTags({ + supertest: supertestWithoutAuth, + expectedHttpCode: 404, + auth: { user: secOnly, space: 'space1' }, + }); + }); + + it('should respect the owner filter when having permissions', async () => { + await Promise.all([ + createCase( supertestWithoutAuth, getPostCaseRequest({ owner: 'securitySolutionFixture', tags: ['sec'] }), 200, - { - user: secOnly, - space: 'space1', - } - ); - - await createCase( + obsSecNoSpaceAuth + ), + createCase( supertestWithoutAuth, getPostCaseRequest({ owner: 'observabilityFixture', tags: ['obs'] }), 200, - { - user: obsOnly, - space: 'space1', - } - ); - - for (const scenario of [ - { - user: globalRead, - expectedTags: ['sec', 'obs'], - }, - { - user: superUser, - expectedTags: ['sec', 'obs'], - }, - { user: secOnlyRead, expectedTags: ['sec'] }, - { user: obsOnlyRead, expectedTags: ['obs'] }, - { - user: obsSecRead, - expectedTags: ['sec', 'obs'], - }, - ]) { - const tags = await getTags({ - supertest: supertestWithoutAuth, - expectedHttpCode: 200, - auth: { - user: scenario.user, - space: 'space1', - }, - }); - - expect(tags).to.eql(scenario.expectedTags); - } + obsSecNoSpaceAuth + ), + ]); + + const tags = await getTags({ + supertest: supertestWithoutAuth, + auth: obsSecNoSpaceAuth, + query: { owner: 'securitySolutionFixture' }, }); - for (const scenario of [ - { user: noKibanaPrivileges, space: 'space1' }, - { user: secOnly, space: 'space2' }, - ]) { - it(`User ${scenario.user.username} with role(s) ${scenario.user.roles.join()} and space ${ - scenario.space - } - should NOT get all tags`, async () => { - // super user creates a case at the appropriate space - await createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'securitySolutionFixture', tags: ['sec'] }), - 200, - { - user: superUser, - space: scenario.space, - } - ); - - // user should not be able to get all tags at the appropriate space - await getTags({ - supertest: supertestWithoutAuth, - expectedHttpCode: 403, - auth: { user: scenario.user, space: scenario.space }, - }); - }); - } - - it('should respect the owner filter when having permissions', async () => { - await Promise.all([ - createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'securitySolutionFixture', tags: ['sec'] }), - 200, - { - user: obsSec, - space: 'space1', - } - ), - createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'observabilityFixture', tags: ['obs'] }), - 200, - { - user: obsSec, - space: 'space1', - } - ), - ]); - - const tags = await getTags({ - supertest: supertestWithoutAuth, - auth: { - user: obsSec, - space: 'space1', - }, - query: { owner: 'securitySolutionFixture' }, - }); + expect(tags).to.eql(['sec']); + }); - expect(tags).to.eql(['sec']); + it('should return the correct cases when trying to exploit RBAC through the owner query parameter', async () => { + await Promise.all([ + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture', tags: ['sec'] }), + 200, + obsSecNoSpaceAuth + ), + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture', tags: ['obs'] }), + 200, + obsSecNoSpaceAuth + ), + ]); + + // User with permissions only to security solution request tags from observability + const tags = await getTags({ + supertest: supertestWithoutAuth, + auth: secOnlyNoSpaceAuth, + query: { owner: ['securitySolutionFixture', 'observabilityFixture'] }, }); - it('should return the correct cases when trying to exploit RBAC through the owner query parameter', async () => { - await Promise.all([ - createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'securitySolutionFixture', tags: ['sec'] }), - 200, - { - user: obsSec, - space: 'space1', - } - ), - createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'observabilityFixture', tags: ['obs'] }), - 200, - { - user: obsSec, - space: 'space1', - } - ), - ]); - - // User with permissions only to security solution request tags from observability - const tags = await getTags({ - supertest: supertestWithoutAuth, - auth: { - user: secOnly, - space: 'space1', - }, - query: { owner: ['securitySolutionFixture', 'observabilityFixture'] }, - }); - - // Only security solution tags are being returned - expect(tags).to.eql(['sec']); - }); + // Only security solution tags are being returned + expect(tags).to.eql(['sec']); }); }); }; diff --git a/x-pack/test/case_api_integration/security_only/tests/common/comments/delete_comment.ts b/x-pack/test/case_api_integration/security_only/tests/common/comments/delete_comment.ts index b7b97557dcd250..d807dfa8b70470 100644 --- a/x-pack/test/case_api_integration/security_only/tests/common/comments/delete_comment.ts +++ b/x-pack/test/case_api_integration/security_only/tests/common/comments/delete_comment.ts @@ -5,16 +5,11 @@ * 2.0. */ -import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; -import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; -import { getPostCaseRequest, postCaseReq, postCommentUserReq } from '../../../../common/lib/mock'; +import { getPostCaseRequest, postCommentUserReq } from '../../../../common/lib/mock'; import { - createCaseAction, - createSubCase, deleteAllCaseItems, - deleteCaseAction, deleteCasesByESQuery, deleteCasesUserActions, deleteComments, @@ -22,23 +17,22 @@ import { createComment, deleteComment, deleteAllComments, - superUserSpace1Auth, + getAuthWithSuperUser, } from '../../../../common/lib/utils'; import { globalRead, noKibanaPrivileges, - obsOnly, obsOnlyRead, obsSecRead, secOnly, secOnlyRead, - superUser, } from '../../../../common/lib/authentication/users'; +import { obsOnlyNoSpaceAuth, secOnlyNoSpaceAuth } from '../../../utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { - const supertest = getService('supertest'); const es = getService('es'); + const superUserNoSpaceAuth = getAuthWithSuperUser(null); describe('delete_comment', () => { afterEach(async () => { @@ -47,325 +41,190 @@ export default ({ getService }: FtrProviderContext): void => { await deleteCasesUserActions(es); }); - describe('happy path', () => { - it('should delete a comment', async () => { - const postedCase = await createCase(supertest, postCaseReq); - const patchedCase = await createComment({ - supertest, - caseId: postedCase.id, - params: postCommentUserReq, - }); - const comment = await deleteComment({ - supertest, - caseId: postedCase.id, - commentId: patchedCase.comments![0].id, - }); + const supertestWithoutAuth = getService('supertestWithoutAuth'); - expect(comment).to.eql({}); - }); + afterEach(async () => { + await deleteAllCaseItems(es); }); - describe('unhappy path', () => { - it('404s when comment belongs to different case', async () => { - const postedCase = await createCase(supertest, postCaseReq); - const patchedCase = await createComment({ - supertest, - caseId: postedCase.id, - params: postCommentUserReq, - }); - const error = (await deleteComment({ - supertest, - caseId: 'fake-id', - commentId: patchedCase.comments![0].id, - expectedHttpCode: 404, - })) as Error; - - expect(error.message).to.be( - `This comment ${patchedCase.comments![0].id} does not exist in fake-id.` - ); - }); - - it('404s when comment is not there', async () => { - await deleteComment({ - supertest, - caseId: 'fake-id', - commentId: 'fake-id', - expectedHttpCode: 404, - }); - }); + it('should delete a comment from the appropriate owner', async () => { + const secCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest(), + 200, + secOnlyNoSpaceAuth + ); - it('should return a 400 when attempting to delete all comments when passing the `subCaseId` parameter', async () => { - const { body } = await supertest - .delete(`${CASES_URL}/case-id/comments?subCaseId=value`) - .set('kbn-xsrf', 'true') - .send() - .expect(400); - // make sure the failure is because of the subCaseId - expect(body.message).to.contain('disabled'); + const commentResp = await createComment({ + supertest: supertestWithoutAuth, + caseId: secCase.id, + params: postCommentUserReq, + auth: secOnlyNoSpaceAuth, }); - it('should return a 400 when attempting to delete a single comment when passing the `subCaseId` parameter', async () => { - const { body } = await supertest - .delete(`${CASES_URL}/case-id/comments/comment-id?subCaseId=value`) - .set('kbn-xsrf', 'true') - .send() - .expect(400); - // make sure the failure is because of the subCaseId - expect(body.message).to.contain('disabled'); + await deleteComment({ + supertest: supertestWithoutAuth, + caseId: secCase.id, + commentId: commentResp.comments![0].id, + auth: secOnlyNoSpaceAuth, }); }); - // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests - describe.skip('sub case comments', () => { - let actionID: string; - before(async () => { - actionID = await createCaseAction(supertest); - }); - after(async () => { - await deleteCaseAction(supertest, actionID); - }); - afterEach(async () => { - await deleteAllCaseItems(es); - }); - - it('deletes a comment from a sub case', async () => { - const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - await supertest - .delete( - `${CASES_URL}/${caseInfo.id}/comments/${caseInfo.comments![0].id}?subCaseId=${ - caseInfo.subCases![0].id - }` - ) - .set('kbn-xsrf', 'true') - .send() - .expect(204); + it('should delete multiple comments from the appropriate owner', async () => { + const secCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest(), + 200, + secOnlyNoSpaceAuth + ); - const { body } = await supertest.get( - `${CASES_URL}/${caseInfo.id}/comments?subCaseId=${caseInfo.subCases![0].id}` - ); - - expect(body.length).to.eql(0); + await createComment({ + supertest: supertestWithoutAuth, + caseId: secCase.id, + params: postCommentUserReq, + auth: secOnlyNoSpaceAuth, }); - it('deletes all comments from a sub case', async () => { - const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - await supertest - .post(`${CASES_URL}/${caseInfo.id}/comments?subCaseId=${caseInfo.subCases![0].id}`) - .set('kbn-xsrf', 'true') - .send(postCommentUserReq) - .expect(200); - - let { body: allComments } = await supertest.get( - `${CASES_URL}/${caseInfo.id}/comments?subCaseId=${caseInfo.subCases![0].id}` - ); - expect(allComments.length).to.eql(2); - - await supertest - .delete(`${CASES_URL}/${caseInfo.id}/comments?subCaseId=${caseInfo.subCases![0].id}`) - .set('kbn-xsrf', 'true') - .send() - .expect(204); - - ({ body: allComments } = await supertest.get( - `${CASES_URL}/${caseInfo.id}/comments?subCaseId=${caseInfo.subCases![0].id}` - )); - - // no comments for the sub case - expect(allComments.length).to.eql(0); - - ({ body: allComments } = await supertest.get(`${CASES_URL}/${caseInfo.id}/comments`)); + await createComment({ + supertest: supertestWithoutAuth, + caseId: secCase.id, + params: postCommentUserReq, + auth: secOnlyNoSpaceAuth, + }); - // no comments for the collection - expect(allComments.length).to.eql(0); + await deleteAllComments({ + supertest: supertestWithoutAuth, + caseId: secCase.id, + auth: secOnlyNoSpaceAuth, }); }); - describe('rbac', () => { - const supertestWithoutAuth = getService('supertestWithoutAuth'); + it('should not delete a comment from a different owner', async () => { + const secCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest(), + 200, + secOnlyNoSpaceAuth + ); - afterEach(async () => { - await deleteAllCaseItems(es); + const commentResp = await createComment({ + supertest: supertestWithoutAuth, + caseId: secCase.id, + params: postCommentUserReq, + auth: secOnlyNoSpaceAuth, }); - it('should delete a comment from the appropriate owner', async () => { - const secCase = await createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'securitySolutionFixture' }), - 200, - { user: secOnly, space: 'space1' } - ); - - const commentResp = await createComment({ - supertest: supertestWithoutAuth, - caseId: secCase.id, - params: postCommentUserReq, - auth: { user: secOnly, space: 'space1' }, - }); - - await deleteComment({ - supertest: supertestWithoutAuth, - caseId: secCase.id, - commentId: commentResp.comments![0].id, - auth: { user: secOnly, space: 'space1' }, - }); + await deleteComment({ + supertest: supertestWithoutAuth, + caseId: secCase.id, + commentId: commentResp.comments![0].id, + auth: obsOnlyNoSpaceAuth, + expectedHttpCode: 403, }); - 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' }, - }); + await deleteAllComments({ + supertest: supertestWithoutAuth, + caseId: secCase.id, + auth: obsOnlyNoSpaceAuth, + expectedHttpCode: 403, }); + }); - it('should not delete a comment from a different owner', async () => { - const secCase = await createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'securitySolutionFixture' }), - 200, - { user: secOnly, space: 'space1' } - ); - - const commentResp = await createComment({ - supertest: supertestWithoutAuth, - caseId: secCase.id, - params: postCommentUserReq, - auth: { user: secOnly, space: 'space1' }, - }); - - await deleteComment({ - supertest: supertestWithoutAuth, - caseId: secCase.id, - commentId: commentResp.comments![0].id, - 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]) { - it(`User ${ - user.username - } with role(s) ${user.roles.join()} - should NOT delete a comment`, async () => { - const postedCase = await createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'securitySolutionFixture' }), - 200, - superUserSpace1Auth - ); - - const commentResp = await createComment({ - supertest: supertestWithoutAuth, - caseId: postedCase.id, - params: postCommentUserReq, - auth: superUserSpace1Auth, - }); - - await deleteComment({ - supertest: supertestWithoutAuth, - caseId: postedCase.id, - commentId: commentResp.comments![0].id, - auth: { user, space: 'space1' }, - expectedHttpCode: 403, - }); - - await deleteAllComments({ - supertest: supertestWithoutAuth, - caseId: postedCase.id, - auth: { user, space: 'space1' }, - expectedHttpCode: 403, - }); - }); - } - - it('should not delete a comment with no kibana privileges', async () => { + for (const user of [globalRead, secOnlyRead, obsOnlyRead, obsSecRead]) { + it(`User ${ + user.username + } with role(s) ${user.roles.join()} - should NOT delete a comment`, async () => { const postedCase = await createCase( supertestWithoutAuth, - getPostCaseRequest({ owner: 'securitySolutionFixture' }), + getPostCaseRequest(), 200, - superUserSpace1Auth + superUserNoSpaceAuth ); const commentResp = await createComment({ supertest: supertestWithoutAuth, caseId: postedCase.id, params: postCommentUserReq, - auth: superUserSpace1Auth, + auth: superUserNoSpaceAuth, }); await deleteComment({ supertest: supertestWithoutAuth, caseId: postedCase.id, commentId: commentResp.comments![0].id, - auth: { user: noKibanaPrivileges, space: 'space1' }, + auth: { user, space: null }, expectedHttpCode: 403, }); await deleteAllComments({ supertest: supertestWithoutAuth, caseId: postedCase.id, - auth: { user: noKibanaPrivileges, space: 'space1' }, - // the find in the delete all will return no results - expectedHttpCode: 404, + auth: { user, space: null }, + 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' } - ); + it('should not delete a comment with no kibana privileges', async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest(), + 200, + superUserNoSpaceAuth + ); - const commentResp = await createComment({ - supertest: supertestWithoutAuth, - caseId: postedCase.id, - params: postCommentUserReq, - auth: { user: superUser, space: 'space2' }, - }); + const commentResp = await createComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + params: postCommentUserReq, + auth: superUserNoSpaceAuth, + }); - await deleteComment({ - supertest: supertestWithoutAuth, - caseId: postedCase.id, - commentId: commentResp.comments![0].id, - auth: { user: secOnly, space: 'space1' }, - expectedHttpCode: 404, - }); + await deleteComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + commentId: commentResp.comments![0].id, + auth: { user: noKibanaPrivileges, space: null }, + expectedHttpCode: 403, + }); - await deleteAllComments({ - supertest: supertestWithoutAuth, - caseId: postedCase.id, - auth: { user: secOnly, space: 'space1' }, - expectedHttpCode: 404, - }); + await deleteAllComments({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + auth: { user: noKibanaPrivileges, space: null }, + // the find in the delete all will return no results + expectedHttpCode: 404, + }); + }); + + it('should return a 404 when attempting to access a space', async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest(), + 200, + superUserNoSpaceAuth + ); + + const commentResp = await createComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + params: postCommentUserReq, + auth: superUserNoSpaceAuth, + }); + + await deleteComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + commentId: commentResp.comments![0].id, + auth: { user: secOnly, space: 'space1' }, + expectedHttpCode: 404, + }); + + await deleteAllComments({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + auth: { user: secOnly, space: 'space1' }, + expectedHttpCode: 404, }); }); }); diff --git a/x-pack/test/case_api_integration/security_only/tests/common/comments/find_comments.ts b/x-pack/test/case_api_integration/security_only/tests/common/comments/find_comments.ts index 2ec99d039dd000..fb5c40b049f018 100644 --- a/x-pack/test/case_api_integration/security_only/tests/common/comments/find_comments.ts +++ b/x-pack/test/case_api_integration/security_only/tests/common/comments/find_comments.ts @@ -9,30 +9,24 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; -import { CommentsResponse, CommentType } from '../../../../../../plugins/cases/common/api'; +import { CommentsResponse } from '../../../../../../plugins/cases/common/api'; import { getPostCaseRequest, - postCaseReq, postCommentAlertReq, postCommentUserReq, } from '../../../../common/lib/mock'; import { - createCaseAction, createComment, - createSubCase, deleteAllCaseItems, - deleteCaseAction, deleteCasesByESQuery, deleteCasesUserActions, deleteComments, ensureSavedObjectIsAuthorized, getSpaceUrlPrefix, createCase, - superUserSpace1Auth, } from '../../../../common/lib/utils'; import { - obsOnly, secOnly, obsOnlyRead, secOnlyRead, @@ -41,6 +35,7 @@ import { globalRead, obsSecRead, } from '../../../../common/lib/authentication/users'; +import { obsOnlyNoSpaceAuth, secOnlyNoSpaceAuth, superUserNoSpaceAuth } from '../../../utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -54,340 +49,222 @@ export default ({ getService }: FtrProviderContext): void => { await deleteCasesUserActions(es); }); - it('should find all case comment', async () => { - const { body: postedCase } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send(postCaseReq) - .expect(200); - - // post 2 comments - await supertest - .post(`${CASES_URL}/${postedCase.id}/comments`) - .set('kbn-xsrf', 'true') - .send(postCommentUserReq) - .expect(200); - - const { body: patchedCase } = await supertest - .post(`${CASES_URL}/${postedCase.id}/comments`) - .set('kbn-xsrf', 'true') - .send(postCommentUserReq) - .expect(200); - - const { body: caseComments } = await supertest - .get(`${CASES_URL}/${postedCase.id}/comments/_find`) - .set('kbn-xsrf', 'true') - .send() - .expect(200); - - expect(caseComments.comments).to.eql(patchedCase.comments); - }); - - it('should filter case comments', async () => { - const { body: postedCase } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send(postCaseReq) - .expect(200); - - // post 2 comments - await supertest - .post(`${CASES_URL}/${postedCase.id}/comments`) - .set('kbn-xsrf', 'true') - .send(postCommentUserReq) - .expect(200); - - const { body: patchedCase } = await supertest - .post(`${CASES_URL}/${postedCase.id}/comments`) - .set('kbn-xsrf', 'true') - .send({ ...postCommentUserReq, comment: 'unique' }) - .expect(200); + const supertestWithoutAuth = getService('supertestWithoutAuth'); - const { body: caseComments } = await supertest - .get(`${CASES_URL}/${postedCase.id}/comments/_find?search=unique`) - .set('kbn-xsrf', 'true') - .send() - .expect(200); - - expect(caseComments.comments).to.eql([patchedCase.comments[1]]); + afterEach(async () => { + await deleteAllCaseItems(es); }); - it('unhappy path - 400s when query is bad', async () => { - const { body: postedCase } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send(postCaseReq) - .expect(200); - - await supertest - .post(`${CASES_URL}/${postedCase.id}/comments`) - .set('kbn-xsrf', 'true') - .send(postCommentUserReq) - .expect(200); + it('should return the correct comments', async () => { + const [secCase, obsCase] = await Promise.all([ + // Create case owned by the security solution user + createCase(supertestWithoutAuth, getPostCaseRequest(), 200, secOnlyNoSpaceAuth), + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + obsOnlyNoSpaceAuth + ), + // Create case owned by the observability user + ]); - await supertest - .get(`${CASES_URL}/${postedCase.id}/comments/_find?perPage=true`) - .set('kbn-xsrf', 'true') - .send() - .expect(400); - }); + await Promise.all([ + createComment({ + supertest: supertestWithoutAuth, + caseId: secCase.id, + params: postCommentUserReq, + auth: secOnlyNoSpaceAuth, + }), + createComment({ + supertest: supertestWithoutAuth, + caseId: obsCase.id, + params: { ...postCommentAlertReq, owner: 'observabilityFixture' }, + auth: obsOnlyNoSpaceAuth, + }), + ]); - it('should return a 400 when passing the subCaseId parameter', async () => { - const { body } = await supertest - .get(`${CASES_URL}/case-id/comments/_find?search=unique&subCaseId=value`) - .set('kbn-xsrf', 'true') - .send() - .expect(400); + for (const scenario of [ + { + user: globalRead, + numExpectedEntites: 1, + owners: ['securitySolutionFixture', 'observabilityFixture'], + caseID: secCase.id, + }, + { + user: globalRead, + numExpectedEntites: 1, + owners: ['securitySolutionFixture', 'observabilityFixture'], + caseID: obsCase.id, + }, + { + user: superUser, + numExpectedEntites: 1, + owners: ['securitySolutionFixture', 'observabilityFixture'], + caseID: secCase.id, + }, + { + user: superUser, + numExpectedEntites: 1, + owners: ['securitySolutionFixture', 'observabilityFixture'], + caseID: obsCase.id, + }, + { + user: secOnlyRead, + numExpectedEntites: 1, + owners: ['securitySolutionFixture'], + caseID: secCase.id, + }, + { + user: obsOnlyRead, + numExpectedEntites: 1, + owners: ['observabilityFixture'], + caseID: obsCase.id, + }, + { + user: obsSecRead, + numExpectedEntites: 1, + owners: ['securitySolutionFixture', 'observabilityFixture'], + caseID: secCase.id, + }, + { + user: obsSecRead, + numExpectedEntites: 1, + owners: ['securitySolutionFixture', 'observabilityFixture'], + caseID: obsCase.id, + }, + ]) { + const { body: caseComments }: { body: CommentsResponse } = await supertestWithoutAuth + .get(`${getSpaceUrlPrefix(null)}${CASES_URL}/${scenario.caseID}/comments/_find`) + .auth(scenario.user.username, scenario.user.password) + .expect(200); - expect(body.message).to.contain('disabled'); + ensureSavedObjectIsAuthorized( + caseComments.comments, + scenario.numExpectedEntites, + scenario.owners + ); + } }); - // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests - describe.skip('sub case comments', () => { - let actionID: string; - before(async () => { - actionID = await createCaseAction(supertest); - }); - after(async () => { - await deleteCaseAction(supertest, actionID); - }); - afterEach(async () => { - await deleteAllCaseItems(es); + it(`User ${ + noKibanaPrivileges.username + } with role(s) ${noKibanaPrivileges.roles.join()} - should NOT read a comment`, async () => { + // super user creates a case and comment in the appropriate space + const caseInfo = await createCase(supertestWithoutAuth, getPostCaseRequest(), 200, { + user: superUser, + space: null, }); - it('finds comments for a sub case', async () => { - const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - await supertest - .post(`${CASES_URL}/${caseInfo.id}/comments?subCaseId=${caseInfo.subCases![0].id}`) - .set('kbn-xsrf', 'true') - .send(postCommentUserReq) - .expect(200); - - const { body: subCaseComments }: { body: CommentsResponse } = await supertest - .get(`${CASES_URL}/${caseInfo.id}/comments/_find?subCaseId=${caseInfo.subCases![0].id}`) - .send() - .expect(200); - expect(subCaseComments.total).to.be(2); - expect(subCaseComments.comments[0].type).to.be(CommentType.generatedAlert); - expect(subCaseComments.comments[1].type).to.be(CommentType.user); + await createComment({ + supertest: supertestWithoutAuth, + auth: { user: superUser, space: null }, + params: { ...postCommentUserReq, owner: 'securitySolutionFixture' }, + caseId: caseInfo.id, }); - }); - describe('rbac', () => { - const supertestWithoutAuth = getService('supertestWithoutAuth'); + // user should not be able to read comments + await supertestWithoutAuth + .get(`${getSpaceUrlPrefix(null)}${CASES_URL}/${caseInfo.id}/comments/_find`) + .auth(noKibanaPrivileges.username, noKibanaPrivileges.password) + .expect(403); + }); - afterEach(async () => { - await deleteAllCaseItems(es); + it('should return a 404 when attempting to access a space', async () => { + const caseInfo = await createCase(supertestWithoutAuth, getPostCaseRequest(), 200, { + user: superUser, + space: null, }); - it('should return the correct comments', async () => { - const space1 = 'space1'; - - const [secCase, obsCase] = await Promise.all([ - // Create case owned by the security solution user - createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'securitySolutionFixture' }), - 200, - { user: secOnly, space: space1 } - ), - createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'observabilityFixture' }), - 200, - { user: obsOnly, space: space1 } - ), - // Create case owned by the observability user - ]); - - await Promise.all([ - createComment({ - supertest: supertestWithoutAuth, - caseId: secCase.id, - params: postCommentUserReq, - auth: { user: secOnly, space: space1 }, - }), - createComment({ - supertest: supertestWithoutAuth, - caseId: obsCase.id, - params: { ...postCommentAlertReq, owner: 'observabilityFixture' }, - auth: { user: obsOnly, space: space1 }, - }), - ]); - - for (const scenario of [ - { - user: globalRead, - numExpectedEntites: 1, - owners: ['securitySolutionFixture', 'observabilityFixture'], - caseID: secCase.id, - }, - { - user: globalRead, - numExpectedEntites: 1, - owners: ['securitySolutionFixture', 'observabilityFixture'], - caseID: obsCase.id, - }, - { - user: superUser, - numExpectedEntites: 1, - owners: ['securitySolutionFixture', 'observabilityFixture'], - caseID: secCase.id, - }, - { - user: superUser, - numExpectedEntites: 1, - owners: ['securitySolutionFixture', 'observabilityFixture'], - caseID: obsCase.id, - }, - { - user: secOnlyRead, - numExpectedEntites: 1, - owners: ['securitySolutionFixture'], - caseID: secCase.id, - }, - { - user: obsOnlyRead, - numExpectedEntites: 1, - owners: ['observabilityFixture'], - caseID: obsCase.id, - }, - { - user: obsSecRead, - numExpectedEntites: 1, - owners: ['securitySolutionFixture', 'observabilityFixture'], - caseID: secCase.id, - }, - { - user: obsSecRead, - numExpectedEntites: 1, - owners: ['securitySolutionFixture', 'observabilityFixture'], - caseID: obsCase.id, - }, - ]) { - const { body: caseComments }: { body: CommentsResponse } = await supertestWithoutAuth - .get(`${getSpaceUrlPrefix(space1)}${CASES_URL}/${scenario.caseID}/comments/_find`) - .auth(scenario.user.username, scenario.user.password) - .expect(200); - - ensureSavedObjectIsAuthorized( - caseComments.comments, - scenario.numExpectedEntites, - scenario.owners - ); - } + await createComment({ + supertest: supertestWithoutAuth, + auth: { user: superUser, space: null }, + params: { ...postCommentUserReq, owner: 'securitySolutionFixture' }, + caseId: caseInfo.id, }); - for (const scenario of [ - { user: noKibanaPrivileges, space: 'space1' }, - { user: secOnly, space: 'space2' }, - ]) { - it(`User ${scenario.user.username} with role(s) ${scenario.user.roles.join()} and space ${ - scenario.space - } - should NOT read a comment`, async () => { - // super user creates a case and comment in the appropriate space - const caseInfo = await createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'securitySolutionFixture' }), - 200, - { user: superUser, space: scenario.space } - ); - - await createComment({ - supertest: supertestWithoutAuth, - auth: { user: superUser, space: scenario.space }, - params: { ...postCommentUserReq, owner: 'securitySolutionFixture' }, - caseId: caseInfo.id, - }); - - // user should not be able to read comments - await supertestWithoutAuth - .get(`${getSpaceUrlPrefix(scenario.space)}${CASES_URL}/${caseInfo.id}/comments/_find`) - .auth(scenario.user.username, scenario.user.password) - .expect(403); - }); - } - - it('should not return any comments when trying to exploit RBAC through the search query parameter', async () => { - const obsCase = await createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'observabilityFixture' }), - 200, - superUserSpace1Auth - ); - - await createComment({ - supertest: supertestWithoutAuth, - auth: superUserSpace1Auth, - params: { ...postCommentUserReq, owner: 'observabilityFixture' }, - caseId: obsCase.id, - }); - - const { body: res }: { body: CommentsResponse } = await supertestWithoutAuth - .get( - `${getSpaceUrlPrefix('space1')}${CASES_URL}/${ - obsCase.id - }/comments/_find?search=securitySolutionFixture+observabilityFixture` - ) - .auth(secOnly.username, secOnly.password) - .expect(200); + await supertestWithoutAuth + .get(`${getSpaceUrlPrefix('space1')}${CASES_URL}/${caseInfo.id}/comments/_find`) + .auth(secOnly.username, secOnly.password) + .expect(404); + }); - // shouldn't find any comments since they were created under the observability ownership - ensureSavedObjectIsAuthorized(res.comments, 0, ['securitySolutionFixture']); + it('should not return any comments when trying to exploit RBAC through the search query parameter', async () => { + const obsCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + superUserNoSpaceAuth + ); + + await createComment({ + supertest: supertestWithoutAuth, + auth: superUserNoSpaceAuth, + params: { ...postCommentUserReq, owner: 'observabilityFixture' }, + caseId: obsCase.id, }); - it('should not allow retrieving unauthorized comments using the filter field', async () => { - const obsCase = await createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'observabilityFixture' }), - 200, - superUserSpace1Auth - ); + const { body: res }: { body: CommentsResponse } = await supertestWithoutAuth + .get( + `${getSpaceUrlPrefix(null)}${CASES_URL}/${ + obsCase.id + }/comments/_find?search=securitySolutionFixture+observabilityFixture` + ) + .auth(secOnly.username, secOnly.password) + .expect(200); - await createComment({ - supertest: supertestWithoutAuth, - auth: superUserSpace1Auth, - params: { ...postCommentUserReq, owner: 'observabilityFixture' }, - caseId: obsCase.id, - }); + // shouldn't find any comments since they were created under the observability ownership + ensureSavedObjectIsAuthorized(res.comments, 0, ['securitySolutionFixture']); + }); - const { body: res } = await supertestWithoutAuth - .get( - `${getSpaceUrlPrefix('space1')}${CASES_URL}/${ - obsCase.id - }/comments/_find?filter=cases-comments.attributes.owner:"observabilityFixture"` - ) - .auth(secOnly.username, secOnly.password) - .expect(200); - expect(res.comments.length).to.be(0); + it('should not allow retrieving unauthorized comments using the filter field', async () => { + const obsCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + superUserNoSpaceAuth + ); + + await createComment({ + supertest: supertestWithoutAuth, + auth: superUserNoSpaceAuth, + params: { ...postCommentUserReq, owner: 'observabilityFixture' }, + caseId: obsCase.id, }); - // This test ensures that the user is not allowed to define the namespaces query param - // so she cannot search across spaces - it('should NOT allow to pass a namespaces query parameter', async () => { - const obsCase = await createCase( - supertest, - getPostCaseRequest({ owner: 'observabilityFixture' }), - 200 - ); + const { body: res } = await supertestWithoutAuth + .get( + `${getSpaceUrlPrefix(null)}${CASES_URL}/${ + obsCase.id + }/comments/_find?filter=cases-comments.attributes.owner:"observabilityFixture"` + ) + .auth(secOnly.username, secOnly.password) + .expect(200); + expect(res.comments.length).to.be(0); + }); - await createComment({ - supertest, - params: { ...postCommentUserReq, owner: 'observabilityFixture' }, - caseId: obsCase.id, - }); + // This test ensures that the user is not allowed to define the namespaces query param + // so she cannot search across spaces + it('should NOT allow to pass a namespaces query parameter', async () => { + const obsCase = await createCase( + supertest, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200 + ); + + await createComment({ + supertest, + params: { ...postCommentUserReq, owner: 'observabilityFixture' }, + caseId: obsCase.id, + }); - await supertest - .get(`${CASES_URL}/${obsCase.id}/comments/_find?namespaces[0]=*`) - .expect(400); + await supertest.get(`${CASES_URL}/${obsCase.id}/comments/_find?namespaces[0]=*`).expect(400); - await supertest.get(`${CASES_URL}/${obsCase.id}/comments/_find?namespaces=*`).expect(400); - }); + await supertest.get(`${CASES_URL}/${obsCase.id}/comments/_find?namespaces=*`).expect(400); + }); - it('should NOT allow to pass a non supported query parameter', async () => { - await supertest.get(`${CASES_URL}/id/comments/_find?notExists=papa`).expect(400); - await supertest.get(`${CASES_URL}/id/comments/_find?owner=papa`).expect(400); - }); + it('should NOT allow to pass a non supported query parameter', async () => { + await supertest.get(`${CASES_URL}/id/comments/_find?notExists=papa`).expect(400); + await supertest.get(`${CASES_URL}/id/comments/_find?owner=papa`).expect(400); }); }); }; diff --git a/x-pack/test/case_api_integration/security_only/tests/common/comments/get_all_comments.ts b/x-pack/test/case_api_integration/security_only/tests/common/comments/get_all_comments.ts index 25df715b43e9a5..105f56d8421a2f 100644 --- a/x-pack/test/case_api_integration/security_only/tests/common/comments/get_all_comments.ts +++ b/x-pack/test/case_api_integration/security_only/tests/common/comments/get_all_comments.ts @@ -8,19 +8,13 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; -import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; -import { postCaseReq, getPostCaseRequest, postCommentUserReq } from '../../../../common/lib/mock'; +import { getPostCaseRequest, postCommentUserReq } from '../../../../common/lib/mock'; import { - createCaseAction, - createSubCase, deleteAllCaseItems, - deleteCaseAction, createCase, createComment, getAllComments, - superUserSpace1Auth, } from '../../../../common/lib/utils'; -import { CommentType } from '../../../../../../plugins/cases/common/api'; import { globalRead, noKibanaPrivileges, @@ -32,10 +26,10 @@ import { secOnlyRead, superUser, } from '../../../../common/lib/authentication/users'; +import { superUserNoSpaceAuth } from '../../../utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { - const supertest = getService('supertest'); const es = getService('es'); describe('get_all_comments', () => { @@ -43,188 +37,95 @@ export default ({ getService }: FtrProviderContext): void => { await deleteAllCaseItems(es); }); - it('should get multiple comments for a single case', async () => { - const postedCase = await createCase(supertest, postCaseReq); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + it('should get all comments when the user has the correct permissions', async () => { + const caseInfo = await createCase( + supertestWithoutAuth, + getPostCaseRequest(), + 200, + superUserNoSpaceAuth + ); + await createComment({ - supertest, - caseId: postedCase.id, + supertest: supertestWithoutAuth, + caseId: caseInfo.id, params: postCommentUserReq, + auth: superUserNoSpaceAuth, }); + await createComment({ - supertest, - caseId: postedCase.id, + supertest: supertestWithoutAuth, + caseId: caseInfo.id, params: postCommentUserReq, + auth: superUserNoSpaceAuth, }); - const comments = await getAllComments({ supertest, caseId: postedCase.id }); - expect(comments.length).to.eql(2); - }); - - it('should return a 400 when passing the subCaseId parameter', async () => { - const { body } = await supertest - .get(`${CASES_URL}/case-id/comments?subCaseId=value`) - .set('kbn-xsrf', 'true') - .send() - .expect(400); - - expect(body.message).to.contain('disabled'); - }); - - it('should return a 400 when passing the includeSubCaseComments parameter', async () => { - const { body } = await supertest - .get(`${CASES_URL}/case-id/comments?includeSubCaseComments=true`) - .set('kbn-xsrf', 'true') - .send() - .expect(400); - - expect(body.message).to.contain('disabled'); - }); - - // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests - describe.skip('sub cases', () => { - let actionID: string; - before(async () => { - actionID = await createCaseAction(supertest); - }); - after(async () => { - await deleteCaseAction(supertest, actionID); - }); - - it('should get comments from a case and its sub cases', async () => { - const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - await supertest - .post(`${CASES_URL}/${caseInfo.id}/comments`) - .set('kbn-xsrf', 'true') - .send(postCommentUserReq) - .expect(200); - - const { body: comments } = await supertest - .get(`${CASES_URL}/${caseInfo.id}/comments?includeSubCaseComments=true`) - .expect(200); - - expect(comments.length).to.eql(2); - expect(comments[0].type).to.eql(CommentType.generatedAlert); - expect(comments[1].type).to.eql(CommentType.user); - }); - - it('should get comments from a sub cases', async () => { - const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - await supertest - .post(`${CASES_URL}/${caseInfo.subCases![0].id}/comments`) - .set('kbn-xsrf', 'true') - .send(postCommentUserReq) - .expect(200); - - const { body: comments } = await supertest - .get(`${CASES_URL}/${caseInfo.id}/comments?subCaseId=${caseInfo.subCases![0].id}`) - .expect(200); - - expect(comments.length).to.eql(2); - expect(comments[0].type).to.eql(CommentType.generatedAlert); - expect(comments[1].type).to.eql(CommentType.user); - }); - - it('should not find any comments for an invalid case id', async () => { - const { body } = await supertest - .get(`${CASES_URL}/fake-id/comments`) - .set('kbn-xsrf', 'true') - .send() - .expect(200); - expect(body.length).to.eql(0); - }); - }); - - describe('rbac', () => { - const supertestWithoutAuth = getService('supertestWithoutAuth'); - - it('should get all comments when the user has the correct permissions', async () => { - const caseInfo = await createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'securitySolutionFixture' }), - 200, - superUserSpace1Auth - ); - - await createComment({ + for (const user of [globalRead, superUser, secOnly, secOnlyRead, obsSec, obsSecRead]) { + const comments = await getAllComments({ supertest: supertestWithoutAuth, caseId: caseInfo.id, - params: postCommentUserReq, - auth: superUserSpace1Auth, + auth: { user, space: null }, }); - await createComment({ - supertest: supertestWithoutAuth, - caseId: caseInfo.id, - params: postCommentUserReq, - auth: superUserSpace1Auth, - }); + expect(comments.length).to.eql(2); + } + }); - for (const user of [globalRead, superUser, secOnly, secOnlyRead, obsSec, obsSecRead]) { - const comments = await getAllComments({ - supertest: supertestWithoutAuth, - caseId: caseInfo.id, - auth: { user, space: 'space1' }, - }); + it('should not get comments when the user does not have correct permission', async () => { + const caseInfo = await createCase( + supertestWithoutAuth, + getPostCaseRequest(), + 200, + superUserNoSpaceAuth + ); - expect(comments.length).to.eql(2); - } + await createComment({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + params: postCommentUserReq, + auth: superUserNoSpaceAuth, }); - it('should not get comments when the user does not have correct permission', async () => { - const caseInfo = await createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'securitySolutionFixture' }), - 200, - superUserSpace1Auth - ); - - await createComment({ + for (const scenario of [ + { user: noKibanaPrivileges, returnCode: 403 }, + { user: obsOnly, returnCode: 200 }, + { user: obsOnlyRead, returnCode: 200 }, + ]) { + const comments = await getAllComments({ supertest: supertestWithoutAuth, caseId: caseInfo.id, - params: postCommentUserReq, - auth: superUserSpace1Auth, + auth: { user: scenario.user, space: null }, + expectedHttpCode: scenario.returnCode, }); - for (const scenario of [ - { user: noKibanaPrivileges, returnCode: 403 }, - { user: obsOnly, returnCode: 200 }, - { user: obsOnlyRead, returnCode: 200 }, - ]) { - const comments = await getAllComments({ - supertest: supertestWithoutAuth, - caseId: caseInfo.id, - auth: { user: scenario.user, space: 'space1' }, - expectedHttpCode: scenario.returnCode, - }); - - // only check the length if we get a 200 in response - if (scenario.returnCode === 200) { - expect(comments.length).to.be(0); - } + // only check the length if we get a 200 in response + if (scenario.returnCode === 200) { + expect(comments.length).to.be(0); } - }); + } + }); - it('should NOT get a comment in a space with no permissions', async () => { - const caseInfo = await createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'securitySolutionFixture' }), - 200, - { user: superUser, space: 'space2' } - ); + it('should return a 404 when attempting to access a space', async () => { + const caseInfo = await createCase( + supertestWithoutAuth, + getPostCaseRequest(), + 200, + superUserNoSpaceAuth + ); - await createComment({ - supertest: supertestWithoutAuth, - caseId: caseInfo.id, - params: postCommentUserReq, - auth: { user: superUser, space: 'space2' }, - }); + await createComment({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + params: postCommentUserReq, + auth: superUserNoSpaceAuth, + }); - await getAllComments({ - supertest: supertestWithoutAuth, - caseId: caseInfo.id, - auth: { user: secOnly, space: 'space2' }, - expectedHttpCode: 403, - }); + await getAllComments({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + auth: { user: secOnly, space: 'space1' }, + expectedHttpCode: 404, }); }); }); diff --git a/x-pack/test/case_api_integration/security_only/tests/common/comments/get_comment.ts b/x-pack/test/case_api_integration/security_only/tests/common/comments/get_comment.ts index 5b606e06e84dfc..60a61136a97568 100644 --- a/x-pack/test/case_api_integration/security_only/tests/common/comments/get_comment.ts +++ b/x-pack/test/case_api_integration/security_only/tests/common/comments/get_comment.ts @@ -5,21 +5,15 @@ * 2.0. */ -import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; -import { postCaseReq, postCommentUserReq, getPostCaseRequest } from '../../../../common/lib/mock'; +import { postCommentUserReq, getPostCaseRequest } from '../../../../common/lib/mock'; import { - createCaseAction, - createSubCase, deleteAllCaseItems, - deleteCaseAction, createCase, createComment, getComment, - superUserSpace1Auth, } from '../../../../common/lib/utils'; -import { CommentType } from '../../../../../../plugins/cases/common/api'; import { globalRead, noKibanaPrivileges, @@ -31,10 +25,10 @@ import { secOnlyRead, superUser, } from '../../../../common/lib/authentication/users'; +import { superUserNoSpaceAuth } from '../../../utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { - const supertest = getService('supertest'); const es = getService('es'); describe('get_comment', () => { @@ -42,127 +36,80 @@ export default ({ getService }: FtrProviderContext): void => { await deleteAllCaseItems(es); }); - it('should get a comment', async () => { - const postedCase = await createCase(supertest, postCaseReq); - const patchedCase = await createComment({ - supertest, - caseId: postedCase.id, - params: postCommentUserReq, - }); - const comment = await getComment({ - supertest, - caseId: postedCase.id, - commentId: patchedCase.comments![0].id, - }); - - expect(comment).to.eql(patchedCase.comments![0]); - }); - - it('unhappy path - 404s when comment is not there', async () => { - await getComment({ - supertest, - caseId: 'fake-id', - commentId: 'fake-id', - expectedHttpCode: 404, - }); - }); + const supertestWithoutAuth = getService('supertestWithoutAuth'); - // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests - describe.skip('sub cases', () => { - let actionID: string; - before(async () => { - actionID = await createCaseAction(supertest); - }); - after(async () => { - await deleteCaseAction(supertest, actionID); - }); - it('should get a sub case comment', async () => { - const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - const comment = await getComment({ - supertest, - caseId: caseInfo.id, - commentId: caseInfo.comments![0].id, - }); - expect(comment.type).to.be(CommentType.generatedAlert); - }); - }); - - describe('rbac', () => { - const supertestWithoutAuth = getService('supertestWithoutAuth'); - - it('should get a comment when the user has the correct permissions', async () => { - const caseInfo = await createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'securitySolutionFixture' }), - 200, - superUserSpace1Auth - ); - - const caseWithComment = await createComment({ - supertest: supertestWithoutAuth, - caseId: caseInfo.id, - params: postCommentUserReq, - auth: superUserSpace1Auth, - }); + it('should get a comment when the user has the correct permissions', async () => { + const caseInfo = await createCase( + supertestWithoutAuth, + getPostCaseRequest(), + 200, + superUserNoSpaceAuth + ); - for (const user of [globalRead, superUser, secOnly, secOnlyRead, obsSec, obsSecRead]) { - await getComment({ - supertest: supertestWithoutAuth, - caseId: caseInfo.id, - commentId: caseWithComment.comments![0].id, - auth: { user, space: 'space1' }, - }); - } + const caseWithComment = await createComment({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + params: postCommentUserReq, + auth: superUserNoSpaceAuth, }); - it('should not get comment when the user does not have correct permissions', async () => { - const caseInfo = await createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'securitySolutionFixture' }), - 200, - superUserSpace1Auth - ); - - const caseWithComment = await createComment({ + for (const user of [globalRead, superUser, secOnly, secOnlyRead, obsSec, obsSecRead]) { + await getComment({ supertest: supertestWithoutAuth, caseId: caseInfo.id, - params: postCommentUserReq, - auth: superUserSpace1Auth, + commentId: caseWithComment.comments![0].id, + auth: { user, space: null }, }); + } + }); - for (const user of [noKibanaPrivileges, obsOnly, obsOnlyRead]) { - await getComment({ - supertest: supertestWithoutAuth, - caseId: caseInfo.id, - commentId: caseWithComment.comments![0].id, - auth: { user, space: 'space1' }, - expectedHttpCode: 403, - }); - } + it('should not get comment when the user does not have correct permissions', async () => { + const caseInfo = await createCase( + supertestWithoutAuth, + getPostCaseRequest(), + 200, + superUserNoSpaceAuth + ); + + const caseWithComment = await createComment({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + params: postCommentUserReq, + auth: superUserNoSpaceAuth, }); - it('should NOT get a case in a space with no permissions', async () => { - const caseInfo = await createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'securitySolutionFixture' }), - 200, - { user: superUser, space: 'space2' } - ); - - const caseWithComment = await createComment({ - supertest: supertestWithoutAuth, - caseId: caseInfo.id, - params: postCommentUserReq, - auth: { user: superUser, space: 'space2' }, - }); - + for (const user of [noKibanaPrivileges, obsOnly, obsOnlyRead]) { await getComment({ supertest: supertestWithoutAuth, caseId: caseInfo.id, commentId: caseWithComment.comments![0].id, - auth: { user: secOnly, space: 'space2' }, + auth: { user, space: null }, expectedHttpCode: 403, }); + } + }); + + it('should return a 404 when attempting to access a space', async () => { + const caseInfo = await createCase( + supertestWithoutAuth, + getPostCaseRequest(), + 200, + superUserNoSpaceAuth + ); + + const caseWithComment = await createComment({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + params: postCommentUserReq, + auth: superUserNoSpaceAuth, + }); + + await getComment({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + commentId: caseWithComment.comments![0].id, + auth: { user: secOnly, space: 'space1' }, + expectedHttpCode: 404, }); }); }); diff --git a/x-pack/test/case_api_integration/security_only/tests/common/comments/migrations.ts b/x-pack/test/case_api_integration/security_only/tests/common/comments/migrations.ts deleted file mode 100644 index 50a219c5e84b30..00000000000000 --- a/x-pack/test/case_api_integration/security_only/tests/common/comments/migrations.ts +++ /dev/null @@ -1,37 +0,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 expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; -import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; - -// eslint-disable-next-line import/no-default-export -export default function createGetTests({ getService }: FtrProviderContext) { - const supertest = getService('supertest'); - const esArchiver = getService('esArchiver'); - - describe('migrations', () => { - before(async () => { - await esArchiver.load('cases/migrations/7.10.0'); - }); - - after(async () => { - await esArchiver.unload('cases/migrations/7.10.0'); - }); - - it('7.11.0 migrates cases comments', async () => { - const { body: comment } = await supertest - .get( - `${CASES_URL}/e1900ac0-017f-11eb-93f8-d161651bf509/comments/da677740-1ac7-11eb-b5a3-25ee88122510` - ) - .set('kbn-xsrf', 'true') - .send(); - - expect(comment.type).to.eql('user'); - }); - }); -} diff --git a/x-pack/test/case_api_integration/security_only/tests/common/comments/patch_comment.ts b/x-pack/test/case_api_integration/security_only/tests/common/comments/patch_comment.ts index b00a0382bc7122..3e0bc17927c41d 100644 --- a/x-pack/test/case_api_integration/security_only/tests/common/comments/patch_comment.ts +++ b/x-pack/test/case_api_integration/security_only/tests/common/comments/patch_comment.ts @@ -5,47 +5,29 @@ * 2.0. */ -import { omit } from 'lodash/fp'; import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; -import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; +import { AttributesTypeUser, CommentType } from '../../../../../../plugins/cases/common/api'; +import { defaultUser, postCommentUserReq, getPostCaseRequest } from '../../../../common/lib/mock'; import { - AttributesTypeAlerts, - AttributesTypeUser, - CaseResponse, - CommentType, -} from '../../../../../../plugins/cases/common/api'; -import { - defaultUser, - postCaseReq, - postCommentUserReq, - postCommentAlertReq, - getPostCaseRequest, -} from '../../../../common/lib/mock'; -import { - createCaseAction, - createSubCase, deleteAllCaseItems, - deleteCaseAction, deleteCasesByESQuery, deleteCasesUserActions, deleteComments, createCase, createComment, updateComment, - superUserSpace1Auth, } from '../../../../common/lib/utils'; import { globalRead, noKibanaPrivileges, - obsOnly, obsOnlyRead, obsSecRead, secOnly, secOnlyRead, - superUser, } from '../../../../common/lib/authentication/users'; +import { obsOnlyNoSpaceAuth, secOnlyNoSpaceAuth, superUserNoSpaceAuth } from '../../../utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -59,111 +41,25 @@ export default ({ getService }: FtrProviderContext): void => { await deleteCasesUserActions(es); }); - it('should return a 400 when the subCaseId parameter is passed', async () => { - const { body } = await supertest - .patch(`${CASES_URL}/case-id}/comments?subCaseId=value`) - .set('kbn-xsrf', 'true') - .send({ - id: 'id', - version: 'version', - type: CommentType.alert, - alertId: 'test-id', - index: 'test-index', - rule: { - id: 'id', - name: 'name', - }, - owner: 'securitySolutionFixture', - }) - .expect(400); + const supertestWithoutAuth = getService('supertestWithoutAuth'); - expect(body.message).to.contain('disabled'); + afterEach(async () => { + await deleteAllCaseItems(es); }); - // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests - describe.skip('sub case comments', () => { - let actionID: string; - before(async () => { - actionID = await createCaseAction(supertest); - }); - after(async () => { - await deleteCaseAction(supertest, actionID); - }); - afterEach(async () => { - await deleteAllCaseItems(es); - }); - - it('patches a comment for a sub case', async () => { - const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - const { body: patchedSubCase }: { body: CaseResponse } = await supertest - .post(`${CASES_URL}/${caseInfo.id}/comments?subCaseId=${caseInfo.subCases![0].id}`) - .set('kbn-xsrf', 'true') - .send(postCommentUserReq) - .expect(200); - - const newComment = 'Well I decided to update my comment. So what? Deal with it.'; - const { body: patchedSubCaseUpdatedComment } = await supertest - .patch(`${CASES_URL}/${caseInfo.id}/comments?subCaseId=${caseInfo.subCases![0].id}`) - .set('kbn-xsrf', 'true') - .send({ - id: patchedSubCase.comments![1].id, - version: patchedSubCase.comments![1].version, - comment: newComment, - type: CommentType.user, - }) - .expect(200); - - expect(patchedSubCaseUpdatedComment.comments.length).to.be(2); - expect(patchedSubCaseUpdatedComment.comments[0].type).to.be(CommentType.generatedAlert); - expect(patchedSubCaseUpdatedComment.comments[1].type).to.be(CommentType.user); - expect(patchedSubCaseUpdatedComment.comments[1].comment).to.be(newComment); - }); - - it('fails to update the generated alert comment type', async () => { - const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - await supertest - .patch(`${CASES_URL}/${caseInfo.id}/comments?subCaseId=${caseInfo.subCases![0].id}`) - .set('kbn-xsrf', 'true') - .send({ - id: caseInfo.comments![0].id, - version: caseInfo.comments![0].version, - type: CommentType.alert, - alertId: 'test-id', - index: 'test-index', - rule: { - id: 'id', - name: 'name', - }, - }) - .expect(400); - }); - - it('fails to update the generated alert comment by using another generated alert comment', async () => { - const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - await supertest - .patch(`${CASES_URL}/${caseInfo.id}/comments?subCaseId=${caseInfo.subCases![0].id}`) - .set('kbn-xsrf', 'true') - .send({ - id: caseInfo.comments![0].id, - version: caseInfo.comments![0].version, - type: CommentType.generatedAlert, - alerts: [{ _id: 'id1' }], - index: 'test-index', - rule: { - id: 'id', - name: 'name', - }, - }) - .expect(400); - }); - }); + it('should update a comment that the user has permissions for', async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest(), + 200, + superUserNoSpaceAuth + ); - it('should patch a comment', async () => { - const postedCase = await createCase(supertest, postCaseReq); const patchedCase = await createComment({ supertest, caseId: postedCase.id, params: postCommentUserReq, + auth: superUserNoSpaceAuth, }); const newComment = 'Well I decided to update my comment. So what? Deal with it.'; @@ -171,390 +67,67 @@ export default ({ getService }: FtrProviderContext): void => { supertest, caseId: postedCase.id, req: { + ...postCommentUserReq, id: patchedCase.comments![0].id, version: patchedCase.comments![0].version, comment: newComment, - type: CommentType.user, - owner: 'securitySolutionFixture', }, + auth: secOnlyNoSpaceAuth, }); const userComment = updatedCase.comments![0] as AttributesTypeUser; expect(userComment.comment).to.eql(newComment); expect(userComment.type).to.eql(CommentType.user); expect(updatedCase.updated_by).to.eql(defaultUser); + expect(userComment.owner).to.eql('securitySolutionFixture'); }); - it('should patch an alert', async () => { - const postedCase = await createCase(supertest, postCaseReq); - const patchedCase = await createComment({ - supertest, - caseId: postedCase.id, - params: postCommentAlertReq, - }); - const updatedCase = await updateComment({ - supertest, - caseId: postedCase.id, - req: { - id: patchedCase.comments![0].id, - version: patchedCase.comments![0].version, - type: CommentType.alert, - alertId: 'new-id', - index: postCommentAlertReq.index, - rule: { - id: 'id', - name: 'name', - }, - owner: 'securitySolutionFixture', - }, - }); - - const alertComment = updatedCase.comments![0] as AttributesTypeAlerts; - expect(alertComment.alertId).to.eql('new-id'); - expect(alertComment.index).to.eql(postCommentAlertReq.index); - expect(alertComment.type).to.eql(CommentType.alert); - expect(alertComment.rule).to.eql({ - id: 'id', - name: 'name', - }); - expect(alertComment.updated_by).to.eql(defaultUser); - }); - - it('should not allow updating the owner of a comment', async () => { - const postedCase = await createCase(supertest, postCaseReq); - const patchedCase = await createComment({ - supertest, - caseId: postedCase.id, - params: postCommentUserReq, - }); - - await updateComment({ - supertest, - caseId: postedCase.id, - req: { - id: patchedCase.comments![0].id, - version: patchedCase.comments![0].version, - type: CommentType.user, - comment: postCommentUserReq.comment, - owner: 'changedOwner', - }, - expectedHttpCode: 400, - }); - }); - - it('unhappy path - 404s when comment is not there', async () => { - const postedCase = await createCase(supertest, postCaseReq); - await updateComment({ - supertest, - caseId: postedCase.id, - req: { - id: 'id', - version: 'version', - type: CommentType.user, - comment: 'comment', - owner: 'securitySolutionFixture', - }, - expectedHttpCode: 404, - }); - }); - - it('unhappy path - 404s when case is not there', async () => { - await updateComment({ - supertest, - caseId: 'fake-id', - req: { - id: 'id', - version: 'version', - type: CommentType.user, - comment: 'comment', - owner: 'securitySolutionFixture', - }, - expectedHttpCode: 404, - }); - }); + it('should not update a comment that has a different owner thant he user has access to', async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest(), + 200, + superUserNoSpaceAuth + ); - it('unhappy path - 400s when trying to change comment type', async () => { - const postedCase = await createCase(supertest, postCaseReq); const patchedCase = await createComment({ - supertest, - caseId: postedCase.id, - params: postCommentUserReq, - }); - - await updateComment({ - supertest, - caseId: postedCase.id, - req: { - id: patchedCase.comments![0].id, - version: patchedCase.comments![0].version, - type: CommentType.alert, - alertId: 'test-id', - index: 'test-index', - rule: { - id: 'id', - name: 'name', - }, - owner: 'securitySolutionFixture', - }, - expectedHttpCode: 400, - }); - }); - - it('unhappy path - 400s when missing attributes for type user', async () => { - const postedCase = await createCase(supertest, postCaseReq); - const patchedCase = await createComment({ - supertest, - caseId: postedCase.id, - params: postCommentUserReq, - }); - - await updateComment({ - supertest, - caseId: postedCase.id, - // @ts-expect-error - req: { - id: patchedCase.comments![0].id, - version: patchedCase.comments![0].version, - }, - expectedHttpCode: 400, - }); - }); - - it('unhappy path - 400s when adding excess attributes for type user', async () => { - const postedCase = await createCase(supertest, postCaseReq); - const patchedCase = await createComment({ - supertest, - caseId: postedCase.id, - params: postCommentUserReq, - }); - - for (const attribute of ['alertId', 'index']) { - await updateComment({ - supertest, - caseId: postedCase.id, - req: { - id: patchedCase.comments![0].id, - version: patchedCase.comments![0].version, - comment: 'a comment', - type: CommentType.user, - [attribute]: attribute, - owner: 'securitySolutionFixture', - }, - expectedHttpCode: 400, - }); - } - }); - - it('unhappy path - 400s when missing attributes for type alert', async () => { - const postedCase = await createCase(supertest, postCaseReq); - const patchedCase = await createComment({ - supertest, - caseId: postedCase.id, - params: postCommentAlertReq, - }); - - const allRequestAttributes = { - type: CommentType.alert, - index: 'test-index', - alertId: 'test-id', - rule: { - id: 'id', - name: 'name', - }, - }; - - for (const attribute of ['alertId', 'index']) { - const requestAttributes = omit(attribute, allRequestAttributes); - await updateComment({ - supertest, - caseId: postedCase.id, - // @ts-expect-error - req: { - id: patchedCase.comments![0].id, - version: patchedCase.comments![0].version, - ...requestAttributes, - }, - expectedHttpCode: 400, - }); - } - }); - - it('unhappy path - 400s when adding excess attributes for type alert', async () => { - const postedCase = await createCase(supertest, postCaseReq); - const patchedCase = await createComment({ - supertest, - caseId: postedCase.id, - params: postCommentAlertReq, - }); - - for (const attribute of ['comment']) { - await updateComment({ - supertest, - caseId: postedCase.id, - req: { - id: patchedCase.comments![0].id, - version: patchedCase.comments![0].version, - type: CommentType.alert, - index: 'test-index', - alertId: 'test-id', - rule: { - id: 'id', - name: 'name', - }, - owner: 'securitySolutionFixture', - [attribute]: attribute, - }, - expectedHttpCode: 400, - }); - } - }); - - it('unhappy path - 409s when conflict', async () => { - const postedCase = await createCase(supertest, postCaseReq); - const patchedCase = await createComment({ - supertest, + supertest: supertestWithoutAuth, caseId: postedCase.id, params: postCommentUserReq, + auth: superUserNoSpaceAuth, }); const newComment = 'Well I decided to update my comment. So what? Deal with it.'; await updateComment({ - supertest, + supertest: supertestWithoutAuth, caseId: postedCase.id, req: { + ...postCommentUserReq, id: patchedCase.comments![0].id, - version: 'version-mismatch', - type: CommentType.user, + version: patchedCase.comments![0].version, comment: newComment, - owner: 'securitySolutionFixture', }, - expectedHttpCode: 409, + auth: obsOnlyNoSpaceAuth, + expectedHttpCode: 403, }); }); - describe('alert format', () => { - type AlertComment = CommentType.alert | CommentType.generatedAlert; - - // ENABLE_CASE_CONNECTOR: once the case connector feature is completed create a test case for generated alerts here - for (const [alertId, index, type] of [ - ['1', ['index1', 'index2'], CommentType.alert], - [['1', '2'], 'index', CommentType.alert], - ]) { - it(`throws an error with an alert comment with contents id: ${alertId} indices: ${index} type: ${type}`, async () => { - const postedCase = await createCase(supertest, postCaseReq); - const patchedCase = await createComment({ - supertest, - caseId: postedCase.id, - params: postCommentAlertReq, - }); - - await updateComment({ - supertest, - caseId: patchedCase.id, - req: { - id: patchedCase.comments![0].id, - version: patchedCase.comments![0].version, - type: type as AlertComment, - alertId, - index, - owner: 'securitySolutionFixture', - rule: postCommentAlertReq.rule, - }, - expectedHttpCode: 400, - }); - }); - } - - for (const [alertId, index, type] of [ - ['1', ['index1'], CommentType.alert], - [['1', '2'], ['index', 'other-index'], CommentType.alert], - ]) { - it(`does not throw an error with an alert comment with contents id: ${alertId} indices: ${index} type: ${type}`, async () => { - const postedCase = await createCase(supertest, postCaseReq); - const patchedCase = await createComment({ - supertest, - caseId: postedCase.id, - params: { - ...postCommentAlertReq, - alertId, - index, - owner: 'securitySolutionFixture', - type: type as AlertComment, - }, - }); - - await updateComment({ - supertest, - caseId: postedCase.id, - req: { - id: patchedCase.comments![0].id, - version: patchedCase.comments![0].version, - type: type as AlertComment, - alertId, - index, - owner: 'securitySolutionFixture', - rule: postCommentAlertReq.rule, - }, - }); - }); - } - }); - - describe('rbac', () => { - const supertestWithoutAuth = getService('supertestWithoutAuth'); - - afterEach(async () => { - await deleteAllCaseItems(es); - }); - - it('should update a comment that the user has permissions for', async () => { + for (const user of [globalRead, secOnlyRead, obsOnlyRead, obsSecRead, noKibanaPrivileges]) { + it(`User ${ + user.username + } with role(s) ${user.roles.join()} - should NOT update a comment`, async () => { const postedCase = await createCase( supertestWithoutAuth, - getPostCaseRequest({ owner: 'securitySolutionFixture' }), + getPostCaseRequest(), 200, - superUserSpace1Auth - ); - - const patchedCase = await createComment({ - supertest, - caseId: postedCase.id, - params: postCommentUserReq, - auth: superUserSpace1Auth, - }); - - const newComment = 'Well I decided to update my comment. So what? Deal with it.'; - const updatedCase = await updateComment({ - supertest, - caseId: postedCase.id, - req: { - ...postCommentUserReq, - id: patchedCase.comments![0].id, - version: patchedCase.comments![0].version, - comment: newComment, - }, - auth: { user: secOnly, space: 'space1' }, - }); - - const userComment = updatedCase.comments![0] as AttributesTypeUser; - expect(userComment.comment).to.eql(newComment); - expect(userComment.type).to.eql(CommentType.user); - expect(updatedCase.updated_by).to.eql(defaultUser); - expect(userComment.owner).to.eql('securitySolutionFixture'); - }); - - it('should not update a comment that has a different owner thant he user has access to', async () => { - const postedCase = await createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'securitySolutionFixture' }), - 200, - superUserSpace1Auth + superUserNoSpaceAuth ); const patchedCase = await createComment({ supertest: supertestWithoutAuth, caseId: postedCase.id, params: postCommentUserReq, - auth: superUserSpace1Auth, + auth: superUserNoSpaceAuth, }); const newComment = 'Well I decided to update my comment. So what? Deal with it.'; @@ -567,74 +140,39 @@ export default ({ getService }: FtrProviderContext): void => { version: patchedCase.comments![0].version, comment: newComment, }, - auth: { user: obsOnly, space: 'space1' }, + auth: { user, space: null }, expectedHttpCode: 403, }); }); + } - for (const user of [globalRead, secOnlyRead, obsOnlyRead, obsSecRead, noKibanaPrivileges]) { - it(`User ${ - user.username - } with role(s) ${user.roles.join()} - should NOT update a comment`, async () => { - const postedCase = await createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'securitySolutionFixture' }), - 200, - superUserSpace1Auth - ); + it('should return a 404 when attempting to access a space', async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest(), + 200, + superUserNoSpaceAuth + ); - const patchedCase = await createComment({ - supertest: supertestWithoutAuth, - caseId: postedCase.id, - params: postCommentUserReq, - auth: superUserSpace1Auth, - }); - - const newComment = 'Well I decided to update my comment. So what? Deal with it.'; - await updateComment({ - supertest: supertestWithoutAuth, - caseId: postedCase.id, - req: { - ...postCommentUserReq, - id: patchedCase.comments![0].id, - version: patchedCase.comments![0].version, - comment: newComment, - }, - auth: { user, space: 'space1' }, - expectedHttpCode: 403, - }); - }); - } - - it('should not update a comment in a space the user does not have permissions', async () => { - const postedCase = await createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'securitySolutionFixture' }), - 200, - { user: superUser, space: 'space2' } - ); - - const patchedCase = await createComment({ - supertest: supertestWithoutAuth, - caseId: postedCase.id, - params: postCommentUserReq, - auth: { user: superUser, space: 'space2' }, - }); + const patchedCase = await createComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + params: postCommentUserReq, + auth: superUserNoSpaceAuth, + }); - const newComment = 'Well I decided to update my comment. So what? Deal with it.'; - await updateComment({ - supertest: supertestWithoutAuth, - caseId: postedCase.id, - req: { - ...postCommentUserReq, - id: patchedCase.comments![0].id, - version: patchedCase.comments![0].version, - comment: newComment, - }, - auth: { user: secOnly, space: 'space2' }, - // getting the case will fail in the saved object layer with a 403 - expectedHttpCode: 403, - }); + const newComment = 'Well I decided to update my comment. So what? Deal with it.'; + await updateComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + req: { + ...postCommentUserReq, + id: patchedCase.comments![0].id, + version: patchedCase.comments![0].version, + comment: newComment, + }, + auth: { user: secOnly, space: 'space1' }, + expectedHttpCode: 404, }); }); }); diff --git a/x-pack/test/case_api_integration/security_only/tests/common/comments/post_comment.ts b/x-pack/test/case_api_integration/security_only/tests/common/comments/post_comment.ts index a1f24de1b87daf..ad3d63cedd2fbe 100644 --- a/x-pack/test/case_api_integration/security_only/tests/common/comments/post_comment.ts +++ b/x-pack/test/case_api_integration/security_only/tests/common/comments/post_comment.ts @@ -5,68 +5,30 @@ * 2.0. */ -import { omit } from 'lodash/fp'; -import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; -import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; -import { DETECTION_ENGINE_QUERY_SIGNALS_URL } from '../../../../../../plugins/security_solution/common/constants'; +import { postCommentUserReq, getPostCaseRequest } from '../../../../common/lib/mock'; import { - CommentsResponse, - CommentType, - AttributesTypeUser, - AttributesTypeAlerts, -} from '../../../../../../plugins/cases/common/api'; -import { - defaultUser, - postCaseReq, - postCommentUserReq, - postCommentAlertReq, - postCollectionReq, - postCommentGenAlertReq, - getPostCaseRequest, -} from '../../../../common/lib/mock'; -import { - createCaseAction, - createSubCase, deleteAllCaseItems, - deleteCaseAction, deleteCasesByESQuery, deleteCasesUserActions, deleteComments, createCase, createComment, - getCaseUserActions, - removeServerGeneratedPropertiesFromUserAction, - removeServerGeneratedPropertiesFromSavedObject, - superUserSpace1Auth, } from '../../../../common/lib/utils'; -import { - createSignalsIndex, - deleteSignalsIndex, - deleteAllAlerts, - getRuleForSignalTesting, - waitForRuleSuccessOrStatus, - waitForSignalsToBePresent, - getSignalsByIds, - createRule, - getQuerySignalIds, -} from '../../../../../detection_engine_api_integration/utils'; + import { globalRead, noKibanaPrivileges, - obsOnly, obsOnlyRead, obsSecRead, secOnly, secOnlyRead, - superUser, } from '../../../../common/lib/authentication/users'; +import { obsOnlyNoSpaceAuth, secOnlyNoSpaceAuth, superUserNoSpaceAuth } from '../../../utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { - const supertest = getService('supertest'); - const esArchiver = getService('esArchiver'); const es = getService('es'); describe('post_comment', () => { @@ -76,529 +38,80 @@ export default ({ getService }: FtrProviderContext): void => { await deleteCasesUserActions(es); }); - describe('happy path', () => { - it('should post a comment', async () => { - const postedCase = await createCase(supertest, postCaseReq); - const patchedCase = await createComment({ - supertest, - caseId: postedCase.id, - params: postCommentUserReq, - }); - const comment = removeServerGeneratedPropertiesFromSavedObject( - patchedCase.comments![0] as AttributesTypeUser - ); - - expect(comment).to.eql({ - type: postCommentUserReq.type, - comment: postCommentUserReq.comment, - associationType: 'case', - created_by: defaultUser, - pushed_at: null, - pushed_by: null, - updated_by: null, - owner: 'securitySolutionFixture', - }); - - // updates the case correctly after adding a comment - expect(patchedCase.totalComment).to.eql(patchedCase.comments!.length); - expect(patchedCase.updated_by).to.eql(defaultUser); - }); - - it('should post an alert', async () => { - const postedCase = await createCase(supertest, postCaseReq); - const patchedCase = await createComment({ - supertest, - caseId: postedCase.id, - params: postCommentAlertReq, - }); - const comment = removeServerGeneratedPropertiesFromSavedObject( - patchedCase.comments![0] as AttributesTypeAlerts - ); - - expect(comment).to.eql({ - type: postCommentAlertReq.type, - alertId: postCommentAlertReq.alertId, - index: postCommentAlertReq.index, - rule: postCommentAlertReq.rule, - associationType: 'case', - created_by: defaultUser, - pushed_at: null, - pushed_by: null, - updated_by: null, - owner: 'securitySolutionFixture', - }); - - // updates the case correctly after adding a comment - expect(patchedCase.totalComment).to.eql(patchedCase.comments!.length); - expect(patchedCase.updated_by).to.eql(defaultUser); - }); - - it('creates a user action', async () => { - const postedCase = await createCase(supertest, postCaseReq); - const patchedCase = await createComment({ - supertest, - caseId: postedCase.id, - params: postCommentUserReq, - }); - const userActions = await getCaseUserActions({ supertest, caseID: postedCase.id }); - const commentUserAction = removeServerGeneratedPropertiesFromUserAction(userActions[1]); - - expect(commentUserAction).to.eql({ - action_field: ['comment'], - action: 'create', - action_by: defaultUser, - new_value: `{"comment":"${postCommentUserReq.comment}","type":"${postCommentUserReq.type}","owner":"securitySolutionFixture"}`, - old_value: null, - case_id: `${postedCase.id}`, - comment_id: `${patchedCase.comments![0].id}`, - sub_case_id: '', - owner: 'securitySolutionFixture', - }); - }); - }); - - describe('unhappy path', () => { - it('400s when attempting to create a comment with a different owner than the case', async () => { - const postedCase = await createCase( - supertest, - getPostCaseRequest({ owner: 'securitySolutionFixture' }) - ); - - await createComment({ - supertest, - caseId: postedCase.id, - params: { ...postCommentUserReq, owner: 'observabilityFixture' }, - expectedHttpCode: 400, - }); - }); - - it('400s when type is missing', async () => { - const postedCase = await createCase(supertest, postCaseReq); - await createComment({ - supertest, - caseId: postedCase.id, - params: { - // @ts-expect-error - bad: 'comment', - }, - expectedHttpCode: 400, - }); - }); - - it('400s when missing attributes for type user', async () => { - const postedCase = await createCase(supertest, postCaseReq); - await createComment({ - supertest, - caseId: postedCase.id, - // @ts-expect-error - params: { - type: CommentType.user, - }, - expectedHttpCode: 400, - }); - }); - - it('400s when adding excess attributes for type user', async () => { - const postedCase = await createCase(supertest, postCaseReq); - - for (const attribute of ['alertId', 'index']) { - await createComment({ - supertest, - caseId: postedCase.id, - params: { - type: CommentType.user, - [attribute]: attribute, - comment: 'a comment', - owner: 'securitySolutionFixture', - }, - expectedHttpCode: 400, - }); - } - }); - - it('400s when missing attributes for type alert', async () => { - const postedCase = await createCase(supertest, postCaseReq); - - const allRequestAttributes = { - type: CommentType.alert, - index: 'test-index', - alertId: 'test-id', - rule: { - id: 'id', - name: 'name', - }, - owner: 'securitySolutionFixture', - }; - - for (const attribute of ['alertId', 'index']) { - const requestAttributes = omit(attribute, allRequestAttributes); - await createComment({ - supertest, - caseId: postedCase.id, - // @ts-expect-error - params: requestAttributes, - expectedHttpCode: 400, - }); - } - }); - - it('400s when adding excess attributes for type alert', async () => { - const postedCase = await createCase(supertest, postCaseReq); - - for (const attribute of ['comment']) { - await createComment({ - supertest, - caseId: postedCase.id, - params: { - type: CommentType.alert, - [attribute]: attribute, - alertId: 'test-id', - index: 'test-index', - rule: { - id: 'id', - name: 'name', - }, - owner: 'securitySolutionFixture', - }, - expectedHttpCode: 400, - }); - } - }); - - it('400s when case is missing', async () => { - await createComment({ - supertest, - caseId: 'not-exists', - params: { - // @ts-expect-error - bad: 'comment', - }, - expectedHttpCode: 400, - }); - }); - - it('400s when adding an alert to a closed case', async () => { - const postedCase = await createCase(supertest, postCaseReq); - - await supertest - .patch(CASES_URL) - .set('kbn-xsrf', 'true') - .send({ - cases: [ - { - id: postedCase.id, - version: postedCase.version, - status: 'closed', - }, - ], - }) - .expect(200); - - await createComment({ - supertest, - caseId: postedCase.id, - params: postCommentAlertReq, - expectedHttpCode: 400, - }); - }); - - // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests - it.skip('400s when adding an alert to a collection case', async () => { - const postedCase = await createCase(supertest, postCollectionReq); - await createComment({ - supertest, - caseId: postedCase.id, - params: postCommentAlertReq, - expectedHttpCode: 400, - }); - }); - - it('400s when adding a generated alert to an individual case', async () => { - const postedCase = await createCase(supertest, postCaseReq); - - await supertest - .post(`${CASES_URL}/${postedCase.id}/comments`) - .set('kbn-xsrf', 'true') - .send(postCommentGenAlertReq) - .expect(400); - }); + const supertestWithoutAuth = getService('supertestWithoutAuth'); - it('should return a 400 when passing the subCaseId', async () => { - const { body } = await supertest - .post(`${CASES_URL}/case-id/comments?subCaseId=value`) - .set('kbn-xsrf', 'true') - .send(postCommentUserReq) - .expect(400); - expect(body.message).to.contain('subCaseId'); - }); + afterEach(async () => { + await deleteAllCaseItems(es); }); - describe('alerts', () => { - beforeEach(async () => { - await esArchiver.load('auditbeat/hosts'); - await createSignalsIndex(supertest); - }); + it('should create a comment when the user has the correct permissions for that owner', async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + superUserNoSpaceAuth + ); - afterEach(async () => { - await deleteSignalsIndex(supertest); - await deleteAllAlerts(supertest); - await esArchiver.unload('auditbeat/hosts'); + await createComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + params: postCommentUserReq, + auth: secOnlyNoSpaceAuth, }); - - it('should change the status of the alert if sync alert is on', async () => { - const rule = getRuleForSignalTesting(['auditbeat-*']); - const postedCase = await createCase(supertest, postCaseReq); - - await supertest - .patch(CASES_URL) - .set('kbn-xsrf', 'true') - .send({ - cases: [ - { - id: postedCase.id, - version: postedCase.version, - status: 'in-progress', - }, - ], - }) - .expect(200); - - const { id } = await createRule(supertest, rule); - await waitForRuleSuccessOrStatus(supertest, id); - await waitForSignalsToBePresent(supertest, 1, [id]); - const signals = await getSignalsByIds(supertest, [id]); - - const alert = signals.hits.hits[0]; - expect(alert._source.signal.status).eql('open'); - - await createComment({ - supertest, - caseId: postedCase.id, - params: { - alertId: alert._id, - index: alert._index, - rule: { - id: 'id', - name: 'name', - }, - owner: 'securitySolutionFixture', - type: CommentType.alert, - }, - }); - - const { body: updatedAlert } = await supertest - .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) - .set('kbn-xsrf', 'true') - .send(getQuerySignalIds([alert._id])) - .expect(200); - - expect(updatedAlert.hits.hits[0]._source.signal.status).eql('in-progress'); - }); - - it('should NOT change the status of the alert if sync alert is off', async () => { - const rule = getRuleForSignalTesting(['auditbeat-*']); - const postedCase = await createCase(supertest, { - ...postCaseReq, - settings: { syncAlerts: false }, - }); - - await supertest - .patch(CASES_URL) - .set('kbn-xsrf', 'true') - .send({ - cases: [ - { - id: postedCase.id, - version: postedCase.version, - status: 'in-progress', - }, - ], - }) - .expect(200); - - const { id } = await createRule(supertest, rule); - await waitForRuleSuccessOrStatus(supertest, id); - await waitForSignalsToBePresent(supertest, 1, [id]); - const signals = await getSignalsByIds(supertest, [id]); - - const alert = signals.hits.hits[0]; - expect(alert._source.signal.status).eql('open'); - - await createComment({ - supertest, - caseId: postedCase.id, - params: { - alertId: alert._id, - index: alert._index, - rule: { - id: 'id', - name: 'name', - }, - owner: 'securitySolutionFixture', - type: CommentType.alert, - }, - }); - - const { body: updatedAlert } = await supertest - .post(DETECTION_ENGINE_QUERY_SIGNALS_URL) - .set('kbn-xsrf', 'true') - .send(getQuerySignalIds([alert._id])) - .expect(200); - - expect(updatedAlert.hits.hits[0]._source.signal.status).eql('open'); - }); - }); - - describe('alert format', () => { - type AlertComment = CommentType.alert | CommentType.generatedAlert; - - for (const [alertId, index, type] of [ - ['1', ['index1', 'index2'], CommentType.alert], - [['1', '2'], 'index', CommentType.alert], - ['1', ['index1', 'index2'], CommentType.generatedAlert], - [['1', '2'], 'index', CommentType.generatedAlert], - ]) { - it(`throws an error with an alert comment with contents id: ${alertId} indices: ${index} type: ${type}`, async () => { - const postedCase = await createCase(supertest, postCaseReq); - await createComment({ - supertest, - caseId: postedCase.id, - params: { ...postCommentAlertReq, alertId, index, type: type as AlertComment }, - expectedHttpCode: 400, - }); - }); - } - - for (const [alertId, index, type] of [ - ['1', ['index1'], CommentType.alert], - [['1', '2'], ['index', 'other-index'], CommentType.alert], - ]) { - it(`does not throw an error with an alert comment with contents id: ${alertId} indices: ${index} type: ${type}`, async () => { - const postedCase = await createCase(supertest, postCaseReq); - await createComment({ - supertest, - caseId: postedCase.id, - params: { - ...postCommentAlertReq, - alertId, - index, - type: type as AlertComment, - }, - expectedHttpCode: 200, - }); - }); - } }); - // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests - describe.skip('sub case comments', () => { - let actionID: string; - before(async () => { - actionID = await createCaseAction(supertest); - }); - after(async () => { - await deleteCaseAction(supertest, actionID); - }); - afterEach(async () => { - await deleteAllCaseItems(es); - }); - - it('posts a new comment for a sub case', async () => { - const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - // create another sub case just to make sure we get the right comments - await createSubCase({ supertest, actionID }); - await supertest - .post(`${CASES_URL}/${caseInfo.id}/comments?subCaseId=${caseInfo.subCases![0].id}`) - .set('kbn-xsrf', 'true') - .send(postCommentUserReq) - .expect(200); - - const { body: subCaseComments }: { body: CommentsResponse } = await supertest - .get(`${CASES_URL}/${caseInfo.id}/comments/_find?subCaseId=${caseInfo.subCases![0].id}`) - .send() - .expect(200); - expect(subCaseComments.total).to.be(2); - expect(subCaseComments.comments[0].type).to.be(CommentType.generatedAlert); - expect(subCaseComments.comments[1].type).to.be(CommentType.user); + it('should not create a comment when the user does not have permissions for that owner', async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + obsOnlyNoSpaceAuth + ); + + await createComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + params: { ...postCommentUserReq, owner: 'observabilityFixture' }, + auth: secOnlyNoSpaceAuth, + expectedHttpCode: 403, }); }); - describe('rbac', () => { - const supertestWithoutAuth = getService('supertestWithoutAuth'); - - afterEach(async () => { - await deleteAllCaseItems(es); - }); - - it('should create a comment when the user has the correct permissions for that owner', async () => { + for (const user of [globalRead, secOnlyRead, obsOnlyRead, obsSecRead, noKibanaPrivileges]) { + it(`User ${ + user.username + } with role(s) ${user.roles.join()} - should not create a comment`, async () => { const postedCase = await createCase( supertestWithoutAuth, getPostCaseRequest({ owner: 'securitySolutionFixture' }), 200, - superUserSpace1Auth + superUserNoSpaceAuth ); await createComment({ supertest: supertestWithoutAuth, caseId: postedCase.id, params: postCommentUserReq, - auth: { user: secOnly, space: 'space1' }, - }); - }); - - it('should not create a comment when the user does not have permissions for that owner', async () => { - const postedCase = await createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'observabilityFixture' }), - 200, - { user: obsOnly, space: 'space1' } - ); - - await createComment({ - supertest: supertestWithoutAuth, - caseId: postedCase.id, - params: { ...postCommentUserReq, owner: 'observabilityFixture' }, - auth: { user: secOnly, space: 'space1' }, + auth: { user, space: null }, expectedHttpCode: 403, }); }); + } - for (const user of [globalRead, secOnlyRead, obsOnlyRead, obsSecRead, noKibanaPrivileges]) { - it(`User ${ - user.username - } with role(s) ${user.roles.join()} - should not create a comment`, async () => { - const postedCase = await createCase( - supertestWithoutAuth, - getPostCaseRequest({ owner: 'securitySolutionFixture' }), - 200, - superUserSpace1Auth - ); - - await createComment({ - supertest: supertestWithoutAuth, - caseId: postedCase.id, - params: postCommentUserReq, - auth: { user, space: 'space1' }, - expectedHttpCode: 403, - }); - }); - } + it('should return a 404 when attempting to access a space', async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + superUserNoSpaceAuth + ); - it('should not create a comment in a space the user does not have permissions for', 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: secOnly, space: 'space2' }, - expectedHttpCode: 403, - }); + await createComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + params: postCommentUserReq, + auth: { user: secOnly, space: 'space1' }, + expectedHttpCode: 404, }); }); }); diff --git a/x-pack/test/case_api_integration/security_only/tests/common/configure/get_configure.ts b/x-pack/test/case_api_integration/security_only/tests/common/configure/get_configure.ts index 279936ebbef462..9b45a393d7ca07 100644 --- a/x-pack/test/case_api_integration/security_only/tests/common/configure/get_configure.ts +++ b/x-pack/test/case_api_integration/security_only/tests/common/configure/get_configure.ts @@ -5,12 +5,9 @@ * 2.0. */ -import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { - removeServerGeneratedPropertiesFromSavedObject, - getConfigurationOutput, deleteConfiguration, getConfiguration, createConfiguration, @@ -18,7 +15,6 @@ import { ensureSavedObjectIsAuthorized, } from '../../../../common/lib/utils'; import { - obsOnly, secOnly, obsOnlyRead, secOnlyRead, @@ -26,12 +22,16 @@ import { superUser, globalRead, obsSecRead, - obsSec, } from '../../../../common/lib/authentication/users'; +import { + obsOnlyNoSpaceAuth, + obsSecNoSpaceAuth, + secOnlyNoSpaceAuth, + superUserNoSpaceAuth, +} from '../../../utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { - const supertest = getService('supertest'); const supertestWithoutAuth = getService('supertestWithoutAuth'); const es = getService('es'); @@ -40,176 +40,148 @@ export default ({ getService }: FtrProviderContext): void => { await deleteConfiguration(es); }); - it('should return an empty find body correctly if no configuration is loaded', async () => { - const configuration = await getConfiguration({ supertest }); - expect(configuration).to.eql([]); - }); - - it('should return a configuration', async () => { - await createConfiguration(supertest); - const configuration = await getConfiguration({ supertest }); + it('should return the correct configuration', async () => { + await createConfiguration( + supertestWithoutAuth, + getConfigurationRequest(), + 200, + secOnlyNoSpaceAuth + ); + + await createConfiguration( + supertestWithoutAuth, + { ...getConfigurationRequest(), owner: 'observabilityFixture' }, + 200, + obsOnlyNoSpaceAuth + ); - const data = removeServerGeneratedPropertiesFromSavedObject(configuration[0]); - expect(data).to.eql(getConfigurationOutput()); - }); - - it('should get a single configuration', async () => { - await createConfiguration(supertest, getConfigurationRequest({ id: 'connector-2' })); - await createConfiguration(supertest); - const res = await getConfiguration({ supertest }); + for (const scenario of [ + { + user: globalRead, + numberOfExpectedCases: 2, + owners: ['securitySolutionFixture', 'observabilityFixture'], + }, + { + user: superUser, + numberOfExpectedCases: 2, + owners: ['securitySolutionFixture', 'observabilityFixture'], + }, + { user: secOnlyRead, numberOfExpectedCases: 1, owners: ['securitySolutionFixture'] }, + { user: obsOnlyRead, numberOfExpectedCases: 1, owners: ['observabilityFixture'] }, + { + user: obsSecRead, + numberOfExpectedCases: 2, + owners: ['securitySolutionFixture', 'observabilityFixture'], + }, + ]) { + const configuration = await getConfiguration({ + supertest: supertestWithoutAuth, + query: { owner: scenario.owners }, + expectedHttpCode: 200, + auth: { + user: scenario.user, + space: null, + }, + }); - expect(res.length).to.eql(1); - const data = removeServerGeneratedPropertiesFromSavedObject(res[0]); - expect(data).to.eql(getConfigurationOutput()); + ensureSavedObjectIsAuthorized( + configuration, + scenario.numberOfExpectedCases, + scenario.owners + ); + } }); - it('should return by descending order', async () => { - await createConfiguration(supertest, getConfigurationRequest({ id: 'connector-2' })); - await createConfiguration(supertest); - const res = await getConfiguration({ supertest }); - - const data = removeServerGeneratedPropertiesFromSavedObject(res[0]); - expect(data).to.eql(getConfigurationOutput()); + it(`User ${ + noKibanaPrivileges.username + } with role(s) ${noKibanaPrivileges.roles.join()} - should NOT read a case configuration`, async () => { + // super user creates a configuration at the appropriate space + await createConfiguration( + supertestWithoutAuth, + getConfigurationRequest(), + 200, + superUserNoSpaceAuth + ); + + // user should not be able to read configurations at the appropriate space + await getConfiguration({ + supertest: supertestWithoutAuth, + expectedHttpCode: 403, + auth: { + user: noKibanaPrivileges, + space: null, + }, + }); }); - describe('rbac', () => { - it('should return the correct configuration', async () => { - await createConfiguration(supertestWithoutAuth, getConfigurationRequest(), 200, { + it('should return a 404 when attempting to access a space', async () => { + await createConfiguration( + supertestWithoutAuth, + getConfigurationRequest(), + 200, + superUserNoSpaceAuth + ); + + await getConfiguration({ + supertest: supertestWithoutAuth, + expectedHttpCode: 404, + auth: { user: secOnly, space: 'space1', - }); + }, + }); + }); - await createConfiguration( + it('should respect the owner filter when having permissions', async () => { + await Promise.all([ + createConfiguration( + supertestWithoutAuth, + getConfigurationRequest(), + 200, + obsSecNoSpaceAuth + ), + createConfiguration( supertestWithoutAuth, { ...getConfigurationRequest(), owner: 'observabilityFixture' }, 200, - { - user: obsOnly, - space: 'space1', - } - ); - - for (const scenario of [ - { - user: globalRead, - numberOfExpectedCases: 2, - owners: ['securitySolutionFixture', 'observabilityFixture'], - }, - { - user: superUser, - numberOfExpectedCases: 2, - owners: ['securitySolutionFixture', 'observabilityFixture'], - }, - { user: secOnlyRead, numberOfExpectedCases: 1, owners: ['securitySolutionFixture'] }, - { user: obsOnlyRead, numberOfExpectedCases: 1, owners: ['observabilityFixture'] }, - { - user: obsSecRead, - numberOfExpectedCases: 2, - owners: ['securitySolutionFixture', 'observabilityFixture'], - }, - ]) { - const configuration = await getConfiguration({ - supertest: supertestWithoutAuth, - query: { owner: scenario.owners }, - expectedHttpCode: 200, - auth: { - user: scenario.user, - space: 'space1', - }, - }); - - ensureSavedObjectIsAuthorized( - configuration, - scenario.numberOfExpectedCases, - scenario.owners - ); - } + obsSecNoSpaceAuth + ), + ]); + + const res = await getConfiguration({ + supertest: supertestWithoutAuth, + query: { owner: 'securitySolutionFixture' }, + auth: obsSecNoSpaceAuth, }); - for (const scenario of [ - { user: noKibanaPrivileges, space: 'space1' }, - { user: secOnly, space: 'space2' }, - ]) { - it(`User ${scenario.user.username} with role(s) ${scenario.user.roles.join()} and space ${ - scenario.space - } - should NOT read a case configuration`, async () => { - // super user creates a configuration at the appropriate space - await createConfiguration(supertestWithoutAuth, getConfigurationRequest(), 200, { - user: superUser, - space: scenario.space, - }); - - // user should not be able to read configurations at the appropriate space - await getConfiguration({ - supertest: supertestWithoutAuth, - expectedHttpCode: 403, - auth: { - user: scenario.user, - space: scenario.space, - }, - }); - }); - } - - it('should respect the owner filter when having permissions', async () => { - await Promise.all([ - createConfiguration(supertestWithoutAuth, getConfigurationRequest(), 200, { - user: obsSec, - space: 'space1', - }), - createConfiguration( - supertestWithoutAuth, - { ...getConfigurationRequest(), owner: 'observabilityFixture' }, - 200, - { - user: obsSec, - space: 'space1', - } - ), - ]); - - const res = await getConfiguration({ - supertest: supertestWithoutAuth, - query: { owner: 'securitySolutionFixture' }, - auth: { - user: obsSec, - space: 'space1', - }, - }); + ensureSavedObjectIsAuthorized(res, 1, ['securitySolutionFixture']); + }); - ensureSavedObjectIsAuthorized(res, 1, ['securitySolutionFixture']); + it('should return the correct cases when trying to exploit RBAC through the owner query parameter', async () => { + await Promise.all([ + createConfiguration( + supertestWithoutAuth, + getConfigurationRequest(), + 200, + obsSecNoSpaceAuth + ), + createConfiguration( + supertestWithoutAuth, + { ...getConfigurationRequest(), owner: 'observabilityFixture' }, + 200, + obsSecNoSpaceAuth + ), + ]); + + // User with permissions only to security solution request cases from observability + const res = await getConfiguration({ + supertest: supertestWithoutAuth, + query: { owner: ['securitySolutionFixture', 'observabilityFixture'] }, + auth: secOnlyNoSpaceAuth, }); - it('should return the correct cases when trying to exploit RBAC through the owner query parameter', async () => { - await Promise.all([ - createConfiguration(supertestWithoutAuth, getConfigurationRequest(), 200, { - user: obsSec, - space: 'space1', - }), - createConfiguration( - supertestWithoutAuth, - { ...getConfigurationRequest(), owner: 'observabilityFixture' }, - 200, - { - user: obsSec, - space: 'space1', - } - ), - ]); - - // User with permissions only to security solution request cases from observability - const res = await getConfiguration({ - supertest: supertestWithoutAuth, - query: { owner: ['securitySolutionFixture', 'observabilityFixture'] }, - auth: { - user: secOnly, - space: 'space1', - }, - }); - - // Only security solution cases are being returned - ensureSavedObjectIsAuthorized(res, 1, ['securitySolutionFixture']); - }); + // Only security solution cases are being returned + ensureSavedObjectIsAuthorized(res, 1, ['securitySolutionFixture']); }); }); }; diff --git a/x-pack/test/case_api_integration/security_only/tests/common/configure/get_connectors.ts b/x-pack/test/case_api_integration/security_only/tests/common/configure/get_connectors.ts deleted file mode 100644 index 46f712ff84aa32..00000000000000 --- a/x-pack/test/case_api_integration/security_only/tests/common/configure/get_connectors.ts +++ /dev/null @@ -1,27 +0,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 expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; - -import { getCaseConnectors } from '../../../../common/lib/utils'; - -// eslint-disable-next-line import/no-default-export -export default ({ getService }: FtrProviderContext): void => { - const supertest = getService('supertest'); - - describe('get_connectors', () => { - it('should return an empty find body correctly if no connectors are loaded', async () => { - const connectors = await getCaseConnectors({ supertest }); - expect(connectors).to.eql([]); - }); - - it.skip('filters out connectors that are not enabled in license', async () => { - // TODO: Should find a way to downgrade license to gold and upgrade back to trial - }); - }); -}; diff --git a/x-pack/test/case_api_integration/security_only/tests/common/configure/patch_configure.ts b/x-pack/test/case_api_integration/security_only/tests/common/configure/patch_configure.ts index ced727f8e4e75e..68768277f1963a 100644 --- a/x-pack/test/case_api_integration/security_only/tests/common/configure/patch_configure.ts +++ b/x-pack/test/case_api_integration/security_only/tests/common/configure/patch_configure.ts @@ -11,8 +11,6 @@ import { ObjectRemover as ActionsRemover } from '../../../../../alerting_api_int import { getConfigurationRequest, - removeServerGeneratedPropertiesFromSavedObject, - getConfigurationOutput, deleteConfiguration, createConfiguration, updateConfiguration, @@ -24,8 +22,8 @@ import { noKibanaPrivileges, globalRead, obsSecRead, - superUser, } from '../../../../common/lib/authentication/users'; +import { secOnlyNoSpaceAuth, superUserNoSpaceAuth } from '../../../utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -41,131 +39,57 @@ export default ({ getService }: FtrProviderContext): void => { await actionsRemover.removeAll(); }); - it('should patch a configuration', async () => { - const configuration = await createConfiguration(supertest); - const newConfiguration = await updateConfiguration(supertest, configuration.id, { - closure_type: 'close-by-pushing', - version: configuration.version, - }); - - const data = removeServerGeneratedPropertiesFromSavedObject(newConfiguration); - expect(data).to.eql({ ...getConfigurationOutput(true), closure_type: 'close-by-pushing' }); - }); - - it('should not patch a configuration with unsupported connector type', async () => { - const configuration = await createConfiguration(supertest); - await updateConfiguration( - supertest, - configuration.id, - // @ts-expect-error - getConfigurationRequest({ type: '.unsupported' }), - 400 + it('User: security solution only - should update a configuration', async () => { + const configuration = await createConfiguration( + supertestWithoutAuth, + getConfigurationRequest(), + 200, + secOnlyNoSpaceAuth ); - }); - - it('should not patch a configuration with unsupported connector fields', async () => { - const configuration = await createConfiguration(supertest); - await updateConfiguration( - supertest, - configuration.id, - // @ts-expect-error - getConfigurationRequest({ type: '.jira', fields: { unsupported: 'value' } }), - 400 - ); - }); - - it('should handle patch request when there is no configuration', async () => { - const error = await updateConfiguration( - supertest, - 'not-exist', - { closure_type: 'close-by-pushing', version: 'no-version' }, - 404 - ); - - expect(error).to.eql({ - error: 'Not Found', - message: 'Saved object [cases-configure/not-exist] not found', - statusCode: 404, - }); - }); - it('should handle patch request when versions are different', async () => { - const configuration = await createConfiguration(supertest); - const error = await updateConfiguration( - supertest, + const newConfiguration = await updateConfiguration( + supertestWithoutAuth, configuration.id, - { closure_type: 'close-by-pushing', version: 'no-version' }, - 409 + { + closure_type: 'close-by-pushing', + version: configuration.version, + }, + 200, + secOnlyNoSpaceAuth ); - expect(error).to.eql({ - error: 'Conflict', - message: - 'This configuration has been updated. Please refresh before saving additional updates.', - statusCode: 409, - }); + expect(newConfiguration.owner).to.eql('securitySolutionFixture'); }); - it('should not allow to change the owner of the configuration', async () => { - const configuration = await createConfiguration(supertest); - await updateConfiguration( - supertest, - configuration.id, - // @ts-expect-error - { owner: 'observabilityFixture', version: configuration.version }, - 400 + it('User: security solution only - should NOT update a configuration of different owner', async () => { + const configuration = await createConfiguration( + supertestWithoutAuth, + { ...getConfigurationRequest(), owner: 'observabilityFixture' }, + 200, + superUserNoSpaceAuth ); - }); - it('should not allow excess attributes', async () => { - const configuration = await createConfiguration(supertest); await updateConfiguration( - supertest, + supertestWithoutAuth, configuration.id, - // @ts-expect-error - { notExist: 'not-exist', version: configuration.version }, - 400 + { + closure_type: 'close-by-pushing', + version: configuration.version, + }, + 403, + secOnlyNoSpaceAuth ); }); - describe('rbac', () => { - it('User: security solution only - should update a configuration', async () => { + for (const user of [globalRead, secOnlyRead, obsOnlyRead, obsSecRead, noKibanaPrivileges]) { + it(`User ${ + user.username + } with role(s) ${user.roles.join()} - should NOT update a configuration`, async () => { const configuration = await createConfiguration( supertestWithoutAuth, getConfigurationRequest(), 200, - { - user: secOnly, - space: 'space1', - } - ); - - const newConfiguration = await updateConfiguration( - supertestWithoutAuth, - configuration.id, - { - closure_type: 'close-by-pushing', - version: configuration.version, - }, - 200, - { - user: secOnly, - space: 'space1', - } - ); - - expect(newConfiguration.owner).to.eql('securitySolutionFixture'); - }); - - it('User: security solution only - should NOT update a configuration of different owner', async () => { - const configuration = await createConfiguration( - supertestWithoutAuth, - { ...getConfigurationRequest(), owner: 'observabilityFixture' }, - 200, - { - user: superUser, - space: 'space1', - } + superUserNoSpaceAuth ); await updateConfiguration( @@ -177,67 +101,34 @@ export default ({ getService }: FtrProviderContext): void => { }, 403, { - user: secOnly, - space: 'space1', + user, + space: null, } ); }); + } + + it('should return a 404 when attempting to access a space', async () => { + const configuration = await createConfiguration( + supertestWithoutAuth, + { ...getConfigurationRequest(), owner: 'securitySolutionFixture' }, + 200, + superUserNoSpaceAuth + ); - for (const user of [globalRead, secOnlyRead, obsOnlyRead, obsSecRead, noKibanaPrivileges]) { - it(`User ${ - user.username - } with role(s) ${user.roles.join()} - should NOT update a configuration`, async () => { - const configuration = await createConfiguration( - supertestWithoutAuth, - getConfigurationRequest(), - 200, - { - user: superUser, - space: 'space1', - } - ); - - await updateConfiguration( - supertestWithoutAuth, - configuration.id, - { - closure_type: 'close-by-pushing', - version: configuration.version, - }, - 403, - { - user, - space: 'space1', - } - ); - }); - } - - it('should NOT update a configuration in a space with no permissions', async () => { - const configuration = await createConfiguration( - supertestWithoutAuth, - { ...getConfigurationRequest(), owner: 'securitySolutionFixture' }, - 200, - { - user: superUser, - space: 'space2', - } - ); - - await updateConfiguration( - supertestWithoutAuth, - configuration.id, - { - closure_type: 'close-by-pushing', - version: configuration.version, - }, - 404, - { - user: secOnly, - space: 'space1', - } - ); - }); + await updateConfiguration( + supertestWithoutAuth, + configuration.id, + { + closure_type: 'close-by-pushing', + version: configuration.version, + }, + 404, + { + user: secOnly, + space: 'space1', + } + ); }); }); }; diff --git a/x-pack/test/case_api_integration/security_only/tests/common/configure/post_configure.ts b/x-pack/test/case_api_integration/security_only/tests/common/configure/post_configure.ts index f1dae9f319109b..03462e72587195 100644 --- a/x-pack/test/case_api_integration/security_only/tests/common/configure/post_configure.ts +++ b/x-pack/test/case_api_integration/security_only/tests/common/configure/post_configure.ts @@ -6,14 +6,11 @@ */ import expect from '@kbn/expect'; -import { ConnectorTypes } from '../../../../../../plugins/cases/common/api'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { ObjectRemover as ActionsRemover } from '../../../../../alerting_api_integration/common/lib'; import { getConfigurationRequest, - removeServerGeneratedPropertiesFromSavedObject, - getConfigurationOutput, deleteConfiguration, createConfiguration, getConfiguration, @@ -27,8 +24,8 @@ import { noKibanaPrivileges, globalRead, obsSecRead, - superUser, } from '../../../../common/lib/authentication/users'; +import { secOnlyNoSpaceAuth, superUserNoSpaceAuth } from '../../../utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -44,255 +41,87 @@ export default ({ getService }: FtrProviderContext): void => { await actionsRemover.removeAll(); }); - it('should create a configuration', async () => { - const configuration = await createConfiguration(supertest); - - const data = removeServerGeneratedPropertiesFromSavedObject(configuration); - expect(data).to.eql(getConfigurationOutput()); - }); - - it('should keep only the latest configuration', async () => { - await createConfiguration(supertest, getConfigurationRequest({ id: 'connector-2' })); - await createConfiguration(supertest); - const configuration = await getConfiguration({ supertest }); - - expect(configuration.length).to.be(1); - }); - - it('should return an error when failing to get mapping', async () => { - const postRes = await createConfiguration( - supertest, - getConfigurationRequest({ - id: 'not-exists', - name: 'not-exists', - type: ConnectorTypes.jira, - }) - ); - - expect(postRes.error).to.not.be(null); - expect(postRes.mappings).to.eql([]); - }); - - it('should not create a configuration when missing connector.id', async () => { - await createConfiguration( - supertest, - { - // @ts-expect-error - connector: { - name: 'Connector', - type: ConnectorTypes.none, - fields: null, - }, - closure_type: 'close-by-user', - }, - 400 + it('User: security solution only - should create a configuration', async () => { + const configuration = await createConfiguration( + supertestWithoutAuth, + getConfigurationRequest(), + 200, + secOnlyNoSpaceAuth ); - }); - it('should not create a configuration when missing connector.name', async () => { - await createConfiguration( - supertest, - { - // @ts-expect-error - connector: { - id: 'test-id', - type: ConnectorTypes.none, - fields: null, - }, - closure_type: 'close-by-user', - }, - 400 - ); + expect(configuration.owner).to.eql('securitySolutionFixture'); }); - it('should not create a configuration when missing connector.type', async () => { + it('User: security solution only - should NOT create a configuration of different owner', async () => { await createConfiguration( - supertest, - { - // @ts-expect-error - connector: { - id: 'test-id', - name: 'Connector', - fields: null, - }, - closure_type: 'close-by-user', - }, - 400 + supertestWithoutAuth, + { ...getConfigurationRequest(), owner: 'observabilityFixture' }, + 403, + secOnlyNoSpaceAuth ); }); - it('should not create a configuration when missing connector.fields', async () => { - await createConfiguration( - supertest, - { - // @ts-expect-error - connector: { - id: 'test-id', - type: ConnectorTypes.none, - name: 'Connector', - }, - closure_type: 'close-by-user', - }, - 400 - ); - }); - - it('should not create a configuration when when missing closure_type', async () => { - await createConfiguration( - supertest, - // @ts-expect-error - { - connector: { - id: 'test-id', - type: ConnectorTypes.none, - name: 'Connector', - fields: null, - }, - }, - 400 - ); - }); + for (const user of [globalRead, secOnlyRead, obsOnlyRead, obsSecRead, noKibanaPrivileges]) { + it(`User ${ + user.username + } with role(s) ${user.roles.join()} - should NOT create a configuration`, async () => { + await createConfiguration( + supertestWithoutAuth, + { ...getConfigurationRequest(), owner: 'securitySolutionFixture' }, + 403, + { + user, + space: null, + } + ); + }); + } - it('should not create a configuration when missing connector', async () => { + it('should return a 404 when attempting to access a space', async () => { await createConfiguration( - supertest, - // @ts-expect-error + supertestWithoutAuth, + { ...getConfigurationRequest(), owner: 'securitySolutionFixture' }, + 404, { - closure_type: 'close-by-user', - }, - 400 + user: secOnly, + space: 'space1', + } ); }); - it('should not create a configuration when fields are not null', async () => { + it('it deletes the correct configurations', async () => { await createConfiguration( - supertest, - { - connector: { - id: 'test-id', - type: ConnectorTypes.none, - name: 'Connector', - // @ts-expect-error - fields: {}, - }, - closure_type: 'close-by-user', - }, - 400 + supertestWithoutAuth, + { ...getConfigurationRequest(), owner: 'securitySolutionFixture' }, + 200, + superUserNoSpaceAuth ); - }); - - it('should not create a configuration with unsupported connector type', async () => { - // @ts-expect-error - await createConfiguration(supertest, getConfigurationRequest({ type: '.unsupported' }), 400); - }); - it('should not create a configuration with unsupported connector fields', async () => { + /** + * This API call should not delete the previously created configuration + * as it belongs to a different owner + */ await createConfiguration( - supertest, - // @ts-expect-error - getConfigurationRequest({ type: '.jira', fields: { unsupported: 'value' } }), - 400 + supertestWithoutAuth, + { ...getConfigurationRequest(), owner: 'observabilityFixture' }, + 200, + superUserNoSpaceAuth ); - }); - describe('rbac', () => { - it('User: security solution only - should create a configuration', async () => { - const configuration = await createConfiguration( - supertestWithoutAuth, - getConfigurationRequest(), - 200, - { - user: secOnly, - space: 'space1', - } - ); - - expect(configuration.owner).to.eql('securitySolutionFixture'); - }); - - it('User: security solution only - should NOT create a configuration of different owner', async () => { - await createConfiguration( - supertestWithoutAuth, - { ...getConfigurationRequest(), owner: 'observabilityFixture' }, - 403, - { - user: secOnly, - space: 'space1', - } - ); - }); - - for (const user of [globalRead, secOnlyRead, obsOnlyRead, obsSecRead, noKibanaPrivileges]) { - it(`User ${ - user.username - } with role(s) ${user.roles.join()} - should NOT create a configuration`, async () => { - await createConfiguration( - supertestWithoutAuth, - { ...getConfigurationRequest(), owner: 'securitySolutionFixture' }, - 403, - { - user, - space: 'space1', - } - ); - }); - } - - it('should NOT create a configuration in a space with no permissions', async () => { - await createConfiguration( - supertestWithoutAuth, - { ...getConfigurationRequest(), owner: 'securitySolutionFixture' }, - 403, - { - user: secOnly, - space: 'space2', - } - ); + const configuration = await getConfiguration({ + supertest: supertestWithoutAuth, + query: { owner: ['securitySolutionFixture', 'observabilityFixture'] }, + auth: superUserNoSpaceAuth, }); - it('it deletes the correct configurations', async () => { - await createConfiguration( - supertestWithoutAuth, - { ...getConfigurationRequest(), owner: 'securitySolutionFixture' }, - 200, - { - user: superUser, - space: 'space1', - } - ); - - /** - * This API call should not delete the previously created configuration - * as it belongs to a different owner - */ - await createConfiguration( - supertestWithoutAuth, - { ...getConfigurationRequest(), owner: 'observabilityFixture' }, - 200, - { - user: superUser, - space: 'space1', - } - ); - - const configuration = await getConfiguration({ - supertest: supertestWithoutAuth, - query: { owner: ['securitySolutionFixture', 'observabilityFixture'] }, - auth: { - user: superUser, - space: 'space1', - }, - }); - - /** - * This ensures that both configuration are returned as expected - * and neither of has been deleted - */ - ensureSavedObjectIsAuthorized(configuration, 2, [ - 'securitySolutionFixture', - 'observabilityFixture', - ]); - }); + /** + * This ensures that both configuration are returned as expected + * and neither of has been deleted + */ + ensureSavedObjectIsAuthorized(configuration, 2, [ + 'securitySolutionFixture', + 'observabilityFixture', + ]); }); }); }; diff --git a/x-pack/test/case_api_integration/security_only/tests/common/connectors/case.ts b/x-pack/test/case_api_integration/security_only/tests/common/connectors/case.ts deleted file mode 100644 index fd9ec8142b49fe..00000000000000 --- a/x-pack/test/case_api_integration/security_only/tests/common/connectors/case.ts +++ /dev/null @@ -1,1078 +0,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 { omit } from 'lodash/fp'; -import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; - -import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; -import { CommentType } from '../../../../../../plugins/cases/common/api'; -import { postCaseReq, postCaseResp } from '../../../../common/lib/mock'; -import { - removeServerGeneratedPropertiesFromCase, - removeServerGeneratedPropertiesFromComments, -} from '../../../../common/lib/utils'; -import { - createRule, - createSignalsIndex, - deleteAllAlerts, - deleteSignalsIndex, - getRuleForSignalTesting, - getSignalsByIds, - waitForRuleSuccessOrStatus, - waitForSignalsToBePresent, -} from '../../../../../detection_engine_api_integration/utils'; - -// eslint-disable-next-line import/no-default-export -export default ({ getService }: FtrProviderContext): void => { - const supertest = getService('supertest'); - const esArchiver = getService('esArchiver'); - - describe('case_connector', () => { - let createdActionId = ''; - - it('should return 400 when creating a case action', async () => { - await supertest - .post('/api/actions/connector') - .set('kbn-xsrf', 'foo') - .send({ - name: 'A case connector', - actionTypeId: '.case', - config: {}, - }) - .expect(400); - }); - - // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests - it.skip('should return 200 when creating a case action successfully', async () => { - const { body: createdAction } = await supertest - .post('/api/actions/connector') - .set('kbn-xsrf', 'foo') - .send({ - name: 'A case connector', - actionTypeId: '.case', - config: {}, - }) - .expect(200); - - createdActionId = createdAction.id; - - expect(createdAction).to.eql({ - id: createdActionId, - isPreconfigured: false, - name: 'A case connector', - actionTypeId: '.case', - config: {}, - }); - - const { body: fetchedAction } = await supertest - .get(`/api/actions/connector/${createdActionId}`) - .expect(200); - - expect(fetchedAction).to.eql({ - id: fetchedAction.id, - isPreconfigured: false, - name: 'A case connector', - actionTypeId: '.case', - config: {}, - }); - }); - - describe.skip('create', () => { - it('should respond with a 400 Bad Request when creating a case without title', async () => { - const { body: createdAction } = await supertest - .post('/api/actions/connector') - .set('kbn-xsrf', 'foo') - .send({ - name: 'A case connector', - actionTypeId: '.case', - config: {}, - }) - .expect(200); - - createdActionId = createdAction.id; - const params = { - subAction: 'create', - subActionParams: { - tags: ['case', 'connector'], - description: 'case description', - connector: { - id: 'jira', - name: 'Jira', - type: '.jira', - fields: { - issueType: '10006', - priority: 'High', - parent: null, - }, - }, - }, - }; - - const caseConnector = await supertest - .post(`/api/actions/connector/${createdActionId}/_execute`) - .set('kbn-xsrf', 'foo') - .send({ params }) - .expect(200); - - expect(caseConnector.body).to.eql({ - status: 'error', - actionId: createdActionId, - message: - 'error validating action params: types that failed validation:\n- [0.subActionParams.title]: expected value of type [string] but got [undefined]\n- [1.subAction]: expected value to equal [update]\n- [2.subAction]: expected value to equal [addComment]', - retry: false, - }); - }); - - it('should respond with a 400 Bad Request when creating a case without description', async () => { - const { body: createdAction } = await supertest - .post('/api/actions/connector') - .set('kbn-xsrf', 'foo') - .send({ - name: 'A case connector', - actionTypeId: '.case', - config: {}, - }) - .expect(200); - - createdActionId = createdAction.id; - const params = { - subAction: 'create', - subActionParams: { - title: 'Case from case connector!!', - tags: ['case', 'connector'], - connector: { - id: 'jira', - name: 'Jira', - type: '.jira', - fields: { - issueType: '10006', - priority: 'High', - parent: null, - }, - }, - }, - }; - - const caseConnector = await supertest - .post(`/api/actions/connector/${createdActionId}/_execute`) - .set('kbn-xsrf', 'foo') - .send({ params }) - .expect(200); - - expect(caseConnector.body).to.eql({ - status: 'error', - actionId: createdActionId, - message: - 'error validating action params: types that failed validation:\n- [0.subActionParams.description]: expected value of type [string] but got [undefined]\n- [1.subAction]: expected value to equal [update]\n- [2.subAction]: expected value to equal [addComment]', - retry: false, - }); - }); - - it('should respond with a 400 Bad Request when creating a case without tags', async () => { - const { body: createdAction } = await supertest - .post('/api/actions/connector') - .set('kbn-xsrf', 'foo') - .send({ - name: 'A case connector', - actionTypeId: '.case', - config: {}, - }) - .expect(200); - - createdActionId = createdAction.id; - const params = { - subAction: 'create', - subActionParams: { - title: 'Case from case connector!!', - description: 'case description', - connector: { - id: 'jira', - name: 'Jira', - type: '.jira', - fields: { - issueType: '10006', - priority: 'High', - parent: null, - }, - }, - }, - }; - - const caseConnector = await supertest - .post(`/api/actions/connector/${createdActionId}/_execute`) - .set('kbn-xsrf', 'foo') - .send({ params }) - .expect(200); - - expect(caseConnector.body).to.eql({ - status: 'error', - actionId: createdActionId, - message: - 'error validating action params: types that failed validation:\n- [0.subActionParams.tags]: expected value of type [array] but got [undefined]\n- [1.subAction]: expected value to equal [update]\n- [2.subAction]: expected value to equal [addComment]', - retry: false, - }); - }); - - it('should respond with a 400 Bad Request when creating a case without connector', async () => { - const { body: createdAction } = await supertest - .post('/api/actions/connector') - .set('kbn-xsrf', 'foo') - .send({ - name: 'A case connector', - actionTypeId: '.case', - config: {}, - }) - .expect(200); - - createdActionId = createdAction.id; - const params = { - subAction: 'create', - subActionParams: { - title: 'Case from case connector!!', - description: 'case description', - tags: ['case', 'connector'], - }, - }; - - const caseConnector = await supertest - .post(`/api/actions/connector/${createdActionId}/_execute`) - .set('kbn-xsrf', 'foo') - .send({ params }) - .expect(200); - - expect(caseConnector.body).to.eql({ - status: 'error', - actionId: createdActionId, - message: - 'error validating action params: types that failed validation:\n- [0.subActionParams.connector.id]: expected value of type [string] but got [undefined]\n- [1.subAction]: expected value to equal [update]\n- [2.subAction]: expected value to equal [addComment]', - retry: false, - }); - }); - - it('should respond with a 400 Bad Request when creating jira without issueType', async () => { - const { body: createdAction } = await supertest - .post('/api/actions/connector') - .set('kbn-xsrf', 'foo') - .send({ - name: 'A case connector', - actionTypeId: '.case', - config: {}, - }) - .expect(200); - - createdActionId = createdAction.id; - const params = { - subAction: 'create', - subActionParams: { - title: 'Case from case connector!!', - description: 'case description', - tags: ['case', 'connector'], - connector: { - id: 'jira', - name: 'Jira', - type: '.jira', - fields: { - priority: 'High', - parent: null, - }, - }, - }, - }; - - const caseConnector = await supertest - .post(`/api/actions/connector/${createdActionId}/_execute`) - .set('kbn-xsrf', 'foo') - .send({ params }) - .expect(200); - - expect(caseConnector.body).to.eql({ - status: 'error', - actionId: createdActionId, - message: - 'error validating action params: types that failed validation:\n- [0.subActionParams.connector.fields.issueType]: expected value of type [string] but got [undefined]\n- [1.subAction]: expected value to equal [update]\n- [2.subAction]: expected value to equal [addComment]', - retry: false, - }); - }); - - it('should respond with a 400 Bad Request when creating a connector with wrong fields', async () => { - const { body: createdAction } = await supertest - .post('/api/actions/connector') - .set('kbn-xsrf', 'foo') - .send({ - name: 'A case connector', - actionTypeId: '.case', - config: {}, - }) - .expect(200); - - createdActionId = createdAction.id; - const params = { - subAction: 'create', - subActionParams: { - title: 'Case from case connector!!', - description: 'case description', - tags: ['case', 'connector'], - connector: { - id: 'servicenow', - name: 'Servicenow', - type: '.servicenow', - fields: { - impact: 'Medium', - severity: 'Medium', - notExists: 'not-exists', - }, - }, - }, - }; - - const caseConnector = await supertest - .post(`/api/actions/connector/${createdActionId}/_execute`) - .set('kbn-xsrf', 'foo') - .send({ params }) - .expect(200); - - expect(caseConnector.body).to.eql({ - status: 'error', - actionId: createdActionId, - message: - 'error validating action params: types that failed validation:\n- [0.subActionParams.connector.fields.notExists]: definition for this key is missing\n- [1.subAction]: expected value to equal [update]\n- [2.subAction]: expected value to equal [addComment]', - retry: false, - }); - }); - - it('should respond with a 400 Bad Request when creating a none without fields as null', async () => { - const { body: createdAction } = await supertest - .post('/api/actions/connector') - .set('kbn-xsrf', 'foo') - .send({ - name: 'A case connector', - actionTypeId: '.case', - config: {}, - }) - .expect(200); - - createdActionId = createdAction.id; - const params = { - subAction: 'create', - subActionParams: { - title: 'Case from case connector!!', - description: 'case description', - tags: ['case', 'connector'], - connector: { - id: 'none', - name: 'None', - type: '.none', - fields: {}, - }, - }, - }; - - const caseConnector = await supertest - .post(`/api/actions/connector/${createdActionId}/_execute`) - .set('kbn-xsrf', 'foo') - .send({ params }) - .expect(200); - - expect(caseConnector.body).to.eql({ - status: 'error', - actionId: createdActionId, - message: - 'error validating action params: types that failed validation:\n- [0.subActionParams.connector]: Fields must be set to null for connectors of type .none\n- [1.subAction]: expected value to equal [update]\n- [2.subAction]: expected value to equal [addComment]', - retry: false, - }); - }); - - it('should create a case', async () => { - const { body: createdAction } = await supertest - .post('/api/actions/connector') - .set('kbn-xsrf', 'foo') - .send({ - name: 'A case connector', - actionTypeId: '.case', - config: {}, - }) - .expect(200); - - createdActionId = createdAction.id; - const params = { - subAction: 'create', - subActionParams: { - title: 'Case from case connector!!', - tags: ['case', 'connector'], - description: 'case description', - connector: { - id: 'jira', - name: 'Jira', - type: '.jira', - fields: { - issueType: '10006', - priority: 'High', - parent: null, - }, - }, - settings: { - syncAlerts: true, - }, - }, - }; - - const caseConnector = await supertest - .post(`/api/actions/connector/${createdActionId}/_execute`) - .set('kbn-xsrf', 'foo') - .send({ params }) - .expect(200); - - const { body } = await supertest - .get(`${CASES_URL}/${caseConnector.body.data.id}`) - .set('kbn-xsrf', 'true') - .send() - .expect(200); - - const data = removeServerGeneratedPropertiesFromCase(body); - expect(data).to.eql({ - ...postCaseResp(caseConnector.body.data.id), - ...params.subActionParams, - created_by: { - email: null, - full_name: null, - username: null, - }, - }); - }); - - it('should create a case with connector with field as null if not provided', async () => { - const { body: createdAction } = await supertest - .post('/api/actions/connector') - .set('kbn-xsrf', 'foo') - .send({ - name: 'A case connector', - actionTypeId: '.case', - config: {}, - }) - .expect(200); - - createdActionId = createdAction.id; - const params = { - subAction: 'create', - subActionParams: { - title: 'Case from case connector!!', - tags: ['case', 'connector'], - description: 'case description', - connector: { - id: 'servicenow', - name: 'Servicenow', - type: '.servicenow', - fields: {}, - }, - settings: { - syncAlerts: true, - }, - }, - }; - - const caseConnector = await supertest - .post(`/api/actions/connector/${createdActionId}/_execute`) - .set('kbn-xsrf', 'foo') - .send({ params }) - .expect(200); - - const { body } = await supertest - .get(`${CASES_URL}/${caseConnector.body.data.id}`) - .set('kbn-xsrf', 'true') - .send() - .expect(200); - - const data = removeServerGeneratedPropertiesFromCase(body); - expect(data).to.eql({ - ...postCaseResp(caseConnector.body.data.id), - ...params.subActionParams, - connector: { - id: 'servicenow', - name: 'Servicenow', - type: '.servicenow', - fields: { - impact: null, - severity: null, - urgency: null, - category: null, - subcategory: null, - }, - }, - created_by: { - email: null, - full_name: null, - username: null, - }, - }); - }); - }); - - describe.skip('update', () => { - it('should respond with a 400 Bad Request when updating a case without id', async () => { - const { body: createdAction } = await supertest - .post('/api/actions/connector') - .set('kbn-xsrf', 'foo') - .send({ - name: 'A case connector', - actionTypeId: '.case', - config: {}, - }) - .expect(200); - - createdActionId = createdAction.id; - const params = { - subAction: 'update', - subActionParams: { - version: '123', - title: 'Case from case connector!!', - }, - }; - - const caseConnector = await supertest - .post(`/api/actions/connector/${createdActionId}/_execute`) - .set('kbn-xsrf', 'foo') - .send({ params }) - .expect(200); - - expect(caseConnector.body).to.eql({ - status: 'error', - actionId: createdActionId, - message: - 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [create]\n- [1.subActionParams.id]: expected value of type [string] but got [undefined]\n- [2.subAction]: expected value to equal [addComment]', - retry: false, - }); - }); - - it('should respond with a 400 Bad Request when updating a case without version', async () => { - const { body: createdAction } = await supertest - .post('/api/actions/connector') - .set('kbn-xsrf', 'foo') - .send({ - name: 'A case connector', - actionTypeId: '.case', - config: {}, - }) - .expect(200); - - createdActionId = createdAction.id; - const params = { - subAction: 'update', - subActionParams: { - id: '123', - title: 'Case from case connector!!', - }, - }; - - const caseConnector = await supertest - .post(`/api/actions/connector/${createdActionId}/_execute`) - .set('kbn-xsrf', 'foo') - .send({ params }) - .expect(200); - - expect(caseConnector.body).to.eql({ - status: 'error', - actionId: createdActionId, - message: - 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [create]\n- [1.subActionParams.version]: expected value of type [string] but got [undefined]\n- [2.subAction]: expected value to equal [addComment]', - retry: false, - }); - }); - - it('should update a case', async () => { - const { body: createdAction } = await supertest - .post('/api/actions/connector') - .set('kbn-xsrf', 'foo') - .send({ - name: 'A case connector', - actionTypeId: '.case', - config: {}, - }) - .expect(200); - - createdActionId = createdAction.id; - - const caseRes = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send(postCaseReq) - .expect(200); - - const params = { - subAction: 'update', - subActionParams: { - id: caseRes.body.id, - version: caseRes.body.version, - title: 'Case from case connector!!', - }, - }; - - await supertest - .post(`/api/actions/connector/${createdActionId}/_execute`) - .set('kbn-xsrf', 'foo') - .send({ params }) - .expect(200); - - const { body } = await supertest - .get(`${CASES_URL}/${caseRes.body.id}`) - .set('kbn-xsrf', 'true') - .send() - .expect(200); - - const data = removeServerGeneratedPropertiesFromCase(body); - expect(data).to.eql({ - ...postCaseResp(caseRes.body.id), - title: 'Case from case connector!!', - updated_by: { - email: null, - full_name: null, - username: null, - }, - }); - }); - }); - - describe.skip('addComment', () => { - it('should respond with a 400 Bad Request when adding a comment to a case without caseId', async () => { - const { body: createdAction } = await supertest - .post('/api/actions/connector') - .set('kbn-xsrf', 'foo') - .send({ - name: 'A case connector', - actionTypeId: '.case', - config: {}, - }) - .expect(200); - - createdActionId = createdAction.id; - const params = { - subAction: 'addComment', - subActionParams: { - comment: { comment: 'a comment', type: CommentType.user }, - }, - }; - - const caseConnector = await supertest - .post(`/api/actions/connector/${createdActionId}/_execute`) - .set('kbn-xsrf', 'foo') - .send({ params }) - .expect(200); - - expect(caseConnector.body).to.eql({ - status: 'error', - actionId: createdActionId, - message: - 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [create]\n- [1.subAction]: expected value to equal [update]\n- [2.subActionParams.caseId]: expected value of type [string] but got [undefined]', - retry: false, - }); - }); - - it('should respond with a 400 Bad Request when missing attributes of type user', async () => { - const { body: createdAction } = await supertest - .post('/api/actions/connector') - .set('kbn-xsrf', 'foo') - .send({ - name: 'A case connector', - actionTypeId: '.case', - config: {}, - }) - .expect(200); - - createdActionId = createdAction.id; - const params = { - subAction: 'addComment', - subActionParams: { - caseId: '123', - }, - }; - - const caseConnector = await supertest - .post(`/api/actions/connector/${createdActionId}/_execute`) - .set('kbn-xsrf', 'foo') - .send({ params }) - .expect(200); - - expect(caseConnector.body).to.eql({ - status: 'error', - actionId: createdActionId, - message: - 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [create]\n- [1.subAction]: expected value to equal [update]\n- [2.subActionParams.comment]: expected at least one defined value but got [undefined]', - retry: false, - }); - }); - - describe('adding alerts using a connector', () => { - beforeEach(async () => { - await esArchiver.load('auditbeat/hosts'); - await createSignalsIndex(supertest); - }); - - afterEach(async () => { - await deleteSignalsIndex(supertest); - await deleteAllAlerts(supertest); - await esArchiver.unload('auditbeat/hosts'); - }); - - it('should add a comment of type alert', async () => { - const rule = getRuleForSignalTesting(['auditbeat-*']); - const { id } = await createRule(supertest, rule); - await waitForRuleSuccessOrStatus(supertest, id); - await waitForSignalsToBePresent(supertest, 1, [id]); - const signals = await getSignalsByIds(supertest, [id]); - const alert = signals.hits.hits[0]; - - const { body: createdAction } = await supertest - .post('/api/actions/connector') - .set('kbn-xsrf', 'foo') - .send({ - name: 'A case connector', - actionTypeId: '.case', - config: {}, - }) - .expect(200); - - createdActionId = createdAction.id; - - const caseRes = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send(postCaseReq) - .expect(200); - - const params = { - subAction: 'addComment', - subActionParams: { - caseId: caseRes.body.id, - comment: { - alertId: alert._id, - index: alert._index, - type: CommentType.alert, - rule: { id: 'id', name: 'name' }, - }, - }, - }; - - const caseConnector = await supertest - .post(`/api/actions/connector/${createdActionId}/_execute`) - .set('kbn-xsrf', 'foo') - .send({ params }) - .expect(200); - - expect(caseConnector.body.status).to.eql('ok'); - - const { body } = await supertest - .get(`${CASES_URL}/${caseRes.body.id}`) - .set('kbn-xsrf', 'true') - .send() - .expect(200); - - const data = removeServerGeneratedPropertiesFromCase(body); - const comments = removeServerGeneratedPropertiesFromComments(data.comments ?? []); - expect({ ...data, comments }).to.eql({ - ...postCaseResp(caseRes.body.id), - comments, - totalAlerts: 1, - totalComment: 1, - updated_by: { - email: null, - full_name: null, - username: null, - }, - }); - }); - }); - - it('should respond with a 400 Bad Request when missing attributes of type alert', async () => { - const { body: createdAction } = await supertest - .post('/api/actions/connector') - .set('kbn-xsrf', 'foo') - .send({ - name: 'A case connector', - actionTypeId: '.case', - config: {}, - }) - .expect(200); - - createdActionId = createdAction.id; - const comment = { - alertId: 'test-id', - index: 'test-index', - type: CommentType.alert, - rule: { id: 'id', name: 'name' }, - }; - const params = { - subAction: 'addComment', - subActionParams: { - caseId: '123', - comment, - }, - }; - - // only omitting alertId here since the type for alertId and index differ, the messages will be different - for (const attribute of ['alertId']) { - const requestAttributes = omit(attribute, comment); - const caseConnector = await supertest - .post(`/api/actions/connector/${createdActionId}/_execute`) - .set('kbn-xsrf', 'foo') - .send({ - params: { - ...params, - subActionParams: { ...params.subActionParams, comment: requestAttributes }, - }, - }) - .expect(200); - - expect(caseConnector.body).to.eql({ - status: 'error', - actionId: createdActionId, - message: `error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [create]\n- [1.subAction]: expected value to equal [update]\n- [2.subActionParams.comment]: types that failed validation:\n - [subActionParams.comment.0.type]: expected value to equal [user]\n - [subActionParams.comment.1.${attribute}]: expected at least one defined value but got [undefined]\n - [subActionParams.comment.2.type]: expected value to equal [generated_alert]`, - retry: false, - }); - } - }); - - it('should respond with a 400 Bad Request when adding excess attributes for type user', async () => { - const { body: createdAction } = await supertest - .post('/api/actions/connector') - .set('kbn-xsrf', 'foo') - .send({ - name: 'A case connector', - actionTypeId: '.case', - config: {}, - }) - .expect(200); - - createdActionId = createdAction.id; - const params = { - subAction: 'addComment', - subActionParams: { - caseId: '123', - comment: { comment: 'a comment', type: CommentType.user }, - }, - }; - - for (const attribute of ['blah', 'bogus']) { - const caseConnector = await supertest - .post(`/api/actions/connector/${createdActionId}/_execute`) - .set('kbn-xsrf', 'foo') - .send({ - params: { - ...params, - subActionParams: { - ...params.subActionParams, - comment: { ...params.subActionParams.comment, [attribute]: attribute }, - }, - }, - }) - .expect(200); - expect(caseConnector.body).to.eql({ - status: 'error', - actionId: createdActionId, - message: `error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [create]\n- [1.subAction]: expected value to equal [update]\n- [2.subActionParams.comment]: types that failed validation:\n - [subActionParams.comment.0.${attribute}]: definition for this key is missing\n - [subActionParams.comment.1.type]: expected value to equal [alert]\n - [subActionParams.comment.2.type]: expected value to equal [generated_alert]`, - retry: false, - }); - } - }); - - it('should respond with a 400 Bad Request when adding excess attributes for type alert', async () => { - const { body: createdAction } = await supertest - .post('/api/actions/connector') - .set('kbn-xsrf', 'foo') - .send({ - name: 'A case connector', - actionTypeId: '.case', - config: {}, - }) - .expect(200); - - createdActionId = createdAction.id; - const params = { - subAction: 'addComment', - subActionParams: { - caseId: '123', - comment: { - alertId: 'test-id', - index: 'test-index', - type: CommentType.alert, - rule: { id: 'id', name: 'name' }, - }, - }, - }; - - for (const attribute of ['comment']) { - const caseConnector = await supertest - .post(`/api/actions/connector/${createdActionId}/_execute`) - .set('kbn-xsrf', 'foo') - .send({ - params: { - ...params, - subActionParams: { - ...params.subActionParams, - comment: { ...params.subActionParams.comment, [attribute]: attribute }, - }, - }, - }) - .expect(200); - - expect(caseConnector.body).to.eql({ - status: 'error', - actionId: createdActionId, - message: `error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [create]\n- [1.subAction]: expected value to equal [update]\n- [2.subActionParams.comment]: types that failed validation:\n - [subActionParams.comment.0.type]: expected value to equal [user]\n - [subActionParams.comment.1.${attribute}]: definition for this key is missing\n - [subActionParams.comment.2.type]: expected value to equal [generated_alert]`, - retry: false, - }); - } - }); - - it('should respond with a 400 Bad Request when adding a comment to a case without type', async () => { - const { body: createdAction } = await supertest - .post('/api/actions/connector') - .set('kbn-xsrf', 'foo') - .send({ - name: 'A case connector', - actionTypeId: '.case', - config: {}, - }) - .expect(200); - - createdActionId = createdAction.id; - const params = { - subAction: 'update', - subActionParams: { - caseId: '123', - comment: { comment: 'a comment' }, - }, - }; - - const caseConnector = await supertest - .post(`/api/actions/connector/${createdActionId}/_execute`) - .set('kbn-xsrf', 'foo') - .send({ params }) - .expect(200); - - expect(caseConnector.body).to.eql({ - status: 'error', - actionId: createdActionId, - message: - 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [create]\n- [1.subActionParams.id]: expected value of type [string] but got [undefined]\n- [2.subAction]: expected value to equal [addComment]', - retry: false, - }); - }); - - it('should add a comment of type user', async () => { - const { body: createdAction } = await supertest - .post('/api/actions/connector') - .set('kbn-xsrf', 'foo') - .send({ - name: 'A case connector', - actionTypeId: '.case', - config: {}, - }) - .expect(200); - - createdActionId = createdAction.id; - - const caseRes = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send(postCaseReq) - .expect(200); - - const params = { - subAction: 'addComment', - subActionParams: { - caseId: caseRes.body.id, - comment: { comment: 'a comment', type: CommentType.user }, - }, - }; - - await supertest - .post(`/api/actions/connector/${createdActionId}/_execute`) - .set('kbn-xsrf', 'foo') - .send({ params }) - .expect(200); - - const { body } = await supertest - .get(`${CASES_URL}/${caseRes.body.id}`) - .set('kbn-xsrf', 'true') - .send() - .expect(200); - - const data = removeServerGeneratedPropertiesFromCase(body); - const comments = removeServerGeneratedPropertiesFromComments(data.comments ?? []); - expect({ ...data, comments }).to.eql({ - ...postCaseResp(caseRes.body.id), - comments, - totalComment: 1, - updated_by: { - email: null, - full_name: null, - username: null, - }, - }); - }); - - it('should add a comment of type alert', async () => { - const { body: createdAction } = await supertest - .post('/api/actions/connector') - .set('kbn-xsrf', 'foo') - .send({ - name: 'A case connector', - actionTypeId: '.case', - config: {}, - }) - .expect(200); - - createdActionId = createdAction.id; - - const caseRes = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send(postCaseReq) - .expect(200); - - const params = { - subAction: 'addComment', - subActionParams: { - caseId: caseRes.body.id, - comment: { - alertId: 'test-id', - index: 'test-index', - type: CommentType.alert, - rule: { id: 'id', name: 'name' }, - }, - }, - }; - - await supertest - .post(`/api/actions/connector/${createdActionId}/_execute`) - .set('kbn-xsrf', 'foo') - .send({ params }) - .expect(200); - - const { body } = await supertest - .get(`${CASES_URL}/${caseRes.body.id}`) - .set('kbn-xsrf', 'true') - .send() - .expect(200); - - const data = removeServerGeneratedPropertiesFromCase(body); - const comments = removeServerGeneratedPropertiesFromComments(data.comments ?? []); - expect({ ...data, comments }).to.eql({ - ...postCaseResp(caseRes.body.id), - comments, - totalComment: 1, - totalAlerts: 1, - updated_by: { - email: null, - full_name: null, - username: null, - }, - }); - }); - }); - }); -}; diff --git a/x-pack/test/case_api_integration/security_only/tests/common/index.ts b/x-pack/test/case_api_integration/security_only/tests/common/index.ts index fda3a82dfd3570..7dd6dd4e22711b 100644 --- a/x-pack/test/case_api_integration/security_only/tests/common/index.ts +++ b/x-pack/test/case_api_integration/security_only/tests/common/index.ts @@ -27,13 +27,7 @@ export default ({ loadTestFile }: FtrProviderContext): void => { loadTestFile(require.resolve('./cases/tags/get_tags')); loadTestFile(require.resolve('./user_actions/get_all_user_actions')); loadTestFile(require.resolve('./configure/get_configure')); - loadTestFile(require.resolve('./configure/get_connectors')); loadTestFile(require.resolve('./configure/patch_configure')); loadTestFile(require.resolve('./configure/post_configure')); - loadTestFile(require.resolve('./connectors/case')); - loadTestFile(require.resolve('./sub_cases/patch_sub_cases')); - loadTestFile(require.resolve('./sub_cases/delete_sub_cases')); - loadTestFile(require.resolve('./sub_cases/get_sub_case')); - loadTestFile(require.resolve('./sub_cases/find_sub_cases')); }); }; diff --git a/x-pack/test/case_api_integration/security_only/tests/common/sub_cases/delete_sub_cases.ts b/x-pack/test/case_api_integration/security_only/tests/common/sub_cases/delete_sub_cases.ts deleted file mode 100644 index 951db263a6c784..00000000000000 --- a/x-pack/test/case_api_integration/security_only/tests/common/sub_cases/delete_sub_cases.ts +++ /dev/null @@ -1,112 +0,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 expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; - -import { - CASES_URL, - SUB_CASES_PATCH_DEL_URL, -} from '../../../../../../plugins/cases/common/constants'; -import { postCommentUserReq } from '../../../../common/lib/mock'; -import { - createCaseAction, - createSubCase, - deleteAllCaseItems, - deleteCaseAction, -} from '../../../../common/lib/utils'; -import { getSubCaseDetailsUrl } from '../../../../../../plugins/cases/common/api/helpers'; -import { CaseResponse } from '../../../../../../plugins/cases/common/api'; - -// eslint-disable-next-line import/no-default-export -export default function ({ getService }: FtrProviderContext) { - const supertest = getService('supertest'); - const es = getService('es'); - - // ENABLE_CASE_CONNECTOR: remove this outer describe once the case connector feature is completed - describe('delete_sub_cases disabled routes', () => { - it('should return a 404 when attempting to access the route and the case connector feature is disabled', async () => { - await supertest - .delete(`${SUB_CASES_PATCH_DEL_URL}?ids=["sub-case-id"]`) - .set('kbn-xsrf', 'true') - .send() - .expect(404); - }); - - // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests - describe.skip('delete_sub_cases', () => { - let actionID: string; - before(async () => { - actionID = await createCaseAction(supertest); - }); - after(async () => { - await deleteCaseAction(supertest, actionID); - }); - afterEach(async () => { - await deleteAllCaseItems(es); - }); - - it('should delete a sub case', async () => { - const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - expect(caseInfo.subCases![0].id).to.not.eql(undefined); - - const { body: subCase } = await supertest - .get(getSubCaseDetailsUrl(caseInfo.id, caseInfo.subCases![0].id)) - .send() - .expect(200); - - expect(subCase.id).to.not.eql(undefined); - - const { body } = await supertest - .delete(`${SUB_CASES_PATCH_DEL_URL}?ids=["${subCase.id}"]`) - .set('kbn-xsrf', 'true') - .send() - .expect(204); - - expect(body).to.eql({}); - await supertest - .get(getSubCaseDetailsUrl(caseInfo.id, caseInfo.subCases![0].id)) - .send() - .expect(404); - }); - - it(`should delete a sub case's comments when that case gets deleted`, async () => { - const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - expect(caseInfo.subCases![0].id).to.not.eql(undefined); - - // there should be two comments on the sub case now - const { body: patchedCaseWithSubCase }: { body: CaseResponse } = await supertest - .post(`${CASES_URL}/${caseInfo.id}/comments`) - .set('kbn-xsrf', 'true') - .query({ subCaseId: caseInfo.subCases![0].id }) - .send(postCommentUserReq) - .expect(200); - - const subCaseCommentUrl = `${CASES_URL}/${patchedCaseWithSubCase.id}/comments/${ - patchedCaseWithSubCase.comments![1].id - }`; - // make sure we can get the second comment - await supertest.get(subCaseCommentUrl).set('kbn-xsrf', 'true').send().expect(200); - - await supertest - .delete(`${SUB_CASES_PATCH_DEL_URL}?ids=["${patchedCaseWithSubCase.subCases![0].id}"]`) - .set('kbn-xsrf', 'true') - .send() - .expect(204); - - await supertest.get(subCaseCommentUrl).set('kbn-xsrf', 'true').send().expect(404); - }); - - it('unhappy path - 404s when sub case id is invalid', async () => { - await supertest - .delete(`${SUB_CASES_PATCH_DEL_URL}?ids=["fake-id"]`) - .set('kbn-xsrf', 'true') - .send() - .expect(404); - }); - }); - }); -} diff --git a/x-pack/test/case_api_integration/security_only/tests/common/sub_cases/find_sub_cases.ts b/x-pack/test/case_api_integration/security_only/tests/common/sub_cases/find_sub_cases.ts deleted file mode 100644 index d54523bec0c4d4..00000000000000 --- a/x-pack/test/case_api_integration/security_only/tests/common/sub_cases/find_sub_cases.ts +++ /dev/null @@ -1,480 +0,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 expect from '@kbn/expect'; -import type { ApiResponse, estypes } from '@elastic/elasticsearch'; -import { FtrProviderContext } from '../../../../common/ftr_provider_context'; - -import { findSubCasesResp, postCollectionReq } from '../../../../common/lib/mock'; -import { - createCaseAction, - createSubCase, - CreateSubCaseResp, - deleteAllCaseItems, - deleteCaseAction, - setStatus, -} from '../../../../common/lib/utils'; -import { getSubCasesUrl } from '../../../../../../plugins/cases/common/api/helpers'; -import { - CaseResponse, - CaseStatuses, - CommentType, - SubCasesFindResponse, -} from '../../../../../../plugins/cases/common/api'; -import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; -import { - ContextTypeGeneratedAlertType, - createAlertsString, -} from '../../../../../../plugins/cases/server/connectors'; - -interface SubCaseAttributes { - 'cases-sub-case': { - created_at: string; - }; -} - -// eslint-disable-next-line import/no-default-export -export default ({ getService }: FtrProviderContext): void => { - const supertest = getService('supertest'); - const es = getService('es'); - - // ENABLE_CASE_CONNECTOR: remove this outer describe once the case connector feature is completed - describe('find_sub_cases disabled route', () => { - it('should return a 404 when attempting to access the route and the case connector feature is disabled', async () => { - await supertest.get(`${getSubCasesUrl('case-id')}/_find`).expect(404); - }); - - // ENABLE_CASE_CONNECTOR: enable these tests once the case connector feature is completed - describe.skip('create case connector', () => { - let actionID: string; - before(async () => { - actionID = await createCaseAction(supertest); - }); - after(async () => { - await deleteCaseAction(supertest, actionID); - }); - - describe('basic find tests', () => { - afterEach(async () => { - await deleteAllCaseItems(es); - }); - it('should not find any sub cases when none exist', async () => { - const { body: caseResp }: { body: CaseResponse } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send(postCollectionReq) - .expect(200); - - const { body: findSubCases } = await supertest - .get(`${getSubCasesUrl(caseResp.id)}/_find`) - .expect(200); - - expect(findSubCases).to.eql({ - page: 1, - per_page: 20, - total: 0, - subCases: [], - count_open_cases: 0, - count_closed_cases: 0, - count_in_progress_cases: 0, - }); - }); - - it('should return a sub cases with comment stats', async () => { - const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - - const { body }: { body: SubCasesFindResponse } = await supertest - .get(`${getSubCasesUrl(caseInfo.id)}/_find`) - .expect(200); - - expect(body).to.eql({ - ...findSubCasesResp, - total: 1, - // find should not return the comments themselves only the stats - subCases: [{ ...caseInfo.subCases![0], comments: [], totalComment: 1, totalAlerts: 2 }], - count_open_cases: 1, - }); - }); - - it('should return multiple sub cases', async () => { - const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - const subCase2Resp = await createSubCase({ supertest, caseID: caseInfo.id, actionID }); - - const { body }: { body: SubCasesFindResponse } = await supertest - .get(`${getSubCasesUrl(caseInfo.id)}/_find`) - .expect(200); - - expect(body).to.eql({ - ...findSubCasesResp, - total: 2, - // find should not return the comments themselves only the stats - subCases: [ - { - // there should only be 1 closed sub case - ...subCase2Resp.modifiedSubCases![0], - comments: [], - totalComment: 1, - totalAlerts: 2, - status: CaseStatuses.closed, - }, - { - ...subCase2Resp.newSubCaseInfo.subCases![0], - comments: [], - totalComment: 1, - totalAlerts: 2, - }, - ], - count_open_cases: 1, - count_closed_cases: 1, - }); - }); - - it('should only return open when filtering for open', async () => { - const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - // this will result in one closed case and one open - await createSubCase({ supertest, caseID: caseInfo.id, actionID }); - - const { body }: { body: SubCasesFindResponse } = await supertest - .get(`${getSubCasesUrl(caseInfo.id)}/_find?status=${CaseStatuses.open}`) - .expect(200); - - expect(body.total).to.be(1); - expect(body.subCases[0].status).to.be(CaseStatuses.open); - expect(body.count_closed_cases).to.be(1); - expect(body.count_open_cases).to.be(1); - expect(body.count_in_progress_cases).to.be(0); - }); - - it('should only return closed when filtering for closed', async () => { - const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - // this will result in one closed case and one open - await createSubCase({ supertest, caseID: caseInfo.id, actionID }); - - const { body }: { body: SubCasesFindResponse } = await supertest - .get(`${getSubCasesUrl(caseInfo.id)}/_find?status=${CaseStatuses.closed}`) - .expect(200); - - expect(body.total).to.be(1); - expect(body.subCases[0].status).to.be(CaseStatuses.closed); - expect(body.count_closed_cases).to.be(1); - expect(body.count_open_cases).to.be(1); - expect(body.count_in_progress_cases).to.be(0); - }); - - it('should only return in progress when filtering for in progress', async () => { - const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - // this will result in one closed case and one open - const { newSubCaseInfo: secondSub } = await createSubCase({ - supertest, - caseID: caseInfo.id, - actionID, - }); - - await setStatus({ - supertest, - cases: [ - { - id: secondSub.subCases![0].id, - version: secondSub.subCases![0].version, - status: CaseStatuses['in-progress'], - }, - ], - type: 'sub_case', - }); - - const { body }: { body: SubCasesFindResponse } = await supertest - .get(`${getSubCasesUrl(caseInfo.id)}/_find?status=${CaseStatuses['in-progress']}`) - .expect(200); - - expect(body.total).to.be(1); - expect(body.subCases[0].status).to.be(CaseStatuses['in-progress']); - expect(body.count_closed_cases).to.be(1); - expect(body.count_open_cases).to.be(0); - expect(body.count_in_progress_cases).to.be(1); - }); - - it('should sort on createdAt field in descending order', async () => { - const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - // this will result in one closed case and one open - await createSubCase({ - supertest, - caseID: caseInfo.id, - actionID, - }); - - const { body }: { body: SubCasesFindResponse } = await supertest - .get(`${getSubCasesUrl(caseInfo.id)}/_find?sortField=createdAt&sortOrder=desc`) - .expect(200); - - expect(body.total).to.be(2); - expect(body.subCases[0].status).to.be(CaseStatuses.open); - expect(body.subCases[1].status).to.be(CaseStatuses.closed); - expect(body.count_closed_cases).to.be(1); - expect(body.count_open_cases).to.be(1); - expect(body.count_in_progress_cases).to.be(0); - }); - - it('should sort on createdAt field in ascending order', async () => { - const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - // this will result in one closed case and one open - await createSubCase({ - supertest, - caseID: caseInfo.id, - actionID, - }); - - const { body }: { body: SubCasesFindResponse } = await supertest - .get(`${getSubCasesUrl(caseInfo.id)}/_find?sortField=createdAt&sortOrder=asc`) - .expect(200); - - expect(body.total).to.be(2); - expect(body.subCases[0].status).to.be(CaseStatuses.closed); - expect(body.subCases[1].status).to.be(CaseStatuses.open); - expect(body.count_closed_cases).to.be(1); - expect(body.count_open_cases).to.be(1); - expect(body.count_in_progress_cases).to.be(0); - }); - - it('should sort on updatedAt field in ascending order', async () => { - const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - // this will result in one closed case and one open - const { newSubCaseInfo: secondSub } = await createSubCase({ - supertest, - caseID: caseInfo.id, - actionID, - }); - - await setStatus({ - supertest, - cases: [ - { - id: secondSub.subCases![0].id, - version: secondSub.subCases![0].version, - status: CaseStatuses['in-progress'], - }, - ], - type: 'sub_case', - }); - - const { body }: { body: SubCasesFindResponse } = await supertest - .get(`${getSubCasesUrl(caseInfo.id)}/_find?sortField=updatedAt&sortOrder=asc`) - .expect(200); - - expect(body.total).to.be(2); - expect(body.subCases[0].status).to.be(CaseStatuses.closed); - expect(body.subCases[1].status).to.be(CaseStatuses['in-progress']); - expect(body.count_closed_cases).to.be(1); - expect(body.count_open_cases).to.be(0); - expect(body.count_in_progress_cases).to.be(1); - }); - }); - - describe('pagination', () => { - const numCases = 4; - let collection: CaseResponse; - before(async () => { - ({ body: collection } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send(postCollectionReq) - .expect(200)); - - await createSubCases(numCases, collection.id); - }); - - after(async () => { - await deleteAllCaseItems(es); - }); - - const createSubCases = async (total: number, caseID: string) => { - const responses: CreateSubCaseResp[] = []; - for (let i = 0; i < total; i++) { - const postCommentGenAlertReq: ContextTypeGeneratedAlertType = { - alerts: createAlertsString([ - { _id: `${i}`, _index: 'test-index', ruleId: 'rule-id', ruleName: 'rule name' }, - ]), - type: CommentType.generatedAlert, - owner: 'securitySolutionFixture', - }; - responses.push( - await createSubCase({ - supertest, - actionID, - caseID, - comment: postCommentGenAlertReq, - }) - ); - } - return responses; - }; - - const getAllCasesSortedByCreatedAtAsc = async () => { - const cases: ApiResponse> = await es.search({ - index: '.kibana', - body: { - size: 10000, - sort: [{ 'cases-sub-case.created_at': { unmapped_type: 'date', order: 'asc' } }], - query: { - term: { type: 'cases-sub-case' }, - }, - }, - }); - return cases.body.hits.hits.map((hit) => hit._source); - }; - - it('returns the correct total when perPage is less than the total', async () => { - const { body }: { body: SubCasesFindResponse } = await supertest - .get(`${getSubCasesUrl(collection.id)}/_find`) - .query({ - sortOrder: 'asc', - page: 1, - perPage: 3, - }) - .set('kbn-xsrf', 'true') - .expect(200); - - expect(body.subCases.length).to.eql(3); - expect(body.total).to.eql(4); - expect(body.page).to.eql(1); - expect(body.per_page).to.eql(3); - // there will only be 1 open sub case, all the rest will be closed - expect(body.count_open_cases).to.eql(1); - expect(body.count_closed_cases).to.eql(3); - expect(body.count_in_progress_cases).to.eql(0); - }); - - it('returns the correct total when perPage is greater than the total', async () => { - const { body }: { body: SubCasesFindResponse } = await supertest - .get(`${getSubCasesUrl(collection.id)}/_find`) - .query({ - sortOrder: 'asc', - page: 1, - perPage: 11, - }) - .set('kbn-xsrf', 'true') - .expect(200); - - expect(body.total).to.eql(4); - expect(body.page).to.eql(1); - expect(body.per_page).to.eql(11); - expect(body.subCases.length).to.eql(4); - expect(body.count_open_cases).to.eql(1); - expect(body.count_closed_cases).to.eql(3); - expect(body.count_in_progress_cases).to.eql(0); - }); - - it('returns the correct total when perPage is equal to the total', async () => { - const { body }: { body: SubCasesFindResponse } = await supertest - .get(`${getSubCasesUrl(collection.id)}/_find`) - .query({ - sortOrder: 'asc', - page: 1, - perPage: 4, - }) - .set('kbn-xsrf', 'true') - .expect(200); - - expect(body.total).to.eql(4); - expect(body.page).to.eql(1); - expect(body.per_page).to.eql(4); - expect(body.subCases.length).to.eql(4); - expect(body.count_open_cases).to.eql(1); - expect(body.count_closed_cases).to.eql(3); - expect(body.count_in_progress_cases).to.eql(0); - }); - - it('returns the second page of results', async () => { - const perPage = 2; - const { body }: { body: SubCasesFindResponse } = await supertest - .get(`${getSubCasesUrl(collection.id)}/_find`) - .query({ - sortOrder: 'asc', - page: 2, - perPage, - }) - .set('kbn-xsrf', 'true') - .expect(200); - - expect(body.total).to.eql(4); - expect(body.page).to.eql(2); - expect(body.per_page).to.eql(2); - expect(body.subCases.length).to.eql(2); - expect(body.count_open_cases).to.eql(1); - expect(body.count_closed_cases).to.eql(3); - expect(body.count_in_progress_cases).to.eql(0); - - const allCases = await getAllCasesSortedByCreatedAtAsc(); - - body.subCases.map((subCaseInfo, index) => { - // we started on the second page of 10 cases with a perPage of 5, so the first case should 0 + 5 (index + perPage) - expect(subCaseInfo.created_at).to.eql( - allCases[index + perPage]?.['cases-sub-case'].created_at - ); - }); - }); - - it('paginates with perPage of 2 through 4 total sub cases', async () => { - const total = 4; - const perPage = 2; - - // it's less than or equal here because the page starts at 1, so page 2 is a valid page number - // and should have sub cases titles 3, and 4 - for (let currentPage = 1; currentPage <= total / perPage; currentPage++) { - const { body }: { body: SubCasesFindResponse } = await supertest - .get(`${getSubCasesUrl(collection.id)}/_find`) - .query({ - sortOrder: 'asc', - page: currentPage, - perPage, - }) - .set('kbn-xsrf', 'true') - .expect(200); - - expect(body.total).to.eql(total); - expect(body.page).to.eql(currentPage); - expect(body.per_page).to.eql(perPage); - expect(body.subCases.length).to.eql(perPage); - expect(body.count_open_cases).to.eql(1); - expect(body.count_closed_cases).to.eql(total - 1); - expect(body.count_in_progress_cases).to.eql(0); - - const allCases = await getAllCasesSortedByCreatedAtAsc(); - - body.subCases.map((subCaseInfo, index) => { - // for page 1, the cases tiles should be 0,1,2 for page 2: 3,4,5 etc (assuming the titles were sorted - // correctly) - expect(subCaseInfo.created_at).to.eql( - allCases[index + perPage * (currentPage - 1)]?.['cases-sub-case'].created_at - ); - }); - } - }); - - it('retrieves the last sub case', async () => { - const { body }: { body: SubCasesFindResponse } = await supertest - .get(`${getSubCasesUrl(collection.id)}/_find`) - .query({ - sortOrder: 'asc', - // this should skip the first 3 sub cases and only return the last one - page: 2, - perPage: 3, - }) - .set('kbn-xsrf', 'true') - .expect(200); - - expect(body.total).to.eql(4); - expect(body.page).to.eql(2); - expect(body.per_page).to.eql(3); - expect(body.subCases.length).to.eql(1); - expect(body.count_open_cases).to.eql(1); - expect(body.count_closed_cases).to.eql(3); - expect(body.count_in_progress_cases).to.eql(0); - }); - }); - }); - }); -}; diff --git a/x-pack/test/case_api_integration/security_only/tests/common/sub_cases/get_sub_case.ts b/x-pack/test/case_api_integration/security_only/tests/common/sub_cases/get_sub_case.ts deleted file mode 100644 index 35ed4ba5c3c71b..00000000000000 --- a/x-pack/test/case_api_integration/security_only/tests/common/sub_cases/get_sub_case.ts +++ /dev/null @@ -1,119 +0,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 expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; - -import { commentsResp, postCommentAlertReq, subCaseResp } from '../../../../common/lib/mock'; -import { - createCaseAction, - createSubCase, - defaultCreateSubComment, - deleteAllCaseItems, - deleteCaseAction, - removeServerGeneratedPropertiesFromComments, - removeServerGeneratedPropertiesFromSubCase, -} from '../../../../common/lib/utils'; -import { - getCaseCommentsUrl, - getSubCaseDetailsUrl, -} from '../../../../../../plugins/cases/common/api/helpers'; -import { - AssociationType, - CaseResponse, - SubCaseResponse, -} from '../../../../../../plugins/cases/common/api'; - -// eslint-disable-next-line import/no-default-export -export default ({ getService }: FtrProviderContext): void => { - const supertest = getService('supertest'); - const es = getService('es'); - - // ENABLE_CASE_CONNECTOR: remove the outer describe once the case connector feature is completed - describe('get_sub_case disabled route', () => { - it('should return a 404 when attempting to access the route and the case connector feature is disabled', async () => { - await supertest.get(getSubCaseDetailsUrl('case-id', 'sub-case-id')).expect(404); - }); - - // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests - describe.skip('get_sub_case', () => { - let actionID: string; - before(async () => { - actionID = await createCaseAction(supertest); - }); - after(async () => { - await deleteCaseAction(supertest, actionID); - }); - afterEach(async () => { - await deleteAllCaseItems(es); - }); - - it('should return a case', async () => { - const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - - const { body }: { body: SubCaseResponse } = await supertest - .get(getSubCaseDetailsUrl(caseInfo.id, caseInfo.subCases![0].id)) - .set('kbn-xsrf', 'true') - .send() - .expect(200); - - expect(removeServerGeneratedPropertiesFromComments(body.comments)).to.eql( - commentsResp({ - comments: [{ comment: defaultCreateSubComment, id: caseInfo.comments![0].id }], - associationType: AssociationType.subCase, - }) - ); - - expect(removeServerGeneratedPropertiesFromSubCase(body)).to.eql( - subCaseResp({ id: body.id, totalComment: 1, totalAlerts: 2 }) - ); - }); - - it('should return the correct number of alerts with multiple types of alerts', async () => { - const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - - const { body: singleAlert }: { body: CaseResponse } = await supertest - .post(getCaseCommentsUrl(caseInfo.id)) - .query({ subCaseId: caseInfo.subCases![0].id }) - .set('kbn-xsrf', 'true') - .send(postCommentAlertReq) - .expect(200); - - const { body }: { body: SubCaseResponse } = await supertest - .get(getSubCaseDetailsUrl(caseInfo.id, caseInfo.subCases![0].id)) - .set('kbn-xsrf', 'true') - .send() - .expect(200); - - expect(removeServerGeneratedPropertiesFromComments(body.comments)).to.eql( - commentsResp({ - comments: [ - { comment: defaultCreateSubComment, id: caseInfo.comments![0].id }, - { - comment: postCommentAlertReq, - id: singleAlert.comments![1].id, - }, - ], - associationType: AssociationType.subCase, - }) - ); - - expect(removeServerGeneratedPropertiesFromSubCase(body)).to.eql( - subCaseResp({ id: body.id, totalComment: 2, totalAlerts: 3 }) - ); - }); - - it('unhappy path - 404s when case is not there', async () => { - await supertest - .get(getSubCaseDetailsUrl('fake-case-id', 'fake-sub-case-id')) - .set('kbn-xsrf', 'true') - .send() - .expect(404); - }); - }); - }); -}; diff --git a/x-pack/test/case_api_integration/security_only/tests/common/sub_cases/patch_sub_cases.ts b/x-pack/test/case_api_integration/security_only/tests/common/sub_cases/patch_sub_cases.ts deleted file mode 100644 index 442644463fa38d..00000000000000 --- a/x-pack/test/case_api_integration/security_only/tests/common/sub_cases/patch_sub_cases.ts +++ /dev/null @@ -1,515 +0,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 expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; - -import { - CASES_URL, - SUB_CASES_PATCH_DEL_URL, -} from '../../../../../../plugins/cases/common/constants'; -import { - createCaseAction, - createSubCase, - deleteAllCaseItems, - deleteCaseAction, - getSignalsWithES, - setStatus, -} from '../../../../common/lib/utils'; -import { getSubCaseDetailsUrl } from '../../../../../../plugins/cases/common/api/helpers'; -import { - CaseStatuses, - CommentType, - SubCaseResponse, -} from '../../../../../../plugins/cases/common/api'; -import { createAlertsString } from '../../../../../../plugins/cases/server/connectors'; -import { postCaseReq, postCollectionReq } from '../../../../common/lib/mock'; - -const defaultSignalsIndex = '.siem-signals-default-000001'; - -// eslint-disable-next-line import/no-default-export -export default function ({ getService }: FtrProviderContext) { - const supertest = getService('supertest'); - const es = getService('es'); - const esArchiver = getService('esArchiver'); - - // ENABLE_CASE_CONNECTOR: remove the outer describe once the case connector feature is completed - describe('patch_sub_cases disabled route', () => { - it('should return a 404 when attempting to access the route and the case connector feature is disabled', async () => { - await supertest - .patch(SUB_CASES_PATCH_DEL_URL) - .set('kbn-xsrf', 'true') - .send({ subCases: [] }) - .expect(404); - }); - - // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests - describe.skip('patch_sub_cases', () => { - let actionID: string; - before(async () => { - actionID = await createCaseAction(supertest); - }); - after(async () => { - await deleteCaseAction(supertest, actionID); - }); - beforeEach(async () => { - await esArchiver.load('cases/signals/default'); - }); - afterEach(async () => { - await esArchiver.unload('cases/signals/default'); - await deleteAllCaseItems(es); - }); - - it('should update the status of a sub case', async () => { - const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - - await setStatus({ - supertest, - cases: [ - { - id: caseInfo.subCases![0].id, - version: caseInfo.subCases![0].version, - status: CaseStatuses['in-progress'], - }, - ], - type: 'sub_case', - }); - const { body: subCase }: { body: SubCaseResponse } = await supertest - .get(getSubCaseDetailsUrl(caseInfo.id, caseInfo.subCases![0].id)) - .expect(200); - - expect(subCase.status).to.eql(CaseStatuses['in-progress']); - }); - - it('should update the status of one alert attached to a sub case', async () => { - const signalID = '5f2b9ec41f8febb1c06b5d1045aeabb9874733b7617e88a370510f2fb3a41a5d'; - - const { newSubCaseInfo: caseInfo } = await createSubCase({ - supertest, - actionID, - comment: { - alerts: createAlertsString([ - { - _id: signalID, - _index: defaultSignalsIndex, - ruleId: 'id', - ruleName: 'name', - }, - ]), - type: CommentType.generatedAlert, - owner: 'securitySolutionFixture', - }, - }); - - await es.indices.refresh({ index: defaultSignalsIndex }); - - let signals = await getSignalsWithES({ es, indices: defaultSignalsIndex, ids: signalID }); - - expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( - CaseStatuses.open - ); - - await setStatus({ - supertest, - cases: [ - { - id: caseInfo.subCases![0].id, - version: caseInfo.subCases![0].version, - status: CaseStatuses['in-progress'], - }, - ], - type: 'sub_case', - }); - - await es.indices.refresh({ index: defaultSignalsIndex }); - - signals = await getSignalsWithES({ es, indices: defaultSignalsIndex, ids: signalID }); - - expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( - CaseStatuses['in-progress'] - ); - }); - - it('should update the status of multiple alerts attached to a sub case', async () => { - const signalID = '5f2b9ec41f8febb1c06b5d1045aeabb9874733b7617e88a370510f2fb3a41a5d'; - - const signalID2 = '4d0f4b1533e46b66b43bdd0330d23f39f2cf42a7253153270e38d30cce9ff0c6'; - - const { newSubCaseInfo: caseInfo } = await createSubCase({ - supertest, - actionID, - comment: { - alerts: createAlertsString([ - { - _id: signalID, - _index: defaultSignalsIndex, - ruleId: 'id', - ruleName: 'name', - }, - { - _id: signalID2, - _index: defaultSignalsIndex, - ruleId: 'id', - ruleName: 'name', - }, - ]), - type: CommentType.generatedAlert, - owner: 'securitySolutionFixture', - }, - }); - - await es.indices.refresh({ index: defaultSignalsIndex }); - - let signals = await getSignalsWithES({ - es, - indices: defaultSignalsIndex, - ids: [signalID, signalID2], - }); - - expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( - CaseStatuses.open - ); - expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source?.signal.status).to.be( - CaseStatuses.open - ); - - await setStatus({ - supertest, - cases: [ - { - id: caseInfo.subCases![0].id, - version: caseInfo.subCases![0].version, - status: CaseStatuses['in-progress'], - }, - ], - type: 'sub_case', - }); - - await es.indices.refresh({ index: defaultSignalsIndex }); - - signals = await getSignalsWithES({ - es, - indices: defaultSignalsIndex, - ids: [signalID, signalID2], - }); - - expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( - CaseStatuses['in-progress'] - ); - expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source?.signal.status).to.be( - CaseStatuses['in-progress'] - ); - }); - - it('should update the status of multiple alerts attached to multiple sub cases in one collection', async () => { - const signalID = '5f2b9ec41f8febb1c06b5d1045aeabb9874733b7617e88a370510f2fb3a41a5d'; - const signalID2 = '4d0f4b1533e46b66b43bdd0330d23f39f2cf42a7253153270e38d30cce9ff0c6'; - - const { newSubCaseInfo: initialCaseInfo } = await createSubCase({ - supertest, - actionID, - caseInfo: { - ...postCollectionReq, - settings: { - syncAlerts: false, - }, - }, - comment: { - alerts: createAlertsString([ - { - _id: signalID, - _index: defaultSignalsIndex, - ruleId: 'id', - ruleName: 'name', - }, - ]), - type: CommentType.generatedAlert, - owner: 'securitySolutionFixture', - }, - }); - - // This will close the first sub case and create a new one - const { newSubCaseInfo: collectionWithSecondSub } = await createSubCase({ - supertest, - actionID, - caseID: initialCaseInfo.id, - comment: { - alerts: createAlertsString([ - { - _id: signalID2, - _index: defaultSignalsIndex, - ruleId: 'id', - ruleName: 'name', - }, - ]), - type: CommentType.generatedAlert, - owner: 'securitySolutionFixture', - }, - }); - - await es.indices.refresh({ index: defaultSignalsIndex }); - - let signals = await getSignalsWithES({ - es, - indices: defaultSignalsIndex, - ids: [signalID, signalID2], - }); - - // There should be no change in their status since syncing is disabled - expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( - CaseStatuses.open - ); - expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source?.signal.status).to.be( - CaseStatuses.open - ); - - await setStatus({ - supertest, - cases: [ - { - id: collectionWithSecondSub.subCases![0].id, - version: collectionWithSecondSub.subCases![0].version, - status: CaseStatuses['in-progress'], - }, - ], - type: 'sub_case', - }); - - await es.indices.refresh({ index: defaultSignalsIndex }); - - signals = await getSignalsWithES({ - es, - indices: defaultSignalsIndex, - ids: [signalID, signalID2], - }); - - // There still should be no change in their status since syncing is disabled - expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( - CaseStatuses.open - ); - expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source?.signal.status).to.be( - CaseStatuses.open - ); - - // Turn sync alerts on - await supertest - .patch(CASES_URL) - .set('kbn-xsrf', 'true') - .send({ - cases: [ - { - id: collectionWithSecondSub.id, - version: collectionWithSecondSub.version, - settings: { syncAlerts: true }, - }, - ], - }) - .expect(200); - - await es.indices.refresh({ index: defaultSignalsIndex }); - - signals = await getSignalsWithES({ - es, - indices: defaultSignalsIndex, - ids: [signalID, signalID2], - }); - - expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( - CaseStatuses.closed - ); - expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source?.signal.status).to.be( - CaseStatuses['in-progress'] - ); - }); - - it('should update the status of alerts attached to a case and sub case when sync settings is turned on', async () => { - const signalID = '5f2b9ec41f8febb1c06b5d1045aeabb9874733b7617e88a370510f2fb3a41a5d'; - const signalID2 = '4d0f4b1533e46b66b43bdd0330d23f39f2cf42a7253153270e38d30cce9ff0c6'; - - const { body: individualCase } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send({ - ...postCaseReq, - settings: { - syncAlerts: false, - }, - }); - - const { newSubCaseInfo: caseInfo } = await createSubCase({ - supertest, - actionID, - caseInfo: { - ...postCollectionReq, - settings: { - syncAlerts: false, - }, - }, - comment: { - alerts: createAlertsString([ - { - _id: signalID, - _index: defaultSignalsIndex, - ruleId: 'id', - ruleName: 'name', - }, - ]), - type: CommentType.generatedAlert, - owner: 'securitySolutionFixture', - }, - }); - - const { body: updatedIndWithComment } = await supertest - .post(`${CASES_URL}/${individualCase.id}/comments`) - .set('kbn-xsrf', 'true') - .send({ - alertId: signalID2, - index: defaultSignalsIndex, - rule: { id: 'test-rule-id', name: 'test-index-id' }, - type: CommentType.alert, - }) - .expect(200); - - await es.indices.refresh({ index: defaultSignalsIndex }); - - let signals = await getSignalsWithES({ - es, - indices: defaultSignalsIndex, - ids: [signalID, signalID2], - }); - - // There should be no change in their status since syncing is disabled - expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( - CaseStatuses.open - ); - expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source?.signal.status).to.be( - CaseStatuses.open - ); - - await setStatus({ - supertest, - cases: [ - { - id: caseInfo.subCases![0].id, - version: caseInfo.subCases![0].version, - status: CaseStatuses['in-progress'], - }, - ], - type: 'sub_case', - }); - - const updatedIndWithStatus = ( - await setStatus({ - supertest, - cases: [ - { - id: updatedIndWithComment.id, - version: updatedIndWithComment.version, - status: CaseStatuses.closed, - }, - ], - type: 'case', - }) - )[0]; // there should only be a single entry in the response - - await es.indices.refresh({ index: defaultSignalsIndex }); - - signals = await getSignalsWithES({ - es, - indices: defaultSignalsIndex, - ids: [signalID, signalID2], - }); - - // There should still be no change in their status since syncing is disabled - expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( - CaseStatuses.open - ); - expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source?.signal.status).to.be( - CaseStatuses.open - ); - - // Turn sync alerts on - await supertest - .patch(CASES_URL) - .set('kbn-xsrf', 'true') - .send({ - cases: [ - { - id: caseInfo.id, - version: caseInfo.version, - settings: { syncAlerts: true }, - }, - ], - }) - .expect(200); - - await supertest - .patch(CASES_URL) - .set('kbn-xsrf', 'true') - .send({ - cases: [ - { - id: updatedIndWithStatus.id, - version: updatedIndWithStatus.version, - settings: { syncAlerts: true }, - }, - ], - }) - .expect(200); - - await es.indices.refresh({ index: defaultSignalsIndex }); - - signals = await getSignalsWithES({ - es, - indices: defaultSignalsIndex, - ids: [signalID, signalID2], - }); - - // alerts should be updated now that the - expect(signals.get(defaultSignalsIndex)?.get(signalID)?._source?.signal.status).to.be( - CaseStatuses['in-progress'] - ); - expect(signals.get(defaultSignalsIndex)?.get(signalID2)?._source?.signal.status).to.be( - CaseStatuses.closed - ); - }); - - it('404s when sub case id is invalid', async () => { - await supertest - .patch(`${SUB_CASES_PATCH_DEL_URL}`) - .set('kbn-xsrf', 'true') - .send({ - subCases: [ - { - id: 'fake-id', - version: 'blah', - status: CaseStatuses.open, - }, - ], - }) - .expect(404); - }); - - it('406s when updating invalid fields for a sub case', async () => { - const { newSubCaseInfo: caseInfo } = await createSubCase({ supertest, actionID }); - - await supertest - .patch(`${SUB_CASES_PATCH_DEL_URL}`) - .set('kbn-xsrf', 'true') - .send({ - subCases: [ - { - id: caseInfo.subCases![0].id, - version: caseInfo.subCases![0].version, - type: 'blah', - }, - ], - }) - .expect(406); - }); - }); - }); -} diff --git a/x-pack/test/case_api_integration/security_only/tests/common/user_actions/get_all_user_actions.ts b/x-pack/test/case_api_integration/security_only/tests/common/user_actions/get_all_user_actions.ts index 35ebb1a4bf7b19..d898d6b320c39a 100644 --- a/x-pack/test/case_api_integration/security_only/tests/common/user_actions/get_all_user_actions.ts +++ b/x-pack/test/case_api_integration/security_only/tests/common/user_actions/get_all_user_actions.ts @@ -8,24 +8,13 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; -import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; -import { - CaseResponse, - CaseStatuses, - CommentType, -} from '../../../../../../plugins/cases/common/api'; -import { - userActionPostResp, - postCaseReq, - postCommentUserReq, - getPostCaseRequest, -} from '../../../../common/lib/mock'; +import { CaseResponse, CaseStatuses } from '../../../../../../plugins/cases/common/api'; +import { getPostCaseRequest } from '../../../../common/lib/mock'; import { deleteAllCaseItems, createCase, updateCase, getCaseUserActions, - superUserSpace1Auth, } from '../../../../common/lib/utils'; import { globalRead, @@ -36,10 +25,10 @@ import { secOnlyRead, superUser, } from '../../../../common/lib/authentication/users'; +import { superUserNoSpaceAuth } from '../../../utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { - const supertest = getService('supertest'); const es = getService('es'); describe('get_all_user_actions', () => { @@ -47,349 +36,62 @@ export default ({ getService }: FtrProviderContext): void => { await deleteAllCaseItems(es); }); - it(`on new case, user action: 'create' should be called with actionFields: ['description', 'status', 'tags', 'title', 'connector', 'settings, owner]`, async () => { - const { body: postedCase } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send(postCaseReq) - .expect(200); - - const { body } = await supertest - .get(`${CASES_URL}/${postedCase.id}/user_actions`) - .set('kbn-xsrf', 'true') - .send() - .expect(200); + const supertestWithoutAuth = getService('supertestWithoutAuth'); - expect(body.length).to.eql(1); - - expect(body[0].action_field).to.eql([ - 'description', - 'status', - 'tags', - 'title', - 'connector', - 'settings', - 'owner', - ]); - expect(body[0].action).to.eql('create'); - expect(body[0].old_value).to.eql(null); - expect(JSON.parse(body[0].new_value)).to.eql(userActionPostResp); - }); + let caseInfo: CaseResponse; + beforeEach(async () => { + caseInfo = await createCase( + supertestWithoutAuth, + getPostCaseRequest(), + 200, + superUserNoSpaceAuth + ); - it(`on close case, user action: 'update' should be called with actionFields: ['status']`, async () => { - const { body: postedCase } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send(postCaseReq) - .expect(200); - - await supertest - .patch(CASES_URL) - .set('kbn-xsrf', 'true') - .send({ + await updateCase({ + supertest: supertestWithoutAuth, + params: { cases: [ { - id: postedCase.id, - version: postedCase.version, - status: 'closed', + id: caseInfo.id, + version: caseInfo.version, + status: CaseStatuses.closed, }, ], - }) - .expect(200); - - const { body } = await supertest - .get(`${CASES_URL}/${postedCase.id}/user_actions`) - .set('kbn-xsrf', 'true') - .send() - .expect(200); - - expect(body.length).to.eql(2); - expect(body[1].action_field).to.eql(['status']); - expect(body[1].action).to.eql('update'); - expect(body[1].old_value).to.eql('open'); - expect(body[1].new_value).to.eql('closed'); - }); - - it(`on update case connector, user action: 'update' should be called with actionFields: ['connector']`, async () => { - const { body: postedCase } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send(postCaseReq) - .expect(200); - - const newConnector = { - id: '123', - name: 'Connector', - type: '.jira', - fields: { issueType: 'Task', priority: 'High', parent: null }, - }; - - await supertest - .patch(CASES_URL) - .set('kbn-xsrf', 'true') - .send({ - cases: [ - { - id: postedCase.id, - version: postedCase.version, - connector: newConnector, - }, - ], - }) - .expect(200); - - const { body } = await supertest - .get(`${CASES_URL}/${postedCase.id}/user_actions`) - .set('kbn-xsrf', 'true') - .send() - .expect(200); - - expect(body.length).to.eql(2); - expect(body[1].action_field).to.eql(['connector']); - expect(body[1].action).to.eql('update'); - expect(JSON.parse(body[1].old_value)).to.eql({ - id: 'none', - name: 'none', - type: '.none', - fields: null, - }); - expect(JSON.parse(body[1].new_value)).to.eql({ - id: '123', - name: 'Connector', - type: '.jira', - fields: { issueType: 'Task', priority: 'High', parent: null }, + }, + auth: superUserNoSpaceAuth, }); }); - it(`on update tags, user action: 'add' and 'delete' should be called with actionFields: ['tags']`, async () => { - const { body: postedCase } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send(postCaseReq) - .expect(200); - - await supertest - .patch(CASES_URL) - .set('kbn-xsrf', 'true') - .send({ - cases: [ - { - id: postedCase.id, - version: postedCase.version, - tags: ['cool', 'neat'], - }, - ], - }) - .expect(200); - - const { body } = await supertest - .get(`${CASES_URL}/${postedCase.id}/user_actions`) - .set('kbn-xsrf', 'true') - .send() - .expect(200); - - expect(body.length).to.eql(3); - expect(body[1].action_field).to.eql(['tags']); - expect(body[1].action).to.eql('add'); - expect(body[1].old_value).to.eql(null); - expect(body[1].new_value).to.eql('cool, neat'); - expect(body[2].action_field).to.eql(['tags']); - expect(body[2].action).to.eql('delete'); - expect(body[2].old_value).to.eql(null); - expect(body[2].new_value).to.eql('defacement'); - }); - - it(`on update title, user action: 'update' should be called with actionFields: ['title']`, async () => { - const { body: postedCase } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send(postCaseReq) - .expect(200); - - const newTitle = 'Such a great title'; - await supertest - .patch(CASES_URL) - .set('kbn-xsrf', 'true') - .send({ - cases: [ - { - id: postedCase.id, - version: postedCase.version, - title: newTitle, - }, - ], - }) - .expect(200); - - const { body } = await supertest - .get(`${CASES_URL}/${postedCase.id}/user_actions`) - .set('kbn-xsrf', 'true') - .send() - .expect(200); - - expect(body.length).to.eql(2); - expect(body[1].action_field).to.eql(['title']); - expect(body[1].action).to.eql('update'); - expect(body[1].old_value).to.eql(postCaseReq.title); - expect(body[1].new_value).to.eql(newTitle); - }); - - it(`on update description, user action: 'update' should be called with actionFields: ['description']`, async () => { - const { body: postedCase } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send(postCaseReq) - .expect(200); - - const newDesc = 'Such a great description'; - await supertest - .patch(CASES_URL) - .set('kbn-xsrf', 'true') - .send({ - cases: [ - { - id: postedCase.id, - version: postedCase.version, - description: newDesc, - }, - ], - }) - .expect(200); - - const { body } = await supertest - .get(`${CASES_URL}/${postedCase.id}/user_actions`) - .set('kbn-xsrf', 'true') - .send() - .expect(200); - - expect(body.length).to.eql(2); - expect(body[1].action_field).to.eql(['description']); - expect(body[1].action).to.eql('update'); - expect(body[1].old_value).to.eql(postCaseReq.description); - expect(body[1].new_value).to.eql(newDesc); - }); - - it(`on new comment, user action: 'create' should be called with actionFields: ['comments']`, async () => { - const { body: postedCase } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send(postCaseReq) - .expect(200); - - await supertest - .post(`${CASES_URL}/${postedCase.id}/comments`) - .set('kbn-xsrf', 'true') - .send(postCommentUserReq) - .expect(200); - - const { body } = await supertest - .get(`${CASES_URL}/${postedCase.id}/user_actions`) - .set('kbn-xsrf', 'true') - .send() - .expect(200); + it('should get the user actions for a case when the user has the correct permissions', async () => { + for (const user of [globalRead, superUser, secOnly, secOnlyRead, obsSec, obsSecRead]) { + const userActions = await getCaseUserActions({ + supertest: supertestWithoutAuth, + caseID: caseInfo.id, + auth: { user, space: null }, + }); - expect(body.length).to.eql(2); - expect(body[1].action_field).to.eql(['comment']); - expect(body[1].action).to.eql('create'); - expect(body[1].old_value).to.eql(null); - expect(JSON.parse(body[1].new_value)).to.eql(postCommentUserReq); + expect(userActions.length).to.eql(2); + } }); - it(`on update comment, user action: 'update' should be called with actionFields: ['comments']`, async () => { - const { body: postedCase } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send(postCaseReq) - .expect(200); - - const { body: patchedCase } = await supertest - .post(`${CASES_URL}/${postedCase.id}/comments`) - .set('kbn-xsrf', 'true') - .send(postCommentUserReq) - .expect(200); - - const newComment = 'Well I decided to update my comment. So what? Deal with it.'; - await supertest - .patch(`${CASES_URL}/${postedCase.id}/comments`) - .set('kbn-xsrf', 'true') - .send({ - id: patchedCase.comments[0].id, - version: patchedCase.comments[0].version, - comment: newComment, - type: CommentType.user, - owner: 'securitySolutionFixture', - }) - .expect(200); - - const { body } = await supertest - .get(`${CASES_URL}/${postedCase.id}/user_actions`) - .set('kbn-xsrf', 'true') - .send() - .expect(200); - - expect(body.length).to.eql(3); - expect(body[2].action_field).to.eql(['comment']); - expect(body[2].action).to.eql('update'); - expect(JSON.parse(body[2].old_value)).to.eql(postCommentUserReq); - expect(JSON.parse(body[2].new_value)).to.eql({ - comment: newComment, - type: CommentType.user, - owner: 'securitySolutionFixture', + it(`should 403 when requesting the user actions of a case with user ${ + noKibanaPrivileges.username + } with role(s) ${noKibanaPrivileges.roles.join()}`, async () => { + await getCaseUserActions({ + supertest: supertestWithoutAuth, + caseID: caseInfo.id, + auth: { user: noKibanaPrivileges, space: null }, + expectedHttpCode: 403, }); }); - describe('rbac', () => { - const supertestWithoutAuth = getService('supertestWithoutAuth'); - - let caseInfo: CaseResponse; - beforeEach(async () => { - caseInfo = await createCase(supertestWithoutAuth, getPostCaseRequest(), 200, { - user: superUser, - space: 'space1', - }); - - await updateCase({ - supertest: supertestWithoutAuth, - params: { - cases: [ - { - id: caseInfo.id, - version: caseInfo.version, - status: CaseStatuses.closed, - }, - ], - }, - auth: superUserSpace1Auth, - }); - }); - - it('should get the user actions for a case when the user has the correct permissions', async () => { - for (const user of [globalRead, superUser, secOnly, secOnlyRead, obsSec, obsSecRead]) { - const userActions = await getCaseUserActions({ - supertest: supertestWithoutAuth, - caseID: caseInfo.id, - auth: { user, space: 'space1' }, - }); - - expect(userActions.length).to.eql(2); - } + it('should return a 404 when attempting to access a space', async () => { + await getCaseUserActions({ + supertest: supertestWithoutAuth, + caseID: caseInfo.id, + auth: { user: secOnly, space: 'space1' }, + expectedHttpCode: 404, }); - - for (const scenario of [ - { user: noKibanaPrivileges, space: 'space1' }, - { user: secOnly, space: 'space2' }, - ]) { - it(`should 403 when requesting the user actions of a case with user ${ - scenario.user.username - } with role(s) ${scenario.user.roles.join()} and space ${scenario.space}`, async () => { - await getCaseUserActions({ - supertest: supertestWithoutAuth, - caseID: caseInfo.id, - auth: { user: scenario.user, space: scenario.space }, - expectedHttpCode: 403, - }); - }); - } }); }); }; diff --git a/x-pack/test/case_api_integration/security_only/tests/trial/cases/push_case.ts b/x-pack/test/case_api_integration/security_only/tests/trial/cases/push_case.ts index 8a58c59718feb6..2a88f3153dcf51 100644 --- a/x-pack/test/case_api_integration/security_only/tests/trial/cases/push_case.ts +++ b/x-pack/test/case_api_integration/security_only/tests/trial/cases/push_case.ts @@ -5,35 +5,19 @@ * 2.0. */ -/* eslint-disable @typescript-eslint/naming-convention */ - -import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { ObjectRemover as ActionsRemover } from '../../../../../alerting_api_integration/common/lib'; +import { getPostCaseRequest } from '../../../../common/lib/mock'; import { - postCaseReq, - defaultUser, - postCommentUserReq, - getPostCaseRequest, -} from '../../../../common/lib/mock'; -import { - getConfigurationRequest, - createCase, pushCase, - createComment, - updateCase, - getCaseUserActions, - removeServerGeneratedPropertiesFromUserAction, deleteAllCaseItems, - superUserSpace1Auth, createCaseWithConnector, } from '../../../../common/lib/utils'; import { ExternalServiceSimulator, getExternalServiceSimulatorPath, } from '../../../../../alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin'; -import { CaseStatuses, CaseUserActionResponse } from '../../../../../../plugins/cases/common/api'; import { globalRead, noKibanaPrivileges, @@ -41,8 +25,8 @@ import { obsSecRead, secOnly, secOnlyRead, - superUser, } from '../../../../common/lib/authentication/users'; +import { secOnlyNoSpaceAuth } from '../../../utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -65,250 +49,73 @@ export default ({ getService }: FtrProviderContext): void => { await actionsRemover.removeAll(); }); - it('should push a case', async () => { - const { postedCase, connector } = await createCaseWithConnector({ - supertest, - servicenowSimulatorURL, - actionsRemover, - }); - const theCase = await pushCase({ - supertest, - caseId: postedCase.id, - connectorId: connector.id, - }); - - const { pushed_at, external_url, ...rest } = theCase.external_service!; - - expect(rest).to.eql({ - pushed_by: defaultUser, - connector_id: connector.id, - connector_name: connector.name, - external_id: '123', - external_title: 'INC01', - }); - - // external_url is of the form http://elastic:changeme@localhost:5620 which is different between various environments like Jekins - expect( - external_url.includes( - 'api/_actions-FTS-external-service-simulators/servicenow/nav_to.do?uri=incident.do?sys_id=123' - ) - ).to.equal(true); - }); - - it('pushes a comment appropriately', async () => { - const { postedCase, connector } = await createCaseWithConnector({ - supertest, - servicenowSimulatorURL, - actionsRemover, - }); - await createComment({ supertest, caseId: postedCase.id, params: postCommentUserReq }); - const theCase = await pushCase({ - supertest, - caseId: postedCase.id, - connectorId: connector.id, - }); - - expect(theCase.comments![0].pushed_by).to.eql(defaultUser); - }); - - it('should pushes a case and closes when closure_type: close-by-pushing', async () => { - const { postedCase, connector } = await createCaseWithConnector({ - configureReq: { - closure_type: 'close-by-pushing', - }, - supertest, - servicenowSimulatorURL, - actionsRemover, - }); - const theCase = await pushCase({ - supertest, - caseId: postedCase.id, - connectorId: connector.id, - }); - - expect(theCase.status).to.eql('closed'); - }); - - it('should create the correct user action', async () => { - const { postedCase, connector } = await createCaseWithConnector({ - supertest, - servicenowSimulatorURL, - actionsRemover, - }); - const pushedCase = await pushCase({ - supertest, - caseId: postedCase.id, - connectorId: connector.id, - }); - const userActions = await getCaseUserActions({ supertest, caseID: pushedCase.id }); - const pushUserAction = removeServerGeneratedPropertiesFromUserAction(userActions[1]); - - const { new_value, ...rest } = pushUserAction as CaseUserActionResponse; - const parsedNewValue = JSON.parse(new_value!); - - expect(rest).to.eql({ - action_field: ['pushed'], - action: 'push-to-service', - action_by: defaultUser, - old_value: null, - case_id: `${postedCase.id}`, - comment_id: null, - sub_case_id: '', - owner: 'securitySolutionFixture', - }); - - expect(parsedNewValue).to.eql({ - pushed_at: pushedCase.external_service!.pushed_at, - pushed_by: defaultUser, - connector_id: connector.id, - connector_name: connector.name, - external_id: '123', - external_title: 'INC01', - external_url: `${servicenowSimulatorURL}/nav_to.do?uri=incident.do?sys_id=123`, - }); - }); + const supertestWithoutAuth = getService('supertestWithoutAuth'); - // ENABLE_CASE_CONNECTOR: once the case connector feature is completed unskip these tests - it.skip('should push a collection case but not close it when closure_type: close-by-pushing', async () => { + it('should push a case that the user has permissions for', async () => { const { postedCase, connector } = await createCaseWithConnector({ supertest, servicenowSimulatorURL, actionsRemover, - configureReq: { - closure_type: 'close-by-pushing', - }, }); - const theCase = await pushCase({ - supertest, - caseId: postedCase.id, - connectorId: connector.id, - }); - expect(theCase.status).to.eql(CaseStatuses.open); - }); - - it('unhappy path - 404s when case does not exist', async () => { - await pushCase({ - supertest, - caseId: 'fake-id', - connectorId: 'fake-connector', - expectedHttpCode: 404, - }); - }); - - it('unhappy path - 404s when connector does not exist', async () => { - const postedCase = await createCase(supertest, { - ...postCaseReq, - connector: getConfigurationRequest().connector, - }); await pushCase({ - supertest, + supertest: supertestWithoutAuth, caseId: postedCase.id, - connectorId: 'fake-connector', - expectedHttpCode: 404, + connectorId: connector.id, + auth: secOnlyNoSpaceAuth, }); }); - it('unhappy path = 409s when case is closed', async () => { + it('should not push a case that the user does not have permissions for', async () => { const { postedCase, connector } = await createCaseWithConnector({ supertest, servicenowSimulatorURL, actionsRemover, - }); - await updateCase({ - supertest, - params: { - cases: [ - { - id: postedCase.id, - version: postedCase.version, - status: CaseStatuses.closed, - }, - ], - }, + createCaseReq: getPostCaseRequest({ owner: 'observabilityFixture' }), }); await pushCase({ - supertest, + supertest: supertestWithoutAuth, caseId: postedCase.id, connectorId: connector.id, - expectedHttpCode: 409, + auth: secOnlyNoSpaceAuth, + expectedHttpCode: 403, }); }); - describe('rbac', () => { - const supertestWithoutAuth = getService('supertestWithoutAuth'); - - it('should push a case that the user has permissions for', async () => { - const { postedCase, connector } = await createCaseWithConnector({ - supertest, - servicenowSimulatorURL, - actionsRemover, - auth: superUserSpace1Auth, - }); - - await pushCase({ - supertest: supertestWithoutAuth, - caseId: postedCase.id, - connectorId: connector.id, - auth: { user: secOnly, space: 'space1' }, - }); - }); - - it('should not push a case that the user does not have permissions for', async () => { + for (const user of [globalRead, secOnlyRead, obsOnlyRead, obsSecRead, noKibanaPrivileges]) { + it(`User ${ + user.username + } with role(s) ${user.roles.join()} - should NOT push a case`, async () => { const { postedCase, connector } = await createCaseWithConnector({ supertest, servicenowSimulatorURL, actionsRemover, - auth: superUserSpace1Auth, - createCaseReq: getPostCaseRequest({ owner: 'observabilityFixture' }), }); await pushCase({ supertest: supertestWithoutAuth, caseId: postedCase.id, connectorId: connector.id, - auth: { user: secOnly, space: 'space1' }, + auth: { user, space: null }, expectedHttpCode: 403, }); }); + } - for (const user of [globalRead, secOnlyRead, obsOnlyRead, obsSecRead, noKibanaPrivileges]) { - it(`User ${ - user.username - } with role(s) ${user.roles.join()} - should NOT push a case`, async () => { - const { postedCase, connector } = await createCaseWithConnector({ - supertest, - servicenowSimulatorURL, - actionsRemover, - auth: superUserSpace1Auth, - }); - - await pushCase({ - supertest: supertestWithoutAuth, - caseId: postedCase.id, - connectorId: connector.id, - auth: { user, space: 'space1' }, - expectedHttpCode: 403, - }); - }); - } - - it('should not push a case in a space that the user does not have permissions for', async () => { - const { postedCase, connector } = await createCaseWithConnector({ - supertest, - servicenowSimulatorURL, - actionsRemover, - auth: { user: superUser, space: 'space2' }, - }); + it('should return a 404 when attempting to access a space', async () => { + const { postedCase, connector } = await createCaseWithConnector({ + supertest, + servicenowSimulatorURL, + actionsRemover, + }); - await pushCase({ - supertest: supertestWithoutAuth, - caseId: postedCase.id, - connectorId: connector.id, - auth: { user: secOnly, space: 'space2' }, - expectedHttpCode: 403, - }); + await pushCase({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + connectorId: connector.id, + auth: { user: secOnly, space: 'space1' }, + expectedHttpCode: 404, }); }); }); diff --git a/x-pack/test/case_api_integration/security_only/tests/trial/cases/user_actions/get_all_user_actions.ts b/x-pack/test/case_api_integration/security_only/tests/trial/cases/user_actions/get_all_user_actions.ts deleted file mode 100644 index 3729b20f82b301..00000000000000 --- a/x-pack/test/case_api_integration/security_only/tests/trial/cases/user_actions/get_all_user_actions.ts +++ /dev/null @@ -1,115 +0,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 expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../../../../common/ftr_provider_context'; - -import { CASE_CONFIGURE_URL, CASES_URL } from '../../../../../../../plugins/cases/common/constants'; -import { defaultUser, postCaseReq } from '../../../../../common/lib/mock'; -import { - deleteCasesByESQuery, - deleteCasesUserActions, - deleteComments, - deleteConfiguration, - getConfigurationRequest, - getServiceNowConnector, -} from '../../../../../common/lib/utils'; - -import { ObjectRemover as ActionsRemover } from '../../../../../../alerting_api_integration/common/lib'; -import { - ExternalServiceSimulator, - getExternalServiceSimulatorPath, -} from '../../../../../../alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin'; - -// eslint-disable-next-line import/no-default-export -export default ({ getService }: FtrProviderContext): void => { - const supertest = getService('supertest'); - const es = getService('es'); - const actionsRemover = new ActionsRemover(supertest); - const kibanaServer = getService('kibanaServer'); - - describe('get_all_user_actions', () => { - let servicenowSimulatorURL: string = ''; - before(() => { - servicenowSimulatorURL = kibanaServer.resolveUrl( - getExternalServiceSimulatorPath(ExternalServiceSimulator.SERVICENOW) - ); - }); - afterEach(async () => { - await deleteCasesByESQuery(es); - await deleteComments(es); - await deleteConfiguration(es); - await deleteCasesUserActions(es); - await actionsRemover.removeAll(); - }); - - it(`on new push to service, user action: 'push-to-service' should be called with actionFields: ['pushed']`, async () => { - const { body: connector } = await supertest - .post('/api/actions/connector') - .set('kbn-xsrf', 'true') - .send({ - ...getServiceNowConnector(), - config: { apiUrl: servicenowSimulatorURL }, - }) - .expect(200); - - actionsRemover.add('default', connector.id, 'action', 'actions'); - - const { body: configure } = await supertest - .post(CASE_CONFIGURE_URL) - .set('kbn-xsrf', 'true') - .send( - getConfigurationRequest({ - id: connector.id, - name: connector.name, - type: connector.connector_type_id, - }) - ) - .expect(200); - - const { body: postedCase } = await supertest - .post(CASES_URL) - .set('kbn-xsrf', 'true') - .send({ - ...postCaseReq, - connector: getConfigurationRequest({ - id: connector.id, - name: connector.name, - type: connector.connector_type_id, - fields: { - urgency: '2', - impact: '2', - severity: '2', - category: 'software', - subcategory: 'os', - }, - }).connector, - }) - .expect(200); - - await supertest - .post(`${CASES_URL}/${postedCase.id}/connector/${connector.id}/_push`) - .set('kbn-xsrf', 'true') - .send({}) - .expect(200); - - const { body } = await supertest - .get(`${CASES_URL}/${postedCase.id}/user_actions`) - .set('kbn-xsrf', 'true') - .send() - .expect(200); - - expect(body.length).to.eql(2); - expect(body[1].action_field).to.eql(['pushed']); - expect(body[1].action).to.eql('push-to-service'); - expect(body[1].old_value).to.eql(null); - const newValue = JSON.parse(body[1].new_value); - expect(newValue.connector_id).to.eql(configure.connector.id); - expect(newValue.pushed_by).to.eql(defaultUser); - }); - }); -}; diff --git a/x-pack/test/case_api_integration/security_only/tests/trial/configure/get_configure.ts b/x-pack/test/case_api_integration/security_only/tests/trial/configure/get_configure.ts deleted file mode 100644 index ff8f1cff884aff..00000000000000 --- a/x-pack/test/case_api_integration/security_only/tests/trial/configure/get_configure.ts +++ /dev/null @@ -1,98 +0,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 expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; - -import { - ExternalServiceSimulator, - getExternalServiceSimulatorPath, -} from '../../../../../alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin'; - -import { ObjectRemover as ActionsRemover } from '../../../../../alerting_api_integration/common/lib'; -import { - getServiceNowConnector, - createConnector, - createConfiguration, - getConfiguration, - getConfigurationRequest, - removeServerGeneratedPropertiesFromSavedObject, - getConfigurationOutput, -} from '../../../../common/lib/utils'; -import { ConnectorTypes } from '../../../../../../plugins/cases/common/api'; - -// eslint-disable-next-line import/no-default-export -export default ({ getService }: FtrProviderContext): void => { - const supertest = getService('supertest'); - const actionsRemover = new ActionsRemover(supertest); - const kibanaServer = getService('kibanaServer'); - - describe('get_configure', () => { - let servicenowSimulatorURL: string = ''; - - before(() => { - servicenowSimulatorURL = kibanaServer.resolveUrl( - getExternalServiceSimulatorPath(ExternalServiceSimulator.SERVICENOW) - ); - }); - - afterEach(async () => { - await actionsRemover.removeAll(); - }); - - it('should return a configuration with mapping', async () => { - const connector = await createConnector({ - supertest, - req: { - ...getServiceNowConnector(), - config: { apiUrl: servicenowSimulatorURL }, - }, - }); - actionsRemover.add('default', connector.id, 'action', 'actions'); - - await createConfiguration( - supertest, - getConfigurationRequest({ - id: connector.id, - name: connector.name, - type: connector.connector_type_id as ConnectorTypes, - }) - ); - - const configuration = await getConfiguration({ supertest }); - - const data = removeServerGeneratedPropertiesFromSavedObject(configuration[0]); - expect(data).to.eql( - getConfigurationOutput(false, { - mappings: [ - { - action_type: 'overwrite', - source: 'title', - target: 'short_description', - }, - { - action_type: 'overwrite', - source: 'description', - target: 'description', - }, - { - action_type: 'append', - source: 'comments', - target: 'work_notes', - }, - ], - connector: { - id: connector.id, - name: connector.name, - type: connector.connector_type_id, - fields: null, - }, - }) - ); - }); - }); -}; diff --git a/x-pack/test/case_api_integration/security_only/tests/trial/configure/get_connectors.ts b/x-pack/test/case_api_integration/security_only/tests/trial/configure/get_connectors.ts deleted file mode 100644 index fb922f8d102434..00000000000000 --- a/x-pack/test/case_api_integration/security_only/tests/trial/configure/get_connectors.ts +++ /dev/null @@ -1,129 +0,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 expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; - -import { CASE_CONFIGURE_CONNECTORS_URL } from '../../../../../../plugins/cases/common/constants'; -import { ObjectRemover as ActionsRemover } from '../../../../../alerting_api_integration/common/lib'; -import { - getServiceNowConnector, - getJiraConnector, - getResilientConnector, - createConnector, - getServiceNowSIRConnector, -} from '../../../../common/lib/utils'; - -// eslint-disable-next-line import/no-default-export -export default ({ getService }: FtrProviderContext): void => { - const supertest = getService('supertest'); - const actionsRemover = new ActionsRemover(supertest); - - describe('get_connectors', () => { - afterEach(async () => { - await actionsRemover.removeAll(); - }); - - it('should return the correct connectors', async () => { - const { body: snConnector } = await supertest - .post('/api/actions/connector') - .set('kbn-xsrf', 'true') - .send(getServiceNowConnector()) - .expect(200); - - const { body: emailConnector } = await supertest - .post('/api/actions/connector') - .set('kbn-xsrf', 'true') - .send({ - name: 'An email action', - connector_type_id: '.email', - config: { - service: '__json', - from: 'bob@example.com', - }, - secrets: { - user: 'bob', - password: 'supersecret', - }, - }) - .expect(200); - - const { body: jiraConnector } = await supertest - .post('/api/actions/connector') - .set('kbn-xsrf', 'true') - .send(getJiraConnector()) - .expect(200); - - const { body: resilientConnector } = await supertest - .post('/api/actions/connector') - .set('kbn-xsrf', 'true') - .send(getResilientConnector()) - .expect(200); - - const sir = await createConnector({ supertest, req: getServiceNowSIRConnector() }); - - actionsRemover.add('default', sir.id, 'action', 'actions'); - actionsRemover.add('default', snConnector.id, 'action', 'actions'); - actionsRemover.add('default', emailConnector.id, 'action', 'actions'); - actionsRemover.add('default', jiraConnector.id, 'action', 'actions'); - actionsRemover.add('default', resilientConnector.id, 'action', 'actions'); - - const { body: connectors } = await supertest - .get(`${CASE_CONFIGURE_CONNECTORS_URL}/_find`) - .set('kbn-xsrf', 'true') - .send() - .expect(200); - - expect(connectors).to.eql([ - { - id: jiraConnector.id, - actionTypeId: '.jira', - name: 'Jira Connector', - config: { - apiUrl: 'http://some.non.existent.com', - projectKey: 'pkey', - }, - isPreconfigured: false, - isMissingSecrets: false, - referencedByCount: 0, - }, - { - id: resilientConnector.id, - actionTypeId: '.resilient', - name: 'Resilient Connector', - config: { - apiUrl: 'http://some.non.existent.com', - orgId: 'pkey', - }, - isPreconfigured: false, - isMissingSecrets: false, - referencedByCount: 0, - }, - { - id: snConnector.id, - actionTypeId: '.servicenow', - name: 'ServiceNow Connector', - config: { - apiUrl: 'http://some.non.existent.com', - }, - isPreconfigured: false, - isMissingSecrets: false, - referencedByCount: 0, - }, - { - id: sir.id, - actionTypeId: '.servicenow-sir', - name: 'ServiceNow Connector', - config: { apiUrl: 'http://some.non.existent.com' }, - isPreconfigured: false, - isMissingSecrets: false, - referencedByCount: 0, - }, - ]); - }); - }); -}; diff --git a/x-pack/test/case_api_integration/security_only/tests/trial/configure/index.ts b/x-pack/test/case_api_integration/security_only/tests/trial/configure/index.ts deleted file mode 100644 index 0c8c3931d15774..00000000000000 --- a/x-pack/test/case_api_integration/security_only/tests/trial/configure/index.ts +++ /dev/null @@ -1,18 +0,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 { FtrProviderContext } from '../../../../common/ftr_provider_context'; - -// eslint-disable-next-line import/no-default-export -export default ({ loadTestFile }: FtrProviderContext): void => { - describe('configuration tests', function () { - loadTestFile(require.resolve('./get_configure')); - loadTestFile(require.resolve('./get_connectors')); - loadTestFile(require.resolve('./patch_configure')); - loadTestFile(require.resolve('./post_configure')); - }); -}; diff --git a/x-pack/test/case_api_integration/security_only/tests/trial/configure/patch_configure.ts b/x-pack/test/case_api_integration/security_only/tests/trial/configure/patch_configure.ts deleted file mode 100644 index 789b68b19beb66..00000000000000 --- a/x-pack/test/case_api_integration/security_only/tests/trial/configure/patch_configure.ts +++ /dev/null @@ -1,168 +0,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 expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../../common/ftr_provider_context'; -import { ObjectRemover as ActionsRemover } from '../../../../../alerting_api_integration/common/lib'; -import { - ExternalServiceSimulator, - getExternalServiceSimulatorPath, -} from '../../../../../alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin'; - -import { - getConfigurationRequest, - removeServerGeneratedPropertiesFromSavedObject, - getConfigurationOutput, - deleteConfiguration, - createConfiguration, - updateConfiguration, - getServiceNowConnector, - createConnector, -} from '../../../../common/lib/utils'; -import { ConnectorTypes } from '../../../../../../plugins/cases/common/api'; - -// eslint-disable-next-line import/no-default-export -export default ({ getService }: FtrProviderContext): void => { - const supertest = getService('supertest'); - const es = getService('es'); - const kibanaServer = getService('kibanaServer'); - - describe('patch_configure', () => { - const actionsRemover = new ActionsRemover(supertest); - let servicenowSimulatorURL: string = ''; - - before(() => { - servicenowSimulatorURL = kibanaServer.resolveUrl( - getExternalServiceSimulatorPath(ExternalServiceSimulator.SERVICENOW) - ); - }); - - afterEach(async () => { - await deleteConfiguration(es); - await actionsRemover.removeAll(); - }); - - it('should patch a configuration connector and create mappings', async () => { - const connector = await createConnector({ - supertest, - req: { - ...getServiceNowConnector(), - config: { apiUrl: servicenowSimulatorURL }, - }, - }); - - actionsRemover.add('default', connector.id, 'action', 'actions'); - - // Configuration is created with no connector so the mappings are empty - const configuration = await createConfiguration(supertest); - - // the update request doesn't accept the owner field - const { owner, ...reqWithoutOwner } = getConfigurationRequest({ - id: connector.id, - name: connector.name, - type: connector.connector_type_id as ConnectorTypes, - fields: null, - }); - - const newConfiguration = await updateConfiguration(supertest, configuration.id, { - ...reqWithoutOwner, - version: configuration.version, - }); - - const data = removeServerGeneratedPropertiesFromSavedObject(newConfiguration); - expect(data).to.eql({ - ...getConfigurationOutput(true), - connector: { - id: connector.id, - name: connector.name, - type: connector.connector_type_id as ConnectorTypes, - fields: null, - }, - mappings: [ - { - action_type: 'overwrite', - source: 'title', - target: 'short_description', - }, - { - action_type: 'overwrite', - source: 'description', - target: 'description', - }, - { - action_type: 'append', - source: 'comments', - target: 'work_notes', - }, - ], - }); - }); - - it('should mappings when updating the connector', async () => { - const connector = await createConnector({ - supertest, - req: { - ...getServiceNowConnector(), - config: { apiUrl: servicenowSimulatorURL }, - }, - }); - - actionsRemover.add('default', connector.id, 'action', 'actions'); - - // Configuration is created with connector so the mappings are created - const configuration = await createConfiguration( - supertest, - getConfigurationRequest({ - id: connector.id, - name: connector.name, - type: connector.connector_type_id as ConnectorTypes, - }) - ); - - // the update request doesn't accept the owner field - const { owner, ...rest } = getConfigurationRequest({ - id: connector.id, - name: 'New name', - type: connector.connector_type_id as ConnectorTypes, - fields: null, - }); - - const newConfiguration = await updateConfiguration(supertest, configuration.id, { - ...rest, - version: configuration.version, - }); - - const data = removeServerGeneratedPropertiesFromSavedObject(newConfiguration); - expect(data).to.eql({ - ...getConfigurationOutput(true), - connector: { - id: connector.id, - name: 'New name', - type: connector.connector_type_id as ConnectorTypes, - fields: null, - }, - mappings: [ - { - action_type: 'overwrite', - source: 'title', - target: 'short_description', - }, - { - action_type: 'overwrite', - source: 'description', - target: 'description', - }, - { - action_type: 'append', - source: 'comments', - target: 'work_notes', - }, - ], - }); - }); - }); -}; diff --git a/x-pack/test/case_api_integration/security_only/tests/trial/configure/post_configure.ts b/x-pack/test/case_api_integration/security_only/tests/trial/configure/post_configure.ts deleted file mode 100644 index 96ffcf4bc3f5c5..00000000000000 --- a/x-pack/test/case_api_integration/security_only/tests/trial/configure/post_configure.ts +++ /dev/null @@ -1,98 +0,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 expect from '@kbn/expect'; -import { ConnectorTypes } from '../../../../../../plugins/cases/common/api'; -import { FtrProviderContext } from '../../../../common/ftr_provider_context'; -import { ObjectRemover as ActionsRemover } from '../../../../../alerting_api_integration/common/lib'; -import { - ExternalServiceSimulator, - getExternalServiceSimulatorPath, -} from '../../../../../alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin'; - -import { - getConfigurationRequest, - removeServerGeneratedPropertiesFromSavedObject, - getConfigurationOutput, - deleteConfiguration, - createConfiguration, - createConnector, - getServiceNowConnector, -} from '../../../../common/lib/utils'; - -// eslint-disable-next-line import/no-default-export -export default ({ getService }: FtrProviderContext): void => { - const supertest = getService('supertest'); - const es = getService('es'); - const kibanaServer = getService('kibanaServer'); - - describe('post_configure', () => { - const actionsRemover = new ActionsRemover(supertest); - let servicenowSimulatorURL: string = ''; - - before(() => { - servicenowSimulatorURL = kibanaServer.resolveUrl( - getExternalServiceSimulatorPath(ExternalServiceSimulator.SERVICENOW) - ); - }); - - afterEach(async () => { - await deleteConfiguration(es); - await actionsRemover.removeAll(); - }); - - it('should create a configuration with mapping', async () => { - const connector = await createConnector({ - supertest, - req: { - ...getServiceNowConnector(), - config: { apiUrl: servicenowSimulatorURL }, - }, - }); - - actionsRemover.add('default', connector.id, 'action', 'actions'); - - const postRes = await createConfiguration( - supertest, - getConfigurationRequest({ - id: connector.id, - name: connector.name, - type: connector.connector_type_id as ConnectorTypes, - }) - ); - - const data = removeServerGeneratedPropertiesFromSavedObject(postRes); - expect(data).to.eql( - getConfigurationOutput(false, { - mappings: [ - { - action_type: 'overwrite', - source: 'title', - target: 'short_description', - }, - { - action_type: 'overwrite', - source: 'description', - target: 'description', - }, - { - action_type: 'append', - source: 'comments', - target: 'work_notes', - }, - ], - connector: { - id: connector.id, - name: connector.name, - type: connector.connector_type_id, - fields: null, - }, - }) - ); - }); - }); -}; diff --git a/x-pack/test/case_api_integration/security_only/tests/trial/index.ts b/x-pack/test/case_api_integration/security_only/tests/trial/index.ts index 0768db843d0ed7..5333a82ccf2e6f 100644 --- a/x-pack/test/case_api_integration/security_only/tests/trial/index.ts +++ b/x-pack/test/case_api_integration/security_only/tests/trial/index.ts @@ -25,8 +25,6 @@ export default ({ loadTestFile, getService }: FtrProviderContext): void => { // Trial loadTestFile(require.resolve('./cases/push_case')); - loadTestFile(require.resolve('./cases/user_actions/get_all_user_actions')); - loadTestFile(require.resolve('./configure/index')); // Common loadTestFile(require.resolve('../common')); diff --git a/x-pack/test/case_api_integration/security_only/utils.ts b/x-pack/test/case_api_integration/security_only/utils.ts new file mode 100644 index 00000000000000..bba1914d3fb0d2 --- /dev/null +++ b/x-pack/test/case_api_integration/security_only/utils.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 { obsOnly, obsSec, secOnly } from '../common/lib/authentication/users'; +import { getAuthWithSuperUser } from '../common/lib/utils'; + +export const secOnlyNoSpaceAuth = { user: secOnly, space: null }; +export const obsOnlyNoSpaceAuth = { user: obsOnly, space: null }; +export const obsSecNoSpaceAuth = { user: obsSec, space: null }; +export const superUserNoSpaceAuth = getAuthWithSuperUser(null); From 1e7d2434ec366d8eb46f278c26b00e8da694c690 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Mon, 10 May 2021 16:31:33 -0400 Subject: [PATCH 09/12] Using helper objects --- .../tests/common/cases/delete_cases.ts | 2 +- .../tests/common/alerts/get_cases.ts | 54 +++++++++---------- .../tests/common/cases/delete_cases.ts | 30 ++++++----- .../tests/common/cases/find_cases.ts | 27 ++++------ .../tests/common/cases/get_case.ts | 32 ++++++----- .../tests/common/cases/patch_cases.ts | 42 +++++---------- .../tests/common/cases/post_case.ts | 7 +-- .../common/cases/reporters/get_reporters.ts | 17 +++--- .../tests/common/cases/status/get_status.ts | 17 +++--- .../tests/common/cases/tags/get_tags.ts | 17 +++--- .../tests/common/comments/find_comments.ts | 22 ++++---- 11 files changed, 124 insertions(+), 143 deletions(-) diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/delete_cases.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/delete_cases.ts index 03bcf0d538fe3b..bbb9624c4b14be 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/delete_cases.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/delete_cases.ts @@ -180,7 +180,7 @@ export default ({ getService }: FtrProviderContext): void => { ); await deleteCases({ - supertest, + supertest: supertestWithoutAuth, caseIDs: [postedCase.id], expectedHttpCode: 204, auth: { user: secOnly, space: 'space1' }, diff --git a/x-pack/test/case_api_integration/security_only/tests/common/alerts/get_cases.ts b/x-pack/test/case_api_integration/security_only/tests/common/alerts/get_cases.ts index 568786184cb84b..a55fe6ce13a4d5 100644 --- a/x-pack/test/case_api_integration/security_only/tests/common/alerts/get_cases.ts +++ b/x-pack/test/case_api_integration/security_only/tests/common/alerts/get_cases.ts @@ -18,15 +18,18 @@ import { import { globalRead, noKibanaPrivileges, - obsOnly, obsOnlyRead, obsSec, obsSecRead, - secOnly, secOnlyRead, superUser, } from '../../../../common/lib/authentication/users'; -import { secOnlyNoSpaceAuth } from '../../../utils'; +import { + obsOnlyNoSpaceAuth, + secOnlyNoSpaceAuth, + superUserNoSpaceAuth, + obsSecNoSpaceAuth, +} from '../../../utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -39,20 +42,16 @@ export default ({ getService }: FtrProviderContext): void => { }); const supertestWithoutAuth = getService('supertestWithoutAuth'); - const obsSecAuth = { user: obsSec, space: null }; it('should return the correct case IDs', async () => { - const secOnlyAuth = { user: secOnly, space: null }; - const obsOnlyAuth = { user: obsOnly, space: null }; - const [case1, case2, case3] = await Promise.all([ - createCase(supertestWithoutAuth, getPostCaseRequest(), 200, secOnlyAuth), - createCase(supertestWithoutAuth, getPostCaseRequest(), 200, secOnlyAuth), + createCase(supertestWithoutAuth, getPostCaseRequest(), 200, secOnlyNoSpaceAuth), + createCase(supertestWithoutAuth, getPostCaseRequest(), 200, secOnlyNoSpaceAuth), createCase( supertestWithoutAuth, getPostCaseRequest({ owner: 'observabilityFixture' }), 200, - obsOnlyAuth + obsOnlyNoSpaceAuth ), ]); @@ -61,19 +60,19 @@ export default ({ getService }: FtrProviderContext): void => { supertest: supertestWithoutAuth, caseId: case1.id, params: postCommentAlertReq, - auth: secOnlyAuth, + auth: secOnlyNoSpaceAuth, }), createComment({ supertest: supertestWithoutAuth, caseId: case2.id, params: postCommentAlertReq, - auth: secOnlyAuth, + auth: secOnlyNoSpaceAuth, }), createComment({ supertest: supertestWithoutAuth, caseId: case3.id, params: { ...postCommentAlertReq, owner: 'observabilityFixture' }, - auth: obsOnlyAuth, + auth: obsOnlyNoSpaceAuth, }), ]); @@ -121,7 +120,7 @@ export default ({ getService }: FtrProviderContext): void => { supertest: supertestWithoutAuth, caseId: caseInfo.id, params: postCommentAlertReq, - auth: { user: superUser, space: null }, + auth: superUserNoSpaceAuth, }); await getCaseIDsByAlert({ @@ -133,14 +132,13 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should return a 404 when attempting to access a space', async () => { - const auth = { user: obsSec, space: null }; const [case1, case2] = await Promise.all([ - createCase(supertestWithoutAuth, getPostCaseRequest(), 200, auth), + createCase(supertestWithoutAuth, getPostCaseRequest(), 200, obsSecNoSpaceAuth), createCase( supertestWithoutAuth, { ...getPostCaseRequest(), owner: 'observabilityFixture' }, 200, - auth + obsSecNoSpaceAuth ), ]); @@ -149,13 +147,13 @@ export default ({ getService }: FtrProviderContext): void => { supertest: supertestWithoutAuth, caseId: case1.id, params: postCommentAlertReq, - auth, + auth: obsSecNoSpaceAuth, }), createComment({ supertest: supertestWithoutAuth, caseId: case2.id, params: { ...postCommentAlertReq, owner: 'observabilityFixture' }, - auth, + auth: obsSecNoSpaceAuth, }), ]); @@ -170,12 +168,12 @@ export default ({ getService }: FtrProviderContext): void => { it('should respect the owner filter when have permissions', async () => { const [case1, case2] = await Promise.all([ - createCase(supertestWithoutAuth, getPostCaseRequest(), 200, obsSecAuth), + createCase(supertestWithoutAuth, getPostCaseRequest(), 200, obsSecNoSpaceAuth), createCase( supertestWithoutAuth, { ...getPostCaseRequest(), owner: 'observabilityFixture' }, 200, - obsSecAuth + obsSecNoSpaceAuth ), ]); @@ -184,20 +182,20 @@ export default ({ getService }: FtrProviderContext): void => { supertest: supertestWithoutAuth, caseId: case1.id, params: postCommentAlertReq, - auth: obsSecAuth, + auth: obsSecNoSpaceAuth, }), createComment({ supertest: supertestWithoutAuth, caseId: case2.id, params: { ...postCommentAlertReq, owner: 'observabilityFixture' }, - auth: obsSecAuth, + auth: obsSecNoSpaceAuth, }), ]); const res = await getCaseIDsByAlert({ supertest: supertestWithoutAuth, alertID: postCommentAlertReq.alertId as string, - auth: obsSecAuth, + auth: obsSecNoSpaceAuth, query: { owner: 'securitySolutionFixture' }, }); @@ -206,12 +204,12 @@ export default ({ getService }: FtrProviderContext): void => { it('should return the correct case IDs when the owner query parameter contains unprivileged values', async () => { const [case1, case2] = await Promise.all([ - createCase(supertestWithoutAuth, getPostCaseRequest(), 200, obsSecAuth), + createCase(supertestWithoutAuth, getPostCaseRequest(), 200, obsSecNoSpaceAuth), createCase( supertestWithoutAuth, { ...getPostCaseRequest(), owner: 'observabilityFixture' }, 200, - obsSecAuth + obsSecNoSpaceAuth ), ]); @@ -220,13 +218,13 @@ export default ({ getService }: FtrProviderContext): void => { supertest: supertestWithoutAuth, caseId: case1.id, params: postCommentAlertReq, - auth: obsSecAuth, + auth: obsSecNoSpaceAuth, }), createComment({ supertest: supertestWithoutAuth, caseId: case2.id, params: { ...postCommentAlertReq, owner: 'observabilityFixture' }, - auth: obsSecAuth, + auth: obsSecNoSpaceAuth, }), ]); diff --git a/x-pack/test/case_api_integration/security_only/tests/common/cases/delete_cases.ts b/x-pack/test/case_api_integration/security_only/tests/common/cases/delete_cases.ts index d60e1b011d9925..e08e3d6a18c4f6 100644 --- a/x-pack/test/case_api_integration/security_only/tests/common/cases/delete_cases.ts +++ b/x-pack/test/case_api_integration/security_only/tests/common/cases/delete_cases.ts @@ -23,14 +23,12 @@ import { obsOnlyRead, obsSecRead, noKibanaPrivileges, - superUser, } from '../../../../common/lib/authentication/users'; -import { obsOnlyNoSpaceAuth, secOnlyNoSpaceAuth } from '../../../utils'; +import { obsOnlyNoSpaceAuth, secOnlyNoSpaceAuth, superUserNoSpaceAuth } from '../../../utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { const supertestWithoutAuth = getService('supertestWithoutAuth'); - const supertest = getService('supertest'); const es = getService('es'); describe('delete_cases', () => { @@ -49,7 +47,7 @@ export default ({ getService }: FtrProviderContext): void => { ); await deleteCases({ - supertest, + supertest: supertestWithoutAuth, caseIDs: [postedCase.id], expectedHttpCode: 204, auth: secOnlyNoSpaceAuth, @@ -99,14 +97,14 @@ export default ({ getService }: FtrProviderContext): void => { supertest: supertestWithoutAuth, caseId: caseSec.id, expectedHttpCode: 200, - auth: { user: superUser, space: null }, + auth: superUserNoSpaceAuth, }); await getCase({ supertest: supertestWithoutAuth, caseId: caseObs.id, expectedHttpCode: 200, - auth: { user: superUser, space: null }, + auth: superUserNoSpaceAuth, }); }); @@ -114,10 +112,12 @@ export default ({ getService }: FtrProviderContext): void => { it(`User ${ user.username } with role(s) ${user.roles.join()} - should NOT delete a case`, async () => { - const postedCase = await createCase(supertestWithoutAuth, getPostCaseRequest(), 200, { - user: superUser, - space: null, - }); + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest(), + 200, + superUserNoSpaceAuth + ); await deleteCases({ supertest: supertestWithoutAuth, @@ -129,10 +129,12 @@ export default ({ getService }: FtrProviderContext): void => { } it('should return a 404 when attempting to access a space', async () => { - const postedCase = await createCase(supertestWithoutAuth, getPostCaseRequest(), 200, { - user: superUser, - space: null, - }); + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest(), + 200, + superUserNoSpaceAuth + ); await deleteCases({ supertest: supertestWithoutAuth, diff --git a/x-pack/test/case_api_integration/security_only/tests/common/cases/find_cases.ts b/x-pack/test/case_api_integration/security_only/tests/common/cases/find_cases.ts index abac7ffb0addd1..b05f7e7fe22831 100644 --- a/x-pack/test/case_api_integration/security_only/tests/common/cases/find_cases.ts +++ b/x-pack/test/case_api_integration/security_only/tests/common/cases/find_cases.ts @@ -24,7 +24,12 @@ import { globalRead, obsSecRead, } from '../../../../common/lib/authentication/users'; -import { obsOnlyNoSpaceAuth, obsSecNoSpaceAuth, secOnlyNoSpaceAuth } from '../../../utils'; +import { + obsOnlyNoSpaceAuth, + obsSecNoSpaceAuth, + secOnlyNoSpaceAuth, + superUserNoSpaceAuth, +} from '../../../utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -89,10 +94,7 @@ export default ({ getService }: FtrProviderContext): void => { it(`User ${ noKibanaPrivileges.username } with role(s) ${noKibanaPrivileges.roles.join()} - should NOT read a case`, async () => { - await createCase(supertestWithoutAuth, getPostCaseRequest(), 200, { - user: superUser, - space: null, - }); + await createCase(supertestWithoutAuth, getPostCaseRequest(), 200, superUserNoSpaceAuth); await findCases({ supertest: supertestWithoutAuth, @@ -105,10 +107,7 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should return a 404 when attempting to access a space', async () => { - await createCase(supertestWithoutAuth, getPostCaseRequest(), 200, { - user: superUser, - space: null, - }); + await createCase(supertestWithoutAuth, getPostCaseRequest(), 200, superUserNoSpaceAuth); await findCases({ supertest: supertestWithoutAuth, @@ -120,19 +119,13 @@ export default ({ getService }: FtrProviderContext): void => { it('should return the correct cases when trying to exploit RBAC through the search query parameter', async () => { await Promise.all([ // super user creates a case with owner securitySolutionFixture - createCase(supertestWithoutAuth, getPostCaseRequest(), 200, { - user: superUser, - space: null, - }), + createCase(supertestWithoutAuth, getPostCaseRequest(), 200, superUserNoSpaceAuth), // super user creates a case with owner observabilityFixture createCase( supertestWithoutAuth, getPostCaseRequest({ owner: 'observabilityFixture' }), 200, - { - user: superUser, - space: null, - } + superUserNoSpaceAuth ), ]); diff --git a/x-pack/test/case_api_integration/security_only/tests/common/cases/get_case.ts b/x-pack/test/case_api_integration/security_only/tests/common/cases/get_case.ts index 21f34714eb2342..6ca109d4428f7e 100644 --- a/x-pack/test/case_api_integration/security_only/tests/common/cases/get_case.ts +++ b/x-pack/test/case_api_integration/security_only/tests/common/cases/get_case.ts @@ -29,7 +29,7 @@ import { obsSec, } from '../../../../common/lib/authentication/users'; import { getUserInfo } from '../../../../common/lib/authentication'; -import { secOnlyNoSpaceAuth } from '../../../utils'; +import { secOnlyNoSpaceAuth, superUserNoSpaceAuth } from '../../../utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -42,10 +42,12 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should get a case', async () => { - const newCase = await createCase(supertestWithoutAuth, getPostCaseRequest(), 200, { - user: superUser, - space: null, - }); + const newCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest(), + 200, + superUserNoSpaceAuth + ); for (const user of [globalRead, superUser, secOnly, secOnlyRead, obsSec, obsSecRead]) { const theCase = await getCase({ @@ -99,10 +101,12 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should not get a case when the user does not have access to owner', async () => { - const newCase = await createCase(supertestWithoutAuth, getPostCaseRequest(), 200, { - user: superUser, - space: null, - }); + const newCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest(), + 200, + superUserNoSpaceAuth + ); for (const user of [noKibanaPrivileges, obsOnly, obsOnlyRead]) { await getCase({ @@ -115,10 +119,12 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should return a 404 when attempting to access a space', async () => { - const newCase = await createCase(supertestWithoutAuth, getPostCaseRequest(), 200, { - user: superUser, - space: null, - }); + const newCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest(), + 200, + superUserNoSpaceAuth + ); await getCase({ supertest: supertestWithoutAuth, diff --git a/x-pack/test/case_api_integration/security_only/tests/common/cases/patch_cases.ts b/x-pack/test/case_api_integration/security_only/tests/common/cases/patch_cases.ts index 8d80885dce4e05..1447de59f2780f 100644 --- a/x-pack/test/case_api_integration/security_only/tests/common/cases/patch_cases.ts +++ b/x-pack/test/case_api_integration/security_only/tests/common/cases/patch_cases.ts @@ -26,7 +26,7 @@ import { secOnlyRead, superUser, } from '../../../../common/lib/authentication/users'; -import { obsOnlyNoSpaceAuth, secOnlyNoSpaceAuth } from '../../../utils'; +import { obsOnlyNoSpaceAuth, secOnlyNoSpaceAuth, superUserNoSpaceAuth } from '../../../utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -67,18 +67,9 @@ export default ({ getService }: FtrProviderContext): void => { it('should update multiple cases when the user has the correct permissions', async () => { const [case1, case2, case3] = await Promise.all([ - createCase(supertestWithoutAuth, postCaseReq, 200, { - user: superUser, - space: null, - }), - createCase(supertestWithoutAuth, postCaseReq, 200, { - user: superUser, - space: null, - }), - createCase(supertestWithoutAuth, postCaseReq, 200, { - user: superUser, - space: null, - }), + createCase(supertestWithoutAuth, postCaseReq, 200, superUserNoSpaceAuth), + createCase(supertestWithoutAuth, postCaseReq, 200, superUserNoSpaceAuth), + createCase(supertestWithoutAuth, postCaseReq, 200, superUserNoSpaceAuth), ]); const patchedCases = await updateCase({ @@ -140,28 +131,19 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getPostCaseRequest({ owner: 'observabilityFixture' }), 200, - { - user: superUser, - space: null, - } + superUserNoSpaceAuth ), createCase( supertestWithoutAuth, getPostCaseRequest({ owner: 'observabilityFixture' }), 200, - { - user: superUser, - space: null, - } + superUserNoSpaceAuth ), createCase( supertestWithoutAuth, getPostCaseRequest({ owner: 'observabilityFixture' }), 200, - { - user: superUser, - space: null, - } + superUserNoSpaceAuth ), ]); @@ -202,10 +184,12 @@ export default ({ getService }: FtrProviderContext): void => { it(`User ${ user.username } with role(s) ${user.roles.join()} - should NOT update a case`, async () => { - const postedCase = await createCase(supertestWithoutAuth, getPostCaseRequest(), 200, { - user: superUser, - space: null, - }); + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest(), + 200, + superUserNoSpaceAuth + ); await updateCase({ supertest: supertestWithoutAuth, diff --git a/x-pack/test/case_api_integration/security_only/tests/common/cases/post_case.ts b/x-pack/test/case_api_integration/security_only/tests/common/cases/post_case.ts index 7f718aad6094bc..7556b768ca6202 100644 --- a/x-pack/test/case_api_integration/security_only/tests/common/cases/post_case.ts +++ b/x-pack/test/case_api_integration/security_only/tests/common/cases/post_case.ts @@ -18,7 +18,7 @@ import { noKibanaPrivileges, } from '../../../../common/lib/authentication/users'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; -import { secOnlyNoSpaceAuth } from '../../../utils'; +import { secOnlyNoSpaceAuth, superUserNoSpaceAuth } from '../../../utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -57,10 +57,7 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getPostCaseRequest({ owner: 'securitySolutionFixture' }), 403, - { - user, - space: null, - } + superUserNoSpaceAuth ); }); } diff --git a/x-pack/test/case_api_integration/security_only/tests/common/cases/reporters/get_reporters.ts b/x-pack/test/case_api_integration/security_only/tests/common/cases/reporters/get_reporters.ts index 2f9bb62837192a..443a9cbdae14cd 100644 --- a/x-pack/test/case_api_integration/security_only/tests/common/cases/reporters/get_reporters.ts +++ b/x-pack/test/case_api_integration/security_only/tests/common/cases/reporters/get_reporters.ts @@ -22,7 +22,12 @@ import { obsSec, } from '../../../../../common/lib/authentication/users'; import { getUserInfo } from '../../../../../common/lib/authentication'; -import { secOnlyNoSpaceAuth, obsOnlyNoSpaceAuth } from '../../../../utils'; +import { + secOnlyNoSpaceAuth, + obsOnlyNoSpaceAuth, + superUserNoSpaceAuth, + obsSecNoSpaceAuth, +} from '../../../../utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -82,10 +87,7 @@ export default ({ getService }: FtrProviderContext): void => { noKibanaPrivileges.username } with role(s) ${noKibanaPrivileges.roles.join()} - should NOT get all reporters`, async () => { // super user creates a case at the appropriate space - await createCase(supertestWithoutAuth, getPostCaseRequest(), 200, { - user: superUser, - space: null, - }); + await createCase(supertestWithoutAuth, getPostCaseRequest(), 200, superUserNoSpaceAuth); // user should not be able to get all reporters at the appropriate space await getReporters({ @@ -121,10 +123,7 @@ export default ({ getService }: FtrProviderContext): void => { const reporters = await getReporters({ supertest: supertestWithoutAuth, - auth: { - user: obsSec, - space: null, - }, + auth: obsSecNoSpaceAuth, query: { owner: 'securitySolutionFixture' }, }); diff --git a/x-pack/test/case_api_integration/security_only/tests/common/cases/status/get_status.ts b/x-pack/test/case_api_integration/security_only/tests/common/cases/status/get_status.ts index 6c11bf5d39ee8f..639a074345a257 100644 --- a/x-pack/test/case_api_integration/security_only/tests/common/cases/status/get_status.ts +++ b/x-pack/test/case_api_integration/security_only/tests/common/cases/status/get_status.ts @@ -15,7 +15,6 @@ import { updateCase, getAllCasesStatuses, deleteAllCaseItems, - getAuthWithSuperUser, } from '../../../../../common/lib/utils'; import { globalRead, @@ -26,6 +25,7 @@ import { secOnlyRead, superUser, } from '../../../../../common/lib/authentication/users'; +import { superUserNoSpaceAuth } from '../../../../utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -37,7 +37,6 @@ export default ({ getService }: FtrProviderContext): void => { }); const supertestWithoutAuth = getService('supertestWithoutAuth'); - const superUserAuth = getAuthWithSuperUser(null); it('should return the correct status stats', async () => { /** @@ -47,19 +46,19 @@ export default ({ getService }: FtrProviderContext): void => { * open: 1, in-prog: 1 */ const [inProgressSec, closedSec, , inProgressObs] = await Promise.all([ - createCase(supertestWithoutAuth, getPostCaseRequest(), 200, superUserAuth), - createCase(supertestWithoutAuth, getPostCaseRequest(), 200, superUserAuth), + createCase(supertestWithoutAuth, getPostCaseRequest(), 200, superUserNoSpaceAuth), + createCase(supertestWithoutAuth, getPostCaseRequest(), 200, superUserNoSpaceAuth), createCase( supertestWithoutAuth, getPostCaseRequest({ owner: 'observabilityFixture' }), 200, - superUserAuth + superUserNoSpaceAuth ), createCase( supertestWithoutAuth, getPostCaseRequest({ owner: 'observabilityFixture' }), 200, - superUserAuth + superUserNoSpaceAuth ), ]); @@ -84,7 +83,7 @@ export default ({ getService }: FtrProviderContext): void => { }, ], }, - auth: superUserAuth, + auth: superUserNoSpaceAuth, }); for (const scenario of [ @@ -110,7 +109,7 @@ export default ({ getService }: FtrProviderContext): void => { it(`should return a 403 when retrieving the statuses when the user ${ noKibanaPrivileges.username } with role(s) ${noKibanaPrivileges.roles.join()}`, async () => { - await createCase(supertestWithoutAuth, getPostCaseRequest(), 200, superUserAuth); + await createCase(supertestWithoutAuth, getPostCaseRequest(), 200, superUserNoSpaceAuth); await getAllCasesStatuses({ supertest: supertestWithoutAuth, @@ -120,7 +119,7 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should return a 404 when attempting to access a space', async () => { - await createCase(supertestWithoutAuth, getPostCaseRequest(), 200, superUserAuth); + await createCase(supertestWithoutAuth, getPostCaseRequest(), 200, superUserNoSpaceAuth); await getAllCasesStatuses({ supertest: supertestWithoutAuth, diff --git a/x-pack/test/case_api_integration/security_only/tests/common/cases/tags/get_tags.ts b/x-pack/test/case_api_integration/security_only/tests/common/cases/tags/get_tags.ts index 8e225dff71eade..c29811e5aaf826 100644 --- a/x-pack/test/case_api_integration/security_only/tests/common/cases/tags/get_tags.ts +++ b/x-pack/test/case_api_integration/security_only/tests/common/cases/tags/get_tags.ts @@ -19,7 +19,12 @@ import { obsSecRead, noKibanaPrivileges, } from '../../../../../common/lib/authentication/users'; -import { secOnlyNoSpaceAuth, obsOnlyNoSpaceAuth, obsSecNoSpaceAuth } from '../../../../utils'; +import { + secOnlyNoSpaceAuth, + obsOnlyNoSpaceAuth, + obsSecNoSpaceAuth, + superUserNoSpaceAuth, +} from '../../../../utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -83,10 +88,7 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getPostCaseRequest({ owner: 'securitySolutionFixture', tags: ['sec'] }), 200, - { - user: superUser, - space: null, - } + superUserNoSpaceAuth ); // user should not be able to get all tags at the appropriate space @@ -103,10 +105,7 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getPostCaseRequest({ owner: 'securitySolutionFixture', tags: ['sec'] }), 200, - { - user: superUser, - space: null, - } + superUserNoSpaceAuth ); await getTags({ diff --git a/x-pack/test/case_api_integration/security_only/tests/common/comments/find_comments.ts b/x-pack/test/case_api_integration/security_only/tests/common/comments/find_comments.ts index fb5c40b049f018..7980b141697eec 100644 --- a/x-pack/test/case_api_integration/security_only/tests/common/comments/find_comments.ts +++ b/x-pack/test/case_api_integration/security_only/tests/common/comments/find_comments.ts @@ -150,10 +150,12 @@ export default ({ getService }: FtrProviderContext): void => { noKibanaPrivileges.username } with role(s) ${noKibanaPrivileges.roles.join()} - should NOT read a comment`, async () => { // super user creates a case and comment in the appropriate space - const caseInfo = await createCase(supertestWithoutAuth, getPostCaseRequest(), 200, { - user: superUser, - space: null, - }); + const caseInfo = await createCase( + supertestWithoutAuth, + getPostCaseRequest(), + 200, + superUserNoSpaceAuth + ); await createComment({ supertest: supertestWithoutAuth, @@ -170,14 +172,16 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should return a 404 when attempting to access a space', async () => { - const caseInfo = await createCase(supertestWithoutAuth, getPostCaseRequest(), 200, { - user: superUser, - space: null, - }); + const caseInfo = await createCase( + supertestWithoutAuth, + getPostCaseRequest(), + 200, + superUserNoSpaceAuth + ); await createComment({ supertest: supertestWithoutAuth, - auth: { user: superUser, space: null }, + auth: superUserNoSpaceAuth, params: { ...postCommentUserReq, owner: 'securitySolutionFixture' }, caseId: caseInfo.id, }); From 8fbd84266dbdd67631a26cb7a71752ee10a1a2cf Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Mon, 10 May 2021 16:38:01 -0400 Subject: [PATCH 10/12] Fixing type error for null space --- .../case_api_integration/common/lib/utils.ts | 7 ++++++ .../tests/trial/configure/get_connectors.ts | 22 ++++++++++--------- .../tests/trial/configure/patch_configure.ts | 6 +++-- .../tests/trial/configure/post_configure.ts | 4 +++- 4 files changed, 26 insertions(+), 13 deletions(-) diff --git a/x-pack/test/case_api_integration/common/lib/utils.ts b/x-pack/test/case_api_integration/common/lib/utils.ts index daa6de208abae8..855cf513f16d56 100644 --- a/x-pack/test/case_api_integration/common/lib/utils.ts +++ b/x-pack/test/case_api_integration/common/lib/utils.ts @@ -552,6 +552,13 @@ export function getAuthWithSuperUser( return { user: superUser, space }; } +/** + * Converts the space into the appropriate string for use by the actions remover utility object. + */ +export function getActionsSpace(space: string | null) { + return space ?? 'default'; +} + export const getSpaceUrlPrefix = (spaceId: string | undefined | null) => { return spaceId && spaceId !== 'default' ? `/s/${spaceId}` : ``; }; diff --git a/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/get_connectors.ts b/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/get_connectors.ts index 66759a4dcb39ad..0301fa3a930cba 100644 --- a/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/get_connectors.ts +++ b/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/get_connectors.ts @@ -17,6 +17,7 @@ import { getServiceNowSIRConnector, getAuthWithSuperUser, getCaseConnectors, + getActionsSpace, } from '../../../../common/lib/utils'; // eslint-disable-next-line import/no-default-export @@ -24,6 +25,7 @@ export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); const actionsRemover = new ActionsRemover(supertest); const authSpace1 = getAuthWithSuperUser(); + const space = getActionsSpace(authSpace1.space); describe('get_connectors', () => { afterEach(async () => { @@ -68,11 +70,11 @@ export default ({ getService }: FtrProviderContext): void => { auth: authSpace1, }); - actionsRemover.add(authSpace1.space, sir.id, 'action', 'actions'); - actionsRemover.add(authSpace1.space, snConnector.id, 'action', 'actions'); - actionsRemover.add(authSpace1.space, emailConnector.id, 'action', 'actions'); - actionsRemover.add(authSpace1.space, jiraConnector.id, 'action', 'actions'); - actionsRemover.add(authSpace1.space, resilientConnector.id, 'action', 'actions'); + actionsRemover.add(space, sir.id, 'action', 'actions'); + actionsRemover.add(space, snConnector.id, 'action', 'actions'); + actionsRemover.add(space, emailConnector.id, 'action', 'actions'); + actionsRemover.add(space, jiraConnector.id, 'action', 'actions'); + actionsRemover.add(space, resilientConnector.id, 'action', 'actions'); const connectors = await getCaseConnectors({ supertest, auth: authSpace1 }); @@ -162,11 +164,11 @@ export default ({ getService }: FtrProviderContext): void => { auth: authSpace1, }); - actionsRemover.add(authSpace1.space, sir.id, 'action', 'actions'); - actionsRemover.add(authSpace1.space, snConnector.id, 'action', 'actions'); - actionsRemover.add(authSpace1.space, emailConnector.id, 'action', 'actions'); - actionsRemover.add(authSpace1.space, jiraConnector.id, 'action', 'actions'); - actionsRemover.add(authSpace1.space, resilientConnector.id, 'action', 'actions'); + actionsRemover.add(space, sir.id, 'action', 'actions'); + actionsRemover.add(space, snConnector.id, 'action', 'actions'); + actionsRemover.add(space, emailConnector.id, 'action', 'actions'); + actionsRemover.add(space, jiraConnector.id, 'action', 'actions'); + actionsRemover.add(space, resilientConnector.id, 'action', 'actions'); const connectors = await getCaseConnectors({ supertest, diff --git a/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/patch_configure.ts b/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/patch_configure.ts index 5015b9c638617b..14d0debe2ac178 100644 --- a/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/patch_configure.ts +++ b/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/patch_configure.ts @@ -23,6 +23,7 @@ import { getServiceNowConnector, createConnector, getAuthWithSuperUser, + getActionsSpace, } from '../../../../common/lib/utils'; import { ConnectorTypes } from '../../../../../../plugins/cases/common/api'; import { nullUser } from '../../../../common/lib/mock'; @@ -33,6 +34,7 @@ export default ({ getService }: FtrProviderContext): void => { const es = getService('es'); const kibanaServer = getService('kibanaServer'); const authSpace1 = getAuthWithSuperUser(); + const space = getActionsSpace(authSpace1.space); describe('patch_configure', () => { const actionsRemover = new ActionsRemover(supertest); @@ -59,7 +61,7 @@ export default ({ getService }: FtrProviderContext): void => { auth: authSpace1, }); - actionsRemover.add(authSpace1.space, connector.id, 'action', 'actions'); + actionsRemover.add(space, connector.id, 'action', 'actions'); // Configuration is created with no connector so the mappings are empty const configuration = await createConfiguration( @@ -129,7 +131,7 @@ export default ({ getService }: FtrProviderContext): void => { auth: authSpace1, }); - actionsRemover.add(authSpace1.space, connector.id, 'action', 'actions'); + actionsRemover.add(space, connector.id, 'action', 'actions'); // Configuration is created with no connector so the mappings are empty const configuration = await createConfiguration( diff --git a/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/post_configure.ts b/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/post_configure.ts index d67ca29229dd10..7c5035193d4656 100644 --- a/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/post_configure.ts +++ b/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/post_configure.ts @@ -23,6 +23,7 @@ import { createConnector, getServiceNowConnector, getAuthWithSuperUser, + getActionsSpace, } from '../../../../common/lib/utils'; import { nullUser } from '../../../../common/lib/mock'; @@ -32,6 +33,7 @@ export default ({ getService }: FtrProviderContext): void => { const es = getService('es'); const kibanaServer = getService('kibanaServer'); const authSpace1 = getAuthWithSuperUser(); + const space = getActionsSpace(authSpace1.space); describe('post_configure', () => { const actionsRemover = new ActionsRemover(supertest); @@ -58,7 +60,7 @@ export default ({ getService }: FtrProviderContext): void => { auth: authSpace1, }); - actionsRemover.add(authSpace1.space, connector.id, 'action', 'actions'); + actionsRemover.add(space, connector.id, 'action', 'actions'); const postRes = await createConfiguration( supertest, From d81559186ef93a86ae88fc9c447127cc5e7fdc76 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Mon, 10 May 2021 16:43:12 -0400 Subject: [PATCH 11/12] Renaming utility variables --- .../tests/common/alerts/get_cases.ts | 50 +++++++++---------- .../tests/common/cases/delete_cases.ts | 28 ++++++----- .../tests/common/cases/find_cases.ts | 34 ++++++------- .../tests/common/cases/get_case.ts | 14 +++--- .../tests/common/cases/patch_cases.ts | 32 ++++++------ .../tests/common/cases/post_case.ts | 8 +-- .../common/cases/reporters/get_reporters.ts | 26 +++++----- .../tests/common/cases/status/get_status.ts | 16 +++--- .../tests/common/cases/tags/get_tags.ts | 28 +++++------ .../tests/common/comments/delete_comment.ts | 24 ++++----- .../tests/common/comments/find_comments.ts | 28 ++++++----- .../tests/common/comments/get_all_comments.ts | 16 +++--- .../tests/common/comments/get_comment.ts | 14 +++--- .../tests/common/comments/patch_comment.ts | 26 ++++++---- .../tests/common/comments/post_comment.ts | 18 ++++--- .../tests/common/configure/get_configure.ts | 28 +++++------ .../tests/common/configure/patch_configure.ts | 14 +++--- .../tests/common/configure/post_configure.ts | 12 ++--- .../user_actions/get_all_user_actions.ts | 6 +-- .../tests/trial/cases/push_case.ts | 6 +-- .../security_only/utils.ts | 8 +-- 21 files changed, 228 insertions(+), 208 deletions(-) diff --git a/x-pack/test/case_api_integration/security_only/tests/common/alerts/get_cases.ts b/x-pack/test/case_api_integration/security_only/tests/common/alerts/get_cases.ts index a55fe6ce13a4d5..0bb7568778f310 100644 --- a/x-pack/test/case_api_integration/security_only/tests/common/alerts/get_cases.ts +++ b/x-pack/test/case_api_integration/security_only/tests/common/alerts/get_cases.ts @@ -25,10 +25,10 @@ import { superUser, } from '../../../../common/lib/authentication/users'; import { - obsOnlyNoSpaceAuth, - secOnlyNoSpaceAuth, - superUserNoSpaceAuth, - obsSecNoSpaceAuth, + obsOnlyDefaultSpaceAuth, + secOnlyDefaultSpaceAuth, + superUserDefaultSpaceAuth, + obsSecDefaultSpaceAuth, } from '../../../utils'; // eslint-disable-next-line import/no-default-export @@ -45,13 +45,13 @@ export default ({ getService }: FtrProviderContext): void => { it('should return the correct case IDs', async () => { const [case1, case2, case3] = await Promise.all([ - createCase(supertestWithoutAuth, getPostCaseRequest(), 200, secOnlyNoSpaceAuth), - createCase(supertestWithoutAuth, getPostCaseRequest(), 200, secOnlyNoSpaceAuth), + createCase(supertestWithoutAuth, getPostCaseRequest(), 200, secOnlyDefaultSpaceAuth), + createCase(supertestWithoutAuth, getPostCaseRequest(), 200, secOnlyDefaultSpaceAuth), createCase( supertestWithoutAuth, getPostCaseRequest({ owner: 'observabilityFixture' }), 200, - obsOnlyNoSpaceAuth + obsOnlyDefaultSpaceAuth ), ]); @@ -60,19 +60,19 @@ export default ({ getService }: FtrProviderContext): void => { supertest: supertestWithoutAuth, caseId: case1.id, params: postCommentAlertReq, - auth: secOnlyNoSpaceAuth, + auth: secOnlyDefaultSpaceAuth, }), createComment({ supertest: supertestWithoutAuth, caseId: case2.id, params: postCommentAlertReq, - auth: secOnlyNoSpaceAuth, + auth: secOnlyDefaultSpaceAuth, }), createComment({ supertest: supertestWithoutAuth, caseId: case3.id, params: { ...postCommentAlertReq, owner: 'observabilityFixture' }, - auth: obsOnlyNoSpaceAuth, + auth: obsOnlyDefaultSpaceAuth, }), ]); @@ -120,7 +120,7 @@ export default ({ getService }: FtrProviderContext): void => { supertest: supertestWithoutAuth, caseId: caseInfo.id, params: postCommentAlertReq, - auth: superUserNoSpaceAuth, + auth: superUserDefaultSpaceAuth, }); await getCaseIDsByAlert({ @@ -133,12 +133,12 @@ export default ({ getService }: FtrProviderContext): void => { it('should return a 404 when attempting to access a space', async () => { const [case1, case2] = await Promise.all([ - createCase(supertestWithoutAuth, getPostCaseRequest(), 200, obsSecNoSpaceAuth), + createCase(supertestWithoutAuth, getPostCaseRequest(), 200, obsSecDefaultSpaceAuth), createCase( supertestWithoutAuth, { ...getPostCaseRequest(), owner: 'observabilityFixture' }, 200, - obsSecNoSpaceAuth + obsSecDefaultSpaceAuth ), ]); @@ -147,13 +147,13 @@ export default ({ getService }: FtrProviderContext): void => { supertest: supertestWithoutAuth, caseId: case1.id, params: postCommentAlertReq, - auth: obsSecNoSpaceAuth, + auth: obsSecDefaultSpaceAuth, }), createComment({ supertest: supertestWithoutAuth, caseId: case2.id, params: { ...postCommentAlertReq, owner: 'observabilityFixture' }, - auth: obsSecNoSpaceAuth, + auth: obsSecDefaultSpaceAuth, }), ]); @@ -168,12 +168,12 @@ export default ({ getService }: FtrProviderContext): void => { it('should respect the owner filter when have permissions', async () => { const [case1, case2] = await Promise.all([ - createCase(supertestWithoutAuth, getPostCaseRequest(), 200, obsSecNoSpaceAuth), + createCase(supertestWithoutAuth, getPostCaseRequest(), 200, obsSecDefaultSpaceAuth), createCase( supertestWithoutAuth, { ...getPostCaseRequest(), owner: 'observabilityFixture' }, 200, - obsSecNoSpaceAuth + obsSecDefaultSpaceAuth ), ]); @@ -182,20 +182,20 @@ export default ({ getService }: FtrProviderContext): void => { supertest: supertestWithoutAuth, caseId: case1.id, params: postCommentAlertReq, - auth: obsSecNoSpaceAuth, + auth: obsSecDefaultSpaceAuth, }), createComment({ supertest: supertestWithoutAuth, caseId: case2.id, params: { ...postCommentAlertReq, owner: 'observabilityFixture' }, - auth: obsSecNoSpaceAuth, + auth: obsSecDefaultSpaceAuth, }), ]); const res = await getCaseIDsByAlert({ supertest: supertestWithoutAuth, alertID: postCommentAlertReq.alertId as string, - auth: obsSecNoSpaceAuth, + auth: obsSecDefaultSpaceAuth, query: { owner: 'securitySolutionFixture' }, }); @@ -204,12 +204,12 @@ export default ({ getService }: FtrProviderContext): void => { it('should return the correct case IDs when the owner query parameter contains unprivileged values', async () => { const [case1, case2] = await Promise.all([ - createCase(supertestWithoutAuth, getPostCaseRequest(), 200, obsSecNoSpaceAuth), + createCase(supertestWithoutAuth, getPostCaseRequest(), 200, obsSecDefaultSpaceAuth), createCase( supertestWithoutAuth, { ...getPostCaseRequest(), owner: 'observabilityFixture' }, 200, - obsSecNoSpaceAuth + obsSecDefaultSpaceAuth ), ]); @@ -218,20 +218,20 @@ export default ({ getService }: FtrProviderContext): void => { supertest: supertestWithoutAuth, caseId: case1.id, params: postCommentAlertReq, - auth: obsSecNoSpaceAuth, + auth: obsSecDefaultSpaceAuth, }), createComment({ supertest: supertestWithoutAuth, caseId: case2.id, params: { ...postCommentAlertReq, owner: 'observabilityFixture' }, - auth: obsSecNoSpaceAuth, + auth: obsSecDefaultSpaceAuth, }), ]); const res = await getCaseIDsByAlert({ supertest: supertestWithoutAuth, alertID: postCommentAlertReq.alertId as string, - auth: secOnlyNoSpaceAuth, + auth: secOnlyDefaultSpaceAuth, // The secOnly user does not have permissions for observability cases, so it should only return the security solution one query: { owner: ['securitySolutionFixture', 'observabilityFixture'] }, }); diff --git a/x-pack/test/case_api_integration/security_only/tests/common/cases/delete_cases.ts b/x-pack/test/case_api_integration/security_only/tests/common/cases/delete_cases.ts index e08e3d6a18c4f6..87c6af5849fcf6 100644 --- a/x-pack/test/case_api_integration/security_only/tests/common/cases/delete_cases.ts +++ b/x-pack/test/case_api_integration/security_only/tests/common/cases/delete_cases.ts @@ -24,7 +24,11 @@ import { obsSecRead, noKibanaPrivileges, } from '../../../../common/lib/authentication/users'; -import { obsOnlyNoSpaceAuth, secOnlyNoSpaceAuth, superUserNoSpaceAuth } from '../../../utils'; +import { + obsOnlyDefaultSpaceAuth, + secOnlyDefaultSpaceAuth, + superUserDefaultSpaceAuth, +} from '../../../utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -43,14 +47,14 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getPostCaseRequest(), 200, - secOnlyNoSpaceAuth + secOnlyDefaultSpaceAuth ); await deleteCases({ supertest: supertestWithoutAuth, caseIDs: [postedCase.id], expectedHttpCode: 204, - auth: secOnlyNoSpaceAuth, + auth: secOnlyDefaultSpaceAuth, }); }); @@ -59,14 +63,14 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getPostCaseRequest(), 200, - secOnlyNoSpaceAuth + secOnlyDefaultSpaceAuth ); await deleteCases({ supertest: supertestWithoutAuth, caseIDs: [postedCase.id], expectedHttpCode: 403, - auth: obsOnlyNoSpaceAuth, + auth: obsOnlyDefaultSpaceAuth, }); }); @@ -75,21 +79,21 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getPostCaseRequest(), 200, - secOnlyNoSpaceAuth + secOnlyDefaultSpaceAuth ); const caseObs = await createCase( supertestWithoutAuth, getPostCaseRequest({ owner: 'observabilityFixture' }), 200, - obsOnlyNoSpaceAuth + obsOnlyDefaultSpaceAuth ); await deleteCases({ supertest: supertestWithoutAuth, caseIDs: [caseSec.id, caseObs.id], expectedHttpCode: 403, - auth: obsOnlyNoSpaceAuth, + auth: obsOnlyDefaultSpaceAuth, }); // Cases should have not been deleted. @@ -97,14 +101,14 @@ export default ({ getService }: FtrProviderContext): void => { supertest: supertestWithoutAuth, caseId: caseSec.id, expectedHttpCode: 200, - auth: superUserNoSpaceAuth, + auth: superUserDefaultSpaceAuth, }); await getCase({ supertest: supertestWithoutAuth, caseId: caseObs.id, expectedHttpCode: 200, - auth: superUserNoSpaceAuth, + auth: superUserDefaultSpaceAuth, }); }); @@ -116,7 +120,7 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getPostCaseRequest(), 200, - superUserNoSpaceAuth + superUserDefaultSpaceAuth ); await deleteCases({ @@ -133,7 +137,7 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getPostCaseRequest(), 200, - superUserNoSpaceAuth + superUserDefaultSpaceAuth ); await deleteCases({ diff --git a/x-pack/test/case_api_integration/security_only/tests/common/cases/find_cases.ts b/x-pack/test/case_api_integration/security_only/tests/common/cases/find_cases.ts index b05f7e7fe22831..60beca5cbe5ce1 100644 --- a/x-pack/test/case_api_integration/security_only/tests/common/cases/find_cases.ts +++ b/x-pack/test/case_api_integration/security_only/tests/common/cases/find_cases.ts @@ -25,10 +25,10 @@ import { obsSecRead, } from '../../../../common/lib/authentication/users'; import { - obsOnlyNoSpaceAuth, - obsSecNoSpaceAuth, - secOnlyNoSpaceAuth, - superUserNoSpaceAuth, + obsOnlyDefaultSpaceAuth, + obsSecDefaultSpaceAuth, + secOnlyDefaultSpaceAuth, + superUserDefaultSpaceAuth, } from '../../../utils'; // eslint-disable-next-line import/no-default-export @@ -49,14 +49,14 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getPostCaseRequest({ owner: 'securitySolutionFixture' }), 200, - secOnlyNoSpaceAuth + secOnlyDefaultSpaceAuth ), // Create case owned by the observability user createCase( supertestWithoutAuth, getPostCaseRequest({ owner: 'observabilityFixture' }), 200, - obsOnlyNoSpaceAuth + obsOnlyDefaultSpaceAuth ), ]); @@ -94,7 +94,7 @@ export default ({ getService }: FtrProviderContext): void => { it(`User ${ noKibanaPrivileges.username } with role(s) ${noKibanaPrivileges.roles.join()} - should NOT read a case`, async () => { - await createCase(supertestWithoutAuth, getPostCaseRequest(), 200, superUserNoSpaceAuth); + await createCase(supertestWithoutAuth, getPostCaseRequest(), 200, superUserDefaultSpaceAuth); await findCases({ supertest: supertestWithoutAuth, @@ -107,7 +107,7 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should return a 404 when attempting to access a space', async () => { - await createCase(supertestWithoutAuth, getPostCaseRequest(), 200, superUserNoSpaceAuth); + await createCase(supertestWithoutAuth, getPostCaseRequest(), 200, superUserDefaultSpaceAuth); await findCases({ supertest: supertestWithoutAuth, @@ -119,13 +119,13 @@ export default ({ getService }: FtrProviderContext): void => { it('should return the correct cases when trying to exploit RBAC through the search query parameter', async () => { await Promise.all([ // super user creates a case with owner securitySolutionFixture - createCase(supertestWithoutAuth, getPostCaseRequest(), 200, superUserNoSpaceAuth), + createCase(supertestWithoutAuth, getPostCaseRequest(), 200, superUserDefaultSpaceAuth), // super user creates a case with owner observabilityFixture createCase( supertestWithoutAuth, getPostCaseRequest({ owner: 'observabilityFixture' }), 200, - superUserNoSpaceAuth + superUserDefaultSpaceAuth ), ]); @@ -135,7 +135,7 @@ export default ({ getService }: FtrProviderContext): void => { search: 'securitySolutionFixture observabilityFixture', searchFields: 'owner', }, - auth: secOnlyNoSpaceAuth, + auth: secOnlyDefaultSpaceAuth, }); ensureSavedObjectIsAuthorized(res.cases, 1, ['securitySolutionFixture']); @@ -183,13 +183,13 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getPostCaseRequest({ owner: 'securitySolutionFixture' }), 200, - obsSecNoSpaceAuth + obsSecDefaultSpaceAuth ), createCase( supertestWithoutAuth, getPostCaseRequest({ owner: 'observabilityFixture' }), 200, - obsOnlyNoSpaceAuth + obsOnlyDefaultSpaceAuth ), ]); @@ -199,7 +199,7 @@ export default ({ getService }: FtrProviderContext): void => { owner: 'securitySolutionFixture', searchFields: 'owner', }, - auth: obsSecNoSpaceAuth, + auth: obsSecDefaultSpaceAuth, }); ensureSavedObjectIsAuthorized(res.cases, 1, ['securitySolutionFixture']); @@ -211,13 +211,13 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getPostCaseRequest({ owner: 'securitySolutionFixture' }), 200, - obsSecNoSpaceAuth + obsSecDefaultSpaceAuth ), createCase( supertestWithoutAuth, getPostCaseRequest({ owner: 'observabilityFixture' }), 200, - obsSecNoSpaceAuth + obsSecDefaultSpaceAuth ), ]); @@ -227,7 +227,7 @@ export default ({ getService }: FtrProviderContext): void => { query: { owner: ['securitySolutionFixture', 'observabilityFixture'], }, - auth: secOnlyNoSpaceAuth, + auth: secOnlyDefaultSpaceAuth, }); // Only security solution cases are being returned diff --git a/x-pack/test/case_api_integration/security_only/tests/common/cases/get_case.ts b/x-pack/test/case_api_integration/security_only/tests/common/cases/get_case.ts index 6ca109d4428f7e..09fc994f4ece2b 100644 --- a/x-pack/test/case_api_integration/security_only/tests/common/cases/get_case.ts +++ b/x-pack/test/case_api_integration/security_only/tests/common/cases/get_case.ts @@ -29,7 +29,7 @@ import { obsSec, } from '../../../../common/lib/authentication/users'; import { getUserInfo } from '../../../../common/lib/authentication'; -import { secOnlyNoSpaceAuth, superUserNoSpaceAuth } from '../../../utils'; +import { secOnlyDefaultSpaceAuth, superUserDefaultSpaceAuth } from '../../../utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -46,7 +46,7 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getPostCaseRequest(), 200, - superUserNoSpaceAuth + superUserDefaultSpaceAuth ); for (const user of [globalRead, superUser, secOnly, secOnlyRead, obsSec, obsSecRead]) { @@ -65,7 +65,7 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getPostCaseRequest(), 200, - secOnlyNoSpaceAuth + secOnlyDefaultSpaceAuth ); await createComment({ @@ -73,14 +73,14 @@ export default ({ getService }: FtrProviderContext): void => { caseId: postedCase.id, params: postCommentUserReq, expectedHttpCode: 200, - auth: secOnlyNoSpaceAuth, + auth: secOnlyDefaultSpaceAuth, }); const theCase = await getCase({ supertest: supertestWithoutAuth, caseId: postedCase.id, includeComments: true, - auth: secOnlyNoSpaceAuth, + auth: secOnlyDefaultSpaceAuth, }); const comment = removeServerGeneratedPropertiesFromSavedObject( @@ -105,7 +105,7 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getPostCaseRequest(), 200, - superUserNoSpaceAuth + superUserDefaultSpaceAuth ); for (const user of [noKibanaPrivileges, obsOnly, obsOnlyRead]) { @@ -123,7 +123,7 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getPostCaseRequest(), 200, - superUserNoSpaceAuth + superUserDefaultSpaceAuth ); await getCase({ diff --git a/x-pack/test/case_api_integration/security_only/tests/common/cases/patch_cases.ts b/x-pack/test/case_api_integration/security_only/tests/common/cases/patch_cases.ts index 1447de59f2780f..eefca21fb6e425 100644 --- a/x-pack/test/case_api_integration/security_only/tests/common/cases/patch_cases.ts +++ b/x-pack/test/case_api_integration/security_only/tests/common/cases/patch_cases.ts @@ -26,7 +26,11 @@ import { secOnlyRead, superUser, } from '../../../../common/lib/authentication/users'; -import { obsOnlyNoSpaceAuth, secOnlyNoSpaceAuth, superUserNoSpaceAuth } from '../../../utils'; +import { + obsOnlyDefaultSpaceAuth, + secOnlyDefaultSpaceAuth, + superUserDefaultSpaceAuth, +} from '../../../utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -45,7 +49,7 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, postCaseReq, 200, - secOnlyNoSpaceAuth + secOnlyDefaultSpaceAuth ); const patchedCases = await updateCase({ @@ -59,7 +63,7 @@ export default ({ getService }: FtrProviderContext): void => { }, ], }, - auth: secOnlyNoSpaceAuth, + auth: secOnlyDefaultSpaceAuth, }); expect(patchedCases[0].owner).to.eql('securitySolutionFixture'); @@ -67,9 +71,9 @@ export default ({ getService }: FtrProviderContext): void => { it('should update multiple cases when the user has the correct permissions', async () => { const [case1, case2, case3] = await Promise.all([ - createCase(supertestWithoutAuth, postCaseReq, 200, superUserNoSpaceAuth), - createCase(supertestWithoutAuth, postCaseReq, 200, superUserNoSpaceAuth), - createCase(supertestWithoutAuth, postCaseReq, 200, superUserNoSpaceAuth), + createCase(supertestWithoutAuth, postCaseReq, 200, superUserDefaultSpaceAuth), + createCase(supertestWithoutAuth, postCaseReq, 200, superUserDefaultSpaceAuth), + createCase(supertestWithoutAuth, postCaseReq, 200, superUserDefaultSpaceAuth), ]); const patchedCases = await updateCase({ @@ -93,7 +97,7 @@ export default ({ getService }: FtrProviderContext): void => { }, ], }, - auth: secOnlyNoSpaceAuth, + auth: secOnlyDefaultSpaceAuth, }); expect(patchedCases[0].owner).to.eql('securitySolutionFixture'); @@ -106,7 +110,7 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getPostCaseRequest({ owner: 'observabilityFixture' }), 200, - obsOnlyNoSpaceAuth + obsOnlyDefaultSpaceAuth ); await updateCase({ @@ -120,7 +124,7 @@ export default ({ getService }: FtrProviderContext): void => { }, ], }, - auth: secOnlyNoSpaceAuth, + auth: secOnlyDefaultSpaceAuth, expectedHttpCode: 403, }); }); @@ -131,19 +135,19 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getPostCaseRequest({ owner: 'observabilityFixture' }), 200, - superUserNoSpaceAuth + superUserDefaultSpaceAuth ), createCase( supertestWithoutAuth, getPostCaseRequest({ owner: 'observabilityFixture' }), 200, - superUserNoSpaceAuth + superUserDefaultSpaceAuth ), createCase( supertestWithoutAuth, getPostCaseRequest({ owner: 'observabilityFixture' }), 200, - superUserNoSpaceAuth + superUserDefaultSpaceAuth ), ]); @@ -168,7 +172,7 @@ export default ({ getService }: FtrProviderContext): void => { }, ], }, - auth: secOnlyNoSpaceAuth, + auth: secOnlyDefaultSpaceAuth, expectedHttpCode: 403, }); @@ -188,7 +192,7 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getPostCaseRequest(), 200, - superUserNoSpaceAuth + superUserDefaultSpaceAuth ); await updateCase({ diff --git a/x-pack/test/case_api_integration/security_only/tests/common/cases/post_case.ts b/x-pack/test/case_api_integration/security_only/tests/common/cases/post_case.ts index 7556b768ca6202..9a046562e68db9 100644 --- a/x-pack/test/case_api_integration/security_only/tests/common/cases/post_case.ts +++ b/x-pack/test/case_api_integration/security_only/tests/common/cases/post_case.ts @@ -18,7 +18,7 @@ import { noKibanaPrivileges, } from '../../../../common/lib/authentication/users'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; -import { secOnlyNoSpaceAuth, superUserNoSpaceAuth } from '../../../utils'; +import { secOnlyDefaultSpaceAuth, superUserDefaultSpaceAuth } from '../../../utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -35,7 +35,7 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getPostCaseRequest({ owner: 'securitySolutionFixture' }), 200, - secOnlyNoSpaceAuth + secOnlyDefaultSpaceAuth ); expect(theCase.owner).to.eql('securitySolutionFixture'); }); @@ -45,7 +45,7 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getPostCaseRequest({ owner: 'observabilityFixture' }), 403, - secOnlyNoSpaceAuth + secOnlyDefaultSpaceAuth ); }); @@ -57,7 +57,7 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getPostCaseRequest({ owner: 'securitySolutionFixture' }), 403, - superUserNoSpaceAuth + superUserDefaultSpaceAuth ); }); } diff --git a/x-pack/test/case_api_integration/security_only/tests/common/cases/reporters/get_reporters.ts b/x-pack/test/case_api_integration/security_only/tests/common/cases/reporters/get_reporters.ts index 443a9cbdae14cd..e56a4d6c099e69 100644 --- a/x-pack/test/case_api_integration/security_only/tests/common/cases/reporters/get_reporters.ts +++ b/x-pack/test/case_api_integration/security_only/tests/common/cases/reporters/get_reporters.ts @@ -23,10 +23,10 @@ import { } from '../../../../../common/lib/authentication/users'; import { getUserInfo } from '../../../../../common/lib/authentication'; import { - secOnlyNoSpaceAuth, - obsOnlyNoSpaceAuth, - superUserNoSpaceAuth, - obsSecNoSpaceAuth, + secOnlyDefaultSpaceAuth, + obsOnlyDefaultSpaceAuth, + superUserDefaultSpaceAuth, + obsSecDefaultSpaceAuth, } from '../../../../utils'; // eslint-disable-next-line import/no-default-export @@ -44,14 +44,14 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getPostCaseRequest({ owner: 'securitySolutionFixture' }), 200, - secOnlyNoSpaceAuth + secOnlyDefaultSpaceAuth ); await createCase( supertestWithoutAuth, getPostCaseRequest({ owner: 'observabilityFixture' }), 200, - obsOnlyNoSpaceAuth + obsOnlyDefaultSpaceAuth ); for (const scenario of [ @@ -87,7 +87,7 @@ export default ({ getService }: FtrProviderContext): void => { noKibanaPrivileges.username } with role(s) ${noKibanaPrivileges.roles.join()} - should NOT get all reporters`, async () => { // super user creates a case at the appropriate space - await createCase(supertestWithoutAuth, getPostCaseRequest(), 200, superUserNoSpaceAuth); + await createCase(supertestWithoutAuth, getPostCaseRequest(), 200, superUserDefaultSpaceAuth); // user should not be able to get all reporters at the appropriate space await getReporters({ @@ -112,18 +112,18 @@ export default ({ getService }: FtrProviderContext): void => { it('should respect the owner filter when having permissions', async () => { await Promise.all([ - createCase(supertestWithoutAuth, getPostCaseRequest(), 200, secOnlyNoSpaceAuth), + createCase(supertestWithoutAuth, getPostCaseRequest(), 200, secOnlyDefaultSpaceAuth), createCase( supertestWithoutAuth, getPostCaseRequest({ owner: 'observabilityFixture' }), 200, - obsOnlyNoSpaceAuth + obsOnlyDefaultSpaceAuth ), ]); const reporters = await getReporters({ supertest: supertestWithoutAuth, - auth: obsSecNoSpaceAuth, + auth: obsSecDefaultSpaceAuth, query: { owner: 'securitySolutionFixture' }, }); @@ -132,19 +132,19 @@ export default ({ getService }: FtrProviderContext): void => { it('should return the correct cases when trying to exploit RBAC through the owner query parameter', async () => { await Promise.all([ - createCase(supertestWithoutAuth, getPostCaseRequest(), 200, secOnlyNoSpaceAuth), + createCase(supertestWithoutAuth, getPostCaseRequest(), 200, secOnlyDefaultSpaceAuth), createCase( supertestWithoutAuth, getPostCaseRequest({ owner: 'observabilityFixture' }), 200, - obsOnlyNoSpaceAuth + obsOnlyDefaultSpaceAuth ), ]); // User with permissions only to security solution request reporters from observability const reporters = await getReporters({ supertest: supertestWithoutAuth, - auth: secOnlyNoSpaceAuth, + auth: secOnlyDefaultSpaceAuth, query: { owner: ['securitySolutionFixture', 'observabilityFixture'] }, }); diff --git a/x-pack/test/case_api_integration/security_only/tests/common/cases/status/get_status.ts b/x-pack/test/case_api_integration/security_only/tests/common/cases/status/get_status.ts index 639a074345a257..9bba570111861a 100644 --- a/x-pack/test/case_api_integration/security_only/tests/common/cases/status/get_status.ts +++ b/x-pack/test/case_api_integration/security_only/tests/common/cases/status/get_status.ts @@ -25,7 +25,7 @@ import { secOnlyRead, superUser, } from '../../../../../common/lib/authentication/users'; -import { superUserNoSpaceAuth } from '../../../../utils'; +import { superUserDefaultSpaceAuth } from '../../../../utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -46,19 +46,19 @@ export default ({ getService }: FtrProviderContext): void => { * open: 1, in-prog: 1 */ const [inProgressSec, closedSec, , inProgressObs] = await Promise.all([ - createCase(supertestWithoutAuth, getPostCaseRequest(), 200, superUserNoSpaceAuth), - createCase(supertestWithoutAuth, getPostCaseRequest(), 200, superUserNoSpaceAuth), + createCase(supertestWithoutAuth, getPostCaseRequest(), 200, superUserDefaultSpaceAuth), + createCase(supertestWithoutAuth, getPostCaseRequest(), 200, superUserDefaultSpaceAuth), createCase( supertestWithoutAuth, getPostCaseRequest({ owner: 'observabilityFixture' }), 200, - superUserNoSpaceAuth + superUserDefaultSpaceAuth ), createCase( supertestWithoutAuth, getPostCaseRequest({ owner: 'observabilityFixture' }), 200, - superUserNoSpaceAuth + superUserDefaultSpaceAuth ), ]); @@ -83,7 +83,7 @@ export default ({ getService }: FtrProviderContext): void => { }, ], }, - auth: superUserNoSpaceAuth, + auth: superUserDefaultSpaceAuth, }); for (const scenario of [ @@ -109,7 +109,7 @@ export default ({ getService }: FtrProviderContext): void => { it(`should return a 403 when retrieving the statuses when the user ${ noKibanaPrivileges.username } with role(s) ${noKibanaPrivileges.roles.join()}`, async () => { - await createCase(supertestWithoutAuth, getPostCaseRequest(), 200, superUserNoSpaceAuth); + await createCase(supertestWithoutAuth, getPostCaseRequest(), 200, superUserDefaultSpaceAuth); await getAllCasesStatuses({ supertest: supertestWithoutAuth, @@ -119,7 +119,7 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should return a 404 when attempting to access a space', async () => { - await createCase(supertestWithoutAuth, getPostCaseRequest(), 200, superUserNoSpaceAuth); + await createCase(supertestWithoutAuth, getPostCaseRequest(), 200, superUserDefaultSpaceAuth); await getAllCasesStatuses({ supertest: supertestWithoutAuth, diff --git a/x-pack/test/case_api_integration/security_only/tests/common/cases/tags/get_tags.ts b/x-pack/test/case_api_integration/security_only/tests/common/cases/tags/get_tags.ts index c29811e5aaf826..1d95c8f4e991de 100644 --- a/x-pack/test/case_api_integration/security_only/tests/common/cases/tags/get_tags.ts +++ b/x-pack/test/case_api_integration/security_only/tests/common/cases/tags/get_tags.ts @@ -20,10 +20,10 @@ import { noKibanaPrivileges, } from '../../../../../common/lib/authentication/users'; import { - secOnlyNoSpaceAuth, - obsOnlyNoSpaceAuth, - obsSecNoSpaceAuth, - superUserNoSpaceAuth, + secOnlyDefaultSpaceAuth, + obsOnlyDefaultSpaceAuth, + obsSecDefaultSpaceAuth, + superUserDefaultSpaceAuth, } from '../../../../utils'; // eslint-disable-next-line import/no-default-export @@ -41,14 +41,14 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getPostCaseRequest({ owner: 'securitySolutionFixture', tags: ['sec'] }), 200, - secOnlyNoSpaceAuth + secOnlyDefaultSpaceAuth ); await createCase( supertestWithoutAuth, getPostCaseRequest({ owner: 'observabilityFixture', tags: ['obs'] }), 200, - obsOnlyNoSpaceAuth + obsOnlyDefaultSpaceAuth ); for (const scenario of [ @@ -88,7 +88,7 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getPostCaseRequest({ owner: 'securitySolutionFixture', tags: ['sec'] }), 200, - superUserNoSpaceAuth + superUserDefaultSpaceAuth ); // user should not be able to get all tags at the appropriate space @@ -105,7 +105,7 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getPostCaseRequest({ owner: 'securitySolutionFixture', tags: ['sec'] }), 200, - superUserNoSpaceAuth + superUserDefaultSpaceAuth ); await getTags({ @@ -121,19 +121,19 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getPostCaseRequest({ owner: 'securitySolutionFixture', tags: ['sec'] }), 200, - obsSecNoSpaceAuth + obsSecDefaultSpaceAuth ), createCase( supertestWithoutAuth, getPostCaseRequest({ owner: 'observabilityFixture', tags: ['obs'] }), 200, - obsSecNoSpaceAuth + obsSecDefaultSpaceAuth ), ]); const tags = await getTags({ supertest: supertestWithoutAuth, - auth: obsSecNoSpaceAuth, + auth: obsSecDefaultSpaceAuth, query: { owner: 'securitySolutionFixture' }, }); @@ -146,20 +146,20 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getPostCaseRequest({ owner: 'securitySolutionFixture', tags: ['sec'] }), 200, - obsSecNoSpaceAuth + obsSecDefaultSpaceAuth ), createCase( supertestWithoutAuth, getPostCaseRequest({ owner: 'observabilityFixture', tags: ['obs'] }), 200, - obsSecNoSpaceAuth + obsSecDefaultSpaceAuth ), ]); // User with permissions only to security solution request tags from observability const tags = await getTags({ supertest: supertestWithoutAuth, - auth: secOnlyNoSpaceAuth, + auth: secOnlyDefaultSpaceAuth, query: { owner: ['securitySolutionFixture', 'observabilityFixture'] }, }); diff --git a/x-pack/test/case_api_integration/security_only/tests/common/comments/delete_comment.ts b/x-pack/test/case_api_integration/security_only/tests/common/comments/delete_comment.ts index d807dfa8b70470..7a122ae44f7934 100644 --- a/x-pack/test/case_api_integration/security_only/tests/common/comments/delete_comment.ts +++ b/x-pack/test/case_api_integration/security_only/tests/common/comments/delete_comment.ts @@ -27,7 +27,7 @@ import { secOnly, secOnlyRead, } from '../../../../common/lib/authentication/users'; -import { obsOnlyNoSpaceAuth, secOnlyNoSpaceAuth } from '../../../utils'; +import { obsOnlyDefaultSpaceAuth, secOnlyDefaultSpaceAuth } from '../../../utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -52,21 +52,21 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getPostCaseRequest(), 200, - secOnlyNoSpaceAuth + secOnlyDefaultSpaceAuth ); const commentResp = await createComment({ supertest: supertestWithoutAuth, caseId: secCase.id, params: postCommentUserReq, - auth: secOnlyNoSpaceAuth, + auth: secOnlyDefaultSpaceAuth, }); await deleteComment({ supertest: supertestWithoutAuth, caseId: secCase.id, commentId: commentResp.comments![0].id, - auth: secOnlyNoSpaceAuth, + auth: secOnlyDefaultSpaceAuth, }); }); @@ -75,27 +75,27 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getPostCaseRequest(), 200, - secOnlyNoSpaceAuth + secOnlyDefaultSpaceAuth ); await createComment({ supertest: supertestWithoutAuth, caseId: secCase.id, params: postCommentUserReq, - auth: secOnlyNoSpaceAuth, + auth: secOnlyDefaultSpaceAuth, }); await createComment({ supertest: supertestWithoutAuth, caseId: secCase.id, params: postCommentUserReq, - auth: secOnlyNoSpaceAuth, + auth: secOnlyDefaultSpaceAuth, }); await deleteAllComments({ supertest: supertestWithoutAuth, caseId: secCase.id, - auth: secOnlyNoSpaceAuth, + auth: secOnlyDefaultSpaceAuth, }); }); @@ -104,28 +104,28 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getPostCaseRequest(), 200, - secOnlyNoSpaceAuth + secOnlyDefaultSpaceAuth ); const commentResp = await createComment({ supertest: supertestWithoutAuth, caseId: secCase.id, params: postCommentUserReq, - auth: secOnlyNoSpaceAuth, + auth: secOnlyDefaultSpaceAuth, }); await deleteComment({ supertest: supertestWithoutAuth, caseId: secCase.id, commentId: commentResp.comments![0].id, - auth: obsOnlyNoSpaceAuth, + auth: obsOnlyDefaultSpaceAuth, expectedHttpCode: 403, }); await deleteAllComments({ supertest: supertestWithoutAuth, caseId: secCase.id, - auth: obsOnlyNoSpaceAuth, + auth: obsOnlyDefaultSpaceAuth, expectedHttpCode: 403, }); }); diff --git a/x-pack/test/case_api_integration/security_only/tests/common/comments/find_comments.ts b/x-pack/test/case_api_integration/security_only/tests/common/comments/find_comments.ts index 7980b141697eec..fab0ad86fcfa7c 100644 --- a/x-pack/test/case_api_integration/security_only/tests/common/comments/find_comments.ts +++ b/x-pack/test/case_api_integration/security_only/tests/common/comments/find_comments.ts @@ -35,7 +35,11 @@ import { globalRead, obsSecRead, } from '../../../../common/lib/authentication/users'; -import { obsOnlyNoSpaceAuth, secOnlyNoSpaceAuth, superUserNoSpaceAuth } from '../../../utils'; +import { + obsOnlyDefaultSpaceAuth, + secOnlyDefaultSpaceAuth, + superUserDefaultSpaceAuth, +} from '../../../utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -58,12 +62,12 @@ export default ({ getService }: FtrProviderContext): void => { it('should return the correct comments', async () => { const [secCase, obsCase] = await Promise.all([ // Create case owned by the security solution user - createCase(supertestWithoutAuth, getPostCaseRequest(), 200, secOnlyNoSpaceAuth), + createCase(supertestWithoutAuth, getPostCaseRequest(), 200, secOnlyDefaultSpaceAuth), createCase( supertestWithoutAuth, getPostCaseRequest({ owner: 'observabilityFixture' }), 200, - obsOnlyNoSpaceAuth + obsOnlyDefaultSpaceAuth ), // Create case owned by the observability user ]); @@ -73,13 +77,13 @@ export default ({ getService }: FtrProviderContext): void => { supertest: supertestWithoutAuth, caseId: secCase.id, params: postCommentUserReq, - auth: secOnlyNoSpaceAuth, + auth: secOnlyDefaultSpaceAuth, }), createComment({ supertest: supertestWithoutAuth, caseId: obsCase.id, params: { ...postCommentAlertReq, owner: 'observabilityFixture' }, - auth: obsOnlyNoSpaceAuth, + auth: obsOnlyDefaultSpaceAuth, }), ]); @@ -154,7 +158,7 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getPostCaseRequest(), 200, - superUserNoSpaceAuth + superUserDefaultSpaceAuth ); await createComment({ @@ -176,12 +180,12 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getPostCaseRequest(), 200, - superUserNoSpaceAuth + superUserDefaultSpaceAuth ); await createComment({ supertest: supertestWithoutAuth, - auth: superUserNoSpaceAuth, + auth: superUserDefaultSpaceAuth, params: { ...postCommentUserReq, owner: 'securitySolutionFixture' }, caseId: caseInfo.id, }); @@ -197,12 +201,12 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getPostCaseRequest({ owner: 'observabilityFixture' }), 200, - superUserNoSpaceAuth + superUserDefaultSpaceAuth ); await createComment({ supertest: supertestWithoutAuth, - auth: superUserNoSpaceAuth, + auth: superUserDefaultSpaceAuth, params: { ...postCommentUserReq, owner: 'observabilityFixture' }, caseId: obsCase.id, }); @@ -225,12 +229,12 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getPostCaseRequest({ owner: 'observabilityFixture' }), 200, - superUserNoSpaceAuth + superUserDefaultSpaceAuth ); await createComment({ supertest: supertestWithoutAuth, - auth: superUserNoSpaceAuth, + auth: superUserDefaultSpaceAuth, params: { ...postCommentUserReq, owner: 'observabilityFixture' }, caseId: obsCase.id, }); diff --git a/x-pack/test/case_api_integration/security_only/tests/common/comments/get_all_comments.ts b/x-pack/test/case_api_integration/security_only/tests/common/comments/get_all_comments.ts index 105f56d8421a2f..38e9f18868f4d7 100644 --- a/x-pack/test/case_api_integration/security_only/tests/common/comments/get_all_comments.ts +++ b/x-pack/test/case_api_integration/security_only/tests/common/comments/get_all_comments.ts @@ -26,7 +26,7 @@ import { secOnlyRead, superUser, } from '../../../../common/lib/authentication/users'; -import { superUserNoSpaceAuth } from '../../../utils'; +import { superUserDefaultSpaceAuth } from '../../../utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -44,21 +44,21 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getPostCaseRequest(), 200, - superUserNoSpaceAuth + superUserDefaultSpaceAuth ); await createComment({ supertest: supertestWithoutAuth, caseId: caseInfo.id, params: postCommentUserReq, - auth: superUserNoSpaceAuth, + auth: superUserDefaultSpaceAuth, }); await createComment({ supertest: supertestWithoutAuth, caseId: caseInfo.id, params: postCommentUserReq, - auth: superUserNoSpaceAuth, + auth: superUserDefaultSpaceAuth, }); for (const user of [globalRead, superUser, secOnly, secOnlyRead, obsSec, obsSecRead]) { @@ -77,14 +77,14 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getPostCaseRequest(), 200, - superUserNoSpaceAuth + superUserDefaultSpaceAuth ); await createComment({ supertest: supertestWithoutAuth, caseId: caseInfo.id, params: postCommentUserReq, - auth: superUserNoSpaceAuth, + auth: superUserDefaultSpaceAuth, }); for (const scenario of [ @@ -111,14 +111,14 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getPostCaseRequest(), 200, - superUserNoSpaceAuth + superUserDefaultSpaceAuth ); await createComment({ supertest: supertestWithoutAuth, caseId: caseInfo.id, params: postCommentUserReq, - auth: superUserNoSpaceAuth, + auth: superUserDefaultSpaceAuth, }); await getAllComments({ diff --git a/x-pack/test/case_api_integration/security_only/tests/common/comments/get_comment.ts b/x-pack/test/case_api_integration/security_only/tests/common/comments/get_comment.ts index 60a61136a97568..79bc3a01f2ee50 100644 --- a/x-pack/test/case_api_integration/security_only/tests/common/comments/get_comment.ts +++ b/x-pack/test/case_api_integration/security_only/tests/common/comments/get_comment.ts @@ -25,7 +25,7 @@ import { secOnlyRead, superUser, } from '../../../../common/lib/authentication/users'; -import { superUserNoSpaceAuth } from '../../../utils'; +import { superUserDefaultSpaceAuth } from '../../../utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -43,14 +43,14 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getPostCaseRequest(), 200, - superUserNoSpaceAuth + superUserDefaultSpaceAuth ); const caseWithComment = await createComment({ supertest: supertestWithoutAuth, caseId: caseInfo.id, params: postCommentUserReq, - auth: superUserNoSpaceAuth, + auth: superUserDefaultSpaceAuth, }); for (const user of [globalRead, superUser, secOnly, secOnlyRead, obsSec, obsSecRead]) { @@ -68,14 +68,14 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getPostCaseRequest(), 200, - superUserNoSpaceAuth + superUserDefaultSpaceAuth ); const caseWithComment = await createComment({ supertest: supertestWithoutAuth, caseId: caseInfo.id, params: postCommentUserReq, - auth: superUserNoSpaceAuth, + auth: superUserDefaultSpaceAuth, }); for (const user of [noKibanaPrivileges, obsOnly, obsOnlyRead]) { @@ -94,14 +94,14 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getPostCaseRequest(), 200, - superUserNoSpaceAuth + superUserDefaultSpaceAuth ); const caseWithComment = await createComment({ supertest: supertestWithoutAuth, caseId: caseInfo.id, params: postCommentUserReq, - auth: superUserNoSpaceAuth, + auth: superUserDefaultSpaceAuth, }); await getComment({ diff --git a/x-pack/test/case_api_integration/security_only/tests/common/comments/patch_comment.ts b/x-pack/test/case_api_integration/security_only/tests/common/comments/patch_comment.ts index 3e0bc17927c41d..028b5148b7f2a8 100644 --- a/x-pack/test/case_api_integration/security_only/tests/common/comments/patch_comment.ts +++ b/x-pack/test/case_api_integration/security_only/tests/common/comments/patch_comment.ts @@ -27,7 +27,11 @@ import { secOnly, secOnlyRead, } from '../../../../common/lib/authentication/users'; -import { obsOnlyNoSpaceAuth, secOnlyNoSpaceAuth, superUserNoSpaceAuth } from '../../../utils'; +import { + obsOnlyDefaultSpaceAuth, + secOnlyDefaultSpaceAuth, + superUserDefaultSpaceAuth, +} from '../../../utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -52,14 +56,14 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getPostCaseRequest(), 200, - superUserNoSpaceAuth + superUserDefaultSpaceAuth ); const patchedCase = await createComment({ supertest, caseId: postedCase.id, params: postCommentUserReq, - auth: superUserNoSpaceAuth, + auth: superUserDefaultSpaceAuth, }); const newComment = 'Well I decided to update my comment. So what? Deal with it.'; @@ -72,7 +76,7 @@ export default ({ getService }: FtrProviderContext): void => { version: patchedCase.comments![0].version, comment: newComment, }, - auth: secOnlyNoSpaceAuth, + auth: secOnlyDefaultSpaceAuth, }); const userComment = updatedCase.comments![0] as AttributesTypeUser; @@ -87,14 +91,14 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getPostCaseRequest(), 200, - superUserNoSpaceAuth + superUserDefaultSpaceAuth ); const patchedCase = await createComment({ supertest: supertestWithoutAuth, caseId: postedCase.id, params: postCommentUserReq, - auth: superUserNoSpaceAuth, + auth: superUserDefaultSpaceAuth, }); const newComment = 'Well I decided to update my comment. So what? Deal with it.'; @@ -107,7 +111,7 @@ export default ({ getService }: FtrProviderContext): void => { version: patchedCase.comments![0].version, comment: newComment, }, - auth: obsOnlyNoSpaceAuth, + auth: obsOnlyDefaultSpaceAuth, expectedHttpCode: 403, }); }); @@ -120,14 +124,14 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getPostCaseRequest(), 200, - superUserNoSpaceAuth + superUserDefaultSpaceAuth ); const patchedCase = await createComment({ supertest: supertestWithoutAuth, caseId: postedCase.id, params: postCommentUserReq, - auth: superUserNoSpaceAuth, + auth: superUserDefaultSpaceAuth, }); const newComment = 'Well I decided to update my comment. So what? Deal with it.'; @@ -151,14 +155,14 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getPostCaseRequest(), 200, - superUserNoSpaceAuth + superUserDefaultSpaceAuth ); const patchedCase = await createComment({ supertest: supertestWithoutAuth, caseId: postedCase.id, params: postCommentUserReq, - auth: superUserNoSpaceAuth, + auth: superUserDefaultSpaceAuth, }); const newComment = 'Well I decided to update my comment. So what? Deal with it.'; diff --git a/x-pack/test/case_api_integration/security_only/tests/common/comments/post_comment.ts b/x-pack/test/case_api_integration/security_only/tests/common/comments/post_comment.ts index ad3d63cedd2fbe..efec43b54674f8 100644 --- a/x-pack/test/case_api_integration/security_only/tests/common/comments/post_comment.ts +++ b/x-pack/test/case_api_integration/security_only/tests/common/comments/post_comment.ts @@ -25,7 +25,11 @@ import { secOnly, secOnlyRead, } from '../../../../common/lib/authentication/users'; -import { obsOnlyNoSpaceAuth, secOnlyNoSpaceAuth, superUserNoSpaceAuth } from '../../../utils'; +import { + obsOnlyDefaultSpaceAuth, + secOnlyDefaultSpaceAuth, + superUserDefaultSpaceAuth, +} from '../../../utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -49,14 +53,14 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getPostCaseRequest({ owner: 'securitySolutionFixture' }), 200, - superUserNoSpaceAuth + superUserDefaultSpaceAuth ); await createComment({ supertest: supertestWithoutAuth, caseId: postedCase.id, params: postCommentUserReq, - auth: secOnlyNoSpaceAuth, + auth: secOnlyDefaultSpaceAuth, }); }); @@ -65,14 +69,14 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getPostCaseRequest({ owner: 'observabilityFixture' }), 200, - obsOnlyNoSpaceAuth + obsOnlyDefaultSpaceAuth ); await createComment({ supertest: supertestWithoutAuth, caseId: postedCase.id, params: { ...postCommentUserReq, owner: 'observabilityFixture' }, - auth: secOnlyNoSpaceAuth, + auth: secOnlyDefaultSpaceAuth, expectedHttpCode: 403, }); }); @@ -85,7 +89,7 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getPostCaseRequest({ owner: 'securitySolutionFixture' }), 200, - superUserNoSpaceAuth + superUserDefaultSpaceAuth ); await createComment({ @@ -103,7 +107,7 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getPostCaseRequest({ owner: 'securitySolutionFixture' }), 200, - superUserNoSpaceAuth + superUserDefaultSpaceAuth ); await createComment({ diff --git a/x-pack/test/case_api_integration/security_only/tests/common/configure/get_configure.ts b/x-pack/test/case_api_integration/security_only/tests/common/configure/get_configure.ts index 9b45a393d7ca07..4734eca9ab15e3 100644 --- a/x-pack/test/case_api_integration/security_only/tests/common/configure/get_configure.ts +++ b/x-pack/test/case_api_integration/security_only/tests/common/configure/get_configure.ts @@ -24,10 +24,10 @@ import { obsSecRead, } from '../../../../common/lib/authentication/users'; import { - obsOnlyNoSpaceAuth, - obsSecNoSpaceAuth, - secOnlyNoSpaceAuth, - superUserNoSpaceAuth, + obsOnlyDefaultSpaceAuth, + obsSecDefaultSpaceAuth, + secOnlyDefaultSpaceAuth, + superUserDefaultSpaceAuth, } from '../../../utils'; // eslint-disable-next-line import/no-default-export @@ -45,14 +45,14 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getConfigurationRequest(), 200, - secOnlyNoSpaceAuth + secOnlyDefaultSpaceAuth ); await createConfiguration( supertestWithoutAuth, { ...getConfigurationRequest(), owner: 'observabilityFixture' }, 200, - obsOnlyNoSpaceAuth + obsOnlyDefaultSpaceAuth ); for (const scenario of [ @@ -100,7 +100,7 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getConfigurationRequest(), 200, - superUserNoSpaceAuth + superUserDefaultSpaceAuth ); // user should not be able to read configurations at the appropriate space @@ -119,7 +119,7 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getConfigurationRequest(), 200, - superUserNoSpaceAuth + superUserDefaultSpaceAuth ); await getConfiguration({ @@ -138,20 +138,20 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getConfigurationRequest(), 200, - obsSecNoSpaceAuth + obsSecDefaultSpaceAuth ), createConfiguration( supertestWithoutAuth, { ...getConfigurationRequest(), owner: 'observabilityFixture' }, 200, - obsSecNoSpaceAuth + obsSecDefaultSpaceAuth ), ]); const res = await getConfiguration({ supertest: supertestWithoutAuth, query: { owner: 'securitySolutionFixture' }, - auth: obsSecNoSpaceAuth, + auth: obsSecDefaultSpaceAuth, }); ensureSavedObjectIsAuthorized(res, 1, ['securitySolutionFixture']); @@ -163,13 +163,13 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getConfigurationRequest(), 200, - obsSecNoSpaceAuth + obsSecDefaultSpaceAuth ), createConfiguration( supertestWithoutAuth, { ...getConfigurationRequest(), owner: 'observabilityFixture' }, 200, - obsSecNoSpaceAuth + obsSecDefaultSpaceAuth ), ]); @@ -177,7 +177,7 @@ export default ({ getService }: FtrProviderContext): void => { const res = await getConfiguration({ supertest: supertestWithoutAuth, query: { owner: ['securitySolutionFixture', 'observabilityFixture'] }, - auth: secOnlyNoSpaceAuth, + auth: secOnlyDefaultSpaceAuth, }); // Only security solution cases are being returned diff --git a/x-pack/test/case_api_integration/security_only/tests/common/configure/patch_configure.ts b/x-pack/test/case_api_integration/security_only/tests/common/configure/patch_configure.ts index 68768277f1963a..284a7687c24348 100644 --- a/x-pack/test/case_api_integration/security_only/tests/common/configure/patch_configure.ts +++ b/x-pack/test/case_api_integration/security_only/tests/common/configure/patch_configure.ts @@ -23,7 +23,7 @@ import { globalRead, obsSecRead, } from '../../../../common/lib/authentication/users'; -import { secOnlyNoSpaceAuth, superUserNoSpaceAuth } from '../../../utils'; +import { secOnlyDefaultSpaceAuth, superUserDefaultSpaceAuth } from '../../../utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -44,7 +44,7 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getConfigurationRequest(), 200, - secOnlyNoSpaceAuth + secOnlyDefaultSpaceAuth ); const newConfiguration = await updateConfiguration( @@ -55,7 +55,7 @@ export default ({ getService }: FtrProviderContext): void => { version: configuration.version, }, 200, - secOnlyNoSpaceAuth + secOnlyDefaultSpaceAuth ); expect(newConfiguration.owner).to.eql('securitySolutionFixture'); @@ -66,7 +66,7 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, { ...getConfigurationRequest(), owner: 'observabilityFixture' }, 200, - superUserNoSpaceAuth + superUserDefaultSpaceAuth ); await updateConfiguration( @@ -77,7 +77,7 @@ export default ({ getService }: FtrProviderContext): void => { version: configuration.version, }, 403, - secOnlyNoSpaceAuth + secOnlyDefaultSpaceAuth ); }); @@ -89,7 +89,7 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getConfigurationRequest(), 200, - superUserNoSpaceAuth + superUserDefaultSpaceAuth ); await updateConfiguration( @@ -113,7 +113,7 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, { ...getConfigurationRequest(), owner: 'securitySolutionFixture' }, 200, - superUserNoSpaceAuth + superUserDefaultSpaceAuth ); await updateConfiguration( diff --git a/x-pack/test/case_api_integration/security_only/tests/common/configure/post_configure.ts b/x-pack/test/case_api_integration/security_only/tests/common/configure/post_configure.ts index 03462e72587195..fdf282d7531d5d 100644 --- a/x-pack/test/case_api_integration/security_only/tests/common/configure/post_configure.ts +++ b/x-pack/test/case_api_integration/security_only/tests/common/configure/post_configure.ts @@ -25,7 +25,7 @@ import { globalRead, obsSecRead, } from '../../../../common/lib/authentication/users'; -import { secOnlyNoSpaceAuth, superUserNoSpaceAuth } from '../../../utils'; +import { secOnlyDefaultSpaceAuth, superUserDefaultSpaceAuth } from '../../../utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -46,7 +46,7 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getConfigurationRequest(), 200, - secOnlyNoSpaceAuth + secOnlyDefaultSpaceAuth ); expect(configuration.owner).to.eql('securitySolutionFixture'); @@ -57,7 +57,7 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, { ...getConfigurationRequest(), owner: 'observabilityFixture' }, 403, - secOnlyNoSpaceAuth + secOnlyDefaultSpaceAuth ); }); @@ -94,7 +94,7 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, { ...getConfigurationRequest(), owner: 'securitySolutionFixture' }, 200, - superUserNoSpaceAuth + superUserDefaultSpaceAuth ); /** @@ -105,13 +105,13 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, { ...getConfigurationRequest(), owner: 'observabilityFixture' }, 200, - superUserNoSpaceAuth + superUserDefaultSpaceAuth ); const configuration = await getConfiguration({ supertest: supertestWithoutAuth, query: { owner: ['securitySolutionFixture', 'observabilityFixture'] }, - auth: superUserNoSpaceAuth, + auth: superUserDefaultSpaceAuth, }); /** diff --git a/x-pack/test/case_api_integration/security_only/tests/common/user_actions/get_all_user_actions.ts b/x-pack/test/case_api_integration/security_only/tests/common/user_actions/get_all_user_actions.ts index d898d6b320c39a..321381e0c0964b 100644 --- a/x-pack/test/case_api_integration/security_only/tests/common/user_actions/get_all_user_actions.ts +++ b/x-pack/test/case_api_integration/security_only/tests/common/user_actions/get_all_user_actions.ts @@ -25,7 +25,7 @@ import { secOnlyRead, superUser, } from '../../../../common/lib/authentication/users'; -import { superUserNoSpaceAuth } from '../../../utils'; +import { superUserDefaultSpaceAuth } from '../../../utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -44,7 +44,7 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getPostCaseRequest(), 200, - superUserNoSpaceAuth + superUserDefaultSpaceAuth ); await updateCase({ @@ -58,7 +58,7 @@ export default ({ getService }: FtrProviderContext): void => { }, ], }, - auth: superUserNoSpaceAuth, + auth: superUserDefaultSpaceAuth, }); }); diff --git a/x-pack/test/case_api_integration/security_only/tests/trial/cases/push_case.ts b/x-pack/test/case_api_integration/security_only/tests/trial/cases/push_case.ts index 2a88f3153dcf51..390859eea41acc 100644 --- a/x-pack/test/case_api_integration/security_only/tests/trial/cases/push_case.ts +++ b/x-pack/test/case_api_integration/security_only/tests/trial/cases/push_case.ts @@ -26,7 +26,7 @@ import { secOnly, secOnlyRead, } from '../../../../common/lib/authentication/users'; -import { secOnlyNoSpaceAuth } from '../../../utils'; +import { secOnlyDefaultSpaceAuth } from '../../../utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -62,7 +62,7 @@ export default ({ getService }: FtrProviderContext): void => { supertest: supertestWithoutAuth, caseId: postedCase.id, connectorId: connector.id, - auth: secOnlyNoSpaceAuth, + auth: secOnlyDefaultSpaceAuth, }); }); @@ -78,7 +78,7 @@ export default ({ getService }: FtrProviderContext): void => { supertest: supertestWithoutAuth, caseId: postedCase.id, connectorId: connector.id, - auth: secOnlyNoSpaceAuth, + auth: secOnlyDefaultSpaceAuth, expectedHttpCode: 403, }); }); diff --git a/x-pack/test/case_api_integration/security_only/utils.ts b/x-pack/test/case_api_integration/security_only/utils.ts index bba1914d3fb0d2..6b2d967bf55b88 100644 --- a/x-pack/test/case_api_integration/security_only/utils.ts +++ b/x-pack/test/case_api_integration/security_only/utils.ts @@ -8,7 +8,7 @@ import { obsOnly, obsSec, secOnly } from '../common/lib/authentication/users'; import { getAuthWithSuperUser } from '../common/lib/utils'; -export const secOnlyNoSpaceAuth = { user: secOnly, space: null }; -export const obsOnlyNoSpaceAuth = { user: obsOnly, space: null }; -export const obsSecNoSpaceAuth = { user: obsSec, space: null }; -export const superUserNoSpaceAuth = getAuthWithSuperUser(null); +export const secOnlyDefaultSpaceAuth = { user: secOnly, space: null }; +export const obsOnlyDefaultSpaceAuth = { user: obsOnly, space: null }; +export const obsSecDefaultSpaceAuth = { user: obsSec, space: null }; +export const superUserDefaultSpaceAuth = getAuthWithSuperUser(null); From 65b81c61835d335ee90129559d7930029bd29c91 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner Date: Tue, 11 May 2021 10:19:28 -0400 Subject: [PATCH 12/12] Refactoring users and roles for security only tests --- .../common/lib/authentication/index.ts | 32 ++--- .../common/lib/authentication/roles.ts | 114 ++++++++++++++++++ .../common/lib/authentication/users.ts | 59 +++++++++ .../tests/common/alerts/get_cases.ts | 18 +-- .../tests/common/cases/delete_cases.ts | 18 ++- .../tests/common/cases/find_cases.ts | 24 ++-- .../tests/common/cases/get_case.ts | 27 +++-- .../tests/common/cases/patch_cases.ts | 18 ++- .../tests/common/cases/post_case.ts | 22 ++-- .../common/cases/reporters/get_reporters.ts | 30 ++--- .../tests/common/cases/status/get_status.ts | 16 +-- .../tests/common/cases/tags/get_tags.ts | 16 +-- .../tests/common/comments/delete_comment.ts | 19 +-- .../tests/common/comments/find_comments.ts | 22 ++-- .../tests/common/comments/get_all_comments.ts | 27 +++-- .../tests/common/comments/get_comment.ts | 25 ++-- .../tests/common/comments/patch_comment.ts | 18 ++- .../tests/common/comments/post_comment.ts | 18 ++- .../tests/common/configure/get_configure.ts | 24 ++-- .../tests/common/configure/patch_configure.ts | 18 ++- .../tests/common/configure/post_configure.ts | 18 ++- .../user_actions/get_all_user_actions.ts | 19 ++- .../tests/trial/cases/push_case.ts | 18 ++- .../security_only/tests/trial/index.ts | 6 +- .../security_only/utils.ts | 12 +- 25 files changed, 457 insertions(+), 181 deletions(-) diff --git a/x-pack/test/case_api_integration/common/lib/authentication/index.ts b/x-pack/test/case_api_integration/common/lib/authentication/index.ts index 0a23982becd3be..86016b273ea44e 100644 --- a/x-pack/test/case_api_integration/common/lib/authentication/index.ts +++ b/x-pack/test/case_api_integration/common/lib/authentication/index.ts @@ -24,23 +24,19 @@ export const createSpaces = async (getService: CommonFtrProviderContext['getServ } }; +/** + * Creates the users and roles for use in the tests. Defaults to specific users and roles used by the security_and_spaces + * scenarios but can be passed specific ones as well. + */ export const createUsersAndRoles = async ( getService: CommonFtrProviderContext['getService'], - overrideSpaces?: string[] + usersToCreate: User[] = users, + rolesToCreate: Role[] = roles ) => { const security = getService('security'); const createRole = async ({ name, privileges }: Role) => { - const modifiedPrivileges = { - ...privileges, - // for roles that don't have kibana set this will just return undefined - kibana: privileges.kibana?.map((kibanaEntry) => ({ - ...kibanaEntry, - spaces: overrideSpaces != null ? overrideSpaces : kibanaEntry.spaces, - })), - }; - - return await security.role.create(name, modifiedPrivileges); + return await security.role.create(name, privileges); }; const createUser = async (user: User) => { @@ -54,11 +50,11 @@ export const createUsersAndRoles = async ( }); }; - for (const role of roles) { + for (const role of rolesToCreate) { await createRole(role); } - for (const user of users) { + for (const user of usersToCreate) { await createUser(user); } }; @@ -74,10 +70,14 @@ export const deleteSpaces = async (getService: CommonFtrProviderContext['getServ } }; -export const deleteUsersAndRoles = async (getService: CommonFtrProviderContext['getService']) => { +export const deleteUsersAndRoles = async ( + getService: CommonFtrProviderContext['getService'], + usersToDelete: User[] = users, + rolesToDelete: Role[] = roles +) => { const security = getService('security'); - for (const user of users) { + for (const user of usersToDelete) { try { await security.user.delete(user.username); } catch (error) { @@ -85,7 +85,7 @@ export const deleteUsersAndRoles = async (getService: CommonFtrProviderContext[' } } - for (const role of roles) { + for (const role of rolesToDelete) { try { await security.role.delete(role.name); } catch (error) { diff --git a/x-pack/test/case_api_integration/common/lib/authentication/roles.ts b/x-pack/test/case_api_integration/common/lib/authentication/roles.ts index 5ddecd92061065..60e50288f88568 100644 --- a/x-pack/test/case_api_integration/common/lib/authentication/roles.ts +++ b/x-pack/test/case_api_integration/common/lib/authentication/roles.ts @@ -150,3 +150,117 @@ export const roles = [ observabilityOnlyAll, observabilityOnlyRead, ]; + +/** + * These roles have access to all spaces. + */ + +export const securitySolutionOnlyAllSpacesAll: Role = { + name: 'sec_only_all', + privileges: { + elasticsearch: { + indices: [ + { + names: ['*'], + privileges: ['all'], + }, + ], + }, + kibana: [ + { + feature: { + securitySolutionFixture: ['all'], + actions: ['all'], + actionsSimulators: ['all'], + }, + spaces: ['*'], + }, + ], + }, +}; + +export const securitySolutionOnlyReadSpacesAll: Role = { + name: 'sec_only_read', + privileges: { + elasticsearch: { + indices: [ + { + names: ['*'], + privileges: ['all'], + }, + ], + }, + kibana: [ + { + feature: { + securitySolutionFixture: ['read'], + actions: ['read'], + actionsSimulators: ['read'], + }, + spaces: ['*'], + }, + ], + }, +}; + +export const observabilityOnlyAllSpacesAll: Role = { + name: 'obs_only_all', + privileges: { + elasticsearch: { + indices: [ + { + names: ['*'], + privileges: ['all'], + }, + ], + }, + kibana: [ + { + feature: { + observabilityFixture: ['all'], + actions: ['all'], + actionsSimulators: ['all'], + }, + spaces: ['*'], + }, + ], + }, +}; + +export const observabilityOnlyReadSpacesAll: Role = { + name: 'obs_only_read', + privileges: { + elasticsearch: { + indices: [ + { + names: ['*'], + privileges: ['all'], + }, + ], + }, + kibana: [ + { + feature: { + observabilityFixture: ['read'], + actions: ['read'], + actionsSimulators: ['read'], + }, + spaces: ['*'], + }, + ], + }, +}; + +/** + * These roles are specifically for the security_only tests where the spaces plugin is disabled. Most of the roles (except + * for noKibanaPrivileges) have spaces: ['*'] effectively giving it access to the default space since no other spaces + * will exist when the spaces plugin is disabled. + */ +export const rolesDefaultSpace = [ + noKibanaPrivileges, + globalRead, + securitySolutionOnlyAllSpacesAll, + securitySolutionOnlyReadSpacesAll, + observabilityOnlyAllSpacesAll, + observabilityOnlyReadSpacesAll, +]; diff --git a/x-pack/test/case_api_integration/common/lib/authentication/users.ts b/x-pack/test/case_api_integration/common/lib/authentication/users.ts index 06add9ae007933..1fa6e3c9f4990e 100644 --- a/x-pack/test/case_api_integration/common/lib/authentication/users.ts +++ b/x-pack/test/case_api_integration/common/lib/authentication/users.ts @@ -12,6 +12,10 @@ import { observabilityOnlyRead, globalRead as globalReadRole, noKibanaPrivileges as noKibanaPrivilegesRole, + securitySolutionOnlyAllSpacesAll, + securitySolutionOnlyReadSpacesAll, + observabilityOnlyAllSpacesAll, + observabilityOnlyReadSpacesAll, } from './roles'; import { User } from './types'; @@ -80,3 +84,58 @@ export const users = [ globalRead, noKibanaPrivileges, ]; + +/** + * These users will have access to all spaces. + */ + +export const secOnlySpacesAll: User = { + username: 'sec_only', + password: 'sec_only', + roles: [securitySolutionOnlyAllSpacesAll.name], +}; + +export const secOnlyReadSpacesAll: User = { + username: 'sec_only_read', + password: 'sec_only_read', + roles: [securitySolutionOnlyReadSpacesAll.name], +}; + +export const obsOnlySpacesAll: User = { + username: 'obs_only', + password: 'obs_only', + roles: [observabilityOnlyAllSpacesAll.name], +}; + +export const obsOnlyReadSpacesAll: User = { + username: 'obs_only_read', + password: 'obs_only_read', + roles: [observabilityOnlyReadSpacesAll.name], +}; + +export const obsSecSpacesAll: User = { + username: 'obs_sec', + password: 'obs_sec', + roles: [securitySolutionOnlyAllSpacesAll.name, observabilityOnlyAllSpacesAll.name], +}; + +export const obsSecReadSpacesAll: User = { + username: 'obs_sec_read', + password: 'obs_sec_read', + roles: [securitySolutionOnlyReadSpacesAll.name, observabilityOnlyReadSpacesAll.name], +}; + +/** + * These users are for the security_only tests because most of them have access to the default space instead of 'space1' + */ +export const usersDefaultSpace = [ + superUser, + secOnlySpacesAll, + secOnlyReadSpacesAll, + obsOnlySpacesAll, + obsOnlyReadSpacesAll, + obsSecSpacesAll, + obsSecReadSpacesAll, + globalRead, + noKibanaPrivileges, +]; diff --git a/x-pack/test/case_api_integration/security_only/tests/common/alerts/get_cases.ts b/x-pack/test/case_api_integration/security_only/tests/common/alerts/get_cases.ts index 0bb7568778f310..9575bd99112f6b 100644 --- a/x-pack/test/case_api_integration/security_only/tests/common/alerts/get_cases.ts +++ b/x-pack/test/case_api_integration/security_only/tests/common/alerts/get_cases.ts @@ -18,10 +18,10 @@ import { import { globalRead, noKibanaPrivileges, - obsOnlyRead, - obsSec, - obsSecRead, - secOnlyRead, + obsOnlyReadSpacesAll, + obsSecSpacesAll, + obsSecReadSpacesAll, + secOnlyReadSpacesAll, superUser, } from '../../../../common/lib/authentication/users'; import { @@ -85,10 +85,10 @@ export default ({ getService }: FtrProviderContext): void => { user: superUser, caseIDs: [case1.id, case2.id, case3.id], }, - { user: secOnlyRead, caseIDs: [case1.id, case2.id] }, - { user: obsOnlyRead, caseIDs: [case3.id] }, + { user: secOnlyReadSpacesAll, caseIDs: [case1.id, case2.id] }, + { user: obsOnlyReadSpacesAll, caseIDs: [case3.id] }, { - user: obsSecRead, + user: obsSecReadSpacesAll, caseIDs: [case1.id, case2.id, case3.id], }, ]) { @@ -160,7 +160,7 @@ export default ({ getService }: FtrProviderContext): void => { await getCaseIDsByAlert({ supertest: supertestWithoutAuth, alertID: postCommentAlertReq.alertId as string, - auth: { user: obsSec, space: 'space1' }, + auth: { user: obsSecSpacesAll, space: 'space1' }, query: { owner: 'securitySolutionFixture' }, expectedHttpCode: 404, }); @@ -232,7 +232,7 @@ export default ({ getService }: FtrProviderContext): void => { supertest: supertestWithoutAuth, alertID: postCommentAlertReq.alertId as string, auth: secOnlyDefaultSpaceAuth, - // The secOnly user does not have permissions for observability cases, so it should only return the security solution one + // The secOnlyDefaultSpace user does not have permissions for observability cases, so it should only return the security solution one query: { owner: ['securitySolutionFixture', 'observabilityFixture'] }, }); diff --git a/x-pack/test/case_api_integration/security_only/tests/common/cases/delete_cases.ts b/x-pack/test/case_api_integration/security_only/tests/common/cases/delete_cases.ts index 87c6af5849fcf6..9ece177b214914 100644 --- a/x-pack/test/case_api_integration/security_only/tests/common/cases/delete_cases.ts +++ b/x-pack/test/case_api_integration/security_only/tests/common/cases/delete_cases.ts @@ -17,11 +17,11 @@ import { getCase, } from '../../../../common/lib/utils'; import { - secOnly, - secOnlyRead, + secOnlySpacesAll, + secOnlyReadSpacesAll, globalRead, - obsOnlyRead, - obsSecRead, + obsOnlyReadSpacesAll, + obsSecReadSpacesAll, noKibanaPrivileges, } from '../../../../common/lib/authentication/users'; import { @@ -112,7 +112,13 @@ export default ({ getService }: FtrProviderContext): void => { }); }); - for (const user of [globalRead, secOnlyRead, obsOnlyRead, obsSecRead, noKibanaPrivileges]) { + for (const user of [ + globalRead, + secOnlyReadSpacesAll, + obsOnlyReadSpacesAll, + obsSecReadSpacesAll, + noKibanaPrivileges, + ]) { it(`User ${ user.username } with role(s) ${user.roles.join()} - should NOT delete a case`, async () => { @@ -144,7 +150,7 @@ export default ({ getService }: FtrProviderContext): void => { supertest: supertestWithoutAuth, caseIDs: [postedCase.id], expectedHttpCode: 404, - auth: { user: secOnly, space: 'space1' }, + auth: { user: secOnlySpacesAll, space: 'space1' }, }); }); }); diff --git a/x-pack/test/case_api_integration/security_only/tests/common/cases/find_cases.ts b/x-pack/test/case_api_integration/security_only/tests/common/cases/find_cases.ts index 60beca5cbe5ce1..711eccbe162786 100644 --- a/x-pack/test/case_api_integration/security_only/tests/common/cases/find_cases.ts +++ b/x-pack/test/case_api_integration/security_only/tests/common/cases/find_cases.ts @@ -16,13 +16,13 @@ import { createCase, } from '../../../../common/lib/utils'; import { - secOnly, - obsOnlyRead, - secOnlyRead, + secOnlySpacesAll, + obsOnlyReadSpacesAll, + secOnlyReadSpacesAll, noKibanaPrivileges, superUser, globalRead, - obsSecRead, + obsSecReadSpacesAll, } from '../../../../common/lib/authentication/users'; import { obsOnlyDefaultSpaceAuth, @@ -71,10 +71,18 @@ export default ({ getService }: FtrProviderContext): void => { numberOfExpectedCases: 2, owners: ['securitySolutionFixture', 'observabilityFixture'], }, - { user: secOnlyRead, numberOfExpectedCases: 1, owners: ['securitySolutionFixture'] }, - { user: obsOnlyRead, numberOfExpectedCases: 1, owners: ['observabilityFixture'] }, { - user: obsSecRead, + user: secOnlyReadSpacesAll, + numberOfExpectedCases: 1, + owners: ['securitySolutionFixture'], + }, + { + user: obsOnlyReadSpacesAll, + numberOfExpectedCases: 1, + owners: ['observabilityFixture'], + }, + { + user: obsSecReadSpacesAll, numberOfExpectedCases: 2, owners: ['securitySolutionFixture', 'observabilityFixture'], }, @@ -111,7 +119,7 @@ export default ({ getService }: FtrProviderContext): void => { await findCases({ supertest: supertestWithoutAuth, - auth: { user: secOnly, space: 'space1' }, + auth: { user: secOnlySpacesAll, space: 'space1' }, expectedHttpCode: 404, }); }); diff --git a/x-pack/test/case_api_integration/security_only/tests/common/cases/get_case.ts b/x-pack/test/case_api_integration/security_only/tests/common/cases/get_case.ts index 09fc994f4ece2b..3bdb4c5ed310e3 100644 --- a/x-pack/test/case_api_integration/security_only/tests/common/cases/get_case.ts +++ b/x-pack/test/case_api_integration/security_only/tests/common/cases/get_case.ts @@ -18,15 +18,15 @@ import { removeServerGeneratedPropertiesFromSavedObject, } from '../../../../common/lib/utils'; import { - secOnly, - obsOnly, + secOnlySpacesAll, + obsOnlySpacesAll, globalRead, superUser, - secOnlyRead, - obsOnlyRead, - obsSecRead, + secOnlyReadSpacesAll, + obsOnlyReadSpacesAll, + obsSecReadSpacesAll, noKibanaPrivileges, - obsSec, + obsSecSpacesAll, } from '../../../../common/lib/authentication/users'; import { getUserInfo } from '../../../../common/lib/authentication'; import { secOnlyDefaultSpaceAuth, superUserDefaultSpaceAuth } from '../../../utils'; @@ -49,7 +49,14 @@ export default ({ getService }: FtrProviderContext): void => { superUserDefaultSpaceAuth ); - for (const user of [globalRead, superUser, secOnly, secOnlyRead, obsSec, obsSecRead]) { + for (const user of [ + globalRead, + superUser, + secOnlySpacesAll, + secOnlyReadSpacesAll, + obsSecSpacesAll, + obsSecReadSpacesAll, + ]) { const theCase = await getCase({ supertest: supertestWithoutAuth, caseId: newCase.id, @@ -92,7 +99,7 @@ export default ({ getService }: FtrProviderContext): void => { type: postCommentUserReq.type, comment: postCommentUserReq.comment, associationType: 'case', - created_by: getUserInfo(secOnly), + created_by: getUserInfo(secOnlySpacesAll), pushed_at: null, pushed_by: null, updated_by: null, @@ -108,7 +115,7 @@ export default ({ getService }: FtrProviderContext): void => { superUserDefaultSpaceAuth ); - for (const user of [noKibanaPrivileges, obsOnly, obsOnlyRead]) { + for (const user of [noKibanaPrivileges, obsOnlySpacesAll, obsOnlyReadSpacesAll]) { await getCase({ supertest: supertestWithoutAuth, caseId: newCase.id, @@ -130,7 +137,7 @@ export default ({ getService }: FtrProviderContext): void => { supertest: supertestWithoutAuth, caseId: newCase.id, expectedHttpCode: 404, - auth: { user: secOnly, space: 'space1' }, + auth: { user: secOnlySpacesAll, space: 'space1' }, }); }); }); diff --git a/x-pack/test/case_api_integration/security_only/tests/common/cases/patch_cases.ts b/x-pack/test/case_api_integration/security_only/tests/common/cases/patch_cases.ts index eefca21fb6e425..bfab3fce7adbe8 100644 --- a/x-pack/test/case_api_integration/security_only/tests/common/cases/patch_cases.ts +++ b/x-pack/test/case_api_integration/security_only/tests/common/cases/patch_cases.ts @@ -20,10 +20,10 @@ import { import { globalRead, noKibanaPrivileges, - obsOnlyRead, - obsSecRead, - secOnly, - secOnlyRead, + obsOnlyReadSpacesAll, + obsSecReadSpacesAll, + secOnlySpacesAll, + secOnlyReadSpacesAll, superUser, } from '../../../../common/lib/authentication/users'; import { @@ -184,7 +184,13 @@ export default ({ getService }: FtrProviderContext): void => { expect(resp.cases[2].title).to.eql(postCaseReq.title); }); - for (const user of [globalRead, secOnlyRead, obsOnlyRead, obsSecRead, noKibanaPrivileges]) { + for (const user of [ + globalRead, + secOnlyReadSpacesAll, + obsOnlyReadSpacesAll, + obsSecReadSpacesAll, + noKibanaPrivileges, + ]) { it(`User ${ user.username } with role(s) ${user.roles.join()} - should NOT update a case`, async () => { @@ -229,7 +235,7 @@ export default ({ getService }: FtrProviderContext): void => { }, ], }, - auth: { user: secOnly, space: 'space1' }, + auth: { user: secOnlySpacesAll, space: 'space1' }, expectedHttpCode: 404, }); }); diff --git a/x-pack/test/case_api_integration/security_only/tests/common/cases/post_case.ts b/x-pack/test/case_api_integration/security_only/tests/common/cases/post_case.ts index 9a046562e68db9..28043d7155e4a6 100644 --- a/x-pack/test/case_api_integration/security_only/tests/common/cases/post_case.ts +++ b/x-pack/test/case_api_integration/security_only/tests/common/cases/post_case.ts @@ -10,15 +10,15 @@ import expect from '@kbn/expect'; import { getPostCaseRequest } from '../../../../common/lib/mock'; import { deleteCasesByESQuery, createCase } from '../../../../common/lib/utils'; import { - secOnly, - secOnlyRead, + secOnlySpacesAll, + secOnlyReadSpacesAll, globalRead, - obsOnlyRead, - obsSecRead, + obsOnlyReadSpacesAll, + obsSecReadSpacesAll, noKibanaPrivileges, } from '../../../../common/lib/authentication/users'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; -import { secOnlyDefaultSpaceAuth, superUserDefaultSpaceAuth } from '../../../utils'; +import { secOnlyDefaultSpaceAuth } from '../../../utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { @@ -49,7 +49,13 @@ export default ({ getService }: FtrProviderContext): void => { ); }); - for (const user of [globalRead, secOnlyRead, obsOnlyRead, obsSecRead, noKibanaPrivileges]) { + for (const user of [ + globalRead, + secOnlyReadSpacesAll, + obsOnlyReadSpacesAll, + obsSecReadSpacesAll, + noKibanaPrivileges, + ]) { it(`User ${ user.username } with role(s) ${user.roles.join()} - should NOT create a case`, async () => { @@ -57,7 +63,7 @@ export default ({ getService }: FtrProviderContext): void => { supertestWithoutAuth, getPostCaseRequest({ owner: 'securitySolutionFixture' }), 403, - superUserDefaultSpaceAuth + { user, space: null } ); }); } @@ -68,7 +74,7 @@ export default ({ getService }: FtrProviderContext): void => { getPostCaseRequest({ owner: 'securitySolutionFixture' }), 404, { - user: secOnly, + user: secOnlySpacesAll, space: 'space1', } ); diff --git a/x-pack/test/case_api_integration/security_only/tests/common/cases/reporters/get_reporters.ts b/x-pack/test/case_api_integration/security_only/tests/common/cases/reporters/get_reporters.ts index e56a4d6c099e69..4c72dafed053b1 100644 --- a/x-pack/test/case_api_integration/security_only/tests/common/cases/reporters/get_reporters.ts +++ b/x-pack/test/case_api_integration/security_only/tests/common/cases/reporters/get_reporters.ts @@ -11,15 +11,15 @@ import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; import { getPostCaseRequest } from '../../../../../common/lib/mock'; import { createCase, deleteCasesByESQuery, getReporters } from '../../../../../common/lib/utils'; import { - secOnly, - obsOnly, + secOnlySpacesAll, + obsOnlySpacesAll, globalRead, superUser, - secOnlyRead, - obsOnlyRead, - obsSecRead, + secOnlyReadSpacesAll, + obsOnlyReadSpacesAll, + obsSecReadSpacesAll, noKibanaPrivileges, - obsSec, + obsSecSpacesAll, } from '../../../../../common/lib/authentication/users'; import { getUserInfo } from '../../../../../common/lib/authentication'; import { @@ -57,17 +57,17 @@ export default ({ getService }: FtrProviderContext): void => { for (const scenario of [ { user: globalRead, - expectedReporters: [getUserInfo(secOnly), getUserInfo(obsOnly)], + expectedReporters: [getUserInfo(secOnlySpacesAll), getUserInfo(obsOnlySpacesAll)], }, { user: superUser, - expectedReporters: [getUserInfo(secOnly), getUserInfo(obsOnly)], + expectedReporters: [getUserInfo(secOnlySpacesAll), getUserInfo(obsOnlySpacesAll)], }, - { user: secOnlyRead, expectedReporters: [getUserInfo(secOnly)] }, - { user: obsOnlyRead, expectedReporters: [getUserInfo(obsOnly)] }, + { user: secOnlyReadSpacesAll, expectedReporters: [getUserInfo(secOnlySpacesAll)] }, + { user: obsOnlyReadSpacesAll, expectedReporters: [getUserInfo(obsOnlySpacesAll)] }, { - user: obsSecRead, - expectedReporters: [getUserInfo(secOnly), getUserInfo(obsOnly)], + user: obsSecReadSpacesAll, + expectedReporters: [getUserInfo(secOnlySpacesAll), getUserInfo(obsOnlySpacesAll)], }, ]) { const reporters = await getReporters({ @@ -106,7 +106,7 @@ export default ({ getService }: FtrProviderContext): void => { await getReporters({ supertest: supertestWithoutAuth, expectedHttpCode: 404, - auth: { user: obsSec, space: 'space1' }, + auth: { user: obsSecSpacesAll, space: 'space1' }, }); }); @@ -127,7 +127,7 @@ export default ({ getService }: FtrProviderContext): void => { query: { owner: 'securitySolutionFixture' }, }); - expect(reporters).to.eql([getUserInfo(secOnly)]); + expect(reporters).to.eql([getUserInfo(secOnlySpacesAll)]); }); it('should return the correct cases when trying to exploit RBAC through the owner query parameter', async () => { @@ -149,7 +149,7 @@ export default ({ getService }: FtrProviderContext): void => { }); // Only security solution reporters are being returned - expect(reporters).to.eql([getUserInfo(secOnly)]); + expect(reporters).to.eql([getUserInfo(secOnlySpacesAll)]); }); }); }; diff --git a/x-pack/test/case_api_integration/security_only/tests/common/cases/status/get_status.ts b/x-pack/test/case_api_integration/security_only/tests/common/cases/status/get_status.ts index 9bba570111861a..78ca48b04560c5 100644 --- a/x-pack/test/case_api_integration/security_only/tests/common/cases/status/get_status.ts +++ b/x-pack/test/case_api_integration/security_only/tests/common/cases/status/get_status.ts @@ -19,10 +19,10 @@ import { import { globalRead, noKibanaPrivileges, - obsOnlyRead, - obsSecRead, - secOnly, - secOnlyRead, + obsOnlyReadSpacesAll, + obsSecReadSpacesAll, + secOnlySpacesAll, + secOnlyReadSpacesAll, superUser, } from '../../../../../common/lib/authentication/users'; import { superUserDefaultSpaceAuth } from '../../../../utils'; @@ -89,9 +89,9 @@ export default ({ getService }: FtrProviderContext): void => { for (const scenario of [ { user: globalRead, stats: { open: 1, inProgress: 2, closed: 1 } }, { user: superUser, stats: { open: 1, inProgress: 2, closed: 1 } }, - { user: secOnlyRead, stats: { open: 0, inProgress: 1, closed: 1 } }, - { user: obsOnlyRead, stats: { open: 1, inProgress: 1, closed: 0 } }, - { user: obsSecRead, stats: { open: 1, inProgress: 2, closed: 1 } }, + { user: secOnlyReadSpacesAll, stats: { open: 0, inProgress: 1, closed: 1 } }, + { user: obsOnlyReadSpacesAll, stats: { open: 1, inProgress: 1, closed: 0 } }, + { user: obsSecReadSpacesAll, stats: { open: 1, inProgress: 2, closed: 1 } }, ]) { const statuses = await getAllCasesStatuses({ supertest: supertestWithoutAuth, @@ -123,7 +123,7 @@ export default ({ getService }: FtrProviderContext): void => { await getAllCasesStatuses({ supertest: supertestWithoutAuth, - auth: { user: secOnly, space: 'space1' }, + auth: { user: secOnlySpacesAll, space: 'space1' }, expectedHttpCode: 404, }); }); diff --git a/x-pack/test/case_api_integration/security_only/tests/common/cases/tags/get_tags.ts b/x-pack/test/case_api_integration/security_only/tests/common/cases/tags/get_tags.ts index 1d95c8f4e991de..c05d956028752d 100644 --- a/x-pack/test/case_api_integration/security_only/tests/common/cases/tags/get_tags.ts +++ b/x-pack/test/case_api_integration/security_only/tests/common/cases/tags/get_tags.ts @@ -11,12 +11,12 @@ import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; import { deleteCasesByESQuery, createCase, getTags } from '../../../../../common/lib/utils'; import { getPostCaseRequest } from '../../../../../common/lib/mock'; import { - secOnly, + secOnlySpacesAll, globalRead, superUser, - secOnlyRead, - obsOnlyRead, - obsSecRead, + secOnlyReadSpacesAll, + obsOnlyReadSpacesAll, + obsSecReadSpacesAll, noKibanaPrivileges, } from '../../../../../common/lib/authentication/users'; import { @@ -60,10 +60,10 @@ export default ({ getService }: FtrProviderContext): void => { user: superUser, expectedTags: ['sec', 'obs'], }, - { user: secOnlyRead, expectedTags: ['sec'] }, - { user: obsOnlyRead, expectedTags: ['obs'] }, + { user: secOnlyReadSpacesAll, expectedTags: ['sec'] }, + { user: obsOnlyReadSpacesAll, expectedTags: ['obs'] }, { - user: obsSecRead, + user: obsSecReadSpacesAll, expectedTags: ['sec', 'obs'], }, ]) { @@ -111,7 +111,7 @@ export default ({ getService }: FtrProviderContext): void => { await getTags({ supertest: supertestWithoutAuth, expectedHttpCode: 404, - auth: { user: secOnly, space: 'space1' }, + auth: { user: secOnlySpacesAll, space: 'space1' }, }); }); diff --git a/x-pack/test/case_api_integration/security_only/tests/common/comments/delete_comment.ts b/x-pack/test/case_api_integration/security_only/tests/common/comments/delete_comment.ts index 7a122ae44f7934..274879c69c4d50 100644 --- a/x-pack/test/case_api_integration/security_only/tests/common/comments/delete_comment.ts +++ b/x-pack/test/case_api_integration/security_only/tests/common/comments/delete_comment.ts @@ -22,10 +22,10 @@ import { import { globalRead, noKibanaPrivileges, - obsOnlyRead, - obsSecRead, - secOnly, - secOnlyRead, + obsOnlyReadSpacesAll, + obsSecReadSpacesAll, + secOnlySpacesAll, + secOnlyReadSpacesAll, } from '../../../../common/lib/authentication/users'; import { obsOnlyDefaultSpaceAuth, secOnlyDefaultSpaceAuth } from '../../../utils'; @@ -130,7 +130,12 @@ export default ({ getService }: FtrProviderContext): void => { }); }); - for (const user of [globalRead, secOnlyRead, obsOnlyRead, obsSecRead]) { + for (const user of [ + globalRead, + secOnlyReadSpacesAll, + obsOnlyReadSpacesAll, + obsSecReadSpacesAll, + ]) { it(`User ${ user.username } with role(s) ${user.roles.join()} - should NOT delete a comment`, async () => { @@ -216,14 +221,14 @@ export default ({ getService }: FtrProviderContext): void => { supertest: supertestWithoutAuth, caseId: postedCase.id, commentId: commentResp.comments![0].id, - auth: { user: secOnly, space: 'space1' }, + auth: { user: secOnlySpacesAll, space: 'space1' }, expectedHttpCode: 404, }); await deleteAllComments({ supertest: supertestWithoutAuth, caseId: postedCase.id, - auth: { user: secOnly, space: 'space1' }, + auth: { user: secOnlySpacesAll, space: 'space1' }, expectedHttpCode: 404, }); }); diff --git a/x-pack/test/case_api_integration/security_only/tests/common/comments/find_comments.ts b/x-pack/test/case_api_integration/security_only/tests/common/comments/find_comments.ts index fab0ad86fcfa7c..5239c616603a8a 100644 --- a/x-pack/test/case_api_integration/security_only/tests/common/comments/find_comments.ts +++ b/x-pack/test/case_api_integration/security_only/tests/common/comments/find_comments.ts @@ -27,13 +27,13 @@ import { } from '../../../../common/lib/utils'; import { - secOnly, - obsOnlyRead, - secOnlyRead, + secOnlySpacesAll, + obsOnlyReadSpacesAll, + secOnlyReadSpacesAll, noKibanaPrivileges, superUser, globalRead, - obsSecRead, + obsSecReadSpacesAll, } from '../../../../common/lib/authentication/users'; import { obsOnlyDefaultSpaceAuth, @@ -113,25 +113,25 @@ export default ({ getService }: FtrProviderContext): void => { caseID: obsCase.id, }, { - user: secOnlyRead, + user: secOnlyReadSpacesAll, numExpectedEntites: 1, owners: ['securitySolutionFixture'], caseID: secCase.id, }, { - user: obsOnlyRead, + user: obsOnlyReadSpacesAll, numExpectedEntites: 1, owners: ['observabilityFixture'], caseID: obsCase.id, }, { - user: obsSecRead, + user: obsSecReadSpacesAll, numExpectedEntites: 1, owners: ['securitySolutionFixture', 'observabilityFixture'], caseID: secCase.id, }, { - user: obsSecRead, + user: obsSecReadSpacesAll, numExpectedEntites: 1, owners: ['securitySolutionFixture', 'observabilityFixture'], caseID: obsCase.id, @@ -192,7 +192,7 @@ export default ({ getService }: FtrProviderContext): void => { await supertestWithoutAuth .get(`${getSpaceUrlPrefix('space1')}${CASES_URL}/${caseInfo.id}/comments/_find`) - .auth(secOnly.username, secOnly.password) + .auth(secOnlySpacesAll.username, secOnlySpacesAll.password) .expect(404); }); @@ -217,7 +217,7 @@ export default ({ getService }: FtrProviderContext): void => { obsCase.id }/comments/_find?search=securitySolutionFixture+observabilityFixture` ) - .auth(secOnly.username, secOnly.password) + .auth(secOnlySpacesAll.username, secOnlySpacesAll.password) .expect(200); // shouldn't find any comments since they were created under the observability ownership @@ -245,7 +245,7 @@ export default ({ getService }: FtrProviderContext): void => { obsCase.id }/comments/_find?filter=cases-comments.attributes.owner:"observabilityFixture"` ) - .auth(secOnly.username, secOnly.password) + .auth(secOnlySpacesAll.username, secOnlySpacesAll.password) .expect(200); expect(res.comments.length).to.be(0); }); diff --git a/x-pack/test/case_api_integration/security_only/tests/common/comments/get_all_comments.ts b/x-pack/test/case_api_integration/security_only/tests/common/comments/get_all_comments.ts index 38e9f18868f4d7..a0010ef19499fa 100644 --- a/x-pack/test/case_api_integration/security_only/tests/common/comments/get_all_comments.ts +++ b/x-pack/test/case_api_integration/security_only/tests/common/comments/get_all_comments.ts @@ -18,12 +18,12 @@ import { import { globalRead, noKibanaPrivileges, - obsOnly, - obsOnlyRead, - obsSec, - obsSecRead, - secOnly, - secOnlyRead, + obsOnlySpacesAll, + obsOnlyReadSpacesAll, + obsSecSpacesAll, + obsSecReadSpacesAll, + secOnlySpacesAll, + secOnlyReadSpacesAll, superUser, } from '../../../../common/lib/authentication/users'; import { superUserDefaultSpaceAuth } from '../../../utils'; @@ -61,7 +61,14 @@ export default ({ getService }: FtrProviderContext): void => { auth: superUserDefaultSpaceAuth, }); - for (const user of [globalRead, superUser, secOnly, secOnlyRead, obsSec, obsSecRead]) { + for (const user of [ + globalRead, + superUser, + secOnlySpacesAll, + secOnlyReadSpacesAll, + obsSecSpacesAll, + obsSecReadSpacesAll, + ]) { const comments = await getAllComments({ supertest: supertestWithoutAuth, caseId: caseInfo.id, @@ -89,8 +96,8 @@ export default ({ getService }: FtrProviderContext): void => { for (const scenario of [ { user: noKibanaPrivileges, returnCode: 403 }, - { user: obsOnly, returnCode: 200 }, - { user: obsOnlyRead, returnCode: 200 }, + { user: obsOnlySpacesAll, returnCode: 200 }, + { user: obsOnlyReadSpacesAll, returnCode: 200 }, ]) { const comments = await getAllComments({ supertest: supertestWithoutAuth, @@ -124,7 +131,7 @@ export default ({ getService }: FtrProviderContext): void => { await getAllComments({ supertest: supertestWithoutAuth, caseId: caseInfo.id, - auth: { user: secOnly, space: 'space1' }, + auth: { user: secOnlySpacesAll, space: 'space1' }, expectedHttpCode: 404, }); }); diff --git a/x-pack/test/case_api_integration/security_only/tests/common/comments/get_comment.ts b/x-pack/test/case_api_integration/security_only/tests/common/comments/get_comment.ts index 79bc3a01f2ee50..79693d3e0a574e 100644 --- a/x-pack/test/case_api_integration/security_only/tests/common/comments/get_comment.ts +++ b/x-pack/test/case_api_integration/security_only/tests/common/comments/get_comment.ts @@ -17,12 +17,12 @@ import { import { globalRead, noKibanaPrivileges, - obsOnly, - obsOnlyRead, - obsSec, - obsSecRead, - secOnly, - secOnlyRead, + obsOnlySpacesAll, + obsOnlyReadSpacesAll, + obsSecSpacesAll, + obsSecReadSpacesAll, + secOnlySpacesAll, + secOnlyReadSpacesAll, superUser, } from '../../../../common/lib/authentication/users'; import { superUserDefaultSpaceAuth } from '../../../utils'; @@ -53,7 +53,14 @@ export default ({ getService }: FtrProviderContext): void => { auth: superUserDefaultSpaceAuth, }); - for (const user of [globalRead, superUser, secOnly, secOnlyRead, obsSec, obsSecRead]) { + for (const user of [ + globalRead, + superUser, + secOnlySpacesAll, + secOnlyReadSpacesAll, + obsSecSpacesAll, + obsSecReadSpacesAll, + ]) { await getComment({ supertest: supertestWithoutAuth, caseId: caseInfo.id, @@ -78,7 +85,7 @@ export default ({ getService }: FtrProviderContext): void => { auth: superUserDefaultSpaceAuth, }); - for (const user of [noKibanaPrivileges, obsOnly, obsOnlyRead]) { + for (const user of [noKibanaPrivileges, obsOnlySpacesAll, obsOnlyReadSpacesAll]) { await getComment({ supertest: supertestWithoutAuth, caseId: caseInfo.id, @@ -108,7 +115,7 @@ export default ({ getService }: FtrProviderContext): void => { supertest: supertestWithoutAuth, caseId: caseInfo.id, commentId: caseWithComment.comments![0].id, - auth: { user: secOnly, space: 'space1' }, + auth: { user: secOnlySpacesAll, space: 'space1' }, expectedHttpCode: 404, }); }); diff --git a/x-pack/test/case_api_integration/security_only/tests/common/comments/patch_comment.ts b/x-pack/test/case_api_integration/security_only/tests/common/comments/patch_comment.ts index 028b5148b7f2a8..7a25ec4ec39812 100644 --- a/x-pack/test/case_api_integration/security_only/tests/common/comments/patch_comment.ts +++ b/x-pack/test/case_api_integration/security_only/tests/common/comments/patch_comment.ts @@ -22,10 +22,10 @@ import { import { globalRead, noKibanaPrivileges, - obsOnlyRead, - obsSecRead, - secOnly, - secOnlyRead, + obsOnlyReadSpacesAll, + obsSecReadSpacesAll, + secOnlySpacesAll, + secOnlyReadSpacesAll, } from '../../../../common/lib/authentication/users'; import { obsOnlyDefaultSpaceAuth, @@ -116,7 +116,13 @@ export default ({ getService }: FtrProviderContext): void => { }); }); - for (const user of [globalRead, secOnlyRead, obsOnlyRead, obsSecRead, noKibanaPrivileges]) { + for (const user of [ + globalRead, + secOnlyReadSpacesAll, + obsOnlyReadSpacesAll, + obsSecReadSpacesAll, + noKibanaPrivileges, + ]) { it(`User ${ user.username } with role(s) ${user.roles.join()} - should NOT update a comment`, async () => { @@ -175,7 +181,7 @@ export default ({ getService }: FtrProviderContext): void => { version: patchedCase.comments![0].version, comment: newComment, }, - auth: { user: secOnly, space: 'space1' }, + auth: { user: secOnlySpacesAll, space: 'space1' }, expectedHttpCode: 404, }); }); diff --git a/x-pack/test/case_api_integration/security_only/tests/common/comments/post_comment.ts b/x-pack/test/case_api_integration/security_only/tests/common/comments/post_comment.ts index efec43b54674f8..500308305d131e 100644 --- a/x-pack/test/case_api_integration/security_only/tests/common/comments/post_comment.ts +++ b/x-pack/test/case_api_integration/security_only/tests/common/comments/post_comment.ts @@ -20,10 +20,10 @@ import { import { globalRead, noKibanaPrivileges, - obsOnlyRead, - obsSecRead, - secOnly, - secOnlyRead, + obsOnlyReadSpacesAll, + obsSecReadSpacesAll, + secOnlySpacesAll, + secOnlyReadSpacesAll, } from '../../../../common/lib/authentication/users'; import { obsOnlyDefaultSpaceAuth, @@ -81,7 +81,13 @@ export default ({ getService }: FtrProviderContext): void => { }); }); - for (const user of [globalRead, secOnlyRead, obsOnlyRead, obsSecRead, noKibanaPrivileges]) { + for (const user of [ + globalRead, + secOnlyReadSpacesAll, + obsOnlyReadSpacesAll, + obsSecReadSpacesAll, + noKibanaPrivileges, + ]) { it(`User ${ user.username } with role(s) ${user.roles.join()} - should not create a comment`, async () => { @@ -114,7 +120,7 @@ export default ({ getService }: FtrProviderContext): void => { supertest: supertestWithoutAuth, caseId: postedCase.id, params: postCommentUserReq, - auth: { user: secOnly, space: 'space1' }, + auth: { user: secOnlySpacesAll, space: 'space1' }, expectedHttpCode: 404, }); }); diff --git a/x-pack/test/case_api_integration/security_only/tests/common/configure/get_configure.ts b/x-pack/test/case_api_integration/security_only/tests/common/configure/get_configure.ts index 4734eca9ab15e3..0a8b3ebd8981e3 100644 --- a/x-pack/test/case_api_integration/security_only/tests/common/configure/get_configure.ts +++ b/x-pack/test/case_api_integration/security_only/tests/common/configure/get_configure.ts @@ -15,13 +15,13 @@ import { ensureSavedObjectIsAuthorized, } from '../../../../common/lib/utils'; import { - secOnly, - obsOnlyRead, - secOnlyRead, + secOnlySpacesAll, + obsOnlyReadSpacesAll, + secOnlyReadSpacesAll, noKibanaPrivileges, superUser, globalRead, - obsSecRead, + obsSecReadSpacesAll, } from '../../../../common/lib/authentication/users'; import { obsOnlyDefaultSpaceAuth, @@ -66,10 +66,18 @@ export default ({ getService }: FtrProviderContext): void => { numberOfExpectedCases: 2, owners: ['securitySolutionFixture', 'observabilityFixture'], }, - { user: secOnlyRead, numberOfExpectedCases: 1, owners: ['securitySolutionFixture'] }, - { user: obsOnlyRead, numberOfExpectedCases: 1, owners: ['observabilityFixture'] }, { - user: obsSecRead, + user: secOnlyReadSpacesAll, + numberOfExpectedCases: 1, + owners: ['securitySolutionFixture'], + }, + { + user: obsOnlyReadSpacesAll, + numberOfExpectedCases: 1, + owners: ['observabilityFixture'], + }, + { + user: obsSecReadSpacesAll, numberOfExpectedCases: 2, owners: ['securitySolutionFixture', 'observabilityFixture'], }, @@ -126,7 +134,7 @@ export default ({ getService }: FtrProviderContext): void => { supertest: supertestWithoutAuth, expectedHttpCode: 404, auth: { - user: secOnly, + user: secOnlySpacesAll, space: 'space1', }, }); diff --git a/x-pack/test/case_api_integration/security_only/tests/common/configure/patch_configure.ts b/x-pack/test/case_api_integration/security_only/tests/common/configure/patch_configure.ts index 284a7687c24348..eb1fa01221ae89 100644 --- a/x-pack/test/case_api_integration/security_only/tests/common/configure/patch_configure.ts +++ b/x-pack/test/case_api_integration/security_only/tests/common/configure/patch_configure.ts @@ -16,12 +16,12 @@ import { updateConfiguration, } from '../../../../common/lib/utils'; import { - secOnly, - obsOnlyRead, - secOnlyRead, + secOnlySpacesAll, + obsOnlyReadSpacesAll, + secOnlyReadSpacesAll, noKibanaPrivileges, globalRead, - obsSecRead, + obsSecReadSpacesAll, } from '../../../../common/lib/authentication/users'; import { secOnlyDefaultSpaceAuth, superUserDefaultSpaceAuth } from '../../../utils'; @@ -81,7 +81,13 @@ export default ({ getService }: FtrProviderContext): void => { ); }); - for (const user of [globalRead, secOnlyRead, obsOnlyRead, obsSecRead, noKibanaPrivileges]) { + for (const user of [ + globalRead, + secOnlyReadSpacesAll, + obsOnlyReadSpacesAll, + obsSecReadSpacesAll, + noKibanaPrivileges, + ]) { it(`User ${ user.username } with role(s) ${user.roles.join()} - should NOT update a configuration`, async () => { @@ -125,7 +131,7 @@ export default ({ getService }: FtrProviderContext): void => { }, 404, { - user: secOnly, + user: secOnlySpacesAll, space: 'space1', } ); diff --git a/x-pack/test/case_api_integration/security_only/tests/common/configure/post_configure.ts b/x-pack/test/case_api_integration/security_only/tests/common/configure/post_configure.ts index fdf282d7531d5d..b3de6ec0487bb0 100644 --- a/x-pack/test/case_api_integration/security_only/tests/common/configure/post_configure.ts +++ b/x-pack/test/case_api_integration/security_only/tests/common/configure/post_configure.ts @@ -18,12 +18,12 @@ import { } from '../../../../common/lib/utils'; import { - secOnly, - obsOnlyRead, - secOnlyRead, + secOnlySpacesAll, + obsOnlyReadSpacesAll, + secOnlyReadSpacesAll, noKibanaPrivileges, globalRead, - obsSecRead, + obsSecReadSpacesAll, } from '../../../../common/lib/authentication/users'; import { secOnlyDefaultSpaceAuth, superUserDefaultSpaceAuth } from '../../../utils'; @@ -61,7 +61,13 @@ export default ({ getService }: FtrProviderContext): void => { ); }); - for (const user of [globalRead, secOnlyRead, obsOnlyRead, obsSecRead, noKibanaPrivileges]) { + for (const user of [ + globalRead, + secOnlyReadSpacesAll, + obsOnlyReadSpacesAll, + obsSecReadSpacesAll, + noKibanaPrivileges, + ]) { it(`User ${ user.username } with role(s) ${user.roles.join()} - should NOT create a configuration`, async () => { @@ -83,7 +89,7 @@ export default ({ getService }: FtrProviderContext): void => { { ...getConfigurationRequest(), owner: 'securitySolutionFixture' }, 404, { - user: secOnly, + user: secOnlySpacesAll, space: 'space1', } ); diff --git a/x-pack/test/case_api_integration/security_only/tests/common/user_actions/get_all_user_actions.ts b/x-pack/test/case_api_integration/security_only/tests/common/user_actions/get_all_user_actions.ts index 321381e0c0964b..bd36ce1b0d9d6a 100644 --- a/x-pack/test/case_api_integration/security_only/tests/common/user_actions/get_all_user_actions.ts +++ b/x-pack/test/case_api_integration/security_only/tests/common/user_actions/get_all_user_actions.ts @@ -19,10 +19,10 @@ import { import { globalRead, noKibanaPrivileges, - obsSec, - obsSecRead, - secOnly, - secOnlyRead, + obsSecSpacesAll, + obsSecReadSpacesAll, + secOnlySpacesAll, + secOnlyReadSpacesAll, superUser, } from '../../../../common/lib/authentication/users'; import { superUserDefaultSpaceAuth } from '../../../utils'; @@ -63,7 +63,14 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should get the user actions for a case when the user has the correct permissions', async () => { - for (const user of [globalRead, superUser, secOnly, secOnlyRead, obsSec, obsSecRead]) { + for (const user of [ + globalRead, + superUser, + secOnlySpacesAll, + secOnlyReadSpacesAll, + obsSecSpacesAll, + obsSecReadSpacesAll, + ]) { const userActions = await getCaseUserActions({ supertest: supertestWithoutAuth, caseID: caseInfo.id, @@ -89,7 +96,7 @@ export default ({ getService }: FtrProviderContext): void => { await getCaseUserActions({ supertest: supertestWithoutAuth, caseID: caseInfo.id, - auth: { user: secOnly, space: 'space1' }, + auth: { user: secOnlySpacesAll, space: 'space1' }, expectedHttpCode: 404, }); }); diff --git a/x-pack/test/case_api_integration/security_only/tests/trial/cases/push_case.ts b/x-pack/test/case_api_integration/security_only/tests/trial/cases/push_case.ts index 390859eea41acc..6294400281b92e 100644 --- a/x-pack/test/case_api_integration/security_only/tests/trial/cases/push_case.ts +++ b/x-pack/test/case_api_integration/security_only/tests/trial/cases/push_case.ts @@ -21,10 +21,10 @@ import { import { globalRead, noKibanaPrivileges, - obsOnlyRead, - obsSecRead, - secOnly, - secOnlyRead, + obsOnlyReadSpacesAll, + obsSecReadSpacesAll, + secOnlySpacesAll, + secOnlyReadSpacesAll, } from '../../../../common/lib/authentication/users'; import { secOnlyDefaultSpaceAuth } from '../../../utils'; @@ -83,7 +83,13 @@ export default ({ getService }: FtrProviderContext): void => { }); }); - for (const user of [globalRead, secOnlyRead, obsOnlyRead, obsSecRead, noKibanaPrivileges]) { + for (const user of [ + globalRead, + secOnlyReadSpacesAll, + obsOnlyReadSpacesAll, + obsSecReadSpacesAll, + noKibanaPrivileges, + ]) { it(`User ${ user.username } with role(s) ${user.roles.join()} - should NOT push a case`, async () => { @@ -114,7 +120,7 @@ export default ({ getService }: FtrProviderContext): void => { supertest: supertestWithoutAuth, caseId: postedCase.id, connectorId: connector.id, - auth: { user: secOnly, space: 'space1' }, + auth: { user: secOnlySpacesAll, space: 'space1' }, expectedHttpCode: 404, }); }); diff --git a/x-pack/test/case_api_integration/security_only/tests/trial/index.ts b/x-pack/test/case_api_integration/security_only/tests/trial/index.ts index 5333a82ccf2e6f..550dad5917d452 100644 --- a/x-pack/test/case_api_integration/security_only/tests/trial/index.ts +++ b/x-pack/test/case_api_integration/security_only/tests/trial/index.ts @@ -5,6 +5,8 @@ * 2.0. */ +import { rolesDefaultSpace } from '../../../common/lib/authentication/roles'; +import { usersDefaultSpace } from '../../../common/lib/authentication/users'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; import { createUsersAndRoles, deleteUsersAndRoles } from '../../../common/lib/authentication'; @@ -16,11 +18,11 @@ export default ({ loadTestFile, getService }: FtrProviderContext): void => { before(async () => { // since spaces are disabled this changes each role to have access to all available spaces (it'll just be the default one) - await createUsersAndRoles(getService, ['*']); + await createUsersAndRoles(getService, usersDefaultSpace, rolesDefaultSpace); }); after(async () => { - await deleteUsersAndRoles(getService); + await deleteUsersAndRoles(getService, usersDefaultSpace, rolesDefaultSpace); }); // Trial diff --git a/x-pack/test/case_api_integration/security_only/utils.ts b/x-pack/test/case_api_integration/security_only/utils.ts index 6b2d967bf55b88..7c5764c558bbe4 100644 --- a/x-pack/test/case_api_integration/security_only/utils.ts +++ b/x-pack/test/case_api_integration/security_only/utils.ts @@ -5,10 +5,14 @@ * 2.0. */ -import { obsOnly, obsSec, secOnly } from '../common/lib/authentication/users'; +import { + obsOnlySpacesAll, + obsSecSpacesAll, + secOnlySpacesAll, +} from '../common/lib/authentication/users'; import { getAuthWithSuperUser } from '../common/lib/utils'; -export const secOnlyDefaultSpaceAuth = { user: secOnly, space: null }; -export const obsOnlyDefaultSpaceAuth = { user: obsOnly, space: null }; -export const obsSecDefaultSpaceAuth = { user: obsSec, space: null }; +export const secOnlyDefaultSpaceAuth = { user: secOnlySpacesAll, space: null }; +export const obsOnlyDefaultSpaceAuth = { user: obsOnlySpacesAll, space: null }; +export const obsSecDefaultSpaceAuth = { user: obsSecSpacesAll, space: null }; export const superUserDefaultSpaceAuth = getAuthWithSuperUser(null);