diff --git a/x-pack/scripts/functional_tests.js b/x-pack/scripts/functional_tests.js index e2b3c951b0722..1a7f9acc9f1a3 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 a72141745e577..86016b273ea44 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,7 +24,15 @@ export const createSpaces = async (getService: CommonFtrProviderContext['getServ } }; -const createUsersAndRoles = async (getService: CommonFtrProviderContext['getService']) => { +/** + * 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'], + usersToCreate: User[] = users, + rolesToCreate: Role[] = roles +) => { const security = getService('security'); const createRole = async ({ name, privileges }: Role) => { @@ -42,11 +50,11 @@ const createUsersAndRoles = async (getService: CommonFtrProviderContext['getServ }); }; - 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); } }; @@ -61,10 +69,15 @@ export const deleteSpaces = async (getService: CommonFtrProviderContext['getServ } } }; -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) { @@ -72,7 +85,7 @@ const deleteUsersAndRoles = async (getService: CommonFtrProviderContext['getServ } } - 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 5ddecd9206106..60e50288f8856 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 06add9ae00793..1fa6e3c9f4990 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/common/lib/utils.ts b/x-pack/test/case_api_integration/common/lib/utils.ts index b7a713b6316cb..855cf513f16d5 100644 --- a/x-pack/test/case_api_integration/common/lib/utils.ts +++ b/x-pack/test/case_api_integration/common/lib/utils.ts @@ -546,10 +546,19 @@ 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 }; } +/** + * 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/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 03bcf0d538fe3..bbb9624c4b14b 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/config.ts b/x-pack/test/case_api_integration/security_only/config.ts new file mode 100644 index 0000000000000..5946b8d25b464 --- /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 0000000000000..9575bd99112f6 --- /dev/null +++ b/x-pack/test/case_api_integration/security_only/tests/common/alerts/get_cases.ts @@ -0,0 +1,242 @@ +/* + * 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, + obsOnlyReadSpacesAll, + obsSecSpacesAll, + obsSecReadSpacesAll, + secOnlyReadSpacesAll, + superUser, +} from '../../../../common/lib/authentication/users'; +import { + obsOnlyDefaultSpaceAuth, + secOnlyDefaultSpaceAuth, + superUserDefaultSpaceAuth, + obsSecDefaultSpaceAuth, +} 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_cases using alertID', () => { + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + it('should return the correct case IDs', async () => { + const [case1, case2, case3] = await Promise.all([ + createCase(supertestWithoutAuth, getPostCaseRequest(), 200, secOnlyDefaultSpaceAuth), + createCase(supertestWithoutAuth, getPostCaseRequest(), 200, secOnlyDefaultSpaceAuth), + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + obsOnlyDefaultSpaceAuth + ), + ]); + + await Promise.all([ + createComment({ + supertest: supertestWithoutAuth, + caseId: case1.id, + params: postCommentAlertReq, + auth: secOnlyDefaultSpaceAuth, + }), + createComment({ + supertest: supertestWithoutAuth, + caseId: case2.id, + params: postCommentAlertReq, + auth: secOnlyDefaultSpaceAuth, + }), + createComment({ + supertest: supertestWithoutAuth, + caseId: case3.id, + params: { ...postCommentAlertReq, owner: 'observabilityFixture' }, + auth: obsOnlyDefaultSpaceAuth, + }), + ]); + + for (const scenario of [ + { + user: globalRead, + caseIDs: [case1.id, case2.id, case3.id], + }, + { + user: superUser, + caseIDs: [case1.id, case2.id, case3.id], + }, + { user: secOnlyReadSpacesAll, caseIDs: [case1.id, case2.id] }, + { user: obsOnlyReadSpacesAll, caseIDs: [case3.id] }, + { + user: obsSecReadSpacesAll, + 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); + } + } + }); + + 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: superUserDefaultSpaceAuth, + }); + + 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 [case1, case2] = await Promise.all([ + createCase(supertestWithoutAuth, getPostCaseRequest(), 200, obsSecDefaultSpaceAuth), + createCase( + supertestWithoutAuth, + { ...getPostCaseRequest(), owner: 'observabilityFixture' }, + 200, + obsSecDefaultSpaceAuth + ), + ]); + + await Promise.all([ + createComment({ + supertest: supertestWithoutAuth, + caseId: case1.id, + params: postCommentAlertReq, + auth: obsSecDefaultSpaceAuth, + }), + createComment({ + supertest: supertestWithoutAuth, + caseId: case2.id, + params: { ...postCommentAlertReq, owner: 'observabilityFixture' }, + auth: obsSecDefaultSpaceAuth, + }), + ]); + + await getCaseIDsByAlert({ + supertest: supertestWithoutAuth, + alertID: postCommentAlertReq.alertId as string, + auth: { user: obsSecSpacesAll, space: 'space1' }, + query: { owner: 'securitySolutionFixture' }, + expectedHttpCode: 404, + }); + }); + + it('should respect the owner filter when have permissions', async () => { + const [case1, case2] = await Promise.all([ + createCase(supertestWithoutAuth, getPostCaseRequest(), 200, obsSecDefaultSpaceAuth), + createCase( + supertestWithoutAuth, + { ...getPostCaseRequest(), owner: 'observabilityFixture' }, + 200, + obsSecDefaultSpaceAuth + ), + ]); + + await Promise.all([ + createComment({ + supertest: supertestWithoutAuth, + caseId: case1.id, + params: postCommentAlertReq, + auth: obsSecDefaultSpaceAuth, + }), + createComment({ + supertest: supertestWithoutAuth, + caseId: case2.id, + params: { ...postCommentAlertReq, owner: 'observabilityFixture' }, + auth: obsSecDefaultSpaceAuth, + }), + ]); + + const res = await getCaseIDsByAlert({ + supertest: supertestWithoutAuth, + alertID: postCommentAlertReq.alertId as string, + auth: obsSecDefaultSpaceAuth, + 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 [case1, case2] = await Promise.all([ + createCase(supertestWithoutAuth, getPostCaseRequest(), 200, obsSecDefaultSpaceAuth), + createCase( + supertestWithoutAuth, + { ...getPostCaseRequest(), owner: 'observabilityFixture' }, + 200, + obsSecDefaultSpaceAuth + ), + ]); + + await Promise.all([ + createComment({ + supertest: supertestWithoutAuth, + caseId: case1.id, + params: postCommentAlertReq, + auth: obsSecDefaultSpaceAuth, + }), + createComment({ + supertest: supertestWithoutAuth, + caseId: case2.id, + params: { ...postCommentAlertReq, owner: 'observabilityFixture' }, + auth: obsSecDefaultSpaceAuth, + }), + ]); + + const res = await getCaseIDsByAlert({ + supertest: supertestWithoutAuth, + alertID: postCommentAlertReq.alertId as string, + auth: secOnlyDefaultSpaceAuth, + // The secOnlyDefaultSpace 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 0000000000000..9ece177b21491 --- /dev/null +++ b/x-pack/test/case_api_integration/security_only/tests/common/cases/delete_cases.ts @@ -0,0 +1,157 @@ +/* + * 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 { getPostCaseRequest } from '../../../../common/lib/mock'; +import { + deleteCasesByESQuery, + deleteCasesUserActions, + deleteComments, + createCase, + deleteCases, + getCase, +} from '../../../../common/lib/utils'; +import { + secOnlySpacesAll, + secOnlyReadSpacesAll, + globalRead, + obsOnlyReadSpacesAll, + obsSecReadSpacesAll, + noKibanaPrivileges, +} from '../../../../common/lib/authentication/users'; +import { + obsOnlyDefaultSpaceAuth, + secOnlyDefaultSpaceAuth, + superUserDefaultSpaceAuth, +} from '../../../utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const es = getService('es'); + + describe('delete_cases', () => { + afterEach(async () => { + await deleteCasesByESQuery(es); + await deleteComments(es); + await deleteCasesUserActions(es); + }); + + it('User: security solution only - should delete a case', async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest(), + 200, + secOnlyDefaultSpaceAuth + ); + + await deleteCases({ + supertest: supertestWithoutAuth, + caseIDs: [postedCase.id], + expectedHttpCode: 204, + auth: secOnlyDefaultSpaceAuth, + }); + }); + + it('User: security solution only - should NOT delete a case of different owner', async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest(), + 200, + secOnlyDefaultSpaceAuth + ); + + await deleteCases({ + supertest: supertestWithoutAuth, + caseIDs: [postedCase.id], + expectedHttpCode: 403, + auth: obsOnlyDefaultSpaceAuth, + }); + }); + + it('should get an error if the user has not permissions to all requested cases', async () => { + const caseSec = await createCase( + supertestWithoutAuth, + getPostCaseRequest(), + 200, + secOnlyDefaultSpaceAuth + ); + + const caseObs = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + obsOnlyDefaultSpaceAuth + ); + + await deleteCases({ + supertest: supertestWithoutAuth, + caseIDs: [caseSec.id, caseObs.id], + expectedHttpCode: 403, + auth: obsOnlyDefaultSpaceAuth, + }); + + // Cases should have not been deleted. + await getCase({ + supertest: supertestWithoutAuth, + caseId: caseSec.id, + expectedHttpCode: 200, + auth: superUserDefaultSpaceAuth, + }); + + await getCase({ + supertest: supertestWithoutAuth, + caseId: caseObs.id, + expectedHttpCode: 200, + auth: superUserDefaultSpaceAuth, + }); + }); + + 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 () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest(), + 200, + superUserDefaultSpaceAuth + ); + + await deleteCases({ + supertest: supertestWithoutAuth, + caseIDs: [postedCase.id], + expectedHttpCode: 403, + auth: { user, space: null }, + }); + }); + } + + it('should return a 404 when attempting to access a space', async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest(), + 200, + superUserDefaultSpaceAuth + ); + + await deleteCases({ + supertest: supertestWithoutAuth, + caseIDs: [postedCase.id], + expectedHttpCode: 404, + 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 new file mode 100644 index 0000000000000..711eccbe16278 --- /dev/null +++ b/x-pack/test/case_api_integration/security_only/tests/common/cases/find_cases.ts @@ -0,0 +1,245 @@ +/* + * 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 { CASES_URL } from '../../../../../../plugins/cases/common/constants'; +import { getPostCaseRequest } from '../../../../common/lib/mock'; +import { + deleteAllCaseItems, + ensureSavedObjectIsAuthorized, + findCases, + createCase, +} from '../../../../common/lib/utils'; +import { + secOnlySpacesAll, + obsOnlyReadSpacesAll, + secOnlyReadSpacesAll, + noKibanaPrivileges, + superUser, + globalRead, + obsSecReadSpacesAll, +} from '../../../../common/lib/authentication/users'; +import { + obsOnlyDefaultSpaceAuth, + obsSecDefaultSpaceAuth, + secOnlyDefaultSpaceAuth, + superUserDefaultSpaceAuth, +} 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'); + + describe('find_cases', () => { + 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, + secOnlyDefaultSpaceAuth + ), + // Create case owned by the observability user + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + obsOnlyDefaultSpaceAuth + ), + ]); + + for (const scenario of [ + { + user: globalRead, + numberOfExpectedCases: 2, + owners: ['securitySolutionFixture', 'observabilityFixture'], + }, + { + user: superUser, + numberOfExpectedCases: 2, + owners: ['securitySolutionFixture', 'observabilityFixture'], + }, + { + user: secOnlyReadSpacesAll, + numberOfExpectedCases: 1, + owners: ['securitySolutionFixture'], + }, + { + user: obsOnlyReadSpacesAll, + numberOfExpectedCases: 1, + owners: ['observabilityFixture'], + }, + { + user: obsSecReadSpacesAll, + numberOfExpectedCases: 2, + owners: ['securitySolutionFixture', 'observabilityFixture'], + }, + ]) { + const res = await findCases({ + supertest: supertestWithoutAuth, + auth: { + user: scenario.user, + space: null, + }, + }); + + ensureSavedObjectIsAuthorized(res.cases, scenario.numberOfExpectedCases, scenario.owners); + } + }); + + it(`User ${ + noKibanaPrivileges.username + } with role(s) ${noKibanaPrivileges.roles.join()} - should NOT read a case`, async () => { + await createCase(supertestWithoutAuth, getPostCaseRequest(), 200, superUserDefaultSpaceAuth); + + await findCases({ + supertest: supertestWithoutAuth, + auth: { + user: noKibanaPrivileges, + space: null, + }, + expectedHttpCode: 403, + }); + }); + + it('should return a 404 when attempting to access a space', async () => { + await createCase(supertestWithoutAuth, getPostCaseRequest(), 200, superUserDefaultSpaceAuth); + + await findCases({ + supertest: supertestWithoutAuth, + auth: { user: secOnlySpacesAll, space: 'space1' }, + expectedHttpCode: 404, + }); + }); + + 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, superUserDefaultSpaceAuth), + // super user creates a case with owner observabilityFixture + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + superUserDefaultSpaceAuth + ), + ]); + + const res = await findCases({ + supertest: supertestWithoutAuth, + query: { + search: 'securitySolutionFixture observabilityFixture', + searchFields: 'owner', + }, + auth: secOnlyDefaultSpaceAuth, + }); + + 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, + obsSecDefaultSpaceAuth + ), + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + obsOnlyDefaultSpaceAuth + ), + ]); + + const res = await findCases({ + supertest: supertestWithoutAuth, + query: { + owner: 'securitySolutionFixture', + searchFields: 'owner', + }, + auth: obsSecDefaultSpaceAuth, + }); + + 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, + obsSecDefaultSpaceAuth + ), + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + obsSecDefaultSpaceAuth + ), + ]); + + // User with permissions only to security solution request cases from observability + const res = await findCases({ + supertest: supertestWithoutAuth, + query: { + owner: ['securitySolutionFixture', 'observabilityFixture'], + }, + auth: secOnlyDefaultSpaceAuth, + }); + + // 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 0000000000000..3bdb4c5ed310e --- /dev/null +++ b/x-pack/test/case_api_integration/security_only/tests/common/cases/get_case.ts @@ -0,0 +1,144 @@ +/* + * 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 { postCommentUserReq, getPostCaseRequest } from '../../../../common/lib/mock'; +import { + deleteCasesByESQuery, + createCase, + getCase, + createComment, + removeServerGeneratedPropertiesFromSavedObject, +} from '../../../../common/lib/utils'; +import { + secOnlySpacesAll, + obsOnlySpacesAll, + globalRead, + superUser, + secOnlyReadSpacesAll, + obsOnlyReadSpacesAll, + obsSecReadSpacesAll, + noKibanaPrivileges, + obsSecSpacesAll, +} from '../../../../common/lib/authentication/users'; +import { getUserInfo } from '../../../../common/lib/authentication'; +import { secOnlyDefaultSpaceAuth, superUserDefaultSpaceAuth } from '../../../utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const es = getService('es'); + + describe('get_case', () => { + afterEach(async () => { + await deleteCasesByESQuery(es); + }); + + it('should get a case', async () => { + const newCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest(), + 200, + superUserDefaultSpaceAuth + ); + + for (const user of [ + globalRead, + superUser, + secOnlySpacesAll, + secOnlyReadSpacesAll, + obsSecSpacesAll, + obsSecReadSpacesAll, + ]) { + const theCase = await getCase({ + supertest: supertestWithoutAuth, + caseId: newCase.id, + auth: { user, space: null }, + }); + + expect(theCase.owner).to.eql('securitySolutionFixture'); + } + }); + + it('should get a case with comments', async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest(), + 200, + secOnlyDefaultSpaceAuth + ); + + await createComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + params: postCommentUserReq, + expectedHttpCode: 200, + auth: secOnlyDefaultSpaceAuth, + }); + + const theCase = await getCase({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + includeComments: true, + auth: secOnlyDefaultSpaceAuth, + }); + + 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(secOnlySpacesAll), + 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(), + 200, + superUserDefaultSpaceAuth + ); + + for (const user of [noKibanaPrivileges, obsOnlySpacesAll, obsOnlyReadSpacesAll]) { + await getCase({ + supertest: supertestWithoutAuth, + caseId: newCase.id, + expectedHttpCode: 403, + auth: { user, space: null }, + }); + } + }); + + it('should return a 404 when attempting to access a space', async () => { + const newCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest(), + 200, + superUserDefaultSpaceAuth + ); + + await getCase({ + supertest: supertestWithoutAuth, + caseId: newCase.id, + expectedHttpCode: 404, + 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 new file mode 100644 index 0000000000000..bfab3fce7adbe --- /dev/null +++ b/x-pack/test/case_api_integration/security_only/tests/common/cases/patch_cases.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 { getPostCaseRequest, postCaseReq } from '../../../../common/lib/mock'; +import { + deleteAllCaseItems, + createCase, + updateCase, + findCases, + getAuthWithSuperUser, +} from '../../../../common/lib/utils'; + +import { + globalRead, + noKibanaPrivileges, + obsOnlyReadSpacesAll, + obsSecReadSpacesAll, + secOnlySpacesAll, + secOnlyReadSpacesAll, + superUser, +} from '../../../../common/lib/authentication/users'; +import { + obsOnlyDefaultSpaceAuth, + secOnlyDefaultSpaceAuth, + superUserDefaultSpaceAuth, +} from '../../../utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + + describe('patch_cases', () => { + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + it('should update a case when the user has the correct permissions', async () => { + const postedCase = await createCase( + supertestWithoutAuth, + postCaseReq, + 200, + secOnlyDefaultSpaceAuth + ); + + const patchedCases = await updateCase({ + supertest: supertestWithoutAuth, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + title: 'new title', + }, + ], + }, + auth: secOnlyDefaultSpaceAuth, + }); + + 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, superUserDefaultSpaceAuth), + createCase(supertestWithoutAuth, postCaseReq, 200, superUserDefaultSpaceAuth), + createCase(supertestWithoutAuth, postCaseReq, 200, superUserDefaultSpaceAuth), + ]); + + 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: secOnlyDefaultSpaceAuth, + }); + + 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, + obsOnlyDefaultSpaceAuth + ); + + await updateCase({ + supertest: supertestWithoutAuth, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + title: 'new title', + }, + ], + }, + auth: secOnlyDefaultSpaceAuth, + 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, + superUserDefaultSpaceAuth + ), + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + superUserDefaultSpaceAuth + ), + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + superUserDefaultSpaceAuth + ), + ]); + + 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: secOnlyDefaultSpaceAuth, + 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); + }); + + 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 () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest(), + 200, + superUserDefaultSpaceAuth + ); + + await updateCase({ + supertest: supertestWithoutAuth, + params: { + cases: [ + { + id: postedCase.id, + version: postedCase.version, + title: 'new title', + }, + ], + }, + 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: 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 new file mode 100644 index 0000000000000..28043d7155e4a --- /dev/null +++ b/x-pack/test/case_api_integration/security_only/tests/common/cases/post_case.ts @@ -0,0 +1,83 @@ +/* + * 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 { getPostCaseRequest } from '../../../../common/lib/mock'; +import { deleteCasesByESQuery, createCase } from '../../../../common/lib/utils'; +import { + secOnlySpacesAll, + secOnlyReadSpacesAll, + globalRead, + obsOnlyReadSpacesAll, + obsSecReadSpacesAll, + noKibanaPrivileges, +} from '../../../../common/lib/authentication/users'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { secOnlyDefaultSpaceAuth } from '../../../utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const es = getService('es'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + describe('post_case', () => { + afterEach(async () => { + await deleteCasesByESQuery(es); + }); + + it('User: security solution only - should create a case', async () => { + const theCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + secOnlyDefaultSpaceAuth + ); + 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, + secOnlyDefaultSpaceAuth + ); + }); + + 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 () => { + await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 403, + { user, space: null } + ); + }); + } + + it('should return a 404 when attempting to access a space', async () => { + await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 404, + { + 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 new file mode 100644 index 0000000000000..4c72dafed053b --- /dev/null +++ b/x-pack/test/case_api_integration/security_only/tests/common/cases/reporters/get_reporters.ts @@ -0,0 +1,155 @@ +/* + * 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 } from '../../../../../common/lib/mock'; +import { createCase, deleteCasesByESQuery, getReporters } from '../../../../../common/lib/utils'; +import { + secOnlySpacesAll, + obsOnlySpacesAll, + globalRead, + superUser, + secOnlyReadSpacesAll, + obsOnlyReadSpacesAll, + obsSecReadSpacesAll, + noKibanaPrivileges, + obsSecSpacesAll, +} from '../../../../../common/lib/authentication/users'; +import { getUserInfo } from '../../../../../common/lib/authentication'; +import { + secOnlyDefaultSpaceAuth, + obsOnlyDefaultSpaceAuth, + superUserDefaultSpaceAuth, + obsSecDefaultSpaceAuth, +} from '../../../../utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const es = getService('es'); + + describe('get_reporters', () => { + afterEach(async () => { + await deleteCasesByESQuery(es); + }); + + it('User: security solution only - should read the correct reporters', async () => { + await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + secOnlyDefaultSpaceAuth + ); + + await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + obsOnlyDefaultSpaceAuth + ); + + for (const scenario of [ + { + user: globalRead, + expectedReporters: [getUserInfo(secOnlySpacesAll), getUserInfo(obsOnlySpacesAll)], + }, + { + user: superUser, + expectedReporters: [getUserInfo(secOnlySpacesAll), getUserInfo(obsOnlySpacesAll)], + }, + { user: secOnlyReadSpacesAll, expectedReporters: [getUserInfo(secOnlySpacesAll)] }, + { user: obsOnlyReadSpacesAll, expectedReporters: [getUserInfo(obsOnlySpacesAll)] }, + { + user: obsSecReadSpacesAll, + expectedReporters: [getUserInfo(secOnlySpacesAll), getUserInfo(obsOnlySpacesAll)], + }, + ]) { + const reporters = await getReporters({ + supertest: supertestWithoutAuth, + expectedHttpCode: 200, + auth: { + user: scenario.user, + space: null, + }, + }); + + expect(reporters).to.eql(scenario.expectedReporters); + } + }); + + 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, superUserDefaultSpaceAuth); + + // user should not be able to get all reporters at the appropriate space + await getReporters({ + supertest: supertestWithoutAuth, + expectedHttpCode: 403, + auth: { user: noKibanaPrivileges, space: null }, + }); + }); + + 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: obsSecSpacesAll, space: 'space1' }, + }); + }); + + it('should respect the owner filter when having permissions', async () => { + await Promise.all([ + createCase(supertestWithoutAuth, getPostCaseRequest(), 200, secOnlyDefaultSpaceAuth), + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + obsOnlyDefaultSpaceAuth + ), + ]); + + const reporters = await getReporters({ + supertest: supertestWithoutAuth, + auth: obsSecDefaultSpaceAuth, + query: { owner: 'securitySolutionFixture' }, + }); + + expect(reporters).to.eql([getUserInfo(secOnlySpacesAll)]); + }); + + it('should return the correct cases when trying to exploit RBAC through the owner query parameter', async () => { + await Promise.all([ + createCase(supertestWithoutAuth, getPostCaseRequest(), 200, secOnlyDefaultSpaceAuth), + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + obsOnlyDefaultSpaceAuth + ), + ]); + + // User with permissions only to security solution request reporters from observability + const reporters = await getReporters({ + supertest: supertestWithoutAuth, + auth: secOnlyDefaultSpaceAuth, + query: { owner: ['securitySolutionFixture', 'observabilityFixture'] }, + }); + + // Only security solution reporters are being returned + 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 new file mode 100644 index 0000000000000..78ca48b04560c --- /dev/null +++ b/x-pack/test/case_api_integration/security_only/tests/common/cases/status/get_status.ts @@ -0,0 +1,131 @@ +/* + * 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 } from '../../../../../common/lib/mock'; +import { + createCase, + updateCase, + getAllCasesStatuses, + deleteAllCaseItems, +} from '../../../../../common/lib/utils'; +import { + globalRead, + noKibanaPrivileges, + obsOnlyReadSpacesAll, + obsSecReadSpacesAll, + secOnlySpacesAll, + secOnlyReadSpacesAll, + superUser, +} from '../../../../../common/lib/authentication/users'; +import { superUserDefaultSpaceAuth } from '../../../../utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const es = getService('es'); + + describe('get_status', () => { + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + 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, superUserDefaultSpaceAuth), + createCase(supertestWithoutAuth, getPostCaseRequest(), 200, superUserDefaultSpaceAuth), + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + superUserDefaultSpaceAuth + ), + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + superUserDefaultSpaceAuth + ), + ]); + + 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: superUserDefaultSpaceAuth, + }); + + for (const scenario of [ + { user: globalRead, stats: { open: 1, inProgress: 2, closed: 1 } }, + { user: superUser, 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, + 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, + }); + } + }); + + 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, superUserDefaultSpaceAuth); + + await getAllCasesStatuses({ + supertest: supertestWithoutAuth, + auth: { user: noKibanaPrivileges, space: null }, + expectedHttpCode: 403, + }); + }); + + it('should return a 404 when attempting to access a space', async () => { + await createCase(supertestWithoutAuth, getPostCaseRequest(), 200, superUserDefaultSpaceAuth); + + await getAllCasesStatuses({ + supertest: supertestWithoutAuth, + 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 new file mode 100644 index 0000000000000..c05d956028752 --- /dev/null +++ b/x-pack/test/case_api_integration/security_only/tests/common/cases/tags/get_tags.ts @@ -0,0 +1,170 @@ +/* + * 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 { + secOnlySpacesAll, + globalRead, + superUser, + secOnlyReadSpacesAll, + obsOnlyReadSpacesAll, + obsSecReadSpacesAll, + noKibanaPrivileges, +} from '../../../../../common/lib/authentication/users'; +import { + secOnlyDefaultSpaceAuth, + obsOnlyDefaultSpaceAuth, + obsSecDefaultSpaceAuth, + superUserDefaultSpaceAuth, +} from '../../../../utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const es = getService('es'); + + describe('get_tags', () => { + afterEach(async () => { + await deleteCasesByESQuery(es); + }); + + it('should read the correct tags', async () => { + await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture', tags: ['sec'] }), + 200, + secOnlyDefaultSpaceAuth + ); + + await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture', tags: ['obs'] }), + 200, + obsOnlyDefaultSpaceAuth + ); + + for (const scenario of [ + { + user: globalRead, + expectedTags: ['sec', 'obs'], + }, + { + user: superUser, + expectedTags: ['sec', 'obs'], + }, + { user: secOnlyReadSpacesAll, expectedTags: ['sec'] }, + { user: obsOnlyReadSpacesAll, expectedTags: ['obs'] }, + { + user: obsSecReadSpacesAll, + expectedTags: ['sec', 'obs'], + }, + ]) { + const tags = await getTags({ + supertest: supertestWithoutAuth, + expectedHttpCode: 200, + auth: { + user: scenario.user, + space: null, + }, + }); + + expect(tags).to.eql(scenario.expectedTags); + } + }); + + 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, + superUserDefaultSpaceAuth + ); + + // user should not be able to get all tags at the appropriate space + await getTags({ + supertest: supertestWithoutAuth, + expectedHttpCode: 403, + auth: { user: noKibanaPrivileges, space: null }, + }); + }); + + 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, + superUserDefaultSpaceAuth + ); + + await getTags({ + supertest: supertestWithoutAuth, + expectedHttpCode: 404, + auth: { user: secOnlySpacesAll, space: 'space1' }, + }); + }); + + it('should respect the owner filter when having permissions', async () => { + await Promise.all([ + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture', tags: ['sec'] }), + 200, + obsSecDefaultSpaceAuth + ), + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture', tags: ['obs'] }), + 200, + obsSecDefaultSpaceAuth + ), + ]); + + const tags = await getTags({ + supertest: supertestWithoutAuth, + auth: obsSecDefaultSpaceAuth, + 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, + obsSecDefaultSpaceAuth + ), + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture', tags: ['obs'] }), + 200, + obsSecDefaultSpaceAuth + ), + ]); + + // User with permissions only to security solution request tags from observability + const tags = await getTags({ + supertest: supertestWithoutAuth, + auth: secOnlyDefaultSpaceAuth, + 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 0000000000000..274879c69c4d5 --- /dev/null +++ b/x-pack/test/case_api_integration/security_only/tests/common/comments/delete_comment.ts @@ -0,0 +1,236 @@ +/* + * 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 { getPostCaseRequest, postCommentUserReq } from '../../../../common/lib/mock'; +import { + deleteAllCaseItems, + deleteCasesByESQuery, + deleteCasesUserActions, + deleteComments, + createCase, + createComment, + deleteComment, + deleteAllComments, + getAuthWithSuperUser, +} from '../../../../common/lib/utils'; +import { + globalRead, + noKibanaPrivileges, + obsOnlyReadSpacesAll, + obsSecReadSpacesAll, + secOnlySpacesAll, + secOnlyReadSpacesAll, +} from '../../../../common/lib/authentication/users'; +import { obsOnlyDefaultSpaceAuth, secOnlyDefaultSpaceAuth } from '../../../utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const es = getService('es'); + const superUserNoSpaceAuth = getAuthWithSuperUser(null); + + describe('delete_comment', () => { + afterEach(async () => { + await deleteCasesByESQuery(es); + await deleteComments(es); + await deleteCasesUserActions(es); + }); + + 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(), + 200, + secOnlyDefaultSpaceAuth + ); + + const commentResp = await createComment({ + supertest: supertestWithoutAuth, + caseId: secCase.id, + params: postCommentUserReq, + auth: secOnlyDefaultSpaceAuth, + }); + + await deleteComment({ + supertest: supertestWithoutAuth, + caseId: secCase.id, + commentId: commentResp.comments![0].id, + auth: secOnlyDefaultSpaceAuth, + }); + }); + + it('should delete multiple comments from the appropriate owner', async () => { + const secCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest(), + 200, + secOnlyDefaultSpaceAuth + ); + + await createComment({ + supertest: supertestWithoutAuth, + caseId: secCase.id, + params: postCommentUserReq, + auth: secOnlyDefaultSpaceAuth, + }); + + await createComment({ + supertest: supertestWithoutAuth, + caseId: secCase.id, + params: postCommentUserReq, + auth: secOnlyDefaultSpaceAuth, + }); + + await deleteAllComments({ + supertest: supertestWithoutAuth, + caseId: secCase.id, + auth: secOnlyDefaultSpaceAuth, + }); + }); + + it('should not delete a comment from a different owner', async () => { + const secCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest(), + 200, + secOnlyDefaultSpaceAuth + ); + + const commentResp = await createComment({ + supertest: supertestWithoutAuth, + caseId: secCase.id, + params: postCommentUserReq, + auth: secOnlyDefaultSpaceAuth, + }); + + await deleteComment({ + supertest: supertestWithoutAuth, + caseId: secCase.id, + commentId: commentResp.comments![0].id, + auth: obsOnlyDefaultSpaceAuth, + expectedHttpCode: 403, + }); + + await deleteAllComments({ + supertest: supertestWithoutAuth, + caseId: secCase.id, + auth: obsOnlyDefaultSpaceAuth, + expectedHttpCode: 403, + }); + }); + + for (const user of [ + globalRead, + secOnlyReadSpacesAll, + obsOnlyReadSpacesAll, + obsSecReadSpacesAll, + ]) { + it(`User ${ + user.username + } with role(s) ${user.roles.join()} - should NOT delete a comment`, 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, space: null }, + expectedHttpCode: 403, + }); + + await deleteAllComments({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + auth: { user, space: null }, + expectedHttpCode: 403, + }); + }); + } + + 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: superUserNoSpaceAuth, + }); + + 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: 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: secOnlySpacesAll, space: 'space1' }, + expectedHttpCode: 404, + }); + + await deleteAllComments({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + 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 new file mode 100644 index 0000000000000..5239c616603a8 --- /dev/null +++ b/x-pack/test/case_api_integration/security_only/tests/common/comments/find_comments.ts @@ -0,0 +1,278 @@ +/* + * 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 } from '../../../../../../plugins/cases/common/api'; +import { + getPostCaseRequest, + postCommentAlertReq, + postCommentUserReq, +} from '../../../../common/lib/mock'; +import { + createComment, + deleteAllCaseItems, + deleteCasesByESQuery, + deleteCasesUserActions, + deleteComments, + ensureSavedObjectIsAuthorized, + getSpaceUrlPrefix, + createCase, +} from '../../../../common/lib/utils'; + +import { + secOnlySpacesAll, + obsOnlyReadSpacesAll, + secOnlyReadSpacesAll, + noKibanaPrivileges, + superUser, + globalRead, + obsSecReadSpacesAll, +} from '../../../../common/lib/authentication/users'; +import { + obsOnlyDefaultSpaceAuth, + secOnlyDefaultSpaceAuth, + superUserDefaultSpaceAuth, +} from '../../../utils'; + +// 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); + }); + + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + 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, secOnlyDefaultSpaceAuth), + createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'observabilityFixture' }), + 200, + obsOnlyDefaultSpaceAuth + ), + // Create case owned by the observability user + ]); + + await Promise.all([ + createComment({ + supertest: supertestWithoutAuth, + caseId: secCase.id, + params: postCommentUserReq, + auth: secOnlyDefaultSpaceAuth, + }), + createComment({ + supertest: supertestWithoutAuth, + caseId: obsCase.id, + params: { ...postCommentAlertReq, owner: 'observabilityFixture' }, + auth: obsOnlyDefaultSpaceAuth, + }), + ]); + + 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: secOnlyReadSpacesAll, + numExpectedEntites: 1, + owners: ['securitySolutionFixture'], + caseID: secCase.id, + }, + { + user: obsOnlyReadSpacesAll, + numExpectedEntites: 1, + owners: ['observabilityFixture'], + caseID: obsCase.id, + }, + { + user: obsSecReadSpacesAll, + numExpectedEntites: 1, + owners: ['securitySolutionFixture', 'observabilityFixture'], + caseID: secCase.id, + }, + { + user: obsSecReadSpacesAll, + 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); + + ensureSavedObjectIsAuthorized( + caseComments.comments, + scenario.numExpectedEntites, + scenario.owners + ); + } + }); + + 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, + superUserDefaultSpaceAuth + ); + + await createComment({ + supertest: supertestWithoutAuth, + auth: { user: superUser, space: null }, + params: { ...postCommentUserReq, owner: 'securitySolutionFixture' }, + caseId: caseInfo.id, + }); + + // 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); + }); + + it('should return a 404 when attempting to access a space', async () => { + const caseInfo = await createCase( + supertestWithoutAuth, + getPostCaseRequest(), + 200, + superUserDefaultSpaceAuth + ); + + await createComment({ + supertest: supertestWithoutAuth, + auth: superUserDefaultSpaceAuth, + params: { ...postCommentUserReq, owner: 'securitySolutionFixture' }, + caseId: caseInfo.id, + }); + + await supertestWithoutAuth + .get(`${getSpaceUrlPrefix('space1')}${CASES_URL}/${caseInfo.id}/comments/_find`) + .auth(secOnlySpacesAll.username, secOnlySpacesAll.password) + .expect(404); + }); + + 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, + superUserDefaultSpaceAuth + ); + + await createComment({ + supertest: supertestWithoutAuth, + auth: superUserDefaultSpaceAuth, + params: { ...postCommentUserReq, owner: 'observabilityFixture' }, + caseId: obsCase.id, + }); + + const { body: res }: { body: CommentsResponse } = await supertestWithoutAuth + .get( + `${getSpaceUrlPrefix(null)}${CASES_URL}/${ + obsCase.id + }/comments/_find?search=securitySolutionFixture+observabilityFixture` + ) + .auth(secOnlySpacesAll.username, secOnlySpacesAll.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, + superUserDefaultSpaceAuth + ); + + await createComment({ + supertest: supertestWithoutAuth, + auth: superUserDefaultSpaceAuth, + params: { ...postCommentUserReq, owner: 'observabilityFixture' }, + caseId: obsCase.id, + }); + + const { body: res } = await supertestWithoutAuth + .get( + `${getSpaceUrlPrefix(null)}${CASES_URL}/${ + obsCase.id + }/comments/_find?filter=cases-comments.attributes.owner:"observabilityFixture"` + ) + .auth(secOnlySpacesAll.username, secOnlySpacesAll.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 0000000000000..a0010ef19499f --- /dev/null +++ b/x-pack/test/case_api_integration/security_only/tests/common/comments/get_all_comments.ts @@ -0,0 +1,139 @@ +/* + * 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, postCommentUserReq } from '../../../../common/lib/mock'; +import { + deleteAllCaseItems, + createCase, + createComment, + getAllComments, +} from '../../../../common/lib/utils'; +import { + globalRead, + noKibanaPrivileges, + obsOnlySpacesAll, + obsOnlyReadSpacesAll, + obsSecSpacesAll, + obsSecReadSpacesAll, + secOnlySpacesAll, + secOnlyReadSpacesAll, + superUser, +} from '../../../../common/lib/authentication/users'; +import { superUserDefaultSpaceAuth } from '../../../utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const es = getService('es'); + + describe('get_all_comments', () => { + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + it('should get all comments when the user has the correct permissions', async () => { + const caseInfo = await createCase( + supertestWithoutAuth, + getPostCaseRequest(), + 200, + superUserDefaultSpaceAuth + ); + + await createComment({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + params: postCommentUserReq, + auth: superUserDefaultSpaceAuth, + }); + + await createComment({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + params: postCommentUserReq, + auth: superUserDefaultSpaceAuth, + }); + + for (const user of [ + globalRead, + superUser, + secOnlySpacesAll, + secOnlyReadSpacesAll, + obsSecSpacesAll, + obsSecReadSpacesAll, + ]) { + const comments = await getAllComments({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + auth: { user, space: null }, + }); + + 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(), + 200, + superUserDefaultSpaceAuth + ); + + await createComment({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + params: postCommentUserReq, + auth: superUserDefaultSpaceAuth, + }); + + for (const scenario of [ + { user: noKibanaPrivileges, returnCode: 403 }, + { user: obsOnlySpacesAll, returnCode: 200 }, + { user: obsOnlyReadSpacesAll, returnCode: 200 }, + ]) { + const comments = await getAllComments({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + auth: { user: scenario.user, space: null }, + 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 return a 404 when attempting to access a space', async () => { + const caseInfo = await createCase( + supertestWithoutAuth, + getPostCaseRequest(), + 200, + superUserDefaultSpaceAuth + ); + + await createComment({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + params: postCommentUserReq, + auth: superUserDefaultSpaceAuth, + }); + + await getAllComments({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + 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 new file mode 100644 index 0000000000000..79693d3e0a574 --- /dev/null +++ b/x-pack/test/case_api_integration/security_only/tests/common/comments/get_comment.ts @@ -0,0 +1,123 @@ +/* + * 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 { postCommentUserReq, getPostCaseRequest } from '../../../../common/lib/mock'; +import { + deleteAllCaseItems, + createCase, + createComment, + getComment, +} from '../../../../common/lib/utils'; +import { + globalRead, + noKibanaPrivileges, + obsOnlySpacesAll, + obsOnlyReadSpacesAll, + obsSecSpacesAll, + obsSecReadSpacesAll, + secOnlySpacesAll, + secOnlyReadSpacesAll, + superUser, +} from '../../../../common/lib/authentication/users'; +import { superUserDefaultSpaceAuth } from '../../../utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const es = getService('es'); + + describe('get_comment', () => { + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + it('should get a comment when the user has the correct permissions', async () => { + const caseInfo = await createCase( + supertestWithoutAuth, + getPostCaseRequest(), + 200, + superUserDefaultSpaceAuth + ); + + const caseWithComment = await createComment({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + params: postCommentUserReq, + auth: superUserDefaultSpaceAuth, + }); + + for (const user of [ + globalRead, + superUser, + secOnlySpacesAll, + secOnlyReadSpacesAll, + obsSecSpacesAll, + obsSecReadSpacesAll, + ]) { + await getComment({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + commentId: caseWithComment.comments![0].id, + auth: { user, space: null }, + }); + } + }); + + it('should not get comment when the user does not have correct permissions', async () => { + const caseInfo = await createCase( + supertestWithoutAuth, + getPostCaseRequest(), + 200, + superUserDefaultSpaceAuth + ); + + const caseWithComment = await createComment({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + params: postCommentUserReq, + auth: superUserDefaultSpaceAuth, + }); + + for (const user of [noKibanaPrivileges, obsOnlySpacesAll, obsOnlyReadSpacesAll]) { + await getComment({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + commentId: caseWithComment.comments![0].id, + 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, + superUserDefaultSpaceAuth + ); + + const caseWithComment = await createComment({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + params: postCommentUserReq, + auth: superUserDefaultSpaceAuth, + }); + + await getComment({ + supertest: supertestWithoutAuth, + caseId: caseInfo.id, + commentId: caseWithComment.comments![0].id, + 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 new file mode 100644 index 0000000000000..7a25ec4ec3981 --- /dev/null +++ b/x-pack/test/case_api_integration/security_only/tests/common/comments/patch_comment.ts @@ -0,0 +1,189 @@ +/* + * 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, CommentType } from '../../../../../../plugins/cases/common/api'; +import { defaultUser, postCommentUserReq, getPostCaseRequest } from '../../../../common/lib/mock'; +import { + deleteAllCaseItems, + deleteCasesByESQuery, + deleteCasesUserActions, + deleteComments, + createCase, + createComment, + updateComment, +} from '../../../../common/lib/utils'; +import { + globalRead, + noKibanaPrivileges, + obsOnlyReadSpacesAll, + obsSecReadSpacesAll, + secOnlySpacesAll, + secOnlyReadSpacesAll, +} from '../../../../common/lib/authentication/users'; +import { + obsOnlyDefaultSpaceAuth, + secOnlyDefaultSpaceAuth, + superUserDefaultSpaceAuth, +} from '../../../utils'; + +// 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); + }); + + 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(), + 200, + superUserDefaultSpaceAuth + ); + + const patchedCase = await createComment({ + supertest, + caseId: postedCase.id, + params: postCommentUserReq, + auth: superUserDefaultSpaceAuth, + }); + + 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: secOnlyDefaultSpaceAuth, + }); + + 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(), + 200, + superUserDefaultSpaceAuth + ); + + const patchedCase = await createComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + params: postCommentUserReq, + auth: superUserDefaultSpaceAuth, + }); + + 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: obsOnlyDefaultSpaceAuth, + expectedHttpCode: 403, + }); + }); + + 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 () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest(), + 200, + superUserDefaultSpaceAuth + ); + + const patchedCase = await createComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + params: postCommentUserReq, + auth: superUserDefaultSpaceAuth, + }); + + 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: null }, + expectedHttpCode: 403, + }); + }); + } + + it('should return a 404 when attempting to access a space', async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest(), + 200, + superUserDefaultSpaceAuth + ); + + const patchedCase = await createComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + params: postCommentUserReq, + auth: superUserDefaultSpaceAuth, + }); + + 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: 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 new file mode 100644 index 0000000000000..500308305d131 --- /dev/null +++ b/x-pack/test/case_api_integration/security_only/tests/common/comments/post_comment.ts @@ -0,0 +1,128 @@ +/* + * 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 { postCommentUserReq, getPostCaseRequest } from '../../../../common/lib/mock'; +import { + deleteAllCaseItems, + deleteCasesByESQuery, + deleteCasesUserActions, + deleteComments, + createCase, + createComment, +} from '../../../../common/lib/utils'; + +import { + globalRead, + noKibanaPrivileges, + obsOnlyReadSpacesAll, + obsSecReadSpacesAll, + secOnlySpacesAll, + secOnlyReadSpacesAll, +} from '../../../../common/lib/authentication/users'; +import { + obsOnlyDefaultSpaceAuth, + secOnlyDefaultSpaceAuth, + superUserDefaultSpaceAuth, +} from '../../../utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const es = getService('es'); + + describe('post_comment', () => { + afterEach(async () => { + await deleteCasesByESQuery(es); + await deleteComments(es); + await deleteCasesUserActions(es); + }); + + 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, + superUserDefaultSpaceAuth + ); + + await createComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + params: postCommentUserReq, + auth: secOnlyDefaultSpaceAuth, + }); + }); + + 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, + obsOnlyDefaultSpaceAuth + ); + + await createComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + params: { ...postCommentUserReq, owner: 'observabilityFixture' }, + auth: secOnlyDefaultSpaceAuth, + expectedHttpCode: 403, + }); + }); + + 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 () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + superUserDefaultSpaceAuth + ); + + await createComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + params: postCommentUserReq, + auth: { user, space: null }, + expectedHttpCode: 403, + }); + }); + } + + it('should return a 404 when attempting to access a space', async () => { + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest({ owner: 'securitySolutionFixture' }), + 200, + superUserDefaultSpaceAuth + ); + + await createComment({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + params: postCommentUserReq, + 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 new file mode 100644 index 0000000000000..0a8b3ebd8981e --- /dev/null +++ b/x-pack/test/case_api_integration/security_only/tests/common/configure/get_configure.ts @@ -0,0 +1,195 @@ +/* + * 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 { + deleteConfiguration, + getConfiguration, + createConfiguration, + getConfigurationRequest, + ensureSavedObjectIsAuthorized, +} from '../../../../common/lib/utils'; +import { + secOnlySpacesAll, + obsOnlyReadSpacesAll, + secOnlyReadSpacesAll, + noKibanaPrivileges, + superUser, + globalRead, + obsSecReadSpacesAll, +} from '../../../../common/lib/authentication/users'; +import { + obsOnlyDefaultSpaceAuth, + obsSecDefaultSpaceAuth, + secOnlyDefaultSpaceAuth, + superUserDefaultSpaceAuth, +} from '../../../utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertestWithoutAuth = getService('supertestWithoutAuth'); + const es = getService('es'); + + describe('get_configure', () => { + afterEach(async () => { + await deleteConfiguration(es); + }); + + it('should return the correct configuration', async () => { + await createConfiguration( + supertestWithoutAuth, + getConfigurationRequest(), + 200, + secOnlyDefaultSpaceAuth + ); + + await createConfiguration( + supertestWithoutAuth, + { ...getConfigurationRequest(), owner: 'observabilityFixture' }, + 200, + obsOnlyDefaultSpaceAuth + ); + + for (const scenario of [ + { + user: globalRead, + numberOfExpectedCases: 2, + owners: ['securitySolutionFixture', 'observabilityFixture'], + }, + { + user: superUser, + numberOfExpectedCases: 2, + owners: ['securitySolutionFixture', 'observabilityFixture'], + }, + { + user: secOnlyReadSpacesAll, + numberOfExpectedCases: 1, + owners: ['securitySolutionFixture'], + }, + { + user: obsOnlyReadSpacesAll, + numberOfExpectedCases: 1, + owners: ['observabilityFixture'], + }, + { + user: obsSecReadSpacesAll, + numberOfExpectedCases: 2, + owners: ['securitySolutionFixture', 'observabilityFixture'], + }, + ]) { + const configuration = await getConfiguration({ + supertest: supertestWithoutAuth, + query: { owner: scenario.owners }, + expectedHttpCode: 200, + auth: { + user: scenario.user, + space: null, + }, + }); + + ensureSavedObjectIsAuthorized( + configuration, + scenario.numberOfExpectedCases, + scenario.owners + ); + } + }); + + 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, + superUserDefaultSpaceAuth + ); + + // user should not be able to read configurations at the appropriate space + await getConfiguration({ + supertest: supertestWithoutAuth, + expectedHttpCode: 403, + auth: { + user: noKibanaPrivileges, + space: null, + }, + }); + }); + + it('should return a 404 when attempting to access a space', async () => { + await createConfiguration( + supertestWithoutAuth, + getConfigurationRequest(), + 200, + superUserDefaultSpaceAuth + ); + + await getConfiguration({ + supertest: supertestWithoutAuth, + expectedHttpCode: 404, + auth: { + user: secOnlySpacesAll, + space: 'space1', + }, + }); + }); + + it('should respect the owner filter when having permissions', async () => { + await Promise.all([ + createConfiguration( + supertestWithoutAuth, + getConfigurationRequest(), + 200, + obsSecDefaultSpaceAuth + ), + createConfiguration( + supertestWithoutAuth, + { ...getConfigurationRequest(), owner: 'observabilityFixture' }, + 200, + obsSecDefaultSpaceAuth + ), + ]); + + const res = await getConfiguration({ + supertest: supertestWithoutAuth, + query: { owner: 'securitySolutionFixture' }, + auth: obsSecDefaultSpaceAuth, + }); + + 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, + obsSecDefaultSpaceAuth + ), + createConfiguration( + supertestWithoutAuth, + { ...getConfigurationRequest(), owner: 'observabilityFixture' }, + 200, + obsSecDefaultSpaceAuth + ), + ]); + + // User with permissions only to security solution request cases from observability + const res = await getConfiguration({ + supertest: supertestWithoutAuth, + query: { owner: ['securitySolutionFixture', 'observabilityFixture'] }, + auth: secOnlyDefaultSpaceAuth, + }); + + // 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/patch_configure.ts b/x-pack/test/case_api_integration/security_only/tests/common/configure/patch_configure.ts new file mode 100644 index 0000000000000..eb1fa01221ae8 --- /dev/null +++ b/x-pack/test/case_api_integration/security_only/tests/common/configure/patch_configure.ts @@ -0,0 +1,140 @@ +/* + * 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, + deleteConfiguration, + createConfiguration, + updateConfiguration, +} from '../../../../common/lib/utils'; +import { + secOnlySpacesAll, + obsOnlyReadSpacesAll, + secOnlyReadSpacesAll, + noKibanaPrivileges, + globalRead, + obsSecReadSpacesAll, +} from '../../../../common/lib/authentication/users'; +import { secOnlyDefaultSpaceAuth, superUserDefaultSpaceAuth } 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'); + + describe('patch_configure', () => { + const actionsRemover = new ActionsRemover(supertest); + + afterEach(async () => { + await deleteConfiguration(es); + await actionsRemover.removeAll(); + }); + + it('User: security solution only - should update a configuration', async () => { + const configuration = await createConfiguration( + supertestWithoutAuth, + getConfigurationRequest(), + 200, + secOnlyDefaultSpaceAuth + ); + + const newConfiguration = await updateConfiguration( + supertestWithoutAuth, + configuration.id, + { + closure_type: 'close-by-pushing', + version: configuration.version, + }, + 200, + secOnlyDefaultSpaceAuth + ); + + 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, + superUserDefaultSpaceAuth + ); + + await updateConfiguration( + supertestWithoutAuth, + configuration.id, + { + closure_type: 'close-by-pushing', + version: configuration.version, + }, + 403, + secOnlyDefaultSpaceAuth + ); + }); + + 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 () => { + const configuration = await createConfiguration( + supertestWithoutAuth, + getConfigurationRequest(), + 200, + superUserDefaultSpaceAuth + ); + + await updateConfiguration( + supertestWithoutAuth, + configuration.id, + { + closure_type: 'close-by-pushing', + version: configuration.version, + }, + 403, + { + user, + space: null, + } + ); + }); + } + + it('should return a 404 when attempting to access a space', async () => { + const configuration = await createConfiguration( + supertestWithoutAuth, + { ...getConfigurationRequest(), owner: 'securitySolutionFixture' }, + 200, + superUserDefaultSpaceAuth + ); + + await updateConfiguration( + supertestWithoutAuth, + configuration.id, + { + closure_type: 'close-by-pushing', + version: configuration.version, + }, + 404, + { + 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 new file mode 100644 index 0000000000000..b3de6ec0487bb --- /dev/null +++ b/x-pack/test/case_api_integration/security_only/tests/common/configure/post_configure.ts @@ -0,0 +1,133 @@ +/* + * 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, + deleteConfiguration, + createConfiguration, + getConfiguration, + ensureSavedObjectIsAuthorized, +} from '../../../../common/lib/utils'; + +import { + secOnlySpacesAll, + obsOnlyReadSpacesAll, + secOnlyReadSpacesAll, + noKibanaPrivileges, + globalRead, + obsSecReadSpacesAll, +} from '../../../../common/lib/authentication/users'; +import { secOnlyDefaultSpaceAuth, superUserDefaultSpaceAuth } 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'); + + describe('post_configure', () => { + const actionsRemover = new ActionsRemover(supertest); + + afterEach(async () => { + await deleteConfiguration(es); + await actionsRemover.removeAll(); + }); + + it('User: security solution only - should create a configuration', async () => { + const configuration = await createConfiguration( + supertestWithoutAuth, + getConfigurationRequest(), + 200, + secOnlyDefaultSpaceAuth + ); + + 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, + secOnlyDefaultSpaceAuth + ); + }); + + 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 () => { + await createConfiguration( + supertestWithoutAuth, + { ...getConfigurationRequest(), owner: 'securitySolutionFixture' }, + 403, + { + user, + space: null, + } + ); + }); + } + + it('should return a 404 when attempting to access a space', async () => { + await createConfiguration( + supertestWithoutAuth, + { ...getConfigurationRequest(), owner: 'securitySolutionFixture' }, + 404, + { + user: secOnlySpacesAll, + space: 'space1', + } + ); + }); + + it('it deletes the correct configurations', async () => { + await createConfiguration( + supertestWithoutAuth, + { ...getConfigurationRequest(), owner: 'securitySolutionFixture' }, + 200, + superUserDefaultSpaceAuth + ); + + /** + * This API call should not delete the previously created configuration + * as it belongs to a different owner + */ + await createConfiguration( + supertestWithoutAuth, + { ...getConfigurationRequest(), owner: 'observabilityFixture' }, + 200, + superUserDefaultSpaceAuth + ); + + const configuration = await getConfiguration({ + supertest: supertestWithoutAuth, + query: { owner: ['securitySolutionFixture', 'observabilityFixture'] }, + auth: superUserDefaultSpaceAuth, + }); + + /** + * 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/index.ts b/x-pack/test/case_api_integration/security_only/tests/common/index.ts new file mode 100644 index 0000000000000..7dd6dd4e22711 --- /dev/null +++ b/x-pack/test/case_api_integration/security_only/tests/common/index.ts @@ -0,0 +1,33 @@ +/* + * 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/patch_configure')); + loadTestFile(require.resolve('./configure/post_configure')); + }); +}; 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 0000000000000..bd36ce1b0d9d6 --- /dev/null +++ b/x-pack/test/case_api_integration/security_only/tests/common/user_actions/get_all_user_actions.ts @@ -0,0 +1,104 @@ +/* + * 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 { CaseResponse, CaseStatuses } from '../../../../../../plugins/cases/common/api'; +import { getPostCaseRequest } from '../../../../common/lib/mock'; +import { + deleteAllCaseItems, + createCase, + updateCase, + getCaseUserActions, +} from '../../../../common/lib/utils'; +import { + globalRead, + noKibanaPrivileges, + obsSecSpacesAll, + obsSecReadSpacesAll, + secOnlySpacesAll, + secOnlyReadSpacesAll, + superUser, +} from '../../../../common/lib/authentication/users'; +import { superUserDefaultSpaceAuth } from '../../../utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const es = getService('es'); + + describe('get_all_user_actions', () => { + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + const supertestWithoutAuth = getService('supertestWithoutAuth'); + + let caseInfo: CaseResponse; + beforeEach(async () => { + caseInfo = await createCase( + supertestWithoutAuth, + getPostCaseRequest(), + 200, + superUserDefaultSpaceAuth + ); + + await updateCase({ + supertest: supertestWithoutAuth, + params: { + cases: [ + { + id: caseInfo.id, + version: caseInfo.version, + status: CaseStatuses.closed, + }, + ], + }, + auth: superUserDefaultSpaceAuth, + }); + }); + + it('should get the user actions for a case when the user has the correct permissions', async () => { + for (const user of [ + globalRead, + superUser, + secOnlySpacesAll, + secOnlyReadSpacesAll, + obsSecSpacesAll, + obsSecReadSpacesAll, + ]) { + const userActions = await getCaseUserActions({ + supertest: supertestWithoutAuth, + caseID: caseInfo.id, + auth: { user, space: null }, + }); + + expect(userActions.length).to.eql(2); + } + }); + + 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, + }); + }); + + it('should return a 404 when attempting to access a space', async () => { + await getCaseUserActions({ + supertest: supertestWithoutAuth, + caseID: caseInfo.id, + 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 new file mode 100644 index 0000000000000..6294400281b92 --- /dev/null +++ b/x-pack/test/case_api_integration/security_only/tests/trial/cases/push_case.ts @@ -0,0 +1,128 @@ +/* + * 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 { ObjectRemover as ActionsRemover } from '../../../../../alerting_api_integration/common/lib'; + +import { getPostCaseRequest } from '../../../../common/lib/mock'; +import { + pushCase, + deleteAllCaseItems, + createCaseWithConnector, +} from '../../../../common/lib/utils'; +import { + ExternalServiceSimulator, + getExternalServiceSimulatorPath, +} from '../../../../../alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin'; +import { + globalRead, + noKibanaPrivileges, + obsOnlyReadSpacesAll, + obsSecReadSpacesAll, + secOnlySpacesAll, + secOnlyReadSpacesAll, +} from '../../../../common/lib/authentication/users'; +import { secOnlyDefaultSpaceAuth } from '../../../utils'; + +// 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 supertestWithoutAuth = getService('supertestWithoutAuth'); + + it('should push a case that the user has permissions for', async () => { + const { postedCase, connector } = await createCaseWithConnector({ + supertest, + servicenowSimulatorURL, + actionsRemover, + }); + + await pushCase({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + connectorId: connector.id, + auth: secOnlyDefaultSpaceAuth, + }); + }); + + it('should not push a case that the user does not have permissions for', async () => { + const { postedCase, connector } = await createCaseWithConnector({ + supertest, + servicenowSimulatorURL, + actionsRemover, + createCaseReq: getPostCaseRequest({ owner: 'observabilityFixture' }), + }); + + await pushCase({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + connectorId: connector.id, + auth: secOnlyDefaultSpaceAuth, + expectedHttpCode: 403, + }); + }); + + 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 () => { + const { postedCase, connector } = await createCaseWithConnector({ + supertest, + servicenowSimulatorURL, + actionsRemover, + }); + + await pushCase({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + connectorId: connector.id, + auth: { user, space: null }, + expectedHttpCode: 403, + }); + }); + } + + 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: 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 new file mode 100644 index 0000000000000..550dad5917d45 --- /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 { 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'; + +// 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, usersDefaultSpace, rolesDefaultSpace); + }); + + after(async () => { + await deleteUsersAndRoles(getService, usersDefaultSpace, rolesDefaultSpace); + }); + + // Trial + loadTestFile(require.resolve('./cases/push_case')); + + // 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 0000000000000..7c5764c558bbe --- /dev/null +++ b/x-pack/test/case_api_integration/security_only/utils.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 { + obsOnlySpacesAll, + obsSecSpacesAll, + secOnlySpacesAll, +} from '../common/lib/authentication/users'; +import { getAuthWithSuperUser } from '../common/lib/utils'; + +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); 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 66759a4dcb39a..0301fa3a930cb 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 5015b9c638617..14d0debe2ac17 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 d67ca29229dd1..7c5035193d465 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,