From 0a95e55c2c9ca87a3693104455224761150a9c63 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Fri, 2 Apr 2021 19:07:20 +0300 Subject: [PATCH] [Cases] RBAC: Create & Find integration tests (#95511) --- x-pack/plugins/cases/common/api/cases/case.ts | 4 +- .../plugins/cases/common/api/runtime_types.ts | 4 +- x-pack/plugins/cases/common/constants.ts | 5 +- .../cases/server/authorization/mock.ts | 20 ++ .../cases/server/client/cases/create.ts | 3 +- .../plugins/cases/server/client/cases/find.ts | 13 +- .../cases/server/client/cases/update.ts | 15 +- x-pack/plugins/cases/server/client/client.ts | 3 +- .../cases/server/client/comments/add.ts | 6 +- .../plugins/cases/server/client/index.test.ts | 3 + x-pack/plugins/cases/server/client/types.ts | 3 +- .../plugins/cases/server/common/utils.test.ts | 32 +-- .../server/connectors/case/index.test.ts | 3 + .../api/__fixtures__/mock_saved_objects.ts | 4 + .../routes/api/__mocks__/request_responses.ts | 1 + .../api/cases/comments/find_comments.ts | 5 +- .../server/routes/api/cases/find_cases.ts | 2 +- .../cases/server/routes/api/cases/helpers.ts | 79 +++++-- .../api/cases/sub_case/patch_sub_cases.ts | 6 +- x-pack/plugins/cases/server/services/index.ts | 50 ++-- .../feature_privilege_builder/index.ts | 2 + .../public/cases/components/create/mock.ts | 1 + .../public/cases/containers/api.test.tsx | 1 + .../public/cases/containers/mock.ts | 1 + .../cases/containers/use_post_case.test.tsx | 1 + x-pack/scripts/functional_tests.js | 3 +- .../case_api_integration/common/config.ts | 50 ++-- .../plugins/observability/kibana.json | 10 + .../plugins/observability/package.json | 14 ++ .../plugins/observability/server/index.ts | 10 + .../plugins/observability/server/plugin.ts | 61 +++++ .../plugins/security_solution/kibana.json | 10 + .../plugins/security_solution/package.json | 14 ++ .../plugins/security_solution/server/index.ts | 10 + .../security_solution/server/plugin.ts | 61 +++++ .../common/lib/authentication/roles.ts | 58 ++++- .../common/lib/authentication/users.ts | 59 ++++- .../case_api_integration/common/lib/mock.ts | 9 + .../case_api_integration/common/lib/utils.ts | 63 +++++- .../tests/common/cases/find_cases.ts | 214 +++++++++++++++++- .../tests/common/cases/post_case.ts | 94 ++++++-- .../common/cases/sub_cases/find_sub_cases.ts | 2 +- 42 files changed, 851 insertions(+), 158 deletions(-) create mode 100644 x-pack/plugins/cases/server/authorization/mock.ts create mode 100644 x-pack/test/case_api_integration/common/fixtures/plugins/observability/kibana.json create mode 100644 x-pack/test/case_api_integration/common/fixtures/plugins/observability/package.json create mode 100644 x-pack/test/case_api_integration/common/fixtures/plugins/observability/server/index.ts create mode 100644 x-pack/test/case_api_integration/common/fixtures/plugins/observability/server/plugin.ts create mode 100644 x-pack/test/case_api_integration/common/fixtures/plugins/security_solution/kibana.json create mode 100644 x-pack/test/case_api_integration/common/fixtures/plugins/security_solution/package.json create mode 100644 x-pack/test/case_api_integration/common/fixtures/plugins/security_solution/server/index.ts create mode 100644 x-pack/test/case_api_integration/common/fixtures/plugins/security_solution/server/plugin.ts diff --git a/x-pack/plugins/cases/common/api/cases/case.ts b/x-pack/plugins/cases/common/api/cases/case.ts index 4050b217556d3..a8b0717104304 100644 --- a/x-pack/plugins/cases/common/api/cases/case.ts +++ b/x-pack/plugins/cases/common/api/cases/case.ts @@ -112,10 +112,10 @@ export const CasesFindRequestRt = rt.partial({ page: NumberFromString, perPage: NumberFromString, search: rt.string, - searchFields: rt.array(rt.string), + searchFields: rt.union([rt.array(rt.string), rt.string]), sortField: rt.string, sortOrder: rt.union([rt.literal('desc'), rt.literal('asc')]), - class: rt.string, + owner: rt.union([rt.array(rt.string), rt.string]), }); export const CaseResponseRt = rt.intersection([ diff --git a/x-pack/plugins/cases/common/api/runtime_types.ts b/x-pack/plugins/cases/common/api/runtime_types.ts index b2ff763838287..9785c0f410744 100644 --- a/x-pack/plugins/cases/common/api/runtime_types.ts +++ b/x-pack/plugins/cases/common/api/runtime_types.ts @@ -58,7 +58,9 @@ const getExcessProps = (props: rt.Props, r: Record): string[] = return ex; }; -export function excess>(codec: C): C { +export function excess | rt.PartialType>( + codec: C +): C { const r = new rt.InterfaceType( codec.name, codec.is, diff --git a/x-pack/plugins/cases/common/constants.ts b/x-pack/plugins/cases/common/constants.ts index c6715f28f13f4..8489787bc5a6f 100644 --- a/x-pack/plugins/cases/common/constants.ts +++ b/x-pack/plugins/cases/common/constants.ts @@ -17,7 +17,6 @@ export const CASE_CONFIGURE_SAVED_OBJECT = 'cases-configure'; export const SAVED_OBJECT_TYPES = [ CASE_SAVED_OBJECT, CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT, - SUB_CASE_SAVED_OBJECT, CASE_USER_ACTION_SAVED_OBJECT, CASE_COMMENT_SAVED_OBJECT, CASE_CONFIGURE_SAVED_OBJECT, @@ -82,3 +81,7 @@ export const SECURITY_SOLUTION_OWNER = 'securitySolution'; * This flag governs enabling the case as a connector feature. It is disabled by default as the feature is not complete. */ export const ENABLE_CASE_CONNECTOR = false; + +if (ENABLE_CASE_CONNECTOR) { + SAVED_OBJECT_TYPES.push(SUB_CASE_SAVED_OBJECT); +} diff --git a/x-pack/plugins/cases/server/authorization/mock.ts b/x-pack/plugins/cases/server/authorization/mock.ts new file mode 100644 index 0000000000000..1fc3395c8e43f --- /dev/null +++ b/x-pack/plugins/cases/server/authorization/mock.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { PublicMethodsOf } from '@kbn/utility-types'; +import { Authorization } from './authorization'; + +type Schema = PublicMethodsOf; +export type AuthorizationMock = jest.Mocked; + +export const createAuthorizationMock = () => { + const mocked: AuthorizationMock = { + ensureAuthorized: jest.fn(), + getFindAuthorizationFilter: jest.fn(), + }; + return mocked; +}; diff --git a/x-pack/plugins/cases/server/client/cases/create.ts b/x-pack/plugins/cases/server/client/cases/create.ts index a03bef06ddb1a..34fdb7aff14a2 100644 --- a/x-pack/plugins/cases/server/client/cases/create.ts +++ b/x-pack/plugins/cases/server/client/cases/create.ts @@ -10,6 +10,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; +import type { PublicMethodsOf } from '@kbn/utility-types'; import { SavedObjectsClientContract, Logger } from 'src/core/server'; import { flattenCaseSavedObject, transformNewCase } from '../../routes/api/utils'; @@ -47,7 +48,7 @@ interface CreateCaseArgs { userActionService: CaseUserActionServiceSetup; theCase: CasePostRequest; logger: Logger; - auth: Authorization; + auth: PublicMethodsOf; } /** diff --git a/x-pack/plugins/cases/server/client/cases/find.ts b/x-pack/plugins/cases/server/client/cases/find.ts index 8907a7f2dacf1..24e8cb6ec5f88 100644 --- a/x-pack/plugins/cases/server/client/cases/find.ts +++ b/x-pack/plugins/cases/server/client/cases/find.ts @@ -11,6 +11,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; +import type { PublicMethodsOf } from '@kbn/utility-types'; import { CasesFindResponse, CasesFindRequest, @@ -18,6 +19,7 @@ import { throwErrors, caseStatuses, CasesFindResponseRt, + excess, } from '../../../common/api'; import { CASE_SAVED_OBJECT } from '../../../common/constants'; @@ -32,7 +34,7 @@ interface FindParams { savedObjectsClient: SavedObjectsClientContract; caseService: CaseServiceSetup; logger: Logger; - auth: Authorization; + auth: PublicMethodsOf; options: CasesFindRequest; } @@ -48,7 +50,7 @@ export const find = async ({ }: FindParams): Promise => { try { const queryParams = pipe( - CasesFindRequestRt.decode(options), + excess(CasesFindRequestRt).decode(options), fold(throwErrors(Boom.badRequest), identity) ); @@ -64,6 +66,7 @@ export const find = async ({ sortByField: queryParams.sortField, status: queryParams.status, caseType: queryParams.type, + owner: queryParams.owner, }; const caseQueries = constructQueryOptions({ ...queryArgs, authorizationFilter }); @@ -72,6 +75,12 @@ export const find = async ({ caseOptions: { ...queryParams, ...caseQueries.case, + searchFields: + queryParams.searchFields != null + ? Array.isArray(queryParams.searchFields) + ? queryParams.searchFields + : [queryParams.searchFields] + : queryParams.searchFields, fields: queryParams.fields ? includeFieldsRequiredForAuthentication(queryParams.fields) : queryParams.fields, diff --git a/x-pack/plugins/cases/server/client/cases/update.ts b/x-pack/plugins/cases/server/client/cases/update.ts index 5f5a2b16f4332..fa9df2060ac5b 100644 --- a/x-pack/plugins/cases/server/client/cases/update.ts +++ b/x-pack/plugins/cases/server/client/cases/update.ts @@ -17,6 +17,8 @@ import { SavedObjectsFindResult, Logger, } from 'kibana/server'; + +import { nodeBuilder } from '../../../../../../src/plugins/data/common'; import { flattenCaseSavedObject, isCommentRequestTypeAlertOrGenAlert, @@ -134,7 +136,13 @@ async function throwIfInvalidUpdateOfTypeWithAlerts({ options: { fields: [], // there should never be generated alerts attached to an individual case but we'll check anyway - filter: `${CASE_COMMENT_SAVED_OBJECT}.attributes.type: ${CommentType.alert} OR ${CASE_COMMENT_SAVED_OBJECT}.attributes.type: ${CommentType.generatedAlert}`, + filter: nodeBuilder.or([ + nodeBuilder.is(`${CASE_COMMENT_SAVED_OBJECT}.attributes.type`, CommentType.alert), + nodeBuilder.is( + `${CASE_COMMENT_SAVED_OBJECT}.attributes.type`, + CommentType.generatedAlert + ), + ]), page: 1, perPage: 1, }, @@ -191,7 +199,10 @@ async function getAlertComments({ id: idsOfCasesToSync, includeSubCaseComments: true, options: { - filter: `${CASE_COMMENT_SAVED_OBJECT}.attributes.type: ${CommentType.alert} OR ${CASE_COMMENT_SAVED_OBJECT}.attributes.type: ${CommentType.generatedAlert}`, + filter: nodeBuilder.or([ + nodeBuilder.is(`${CASE_COMMENT_SAVED_OBJECT}.attributes.type`, CommentType.alert), + nodeBuilder.is(`${CASE_COMMENT_SAVED_OBJECT}.attributes.type`, CommentType.generatedAlert), + ]), }, }); } diff --git a/x-pack/plugins/cases/server/client/client.ts b/x-pack/plugins/cases/server/client/client.ts index c0da5b7bc6bb5..e9bfd1ef754b0 100644 --- a/x-pack/plugins/cases/server/client/client.ts +++ b/x-pack/plugins/cases/server/client/client.ts @@ -5,6 +5,7 @@ * 2.0. */ +import type { PublicMethodsOf } from '@kbn/utility-types'; import { ElasticsearchClient, SavedObjectsClientContract, Logger } from 'src/core/server'; import { CasesClientConstructorArguments, @@ -53,7 +54,7 @@ export class CasesClientHandler implements CasesClient { private readonly _userActionService: CaseUserActionServiceSetup; private readonly _alertsService: AlertServiceContract; private readonly logger: Logger; - private readonly authorization: Authorization; + private readonly authorization: PublicMethodsOf; constructor(clientArgs: CasesClientConstructorArguments) { this._scopedClusterClient = clientArgs.scopedClusterClient; diff --git a/x-pack/plugins/cases/server/client/comments/add.ts b/x-pack/plugins/cases/server/client/comments/add.ts index d8fe985b6c1ea..f077571019f60 100644 --- a/x-pack/plugins/cases/server/client/comments/add.ts +++ b/x-pack/plugins/cases/server/client/comments/add.ts @@ -11,6 +11,7 @@ import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; import { SavedObject, SavedObjectsClientContract, Logger } from 'src/core/server'; +import { nodeBuilder } from '../../../../../../src/plugins/data/common'; import { decodeCommentRequest, isCommentRequestTypeGenAlert } from '../../routes/api/utils'; import { @@ -63,7 +64,10 @@ async function getSubCase({ id: mostRecentSubCase.id, options: { fields: [], - filter: `${CASE_COMMENT_SAVED_OBJECT}.attributes.type: ${CommentType.generatedAlert}`, + filter: nodeBuilder.is( + `${CASE_COMMENT_SAVED_OBJECT}.attributes.type`, + CommentType.generatedAlert + ), page: 1, perPage: 1, }, diff --git a/x-pack/plugins/cases/server/client/index.test.ts b/x-pack/plugins/cases/server/client/index.test.ts index cfb30d6d5bcb6..455e4ae106688 100644 --- a/x-pack/plugins/cases/server/client/index.test.ts +++ b/x-pack/plugins/cases/server/client/index.test.ts @@ -18,6 +18,7 @@ import { createUserActionServiceMock, createAlertServiceMock, } from '../services/mocks'; +import { createAuthorizationMock } from '../authorization/mock'; jest.mock('./client'); import { CasesClientHandler } from './client'; @@ -31,6 +32,7 @@ const caseService = createCaseServiceMock(); const connectorMappingsService = connectorMappingsServiceMock(); const savedObjectsClient = savedObjectsClientMock.create(); const userActionService = createUserActionServiceMock(); +const authorization = createAuthorizationMock(); describe('createExternalCasesClient()', () => { test('it creates the client correctly', async () => { @@ -44,6 +46,7 @@ describe('createExternalCasesClient()', () => { savedObjectsClient, userActionService, logger, + authorization, }); expect(CasesClientHandler).toHaveBeenCalledTimes(1); }); diff --git a/x-pack/plugins/cases/server/client/types.ts b/x-pack/plugins/cases/server/client/types.ts index 1e3251df91aba..b0276fc10aada 100644 --- a/x-pack/plugins/cases/server/client/types.ts +++ b/x-pack/plugins/cases/server/client/types.ts @@ -5,6 +5,7 @@ * 2.0. */ +import type { PublicMethodsOf } from '@kbn/utility-types'; import { ElasticsearchClient, SavedObjectsClientContract, Logger } from 'kibana/server'; import { ActionsClient } from '../../../actions/server'; import { @@ -78,7 +79,7 @@ export interface CasesClientConstructorArguments { userActionService: CaseUserActionServiceSetup; alertsService: AlertServiceContract; logger: Logger; - authorization: Authorization; + authorization: PublicMethodsOf; } export interface ConfigureFields { diff --git a/x-pack/plugins/cases/server/common/utils.test.ts b/x-pack/plugins/cases/server/common/utils.test.ts index 5e6a86358de25..46e73c8b5d79c 100644 --- a/x-pack/plugins/cases/server/common/utils.test.ts +++ b/x-pack/plugins/cases/server/common/utils.test.ts @@ -8,7 +8,7 @@ import { SavedObjectsFindResponse } from 'kibana/server'; import { AssociationType, CommentAttributes, CommentRequest, CommentType } from '../../common/api'; import { transformNewComment } from '../routes/api/utils'; -import { combineFilters, countAlerts, countAlertsForID, groupTotalAlertsByID } from './utils'; +import { countAlerts, countAlertsForID, groupTotalAlertsByID } from './utils'; interface CommentReference { ids: string[]; @@ -47,36 +47,6 @@ function createCommentFindResponse( } describe('common utils', () => { - describe('combineFilters', () => { - it("creates a filter string with two values and'd together", () => { - expect(combineFilters(['a', 'b'], 'AND')).toBe('(a AND b)'); - }); - - it('creates a filter string with three values or together', () => { - expect(combineFilters(['a', 'b', 'c'], 'OR')).toBe('(a OR b OR c)'); - }); - - it('ignores empty strings', () => { - expect(combineFilters(['', 'a', '', 'b'], 'AND')).toBe('(a AND b)'); - }); - - it('returns an empty string if all filters are empty strings', () => { - expect(combineFilters(['', ''], 'OR')).toBe(''); - }); - - it('returns an empty string if the filters are undefined', () => { - expect(combineFilters(undefined, 'OR')).toBe(''); - }); - - it('returns a value without parenthesis when only a single filter is provided', () => { - expect(combineFilters(['a'], 'OR')).toBe('a'); - }); - - it('returns a string without parenthesis when only a single non empty filter is provided', () => { - expect(combineFilters(['', ''], 'AND')).toBe(''); - }); - }); - describe('countAlerts', () => { it('returns 0 when no alerts are found', () => { expect( diff --git a/x-pack/plugins/cases/server/connectors/case/index.test.ts b/x-pack/plugins/cases/server/connectors/case/index.test.ts index c21761dba0acb..95fe562d9e140 100644 --- a/x-pack/plugins/cases/server/connectors/case/index.test.ts +++ b/x-pack/plugins/cases/server/connectors/case/index.test.ts @@ -981,6 +981,7 @@ describe('case connector', () => { settings: { syncAlerts: true, }, + owner: 'securitySolution', }; mockCasesClient.create.mockReturnValue(Promise.resolve(createReturn)); @@ -1077,6 +1078,7 @@ describe('case connector', () => { settings: { syncAlerts: true, }, + owner: 'securitySolution', }, ]; @@ -1168,6 +1170,7 @@ describe('case connector', () => { settings: { syncAlerts: true, }, + owner: 'securitySolution', }; mockCasesClient.addComment.mockReturnValue(Promise.resolve(commentReturn)); diff --git a/x-pack/plugins/cases/server/routes/api/__fixtures__/mock_saved_objects.ts b/x-pack/plugins/cases/server/routes/api/__fixtures__/mock_saved_objects.ts index e37b3a2ac257b..bb4e529192df3 100644 --- a/x-pack/plugins/cases/server/routes/api/__fixtures__/mock_saved_objects.ts +++ b/x-pack/plugins/cases/server/routes/api/__fixtures__/mock_saved_objects.ts @@ -58,6 +58,7 @@ export const mockCases: Array> = [ settings: { syncAlerts: true, }, + owner: 'securitySolution', }, references: [], updated_at: '2019-11-25T21:54:48.952Z', @@ -96,6 +97,7 @@ export const mockCases: Array> = [ settings: { syncAlerts: true, }, + owner: 'securitySolution', }, references: [], updated_at: '2019-11-25T22:32:00.900Z', @@ -138,6 +140,7 @@ export const mockCases: Array> = [ settings: { syncAlerts: true, }, + owner: 'securitySolution', }, references: [], updated_at: '2019-11-25T22:32:17.947Z', @@ -184,6 +187,7 @@ export const mockCases: Array> = [ settings: { syncAlerts: true, }, + owner: 'securitySolution', }, references: [], updated_at: '2019-11-25T22:32:17.947Z', diff --git a/x-pack/plugins/cases/server/routes/api/__mocks__/request_responses.ts b/x-pack/plugins/cases/server/routes/api/__mocks__/request_responses.ts index ae14b44e7dffe..7419452f27c0a 100644 --- a/x-pack/plugins/cases/server/routes/api/__mocks__/request_responses.ts +++ b/x-pack/plugins/cases/server/routes/api/__mocks__/request_responses.ts @@ -27,6 +27,7 @@ export const newCase: CasePostRequest = { settings: { syncAlerts: true, }, + owner: 'securitySolution', }; export const getActions = (): FindActionResult[] => [ diff --git a/x-pack/plugins/cases/server/routes/api/cases/comments/find_comments.ts b/x-pack/plugins/cases/server/routes/api/cases/comments/find_comments.ts index f7ae8db4d96aa..9e23a28c0725b 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/comments/find_comments.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/comments/find_comments.ts @@ -14,6 +14,7 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; +import { esKuery } from '../../../../../../../../src/plugins/data/server'; import { AssociationType, CommentsResponseRt, @@ -63,6 +64,7 @@ export function initFindCaseCommentsApi({ caseService, router, logger }: RouteDe const id = query.subCaseId ?? request.params.case_id; const associationType = query.subCaseId ? AssociationType.subCase : AssociationType.case; + const { filter, ...queryWithoutFilter } = query; const args = query ? { caseService, @@ -75,7 +77,8 @@ export function initFindCaseCommentsApi({ caseService, router, logger }: RouteDe page: defaultPage, perPage: defaultPerPage, sortField: 'created_at', - ...query, + filter: filter != null ? esKuery.fromKueryExpression(filter) : filter, + ...queryWithoutFilter, }, associationType, } diff --git a/x-pack/plugins/cases/server/routes/api/cases/find_cases.ts b/x-pack/plugins/cases/server/routes/api/cases/find_cases.ts index 1c7eed480eedf..7bee574894d39 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/find_cases.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/find_cases.ts @@ -24,7 +24,7 @@ export function initFindCasesApi({ caseService, router, logger }: RouteDeps) { return response.badRequest({ body: 'RouteHandlerContext is not registered for cases' }); } const casesClient = await context.cases.getCasesClient(); - const options = request.body as CasesFindRequest; + const options = request.query as CasesFindRequest; return response.ok({ body: await casesClient.find({ ...options }), diff --git a/x-pack/plugins/cases/server/routes/api/cases/helpers.ts b/x-pack/plugins/cases/server/routes/api/cases/helpers.ts index 7717a5241fe94..697b4d5df7ad1 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/helpers.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/helpers.ts @@ -31,20 +31,18 @@ export const addStatusFilter = ({ appendFilter, type = CASE_SAVED_OBJECT, }: { - status?: CaseStatuses; + status: CaseStatuses; appendFilter?: KueryNode; type?: string; }): KueryNode => { const filters: KueryNode[] = []; - if (status) { - filters.push(nodeBuilder.is(`${type}.attributes.status`, status)); - } + filters.push(nodeBuilder.is(`${type}.attributes.status`, status)); if (appendFilter) { filters.push(appendFilter); } - return nodeBuilder.and(filters); + return filters.length > 1 ? nodeBuilder.and(filters) : filters[0]; }; export const buildFilter = ({ @@ -53,12 +51,17 @@ export const buildFilter = ({ operator, type = CASE_SAVED_OBJECT, }: { - filters: string | string[] | undefined; + filters: string | string[]; field: string; operator: 'or' | 'and'; type?: string; -}): KueryNode => { - const filtersAsArray = Array.isArray(filters) ? filters : filters != null ? [filters] : []; +}): KueryNode | null => { + const filtersAsArray = Array.isArray(filters) ? filters : [filters]; + + if (filtersAsArray.length === 0) { + return null; + } + return nodeBuilder[operator]( filtersAsArray.map((filter) => nodeBuilder.is(`${type}.attributes.${field}`, filter)) ); @@ -96,6 +99,7 @@ export const constructQueryOptions = ({ status, sortByField, caseType, + owner, authorizationFilter, }: { tags?: string | string[]; @@ -103,15 +107,20 @@ export const constructQueryOptions = ({ status?: CaseStatuses; sortByField?: string; caseType?: CaseType; + owner?: string | string[]; authorizationFilter?: KueryNode; }): { case: SavedObjectFindOptionsKueryNode; subCase?: SavedObjectFindOptionsKueryNode } => { - const tagsFilter = buildFilter({ filters: tags, field: 'tags', operator: 'or' }); + const kueryNodeExists = (filter: KueryNode | null | undefined): filter is KueryNode => + filter != null; + + const tagsFilter = buildFilter({ filters: tags ?? [], field: 'tags', operator: 'or' }); const reportersFilter = buildFilter({ - filters: reporters, + filters: reporters ?? [], field: 'created_by.username', operator: 'or', }); const sortField = sortToSnake(sortByField); + const ownerFilter = buildFilter({ filters: owner ?? [], field: 'owner', operator: 'or' }); switch (caseType) { case CaseType.individual: { @@ -123,15 +132,23 @@ export const constructQueryOptions = ({ `${CASE_SAVED_OBJECT}.attributes.type`, CaseType.individual ); - const caseFilters = addStatusFilter({ - status, - appendFilter: nodeBuilder.and([tagsFilter, reportersFilter, typeFilter]), - }); + + const filters: KueryNode[] = [typeFilter, tagsFilter, reportersFilter, ownerFilter].filter( + kueryNodeExists + ); + + const caseFilters = + status != null + ? addStatusFilter({ + status, + appendFilter: filters.length > 1 ? nodeBuilder.and(filters) : filters[0], + }) + : undefined; return { case: { filter: - authorizationFilter != null + authorizationFilter != null && caseFilters != null ? combineFilterWithAuthorizationFilter(caseFilters, authorizationFilter) : caseFilters, sortField, @@ -145,8 +162,13 @@ export const constructQueryOptions = ({ `${CASE_SAVED_OBJECT}.attributes.type`, CaseType.collection ); - const caseFilters = nodeBuilder.and([tagsFilter, reportersFilter, typeFilter]); - const subCaseFilters = addStatusFilter({ status, type: SUB_CASE_SAVED_OBJECT }); + + const filters: KueryNode[] = [typeFilter, tagsFilter, reportersFilter, ownerFilter].filter( + kueryNodeExists + ); + const caseFilters = filters.length > 1 ? nodeBuilder.and(filters) : filters[0]; + const subCaseFilters = + status != null ? addStatusFilter({ status, type: SUB_CASE_SAVED_OBJECT }) : undefined; return { case: { @@ -158,8 +180,8 @@ export const constructQueryOptions = ({ }, subCase: { filter: - authorizationFilter != null - ? combineFilterWithAuthorizationFilter(caseFilters, authorizationFilter) + authorizationFilter != null && subCaseFilters != null + ? combineFilterWithAuthorizationFilter(subCaseFilters, authorizationFilter) : subCaseFilters, sortField, }, @@ -185,10 +207,19 @@ export const constructQueryOptions = ({ CaseType.collection ); - const statusFilter = nodeBuilder.and([addStatusFilter({ status }), typeIndividual]); + const statusFilter = + status != null + ? nodeBuilder.and([addStatusFilter({ status }), typeIndividual]) + : typeIndividual; const statusAndType = nodeBuilder.or([statusFilter, typeParent]); - const caseFilters = nodeBuilder.and([statusAndType, tagsFilter, reportersFilter]); - const subCaseFilters = addStatusFilter({ status, type: SUB_CASE_SAVED_OBJECT }); + + const filters: KueryNode[] = [statusAndType, tagsFilter, reportersFilter, ownerFilter].filter( + kueryNodeExists + ); + + const caseFilters = filters.length > 1 ? nodeBuilder.and(filters) : filters[0]; + const subCaseFilters = + status != null ? addStatusFilter({ status, type: SUB_CASE_SAVED_OBJECT }) : undefined; return { case: { @@ -200,8 +231,8 @@ export const constructQueryOptions = ({ }, subCase: { filter: - authorizationFilter != null - ? combineFilterWithAuthorizationFilter(caseFilters, authorizationFilter) + authorizationFilter != null && subCaseFilters != null + ? combineFilterWithAuthorizationFilter(subCaseFilters, authorizationFilter) : subCaseFilters, sortField, }, diff --git a/x-pack/plugins/cases/server/routes/api/cases/sub_case/patch_sub_cases.ts b/x-pack/plugins/cases/server/routes/api/cases/sub_case/patch_sub_cases.ts index 3808cd3dc45dd..5b623815f027f 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/sub_case/patch_sub_cases.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/sub_case/patch_sub_cases.ts @@ -17,6 +17,7 @@ import { Logger, } from 'kibana/server'; +import { nodeBuilder } from '../../../../../../../../src/plugins/data/common'; import { CasesClient } from '../../../../client'; import { CaseServiceSetup, CaseUserActionServiceSetup } from '../../../../services'; import { @@ -209,7 +210,10 @@ async function getAlertComments({ client, id: ids, options: { - filter: `${CASE_COMMENT_SAVED_OBJECT}.attributes.type: ${CommentType.alert} OR ${CASE_COMMENT_SAVED_OBJECT}.attributes.type: ${CommentType.generatedAlert}`, + filter: nodeBuilder.or([ + nodeBuilder.is(`${CASE_COMMENT_SAVED_OBJECT}.attributes.type`, CommentType.alert), + nodeBuilder.is(`${CASE_COMMENT_SAVED_OBJECT}.attributes.type`, CommentType.generatedAlert), + ]), }, }); } diff --git a/x-pack/plugins/cases/server/services/index.ts b/x-pack/plugins/cases/server/services/index.ts index ccbd806d43984..cb275b3f5d44d 100644 --- a/x-pack/plugins/cases/server/services/index.ts +++ b/x-pack/plugins/cases/server/services/index.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { cloneDeep } from 'lodash'; import { KibanaRequest, Logger, @@ -25,7 +26,6 @@ import { AuthenticatedUser, SecurityPluginSetup } from '../../../security/server import { ESCaseAttributes, CommentAttributes, - SavedObjectFindOptions, User, CommentPatchAttributes, SubCaseAttributes, @@ -82,20 +82,20 @@ interface GetSubCasesArgs extends ClientArgs { interface FindCommentsArgs { client: SavedObjectsClientContract; id: string | string[]; - options?: SavedObjectFindOptions; + options?: SavedObjectFindOptionsKueryNode; } interface FindCaseCommentsArgs { client: SavedObjectsClientContract; id: string | string[]; - options?: SavedObjectFindOptions; + options?: SavedObjectFindOptionsKueryNode; includeSubCaseComments?: boolean; } interface FindSubCaseCommentsArgs { client: SavedObjectsClientContract; id: string | string[]; - options?: SavedObjectFindOptions; + options?: SavedObjectFindOptionsKueryNode; } interface FindCasesArgs extends ClientArgs { @@ -186,7 +186,7 @@ interface FindCommentsByAssociationArgs { client: SavedObjectsClientContract; id: string | string[]; associationType: AssociationType; - options?: SavedObjectFindOptions; + options?: SavedObjectFindOptionsKueryNode; } interface Collection { @@ -419,7 +419,7 @@ export class CaseService implements CaseServiceSetup { if (ENABLE_CASE_CONNECTOR && subCaseOptions) { subCasesTotal = await this.findSubCaseStatusStats({ client, - options: subCaseOptions, + options: cloneDeep(subCaseOptions), ids: caseIds, }); } @@ -493,7 +493,13 @@ export class CaseService implements CaseServiceSetup { associationType, id: ids, options: { - filter: `(${CASE_COMMENT_SAVED_OBJECT}.attributes.type: ${CommentType.alert} OR ${CASE_COMMENT_SAVED_OBJECT}.attributes.type: ${CommentType.generatedAlert})`, + filter: nodeBuilder.or([ + nodeBuilder.is(`${CASE_COMMENT_SAVED_OBJECT}.attributes.type`, CommentType.alert), + nodeBuilder.is( + `${CASE_COMMENT_SAVED_OBJECT}.attributes.type`, + CommentType.generatedAlert + ), + ]), }, }); @@ -768,7 +774,7 @@ export class CaseService implements CaseServiceSetup { this.log.debug(`Attempting to find cases`); return await client.find({ sortField: defaultSortField, - ...options, + ...cloneDeep(options), type: CASE_SAVED_OBJECT, }); } catch (error) { @@ -788,7 +794,7 @@ export class CaseService implements CaseServiceSetup { if (options?.page !== undefined || options?.perPage !== undefined) { return client.find({ sortField: defaultSortField, - ...options, + ...cloneDeep(options), type: SUB_CASE_SAVED_OBJECT, }); } @@ -798,14 +804,14 @@ export class CaseService implements CaseServiceSetup { page: 1, perPage: 1, sortField: defaultSortField, - ...options, + ...cloneDeep(options), type: SUB_CASE_SAVED_OBJECT, }); return client.find({ page: 1, perPage: stats.total, sortField: defaultSortField, - ...options, + ...cloneDeep(options), type: SUB_CASE_SAVED_OBJECT, }); } catch (error) { @@ -875,7 +881,7 @@ export class CaseService implements CaseServiceSetup { return client.find({ type: CASE_COMMENT_SAVED_OBJECT, sortField: defaultSortField, - ...options, + ...cloneDeep(options), }); } // get the total number of comments that are in ES then we'll grab them all in one go @@ -886,7 +892,7 @@ export class CaseService implements CaseServiceSetup { perPage: 1, sortField: defaultSortField, // spread the options after so the caller can override the default behavior if they want - ...options, + ...cloneDeep(options), }); return client.find({ @@ -894,7 +900,7 @@ export class CaseService implements CaseServiceSetup { page: 1, perPage: stats.total, sortField: defaultSortField, - ...options, + ...cloneDeep(options), }); } catch (error) { this.log.error(`Error on GET all comments for ${JSON.stringify(id)}: ${error}`); @@ -929,13 +935,15 @@ export class CaseService implements CaseServiceSetup { let filter: KueryNode | undefined; if (!includeSubCaseComments) { // if other filters were passed in then combine them to filter out sub case comments - filter = nodeBuilder.and([ - options?.filter, - nodeBuilder.is( - `${CASE_COMMENT_SAVED_OBJECT}.attributes.associationType`, - AssociationType.case - ), - ]); + const associationFilter = nodeBuilder.is( + `${CASE_COMMENT_SAVED_OBJECT}.attributes.associationType`, + AssociationType.case + ); + + filter = + options?.filter != null + ? nodeBuilder.and([options?.filter, associationFilter]) + : associationFilter; } this.log.debug(`Attempting to GET all comments for case caseID ${JSON.stringify(id)}`); diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/index.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/index.ts index 21cf2421ce1b2..81d1339052301 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/index.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/index.ts @@ -12,6 +12,7 @@ import type { Actions } from '../../actions'; import { FeaturePrivilegeAlertingBuilder } from './alerting'; import { FeaturePrivilegeApiBuilder } from './api'; import { FeaturePrivilegeAppBuilder } from './app'; +import { FeaturePrivilegeCasesBuilder } from './cases'; import { FeaturePrivilegeCatalogueBuilder } from './catalogue'; import { FeaturePrivilegeBuilder } from './feature_privilege_builder'; import { FeaturePrivilegeManagementBuilder } from './management'; @@ -31,6 +32,7 @@ export const featurePrivilegeBuilderFactory = (actions: Actions): FeaturePrivile new FeaturePrivilegeSavedObjectBuilder(actions), new FeaturePrivilegeUIBuilder(actions), new FeaturePrivilegeAlertingBuilder(actions), + new FeaturePrivilegeCasesBuilder(actions), ]; return { diff --git a/x-pack/plugins/security_solution/public/cases/components/create/mock.ts b/x-pack/plugins/security_solution/public/cases/components/create/mock.ts index 6e17be8d53e5a..277e51f886ab0 100644 --- a/x-pack/plugins/security_solution/public/cases/components/create/mock.ts +++ b/x-pack/plugins/security_solution/public/cases/components/create/mock.ts @@ -24,6 +24,7 @@ export const sampleData: CasePostRequest = { settings: { syncAlerts: true, }, + owner: 'securitySolution', }; export const sampleConnectorData = { loading: false, connectors: [] }; diff --git a/x-pack/plugins/security_solution/public/cases/containers/api.test.tsx b/x-pack/plugins/security_solution/public/cases/containers/api.test.tsx index e6ecf45097a1a..8f0fb3ea5a1d0 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/api.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/api.test.tsx @@ -395,6 +395,7 @@ describe('Case Configuration API', () => { settings: { syncAlerts: true, }, + owner: 'securitySolution', }; test('check url, method, signal', async () => { diff --git a/x-pack/plugins/security_solution/public/cases/containers/mock.ts b/x-pack/plugins/security_solution/public/cases/containers/mock.ts index 6e937fe7760cd..4559f6000493f 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/mock.ts +++ b/x-pack/plugins/security_solution/public/cases/containers/mock.ts @@ -254,6 +254,7 @@ export const basicCaseSnake: CaseResponse = { external_service: null, updated_at: basicUpdatedAt, updated_by: elasticUserSnake, + owner: 'securitySolution', } as CaseResponse; export const casesStatusSnake: CasesStatusResponse = { diff --git a/x-pack/plugins/security_solution/public/cases/containers/use_post_case.test.tsx b/x-pack/plugins/security_solution/public/cases/containers/use_post_case.test.tsx index 3731af4d73db5..5cbbf75d80f39 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/use_post_case.test.tsx +++ b/x-pack/plugins/security_solution/public/cases/containers/use_post_case.test.tsx @@ -28,6 +28,7 @@ describe('usePostCase', () => { settings: { syncAlerts: true, }, + owner: 'securitySolution', }; beforeEach(() => { jest.clearAllMocks(); diff --git a/x-pack/scripts/functional_tests.js b/x-pack/scripts/functional_tests.js index 90306466a9753..c6945282e0742 100644 --- a/x-pack/scripts/functional_tests.js +++ b/x-pack/scripts/functional_tests.js @@ -31,7 +31,8 @@ const onlyNotInCoverageTests = [ require.resolve('../test/alerting_api_integration/basic/config.ts'), require.resolve('../test/alerting_api_integration/spaces_only/config.ts'), require.resolve('../test/alerting_api_integration/security_and_spaces/config.ts'), - require.resolve('../test/case_api_integration/basic/config.ts'), + require.resolve('../test/case_api_integration/security_and_spaces/config_basic.ts'), + require.resolve('../test/case_api_integration/security_and_spaces/config_trial.ts'), require.resolve('../test/apm_api_integration/basic/config.ts'), require.resolve('../test/apm_api_integration/trial/config.ts'), require.resolve('../test/detection_engine_api_integration/security_and_spaces/config.ts'), diff --git a/x-pack/test/case_api_integration/common/config.ts b/x-pack/test/case_api_integration/common/config.ts index fe663cfa8dc07..9b6c066c3f813 100644 --- a/x-pack/test/case_api_integration/common/config.ts +++ b/x-pack/test/case_api_integration/common/config.ts @@ -56,32 +56,10 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) }, }; - const allFiles = fs.readdirSync( - path.resolve( - __dirname, - '..', - '..', - 'alerting_api_integration', - 'common', - 'fixtures', - 'plugins' - ) - ); + // Find all folders in ./fixtures/plugins + const allFiles = fs.readdirSync(path.resolve(__dirname, 'fixtures', 'plugins')); const plugins = allFiles.filter((file) => - fs - .statSync( - path.resolve( - __dirname, - '..', - '..', - 'alerting_api_integration', - 'common', - 'fixtures', - 'plugins', - file - ) - ) - .isDirectory() + fs.statSync(path.resolve(__dirname, 'fixtures', 'plugins', file)).isDirectory() ); return { @@ -109,20 +87,22 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions) ...xPackApiIntegrationTestsConfig.get('kbnTestServer.serverArgs'), `--xpack.actions.allowedHosts=${JSON.stringify(['localhost', 'some.non.existent.com'])}`, `--xpack.actions.enabledActionTypes=${JSON.stringify(enabledActionTypes)}`, + '--xpack.cases.enableAuthorization=true', '--xpack.eventLog.logEntries=true', ...disabledPlugins.map((key) => `--xpack.${key}.enabled=false`), + // Actions simulators plugin. Needed for testing push to external services. + `--plugin-path=${path.resolve( + __dirname, + '..', + '..', + 'alerting_api_integration', + 'common', + 'fixtures', + 'plugins' + )}`, ...plugins.map( (pluginDir) => - `--plugin-path=${path.resolve( - __dirname, - '..', - '..', - 'alerting_api_integration', - 'common', - 'fixtures', - 'plugins', - pluginDir - )}` + `--plugin-path=${path.resolve(__dirname, 'fixtures', 'plugins', pluginDir)}` ), `--server.xsrf.whitelist=${JSON.stringify(getAllExternalServiceSimulatorPaths())}`, ...(ssl diff --git a/x-pack/test/case_api_integration/common/fixtures/plugins/observability/kibana.json b/x-pack/test/case_api_integration/common/fixtures/plugins/observability/kibana.json new file mode 100644 index 0000000000000..b4b540fc9a821 --- /dev/null +++ b/x-pack/test/case_api_integration/common/fixtures/plugins/observability/kibana.json @@ -0,0 +1,10 @@ +{ + "id": "observabilityFixtures", + "version": "1.0.0", + "kibanaVersion": "kibana", + "configPath": ["xpack"], + "requiredPlugins": ["features"], + "optionalPlugins": ["security", "spaces"], + "server": true, + "ui": false +} diff --git a/x-pack/test/case_api_integration/common/fixtures/plugins/observability/package.json b/x-pack/test/case_api_integration/common/fixtures/plugins/observability/package.json new file mode 100644 index 0000000000000..4d199ccd1badc --- /dev/null +++ b/x-pack/test/case_api_integration/common/fixtures/plugins/observability/package.json @@ -0,0 +1,14 @@ +{ + "name": "observability-fixtures", + "version": "1.0.0", + "kibana": { + "version": "kibana", + "templateVersion": "1.0.0" + }, + "main": "target/test/plugin_api_integration/plugins/observability_fixtures", + "scripts": { + "kbn": "node ../../../../../../../scripts/kbn.js", + "build": "rm -rf './target' && ../../../../../../../node_modules/.bin/tsc" + }, + "license": "Elastic License 2.0" +} \ No newline at end of file diff --git a/x-pack/test/case_api_integration/common/fixtures/plugins/observability/server/index.ts b/x-pack/test/case_api_integration/common/fixtures/plugins/observability/server/index.ts new file mode 100644 index 0000000000000..700aee6bfd49d --- /dev/null +++ b/x-pack/test/case_api_integration/common/fixtures/plugins/observability/server/index.ts @@ -0,0 +1,10 @@ +/* + * 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 { FixturePlugin } from './plugin'; + +export const plugin = () => new FixturePlugin(); diff --git a/x-pack/test/case_api_integration/common/fixtures/plugins/observability/server/plugin.ts b/x-pack/test/case_api_integration/common/fixtures/plugins/observability/server/plugin.ts new file mode 100644 index 0000000000000..802c823202b76 --- /dev/null +++ b/x-pack/test/case_api_integration/common/fixtures/plugins/observability/server/plugin.ts @@ -0,0 +1,61 @@ +/* + * 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 { Plugin, CoreSetup } from 'kibana/server'; + +import { PluginSetupContract as FeaturesPluginSetup } from '../../../../../../../plugins/features/server'; +import { SpacesPluginStart } from '../../../../../../../plugins/spaces/server'; +import { SecurityPluginStart } from '../../../../../../../plugins/security/server'; +import { SAVED_OBJECT_TYPES as casesSavedObjectTypes } from '../../../../../../../plugins/cases/common/constants'; + +export interface FixtureSetupDeps { + features: FeaturesPluginSetup; +} + +export interface FixtureStartDeps { + security?: SecurityPluginStart; + spaces?: SpacesPluginStart; +} + +export class FixturePlugin implements Plugin { + public setup(core: CoreSetup, deps: FixtureSetupDeps) { + const { features } = deps; + features.registerKibanaFeature({ + id: 'observabilityFixture', + name: 'ObservabilityFixture', + app: ['kibana'], + category: { id: 'cases-fixtures', label: 'Cases Fixtures' }, + cases: ['observabilityFixture'], + privileges: { + all: { + app: ['kibana'], + cases: { + all: ['observabilityFixture'], + }, + savedObject: { + all: ['alert', ...casesSavedObjectTypes], + read: [], + }, + ui: [], + }, + read: { + app: ['kibana'], + cases: { + read: ['observabilityFixture'], + }, + savedObject: { + all: [], + read: [...casesSavedObjectTypes], + }, + ui: [], + }, + }, + }); + } + public start() {} + public stop() {} +} diff --git a/x-pack/test/case_api_integration/common/fixtures/plugins/security_solution/kibana.json b/x-pack/test/case_api_integration/common/fixtures/plugins/security_solution/kibana.json new file mode 100644 index 0000000000000..000848e771af3 --- /dev/null +++ b/x-pack/test/case_api_integration/common/fixtures/plugins/security_solution/kibana.json @@ -0,0 +1,10 @@ +{ + "id": "securitySolutionFixtures", + "version": "1.0.0", + "kibanaVersion": "kibana", + "configPath": ["xpack"], + "requiredPlugins": ["features"], + "optionalPlugins": ["security", "spaces"], + "server": true, + "ui": false +} diff --git a/x-pack/test/case_api_integration/common/fixtures/plugins/security_solution/package.json b/x-pack/test/case_api_integration/common/fixtures/plugins/security_solution/package.json new file mode 100644 index 0000000000000..9a852dc1f0c49 --- /dev/null +++ b/x-pack/test/case_api_integration/common/fixtures/plugins/security_solution/package.json @@ -0,0 +1,14 @@ +{ + "name": "security-solution-fixtures", + "version": "1.0.0", + "kibana": { + "version": "kibana", + "templateVersion": "1.0.0" + }, + "main": "target/test/plugin_api_integration/plugins/security_solution_fixtures", + "scripts": { + "kbn": "node ../../../../../../../scripts/kbn.js", + "build": "rm -rf './target' && ../../../../../../../node_modules/.bin/tsc" + }, + "license": "Elastic License 2.0" +} \ No newline at end of file diff --git a/x-pack/test/case_api_integration/common/fixtures/plugins/security_solution/server/index.ts b/x-pack/test/case_api_integration/common/fixtures/plugins/security_solution/server/index.ts new file mode 100644 index 0000000000000..700aee6bfd49d --- /dev/null +++ b/x-pack/test/case_api_integration/common/fixtures/plugins/security_solution/server/index.ts @@ -0,0 +1,10 @@ +/* + * 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 { FixturePlugin } from './plugin'; + +export const plugin = () => new FixturePlugin(); diff --git a/x-pack/test/case_api_integration/common/fixtures/plugins/security_solution/server/plugin.ts b/x-pack/test/case_api_integration/common/fixtures/plugins/security_solution/server/plugin.ts new file mode 100644 index 0000000000000..46432a2507cb6 --- /dev/null +++ b/x-pack/test/case_api_integration/common/fixtures/plugins/security_solution/server/plugin.ts @@ -0,0 +1,61 @@ +/* + * 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 { Plugin, CoreSetup } from 'kibana/server'; + +import { PluginSetupContract as FeaturesPluginSetup } from '../../../../../../../plugins/features/server'; +import { SpacesPluginStart } from '../../../../../../../plugins/spaces/server'; +import { SecurityPluginStart } from '../../../../../../../plugins/security/server'; +import { SAVED_OBJECT_TYPES as casesSavedObjectTypes } from '../../../../../../../plugins/cases/common/constants'; + +export interface FixtureSetupDeps { + features: FeaturesPluginSetup; +} + +export interface FixtureStartDeps { + security?: SecurityPluginStart; + spaces?: SpacesPluginStart; +} + +export class FixturePlugin implements Plugin { + public setup(core: CoreSetup, deps: FixtureSetupDeps) { + const { features } = deps; + features.registerKibanaFeature({ + id: 'securitySolutionFixture', + name: 'SecuritySolutionFixture', + app: ['kibana'], + category: { id: 'cases-fixtures', label: 'Cases Fixtures' }, + cases: ['securitySolutionFixture'], + privileges: { + all: { + app: ['kibana'], + cases: { + all: ['securitySolutionFixture'], + }, + savedObject: { + all: ['alert', ...casesSavedObjectTypes], + read: [], + }, + ui: [], + }, + read: { + app: ['kibana'], + cases: { + read: ['securitySolutionFixture'], + }, + savedObject: { + all: [], + read: [...casesSavedObjectTypes], + }, + ui: [], + }, + }, + }); + } + public start() {} + public stop() {} +} 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 e711a59229e77..cf21b01c3967e 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 @@ -35,7 +35,8 @@ export const globalRead: Role = { kibana: [ { feature: { - cases: ['read'], + securitySolutionFixture: ['read'], + observabilityFixture: ['all'], }, spaces: ['*'], }, @@ -57,7 +58,29 @@ export const securitySolutionOnlyAll: Role = { kibana: [ { feature: { - siem: ['all'], + securitySolutionFixture: ['all'], + }, + spaces: ['space1'], + }, + ], + }, +}; + +export const securitySolutionOnlyRead: Role = { + name: 'sec_only_read', + privileges: { + elasticsearch: { + indices: [ + { + names: ['*'], + privileges: ['all'], + }, + ], + }, + kibana: [ + { + feature: { + securitySolutionFixture: ['read'], }, spaces: ['space1'], }, @@ -66,7 +89,29 @@ export const securitySolutionOnlyAll: Role = { }; export const observabilityOnlyAll: Role = { - name: 'sec_only_all', + name: 'obs_only_all', + privileges: { + elasticsearch: { + indices: [ + { + names: ['*'], + privileges: ['all'], + }, + ], + }, + kibana: [ + { + feature: { + observabilityFixture: ['all'], + }, + spaces: ['space1'], + }, + ], + }, +}; + +export const observabilityOnlyRead: Role = { + name: 'obs_only_read', privileges: { elasticsearch: { indices: [ @@ -79,10 +124,7 @@ export const observabilityOnlyAll: Role = { kibana: [ { feature: { - logs: ['all'], - infrastructure: ['all'], - apm: ['all'], - uptime: ['all'], + observabilityFixture: ['read'], }, spaces: ['space1'], }, @@ -94,5 +136,7 @@ export const roles = [ noKibanaPrivileges, globalRead, securitySolutionOnlyAll, + securitySolutionOnlyRead, observabilityOnlyAll, + observabilityOnlyRead, ]; 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 43e21b79ee4b6..06add9ae00793 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 @@ -5,31 +5,78 @@ * 2.0. */ -import { securitySolutionOnlyAll, observabilityOnlyAll } from './roles'; +import { + securitySolutionOnlyAll, + observabilityOnlyAll, + securitySolutionOnlyRead, + observabilityOnlyRead, + globalRead as globalReadRole, + noKibanaPrivileges as noKibanaPrivilegesRole, +} from './roles'; import { User } from './types'; -const superUser: User = { +export const superUser: User = { username: 'superuser', password: 'superuser', roles: ['superuser'], }; -const secOnly: User = { +export const secOnly: User = { username: 'sec_only', password: 'sec_only', roles: [securitySolutionOnlyAll.name], }; -const obsOnly: User = { +export const secOnlyRead: User = { + username: 'sec_only_read', + password: 'sec_only_read', + roles: [securitySolutionOnlyRead.name], +}; + +export const obsOnly: User = { username: 'obs_only', password: 'obs_only', roles: [observabilityOnlyAll.name], }; -const obsSec: User = { +export const obsOnlyRead: User = { + username: 'obs_only_read', + password: 'obs_only_read', + roles: [observabilityOnlyRead.name], +}; + +export const obsSec: User = { username: 'obs_sec', password: 'obs_sec', roles: [securitySolutionOnlyAll.name, observabilityOnlyAll.name], }; -export const users = [superUser, secOnly, obsOnly, obsSec]; +export const obsSecRead: User = { + username: 'obs_sec_read', + password: 'obs_sec_read', + roles: [securitySolutionOnlyRead.name, observabilityOnlyRead.name], +}; + +export const globalRead: User = { + username: 'global_read', + password: 'global_read', + roles: [globalReadRole.name], +}; + +export const noKibanaPrivileges: User = { + username: 'no_kibana_privileges', + password: 'no_kibana_privileges', + roles: [noKibanaPrivilegesRole.name], +}; + +export const users = [ + superUser, + secOnly, + secOnlyRead, + obsOnly, + obsOnlyRead, + obsSec, + obsSecRead, + globalRead, + noKibanaPrivileges, +]; diff --git a/x-pack/test/case_api_integration/common/lib/mock.ts b/x-pack/test/case_api_integration/common/lib/mock.ts index 53dd6440a47df..f1f088e5c5042 100644 --- a/x-pack/test/case_api_integration/common/lib/mock.ts +++ b/x-pack/test/case_api_integration/common/lib/mock.ts @@ -44,8 +44,17 @@ export const postCaseReq: CasePostRequest = { settings: { syncAlerts: true, }, + owner: 'securitySolutionFixture', }; +/** + * Return a request for creating a case. + */ +export const getPostCaseRequest = (req?: Partial): CasePostRequest => ({ + ...postCaseReq, + ...req, +}); + /** * The fields for creating a collection style case. */ 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 f7ff49727df33..82189c9d7abe3 100644 --- a/x-pack/test/case_api_integration/common/lib/utils.ts +++ b/x-pack/test/case_api_integration/common/lib/utils.ts @@ -23,11 +23,13 @@ import { CaseStatuses, SubCasesResponse, CasesResponse, + CasesFindResponse, } from '../../../../plugins/cases/common/api'; -import { postCollectionReq, postCommentGenAlertReq } from './mock'; +import { getPostCaseRequest, postCollectionReq, postCommentGenAlertReq } from './mock'; import { getSubCasesUrl } from '../../../../plugins/cases/common/api/helpers'; import { ContextTypeGeneratedAlertType } from '../../../../plugins/cases/server/connectors'; import { SignalHit } from '../../../../plugins/security_solution/server/lib/detection_engine/signals/types'; +import { User } from './authentication/types'; function toArray(input: T | T[]): T[] { if (Array.isArray(input)) { @@ -407,3 +409,62 @@ export const deleteConfiguration = async (es: KibanaClient): Promise => { body: {}, }); }; + +export const getSpaceUrlPrefix = (spaceId: string) => { + return spaceId && spaceId !== 'default' ? `/s/${spaceId}` : ``; +}; + +export const createCaseAsUser = async ({ + supertestWithoutAuth, + user, + space, + owner, + expectedHttpCode = 200, +}: { + supertestWithoutAuth: st.SuperTest; + user: User; + space: string; + owner?: string; + expectedHttpCode?: number; +}): Promise => { + const { body: theCase } = await supertestWithoutAuth + .post(`${getSpaceUrlPrefix(space)}${CASES_URL}`) + .auth(user.username, user.password) + .set('kbn-xsrf', 'true') + .send(getPostCaseRequest({ owner })) + .expect(expectedHttpCode); + + return theCase; +}; + +export const findCasesAsUser = async ({ + supertestWithoutAuth, + user, + space, + expectedHttpCode = 200, + appendToUrl = '', +}: { + supertestWithoutAuth: st.SuperTest; + user: User; + space: string; + expectedHttpCode?: number; + appendToUrl?: string; +}): Promise => { + const { body: res } = await supertestWithoutAuth + .get(`${getSpaceUrlPrefix(space)}${CASES_URL}/_find?sortOrder=asc&${appendToUrl}`) + .auth(user.username, user.password) + .set('kbn-xsrf', 'true') + .send() + .expect(expectedHttpCode); + + return res; +}; + +export const ensureSavedObjectIsAuthorized = ( + cases: CaseResponse[], + numberOfExpectedCases: number, + owners: string[] +) => { + expect(cases.length).to.eql(numberOfExpectedCases); + cases.forEach((theCase) => expect(owners.includes(theCase.owner)).to.be(true)); +}; diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/find_cases.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/find_cases.ts index f889887d40381..195ada335e086 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/find_cases.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/find_cases.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; import supertestAsPromised from 'supertest-as-promised'; import type { ApiResponse, estypes } from '@elastic/elasticsearch'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { CASES_URL, @@ -22,12 +22,26 @@ import { CreateSubCaseResp, createCaseAction, deleteCaseAction, + createCaseAsUser, + ensureSavedObjectIsAuthorized, + findCasesAsUser, } from '../../../../common/lib/utils'; import { CasesFindResponse, CaseStatuses, CaseType, } from '../../../../../../plugins/cases/common/api'; +import { + obsOnly, + secOnly, + obsOnlyRead, + secOnlyRead, + noKibanaPrivileges, + superUser, + globalRead, + obsSecRead, + obsSec, +} from '../../../../common/lib/authentication/users'; interface CaseAttributes { cases: { @@ -39,6 +53,8 @@ interface CaseAttributes { export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); const es = getService('es'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); + describe('find_cases', () => { describe('basic tests', () => { afterEach(async () => { @@ -670,5 +686,201 @@ export default ({ getService }: FtrProviderContext): void => { expect(body.count_in_progress_cases).to.eql(0); }); }); + + describe('rbac', () => { + afterEach(async () => { + await deleteAllCaseItems(es); + }); + + it('should return the correct cases', async () => { + await Promise.all([ + // Create case owned by the security solution user + await createCaseAsUser({ + supertestWithoutAuth, + user: secOnly, + space: 'space1', + owner: 'securitySolutionFixture', + }), + // Create case owned by the observability user + await createCaseAsUser({ + supertestWithoutAuth, + user: obsOnly, + space: 'space1', + owner: 'observabilityFixture', + }), + ]); + + for (const scenario of [ + { + user: globalRead, + numberOfExpectedCases: 2, + owners: ['securitySolutionFixture', 'observabilityFixture'], + }, + { + user: superUser, + numberOfExpectedCases: 2, + owners: ['securitySolutionFixture', 'observabilityFixture'], + }, + { user: secOnlyRead, numberOfExpectedCases: 1, owners: ['securitySolutionFixture'] }, + { user: obsOnlyRead, numberOfExpectedCases: 1, owners: ['observabilityFixture'] }, + { + user: obsSecRead, + numberOfExpectedCases: 2, + owners: ['securitySolutionFixture', 'observabilityFixture'], + }, + ]) { + const res = await findCasesAsUser({ + supertestWithoutAuth, + user: scenario.user, + space: 'space1', + }); + + ensureSavedObjectIsAuthorized(res.cases, scenario.numberOfExpectedCases, scenario.owners); + } + }); + + for (const scenario of [ + { user: noKibanaPrivileges, space: 'space1' }, + { user: secOnly, space: 'space2' }, + ]) { + it(`User ${scenario.user.username} with role(s) ${scenario.user.roles.join()} and space ${ + scenario.space + } - should NOT read a case`, async () => { + // super user creates a case at the appropriate space + await createCaseAsUser({ + supertestWithoutAuth, + user: superUser, + space: scenario.space, + owner: 'securitySolutionFixture', + }); + + // user should not be able to read cases at the appropriate space + await findCasesAsUser({ + supertestWithoutAuth, + user: scenario.user, + space: scenario.space, + expectedHttpCode: 403, + }); + }); + } + + it('should return the correct cases when trying to exploit RBAC through the search query parameter', async () => { + await Promise.all([ + // super user creates a case with owner securitySolutionFixture + await createCaseAsUser({ + supertestWithoutAuth, + user: superUser, + space: 'space1', + owner: 'securitySolutionFixture', + }), + // super user creates a case with owner observabilityFixture + await createCaseAsUser({ + supertestWithoutAuth, + user: superUser, + space: 'space1', + owner: 'observabilityFixture', + }), + ]); + + const res = await findCasesAsUser({ + supertestWithoutAuth, + user: secOnly, + space: 'space1', + appendToUrl: 'search=securitySolutionFixture+observabilityFixture&searchFields=owner', + }); + + 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([ + await createCaseAsUser({ + supertestWithoutAuth, + user: obsSec, + space: 'space1', + owner: 'securitySolutionFixture', + }), + await createCaseAsUser({ + supertestWithoutAuth, + user: obsSec, + space: 'space1', + owner: 'observabilityFixture', + }), + ]); + + const res = await findCasesAsUser({ + supertestWithoutAuth, + user: obsSec, + space: 'space1', + appendToUrl: 'owner=securitySolutionFixture', + }); + + 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([ + await createCaseAsUser({ + supertestWithoutAuth, + user: obsSec, + space: 'space1', + owner: 'securitySolutionFixture', + }), + await createCaseAsUser({ + supertestWithoutAuth, + user: obsSec, + space: 'space1', + owner: 'observabilityFixture', + }), + ]); + + // User with permissions only to security solution request cases from observability + const res = await findCasesAsUser({ + supertestWithoutAuth, + user: secOnly, + space: 'space1', + appendToUrl: 'owner=securitySolutionFixture&owner=observabilityFixture', + }); + + // Only security solution cases are being returned + ensureSavedObjectIsAuthorized(res.cases, 1, ['securitySolutionFixture']); + }); + }); }); }; diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/post_case.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/post_case.ts index afcc36d041c11..2249587620d5f 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/post_case.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/post_case.ts @@ -6,20 +6,33 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; import { CASES_URL } from '../../../../../../plugins/cases/common/constants'; import { - postCaseReq, + ConnectorTypes, + ConnectorJiraTypeFields, +} from '../../../../../../plugins/cases/common/api'; +import { + getPostCaseRequest, postCaseResp, removeServerGeneratedPropertiesFromCase, } from '../../../../common/lib/mock'; -import { deleteCases } from '../../../../common/lib/utils'; +import { createCaseAsUser, deleteCases } from '../../../../common/lib/utils'; +import { + secOnly, + secOnlyRead, + globalRead, + obsOnlyRead, + obsSecRead, + noKibanaPrivileges, +} from '../../../../common/lib/authentication/users'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); const es = getService('es'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); describe('post_case', () => { afterEach(async () => { @@ -30,7 +43,7 @@ export default ({ getService }: FtrProviderContext): void => { const { body: postedCase } = await supertest .post(CASES_URL) .set('kbn-xsrf', 'true') - .send(postCaseReq) + .send(getPostCaseRequest()) .expect(200); const data = removeServerGeneratedPropertiesFromCase(postedCase); @@ -41,12 +54,13 @@ export default ({ getService }: FtrProviderContext): void => { await supertest .post(CASES_URL) .set('kbn-xsrf', 'true') - .send({ ...postCaseReq, badKey: true }) + // @ts-expect-error + .send({ ...getPostCaseRequest({ badKey: true }) }) .expect(400); }); it('unhappy path - 400s when connector is not supplied', async () => { - const { connector, ...caseWithoutConnector } = postCaseReq; + const { connector, ...caseWithoutConnector } = getPostCaseRequest(); await supertest .post(CASES_URL) @@ -60,8 +74,10 @@ export default ({ getService }: FtrProviderContext): void => { .post(CASES_URL) .set('kbn-xsrf', 'true') .send({ - ...postCaseReq, - connector: { id: 'wrong', name: 'wrong', type: '.not-exists', fields: null }, + ...getPostCaseRequest({ + // @ts-expect-error + connector: { id: 'wrong', name: 'wrong', type: '.not-exists', fields: null }, + }), }) .expect(400); }); @@ -71,15 +87,63 @@ export default ({ getService }: FtrProviderContext): void => { .post(CASES_URL) .set('kbn-xsrf', 'true') .send({ - ...postCaseReq, - connector: { - id: 'wrong', - name: 'wrong', - type: '.jira', - fields: { unsupported: 'value' }, - }, + ...getPostCaseRequest({ + // @ts-expect-error + connector: { + id: 'wrong', + name: 'wrong', + type: ConnectorTypes.jira, + fields: { unsupported: 'value' }, + } as ConnectorJiraTypeFields, + }), }) .expect(400); }); + + describe('rbac', () => { + it('User: security solution only - should create a case', async () => { + const theCase = await createCaseAsUser({ + supertestWithoutAuth, + user: secOnly, + space: 'space1', + owner: 'securitySolutionFixture', + }); + expect(theCase.owner).to.eql('securitySolutionFixture'); + }); + + it('User: security solution only - should NOT create a case of different owner', async () => { + await createCaseAsUser({ + supertestWithoutAuth, + user: secOnly, + space: 'space1', + owner: 'observabilityFixture', + expectedHttpCode: 403, + }); + }); + + for (const user of [globalRead, secOnlyRead, obsOnlyRead, obsSecRead, noKibanaPrivileges]) { + it(`User ${ + user.username + } with role(s) ${user.roles.join()} - should NOT create a case`, async () => { + await createCaseAsUser({ + supertestWithoutAuth, + user, + space: 'space1', + owner: 'securitySolutionFixture', + expectedHttpCode: 403, + }); + }); + } + + it('should NOT create a case in a space with no permissions', async () => { + await createCaseAsUser({ + supertestWithoutAuth, + user: secOnly, + space: 'space2', + owner: 'securitySolutionFixture', + expectedHttpCode: 403, + }); + }); + }); }); }; diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/sub_cases/find_sub_cases.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/sub_cases/find_sub_cases.ts index 43a0d6bf6203b..466eca95b0d72 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/sub_cases/find_sub_cases.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/common/cases/sub_cases/find_sub_cases.ts @@ -7,7 +7,7 @@ import expect from '@kbn/expect'; import type { ApiResponse, estypes } from '@elastic/elasticsearch'; -import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; import { findSubCasesResp, postCollectionReq } from '../../../../../common/lib/mock'; import {